项目初始化
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7499f48
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,52 @@
+HELP.md
+flyway.conf
+.gradle
+.*
+/build/
+!gradle/wrapper/gradle-wrapper.jar
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+.DS_Store
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+/out/
+/**/out/
+/**/build/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
+### Gradle template
+.gradle
+/build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Cache of project
+.gradletasknamecache
+
+# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
+# gradle/wrapper/gradle-wrapper.properties
+
+/log
\ No newline at end of file
diff --git a/backend/build.gradle b/backend/build.gradle
new file mode 100644
index 0000000..f922424
--- /dev/null
+++ b/backend/build.gradle
@@ -0,0 +1,74 @@
+plugins {
+    id 'java'
+    id 'org.springframework.boot'
+    id 'org.jetbrains.kotlin.jvm'
+    id 'org.jetbrains.kotlin.plugin.spring'
+    id 'org.jetbrains.kotlin.plugin.jpa' version '1.3.31'
+    id 'io.spring.dependency-management'
+}
+
+group 'com.supwisdom.dlpay'
+version '1.0-SNAPSHOT'
+
+sourceCompatibility = 1.8
+
+repositories {
+    mavenCentral()
+}
+
+ext {
+    set('springCloudVersion', "Greenwich.SR2")
+}
+
+dependencies {
+    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+    implementation 'org.springframework.boot:spring-boot-starter-web'
+    implementation 'org.springframework.boot:spring-boot-starter-security'
+    implementation 'org.springframework.boot:spring-boot-starter-cache'
+    implementation 'org.springframework.boot:spring-boot-autoconfigure'
+    implementation 'org.springframework.security:spring-security-oauth2-jose'
+    implementation 'org.springframework.security:spring-security-oauth2-client'
+    implementation 'org.springframework.security:spring-security-oauth2-jose'
+    implementation 'org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.1.5.RELEASE'
+    implementation 'org.springframework.session:spring-session-data-redis'
+    implementation 'org.springframework.boot:spring-boot-starter-cache'
+    implementation 'org.springframework.kafka:spring-kafka'
+    implementation 'org.springframework.social:spring-social-web:1.1.6.RELEASE'
+    implementation 'org.springframework.kafka:spring-kafka:2.2.8.RELEASE'
+    implementation 'org.jetbrains.kotlin:kotlin-reflect'
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
+    implementation 'net.javacrumbs.shedlock:shedlock-spring:2.5.0'
+    implementation 'net.javacrumbs.shedlock:shedlock-provider-redis-spring:2.5.0'
+
+    implementation 'org.postgresql:postgresql:42.2.5'
+
+    implementation 'org.springframework.cloud:spring-cloud-starter'
+    implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery'
+    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-hystrix'
+    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-hystrix-dashboard'
+
+    implementation 'org.bitbucket.b_c:jose4j:0.6.5'
+    implementation 'io.github.microutils:kotlin-logging:1.6.26'
+    implementation files('libs/masmgc.sdk.sms-0.0.1-SNAPSHOT.jar')
+    implementation 'commons-beanutils:commons-beanutils:1.9.3'
+
+    implementation 'com.alibaba:fastjson:1.2.60'
+
+    implementation 'com.github.penggle:kaptcha:2.3.2'
+
+    testCompile group: 'junit', name: 'junit', version: '4.12'
+}
+
+dependencyManagement {
+    imports {
+        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
+    }
+}
+
+compileKotlin {
+    kotlinOptions.jvmTarget = "1.8"
+}
+compileTestKotlin {
+    kotlinOptions.jvmTarget = "1.8"
+}
\ No newline at end of file
diff --git a/backend/libs/masmgc.sdk.sms-0.0.1-SNAPSHOT.jar b/backend/libs/masmgc.sdk.sms-0.0.1-SNAPSHOT.jar
new file mode 100644
index 0000000..a20d702
--- /dev/null
+++ b/backend/libs/masmgc.sdk.sms-0.0.1-SNAPSHOT.jar
Binary files differ
diff --git a/backend/src/main/java/com/supwisdom/dlpay/agent/AgentCode.java b/backend/src/main/java/com/supwisdom/dlpay/agent/AgentCode.java
new file mode 100644
index 0000000..0d56cc1
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/agent/AgentCode.java
@@ -0,0 +1,35 @@
+package com.supwisdom.dlpay.agent;
+
+public enum AgentCode {
+  SUCCESS("success", "成功"),
+  FAIL("fail", "交易失败"),
+  REFNO_NOT_EXISTS("refno_not_exists", "指定流水不存在"),
+  ILLEGAL_DATA("illegal_data", "数据不合法,不能交易"),
+  SERVER_INTERNAL_ERROR("server_internal_error", "交易系统错误"),
+  REQUIRE_QUERY("require_query", "交易未完成,请查询"),
+  WAIT_NOTIFY("wait_notify", "交易已发起,等待通知"),
+  SHORT_OF_BALANCE("short_of_balance", "余额不足"),
+  NOT_SUPPORT("not_support", "不支持功能"),
+  COMMON_ERROR("common_error", "其它错误"),
+  CONFIG_ERROR("config_error", "参数配置错误");
+  AgentCode(String code, String msg) {
+    this.code = code;
+    this.msg = msg;
+  }
+
+  public String value() {
+    return this.code;
+  }
+
+  public String message() {
+    return this.msg;
+  }
+
+  private String code;
+  private String msg;
+
+  @Override
+  public String toString() {
+    return code;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/agent/AgentPayService.java b/backend/src/main/java/com/supwisdom/dlpay/agent/AgentPayService.java
new file mode 100644
index 0000000..eb3c5f6
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/agent/AgentPayService.java
@@ -0,0 +1,7 @@
+package com.supwisdom.dlpay.agent;
+
+import com.supwisdom.dlpay.api.domain.TTransactionMain;
+
+public interface AgentPayService<T> {
+  AgentResponse<T> query(TTransactionMain transaction);
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/agent/AgentResponse.java b/backend/src/main/java/com/supwisdom/dlpay/agent/AgentResponse.java
new file mode 100644
index 0000000..149c3ae
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/agent/AgentResponse.java
@@ -0,0 +1,75 @@
+package com.supwisdom.dlpay.agent;
+
+public class AgentResponse<T> {
+  public static final String AGENTCODE_SUCCESS = "0";
+
+  public static final String AGENTCODE_FAIL = "1";
+
+  private AgentCode code;
+  private String agentCode;
+  private String agentMsg;
+  private String agentBody;
+  private String agentRefno;
+  private DtlStatus dtlStatus;
+
+  private T payload;
+
+  public String getAgentBody() {
+    return agentBody;
+  }
+
+  public void setAgentBody(String agentBody) {
+    this.agentBody = agentBody;
+  }
+
+  public AgentCode getCode() {
+    return code;
+  }
+
+  public void setCode(AgentCode code) {
+    this.code = code;
+  }
+
+  public String getAgentCode() {
+    return agentCode;
+  }
+
+  public void setAgentCode(String agentCode) {
+    this.agentCode = agentCode;
+  }
+
+  public String getAgentMsg() {
+    return agentMsg;
+  }
+
+  public void setAgentMsg(String agentMsg) {
+    this.agentMsg = agentMsg;
+  }
+
+  public String getAgentRefno() {
+    return agentRefno;
+  }
+
+  public void setAgentRefno(String agentRefno) {
+    this.agentRefno = agentRefno;
+  }
+
+  public DtlStatus getDtlStatus() {
+    return dtlStatus;
+  }
+
+  public void setDtlStatus(DtlStatus dtlStatus) {
+    this.dtlStatus = dtlStatus;
+  }
+
+  public T getPayload() {
+    return payload;
+  }
+
+  public void setPayload(T payload) {
+    this.payload = payload;
+  }
+
+  public AgentResponse() {
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/agent/DtlStatus.java b/backend/src/main/java/com/supwisdom/dlpay/agent/DtlStatus.java
new file mode 100644
index 0000000..a13f1d5
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/agent/DtlStatus.java
@@ -0,0 +1,9 @@
+package com.supwisdom.dlpay.agent;
+
+public enum DtlStatus {
+  SUCCESS, //成功
+  FAIL,  //失败
+  REFUND, //已退款
+  PARTIAL_REFUND, //部分退款
+  WAIT //等待付款
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/agent/citizencard/DlpayResp.java b/backend/src/main/java/com/supwisdom/dlpay/agent/citizencard/DlpayResp.java
new file mode 100644
index 0000000..4e90b51
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/agent/citizencard/DlpayResp.java
@@ -0,0 +1,103 @@
+package com.supwisdom.dlpay.agent.citizencard;
+
+public class DlpayResp {
+  private String code;
+  private String message;
+  private String transdate;
+  private String transtime;
+  private String bankjourno; //银行流水号
+  private String sinstatus; //1-已签约 2-未签约
+  private String captcha; //验证码
+  private String status; //0-成功;1-失败;2-已退款;3-部分退款
+  private String filename;
+  private String trxdate; //银行记账日期
+  private Integer amount; //查询时返回流水金额
+
+  public String getCode() {
+    return code;
+  }
+
+  public void setCode(String code) {
+    this.code = code;
+  }
+
+  public String getMessage() {
+    return message;
+  }
+
+  public void setMessage(String message) {
+    this.message = message;
+  }
+
+  public String getTransdate() {
+    return transdate;
+  }
+
+  public void setTransdate(String transdate) {
+    this.transdate = transdate;
+  }
+
+  public String getTranstime() {
+    return transtime;
+  }
+
+  public void setTranstime(String transtime) {
+    this.transtime = transtime;
+  }
+
+  public String getBankjourno() {
+    return bankjourno;
+  }
+
+  public void setBankjourno(String bankjourno) {
+    this.bankjourno = bankjourno;
+  }
+
+  public String getSinstatus() {
+    return sinstatus;
+  }
+
+  public void setSinstatus(String sinstatus) {
+    this.sinstatus = sinstatus;
+  }
+
+  public String getCaptcha() {
+    return captcha;
+  }
+
+  public void setCaptcha(String captcha) {
+    this.captcha = captcha;
+  }
+
+  public String getStatus() {
+    return status;
+  }
+
+  public void setStatus(String status) {
+    this.status = status;
+  }
+
+  public String getFilename() {
+    return filename;
+  }
+
+  public void setFilename(String filename) {
+    this.filename = filename;
+  }
+
+  public String getTrxdate() {
+    return trxdate;
+  }
+
+  public void setTrxdate(String trxdate) {
+    this.trxdate = trxdate;
+  }
+
+  public Integer getAmount() {
+    return amount;
+  }
+
+  public void setAmount(Integer amount) {
+    this.amount = amount;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/agent/citizencard/YnrccRespCode.java b/backend/src/main/java/com/supwisdom/dlpay/agent/citizencard/YnrccRespCode.java
new file mode 100644
index 0000000..6fdd230
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/agent/citizencard/YnrccRespCode.java
@@ -0,0 +1,27 @@
+package com.supwisdom.dlpay.agent.citizencard;
+
+public class YnrccRespCode {
+  private String code;
+  private String msg;
+
+  public YnrccRespCode(String code, String msg) {
+    this.code = code;
+    this.msg = msg;
+  }
+
+  public String getCode() {
+    return code;
+  }
+
+  public void setCode(String code) {
+    this.code = code;
+  }
+
+  public String getMsg() {
+    return msg;
+  }
+
+  public void setMsg(String msg) {
+    this.msg = msg;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/agent/citizencard/YnrccUtil.java b/backend/src/main/java/com/supwisdom/dlpay/agent/citizencard/YnrccUtil.java
new file mode 100644
index 0000000..79daf31
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/agent/citizencard/YnrccUtil.java
@@ -0,0 +1,100 @@
+package com.supwisdom.dlpay.agent.citizencard;
+
+import com.supwisdom.dlpay.agent.AgentCode;
+import org.springframework.data.util.Pair;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class YnrccUtil {
+  public static final String BANKCARD_BIND_TRANSCODE = "BC5510";
+  public static final String BANKCARD_SIGN_TRANSCODE = "BC5511";
+  public static final String BANKCARD_PAY_TRANSCODE = "BC5512";
+  public static final String BANKCARD_PAYREFUND_TRANSCODE = "BC5513";
+  public static final String BANKCARD_QUERYRESULT_TRANSCODE = "BC5514";
+  public static final String BANKCARD_CHKFILE_TRANSCODE = "BC5515";
+  public static final String BANKCARD_CHKMAKESURE_TRANSCODE = "BC5516";
+  public static final String BANKCARD_BANKCARDLOSS_TRANSCODE = "BC5517";
+
+  public static final String DLPAY_CATEGORIE = "C001"; //消费类别
+
+  public static final String YNRCC_ANGENT_URL = "ynrcc.agent.url"; //农商行前置url
+  public static final String YNRCC_SIGNKEY = "ynrcc.agent.signkey"; //农商行前置md5key
+  public static final String YNRCC_MERCHANT_BANKCARDNO = "merchant.bankcardno";
+  public static final String YNRCC_MERCHANT_BANKACCNAME = "merchant.bankaccname";
+
+  public static final String PARAM_CONFIG_ERROR = "90000"; // 参数未配置
+  public static final String PARAM_VALUE_ERROR = "90001"; // 参数值错误
+
+  public static final int AGENT_CONNECT_TIMEOUT = 10;
+  public static final int AGENT_READ_TIMEOUT = 10;
+  public static final String TRANSTYPE_SIGNCARD = "1"; //签约
+  public static final String TRANSTYPE_UNSIGNCARD = "2"; //解约
+
+  public static final String CODE_SUCCESS = "0000"; //成功
+  public static final String CODE_NOT_EXISTS = "0401"; //流水不存在
+  public static final String CODE_EXCEPTION = "10000"; //异常
+  public static final String NO_RECORDS_TODAY = "0406"; //当日无交易明细
+
+  //查询接口返回的流水状态
+  public static final String DTL_STATUS_SUCCESS = "0"; //成功
+  public static final String DTL_STATUS_FAIL = "1"; //失败
+  public static final String DTL_STATUS_REFUND = "2"; //已退款
+  public static final String DTL_STATUS_PART_REFUND = "3"; //部分退款
+
+  public static final int QUERY_MAX_COUNT = 3; //最大查询次数
+
+  public static final String YNRCC_BILLS_DOWNLOAD_LASTDATE = "ynrcc.download.bills.lastdate";
+
+  public static final List<Pair<AgentCode, YnrccRespCode>> errcode = new ArrayList<>(0);
+
+  static {
+    errcode.add(Pair.of(AgentCode.SUCCESS, new YnrccRespCode("0000", "成功")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0101", "手机号不一致")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0102", "无预留手机号")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0103", "证件类型不一致")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0104", "证件号码不一致")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0105", "户名不符")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0106", "卡状态异常")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0107", "无此卡号")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0108", "已存在签约信息,与签约证件号码不符")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0109", "已存在签约信息,与签约证件类型不符")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0110", "已存在签约信息,与签约户名不符")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0111", "已存在签约信息,与签约手机号不符")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0112", "卡已挂失")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0113", "卡已密码挂失")));
+
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0201", "未进行银行卡绑定")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0202", "无效的交易类型")));
+    
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0301", "未签约")));
+    errcode.add(Pair.of(AgentCode.SHORT_OF_BALANCE, new YnrccRespCode("0302", "余额不足")));
+    errcode.add(Pair.of(AgentCode.SERVER_INTERNAL_ERROR, new YnrccRespCode("0303", "MD5校验失败")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0304", "该卡号已经解约,不允许重复解约")));
+
+    errcode.add(Pair.of(AgentCode.REFNO_NOT_EXISTS, new YnrccRespCode("0401", "原始流水不存在")));
+    errcode.add(Pair.of(AgentCode.FAIL, new YnrccRespCode("0402", "原始流水未成功")));
+    errcode.add(Pair.of(AgentCode.FAIL, new YnrccRespCode("0403", "原始流水已退款")));
+    errcode.add(Pair.of(AgentCode.SHORT_OF_BALANCE, new YnrccRespCode("0404", "商户账户余额不足")));
+    errcode.add(Pair.of(AgentCode.FAIL, new YnrccRespCode("0405", "不在退款时间范围内")));
+    errcode.add(Pair.of(AgentCode.SUCCESS, new YnrccRespCode("0406", "当日无交易明细")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0407", "非大理市民卡,不允许交易")));
+    errcode.add(Pair.of(AgentCode.FAIL, new YnrccRespCode("0408", "同一请求流水号不允许重复提交")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0409", "商户卡号为空")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0410", "商户户名错")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0411", "退款金额大于原代扣金额")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0412", "退款金额大于剩余可退金额")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0413", "不允许该类型的商户账户交易")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0414", "退款时请输入原应用系统唯一流水号")));
+    errcode.add(Pair.of(AgentCode.ILLEGAL_DATA, new YnrccRespCode("0415", "交易金额大于单笔限额")));
+    errcode.add(Pair.of(AgentCode.SERVER_INTERNAL_ERROR, new YnrccRespCode("0416", "通用限额未配置")));
+
+    errcode.add(Pair.of(AgentCode.COMMON_ERROR, new YnrccRespCode("4444", "其他错误[{message}]")));
+    errcode.add(Pair.of(AgentCode.REQUIRE_QUERY, new YnrccRespCode(CODE_EXCEPTION, "待查询")));
+    errcode.add(Pair.of(AgentCode.SERVER_INTERNAL_ERROR, new YnrccRespCode(PARAM_CONFIG_ERROR, "{message}")));
+    errcode.add(Pair.of(AgentCode.SERVER_INTERNAL_ERROR, new YnrccRespCode(PARAM_VALUE_ERROR, "{message}")));
+  }
+
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/annotation/IDNoCheck.java b/backend/src/main/java/com/supwisdom/dlpay/api/annotation/IDNoCheck.java
new file mode 100644
index 0000000..69f8007
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/annotation/IDNoCheck.java
@@ -0,0 +1,37 @@
+package com.supwisdom.dlpay.api.annotation;
+
+import com.supwisdom.dlpay.api.types.IDTypes;
+import com.supwisdom.dlpay.api.validator.IDNoCheckValidator;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Constraint(validatedBy = IDNoCheckValidator.class)
+@Documented
+public @interface IDNoCheck {
+  String message() default "{constraintdescriptor.fieldmatch}";
+
+  Class<?>[] groups() default {};
+
+  Class<? extends Payload>[] payload() default {};
+
+  /**
+   * @return The idno field
+   */
+  String idno();
+
+  /**
+   * @return The idtype field
+   */
+  String idtype();
+
+  /**
+   * 证件类型字典
+   *
+   * @return
+   */
+  IDTypes value() default IDTypes.IDTYPE_LIST;
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/annotation/MobileNumber.java b/backend/src/main/java/com/supwisdom/dlpay/api/annotation/MobileNumber.java
new file mode 100644
index 0000000..8558151
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/annotation/MobileNumber.java
@@ -0,0 +1,19 @@
+package com.supwisdom.dlpay.api.annotation;
+
+import com.supwisdom.dlpay.api.validator.MobileNumberValidator;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+@Constraint(validatedBy = {MobileNumberValidator.class})
+@Documented
+@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface MobileNumber {
+  String message() default "手机号码不合法";
+
+  Class<?>[] groups() default {};
+
+  Class<? extends Payload>[] payload() default {};
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/bean/ApiResponse.java b/backend/src/main/java/com/supwisdom/dlpay/api/bean/ApiResponse.java
new file mode 100644
index 0000000..e4cbca1
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/bean/ApiResponse.java
@@ -0,0 +1,31 @@
+package com.supwisdom.dlpay.api.bean;
+
+public class ApiResponse {
+  private Integer retcode;
+  private String retmsg;
+  private String exception;
+
+  public Integer getRetcode() {
+    return retcode;
+  }
+
+  public void setRetcode(Integer retcode) {
+    this.retcode = retcode;
+  }
+
+  public String getRetmsg() {
+    return retmsg;
+  }
+
+  public void setRetmsg(String retmsg) {
+    this.retmsg = retmsg;
+  }
+
+  public String getException() {
+    return exception;
+  }
+
+  public void setException(String exception) {
+    this.exception = exception;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/bean/BaseResp.java b/backend/src/main/java/com/supwisdom/dlpay/api/bean/BaseResp.java
new file mode 100644
index 0000000..afe4165
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/bean/BaseResp.java
@@ -0,0 +1,38 @@
+package com.supwisdom.dlpay.api.bean;
+
+import com.supwisdom.dlpay.util.Code;
+
+public class BaseResp {
+    private String retcode;
+    private String retmsg;
+    private Object data;
+
+    public String getRetcode() {
+        return retcode;
+    }
+
+    public void setRetcode(String retcode) {
+        this.retcode = retcode;
+    }
+
+    public String getRetmsg() {
+        return retmsg;
+    }
+
+    public void setRetmsg(String retmsg) {
+        this.retmsg = retmsg;
+    }
+
+    public Object getData() {
+        return data;
+    }
+
+    public void setData(Object data) {
+        this.data = data;
+    }
+
+    public void setCode(Code code) {
+        this.retcode = code.getCode();
+        this.retmsg = code.getRetmsg();
+    }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/bean/JsonResult.java b/backend/src/main/java/com/supwisdom/dlpay/api/bean/JsonResult.java
new file mode 100644
index 0000000..dafde31
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/bean/JsonResult.java
@@ -0,0 +1,78 @@
+package com.supwisdom.dlpay.api.bean;
+
+import java.util.HashMap;
+
+public class JsonResult extends HashMap<String, Object> {
+    public JsonResult() {
+    }
+
+    /**
+     * 返回成功
+     */
+    public static JsonResult ok() {
+        return ok("操作成功");
+    }
+
+    /**
+     * 返回成功
+     */
+    public static JsonResult ok(String message) {
+        return ok(200, message);
+    }
+
+    /**
+     * 返回成功
+     */
+    public static JsonResult ok(int code, String message) {
+        JsonResult jsonResult = new JsonResult();
+        jsonResult.put("code", code);
+        jsonResult.put("msg", message);
+        return jsonResult;
+    }
+
+    /**
+     * 返回失败
+     */
+    public static JsonResult error() {
+        return error("操作失败");
+    }
+
+    /**
+     * 返回失败
+     */
+    public static JsonResult error(String messag) {
+        return error(500, messag);
+    }
+
+    /**
+     * 返回失败
+     */
+    public static JsonResult error(int code, String message) {
+        return ok(code, message);
+    }
+
+    /**
+     * 设置code
+     */
+    public JsonResult setCode(int code) {
+        super.put("code", code);
+        return this;
+    }
+
+    /**
+     * 设置message
+     */
+    public JsonResult setMessage(String message) {
+        super.put("msg", message);
+        return this;
+    }
+
+    /**
+     * 放入object
+     */
+    @Override
+    public JsonResult put(String key, Object object) {
+        super.put(key, object);
+        return this;
+    }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/dao/AccountDao.java b/backend/src/main/java/com/supwisdom/dlpay/api/dao/AccountDao.java
new file mode 100644
index 0000000..f3320fb
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/dao/AccountDao.java
@@ -0,0 +1,10 @@
+package com.supwisdom.dlpay.api.dao;
+
+import com.supwisdom.dlpay.api.domain.TAccount;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+public interface AccountDao extends JpaRepository<TAccount, String> {
+  @Query("select a from TAccount a where a.userid = ?1 and a.subjno=?2")
+  TAccount findByUseridAndSubjno(String userid, String subjno);
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/dao/CardDao.java b/backend/src/main/java/com/supwisdom/dlpay/api/dao/CardDao.java
new file mode 100644
index 0000000..c9d24d3
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/dao/CardDao.java
@@ -0,0 +1,18 @@
+package com.supwisdom.dlpay.api.dao;
+
+import com.supwisdom.dlpay.api.domain.TCard;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+public interface CardDao extends JpaRepository<TCard, String> {
+
+  @Query("from TCard t where t.cardno=?1 and t.cardtype=?2")
+  TCard findCardByCardnoAndCardtype(String cardno, String cardtype);
+
+  @Query("from TCard t where t.userid=?1 and t.cardtype=?2 and t.status='normal' ")
+  TCard findCardByUseridAndCardtype(String userid, String cardtype);
+
+  @Query("from TCard t where t.userid=?1 and t.cardtype=?2 and t.cardphyid=?3 ")
+  TCard findBankcardByCitizencard(String userid, String cardtype, String cardphyid);
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/dao/PersonDao.java b/backend/src/main/java/com/supwisdom/dlpay/api/dao/PersonDao.java
new file mode 100644
index 0000000..cf9fd26
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/dao/PersonDao.java
@@ -0,0 +1,15 @@
+package com.supwisdom.dlpay.api.dao;
+
+import com.supwisdom.dlpay.api.domain.TPerson;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+
+/**
+ * Created by shuwei on 2019/4/12.
+ */
+@Repository
+public interface PersonDao extends JpaRepository<TPerson, String> {
+
+    TPerson findByUserid(String userid);
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/dao/PersondtlDao.java b/backend/src/main/java/com/supwisdom/dlpay/api/dao/PersondtlDao.java
new file mode 100644
index 0000000..63e97ec
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/dao/PersondtlDao.java
@@ -0,0 +1,13 @@
+package com.supwisdom.dlpay.api.dao;
+
+import com.supwisdom.dlpay.api.domain.TPersondtl;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface PersondtlDao extends JpaRepository<TPersondtl, String>,JpaSpecificationExecutor<TPersondtl> {
+    Page<TPersondtl> findByUseridAndStatus(String userid, String status, Pageable pageable);
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/dao/PointsAccountDao.java b/backend/src/main/java/com/supwisdom/dlpay/api/dao/PointsAccountDao.java
new file mode 100644
index 0000000..aa57a2f
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/dao/PointsAccountDao.java
@@ -0,0 +1,15 @@
+package com.supwisdom.dlpay.api.dao;
+
+import com.supwisdom.dlpay.api.domain.TPointsAccount;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface PointsAccountDao extends JpaRepository<TPointsAccount, String> {
+  TPointsAccount findByUserid(String userid);
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/dao/ShopSourceTypeConfigDao.java b/backend/src/main/java/com/supwisdom/dlpay/api/dao/ShopSourceTypeConfigDao.java
new file mode 100644
index 0000000..0b99a69
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/dao/ShopSourceTypeConfigDao.java
@@ -0,0 +1,14 @@
+package com.supwisdom.dlpay.api.dao;
+
+import com.supwisdom.dlpay.api.domain.TShopSourceTypeConfig;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface ShopSourceTypeConfigDao extends JpaRepository<TShopSourceTypeConfig, String> {
+  @Query("select a from TShopSourceTypeConfig a where a.sourceType=?1 and a.shopaccno=?2 ")
+  List<TShopSourceTypeConfig> getShopSourceTypeConfigs(String paytype, String shopaccno);
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/dao/ShopSourceTypeDao.java b/backend/src/main/java/com/supwisdom/dlpay/api/dao/ShopSourceTypeDao.java
new file mode 100644
index 0000000..8099292
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/dao/ShopSourceTypeDao.java
@@ -0,0 +1,14 @@
+package com.supwisdom.dlpay.api.dao;
+
+import com.supwisdom.dlpay.api.domain.TShopSourceType;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface ShopSourceTypeDao extends JpaRepository<TShopSourceType, String>, JpaSpecificationExecutor<TShopSourceType> {
+  @Query("select a from TShopSourceType a where a.sourceType=?1 and a.shopaccno=?2 ")
+  TShopSourceType getById(String sourceType, String shopaccno);
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/dao/SourceTypeCheckDao.java b/backend/src/main/java/com/supwisdom/dlpay/api/dao/SourceTypeCheckDao.java
new file mode 100644
index 0000000..7e7345b
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/dao/SourceTypeCheckDao.java
@@ -0,0 +1,10 @@
+package com.supwisdom.dlpay.api.dao;
+
+import com.supwisdom.dlpay.api.domain.TSourceTypeCheckStatus;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface SourceTypeCheckDao extends CrudRepository<TSourceTypeCheckStatus, Integer> {
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/dao/SourceTypeConfigDao.java b/backend/src/main/java/com/supwisdom/dlpay/api/dao/SourceTypeConfigDao.java
new file mode 100644
index 0000000..bfa3bfa
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/dao/SourceTypeConfigDao.java
@@ -0,0 +1,16 @@
+package com.supwisdom.dlpay.api.dao;
+
+import com.supwisdom.dlpay.api.domain.TSourceTypeConfig;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * Created by shuwei on 2019/4/9.
+ */
+@Repository
+public interface SourceTypeConfigDao extends JpaRepository<TSourceTypeConfig, String> {
+
+  List<TSourceTypeConfig> getBySourceType(String sourceType);
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/dao/SourceTypeDao.java b/backend/src/main/java/com/supwisdom/dlpay/api/dao/SourceTypeDao.java
new file mode 100644
index 0000000..718841a
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/dao/SourceTypeDao.java
@@ -0,0 +1,28 @@
+package com.supwisdom.dlpay.api.dao;
+
+import com.supwisdom.dlpay.api.domain.TSourceType;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * Created by shuwei on 2019/4/9.
+ */
+@Repository
+public interface SourceTypeDao extends JpaRepository<TSourceType, String> {
+  TSourceType getBySourceType(String paytype);
+
+  Page<TSourceType> findBySourceTypeContaining(String paytype, Pageable pageable);
+
+  @Query("select t from TSourceType t")
+  List<TSourceType> getConsumeSourceTypes();
+
+  List<TSourceType> findByEnableAndTenantid(Boolean enabled, String tenantid);
+
+  @Query("from TSourceType t where t.enable=true  and t.checkable=true and t.tenantid=?1 order by t.sourceType ")
+  List<TSourceType> findNeedChecks(String tenantid);
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/domain/TAccount.java b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TAccount.java
new file mode 100644
index 0000000..56e0cae
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TAccount.java
@@ -0,0 +1,323 @@
+package com.supwisdom.dlpay.api.domain;
+
+import com.supwisdom.dlpay.framework.util.MoneyUtil;
+import com.supwisdom.dlpay.framework.util.Signature;
+import com.supwisdom.dlpay.framework.util.TradeDict;
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+import java.sql.Timestamp;
+
+@Entity
+@Table(name = "TB_ACCOUNT",
+    indexes = {@Index(name = "acc_userid_idx", columnList = "userid"),
+        @Index(name = "acc_status_idx", columnList = "TRANS_STATUS"),
+        @Index(name = "acc_subjno_uk", unique = true, columnList = "subjno,userid")})
+public class TAccount implements Serializable {
+  @Id
+  @GenericGenerator(name = "idGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "idGenerator")
+  @Column(name = "ACCNO", nullable = false, length = 32)
+  private String accno; //账号
+
+  @Column(name = "ACCNAME", length = 100)
+  private String accname; //账户名
+
+  @Column(name = "SUBJNO", length = 10)
+  private String subjno; //科目号
+
+  @Column(name = "USERID", length = 32)
+  @NotNull
+  private String userid; //用户ID
+
+  @Column(name = "TRANS_STATUS", length = 20)
+  @NotNull
+  private String transStatus = TradeDict.STATUS_NORMAL; //状态:normal-正常;closed-注销;locked-冻结
+
+  @Column(name = "BALANCE", precision = 15, scale = 2)
+  @NotNull
+  private Double balance; //总余额
+
+  @Column(name = "AVAILBAL", precision = 15, scale = 2)
+  @NotNull
+  private Double availbal; //可用余额
+
+  @Column(name = "FROZEBAL", precision = 15, scale = 2)
+  @NotNull
+  private Double frozebal; //冻结金额
+
+  @Column(name = "LOWFREE_FLAG", precision = 1, scale = 0)
+  @NotNull
+  private Boolean lowfreeFlag; //低额免密开关
+
+  @Column(name = "LOWFREE_LIMIT", precision = 9, scale = 2)
+  private Double lowfreeLimit; //免密额度
+
+  @Column(name = "DAY_LIMIT", precision = 9, scale = 2)
+  private Double daylimit; // 日累计消费额度
+
+  @Column(name = "MAX_BAL", precision = 15, scale = 2)
+  private Double maxbal; // 最大余额限制
+
+  @Version
+  @Column(name = "LAST_TRANSTIME")
+  private Timestamp lasttranstime; //最后交易日期
+
+  @Column(name = "LASTDAY_TRANSAMT", precision = 9, scale = 2)
+  private Double lastdayTransamt; //最后一天消费金额
+
+  @Column(name = "LASTDAY_DPSAMT", precision = 9, scale = 2)
+  private Double lastdayDpsamt; //最后一天充值金额
+
+  @Column(name = "TAC", length = 32)
+  private String tac; //校验
+
+  @Column(name = "OPENDATE", length = 8)
+  @NotNull
+  private String opendate;
+
+  @Column(name = "CLOSEDATE", length = 8)
+  private String closedate;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantid = "";
+
+  @OneToOne
+  @JoinColumn(name = "USERID", insertable = false, updatable = false)
+  private TPerson person;
+
+  public TAccount() {
+  }
+
+  public TAccount(String accname, String subjno, String userid, String transStatus, Double balance, Double availbal, Double frozebal, Boolean lowfreeFlag, Double lowfreeLimit, Double daylimit, Double maxbal, Timestamp lasttranstime, Double lastdayTransamt, Double lastdayDpsamt, String tac, String opendate, String closedate) {
+    this.accname = accname;
+    this.subjno = subjno;
+    this.userid = userid;
+    this.transStatus = transStatus;
+    this.balance = balance;
+    this.availbal = availbal;
+    this.frozebal = frozebal;
+    this.lowfreeFlag = lowfreeFlag;
+    this.lowfreeLimit = lowfreeLimit;
+    this.daylimit = daylimit;
+    this.maxbal = maxbal;
+    this.lasttranstime = lasttranstime;
+    this.lastdayTransamt = lastdayTransamt;
+    this.lastdayDpsamt = lastdayDpsamt;
+    this.tac = tac;
+    this.opendate = opendate;
+    this.closedate = closedate;
+  }
+
+  public String getAccno() {
+    return accno;
+  }
+
+  public void setAccno(String accno) {
+    this.accno = accno;
+  }
+
+  public String getAccname() {
+    return accname;
+  }
+
+  public void setAccname(String accname) {
+    this.accname = accname;
+  }
+
+  public String getSubjno() {
+    return subjno;
+  }
+
+  public void setSubjno(String subjno) {
+    this.subjno = subjno;
+  }
+
+  public String getUserid() {
+    return userid;
+  }
+
+  public void setUserid(String userid) {
+    this.userid = userid;
+  }
+
+  public String getTransStatus() {
+    return transStatus;
+  }
+
+  public void setTransStatus(String transStatus) {
+    this.transStatus = transStatus;
+  }
+
+  public Double getBalance() {
+    return balance;
+  }
+
+  public void setBalance(Double balance) {
+    this.balance = balance;
+  }
+
+  public Double getAvailbal() {
+    return availbal;
+  }
+
+  public void setAvailbal(Double availbal) {
+    this.availbal = availbal;
+  }
+
+  public Double getFrozebal() {
+    return frozebal;
+  }
+
+  public void setFrozebal(Double frozebal) {
+    this.frozebal = frozebal;
+  }
+
+  public Boolean getLowfreeFlag() {
+    return lowfreeFlag;
+  }
+
+  public void setLowfreeFlag(Boolean lowfreeFlag) {
+    this.lowfreeFlag = lowfreeFlag;
+  }
+
+  public Double getLowfreeLimit() {
+    return lowfreeLimit;
+  }
+
+  public void setLowfreeLimit(Double lowfreeLimit) {
+    this.lowfreeLimit = lowfreeLimit;
+  }
+
+  public Double getDaylimit() {
+    return daylimit;
+  }
+
+  public void setDaylimit(Double daylimit) {
+    this.daylimit = daylimit;
+  }
+
+  public Double getMaxbal() {
+    return maxbal;
+  }
+
+  public void setMaxbal(Double maxbal) {
+    this.maxbal = maxbal;
+  }
+
+  public Timestamp getLasttranstime() {
+    return lasttranstime;
+  }
+
+  public void setLasttranstime(Timestamp lasttranstime) {
+    this.lasttranstime = lasttranstime;
+  }
+
+  public Double getLastdayTransamt() {
+    return lastdayTransamt;
+  }
+
+  public void setLastdayTransamt(Double lastdayTransamt) {
+    this.lastdayTransamt = lastdayTransamt;
+  }
+
+  public Double getLastdayDpsamt() {
+    return lastdayDpsamt;
+  }
+
+  public void setLastdayDpsamt(Double lastdayDpsamt) {
+    this.lastdayDpsamt = lastdayDpsamt;
+  }
+
+  public String getTac() {
+    return tac;
+  }
+
+  public void setTac(String tac) {
+    this.tac = tac;
+  }
+
+  public String getOpendate() {
+    return opendate;
+  }
+
+  public void setOpendate(String opendate) {
+    this.opendate = opendate;
+  }
+
+  public String getClosedate() {
+    return closedate;
+  }
+
+  public void setClosedate(String closedate) {
+    this.closedate = closedate;
+  }
+
+  public String generateTac() {
+    String data = this.accno
+        + this.opendate
+        + this.transStatus
+        + this.userid
+        + MoneyUtil.YuanToFen(this.availbal)
+        + MoneyUtil.YuanToFen(this.balance)
+        + MoneyUtil.YuanToFen(this.frozebal);
+//    System.out.println("accno="+this.accno+",data="+data);
+    return Signature.generateTac(this.accno, data);
+  }
+
+  public boolean tacCheck() {
+    String tac_c = generateTac();
+    if (tac_c.equalsIgnoreCase(this.tac) || Signature.SPY_TAC.equals(this.tac)) {
+      return true;
+    }
+    return false;
+  }
+
+  @PrePersist
+  @PreUpdate
+  public void updateTac() {
+    this.tac = generateTac();
+  }
+
+  public boolean checkOverflow() {
+    if (null != this.maxbal && this.maxbal > 0) {
+      if (MoneyUtil.moneyCompare(this.balance, this.maxbal) > 0) {
+        return true; //超出账户最大值
+      }
+    }
+    return false;
+  }
+
+  public boolean checkOverdraft() {
+    if (MoneyUtil.moneyCompare(this.balance, 0) < 0) {
+      return true; //欠费透支
+    }
+    return false;
+  }
+
+  public boolean checkOverdraft(double limit) {
+    if (MoneyUtil.moneyCompare(this.balance, limit) < 0) {
+      return true; //余额低于某阀值
+    }
+    return false;
+  }
+
+  public TPerson getPerson() {
+    return person;
+  }
+
+  public void setPerson(TPerson person) {
+    this.person = person;
+  }
+
+  public String getTenantid() {
+    return tenantid;
+  }
+
+  public void setTenantid(String tenantid) {
+    this.tenantid = tenantid;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/domain/TCard.java b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TCard.java
new file mode 100644
index 0000000..02e6bf9
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TCard.java
@@ -0,0 +1,151 @@
+package com.supwisdom.dlpay.api.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+
+@Entity
+@Table(name = "TB_CARD",
+    indexes = {@Index(name = "tb_card_uk", columnList = "cardno,cardtype", unique = true)})
+public class TCard {
+  @Id
+  @GenericGenerator(name = "idGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "idGenerator")
+  @Column(name = "ID", nullable = false, length = 32)
+  private String id;
+
+  @Column(name = "CARDNO", nullable = false, length = 32)
+  private String cardno;
+
+  @Column(name = "CARDTYPE", nullable = false, length = 20)
+  private String cardtype;
+
+  @Column(name = "CARDPHYID", length = 20)
+  private String cardphyid;
+
+  @Column(name = "STATUS", nullable = false, length = 20)
+  private String status; //normal/closed
+
+  @Column(name = "TRANS_STATUS", nullable = false, length = 20)
+  private String transStatus; //normal/loss/frozen/locked/abnormal/unuse
+
+  @Column(name = "EXPIREDATE", length = 20)
+  private String expiredate;
+
+  @Column(name = "SIGNED", nullable = false, length = 10)
+  private Boolean signed = false; //签约状态
+
+  @Column(name = "USERID", nullable = false, length = 32)
+  private String userid;
+
+  @Column(name = "LASTSAVED", length = 20)
+  private String lastsaved;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantid = "";
+
+  public TCard() {
+  }
+
+  public TCard(String cardno, String cardtype, String cardphyid, String status, String transStatus, String expiredate, Boolean signed, String userid, String lastsaved) {
+    this.cardno = cardno;
+    this.cardtype = cardtype;
+    this.cardphyid = cardphyid;
+    this.status = status;
+    this.transStatus = transStatus;
+    this.expiredate = expiredate;
+    this.signed = signed;
+    this.userid = userid;
+    this.lastsaved = lastsaved;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  public void setId(String id) {
+    this.id = id;
+  }
+
+  public String getCardno() {
+    return cardno;
+  }
+
+  public void setCardno(String cardno) {
+    this.cardno = cardno;
+  }
+
+  public String getCardtype() {
+    return cardtype;
+  }
+
+  public void setCardtype(String cardtype) {
+    this.cardtype = cardtype;
+  }
+
+  public String getCardphyid() {
+    return cardphyid;
+  }
+
+  public void setCardphyid(String cardphyid) {
+    this.cardphyid = cardphyid;
+  }
+
+  public String getStatus() {
+    return status;
+  }
+
+  public void setStatus(String status) {
+    this.status = status;
+  }
+
+  public String getTransStatus() {
+    return transStatus;
+  }
+
+  public void setTransStatus(String transStatus) {
+    this.transStatus = transStatus;
+  }
+
+  public String getExpiredate() {
+    return expiredate;
+  }
+
+  public void setExpiredate(String expiredate) {
+    this.expiredate = expiredate;
+  }
+
+  public Boolean getSigned() {
+    return signed;
+  }
+
+  public void setSigned(Boolean signed) {
+    this.signed = signed;
+  }
+
+  public String getUserid() {
+    return userid;
+  }
+
+  public void setUserid(String userid) {
+    this.userid = userid;
+  }
+
+  public String getLastsaved() {
+    return lastsaved;
+  }
+
+  public void setLastsaved(String lastsaved) {
+    this.lastsaved = lastsaved;
+  }
+
+  public String getTenantid() {
+    return tenantid;
+  }
+
+  public void setTenantid(String tenantid) {
+    this.tenantid = tenantid;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/domain/TDebitCreditDtl.java b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TDebitCreditDtl.java
new file mode 100644
index 0000000..b9cfdc8
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TDebitCreditDtl.java
@@ -0,0 +1,131 @@
+package com.supwisdom.dlpay.api.domain;
+
+import javax.persistence.*;
+import javax.validation.constraints.Digits;
+import javax.validation.constraints.NotNull;
+
+@Entity
+@Table(name = "TB_USERDTL_DEBITCREDIT")
+@IdClass(TDebitCreditDtlPK.class)
+public class TDebitCreditDtl {
+
+  @Id
+  @Column(name = "REFNO", nullable = false, length = 32)
+  private String refno;
+
+  @Id
+  @Column(name = "SEQNO", nullable = false, precision = 2)
+  private Integer seqno;
+
+  @Column(name = "settledate", length = 8)
+  @Digits(integer = 8, fraction = 0)
+  private String settleDate;
+
+  @Column(name = "DRSUBJNO", length = 10)
+  @NotNull
+  private String drsubjno; //借方科目
+
+  @Column(name = "DRACCNO", length = 32)
+  @NotNull
+  private String draccno;  //借方账号
+
+  @Column(name = "AMOUNT", precision = 9, scale = 2)
+  @NotNull
+  private Double amount; //金额
+
+  @Column(name = "CRSUBJNO", length = 10)
+  @NotNull
+  private String crsubjno; //贷方科目
+
+  @Column(name = "CRACCNO", length = 32)
+  @NotNull
+  private String craccno;  //贷方账号
+
+  @Column(name = "SUMMARY", length = 240)
+  private String summary;  //摘要
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantid = "";
+
+
+  public String getRefno() {
+    return refno;
+  }
+
+  public void setRefno(String refno) {
+    this.refno = refno;
+  }
+
+  public Integer getSeqno() {
+    return seqno;
+  }
+
+  public void setSeqno(Integer seqno) {
+    this.seqno = seqno;
+  }
+
+  public String getDrsubjno() {
+    return drsubjno;
+  }
+
+  public void setDrsubjno(String drsubjno) {
+    this.drsubjno = drsubjno;
+  }
+
+  public String getDraccno() {
+    return draccno;
+  }
+
+  public void setDraccno(String draccno) {
+    this.draccno = draccno;
+  }
+
+  public Double getAmount() {
+    return amount;
+  }
+
+  public void setAmount(Double amount) {
+    this.amount = amount;
+  }
+
+  public String getCrsubjno() {
+    return crsubjno;
+  }
+
+  public void setCrsubjno(String crsubjno) {
+    this.crsubjno = crsubjno;
+  }
+
+  public String getCraccno() {
+    return craccno;
+  }
+
+  public void setCraccno(String craccno) {
+    this.craccno = craccno;
+  }
+
+  public String getSummary() {
+    return summary;
+  }
+
+  public void setSummary(String summary) {
+    this.summary = summary;
+  }
+
+  public String getSettleDate() {
+    return settleDate;
+  }
+
+  public void setSettleDate(String settleDate) {
+    this.settleDate = settleDate;
+  }
+
+  public String getTenantid() {
+    return tenantid;
+  }
+
+  public void setTenantid(String tenantid) {
+    this.tenantid = tenantid;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/domain/TDebitCreditDtlPK.java b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TDebitCreditDtlPK.java
new file mode 100644
index 0000000..5a0a48b
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TDebitCreditDtlPK.java
@@ -0,0 +1,51 @@
+package com.supwisdom.dlpay.api.domain;
+
+import javax.persistence.Column;
+import javax.persistence.Id;
+import java.io.Serializable;
+
+public class TDebitCreditDtlPK implements Serializable {
+  @Id
+  @Column(name = "REFNO", nullable = false, length = 32)
+  private String refno;
+
+  @Id
+  @Column(name = "SEQNO", nullable = false, precision = 2)
+  private Integer seqno;
+
+  public String getRefno() {
+    return refno;
+  }
+
+  public void setRefno(String refno) {
+    this.refno = refno;
+  }
+
+  public Integer getSeqno() {
+    return seqno;
+  }
+
+  public void setSeqno(Integer seqno) {
+    this.seqno = seqno;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    TDebitCreditDtlPK tDebitCreditDtlPK = (TDebitCreditDtlPK) o;
+    if (refno != null ? !refno.equals(tDebitCreditDtlPK.getRefno()) : refno != null)
+      return false;
+    if (seqno != null ? !seqno.equals(tDebitCreditDtlPK.getSeqno()) : seqno != null)
+      return false;
+
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = refno != null ? refno.hashCode() : 0;
+    result = 31 * result + (seqno != null ? seqno.hashCode() : 0);
+    return result;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/domain/TPerson.java b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TPerson.java
new file mode 100644
index 0000000..a8cfbd6
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TPerson.java
@@ -0,0 +1,208 @@
+package com.supwisdom.dlpay.api.domain;
+
+import com.supwisdom.dlpay.api.annotation.IDNoCheck;
+import com.supwisdom.dlpay.api.annotation.MobileNumber;
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotNull;
+
+@Entity
+@Table(name = "TB_PERSON",
+    indexes = {@Index(name = "person_name_idx", columnList = "name"),
+        @Index(name = "person_idno_uk", unique = true, columnList = "idtype,idno")})
+@IDNoCheck(message = "证件信息错误", idno = "idno", idtype = "idtype")
+public class TPerson {
+  @Id
+  @GenericGenerator(name = "idGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "idGenerator")
+  @Column(name = "USERID", nullable = false, length = 32)
+  private String userid;
+
+  @Column(name = "NAME", length = 200)
+  private String name;
+
+  @Column(name = "SEX", length = 10)
+  private String sex;
+
+  @Column(name = "STATUS", length = 10)
+  @NotNull
+  private String status;
+
+  @Column(name = "IDTYPE", length = 60)
+  private String idtype;
+
+  @Column(name = "IDNO", length = 30)
+  private String idno;
+
+  @Column(name = "COUNTRY", length = 3)
+  private String country;
+
+  @Column(name = "NATION", length = 3)
+  private String nation;
+
+  @Column(name = "EMAIL", length = 60)
+  @Email(message = "电子邮件格式错误")
+  private String email;
+
+  @Column(name = "TEL", length = 20)
+  private String tel;
+
+  @Column(name = "MOBILE", length = 30)
+  @MobileNumber(message = "手机号格式错误")
+  private String mobile;
+
+  @Column(name = "ADDR", length = 240)
+  private String addr;
+
+  @Column(name = "ZIPCODE", length = 10)
+  private String zipcode;
+
+  @Column(name = "LASTSAVED", length = 14)
+  private String lastsaved;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantid = "";
+
+  public TPerson() {
+  }
+
+  public TPerson(String name, String sex, String status, String idtype, String idno, String country, String nation, String email, String tel, String mobile, String addr, String zipcode, String lastsaved) {
+    this.name = name;
+    this.sex = sex;
+    this.status = status;
+    this.idtype = idtype;
+    this.idno = idno;
+    this.country = country;
+    this.nation = nation;
+    this.email = email;
+    this.tel = tel;
+    this.mobile = mobile;
+    this.addr = addr;
+    this.zipcode = zipcode;
+    this.lastsaved = lastsaved;
+  }
+
+
+  public String getUserid() {
+    return userid;
+  }
+
+  public void setUserid(String userid) {
+    this.userid = userid;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public String getSex() {
+    return sex;
+  }
+
+  public void setSex(String sex) {
+    this.sex = sex;
+  }
+
+  public String getStatus() {
+    return status;
+  }
+
+  public void setStatus(String status) {
+    this.status = status;
+  }
+
+  public String getIdtype() {
+    return idtype;
+  }
+
+  public void setIdtype(String idtype) {
+    this.idtype = idtype;
+  }
+
+  public String getIdno() {
+    return idno;
+  }
+
+  public void setIdno(String idno) {
+    this.idno = idno;
+  }
+
+  public String getCountry() {
+    return country;
+  }
+
+  public void setCountry(String country) {
+    this.country = country;
+  }
+
+  public String getNation() {
+    return nation;
+  }
+
+  public void setNation(String nation) {
+    this.nation = nation;
+  }
+
+  public String getEmail() {
+    return email;
+  }
+
+  public void setEmail(String email) {
+    this.email = email;
+  }
+
+  public String getTel() {
+    return tel;
+  }
+
+  public void setTel(String tel) {
+    this.tel = tel;
+  }
+
+  public String getMobile() {
+    return mobile;
+  }
+
+  public void setMobile(String mobile) {
+    this.mobile = mobile;
+  }
+
+  public String getAddr() {
+    return addr;
+  }
+
+  public void setAddr(String addr) {
+    this.addr = addr;
+  }
+
+  public String getZipcode() {
+    return zipcode;
+  }
+
+  public void setZipcode(String zipcode) {
+    this.zipcode = zipcode;
+  }
+
+  public String getLastsaved() {
+    return lastsaved;
+  }
+
+  public void setLastsaved(String lastsaved) {
+    this.lastsaved = lastsaved;
+  }
+
+  public String getTenantid() {
+    return tenantid;
+  }
+
+  public void setTenantid(String tenantid) {
+    this.tenantid = tenantid;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/domain/TPersondtl.java b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TPersondtl.java
new file mode 100644
index 0000000..c961f32
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TPersondtl.java
@@ -0,0 +1,301 @@
+package com.supwisdom.dlpay.api.domain;
+
+import com.supwisdom.dlpay.framework.util.TradeDict;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+
+@Entity
+@Table(name = "TB_PERSONDTL",
+    indexes = {@Index(name = "prsndtl_transdate_idx", columnList = "transdate"),
+        @Index(name = "prsndtl_accdate_idx", columnList = "accdate"),
+        @Index(name = "prsndtl_status_idx", columnList = "status"),
+        @Index(name = "prsndlt_tenantid_idx", columnList = "tenantid"),
+        @Index(name = "prsndtl_reverse_idx", columnList = "REVERSE_FLAG")})
+public class TPersondtl {
+
+  @Id
+  @Column(name = "REFNO", nullable = false, length = 32)
+  private String refno; //流水号
+
+  @Column(name = "ACCDATE", length = 8)
+  @NotNull
+  private String accdate; //记账日期
+
+  @Column(name = "USERID", length = 32)
+  private String userid;  //用户ID,或账号
+
+  @Column(name = "ACCNO", length = 32)
+  private String accountNo;
+
+  @Column(name = "USERNAME", length = 200)
+  private String userName;
+
+  @Column(name = "TRANSDATE", length = 8)
+  @NotNull
+  private String transdate;
+
+  @Column(name = "TRANSTIME", length = 6)
+  @NotNull
+  private String transtime;
+
+  @Column(name = "STATUS", length = 20)
+  @NotNull
+  private String status = TradeDict.DTL_STATUS_NONE;
+
+  @Column(name = "BEFBAL", precision = 9, scale = 2)
+  private Double befbal;
+
+  @Column(name = "amount", precision = 9, scale = 2)
+  @NotNull
+  private Double amount; //实际付款金额
+
+  @Column(name = "SOURCETYPE", length = 20)
+  private String sourceType; //支付方式 balance,wechat,alipay
+
+  @Column(name = "PAYINFO", length = 200)
+  private String payinfo; //记录支付信息备用字段
+
+  @Column(name = "TRANSCODE", precision = 4)
+  @NotNull
+  private Integer transcode;
+
+  @Column(name = "TRANSDESC", length = 240)
+  private String transdesc; //交易描述
+
+  @Column(name = "OUTTRADENO", length = 60)
+  private String outtradeno; //第三方流水号
+
+  @Column(name = "OPPOSITEACCNO", length = 20)
+  private String oppositeAccNo;
+
+  @Column(name = "OPPOSITEACCNAME", length = 200)
+  private String oppositeAccName;
+
+  @Column(name = "OPERID", precision = 9)
+  private Integer operid; //操作员ID
+
+  @Column(name = "REVERSE_FLAG", length = 10)
+  @NotNull
+  private String reverseFlag = "none"; //none, cancel, reversed
+
+  @Column(name = "REVERSE_AMOUNT", precision = 9, scale = 2)
+  private Double reverseAmount = 0D; //撤销金额填写
+
+  @Column(name = "TRADEFLAG", length = 10)
+  @NotNull
+  private String tradeflag; // out - 支出,in - 收入
+
+  @Column(name = "REMARK", length = 240)
+  private String remark;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantid = "";
+
+  @Column(name = "DTLTYPE", length = 20)
+  private String dtltype;
+
+  @Column(name = "anonymous")
+  private Boolean anonymous;
+
+  public String getDtltype() {
+    return dtltype;
+  }
+
+  public void setDtltype(String dtltype) {
+    this.dtltype = dtltype;
+  }
+
+  public String getRefno() {
+    return refno;
+  }
+
+  public void setRefno(String refno) {
+    this.refno = refno;
+  }
+
+  public String getAccdate() {
+    return accdate;
+  }
+
+  public void setAccdate(String accdate) {
+    this.accdate = accdate;
+  }
+
+  public String getUserid() {
+    return userid;
+  }
+
+  public void setUserid(String userid) {
+    this.userid = userid;
+  }
+
+  public String getTransdate() {
+    return transdate;
+  }
+
+  public void setTransdate(String transdate) {
+    this.transdate = transdate;
+  }
+
+  public String getTranstime() {
+    return transtime;
+  }
+
+  public void setTranstime(String transtime) {
+    this.transtime = transtime;
+  }
+
+  public String getStatus() {
+    return status;
+  }
+
+  public void setStatus(String status) {
+    this.status = status;
+  }
+
+  public Double getBefbal() {
+    return befbal;
+  }
+
+  public void setBefbal(Double befbal) {
+    this.befbal = befbal;
+  }
+
+  public Double getAmount() {
+    return amount;
+  }
+
+  public void setAmount(Double amount) {
+    this.amount = amount;
+  }
+
+  public String getSourceType() {
+    return sourceType;
+  }
+
+  public void setSourceType(String sourceType) {
+    this.sourceType = sourceType;
+  }
+
+  public String getPayinfo() {
+    return payinfo;
+  }
+
+  public void setPayinfo(String payinfo) {
+    this.payinfo = payinfo;
+  }
+
+  public Integer getTranscode() {
+    return transcode;
+  }
+
+  public void setTranscode(Integer transcode) {
+    this.transcode = transcode;
+  }
+
+  public String getTransdesc() {
+    return transdesc;
+  }
+
+  public void setTransdesc(String transdesc) {
+    this.transdesc = transdesc;
+  }
+
+  public String getOuttradeno() {
+    return outtradeno;
+  }
+
+  public void setOuttradeno(String outtradeno) {
+    this.outtradeno = outtradeno;
+  }
+
+  public String getOppositeAccNo() {
+    return oppositeAccNo;
+  }
+
+  public void setOppositeAccNo(String oppositeAccNo) {
+    this.oppositeAccNo = oppositeAccNo;
+  }
+
+  public Integer getOperid() {
+    return operid;
+  }
+
+  public void setOperid(Integer operid) {
+    this.operid = operid;
+  }
+
+  public String getReverseFlag() {
+    return reverseFlag;
+  }
+
+  public void setReverseFlag(String reverseFlag) {
+    this.reverseFlag = reverseFlag;
+  }
+
+  public Double getReverseAmount() {
+    return reverseAmount;
+  }
+
+  public void setReverseAmount(Double reverseAmount) {
+    this.reverseAmount = reverseAmount;
+  }
+
+  public String getTradeflag() {
+    return tradeflag;
+  }
+
+  public void setTradeflag(String tradeflag) {
+    this.tradeflag = tradeflag;
+  }
+
+  public String getRemark() {
+    return remark;
+  }
+
+  public void setRemark(String remark) {
+    this.remark = remark;
+  }
+
+  public String getUserName() {
+    return userName;
+  }
+
+  public void setUserName(String userName) {
+    this.userName = userName;
+  }
+
+  public String getOppositeAccName() {
+    return oppositeAccName;
+  }
+
+  public void setOppositeAccName(String oppositeAccName) {
+    this.oppositeAccName = oppositeAccName;
+  }
+
+  public String getAccountNo() {
+    return accountNo;
+  }
+
+  public void setAccountNo(String accountNo) {
+    this.accountNo = accountNo;
+  }
+
+  public String getTenantid() {
+    return tenantid;
+  }
+
+  public void setTenantid(String tenantid) {
+    this.tenantid = tenantid;
+  }
+
+  public Boolean getAnonymous() {
+    return anonymous;
+  }
+
+  public void setAnonymous(Boolean anonymous) {
+    this.anonymous = anonymous;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/domain/TPointsAccount.java b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TPointsAccount.java
new file mode 100644
index 0000000..b023c38
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TPointsAccount.java
@@ -0,0 +1,155 @@
+package com.supwisdom.dlpay.api.domain;
+
+import com.supwisdom.dlpay.framework.util.MD5;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 积分账户表
+ */
+@Entity
+@Table(name = "TB_POINTS_ACCOUNT")
+public class TPointsAccount {
+  @Id
+  @Column(name = "USERID", length = 32)
+  private String userid;
+
+  @Column(name = "POINTS", precision = 15)
+  @NotNull
+  private Long points; //积分
+
+  @Column(name = "ACCUMPOINTS", precision = 15)
+  private Long accumPoints; //历史累计获取的积分
+
+  @Column(name = "SUMPAYPOINTS", precision = 15)
+  private Long sumpayPoints; //历史累计消费的积分
+
+  @Column(name = "TAC", precision = 15)
+  private String tac;
+
+  @Column(name = "LASTSAVED", precision = 15)
+  private String lastsaved;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantid = "";
+
+  @OneToOne
+  @JoinColumn(name = "USERID", insertable = false, updatable = false)
+  private TPerson person;
+
+  public TPerson getPerson() {
+    return person;
+  }
+
+  public void setPerson(TPerson person) {
+    this.person = person;
+  }
+
+  public TPointsAccount() {
+  }
+
+  public TPointsAccount(String userid, Long points, Long accumPoints, Long sumpayPoints, String tac, String lastsaved) {
+    this.userid = userid;
+    this.points = points;
+    this.accumPoints = accumPoints;
+    this.sumpayPoints = sumpayPoints;
+    this.tac = tac;
+    this.lastsaved = lastsaved;
+  }
+
+  public String getUserid() {
+    return userid;
+  }
+
+  public void setUserid(String userid) {
+    this.userid = userid;
+  }
+
+  public Long getPoints() {
+    return points;
+  }
+
+  public void setPoints(Long points) {
+    this.points = points;
+  }
+
+  public Long getAccumPoints() {
+    return accumPoints;
+  }
+
+  public void setAccumPoints(Long accumPoints) {
+    this.accumPoints = accumPoints;
+  }
+
+  public Long getSumpayPoints() {
+    return sumpayPoints;
+  }
+
+  public void setSumpayPoints(Long sumpayPoints) {
+    this.sumpayPoints = sumpayPoints;
+  }
+
+  public String getTac() {
+    return tac;
+  }
+
+  public void setTac(String tac) {
+    this.tac = tac;
+  }
+
+  public String getLastsaved() {
+    return lastsaved;
+  }
+
+  public void setLastsaved(String lastsaved) {
+    this.lastsaved = lastsaved;
+  }
+
+  public String generateTac() {
+    String data = this.userid + this.points;
+    return MD5.generatePassword(data, this.userid);
+  }
+
+  public boolean tacCheck() {
+    String tac_c = generateTac();
+    if (tac_c.equalsIgnoreCase(this.tac) || this.tac == null) {
+      return true;
+    }
+    return false;
+  }
+
+  public void addPoints(int points) {
+    this.points = this.points + points;
+    if (points > 0) {
+      this.accumPoints = ((this.accumPoints == null ? 0 : this.accumPoints) + Math.abs(points)); //获得积分累加
+    } else {
+      this.sumpayPoints = ((this.sumpayPoints == null ? 0 : this.sumpayPoints) + Math.abs(points)); //消费积分累加
+    }
+    this.tac = this.generateTac();
+  }
+
+  public void addPoints(int points, boolean revflag) {
+    if (revflag) {
+      //冲正返回积分
+      this.points = this.points + points;
+      if (points > 0) {
+        this.sumpayPoints = ((this.sumpayPoints == null ? 0 : this.sumpayPoints) - Math.abs(points)); //消费积分累计冲正
+      } else {
+        this.accumPoints = ((this.accumPoints == null ? 0 : this.accumPoints) - Math.abs(points)); //获得积分累计冲正
+      }
+      this.tac = this.generateTac();
+    } else {
+      this.addPoints(points);
+    }
+  }
+
+  public String getTenantid() {
+    return tenantid;
+  }
+
+  public void setTenantid(String tenantid) {
+    this.tenantid = tenantid;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/domain/TShopSourceType.java b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TShopSourceType.java
new file mode 100644
index 0000000..f28f307
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TShopSourceType.java
@@ -0,0 +1,112 @@
+package com.supwisdom.dlpay.api.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+@Entity
+@Table(name = "TB_SHOP_SOURCETYPE",
+    indexes = {@Index(name = "shop_sourcetype_idx",
+        columnList = "shopaccno, sourcetype, tenantid", unique = true)})
+public class TShopSourceType implements Serializable {
+  private static final long serialVersionUID = -5789612905074310179L;
+
+  @Id
+  @GenericGenerator(name = "idGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "idGenerator")
+  @Column(name = "id")
+  private String id;
+
+  @Column(name = "SHOPACCNO", length = 10)
+  @NotNull
+  private String shopaccno;
+
+  @Column(name = "SOURCETYPE", length = 20)
+  @NotNull
+  private String sourceType;
+
+  @Column(name = "CONSUME_ENABLE", length = 10)
+  @NotNull
+  private boolean consumeEnable; //该商户下,此支付方式能否消费
+
+  @Column(name = "ANONYMOUS_ENABLE", length = 10)
+  @NotNull
+  private boolean anonymousEnable; //该商户下,此支付方式能否匿名消费
+
+  @Column(name = "REVERSE_ENABLE", length = 10)
+  @NotNull
+  private boolean reverseEnable; //该商户下,此支付方式能否冲正
+
+  @Column(name = "CREATETIME", length = 14)
+  private String createtime;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantid = "";
+
+  public String getShopaccno() {
+    return shopaccno;
+  }
+
+  public void setShopaccno(String shopaccno) {
+    this.shopaccno = shopaccno;
+  }
+
+  public String getSourceType() {
+    return sourceType;
+  }
+
+  public void setSourceType(String sourceType) {
+    this.sourceType = sourceType;
+  }
+
+  public boolean getConsumeEnable() {
+    return consumeEnable;
+  }
+
+  public void setConsumeEnable(boolean consumeEnable) {
+    this.consumeEnable = consumeEnable;
+  }
+
+  public boolean getAnonymousEnable() {
+    return anonymousEnable;
+  }
+
+  public void setAnonymousEnable(boolean anonymousEnable) {
+    this.anonymousEnable = anonymousEnable;
+  }
+
+  public boolean getReverseEnable() {
+    return reverseEnable;
+  }
+
+  public void setReverseEnable(boolean reverseEnable) {
+    this.reverseEnable = reverseEnable;
+  }
+
+  public String getCreatetime() {
+    return createtime;
+  }
+
+  public void setCreatetime(String createtime) {
+    this.createtime = createtime;
+  }
+
+  public String getTenantid() {
+    return tenantid;
+  }
+
+  public void setTenantid(String tenantid) {
+    this.tenantid = tenantid;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  public void setId(String id) {
+    this.id = id;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/domain/TShopSourceTypeConfig.java b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TShopSourceTypeConfig.java
new file mode 100644
index 0000000..981b0d1
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TShopSourceTypeConfig.java
@@ -0,0 +1,110 @@
+package com.supwisdom.dlpay.api.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+@Entity
+@Table(name = "TB_SHOP_PAYTYPE_CONFIG",
+    indexes = {@Index(name = "shop_sourceype_config_idx", unique = true,
+        columnList = "shopaccno,sourcetype,configid,tenantid")})
+public class TShopSourceTypeConfig implements Serializable {
+  private static final long serialVersionUID = 135187738251025666L;
+
+  @Id
+  @GenericGenerator(name = "idGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "idGenerator")
+  @Column(name = "CFGID", nullable = false, length = 32)
+  private String cfgid;
+
+  @Column(name = "SHOPACCNO", length = 10)
+  @NotNull
+  private String shopaccno;
+
+  @Column(name = "SOURCETYPE", length = 20)
+  @NotNull
+  private String sourceType;
+
+  @Column(name = "CONFIGID", length = 40)
+  @NotNull
+  private String configid;
+
+  @Column(name = "CONFIG_VALUE", length = 2000)
+  private String configValue;
+
+  @Column(name = "CONFIG_NAME", length = 200)
+  private String configName;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantid = "";
+
+  public TShopSourceTypeConfig() {
+  }
+
+  public TShopSourceTypeConfig(String shopaccno, String sourceType, String configid, String configValue, String configName) {
+    this.shopaccno = shopaccno;
+    this.sourceType = sourceType;
+    this.configid = configid;
+    this.configValue = configValue;
+    this.configName = configName;
+  }
+
+  public String getCfgid() {
+    return cfgid;
+  }
+
+  public void setCfgid(String cfgid) {
+    this.cfgid = cfgid;
+  }
+
+  public String getShopaccno() {
+    return shopaccno;
+  }
+
+  public void setShopaccno(String shopaccno) {
+    this.shopaccno = shopaccno;
+  }
+
+  public String getSourceType() {
+    return sourceType;
+  }
+
+  public void setSourceType(String sourceType) {
+    this.sourceType = sourceType;
+  }
+
+  public String getConfigid() {
+    return configid;
+  }
+
+  public void setConfigid(String configid) {
+    this.configid = configid;
+  }
+
+  public String getConfigValue() {
+    return configValue;
+  }
+
+  public void setConfigValue(String configValue) {
+    this.configValue = configValue;
+  }
+
+  public String getConfigName() {
+    return configName;
+  }
+
+  public void setConfigName(String configName) {
+    this.configName = configName;
+  }
+
+  public String getTenantid() {
+    return tenantid;
+  }
+
+  public void setTenantid(String tenantid) {
+    this.tenantid = tenantid;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/domain/TShopdtl.java b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TShopdtl.java
new file mode 100644
index 0000000..e930ce8
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TShopdtl.java
@@ -0,0 +1,243 @@
+package com.supwisdom.dlpay.api.domain;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+
+@Entity
+@Table(name = "TB_SHOPDTL",
+    indexes = {@Index(name = "shopdtl_accdate", columnList = "accdate"),
+        @Index(name = "shopdtl_shopaccno", columnList = "shopaccno"),
+        @Index(name = "shopdtl_transdate", columnList = "transdate"),
+        @Index(name = "shopdtl_tenantid_idx", columnList = "tenantid"),
+        @Index(name = "shopdtl_updateflag", columnList = "updatebala")})
+public class TShopdtl {
+  @Id
+  @Column(name = "REFNO", length = 32, nullable = false)
+  private String refno;
+
+  @Column(name = "ACCDATE", length = 8)
+  @NotNull
+  private String accdate;
+
+  @Column(name = "SHOPACCNO", length = 10)
+  @NotNull
+  private String shopaccno;
+
+  @Column(name = "SHOPNAME", length = 200)
+  private String shopname;
+
+  @Column(name = "AMOUNT", scale = 2, precision = 15)
+  @NotNull
+  private Double amount;
+
+  @Column(name = "TRANSDATE", length = 8)
+  @NotNull
+  private String transdate;
+
+  @Column(name = "TRANSTIME", length = 6)
+  @NotNull
+  private String transtime;
+
+  @Column(name = "TRANSCODE", precision = 8)
+  private Integer transcode;
+
+  @Column(name = "SOURCETYPE", length = 20)
+  private String sourceType;
+
+  @Column(name = "PAYINFO", length = 200)
+  private String payInfo;
+
+  @Column(name = "TRADEFLAG", length = 6)
+  @NotNull
+  private String tradeflag; // out - 支出 , in - 收入
+
+  @Column(name = "TRANSDESC", length = 240)
+  private String transdesc; //交易描述
+
+  @Column(name = "REVERSEFLAG", length = 10)
+  private String reverseFlag;
+
+  @Column(name = "STATUS", length = 20)
+  @NotNull
+  private String status;
+
+  @Column(name = "updatebala")
+  @NotNull
+  private Boolean updateBala = false;
+
+  @Column(name = "OPPOSITEACCNO", length = 64)
+  private String oppositeAccNo;
+
+  @Column(name = "OPPOSITEACCNAME", length = 200)
+  private String oppositeAccName;
+
+  @Column(name = "REMARK", length = 240)
+  private String remark;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantid = "";
+
+  @Column(name = "DTLTYPE", length = 20)
+  private String dtltype;
+
+  public String getDtltype() {
+    return dtltype;
+  }
+
+  public void setDtltype(String dtltype) {
+    this.dtltype = dtltype;
+  }
+
+  public String getRefno() {
+    return refno;
+  }
+
+  public void setRefno(String refno) {
+    this.refno = refno;
+  }
+
+  public String getAccdate() {
+    return accdate;
+  }
+
+  public void setAccdate(String accdate) {
+    this.accdate = accdate;
+  }
+
+  public String getShopaccno() {
+    return shopaccno;
+  }
+
+  public void setShopaccno(String shopaccno) {
+    this.shopaccno = shopaccno;
+  }
+
+  public String getShopname() {
+    return shopname;
+  }
+
+  public void setShopname(String shopname) {
+    this.shopname = shopname;
+  }
+
+  public Double getAmount() {
+    return amount;
+  }
+
+  public void setAmount(Double amount) {
+    this.amount = amount;
+  }
+
+  public String getTransdate() {
+    return transdate;
+  }
+
+  public void setTransdate(String transdate) {
+    this.transdate = transdate;
+  }
+
+  public String getTranstime() {
+    return transtime;
+  }
+
+  public void setTranstime(String transtime) {
+    this.transtime = transtime;
+  }
+
+  public Integer getTranscode() {
+    return transcode;
+  }
+
+  public void setTranscode(Integer transcode) {
+    this.transcode = transcode;
+  }
+
+  public String getSourceType() {
+    return sourceType;
+  }
+
+  public void setSourceType(String sourceType) {
+    this.sourceType = sourceType;
+  }
+
+  public String getPayInfo() {
+    return payInfo;
+  }
+
+  public void setPayInfo(String payInfo) {
+    this.payInfo = payInfo;
+  }
+
+  public String getTradeflag() {
+    return tradeflag;
+  }
+
+  public void setTradeflag(String tradeflag) {
+    this.tradeflag = tradeflag;
+  }
+
+  public String getTransdesc() {
+    return transdesc;
+  }
+
+  public void setTransdesc(String transdesc) {
+    this.transdesc = transdesc;
+  }
+
+  public String getReverseFlag() {
+    return reverseFlag;
+  }
+
+  public void setReverseFlag(String reverseFlag) {
+    this.reverseFlag = reverseFlag;
+  }
+
+  public String getStatus() {
+    return status;
+  }
+
+  public void setStatus(String status) {
+    this.status = status;
+  }
+
+  public String getOppositeAccNo() {
+    return oppositeAccNo;
+  }
+
+  public void setOppositeAccNo(String oppositeAccNo) {
+    this.oppositeAccNo = oppositeAccNo;
+  }
+
+  public String getOppositeAccName() {
+    return oppositeAccName;
+  }
+
+  public void setOppositeAccName(String oppositeAccName) {
+    this.oppositeAccName = oppositeAccName;
+  }
+
+  public String getRemark() {
+    return remark;
+  }
+
+  public void setRemark(String remark) {
+    this.remark = remark;
+  }
+
+  public Boolean getUpdateBala() {
+    return updateBala;
+  }
+
+  public void setUpdateBala(Boolean updateBala) {
+    this.updateBala = updateBala;
+  }
+
+  public String getTenantid() {
+    return tenantid;
+  }
+
+  public void setTenantid(String tenantid) {
+    this.tenantid = tenantid;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/domain/TSourceType.java b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TSourceType.java
new file mode 100644
index 0000000..befec80
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TSourceType.java
@@ -0,0 +1,192 @@
+package com.supwisdom.dlpay.api.domain;
+
+import com.supwisdom.dlpay.framework.domain.DictionaryTable;
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * Created by shuwei on 2019/4/9.
+ */
+@Entity
+@Table(name = "TB_SOURCETYPE",
+    indexes = {@Index(name = "sourcetype_idx", columnList = "sourcetype, tenantid", unique = true)})
+public class TSourceType implements DictionaryTable, Serializable {
+  private static final long serialVersionUID = 2424711106241326859L;
+
+  @Id
+  @GenericGenerator(name = "idGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "idGenerator")
+  @Column(name = "sourcetype_id", length = 32)
+  private String sourceTypeId;
+
+  @Column(name = "SOURCETYPE", nullable = false, length = 20)
+  private String sourceType;
+
+  @Column(name = "ENABLE", length = 20)
+  @NotNull
+  private Boolean enable;
+
+  @Column(name = "CHARGE_ENABLE", length = 10)
+  @NotNull
+  private Boolean chargeEnable; //充值总开关
+
+  @Column(name = "CONSUME_ENABLE", length = 10)
+  @NotNull
+  private Boolean consumeEnable; //消费总开关
+
+  @Column(name = "ANONYMOUS_ENABLE", length = 10)
+  @NotNull
+  private Boolean anonymousEnable; //匿名消费总开关
+
+  @Column(name = "reversable")
+  @NotNull
+  private Boolean reversable;
+
+  @Column(name = "checkable")
+  @NotNull
+  private Boolean checkable; // 是否需要清算
+
+  @Column(name = "pay_subjno", length = 12)
+  private @NotNull String paySubjno;
+
+  @Column(name = "deposite_subjno", length = 12)
+  private String depositeSubjno;
+
+  @Column(name = "PAYDESC", length = 200)
+  private String paydesc;
+
+  @Column(name = "tplusn", nullable = false, precision = 5)
+  private Integer tplusN; // 系统结算日期,  T+0 ~ T+N
+
+  @Column(name = "START_CHKTIME", precision = 5)
+  private String startChktime; //开始能对账的时间。比如微信、支付宝是上午10点生成对账单,一般设为10:30开始对,则填:103000;为空时这说明无限制
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantid = "";
+
+  public String getSourceType() {
+    return sourceType;
+  }
+
+  public void setSourceType(String sourceType) {
+    this.sourceType = sourceType;
+  }
+
+  public Boolean getEnable() {
+    return enable;
+  }
+
+  public void setEnable(Boolean enable) {
+    this.enable = enable;
+  }
+
+  public Boolean getChargeEnable() {
+    return chargeEnable;
+  }
+
+  public void setChargeEnable(Boolean chargeEnable) {
+    this.chargeEnable = chargeEnable;
+  }
+
+  public Boolean getConsumeEnable() {
+    return consumeEnable;
+  }
+
+  public void setConsumeEnable(Boolean consumeEnable) {
+    this.consumeEnable = consumeEnable;
+  }
+
+  public Boolean getAnonymousEnable() {
+    return anonymousEnable;
+  }
+
+  public void setAnonymousEnable(Boolean anonymousEnable) {
+    this.anonymousEnable = anonymousEnable;
+  }
+
+  public String getPaydesc() {
+    return paydesc;
+  }
+
+  public void setPaydesc(String paydesc) {
+    this.paydesc = paydesc;
+  }
+
+  public Boolean getCheckable() {
+    return checkable;
+  }
+
+  public void setCheckable(Boolean checkable) {
+    this.checkable = checkable;
+  }
+
+  public Boolean getReversable() {
+    return reversable;
+  }
+
+  public void setReversable(Boolean reversable) {
+    this.reversable = reversable;
+  }
+
+  public String getTenantid() {
+    return tenantid;
+  }
+
+  public void setTenantid(String tenantid) {
+    this.tenantid = tenantid;
+  }
+
+  @Override
+  public String getDictKey() {
+    return this.sourceType;
+  }
+
+  @Override
+  public Object getDictValue() {
+    return this.paydesc;
+  }
+
+  public String getSourceTypeId() {
+    return sourceTypeId;
+  }
+
+  public void setSourceTypeId(String sourceTypeId) {
+    this.sourceTypeId = sourceTypeId;
+  }
+
+  public @NotNull String getPaySubjno() {
+    return paySubjno;
+  }
+
+  public void setPaySubjno(@NotNull String paySubjno) {
+    this.paySubjno = paySubjno;
+  }
+
+  public String getDepositeSubjno() {
+    return depositeSubjno;
+  }
+
+  public void setDepositeSubjno(String depositeSubjno) {
+    this.depositeSubjno = depositeSubjno;
+  }
+
+  public Integer getTplusN() {
+    return tplusN;
+  }
+
+  public void setTplusN(Integer tplusN) {
+    this.tplusN = tplusN;
+  }
+
+  public String getStartChktime() {
+    return startChktime;
+  }
+
+  public void setStartChktime(String startChktime) {
+    this.startChktime = startChktime;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/domain/TSourceTypeCheckStatus.java b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TSourceTypeCheckStatus.java
new file mode 100644
index 0000000..e055f3c
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TSourceTypeCheckStatus.java
@@ -0,0 +1,166 @@
+package com.supwisdom.dlpay.api.domain;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+import java.sql.Timestamp;
+
+@Entity
+@Table(name = "tb_sourcetype_check",
+    indexes = {@Index(name = "sourcetype_check_idx", columnList = "sourcetype, tenantid", unique = true)})
+public class TSourceTypeCheckStatus {
+  @Id
+  @SequenceGenerator(name = "st_checker_id", sequenceName = "SEQ_SOURCETYPE_CHECK", allocationSize = 1)
+  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "st_checker_id")
+  @Column(name = "ID", nullable = false, length = 9)
+  private Integer id;
+
+  @Column(name = "sourcetype", length = 30)
+  @NotNull
+  private String sourceType;
+
+  @Column(name = "start_accdate", length = 8)
+  @NotNull
+  private String startAccdate; // 对账起始日期
+
+  @Column(name = "check_accdate", length = 8)
+  @NotNull
+  private String checkAccdate; // 当前对账记账日期
+
+  @Column(name = "check_file_status")
+  private Boolean checkFileOk; // 当前对账文件下载成功标志
+
+  @Column(name = "check_status")
+  @NotNull
+  private Boolean checkStatus; // 当前对账完成状态
+
+  @Column(name = "repair_status")
+  private Boolean repairStatus; // 补帐完成状态
+
+  @Column(name = "settle_status")
+  @NotNull
+  private Boolean settleStatus; // 是否可以结算
+
+  @Column(name = "force_recheck")
+  @NotNull
+  private Boolean forceRecheck; // 是否对当前日期强制对账
+
+  @Column(name = "last_update")
+  @NotNull
+  @Version
+  private Timestamp lastUpdate;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantId;
+
+  @Column(name = "chkfile_url", length = 400)
+  private String chkfileUrl;
+
+  @Column(name = "remark", length = 600)
+  private String remark;
+
+  public Integer getId() {
+    return id;
+  }
+
+  public void setId(Integer id) {
+    this.id = id;
+  }
+
+  public String getSourceType() {
+    return sourceType;
+  }
+
+  public void setSourceType(String sourceType) {
+    this.sourceType = sourceType;
+  }
+
+  public String getCheckAccdate() {
+    return checkAccdate;
+  }
+
+  public void setCheckAccdate(String checkAccdate) {
+    this.checkAccdate = checkAccdate;
+  }
+
+  public Boolean getForceRecheck() {
+    return forceRecheck;
+  }
+
+  public void setForceRecheck(Boolean forceRecheck) {
+    this.forceRecheck = forceRecheck;
+  }
+
+  public Timestamp getLastUpdate() {
+    return lastUpdate;
+  }
+
+  public void setLastUpdate(Timestamp lastUpdate) {
+    this.lastUpdate = lastUpdate;
+  }
+
+  public String getTenantId() {
+    return tenantId;
+  }
+
+  public void setTenantId(String tenantId) {
+    this.tenantId = tenantId;
+  }
+
+  public String getStartAccdate() {
+    return startAccdate;
+  }
+
+  public void setStartAccdate(String startAccdate) {
+    this.startAccdate = startAccdate;
+  }
+
+  public Boolean getCheckFileOk() {
+    return checkFileOk;
+  }
+
+  public void setCheckFileOk(Boolean checkFileOk) {
+    this.checkFileOk = checkFileOk;
+  }
+
+  public Boolean getCheckStatus() {
+    return checkStatus;
+  }
+
+  public void setCheckStatus(Boolean checkStatus) {
+    this.checkStatus = checkStatus;
+  }
+
+  public String getChkfileUrl() {
+    return chkfileUrl;
+  }
+
+  public void setChkfileUrl(String chkfileUrl) {
+    this.chkfileUrl = chkfileUrl;
+  }
+
+  public String getRemark() {
+    return remark;
+  }
+
+  public void setRemark(String remark) {
+    this.remark = remark;
+  }
+
+  public Boolean getSettleStatus() {
+    return settleStatus;
+  }
+
+  public void setSettleStatus(Boolean settleStatus) {
+    this.settleStatus = settleStatus;
+  }
+
+  public Boolean getRepairStatus() {
+    return repairStatus;
+  }
+
+  public void setRepairStatus(Boolean repairStatus) {
+    this.repairStatus = repairStatus;
+  }
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/domain/TSourceTypeConfig.java b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TSourceTypeConfig.java
new file mode 100644
index 0000000..64f8131
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TSourceTypeConfig.java
@@ -0,0 +1,97 @@
+package com.supwisdom.dlpay.api.domain;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * Created by shuwei on 2019/4/9.
+ */
+@Entity
+@Table(name = "TB_SOURCETYPE_CONFIG",
+    indexes = {@Index(name = "source_type_config_idx", columnList = "sourcetype, configid, tenantid", unique = true)})
+public class TSourceTypeConfig implements Serializable {
+  private static final long serialVersionUID = 9020235338229267313L;
+  @Id
+  @SequenceGenerator(name = "sourcetype_cfg_id", sequenceName = "SEQ_SOURCETYPE_CONFIG", allocationSize = 1, initialValue = 1000)
+  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sourcetype_cfg_id")
+  @Column(name = "id", length = 32)
+  private String id;
+
+  @Column(name = "SOURCETYPE", length = 20)
+  @NotNull
+  private String sourceType;
+
+  @Column(name = "CONFIGID", length = 40)
+  @NotNull
+  private String configid;
+
+  @Column(name = "CONFIG_VALUE", length = 2000)
+  private String configValue;
+
+  @Column(name = "CONFIG_NAME", length = 200)
+  private String configName;
+
+  @Column(name = "GLOBALFLAG")
+  private Boolean globalflag = false;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantid = "";
+
+  public String getSourceType() {
+    return sourceType;
+  }
+
+  public void setSourceType(String sourceType) {
+    this.sourceType = sourceType;
+  }
+
+  public String getConfigid() {
+    return configid;
+  }
+
+  public void setConfigid(String configid) {
+    this.configid = configid;
+  }
+
+  public String getConfigValue() {
+    return configValue;
+  }
+
+  public void setConfigValue(String configValue) {
+    this.configValue = configValue;
+  }
+
+  public String getConfigName() {
+    return configName;
+  }
+
+  public void setConfigName(String configName) {
+    this.configName = configName;
+  }
+
+  public String getTenantid() {
+    return tenantid;
+  }
+
+  public void setTenantid(String tenantid) {
+    this.tenantid = tenantid;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  public void setId(String id) {
+    this.id = id;
+  }
+
+  public Boolean getGlobalflag() {
+    return globalflag;
+  }
+
+  public void setGlobalflag(Boolean globalflag) {
+    this.globalflag = globalflag;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/domain/TSubjectdtl.java b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TSubjectdtl.java
new file mode 100644
index 0000000..eaa385c
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TSubjectdtl.java
@@ -0,0 +1,185 @@
+package com.supwisdom.dlpay.api.domain;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+
+@Entity
+@Table(name = "TB_SUBJECTDTL",
+    indexes = {@Index(name = "subjdtl_accdate", columnList = "accdate"),
+        @Index(name = "subjdtl_subjno", columnList = "subjno")})
+public class TSubjectdtl {
+  @Id
+  @Column(name = "refno", length = 32, nullable = false)
+  private String refno;
+
+  @Column(name = "accdate", length = 8)
+  @NotNull
+  private String accdate;
+
+  @Column(name = "subjno", length = 10)
+  @NotNull
+  private String subjectno;
+
+  @Column(name = "subjname", length = 200)
+  private String subjectName;
+
+  @Column(name = "amount", scale = 2, precision = 15)
+  @NotNull
+  private Double amount;
+
+  @Column(name = "tradecode", precision = 8)
+  @NotNull
+  private Integer tradeCode;
+
+  @Column(name = "transdate", length = 8)
+  @NotNull
+  private String transDate;
+
+  @Column(name = "transtime", length = 6)
+  @NotNull
+  private String transTime;
+
+  @Column(name = "paytype", length = 20)
+  private String payType;
+
+  @Column(name = "payinfo", length = 200)
+  private String payInfo;
+
+  @Column(name = "OPPOSITEACCNO", length = 20)
+  private String oppositeAccNo;
+
+  @Column(name = "OPPOSITEACCNAME", length = 200)
+  private String oppositeAccName;
+
+  @Column(name = "status", length = 20)
+  @NotNull
+  private String status;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantid = "";
+
+  @Column(name = "DTLTYPE", length = 20)
+  @NotNull
+  private String dtltype;
+
+  public String getRefno() {
+    return refno;
+  }
+
+  public void setRefno(String refno) {
+    this.refno = refno;
+  }
+
+  public String getAccdate() {
+    return accdate;
+  }
+
+  public void setAccdate(String accdate) {
+    this.accdate = accdate;
+  }
+
+  public String getSubjectno() {
+    return subjectno;
+  }
+
+  public void setSubjectno(String subjectno) {
+    this.subjectno = subjectno;
+  }
+
+  public String getSubjectName() {
+    return subjectName;
+  }
+
+  public void setSubjectName(String subjectName) {
+    this.subjectName = subjectName;
+  }
+
+  public Double getAmount() {
+    return amount;
+  }
+
+  public void setAmount(Double amount) {
+    this.amount = amount;
+  }
+
+  public String getTransDate() {
+    return transDate;
+  }
+
+  public void setTransDate(String transDate) {
+    this.transDate = transDate;
+  }
+
+  public String getTransTime() {
+    return transTime;
+  }
+
+  public void setTransTime(String transTime) {
+    this.transTime = transTime;
+  }
+
+  public String getPayType() {
+    return payType;
+  }
+
+  public void setPayType(String payType) {
+    this.payType = payType;
+  }
+
+  public String getPayInfo() {
+    return payInfo;
+  }
+
+  public void setPayInfo(String payInfo) {
+    this.payInfo = payInfo;
+  }
+
+  public String getOppositeAccNo() {
+    return oppositeAccNo;
+  }
+
+  public void setOppositeAccNo(String oppositeAccNo) {
+    this.oppositeAccNo = oppositeAccNo;
+  }
+
+  public String getOppositeAccName() {
+    return oppositeAccName;
+  }
+
+  public void setOppositeAccName(String oppositeAccName) {
+    this.oppositeAccName = oppositeAccName;
+  }
+
+  public String getStatus() {
+    return status;
+  }
+
+  public void setStatus(String status) {
+    this.status = status;
+  }
+
+  public Integer getTradeCode() {
+    return tradeCode;
+  }
+
+  public void setTradeCode(Integer tradeCode) {
+    this.tradeCode = tradeCode;
+  }
+
+  public String getTenantid() {
+    return tenantid;
+  }
+
+  public void setTenantid(String tenantid) {
+    this.tenantid = tenantid;
+  }
+
+  public String getDtltype() {
+    return dtltype;
+  }
+
+  public void setDtltype(String dtltype) {
+    this.dtltype = dtltype;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/domain/TTransactionMain.java b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TTransactionMain.java
new file mode 100644
index 0000000..06a4184
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/domain/TTransactionMain.java
@@ -0,0 +1,394 @@
+package com.supwisdom.dlpay.api.domain;
+
+import com.supwisdom.dlpay.framework.util.Subject;
+import com.supwisdom.dlpay.framework.util.TradeDict;
+
+import javax.persistence.*;
+import javax.validation.constraints.Digits;
+import javax.validation.constraints.NotNull;
+import java.sql.Timestamp;
+import java.util.List;
+
+import static javax.persistence.FetchType.LAZY;
+
+@Entity
+@Table(name = "TB_TRANSACTIONMAIN",
+    indexes = {@Index(name = "transmain_accdate", columnList = "accdate"),
+        @Index(name = "transmain_status", columnList = "status"),
+        @Index(name = "transmain_tenantid_idx", columnList = "tenantid"),
+        @Index(name = "transmain_outtrade", unique = true, columnList = "outid, outtradeno")})
+@SequenceGenerator(name = "seq_refno", allocationSize = 100)
+public class TTransactionMain {
+  @Id
+  @Column(name = "refno", nullable = false, length = 32)
+  private String refno;
+
+  @Column(name = "accdate", length = 8)
+  @NotNull
+  @Digits(integer = 8, fraction = 0)
+  private String accdate;
+
+  @Column(name = "checkable")
+  @NotNull
+  private Boolean checkable; // 是否需要清算的交易
+
+  @Column(name = "checkdate", length = 8)
+  private String checkDate;
+
+  @Column(name = "transcode")
+  @NotNull
+  private Integer transCode;
+
+  @Column(name = "person")
+  @NotNull
+  private Boolean person = false;
+
+  @Column(name = "shop")
+  @NotNull
+  private Boolean shop = false;
+
+  @Column(name = "subject")
+  @NotNull
+  private Boolean subject = false;
+
+  @Column(name = "status", length = 20)
+  @NotNull
+  private String status = TradeDict.DTL_STATUS_NONE;
+
+  @Column(name = "sourcetype", length = 20)
+  private String sourceType = "";
+
+  @Column(name = "SOURCETYPE_REFNO", length = 32)
+  private String sourceTypeRefno;
+
+  @Column(name = "outtradeno", length = 60)
+  private String outTradeNo = "";
+
+  @Column(name = "outid", length = 60)
+  private String outId;
+
+  @Column(name = "OPERID", precision = 9)
+  private String operid; //操作员ID
+
+  @Column(name = "OPERTYPE", length = 10)
+  private String opertype; // person - 个人, shop - 商户, operator - 操作员
+
+  @Column(name = "settledate", length = 8)
+  private String settleDate;
+
+  @Column(name = "create_time")
+  @NotNull
+  private Timestamp createTime;
+
+  @Column(name = "end_time")
+  private Timestamp endTime;
+
+  @Column(name = "update_time")
+  @Version
+  private Timestamp updateTime;
+
+  @Column(name = "reverse_type", nullable = false, length = 10)
+  @NotNull
+  private String reverseType = TradeDict.REVERSE_FLAG_NONE; // 流水标识, none - 正常交易流水, cancel - 撤销流水, refund - 退款流水
+
+  // 撤销、退款原流水参考号
+  @Column(name = "reverse_refno")
+  private String reverseRefno = "";
+
+  @Column(name = "reverse_flag", nullable = false, length = 10)
+  @NotNull
+  private String reverseFlag = TradeDict.REVERSE_FLAG_NONE; // 冲正标识, none - 未冲正, refund - 被退款, cancel - 被冲正
+
+  @Column(name = "refund_amount", nullable = false)
+  private Double refundAmount = 0.0;
+
+  @OneToOne(targetEntity = TPersondtl.class, fetch = LAZY, cascade = CascadeType.ALL)
+  @JoinColumn(name = "refno", referencedColumnName = "refno")
+  private TPersondtl personDtl;
+
+  @OneToOne(targetEntity = TShopdtl.class, fetch = LAZY, cascade = CascadeType.ALL)
+  @JoinColumn(name = "refno", referencedColumnName = "refno")
+  private TShopdtl shopDtl;
+
+  @OneToOne(targetEntity = TSubjectdtl.class, fetch = LAZY, cascade = CascadeType.ALL)
+  @JoinColumn(name = "refno", referencedColumnName = "refno")
+  private TSubjectdtl subjectDtl;
+
+  @OneToMany(targetEntity = TDebitCreditDtl.class, fetch = LAZY, cascade = CascadeType.ALL)
+  @JoinColumn(name = "refno", referencedColumnName = "refno")
+  private List<TDebitCreditDtl> details;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantid = "";
+
+  @Column(name = "DTLTYPE", length = 20)
+  @NotNull
+  private String dtltype;
+
+  public String getDtltype() {
+    return dtltype;
+  }
+
+  public void setDtltype(String dtltype) {
+    this.dtltype = dtltype;
+  }
+
+  public String getRefno() {
+    return refno;
+  }
+
+  public void setRefno(String refno) {
+    this.refno = refno;
+  }
+
+  public String getAccdate() {
+    return accdate;
+  }
+
+  public void setAccdate(String accdate) {
+    this.accdate = accdate;
+  }
+
+
+  public Boolean getPerson() {
+    return person;
+  }
+
+  public void setPerson(Boolean person) {
+    this.person = person;
+  }
+
+  public Boolean getShop() {
+    return shop;
+  }
+
+  public void setShop(Boolean shop) {
+    this.shop = shop;
+  }
+
+  public Boolean getSubject() {
+    return subject;
+  }
+
+  public void setSubject(Boolean subject) {
+    this.subject = subject;
+  }
+
+  public String getStatus() {
+    return status;
+  }
+
+  public void setStatus(String status) {
+    this.status = status;
+  }
+
+  public String getOutTradeNo() {
+    return outTradeNo;
+  }
+
+  public void setOutTradeNo(String outTradeNo) {
+    this.outTradeNo = outTradeNo;
+  }
+
+  public String getOutId() {
+    return outId;
+  }
+
+  public void setOutId(String outId) {
+    this.outId = outId;
+  }
+
+  public String getReverseFlag() {
+    return reverseFlag;
+  }
+
+  public void setReverseFlag(String reverseFlag) {
+    this.reverseFlag = reverseFlag;
+  }
+
+  public TPersondtl getPersonDtl() {
+    return personDtl;
+  }
+
+  public TShopdtl getShopDtl() {
+    return shopDtl;
+  }
+
+  public TSubjectdtl getSubjectDtl() {
+    return subjectDtl;
+  }
+
+  public List<TDebitCreditDtl> getDetails() {
+    return details;
+  }
+
+  public void setPersonDtl(TPersondtl personDtl) {
+    this.personDtl = personDtl;
+  }
+
+  public void setShopDtl(TShopdtl shopDtl) {
+    this.shopDtl = shopDtl;
+  }
+
+  public void setSubjectDtl(TSubjectdtl subjectDtl) {
+    this.subjectDtl = subjectDtl;
+  }
+
+  public void setDetails(List<TDebitCreditDtl> details) {
+    this.details = details;
+  }
+
+  public Timestamp getCreateTime() {
+    return createTime;
+  }
+
+  public void setCreateTime(Timestamp createTime) {
+    this.createTime = createTime;
+  }
+
+  public Timestamp getEndTime() {
+    return endTime;
+  }
+
+  public void setEndTime(Timestamp endTime) {
+    this.endTime = endTime;
+  }
+
+  public String getOperid() {
+    return operid;
+  }
+
+  public void setOperid(String operid) {
+    this.operid = operid;
+  }
+
+  public String getOpertype() {
+    return opertype;
+  }
+
+  public void setOpertype(String opertype) {
+    this.opertype = opertype;
+  }
+
+  public String getCheckDate() {
+    return checkDate;
+  }
+
+  public void setCheckDate(String checkDate) {
+    this.checkDate = checkDate;
+  }
+
+  public Boolean getCheckable() {
+    return checkable;
+  }
+
+  public void setCheckable(Boolean checkable) {
+    this.checkable = checkable;
+  }
+
+  public String getSettleDate() {
+    return settleDate;
+  }
+
+  public void setSettleDate(String settleDate) {
+    this.settleDate = settleDate;
+  }
+
+  public String getSourceType() {
+    return sourceType;
+  }
+
+  public void setSourceType(String sourceType) {
+    this.sourceType = sourceType;
+  }
+
+  public String getSourceTypeRefno() {
+    return sourceTypeRefno;
+  }
+
+  public void setSourceTypeRefno(String sourceTypeRefno) {
+    this.sourceTypeRefno = sourceTypeRefno;
+  }
+
+  public Integer getTransCode() {
+    return transCode;
+  }
+
+  public void setTransCode(Integer transCode) {
+    this.transCode = transCode;
+  }
+
+  public String getReverseType() {
+    return reverseType;
+  }
+
+  public void setReverseType(String reverseType) {
+    this.reverseType = reverseType;
+  }
+
+  public String getReverseRefno() {
+    return reverseRefno;
+  }
+
+  public void setReverseRefno(String reverseRefno) {
+    this.reverseRefno = reverseRefno;
+  }
+
+  public Double getRefundAmount() {
+    return refundAmount;
+  }
+
+  public void setRefundAmount(Double refundAmount) {
+    this.refundAmount = refundAmount;
+  }
+
+  public Double sumAmountByAccno(String accno, String subjno,
+                                 String debitOrCredit) {
+    Double debitSum = 0.0;
+    Double creditSum = 0.0;
+    for (TDebitCreditDtl dtl : getDetails()) {
+      if (dtl.getDraccno().equals(accno) && dtl.getDrsubjno().equals(subjno)) {
+        debitSum += dtl.getAmount();
+      }
+      if (dtl.getCraccno().equals(accno) && dtl.getCrsubjno().equals(subjno)) {
+        creditSum += dtl.getAmount();
+      }
+    }
+    if (Subject.SUBJNO_MACHANT_INCOME.equals(subjno)
+        || Subject.SUBJNO_PERSONAL_DEPOSIT.equals(subjno)) {
+      if ("debit".equals(debitOrCredit)) {
+        return -debitSum;
+      } else if ("credit".equals(debitOrCredit)) {
+        return creditSum;
+      } else {
+        return creditSum - debitSum;
+      }
+    } else if (subjno.startsWith("1")) {
+      if ("debit".equals(debitOrCredit)) {
+        return debitSum;
+      } else if ("credit".equals(debitOrCredit)) {
+        return -creditSum;
+      } else {
+        return debitSum - creditSum;
+      }
+    }
+    return 0.0;
+  }
+
+  public String getTenantid() {
+    return tenantid;
+  }
+
+  public void setTenantid(String tenantid) {
+    this.tenantid = tenantid;
+  }
+
+  public Timestamp getUpdateTime() {
+    return updateTime;
+  }
+
+  public void setUpdateTime(Timestamp updateTime) {
+    this.updateTime = updateTime;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/service/SourceTypeService.java b/backend/src/main/java/com/supwisdom/dlpay/api/service/SourceTypeService.java
new file mode 100644
index 0000000..930f861
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/service/SourceTypeService.java
@@ -0,0 +1,25 @@
+package com.supwisdom.dlpay.api.service;
+
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Map;
+
+/**
+ * Created by shuwei on 2019/4/9.
+ */
+public interface SourceTypeService {
+
+  /**
+   * 获取支付能力充值参数全局配置
+   */
+  @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class, readOnly = true)
+  Map<String, String> getChargePaytypeConfig(String paytype, boolean ignoreStatus) throws Exception;
+
+  /**
+   * 获取商户支付能力消费参数配置
+   */
+  @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class, readOnly = true)
+  Map<String, String> getConsumePaytypeConfig(String paytype, String shopaccno, boolean anonymousflag, boolean ignoreStatus) throws Exception;
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/service/impl/SourceTypeServiceImpl.java b/backend/src/main/java/com/supwisdom/dlpay/api/service/impl/SourceTypeServiceImpl.java
new file mode 100644
index 0000000..8871526
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/service/impl/SourceTypeServiceImpl.java
@@ -0,0 +1,106 @@
+package com.supwisdom.dlpay.api.service.impl;
+
+import com.supwisdom.dlpay.api.dao.*;
+import com.supwisdom.dlpay.api.domain.*;
+import com.supwisdom.dlpay.api.service.SourceTypeService;
+import com.supwisdom.dlpay.exception.TransactionProcessException;
+import com.supwisdom.dlpay.framework.tenant.TenantContext;
+import com.supwisdom.dlpay.framework.util.StringUtil;
+import com.supwisdom.dlpay.framework.util.TradeErrorCode;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Created by shuwei on 2019/4/9.
+ */
+@Service
+public class SourceTypeServiceImpl implements SourceTypeService {
+  private final SourceTypeDao sourceTypeDao;
+  private final SourceTypeConfigDao sourceTypeConfigDao;
+  private final ShopSourceTypeDao shopSourceTypeDao;
+  private final ShopSourceTypeConfigDao shopSourceTypeConfigDao;
+  private final SourceTypeCheckDao sourceTypeCheckDao;
+
+  @Autowired
+  public SourceTypeServiceImpl(SourceTypeDao sourceTypeDao, SourceTypeConfigDao sourceTypeConfigDao,
+                               ShopSourceTypeDao shopSourceTypeDao, ShopSourceTypeConfigDao shopSourceTypeConfigDao, SourceTypeCheckDao sourceTypeCheckDao) {
+    this.sourceTypeDao = sourceTypeDao;
+    this.sourceTypeConfigDao = sourceTypeConfigDao;
+    this.shopSourceTypeDao = shopSourceTypeDao;
+    this.shopSourceTypeConfigDao = shopSourceTypeConfigDao;
+    this.sourceTypeCheckDao = sourceTypeCheckDao;
+  }
+
+
+  @Override
+  public Map<String, String> getChargePaytypeConfig(String paytype, boolean ignoreStatus) throws Exception {
+    TSourceType tSourceType = sourceTypeDao.getBySourceType(paytype);
+    if (null == tSourceType) {
+      throw new TransactionProcessException(TradeErrorCode.INPUT_DATA_ERROR, "系统不支持支付方式[" + paytype + "]");
+    } else if (!ignoreStatus && (!tSourceType.getEnable() || !tSourceType.getChargeEnable())) {
+      throw new TransactionProcessException(TradeErrorCode.INPUT_DATA_ERROR, "系统充值未启用支付方式[" + paytype + "]");
+    }
+
+    Map<String, String> result = new HashMap<>(0);
+    List<TSourceTypeConfig> list = sourceTypeConfigDao.getBySourceType(paytype);
+    if (!StringUtil.isEmpty(list)) {
+      for (TSourceTypeConfig config : list) {
+        result.put(config.getConfigid(), config.getConfigValue());
+      }
+    }
+    return result;
+  }
+
+  @Override
+  public Map<String, String> getConsumePaytypeConfig(String paytype, String shopaccno, boolean anonymousflag, boolean ignoreStatus) throws Exception {
+    //step1: 判断系统支付能力是否启用
+    TSourceType tSourceType = sourceTypeDao.getBySourceType(paytype);
+    if (null == tSourceType) {
+      throw new TransactionProcessException(TradeErrorCode.INPUT_DATA_ERROR, "系统不支持支付方式[" + paytype + "]");
+    } else {
+      if (!ignoreStatus && (!tSourceType.getEnable() || !tSourceType.getConsumeEnable())) {
+        throw new TransactionProcessException(TradeErrorCode.INPUT_DATA_ERROR, "系统未启用支付方式[" + paytype + "]消费");
+      }
+      if (!ignoreStatus && anonymousflag && !tSourceType.getAnonymousEnable()) {
+        throw new TransactionProcessException(TradeErrorCode.INPUT_DATA_ERROR, "系统支付方式[" + paytype + "]未启用匿名消费");
+      }
+    }
+
+    //step2: 判断商户支付能力是否启用
+    TShopSourceType tShopSourceType = shopSourceTypeDao.getById(paytype, shopaccno);
+    if (null == tShopSourceType) {
+      throw new TransactionProcessException(TradeErrorCode.INPUT_DATA_ERROR, "该商户[" + shopaccno + "]未启用支付方式[" + paytype + "]");
+    } else {
+      if (!ignoreStatus && !tShopSourceType.getConsumeEnable()) {
+        throw new TransactionProcessException(TradeErrorCode.INPUT_DATA_ERROR, "该商户[" + shopaccno + "]未启用支付方式[" + paytype + "]消费");
+      }
+      if (!ignoreStatus && anonymousflag && !tShopSourceType.getAnonymousEnable()) {
+        throw new TransactionProcessException(TradeErrorCode.INPUT_DATA_ERROR, "该商户[" + shopaccno + "]的支付方式[" + paytype + "]未启用匿名消费");
+      }
+    }
+
+    Map<String, String> result = new HashMap<>(0);
+    List<TSourceTypeConfig> list = sourceTypeConfigDao.getBySourceType(paytype);
+    if (!StringUtil.isEmpty(list)) {
+      for (TSourceTypeConfig config : list) {
+        if (config.getGlobalflag()) {
+          result.put(config.getConfigid(), config.getConfigValue()); //统用参数
+        }
+      }
+    }
+
+    List<TShopSourceTypeConfig> shoplist = shopSourceTypeConfigDao.getShopSourceTypeConfigs(paytype, shopaccno);
+    if (!StringUtil.isEmpty(shoplist)) {
+      for (TShopSourceTypeConfig shopconfig : shoplist) {
+        result.put(shopconfig.getConfigid(), shopconfig.getConfigValue()); //个性参数
+      }
+    }
+
+    return result;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/types/IDTypes.java b/backend/src/main/java/com/supwisdom/dlpay/api/types/IDTypes.java
new file mode 100644
index 0000000..92cc8d7
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/types/IDTypes.java
@@ -0,0 +1,65 @@
+package com.supwisdom.dlpay.api.types;
+
+public enum IDTypes {
+  IDCARD(52, "idcard"), // 身份证
+  RESIDENCE_BOOKLET(55, "residence_booklet"), //户口簿
+  PASSPORT(70, "passport"), // 护照
+  HK_MACAU_PASS(76, "hk_macau_pass"), // 港澳居民来往内地通行证
+  TAIWAN_PASS(67, "taiwan_pass"), // 台湾同胞来往内地通行证
+  FOREIGNER_RESIDENCE_PERMIT(68, "foreigner_residence_permit"), // 外国人居留证
+  MILITARY_IDCARD(58, "military_idcard"), //  军官证
+  SOLDIER_IDCARD(64, "soldier_idcard"), //  士兵证
+  DRIVING_LICENSE(-97, "driving_license"), // 驾照
+  STUDENT_IDCARD(-98, "student_idcard"), // 学生证
+  OTHERS(-99, "unknown"),
+  IDTYPE_LIST(-100, new IDTypes[]{IDCARD, RESIDENCE_BOOKLET, PASSPORT, HK_MACAU_PASS, TAIWAN_PASS, FOREIGNER_RESIDENCE_PERMIT, MILITARY_IDCARD, SOLDIER_IDCARD,
+      DRIVING_LICENSE, STUDENT_IDCARD, OTHERS}); // 其它
+
+
+  IDTypes(int id, Object data) {
+    this.id = id;
+    this.data = data;
+  }
+
+  public String value() {
+    return this.data.toString();
+  }
+
+  public int getId() {
+    return this.id;
+  }
+
+  public Object getData() {
+    return this.data;
+  }
+
+
+  private Object data;
+
+  private int id;
+
+  public static String findById(int id) {
+    IDTypes[] list = (IDTypes[]) IDTypes.IDTYPE_LIST.getData();
+    for (IDTypes item : list) {
+      if (item.getId() == id) {
+        return item.value();
+      }
+    }
+    return null;
+  }
+
+  public static int findByValue(String idtype) {
+    IDTypes[] list = (IDTypes[]) IDTypes.IDTYPE_LIST.getData();
+    for (IDTypes item : list) {
+      if (item.value().equals(idtype)) {
+        return item.getId();
+      }
+    }
+    return -999;
+  }
+
+  @Override
+  public String toString() {
+    return value();
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/util/DateUtil.java b/backend/src/main/java/com/supwisdom/dlpay/api/util/DateUtil.java
new file mode 100644
index 0000000..2e45a7e
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/util/DateUtil.java
@@ -0,0 +1,345 @@
+package com.supwisdom.dlpay.api.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.Timestamp;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.TimeZone;
+
+public class DateUtil {
+  private static final Logger logger = LoggerFactory.getLogger(DateUtil.class);
+  public static final String DATE_FMT = "yyyyMMdd";
+  public static final String TIME_FMT = "HHmmss";
+  public static final String DATETIME_FMT = "yyyyMMddHHmmss";
+
+  /**
+   * Description: 返回一个当前时间 @return String 格式:yyyyMMddHHmmss @exception Modify
+   * History:
+   */
+  public static String getNow() {
+    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+    return sdf.format(new Date());
+  }
+
+
+  /**
+   * Description: 根据类型返回一个当前时间 @param partten String @return String 格式:partten
+   */
+  public static String getNow(String partten) {
+    SimpleDateFormat sdf = new SimpleDateFormat(partten);
+    return sdf.format(new Date());
+  }
+
+  /**
+   * Description: 得到一个特殊的时间 @param startTime String 格式:yyyyMMddHHmmss @param
+   * interval int 秒 @return String 格式:partten @exception Modify History:
+   */
+  public static String getNewTime(String startTime, int interval) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+      Date d = sdf.parse(startTime);
+      Calendar calendar = Calendar.getInstance();
+      calendar.setTimeInMillis(d.getTime());
+      calendar.add(Calendar.SECOND, interval);
+      return sdf.format(calendar.getTime());
+    } catch (ParseException e) {
+      return startTime;
+    }
+  }
+
+  /**
+   * Description: 得到一个特殊的时间 @param startTime String 格式:partten @param
+   * interval int 秒 @return String 格式:partten @exception Modify History:
+   */
+  public static String getNewTime(String startTime, int interval, String partten) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat(partten);
+      Date d = sdf.parse(startTime);
+      Calendar calendar = Calendar.getInstance();
+      calendar.setTimeInMillis(d.getTime());
+      calendar.add(Calendar.SECOND, interval);
+      return sdf.format(calendar.getTime());
+    } catch (ParseException e) {
+      return startTime;
+    }
+  }
+
+  public static String getNewDay(String startDay, int intervalday) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
+      Date d = sdf.parse(startDay);
+      Calendar calendar = Calendar.getInstance();
+      calendar.setTimeInMillis(d.getTime());
+      calendar.add(Calendar.DATE, intervalday);
+      return sdf.format(calendar.getTime());
+    } catch (ParseException e) {
+      return startDay;
+    }
+  }
+
+  /**
+   * 得到两个日期相差的天数 格式 yyyyMMdd @return diffdays = secondDay - firstDay
+   */
+  public static long getIntervalDay(String firstDay, String secondDay) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
+      Date f = sdf.parse(firstDay);
+      Date s = sdf.parse(secondDay);
+      long time = s.getTime() - f.getTime();
+      return time / (24 * 60 * 60 * 1000);
+    } catch (ParseException e) {
+      return 0;
+    }
+  }
+
+  /**
+   * Description: 比较两个时间字符串的前后关系 @param firstTime String 格式:yyyyMMddHHmmss
+   *
+   * @param secondTime String 格式: yyyyMMddHHmmss @return int |
+   *                   firstTime=second int=0 | firstTime>secondTime int>0 |
+   *                   firstTime<secondTime int<0 @exception Modify History:
+   */
+  public static int compareDatetime(String firstTime, String secondTime) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+      Date f = sdf.parse(firstTime);
+      Date s = sdf.parse(secondTime);
+      return f.compareTo(s);
+    } catch (ParseException e) {
+      return 0;
+    }
+  }
+
+  /**
+   * Description: 比较两个时间字符串的前后关系 @param firstTime String 格式:pattern
+   *
+   * @param secondTime String 格式: yyyyMMddHHmmss @return int |
+   *                   firstTime=second int=0 | firstTime>secondTime int>0 |
+   *                   firstTime<secondTime int<0 @exception Modify History:
+   */
+  public static int compareDatetime(String firstTime, String secondTime, String pattern) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat(pattern);
+      Date f = sdf.parse(firstTime);
+      Date s = sdf.parse(secondTime);
+      return f.compareTo(s);
+    } catch (ParseException e) {
+      return 0;
+    }
+  }
+
+  /**
+   * Description: 比较两个时间字符串的时间差 @param firstTime String 格式:yyyyMMddHHmmss
+   *
+   * @param secondTime String 格式: yyyyMMddHHmmss @param second int 格式 @return
+   *                   int | firstTime+seconds=secondTime int=0 | firstTime+seconds>secondTime
+   *                   int>0 | firstTime+seconds<secondTime int<0 @exception Modify History:
+   */
+  public static int compareDatetime(String firstTime, String secondTime, int seconds) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+      Date f = sdf.parse(firstTime);
+      Date s = sdf.parse(secondTime);
+      Calendar calendar = Calendar.getInstance();
+      calendar.setTimeInMillis(f.getTime());
+      calendar.add(Calendar.SECOND, seconds);
+      Date temp = calendar.getTime();
+      return temp.compareTo(s);
+    } catch (Exception e) {
+      return 0;
+    }
+  }
+
+  /**
+   * Description: 对time重新格式化
+   */
+  public static String reformatDatetime(String time, String fromPattern, String toPattern) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat(fromPattern);
+      Date d = sdf.parse(time);
+      Calendar calendar = Calendar.getInstance();
+      calendar.setTimeInMillis(d.getTime());
+      sdf = new SimpleDateFormat(toPattern);
+      return sdf.format(calendar.getTime());
+    } catch (Exception e) {
+      e.printStackTrace();
+      return time;
+    }
+  }
+
+  /**
+   * 获得两个字符串日期之间的时间差(单位毫秒) 格式 yyyyMMddHHmmss
+   */
+  public static long getInterval(String startTime, String endTime) {
+    long duration = 0;
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+      duration = sdf.parse(endTime).getTime() - sdf.parse(startTime).getTime();
+    } catch (ParseException e) {
+      logger.error("Hi guys,there is an error when you try to parse the date string");
+    }
+    return duration;
+  }
+
+  /**
+   * 获得两个字符串日期之间的时间差(单位毫秒)
+   */
+  public static long getIntervalTime(String startTime, String endTime, String pattern) {
+    long duration = 0;
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat(pattern);
+      duration = sdf.parse(endTime).getTime() - sdf.parse(startTime).getTime();
+    } catch (ParseException e) {
+      logger.error("Hi guys,there is an error when you try to parse the date string");
+    }
+    return duration;
+  }
+
+  /**
+   * 转换成日期格式
+   * 短格式:20140401 -> 2014-04-01
+   * 中格式:201404011200 -> 2014-04-01 12:00
+   * 长格式:20140401123025 -> 2014-04-01 12:30:25
+   **/
+  public static String parseToDateFormat(String str) {
+    switch (str.length()) {
+      case 8:
+        str = str.substring(0, 4) + "-" + str.substring(4, 6) + "-" + str.substring(6, 8);
+        break;
+      case 12:
+        str = str.substring(0, 4) + "-" + str.substring(4, 6) + "-" + str.substring(6, 8) + " " + str.substring(8, 10) + ":" + str.substring(10, 12);
+        break;
+      case 14:
+        str = str.substring(0, 4) + "-" + str.substring(4, 6) + "-" + str.substring(6, 8) + " " + str.substring(8, 10) + ":" + str.substring(10, 12) + ":" + str.substring(12, 14);
+        break;
+      default:
+        break;
+    }
+    return str;
+  }
+
+  /**
+   * 解日期格式
+   * 短格式:2014-04-01 -> 20140401
+   * 中格式:2014-04-01 12:00 -> 201404011200
+   * 长格式:2014-04-01 12:30:25 -> 20140401123025
+   **/
+  public static String unParseToDateFormat(String str) {
+    return str.replaceAll("-", "").replaceAll(" ", "").replaceAll(":", "");
+  }
+
+  /**
+   * 检验时间格式
+   */
+  public static boolean checkDatetimeValid(String datetime, String pattern) {
+    if (null == datetime) return false;
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat(pattern);
+      Date d = sdf.parse(datetime);
+      return datetime.trim().equals(sdf.format(d));
+    } catch (Exception e) {
+    }
+    return false;
+  }
+
+  /**
+   * 获取指定日期是星期几 格式 yyyyMMdd
+   * MON|TUE|WED|THU|FRI|SAT|SUN
+   * 1		2		3		4		5		6		7
+   */
+  public static int getWeekday(String datestr) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
+      Calendar calendar = Calendar.getInstance();
+      boolean isFirstSunday = (calendar.getFirstDayOfWeek() == Calendar.SUNDAY); //一周第一天是否为星期天
+      Date d = sdf.parse(datestr);
+      calendar.setTimeInMillis(d.getTime());
+      int weekDay = calendar.get(calendar.DAY_OF_WEEK);
+      if (isFirstSunday) {
+        weekDay = weekDay - 1;
+        if (weekDay == 0) {
+          weekDay = 7;
+        }
+      }
+      return weekDay;
+    } catch (Exception e) {
+      return -1;
+    }
+  }
+
+  /**
+   * 获取指定日期
+   */
+  public static Date getSpecifyDate(String datestr, String pattern) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat(pattern);
+      Date result = sdf.parse(datestr);
+      return result;
+    } catch (Exception e) {
+      return new Date();
+    }
+  }
+
+  public static Integer getLastDayOfMonth(Integer year, Integer month) {
+    Calendar cal = Calendar.getInstance();
+    cal.set(Calendar.YEAR, year);
+    cal.set(Calendar.MONTH, month - 1);
+    cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DATE));
+    String str = new SimpleDateFormat("yyyyMMdd ").format(cal.getTime()).toString();
+    Integer result = Integer.parseInt(str.substring(0, 4) + str.substring(4, 6) + str.substring(6, 8));
+    return result;
+  }
+
+  private static Date set(Date date, int calendarField, int amount) {
+    Calendar c = Calendar.getInstance();
+    c.setLenient(false);
+    c.setTime(date);
+    c.add(calendarField, amount);
+    return c.getTime();
+  }
+
+
+  public static Date setMinutes(Date date, int amount) {
+    return set(date, Calendar.MINUTE, amount);
+  }
+
+
+  public static long getNowSecond() {
+    Calendar calendar = Calendar.getInstance();
+    return calendar.getTimeInMillis() / 1000;
+  }
+
+
+  public static String getUTCTime(Long timeInMillisSecond) {
+    Calendar time = Calendar.getInstance();
+    SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
+    fmt.setTimeZone(TimeZone.getTimeZone("UTC"));
+    time.setTimeInMillis(timeInMillisSecond);
+    return fmt.format(time.getTime());
+  }
+
+  public static String getUTCTime() {
+    return getUTCTime(System.currentTimeMillis());
+  }
+
+  public static int compareDay(Timestamp d1, Timestamp d2) {
+    Calendar cd1 = Calendar.getInstance();
+    cd1.setTimeInMillis(d1.getTime());
+    Calendar cd2 = Calendar.getInstance();
+    cd2.setTimeInMillis(d2.getTime());
+
+    if (cd1.get(Calendar.YEAR) != cd2.get(Calendar.YEAR)) {
+      return cd1.compareTo(cd2);
+    }
+
+    return Integer.compare(cd1.get(Calendar.DAY_OF_YEAR), cd2.get(Calendar.DAY_OF_YEAR));
+  }
+
+  public static Boolean sameDay(Timestamp d1, Timestamp d2) {
+    return (compareDay(d1, d2) == 0);
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/util/HMACUtil.java b/backend/src/main/java/com/supwisdom/dlpay/api/util/HMACUtil.java
new file mode 100644
index 0000000..06d7277
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/util/HMACUtil.java
@@ -0,0 +1,17 @@
+package com.supwisdom.dlpay.api.util;
+
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.codec.digest.HmacAlgorithms;
+import org.apache.commons.codec.digest.HmacUtils;
+
+public class HMACUtil {
+  public static String sha256HMAC(String message, String secret) {
+    byte[] bytes = HmacUtils.getInitializedMac(HmacAlgorithms.HMAC_SHA_256, secret.getBytes())
+        .doFinal(message.getBytes());
+    return Hex.encodeHexString(bytes, true);
+  }
+
+  private static String byteArrayToHexString(byte[] bytes) {
+    return Hex.encodeHexString(bytes, true);
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/util/MobileNumberCheck.java b/backend/src/main/java/com/supwisdom/dlpay/api/util/MobileNumberCheck.java
new file mode 100644
index 0000000..4508bbf
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/util/MobileNumberCheck.java
@@ -0,0 +1,163 @@
+package com.supwisdom.dlpay.api.util;
+
+import java.util.regex.Pattern;
+
+public class MobileNumberCheck {
+
+  /**
+   * 座机电话格式验证
+   **/
+  private static final String PHONE_CALL_PATTERN = "^(?:\\(\\d{3,4}\\)|\\d{3,4}-)?\\d{7,8}(?:-\\d{1,4})?$";
+
+  /**
+   * 中国电信号码格式验证 手机段: 133,153,180,181,189,177,1700,173 新增段:199
+   **/
+  private static final String CHINA_TELECOM_PATTERN = "(?:^(?:\\+86)?1(?:33|53|7[37]|8[019]|99)\\d{8}$)|(?:^(?:\\+86)?1700\\d{7}$)";
+
+  /**
+   * 中国联通号码格式验证 手机段:130,131,132,155,156,185,186,145,176,1707,1708,1709,175 新增段:166,146
+   **/
+  private static final String CHINA_UNICOM_PATTERN = "(?:^(?:\\+86)?1(?:3[0-2]|4[56]|5[56]|66|7[56]|8[56])\\d{8}$)|(?:^(?:\\+86)?170[7-9]\\d{7}$)";
+  /**
+   * 简单手机号码校验,校验手机号码的长度和1开头
+   */
+  private static final String SIMPLE_PHONE_CHECK = "^(?:\\+86)?1\\d{10}$";
+  /**
+   * 中国移动号码格式验证
+   * 手机段:134,135,136,137,138,139,150,151,152,157,158,159,182,183,184
+   * ,187,188,147,178,1705 新增段:198,148
+   **/
+  private static final String CHINA_MOBILE_PATTERN = "(?:^(?:\\+86)?1(?:3[4-9]|4[78]|5[0-27-9]|7[8]|8[2-478]|98)\\d{8}$)|(?:^(?:\\+86)?1705\\d{7}$)";
+
+  /**
+   * 仅手机号格式校验
+   */
+  private static final String PHONE_PATTERN = new StringBuilder(300)
+      .append(CHINA_MOBILE_PATTERN).append("|")
+      .append(CHINA_TELECOM_PATTERN).append("|")
+      .append(CHINA_UNICOM_PATTERN).toString();
+
+  /**
+   * 手机和座机号格式校验
+   */
+  private static final String PHONE_TEL_PATTERN = new StringBuilder(350)
+      .append(PHONE_PATTERN).append("|").append("(")
+      .append(PHONE_CALL_PATTERN).append(")").toString();
+
+
+  /**
+   * 匹配多个号码以,、或空格隔开的格式,如 17750581369
+   * 13306061248、(596)3370653,17750581369,13306061248 (0596)3370653
+   *
+   * @param input
+   * @param separator 可以自己指定分隔符,如"、, "表示可以以顿号、逗号和空格分隔
+   * @return
+   */
+  public static boolean checkMultiPhone(String input, String separator) {
+    separator = escapeMetacharacterOfStr(separator);
+    String regex = "^(?!.+["
+        + separator
+        + "]$)(?:(?:(?:(?:\\(\\d{3,4}\\)|\\d{3,4}-)?\\d{7,8}(?:-\\d{1,4})?)|(?:1\\d{10}))(?:["
+        + separator + "]|$))+$";
+    return match(regex, input);
+  }
+
+  /**
+   * 转义字符串中的[]-^\元字符
+   *
+   * @param input
+   * @param separator
+   * @return
+   */
+  private static String escapeMetacharacterOfStr(String input) {
+    String regex = "[-^\\[\\]\\\\]";
+    return input.replaceAll(regex, "\\\\$0");
+  }
+
+  /**
+   * 仅手机号码校验
+   *
+   * @param input
+   * @return
+   */
+  public static boolean isPhone(String input) {
+    return match(PHONE_PATTERN, input);
+  }
+
+  /**
+   * 手机号或座机号校验
+   *
+   * @param input
+   * @return
+   */
+  public static boolean isPhoneOrTel(String input) {
+    System.out.println(PHONE_TEL_PATTERN);
+    return match(PHONE_TEL_PATTERN, input);
+  }
+
+  /**
+   * 验证电话号码的格式
+   *
+   * @param str 校验电话字符串
+   * @return 返回true, 否则为false
+   * @author LinBilin
+   */
+  public static boolean isPhoneCallNum(String str) {
+    return match(PHONE_CALL_PATTERN, str);
+  }
+
+  /**
+   * 验证【电信】手机号码的格式
+   *
+   * @param str 校验手机字符串
+   * @return 返回true, 否则为false
+   * @author LinBilin
+   */
+  public static boolean isChinaTelecomPhoneNum(String str) {
+    return match(CHINA_TELECOM_PATTERN, str);
+  }
+
+  /**
+   * 验证【联通】手机号码的格式
+   *
+   * @param str 校验手机字符串
+   * @return 返回true, 否则为false
+   * @author LinBilin
+   */
+  public static boolean isChinaUnicomPhoneNum(String str) {
+    return match(CHINA_UNICOM_PATTERN, str);
+  }
+
+  /**
+   * 验证【移动】手机号码的格式
+   *
+   * @param str 校验手机字符串
+   * @return 返回true, 否则为false
+   * @author LinBilin
+   */
+  public static boolean isChinaMobilePhoneNum(String str) {
+    return match(CHINA_MOBILE_PATTERN, str);
+  }
+
+  /**
+   * 简单手机号码校验,校验手机号码的长度和1开头
+   *
+   * @param str
+   * @return
+   */
+  public static boolean isPhoneSimple(String str) {
+    return match(SIMPLE_PHONE_CHECK, str);
+  }
+
+  /**
+   * 匹配函数
+   *
+   * @param regex
+   * @param input
+   * @return
+   */
+  private static boolean match(String regex, String input) {
+    return Pattern.matches(regex, input);
+  }
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/validator/IDNoCheckValidator.java b/backend/src/main/java/com/supwisdom/dlpay/api/validator/IDNoCheckValidator.java
new file mode 100644
index 0000000..1837138
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/validator/IDNoCheckValidator.java
@@ -0,0 +1,65 @@
+package com.supwisdom.dlpay.api.validator;
+
+import com.supwisdom.dlpay.api.annotation.IDNoCheck;
+import com.supwisdom.dlpay.api.types.IDTypes;
+import org.apache.commons.beanutils.BeanUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+
+public class IDNoCheckValidator implements ConstraintValidator<IDNoCheck, Object> {
+  private String idnoField;
+  private String idtypeField;
+  private String[] idtypeList;
+
+  @Override
+  public boolean isValid(Object value, ConstraintValidatorContext context) {
+    try {
+      String idtype = BeanUtils.getProperty(value, idtypeField);
+      if (idtype == null) {
+        return true;
+      }
+      if (!Arrays.asList(idtypeList).contains(idtype)) {
+        return false;
+      }
+      String idno = BeanUtils.getProperty(value, idnoField);
+      if (StringUtils.isEmpty(idno)) {
+        return false;
+      }
+      if (IDTypes.IDCARD.value().equals(idtype) && idno.length() != 18) {
+        return false;
+      }
+      return true;
+    } catch (IllegalAccessException e) {
+      e.printStackTrace();
+    } catch (InvocationTargetException e) {
+      e.printStackTrace();
+    } catch (NoSuchMethodException e) {
+      e.printStackTrace();
+    }
+    return false;
+  }
+
+  @Override
+  public void initialize(IDNoCheck constraintAnnotation) {
+    idnoField = constraintAnnotation.idno();
+    idtypeField = constraintAnnotation.idtype();
+    if (StringUtils.isEmpty(idnoField) || StringUtils.isEmpty(idtypeField)) {
+      throw new IllegalArgumentException("IDNO 和 IDType 字段必须指定");
+    }
+    Object value = constraintAnnotation.value().getData();
+    if (value instanceof IDTypes[]) {
+      idtypeList = new String[((IDTypes[]) value).length];
+      for (int i = 0; i < ((IDTypes[]) value).length; ++i) {
+        idtypeList[i] = ((IDTypes[]) value)[i].value();
+      }
+    } else if (value instanceof String) {
+      idtypeList = new String[]{(String) value};
+    } else {
+      throw new IllegalArgumentException("IDTypes 类型错误");
+    }
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/validator/MobileNumberValidator.java b/backend/src/main/java/com/supwisdom/dlpay/api/validator/MobileNumberValidator.java
new file mode 100644
index 0000000..4ded93f
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/validator/MobileNumberValidator.java
@@ -0,0 +1,23 @@
+package com.supwisdom.dlpay.api.validator;
+
+import com.supwisdom.dlpay.api.annotation.MobileNumber;
+import com.supwisdom.dlpay.api.util.MobileNumberCheck;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+public class MobileNumberValidator implements ConstraintValidator<MobileNumber, String> {
+
+  @Override
+  public boolean isValid(String value, ConstraintValidatorContext context) {
+    if (value == null || StringUtils.isEmpty(value)) {
+      return true;
+    }
+    return MobileNumberCheck.isPhone(value);
+  }
+
+  @Override
+  public void initialize(MobileNumber constraintAnnotation) {
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/exception/TransactionException.java b/backend/src/main/java/com/supwisdom/dlpay/exception/TransactionException.java
new file mode 100644
index 0000000..e84657e
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/exception/TransactionException.java
@@ -0,0 +1,14 @@
+package com.supwisdom.dlpay.exception;
+
+public class TransactionException extends Exception {
+  private int errCode;
+
+  public TransactionException(int code, String message) {
+    super(message);
+    errCode = code;
+  }
+
+  public int code() {
+    return this.errCode;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/exception/TransactionProcessException.java b/backend/src/main/java/com/supwisdom/dlpay/exception/TransactionProcessException.java
new file mode 100644
index 0000000..aac9273
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/exception/TransactionProcessException.java
@@ -0,0 +1,7 @@
+package com.supwisdom.dlpay.exception;
+
+public class TransactionProcessException extends TransactionException {
+  public TransactionProcessException(int errCode, String message) {
+    super(errCode, String.format("E-%d : %s", errCode, message));
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/exception/ValidateCodeException.java b/backend/src/main/java/com/supwisdom/dlpay/exception/ValidateCodeException.java
new file mode 100644
index 0000000..98ea42b
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/exception/ValidateCodeException.java
@@ -0,0 +1,23 @@
+/**
+ * 
+ */
+package com.supwisdom.dlpay.exception;
+
+import org.springframework.security.core.AuthenticationException;
+
+/**
+ * @author lenovo
+ *
+ */
+public class ValidateCodeException extends AuthenticationException {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 1170189980006964105L;
+
+	
+	public ValidateCodeException(String msg) {
+		super(msg);
+	}
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/core/DatabaseConfig.java b/backend/src/main/java/com/supwisdom/dlpay/framework/core/DatabaseConfig.java
new file mode 100644
index 0000000..99bb014
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/core/DatabaseConfig.java
@@ -0,0 +1,14 @@
+package com.supwisdom.dlpay.framework.core;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class DatabaseConfig {
+  @Value("${spring.datasource.platform}")
+  private String platform;
+
+  public String getPlatform() {
+    return platform;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/core/JwtConfig.java b/backend/src/main/java/com/supwisdom/dlpay/framework/core/JwtConfig.java
new file mode 100644
index 0000000..85167fd
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/core/JwtConfig.java
@@ -0,0 +1,47 @@
+package com.supwisdom.dlpay.framework.core;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class JwtConfig {
+  @Value("${jwt.secret}")
+  private String secret;
+  @Value("${jwt.expiration:3600}")
+  private Long expiration = 3600L;
+  @Value("${jwt.header:Authorization}")
+  private String header = "Authorization";
+  @Value("${jwt.token_header:Bearer }")
+  private String tokenHeader = "Bearer ";
+
+  @Value("${jwt.multitenant:false}")
+  private Boolean multiTenant = false;
+
+  public String getSecret() {
+    return secret;
+  }
+
+  public Long getExpiration() {
+    return expiration;
+  }
+
+  public String getHeader() {
+    return header;
+  }
+
+  public String getTokenHeader() {
+    return tokenHeader;
+  }
+
+  public void setExpiration(Long expiration) {
+    this.expiration = expiration;
+  }
+
+  public Boolean getMultiTenant() {
+    return multiTenant;
+  }
+
+  public void setMultiTenant(Boolean multiTenant) {
+    this.multiTenant = multiTenant;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/core/JwtToken.java b/backend/src/main/java/com/supwisdom/dlpay/framework/core/JwtToken.java
new file mode 100644
index 0000000..072ea5d
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/core/JwtToken.java
@@ -0,0 +1,39 @@
+package com.supwisdom.dlpay.framework.core;
+
+import org.jose4j.jwt.NumericDate;
+
+public class JwtToken {
+  private String jti;
+  private NumericDate expiration;
+  private String jwtToken;
+
+  public JwtToken(String jti, String jwtToken, NumericDate exp) {
+    this.jti = jti;
+    this.jwtToken = jwtToken;
+    this.expiration = exp;
+  }
+
+  public String getJti() {
+    return jti;
+  }
+
+  public void setJti(String jti) {
+    this.jti = jti;
+  }
+
+  public String getJwtToken() {
+    return jwtToken;
+  }
+
+  public void setJwtToken(String jwtToken) {
+    this.jwtToken = jwtToken;
+  }
+
+  public NumericDate getExpiration() {
+    return expiration;
+  }
+
+  public void setExpiration(NumericDate expiration) {
+    this.expiration = expiration;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/core/JwtTokenUtil.java b/backend/src/main/java/com/supwisdom/dlpay/framework/core/JwtTokenUtil.java
new file mode 100644
index 0000000..8ef087f
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/core/JwtTokenUtil.java
@@ -0,0 +1,95 @@
+package com.supwisdom.dlpay.framework.core;
+
+import com.supwisdom.dlpay.framework.util.Constants;
+import org.jose4j.jwa.AlgorithmConstraints;
+import org.jose4j.jwk.JsonWebKey;
+import org.jose4j.jws.AlgorithmIdentifiers;
+import org.jose4j.jws.JsonWebSignature;
+import org.jose4j.jwt.JwtClaims;
+import org.jose4j.jwt.MalformedClaimException;
+import org.jose4j.jwt.consumer.InvalidJwtException;
+import org.jose4j.jwt.consumer.JwtConsumer;
+import org.jose4j.jwt.consumer.JwtConsumerBuilder;
+import org.jose4j.lang.JoseException;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class JwtTokenUtil {
+  private JwtConfig jwtConfig;
+
+  public JwtTokenUtil(JwtConfig config) {
+    this.jwtConfig = config;
+  }
+
+  public String getHeader() {
+    return jwtConfig.getHeader();
+  }
+
+  public JwtToken generateToken(Map<String, Object> params) throws JoseException, MalformedClaimException {
+    JwtClaims claims = new JwtClaims();
+    claims.setIssuer(params.get("issuer").toString());  // who creates the token and signs it
+    if (params.get("audience") != null) {
+      claims.setAudience(params.get("audience").toString());
+    }
+    claims.setExpirationTimeMinutesInTheFuture(jwtConfig.getExpiration() / 60); // time when the token will expire (10 minutes from now)
+    claims.setGeneratedJwtId();
+    claims.setIssuedAtToNow();  // when the token was issued/created (now)
+    claims.setNotBeforeMinutesInThePast(2); // time before which the token is not yet valid (2 minutes ago)
+    if (params.get("subject") != null) {
+      claims.setSubject(params.get("subject").toString()); // the subject/principal is whom the token is about
+    }
+    if (params.get(Constants.JWT_CLAIM_AUTHORITIES) != null) {
+      claims.setClaim(Constants.JWT_CLAIM_AUTHORITIES, params.get(Constants.JWT_CLAIM_AUTHORITIES));
+    }
+    if (params.get(Constants.JWT_CLAIM_UID) != null) {
+      claims.setClaim(Constants.JWT_CLAIM_UID, params.get(Constants.JWT_CLAIM_UID));
+    }
+    if (params.get(Constants.JWT_CLAIM_TENANTID) != null) {
+      claims.setClaim(Constants.JWT_CLAIM_TENANTID, params.get(Constants.JWT_CLAIM_TENANTID));
+    }
+    /*
+    claims.setClaim("email", "mail@example.com"); // additional claims/attributes about the subject can be added
+    List<String> groups = Arrays.asList("group-one", "other-group", "group-three");
+    claims.setStringListClaim("groups", groups); // multi-valued claims work too and will end up as a JSON array
+     */
+
+    Map<String, Object> keySpec = new HashMap<>();
+    keySpec.put("kty", "oct");
+    keySpec.put("k", jwtConfig.getSecret());
+    JsonWebKey key = JsonWebKey.Factory.newJwk(keySpec);
+    JsonWebSignature jws = new JsonWebSignature();
+    jws.setPayload(claims.toJson());
+    jws.setKey(key.getKey());
+    jws.setKeyIdHeaderValue(key.getKeyId());
+    jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256);
+    return new JwtToken(claims.getJwtId(), jws.getCompactSerialization(), claims.getExpirationTime());
+  }
+
+  public JwtToken generateToken(UserDetails userDetails) throws JoseException, MalformedClaimException {
+    Map<String, Object> claims = new HashMap<>();
+    claims.put("uid", userDetails.getUsername());
+    return generateToken(claims);
+  }
+
+  public Map<String, Object> verifyToken(String token) throws JoseException, InvalidJwtException {
+    Map<String, Object> keySpec = new HashMap<>();
+    keySpec.put("kty", "oct");
+    keySpec.put("k", jwtConfig.getSecret());
+    JsonWebKey key = JsonWebKey.Factory.newJwk(keySpec);
+    JwtConsumer jwtConsumer = new JwtConsumerBuilder()
+        .setRequireExpirationTime() // the JWT must have an expiration time
+        .setAllowedClockSkewInSeconds(30) // allow some leeway in validating time based claims to account for clock skew
+        .setVerificationKey(key.getKey()) // verify the signature with the public key
+        .setSkipDefaultAudienceValidation()
+        .setJwsAlgorithmConstraints( // only allow the expected signature algorithm(s) in the given context
+            new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.WHITELIST, // which is only RS256 here
+                AlgorithmIdentifiers.HMAC_SHA256))
+        .build(); // create the JwtConsumer instance
+
+    //  Validate the JWT and process it to the Claims
+    JwtClaims jwtClaims = jwtConsumer.processToClaims(token);
+    return jwtClaims.getClaimsMap();
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/core/PasswordBCryptConfig.java b/backend/src/main/java/com/supwisdom/dlpay/framework/core/PasswordBCryptConfig.java
new file mode 100644
index 0000000..1743e2e
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/core/PasswordBCryptConfig.java
@@ -0,0 +1,28 @@
+package com.supwisdom.dlpay.framework.core;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class PasswordBCryptConfig {
+  @Value("${auth.password.bcrypt.length:10}")
+  private Integer length;
+  @Value("${auth.password.bcrypt.seed}")
+  private String seed = "";
+
+  public Integer getLength() {
+    return length;
+  }
+
+  public void setLength(Integer length) {
+    this.length = length;
+  }
+
+  public String getSeed() {
+    return seed;
+  }
+
+  public void setSeed(String seed) {
+    this.seed = seed;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/dao/ApiClientDao.java b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/ApiClientDao.java
new file mode 100644
index 0000000..3875703
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/ApiClientDao.java
@@ -0,0 +1,14 @@
+package com.supwisdom.dlpay.framework.dao;
+
+import com.supwisdom.dlpay.framework.domain.TApiClient;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface ApiClientDao extends JpaRepository<TApiClient, String> {
+  TApiClient findByAppid(String appid);
+
+  Page<TApiClient> findByAppidContaining(String appid, Pageable pageable);
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/dao/BusinessparaDao.java b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/BusinessparaDao.java
new file mode 100644
index 0000000..4b8addc
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/BusinessparaDao.java
@@ -0,0 +1,30 @@
+package com.supwisdom.dlpay.framework.dao;
+
+import com.supwisdom.dlpay.framework.domain.TBusinesspara;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.*;
+import org.springframework.stereotype.Repository;
+
+import javax.persistence.LockModeType;
+import javax.persistence.QueryHint;
+
+@Repository
+public interface BusinessparaDao extends JpaRepository<TBusinesspara, String> {
+  TBusinesspara findByParakey(String parakey);
+
+  @Lock(LockModeType.PESSIMISTIC_WRITE)
+  @Query(value = " from TBusinesspara where parakey=?1 ")
+  TBusinesspara findByParakeyForUpdate(String parakey);
+
+  @Lock(LockModeType.PESSIMISTIC_WRITE)
+  @QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value ="0")})
+  @Query(value = " from TBusinesspara where parakey=?1 ")
+  TBusinesspara findByParakeyForUpdateNowait(String parakey);
+
+  Page<TBusinesspara> findAllByParakeyContaining(String parakey, Pageable pageable);
+
+  @Modifying
+  @Query("update TBusinesspara t set t.paraval=?2 where t.parakey=?1 ")
+  void updateBusinessparaValue(String parakey, String paraval);
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/dao/DictionaryDao.java b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/DictionaryDao.java
new file mode 100644
index 0000000..e731908
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/DictionaryDao.java
@@ -0,0 +1,19 @@
+package com.supwisdom.dlpay.framework.dao;
+
+import com.supwisdom.dlpay.framework.domain.TDictionary;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface DictionaryDao extends JpaRepository<TDictionary, Integer> {
+  @Query("from TDictionary t where t.dicttype=?1 order by t.dictid ")
+  List<TDictionary> findAllByDicttype(String dicttype);
+
+  void deleteByDicttype(String dicttype);
+
+  @Query("from TDictionary t where t.dicttype=?1 and t.dictval=?2 ")
+  TDictionary getByDicttypeAndDictval(String dicttype, String dictval);
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/dao/OperRoleDao.java b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/OperRoleDao.java
new file mode 100644
index 0000000..fa3dd5a
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/OperRoleDao.java
@@ -0,0 +1,21 @@
+package com.supwisdom.dlpay.framework.dao;
+
+import com.supwisdom.dlpay.framework.domain.TOperRole;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface OperRoleDao extends JpaRepository<TOperRole, String> {
+
+  @Query(value = "select distinct rolecode from TB_OPER_ROLE a,TB_ROLE b where a.roleid=b.roleid and a.operid=?1", nativeQuery = true)
+  List<String> getRolecodeByOperid(String operid);
+
+  void deleteByRoleId(String roleId);
+
+  List<TOperRole> findAllByOperid(String operid);
+
+  void deleteByOperid(String operid);
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/dao/OperatorDao.java b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/OperatorDao.java
new file mode 100644
index 0000000..28cc18e
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/OperatorDao.java
@@ -0,0 +1,14 @@
+package com.supwisdom.dlpay.framework.dao;
+
+import com.supwisdom.dlpay.framework.domain.TOperator;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface OperatorDao extends JpaRepository<TOperator, String>, JpaSpecificationExecutor<TOperator> {
+  TOperator findByOpercode(String opercode);
+
+  TOperator findByOperid(String operid);
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/dao/SubjectDao.java b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/SubjectDao.java
new file mode 100644
index 0000000..d243dbc
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/SubjectDao.java
@@ -0,0 +1,15 @@
+package com.supwisdom.dlpay.framework.dao;
+
+import com.supwisdom.dlpay.framework.domain.TSubject;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface SubjectDao extends JpaRepository<TSubject, Integer> {
+
+  @Query("from TSubject where displayflag='y' order by subjno ")
+  List<TSubject> findAllDisplaySubjects();
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/dao/SysparaDao.java b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/SysparaDao.java
new file mode 100644
index 0000000..b4f5da3
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/SysparaDao.java
@@ -0,0 +1,24 @@
+package com.supwisdom.dlpay.framework.dao;
+
+import com.supwisdom.dlpay.framework.domain.TSyspara;
+import org.springframework.data.jpa.repository.*;
+import org.springframework.stereotype.Repository;
+
+import javax.persistence.LockModeType;
+import javax.persistence.QueryHint;
+
+@Repository
+public interface SysparaDao extends JpaRepository<TSyspara, Integer>, JpaSpecificationExecutor<TSyspara> {
+  TSyspara findByParaid(int paraid);
+
+  @Lock(LockModeType.PESSIMISTIC_WRITE)
+  @Query(value = "from TSyspara where paraid=?1 ")
+  TSyspara findByParaidWithLock(int paraid);
+
+  @Lock(LockModeType.PESSIMISTIC_WRITE)
+  @QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value ="0")})
+  @Query(value = "from TSyspara where paraid=?1 ")
+  TSyspara findByParaidWithLockNowait(int paraid);
+
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/dao/TaskLockDao.java b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/TaskLockDao.java
new file mode 100644
index 0000000..7a030a3
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/TaskLockDao.java
@@ -0,0 +1,27 @@
+package com.supwisdom.dlpay.framework.dao;
+
+import com.supwisdom.dlpay.framework.data.SystemDateTime;
+import com.supwisdom.dlpay.framework.domain.TTaskLock;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface TaskLockDao extends JpaRepository<TTaskLock, String> {
+
+  //================= database=Oracle =================//
+  @Query(value = "select  to_char(sysdate,'yyyymmdd') as hostdate,to_char(sysdate,'hh24miss') as hosttime,to_char(sysdate,'yyyymmddhh24miss') as hostdatetime, sysdate from dual", nativeQuery = true)
+  SystemDateTime getOracleDatetime();
+
+  @Query(value = "select  to_char(CURRENT_TIMESTAMP,'yyyymmdd') as hostdate," + "" +
+      "to_char(CURRENT_TIMESTAMP,'hh24miss') as hosttime,to_char(CURRENT_TIMESTAMP,'yyyymmddhh24miss') as hostdatetime," +
+      " CURRENT_TIMESTAMP as sysdate", nativeQuery = true)
+  SystemDateTime getPGDatetime();
+
+  @Query(value = " select to_char(sysdate,'yyyyMMddhh24miss')||to_char(SEQ_REFNO.nextval,'FM000000') as billno from dual ", nativeQuery = true)
+  String getOracleRefno();
+
+  //================= database=PG =================//
+  @Query(value = " select to_char(CURRENT_TIMESTAMP,'yyyyMMddhh24miss')||to_char(nextval('SEQ_REFNO'),'FM000000') as billno ", nativeQuery = true)
+  String getPgRefno();
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/dao/TranscodeDao.java b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/TranscodeDao.java
new file mode 100644
index 0000000..516ff2c
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/dao/TranscodeDao.java
@@ -0,0 +1,10 @@
+package com.supwisdom.dlpay.framework.dao;
+
+import com.supwisdom.dlpay.framework.domain.TTranscode;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface TranscodeDao extends JpaRepository<TTranscode, Integer> {
+  TTranscode getByTranscode(Integer transcode);
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/data/SystemDateTime.java b/backend/src/main/java/com/supwisdom/dlpay/framework/data/SystemDateTime.java
new file mode 100644
index 0000000..b876d9d
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/data/SystemDateTime.java
@@ -0,0 +1,14 @@
+package com.supwisdom.dlpay.framework.data;
+
+import java.sql.Timestamp;
+
+public interface SystemDateTime {
+  String getHostdate();
+
+  String getHosttime();
+
+  String getHostdatetime();
+
+  Timestamp getSysdate();
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/domain/DictionaryTable.java b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/DictionaryTable.java
new file mode 100644
index 0000000..7d1d36a
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/DictionaryTable.java
@@ -0,0 +1,7 @@
+package com.supwisdom.dlpay.framework.domain;
+
+public interface DictionaryTable {
+  String getDictKey();
+
+  Object getDictValue();
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/domain/JwtRedis.java b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/JwtRedis.java
new file mode 100644
index 0000000..d32ff8e
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/JwtRedis.java
@@ -0,0 +1,51 @@
+package com.supwisdom.dlpay.framework.domain;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.redis.core.RedisHash;
+import org.springframework.data.redis.core.TimeToLive;
+
+
+@RedisHash(value = "api_jwt")
+public class JwtRedis {
+  @Id
+  String jti;
+
+  String status;
+
+  String uid;
+
+  @TimeToLive
+  Long expiration;
+
+  public String getJti() {
+    return jti;
+  }
+
+  public void setJti(String jti) {
+    this.jti = jti;
+  }
+
+  public String getStatus() {
+    return status;
+  }
+
+  public void setStatus(String status) {
+    this.status = status;
+  }
+
+  public Long getExpiration() {
+    return expiration;
+  }
+
+  public void setExpiration(Long expiration) {
+    this.expiration = expiration;
+  }
+
+  public String getUid() {
+    return uid;
+  }
+
+  public void setUid(String uid) {
+    this.uid = uid;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TApiClient.java b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TApiClient.java
new file mode 100644
index 0000000..a9c2b05
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TApiClient.java
@@ -0,0 +1,106 @@
+package com.supwisdom.dlpay.framework.domain;
+
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+
+@Entity
+@Table(name = "TB_APICLIENT",
+    indexes = {@Index(name = "apiclient_idx", columnList = "appid, tenantid", unique = true)})
+public class TApiClient {
+  @Id
+  @SequenceGenerator(name = "apiclient_id", sequenceName = "SEQ_APICLIENT", allocationSize = 1, initialValue = 10)
+  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "apiclient_id")
+  @Column(name = "id")
+  @NotNull
+  private Integer id;
+
+  @Column(name = "appid", length = 20)
+  @NotNull
+  private String appid;
+
+  @Column(name = "secret", length = 64)
+  @NotNull
+  private String secret;
+
+  @Column(name = "status", length = 10)
+  @NotNull
+  private String status;
+
+  @Column(name = "roles", length = 300)
+  private String roles;
+
+  @Column(name = "BCRYPT_SECRET", length = 64)
+  @NotNull
+  private String bcryptSecret;
+
+  @Column(name = "THIRDURL", length = 200)
+  private String thirdurl;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantId;
+
+
+  public String getAppid() {
+    return appid;
+  }
+
+  public void setAppid(String appid) {
+    this.appid = appid;
+  }
+
+  public String getSecret() {
+    return secret;
+  }
+
+  public void setSecret(String secret) {
+    this.secret = secret;
+    this.bcryptSecret = new BCryptPasswordEncoder().encode(secret);
+  }
+
+  public String getStatus() {
+    return status;
+  }
+
+  public void setStatus(String status) {
+    this.status = status;
+  }
+
+  public String getRoles() {
+    return roles;
+  }
+
+  public void setRoles(String roles) {
+    this.roles = roles;
+  }
+
+  public String getBcryptSecret() {
+    return bcryptSecret;
+  }
+
+  public String getThirdurl() {
+    return thirdurl;
+  }
+
+  public void setThirdurl(String thirdurl) {
+    this.thirdurl = thirdurl;
+  }
+
+  public String getTenantId() {
+    return tenantId;
+  }
+
+  public void setTenantId(String tenantId) {
+    this.tenantId = tenantId;
+  }
+
+  public Integer getId() {
+    return id;
+  }
+
+  public void setId(Integer id) {
+    this.id = id;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TBusinesspara.java b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TBusinesspara.java
new file mode 100644
index 0000000..a1e1209
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TBusinesspara.java
@@ -0,0 +1,54 @@
+package com.supwisdom.dlpay.framework.domain;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import javax.validation.constraints.NotNull;
+
+@Entity
+@Table(name = "TB_BUSINESSPARA")
+public class TBusinesspara {
+  @Id
+  @Column(name="PARAKEY", nullable = false, length = 60)
+  private String parakey;
+
+  @Column(name="PARAVAL", length = 1000)
+  private String paraval;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantId;
+
+  public TBusinesspara() {
+  }
+
+  public TBusinesspara(String parakey, String paraval) {
+    this.parakey = parakey;
+    this.paraval = paraval;
+  }
+
+  public String getParakey() {
+    return parakey;
+  }
+
+  public void setParakey(String parakey) {
+    this.parakey = parakey;
+  }
+
+  public String getParaval() {
+    return paraval;
+  }
+
+  public void setParaval(String paraval) {
+    this.paraval = paraval;
+  }
+
+  public String getTenantId() {
+    return tenantId;
+  }
+
+  public void setTenantId(String tenantId) {
+    this.tenantId = tenantId;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TDictionary.java b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TDictionary.java
new file mode 100644
index 0000000..1f766c9
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TDictionary.java
@@ -0,0 +1,94 @@
+package com.supwisdom.dlpay.framework.domain;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+@Entity
+@Table(name = "TB_DICTIONARY",
+    indexes = {@Index(name = "dictionary_idx", columnList = "dicttype, dictval, tenantid", unique = true)})
+public class TDictionary implements DictionaryTable, Serializable {
+  private static final long serialVersionUID = -4346024672769882893L;
+  @Id
+  @Column(name = "id")
+  @SequenceGenerator(name = "dictid", sequenceName = "SEQ_DICTIONARY", allocationSize = 1, initialValue = 1000)
+  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "dictid")
+  @NotNull
+  private Integer dictid;
+
+  @Column(name = "DICTTYPE")
+  @NotNull
+  private String dicttype;
+
+  @Column(name = "DICTVAL", length = 30)
+  @NotNull
+  private String dictval;
+
+  @Column(name = "DICTTYPENAME", length = 60)
+  private String dicttypename;
+
+  @Column(name = "DICTCAPTION", length = 60)
+  private String dictcaption;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantId;
+
+  public String getDicttype() {
+    return dicttype;
+  }
+
+  public void setDicttype(String dicttype) {
+    this.dicttype = dicttype;
+  }
+
+  public String getDictval() {
+    return dictval;
+  }
+
+  public void setDictval(String dictval) {
+    this.dictval = dictval;
+  }
+
+  public String getDicttypename() {
+    return dicttypename;
+  }
+
+  public void setDicttypename(String dicttypename) {
+    this.dicttypename = dicttypename;
+  }
+
+  public String getDictcaption() {
+    return dictcaption;
+  }
+
+  public void setDictcaption(String dictcaption) {
+    this.dictcaption = dictcaption;
+  }
+
+  public Integer getDictid() {
+    return dictid;
+  }
+
+  public void setDictid(Integer dictid) {
+    this.dictid = dictid;
+  }
+
+  public String getTenantId() {
+    return tenantId;
+  }
+
+  public void setTenantId(String tenantId) {
+    this.tenantId = tenantId;
+  }
+
+  @Override
+  public String getDictKey() {
+    return this.dictval;
+  }
+
+  @Override
+  public Object getDictValue() {
+    return this.dictcaption;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TOperRole.java b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TOperRole.java
new file mode 100644
index 0000000..54df513
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TOperRole.java
@@ -0,0 +1,61 @@
+package com.supwisdom.dlpay.framework.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+
+@Entity
+@Table(name = "TB_OPER_ROLE",
+    indexes = {@Index(name = "operrole_operid_idx", columnList = "operid")})
+public class TOperRole {
+  @Id
+  @GenericGenerator(name = "idGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "idGenerator")
+  @Column(name = "ID", nullable = false, length = 32)
+  private String id;
+
+  @Column(name = "ROLEID", length = 32)
+  @NotNull
+  private String roleId;
+
+  @Column(name = "OPERID", length = 32)
+  @NotNull
+  private String operid;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantId;
+
+  public String getId() {
+    return id;
+  }
+
+  public void setId(String id) {
+    this.id = id;
+  }
+
+  public String getRoleId() {
+    return roleId;
+  }
+
+  public void setRoleId(String roleId) {
+    this.roleId = roleId;
+  }
+
+  public String getOperid() {
+    return operid;
+  }
+
+  public void setOperid(String operid) {
+    this.operid = operid;
+  }
+
+  public String getTenantId() {
+    return tenantId;
+  }
+
+  public void setTenantId(String tenantId) {
+    this.tenantId = tenantId;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TOperator.java b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TOperator.java
new file mode 100644
index 0000000..8e8f83b
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TOperator.java
@@ -0,0 +1,248 @@
+package com.supwisdom.dlpay.framework.domain;
+
+import com.supwisdom.dlpay.api.annotation.MobileNumber;
+import com.supwisdom.dlpay.framework.util.TradeDict;
+import org.hibernate.annotations.GenericGenerator;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import javax.persistence.*;
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+import java.util.Collection;
+
+@Entity
+@Table(name = "TB_OPERATOR",
+    indexes = {@Index(name = "opercode_idx", columnList = "OPERCODE, tenantid", unique = true)})
+public class TOperator implements UserDetails, Serializable {
+  private static final long serialVersionUID = 2148742269021236587L;
+  @Id
+  @GenericGenerator(name = "idGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "idGenerator")
+  @Column(name = "OPERID", nullable = false, length = 32)
+  private String operid;
+
+  @Column(name = "OPERCODE", length = 20)
+  @NotNull
+  private String opercode;
+
+  @Column(name = "OPERTYPE", length = 20)
+  @NotNull
+  private String opertype;
+
+  @Column(name = "OPERNAME", length = 100)
+  @NotNull
+  private String opername;
+
+  @Column(name = "OPERPWD", length = 80)
+  @NotNull
+  private String operpwd;
+
+  @Column(name = "STATUS", length = 32)
+  @NotNull
+  private String status;
+
+  @Column(name = "SEX", length = 10)
+  private String sex;
+
+  @Column(name = "MOBILE", length = 20)
+  @MobileNumber(message = "请输入正确手机号")
+  private String mobile;
+
+  @Column(name = "EMAIL", length = 60)
+  @Email(message = "请输入正确邮箱")
+  private String email;
+
+  @Column(name = "OPENDATE", length = 8)
+  private String opendate;
+
+  @Column(name = "CLOSEDATE", length = 8)
+  private String closedate;
+  @Transient
+  private String roleids;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantId;
+
+  @Column(name = "thirdadmin", length = 20)
+  @NotNull
+  private String thirdadmin;
+
+  @Transient
+  private Collection<? extends GrantedAuthority> authorities;  //权限
+
+  public TOperator() {
+  }
+
+  public TOperator(String opercode, String opername) {
+    this.opercode = opercode;
+    this.opername = opername;
+  }
+
+  public TOperator(String opercode, String opertype, String opername, String operpwd, String status, String sex, String mobile, String email, String opendate, String closedate, Collection<? extends GrantedAuthority> authorities) {
+    this.opercode = opercode;
+    this.opertype = opertype;
+    this.opername = opername;
+    this.operpwd = operpwd;
+    this.status = status;
+    this.sex = sex;
+    this.mobile = mobile;
+    this.email = email;
+    this.opendate = opendate;
+    this.closedate = closedate;
+    this.authorities = authorities;
+  }
+
+  public String getOperid() {
+    return operid;
+  }
+
+  public void setOperid(String operid) {
+    this.operid = operid;
+  }
+
+  public String getOpercode() {
+    return opercode;
+  }
+
+  public void setOpercode(String opercode) {
+    this.opercode = opercode;
+  }
+
+  public String getOpertype() {
+    return opertype;
+  }
+
+  public void setOpertype(String opertype) {
+    this.opertype = opertype;
+  }
+
+  public String getOpername() {
+    return opername;
+  }
+
+  public void setOpername(String opername) {
+    this.opername = opername;
+  }
+
+  public String getOperpwd() {
+    return operpwd;
+  }
+
+  public void setOperpwd(String operpwd) {
+    this.operpwd = operpwd;
+  }
+
+  public String getStatus() {
+    return status;
+  }
+
+  public void setStatus(String status) {
+    this.status = status;
+  }
+
+  public String getSex() {
+    return sex;
+  }
+
+  public void setSex(String sex) {
+    this.sex = sex;
+  }
+
+  public String getMobile() {
+    return mobile;
+  }
+
+  public void setMobile(String mobile) {
+    this.mobile = mobile;
+  }
+
+  public String getEmail() {
+    return email;
+  }
+
+  public void setEmail(String email) {
+    this.email = email;
+  }
+
+  public String getOpendate() {
+    return opendate;
+  }
+
+  public void setOpendate(String opendate) {
+    this.opendate = opendate;
+  }
+
+  public String getClosedate() {
+    return closedate;
+  }
+
+  public void setClosedate(String closedate) {
+    this.closedate = closedate;
+  }
+
+  public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
+    this.authorities = authorities;
+  }
+
+  @Override
+  public Collection<? extends GrantedAuthority> getAuthorities() {
+    return this.authorities;
+  }
+
+  @Override
+  public String getPassword() {
+    return this.operpwd;
+  }
+
+  @Override
+  public String getUsername() {
+    return this.opercode;
+  }
+
+  @Override
+  public boolean isAccountNonExpired() {
+    return true; //账户不会过期
+  }
+
+  @Override
+  public boolean isAccountNonLocked() {
+    return !TradeDict.STATUS_LOCKED.equals(this.status); //true - 没锁定
+  }
+
+  @Override
+  public boolean isCredentialsNonExpired() {
+    return true; //TODO:密码是否未过期
+  }
+
+  @Override
+  public boolean isEnabled() {
+    return !TradeDict.STATUS_CLOSED.equals(this.status);  //注销操作员不启用
+  }
+
+  public String getRoleids() {
+    return roleids;
+  }
+
+  public void setRoleids(String roleids) {
+    this.roleids = roleids;
+  }
+
+  public String getTenantId() {
+    return tenantId;
+  }
+
+  public void setTenantId(String tenantId) {
+    this.tenantId = tenantId;
+  }
+
+  public String getThirdadmin() {
+    return thirdadmin;
+  }
+
+  public void setThirdadmin(String thirdadmin) {
+    this.thirdadmin = thirdadmin;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TSubject.java b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TSubject.java
new file mode 100644
index 0000000..d76cd59
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TSubject.java
@@ -0,0 +1,148 @@
+package com.supwisdom.dlpay.framework.domain;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+@Entity
+@Table(name = "TB_SUBJECT",
+    indexes = {@Index(name = "subject_idx", columnList = "subjno, tenantid", unique = true)})
+public class TSubject implements Serializable, DictionaryTable {
+
+  @Id
+  @Column(name = "subjid")
+  @NotNull
+  @SequenceGenerator(name = "subjectid", sequenceName = "SEQ_SUBJECTID", allocationSize = 1, initialValue = 1000)
+  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "subjectid")
+  private Integer id;
+
+  @Column(name = "SUBJNO", nullable = false, length = 10)
+  private String subjno;
+
+  @Column(name = "SUBJNAME", length = 60)
+  @NotNull
+  private String subjname;
+
+  @Column(name = "SUBJTYPE", precision = 1)
+  private Integer subjtype;
+
+  @Column(name = "BALFLAG", precision = 1)
+  private Integer balflag;
+
+  @Column(name = "FSUBJNO", length = 10)
+  private String fsubjno;
+
+  @Column(name = "SUBJLEVEL", precision = 1)
+  private Integer subjlevel;
+
+  @Column(name = "ENDFLAG", precision = 1)
+  private Integer endflag;
+
+  @Column(name = "OPENDATE", precision = 8)
+  private Integer opendate;
+
+  @Column(name = "DISPLAYFLAG", length = 1)
+  private String displayflag;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantId;
+
+  public String getSubjno() {
+    return subjno;
+  }
+
+  public void setSubjno(String subjno) {
+    this.subjno = subjno;
+  }
+
+  public String getSubjname() {
+    return subjname;
+  }
+
+  public void setSubjname(String subjname) {
+    this.subjname = subjname;
+  }
+
+  public Integer getSubjtype() {
+    return subjtype;
+  }
+
+  public void setSubjtype(Integer subjtype) {
+    this.subjtype = subjtype;
+  }
+
+  public Integer getBalflag() {
+    return balflag;
+  }
+
+  public void setBalflag(Integer balflag) {
+    this.balflag = balflag;
+  }
+
+  public String getFsubjno() {
+    return fsubjno;
+  }
+
+  public void setFsubjno(String fsubjno) {
+    this.fsubjno = fsubjno;
+  }
+
+  public Integer getSubjlevel() {
+    return subjlevel;
+  }
+
+  public void setSubjlevel(Integer subjlevel) {
+    this.subjlevel = subjlevel;
+  }
+
+  public Integer getEndflag() {
+    return endflag;
+  }
+
+  public void setEndflag(Integer endflag) {
+    this.endflag = endflag;
+  }
+
+  public Integer getOpendate() {
+    return opendate;
+  }
+
+  public void setOpendate(Integer opendate) {
+    this.opendate = opendate;
+  }
+
+  public String getDisplayflag() {
+    return displayflag;
+  }
+
+  public void setDisplayflag(String displayflag) {
+    this.displayflag = displayflag;
+  }
+
+  public String getTenantId() {
+    return tenantId;
+  }
+
+  public void setTenantId(String tenantId) {
+    this.tenantId = tenantId;
+  }
+
+  public Integer getId() {
+    return id;
+  }
+
+  public void setId(Integer id) {
+    this.id = id;
+  }
+
+  @Override
+  public String getDictKey() {
+    return this.subjno;
+  }
+
+  @Override
+  public Object getDictValue() {
+    return this.subjname;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TSyspara.java b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TSyspara.java
new file mode 100644
index 0000000..8269487
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TSyspara.java
@@ -0,0 +1,177 @@
+package com.supwisdom.dlpay.framework.domain;
+
+import com.supwisdom.dlpay.framework.util.DateUtil;
+import com.supwisdom.dlpay.framework.util.NumberUtil;
+import com.supwisdom.dlpay.framework.util.StringUtil;
+import com.supwisdom.dlpay.framework.util.SysparaUtil;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+@Entity
+@Table(name = "TB_SYSPARA")
+@IdClass(TSysparaPK.class)
+public class TSyspara implements Serializable {
+  private static final long serialVersionUID = 6760877848228345624L;
+  @Id
+  @Column(name = "PARAID", nullable = false, precision = 9)
+  private Integer paraid;
+
+  @Id
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantId;
+
+  @Column(name = "PARAVAL", length = 100)
+  private String paraval;
+
+  @Column(name = "PARANAME", length = 90)
+  private String paraname;
+
+  @Column(name = "PARAUNIT", length = 20)
+  private String paraunit;
+
+  @Column(name = "VALUETYPE", length = 10)
+  private String valueType = SysparaUtil.VALUETYPE_STRING;
+
+  @Column(name = "EDITFLAG", precision = 1)
+  @NotNull
+  private Integer editflag;
+
+  @Column(name = "DISPLAYFLAG", length = 10)
+  @NotNull
+  private String displayflag;
+
+  @Column(name = "REMARK", length = 240)
+  private String remark;
+
+  @Column(name = "LASTSAVED", length = 14)
+  @NotNull
+  private String lastsaved;
+
+
+  public TSyspara() {
+  }
+
+  public TSyspara(Integer paraid, Integer editflag) {
+    this.paraid = paraid;
+    this.editflag = editflag;
+  }
+
+  public TSyspara(Integer paraid, String paraval, String paraname, String paraunit, String valueType, Integer editflag, String displayflag, String remark, String lastsaved) {
+    this.paraid = paraid;
+    this.paraval = paraval;
+    this.paraname = paraname;
+    this.paraunit = paraunit;
+    this.valueType = valueType;
+    this.editflag = editflag;
+    this.displayflag = displayflag;
+    this.remark = remark;
+    this.lastsaved = lastsaved;
+  }
+
+  public Integer getParaid() {
+    return paraid;
+  }
+
+  public void setParaid(Integer paraid) {
+    this.paraid = paraid;
+  }
+
+  public String getParaval() {
+    return paraval;
+  }
+
+  public void setParaval(String paraval) {
+    this.paraval = paraval;
+  }
+
+  public String getParaname() {
+    return paraname;
+  }
+
+  public void setParaname(String paraname) {
+    this.paraname = paraname;
+  }
+
+  public String getParaunit() {
+    return paraunit;
+  }
+
+  public void setParaunit(String paraunit) {
+    this.paraunit = paraunit;
+  }
+
+  public String getValueType() {
+    return valueType;
+  }
+
+  public void setValueType(String valueType) {
+    this.valueType = valueType;
+  }
+
+  public Integer getEditflag() {
+    return editflag;
+  }
+
+  public void setEditflag(Integer editflag) {
+    this.editflag = editflag;
+  }
+
+  public String getDisplayflag() {
+    return displayflag;
+  }
+
+  public void setDisplayflag(String displayflag) {
+    this.displayflag = displayflag;
+  }
+
+  public String getRemark() {
+    return remark;
+  }
+
+  public void setRemark(String remark) {
+    this.remark = remark;
+  }
+
+  public String getLastsaved() {
+    return lastsaved;
+  }
+
+  public void setLastsaved(String lastsaved) {
+    this.lastsaved = lastsaved;
+  }
+
+  public String getTenantId() {
+    return tenantId;
+  }
+
+  public void setTenantId(String tenantId) {
+    this.tenantId = tenantId;
+  }
+
+  /**
+   * 判断参数是否非法
+   *
+   * @return 非法 -> true; 合法 -> false
+   */
+  public boolean checkValueInvalid() {
+    if (!StringUtil.isEmpty(this.paraval)) {
+      if (SysparaUtil.VALUETYPE_AMOUNT.equals(this.valueType) && !NumberUtil.isAmount(this.paraval)) {
+        return true; //参数值非金额类型
+      } else if (SysparaUtil.VALUETYPE_DECIMAL.equals(this.valueType) && !NumberUtil.isDecimal(this.paraval)) {
+        return true; //参数值非数值类型
+      } else if (SysparaUtil.VALUETYPE_NUMBER.equals(this.valueType) && !NumberUtil.isNumber(this.paraval)) {
+        return true; //参数值非整数
+      } else if (SysparaUtil.VALUETYPE_SWITCH.equals(this.valueType) && !"1".equals(this.paraval) && !"0".equals(this.paraval)) {
+        return true; //参数值非开关量 0/1
+      } else if (SysparaUtil.VALUETYPE_DATE.equals(this.valueType) && !DateUtil.checkDatetimeValid(this.paraval, "yyyyMMdd")) {
+        return true; //参数值非日期格式
+      } else if (SysparaUtil.VALUETYPE_DATETIME.equals(this.valueType) && !DateUtil.checkDatetimeValid(this.paraval, "yyyyMMddHHmmss")) {
+        return true; //参数值非日期时间格式
+      }
+    }
+    return false; //默认字符串格式都合法
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TSysparaPK.java b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TSysparaPK.java
new file mode 100644
index 0000000..353225a
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TSysparaPK.java
@@ -0,0 +1,48 @@
+package com.supwisdom.dlpay.framework.domain;
+
+import javax.persistence.Column;
+import javax.persistence.Id;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+import java.util.Objects;
+
+public class TSysparaPK implements Serializable {
+  @Id
+  @Column(name = "PARAID", nullable = false, precision = 9)
+  private Integer paraid;
+
+  @Id
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantId;
+
+  public Integer getParaid() {
+    return paraid;
+  }
+
+  public void setParaid(Integer paraid) {
+    this.paraid = paraid;
+  }
+
+  public String getTenantId() {
+    return tenantId;
+  }
+
+  public void setTenantId(String tenantId) {
+    this.tenantId = tenantId;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (!(o instanceof TSysparaPK)) return false;
+    TSysparaPK that = (TSysparaPK) o;
+    return Objects.equals(getParaid(), that.getParaid()) &&
+        Objects.equals(getTenantId(), that.getTenantId());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(getParaid(), getTenantId());
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TTaskLock.java b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TTaskLock.java
new file mode 100644
index 0000000..6baf8b4
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TTaskLock.java
@@ -0,0 +1,69 @@
+package com.supwisdom.dlpay.framework.domain;
+
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import javax.validation.constraints.NotNull;
+
+@Entity
+@Table(name = "TB_TASK_LOCK")
+public class TTaskLock {
+  @Id
+  @Column(name="TASKCODE", nullable = false, length = 30)
+  private String taskcode;
+
+  @Column(name="TASKSTATUS", nullable = false, precision = 1)
+  private Integer taskstatus;
+
+  @Column(name="TASKTIME", length = 20)
+  private String tasktime;
+
+  @Column(name="REMARK", length = 600)
+  private String remark;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantId;
+
+  public String getTaskcode() {
+    return taskcode;
+  }
+
+  public void setTaskcode(String taskcode) {
+    this.taskcode = taskcode;
+  }
+
+  public Integer getTaskstatus() {
+    return taskstatus;
+  }
+
+  public void setTaskstatus(Integer taskstatus) {
+    this.taskstatus = taskstatus;
+  }
+
+  public String getTasktime() {
+    return tasktime;
+  }
+
+  public void setTasktime(String tasktime) {
+    this.tasktime = tasktime;
+  }
+
+  public String getRemark() {
+    return remark;
+  }
+
+  public void setRemark(String remark) {
+    this.remark = remark;
+  }
+
+  public String getTenantId() {
+    return tenantId;
+  }
+
+  public void setTenantId(String tenantId) {
+    this.tenantId = tenantId;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TTranscode.java b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TTranscode.java
new file mode 100644
index 0000000..1506e65
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/domain/TTranscode.java
@@ -0,0 +1,68 @@
+package com.supwisdom.dlpay.framework.domain;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+@Entity
+@Table(name = "TB_TRANSCODE",
+    indexes = {@Index(name = "transcode_idx", columnList = "transcode, tenantid", unique = true)})
+public class TTranscode implements DictionaryTable, Serializable {
+  private static final long serialVersionUID = -8077649544237207668L;
+  @Id
+  @Column(name = "transcode_id")
+  @NotNull
+  private Integer trascodeId;
+
+  @Column(name = "TRANSCODE", nullable = false, precision = 4)
+  private Integer transcode;
+
+  @Column(name = "TRANSNAME", nullable = false, length = 100)
+  private String transname;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantId;
+
+  public Integer getTranscode() {
+    return transcode;
+  }
+
+  public void setTranscode(Integer transcode) {
+    this.transcode = transcode;
+  }
+
+  public String getTransname() {
+    return transname;
+  }
+
+  public void setTransname(String transname) {
+    this.transname = transname;
+  }
+
+  public String getTenantId() {
+    return tenantId;
+  }
+
+  public void setTenantId(String tenantId) {
+    this.tenantId = tenantId;
+  }
+
+  @Override
+  public String getDictKey() {
+    return String.valueOf(this.transcode);
+  }
+
+  @Override
+  public Object getDictValue() {
+    return this.transname;
+  }
+
+  public Integer getTrascodeId() {
+    return trascodeId;
+  }
+
+  public void setTrascodeId(Integer trascodeId) {
+    this.trascodeId = trascodeId;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/filter/ValidateCodeFilter.java b/backend/src/main/java/com/supwisdom/dlpay/framework/filter/ValidateCodeFilter.java
new file mode 100644
index 0000000..801ce84
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/filter/ValidateCodeFilter.java
@@ -0,0 +1,90 @@
+package com.supwisdom.dlpay.framework.filter;
+
+
+import com.supwisdom.dlpay.exception.ValidateCodeException;
+import com.supwisdom.dlpay.framework.security.CodeUtil;
+import com.supwisdom.dlpay.framework.security.MyAuthenticationFailureHandler;
+import com.supwisdom.dlpay.framework.util.StringUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+
+@Component("validateCodeFilter")
+public class ValidateCodeFilter extends OncePerRequestFilter {
+
+  /**
+   * 校验失败处理器
+   */
+  @Autowired
+  private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
+
+
+  @Override
+  protected void doFilterInternal(HttpServletRequest request,
+                                  HttpServletResponse response, FilterChain filterChain)
+      throws ServletException, IOException {
+    String context = request.getContextPath();
+    if (context == null || "" == context.trim()) {
+      context = "/";
+    }
+    if (request.getRequestURI().isEmpty()) {
+      filterChain.doFilter(request, response);
+      return;
+    }
+    String url = request.getRequestURI();
+    if (!"/".equals(context)) {
+      url = url.replace(context, "");
+    }
+    if (StringUtil.equals("/login/form", url)
+        && StringUtil.equalsIgnoreCase(request.getMethod(), "post")) {
+      try {
+        validate(request);
+      } catch (ValidateCodeException e) {
+        //response.setStatus(HttpStatus.OK.value());
+        //response.setContentType("application/json;charset=UTF-8");
+        //response.getWriter().write(objectMapper.writeValueAsString(JsonResult.error(400, e.getMessage())));
+        //response.sendError(HttpStatus.UNAUTHORIZED.value(),e.getMessage());
+        myAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
+        return;
+      }
+    }
+    filterChain.doFilter(request, response);
+  }
+
+  private void validate(HttpServletRequest request) throws ValidateCodeException {
+    if (!CodeUtil.checkVerifyCode(request)) {
+      throw new ValidateCodeException("验证码不匹配");
+    } else {
+      request.getSession().removeAttribute(com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY);
+    }
+//    VerifyCode imageCode = (VerifyCode) request.getSession().getAttribute(ImageCodeUtil.LOGIN_IMAGECODE_SESSIONKEY);
+//    String inputCode;
+//    try {
+//      inputCode = request.getParameter("imageCode");
+//    } catch (Exception e) {
+//      throw new ValidateCodeException("获取验证码的值失败");
+//    }
+//    if (StringUtil.isEmpty(inputCode)) {
+//      throw new ValidateCodeException("验证码不能为空");
+//    }
+//    if (null == imageCode) {
+//      throw new ValidateCodeException("验证码不存在");
+//    }
+//    if (imageCode.isExpired()) {
+//      request.getSession().removeAttribute(ImageCodeUtil.LOGIN_IMAGECODE_SESSIONKEY);
+//      throw new ValidateCodeException("验证码已过期");
+//    }
+//    if (!StringUtil.equalsIgnoreCase(imageCode.getText(), inputCode)) {
+//      throw new ValidateCodeException("验证码不匹配");
+//    }
+//    request.getSession().removeAttribute(ImageCodeUtil.LOGIN_IMAGECODE_SESSIONKEY);
+  }
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/redisrepo/ApiJwtRepository.java b/backend/src/main/java/com/supwisdom/dlpay/framework/redisrepo/ApiJwtRepository.java
new file mode 100644
index 0000000..38a3fbe
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/redisrepo/ApiJwtRepository.java
@@ -0,0 +1,7 @@
+package com.supwisdom.dlpay.framework.redisrepo;
+
+import com.supwisdom.dlpay.framework.domain.JwtRedis;
+import org.springframework.data.repository.CrudRepository;
+
+public interface ApiJwtRepository extends CrudRepository<JwtRedis, String> {
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/security/CodeUtil.java b/backend/src/main/java/com/supwisdom/dlpay/framework/security/CodeUtil.java
new file mode 100644
index 0000000..ef3b475
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/security/CodeUtil.java
@@ -0,0 +1,45 @@
+package com.supwisdom.dlpay.framework.security;
+
+import javax.servlet.http.HttpServletRequest;
+
+public class CodeUtil {
+  /**
+   * 将获取到的前端参数转为string类型
+   *
+   * @param request
+   * @param key
+   * @return
+   */
+  public static String getString(HttpServletRequest request, String key) {
+    try {
+      String result = request.getParameter(key);
+      if (result != null) {
+        result = result.trim();
+      }
+      if ("".equals(result)) {
+        result = null;
+      }
+      return result;
+    } catch (Exception e) {
+      return null;
+    }
+  }
+
+  /**
+   * 验证码校验
+   *
+   * @param request
+   * @return
+   */
+  public static boolean checkVerifyCode(HttpServletRequest request) {
+    //获取生成的验证码
+    String verifyCodeExpected = (String) request.getSession()
+        .getAttribute(com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY);
+    //获取用户输入的验证码
+    String verifyCodeActual = CodeUtil.getString(request, "verifyCodeActual");
+    if (verifyCodeActual == null || !verifyCodeActual.equals(verifyCodeExpected)) {
+      return false;
+    }
+    return true;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/security/MyAuthenticationFailureHandler.java b/backend/src/main/java/com/supwisdom/dlpay/framework/security/MyAuthenticationFailureHandler.java
new file mode 100644
index 0000000..4882b83
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/security/MyAuthenticationFailureHandler.java
@@ -0,0 +1,45 @@
+package com.supwisdom.dlpay.framework.security;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.supwisdom.dlpay.exception.ValidateCodeException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.LockedException;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Component("myAuthenticationFailureHandler")
+public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
+
+  @Autowired
+  private ObjectMapper objectMapper;
+
+
+  @Override
+  public void onAuthenticationFailure(HttpServletRequest request,
+                                      HttpServletResponse response, AuthenticationException exception)
+      throws IOException, ServletException {
+
+    logger.error("登录失败:" + exception.getMessage() + "|" + exception.getClass());
+    String errmsg = "登录失败";
+    if (exception instanceof BadCredentialsException) {
+      errmsg = "账号或密码错误";
+    } else if (exception instanceof LockedException) {
+      errmsg = "账号被锁定";
+    } else if (exception instanceof ValidateCodeException) {
+      errmsg = exception.getMessage();
+    }
+    setDefaultFailureUrl("/login");
+    super.onAuthenticationFailure(request, response, new ValidateCodeException(errmsg));
+    /*response.setTransStatus(HttpStatus.OK.value());
+    response.setContentType("application/json;charset=UTF-8");
+    response.getWriter().write(objectMapper.writeValueAsString(JsonResult.error(400, errmsg)));*/
+  }
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/security/ValidateCodeSecurityConfig.java b/backend/src/main/java/com/supwisdom/dlpay/framework/security/ValidateCodeSecurityConfig.java
new file mode 100644
index 0000000..5814fd0
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/security/ValidateCodeSecurityConfig.java
@@ -0,0 +1,24 @@
+package com.supwisdom.dlpay.framework.security;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.web.DefaultSecurityFilterChain;
+import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.Filter;
+
+@Component("validateCodeSecurityConfig")
+public class ValidateCodeSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain,HttpSecurity> {
+
+	@Autowired
+	private Filter validateCodeFilter;
+
+
+	@Override
+	public void configure(HttpSecurity http) throws Exception {
+		http.addFilterBefore(validateCodeFilter, AbstractPreAuthenticatedProcessingFilter.class);
+	}
+	
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/service/OperatorDetailService.java b/backend/src/main/java/com/supwisdom/dlpay/framework/service/OperatorDetailService.java
new file mode 100644
index 0000000..bf72bfe
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/service/OperatorDetailService.java
@@ -0,0 +1,6 @@
+package com.supwisdom.dlpay.framework.service;
+
+import org.springframework.security.core.userdetails.UserDetailsService;
+
+public interface OperatorDetailService extends UserDetailsService {
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/service/SystemUtilService.java b/backend/src/main/java/com/supwisdom/dlpay/framework/service/SystemUtilService.java
new file mode 100644
index 0000000..7128f2c
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/service/SystemUtilService.java
@@ -0,0 +1,23 @@
+package com.supwisdom.dlpay.framework.service;
+
+import com.supwisdom.dlpay.framework.data.SystemDateTime;
+import org.springframework.transaction.annotation.Transactional;
+
+public interface SystemUtilService {
+
+  /**
+   * 取数据库日期
+   */
+  @Transactional
+  SystemDateTime getSysdatetime();
+
+  /**
+   * 获取流水号
+   */
+  @Transactional
+  String getRefno();
+
+  int getSysparaValueAsInt(int paraid, int defaultValue);
+
+  String getBusinessValue(String parakey);
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/service/impl/OperatorDetailServiceImpl.java b/backend/src/main/java/com/supwisdom/dlpay/framework/service/impl/OperatorDetailServiceImpl.java
new file mode 100644
index 0000000..755828d
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/service/impl/OperatorDetailServiceImpl.java
@@ -0,0 +1,42 @@
+package com.supwisdom.dlpay.framework.service.impl;
+
+import com.supwisdom.dlpay.framework.dao.OperRoleDao;
+import com.supwisdom.dlpay.framework.dao.OperatorDao;
+import com.supwisdom.dlpay.framework.domain.TOperator;
+import com.supwisdom.dlpay.framework.service.OperatorDetailService;
+import com.supwisdom.dlpay.framework.util.StringUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+@Service
+public class OperatorDetailServiceImpl implements OperatorDetailService {
+  @Autowired
+  private OperatorDao operatorDao;
+  @Autowired
+  private OperRoleDao operRoleDao;
+
+  @Override
+  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+    TOperator oper = operatorDao.findByOpercode(username);
+    if (null == oper) {
+      throw new UsernameNotFoundException("管理员不存在");
+    }
+    Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>() {
+    };
+
+    List<String> roles = operRoleDao.getRolecodeByOperid(oper.getOperid());
+    if (!StringUtil.isEmpty(roles)) {
+      authorities = AuthorityUtils.createAuthorityList(roles.toArray(new String[0]));
+    }
+    oper.setAuthorities(authorities);
+    return oper;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/service/impl/SystemUtilServiceImpl.java b/backend/src/main/java/com/supwisdom/dlpay/framework/service/impl/SystemUtilServiceImpl.java
new file mode 100644
index 0000000..92a2acd
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/service/impl/SystemUtilServiceImpl.java
@@ -0,0 +1,73 @@
+package com.supwisdom.dlpay.framework.service.impl;
+
+import com.supwisdom.dlpay.framework.core.DatabaseConfig;
+import com.supwisdom.dlpay.framework.dao.*;
+import com.supwisdom.dlpay.framework.data.SystemDateTime;
+import com.supwisdom.dlpay.framework.domain.*;
+import com.supwisdom.dlpay.framework.service.SystemUtilService;
+import com.supwisdom.dlpay.framework.util.NumberUtil;
+import com.supwisdom.dlpay.framework.util.StringUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class SystemUtilServiceImpl implements SystemUtilService {
+  @Autowired
+  private DatabaseConfig databaseConfig;
+  @Autowired
+  private TaskLockDao taskLockDao;
+  @Autowired
+  private SysparaDao sysparaDao;
+  @Autowired
+  private BusinessparaDao businessparaDao;
+
+  /**
+   * 获取oracle数据库时间
+   */
+  private SystemDateTime getOracleDatetime() {
+    return taskLockDao.getOracleDatetime();
+  }
+
+  private SystemDateTime getPGDatetime() {
+    return taskLockDao.getPGDatetime();
+  }
+
+  @Override
+  public SystemDateTime getSysdatetime() {
+    switch (databaseConfig.getPlatform()) {
+      case "postgresql":
+        return getPGDatetime();
+      default:
+        return getOracleDatetime();
+    }
+  }
+
+  @Override
+  public String getRefno() {
+    switch (databaseConfig.getPlatform()) {
+      case "postgresql":
+        return taskLockDao.getPgRefno();
+      default:
+        return taskLockDao.getOracleRefno();
+    }
+  }
+
+  @Override
+  public int getSysparaValueAsInt(int paraid, int defaultValue) {
+    TSyspara syspara = sysparaDao.findByParaid(paraid);
+    if (null != syspara && NumberUtil.isNumber(syspara.getParaval()))
+      return Integer.parseInt(syspara.getParaval());
+    return defaultValue;
+  }
+
+
+  @Override
+  public String getBusinessValue(String parakey) {
+    if (!StringUtil.isEmpty(parakey)) {
+      TBusinesspara businesspara = businessparaDao.findByParakey(parakey.trim());
+      if (null != businesspara) return businesspara.getParaval();
+    }
+    return null;
+  }
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/tenant/TenantCacheKeyGen.java b/backend/src/main/java/com/supwisdom/dlpay/framework/tenant/TenantCacheKeyGen.java
new file mode 100644
index 0000000..254c4e4
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/tenant/TenantCacheKeyGen.java
@@ -0,0 +1,27 @@
+package com.supwisdom.dlpay.framework.tenant;
+
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.cache.interceptor.KeyGenerator;
+
+import java.lang.reflect.Method;
+
+public class TenantCacheKeyGen implements KeyGenerator {
+  private static final char delimiter = ':';
+
+  @Override
+  public Object generate(Object target, Method method, Object... params) {
+    StringBuilder name = new StringBuilder();
+    name.append(target.getClass().getSimpleName())
+        .append(delimiter);
+    String tenant = TenantContext.getTenantSchema();
+    if (StringUtils.isEmpty(tenant)) {
+      tenant = "default";
+    }
+    name.append(tenant).append(delimiter);
+    for (Object item : params) {
+      name.append(delimiter).append(item.toString());
+    }
+    return name.toString();
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/tenant/TenantContext.java b/backend/src/main/java/com/supwisdom/dlpay/framework/tenant/TenantContext.java
new file mode 100644
index 0000000..c1d4ca4
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/tenant/TenantContext.java
@@ -0,0 +1,20 @@
+package com.supwisdom.dlpay.framework.tenant;
+
+/**
+ * Created by shuwei on 2018/11/29.
+ */
+public class TenantContext {
+  private static ThreadLocal<String> currentTenant = new ThreadLocal<>();
+
+  public static void setTenantSchema(String tid) {
+    currentTenant.set(tid);
+  }
+
+  public static String getTenantSchema() {
+    return currentTenant.get();
+  }
+
+  public static void clear() {
+    currentTenant.set(null);
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/tenant/TenantHolder.java b/backend/src/main/java/com/supwisdom/dlpay/framework/tenant/TenantHolder.java
new file mode 100644
index 0000000..585767f
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/tenant/TenantHolder.java
@@ -0,0 +1,27 @@
+package com.supwisdom.dlpay.framework.tenant;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Component;
+
+@Component("tenantHolder")
+public class TenantHolder {
+  private static final char delimiter = '-';
+
+  public String getId() {
+    String id = TenantContext.getTenantSchema();
+    if (id == null || StringUtils.isEmpty(id)) {
+      return "default";
+    }
+    return id;
+  }
+
+  public String genKey(String prefix, String... args) {
+    StringBuilder builder = new StringBuilder();
+    builder.append(getId()).append(delimiter)
+        .append(prefix);
+    for (String item : args) {
+      builder.append(delimiter).append(item);
+    }
+    return builder.toString();
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/util/Constants.java b/backend/src/main/java/com/supwisdom/dlpay/framework/util/Constants.java
new file mode 100644
index 0000000..a58a7f5
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/util/Constants.java
@@ -0,0 +1,16 @@
+package com.supwisdom.dlpay.framework.util;
+
+public class Constants {
+  //  HTTP HEADER define
+  public static final String HEADER_TETANTID = "X-TENANT-ID";
+  public static final String DEFAULT_TENANTID = "{tenantid}";
+
+  // define
+  public static final String JWT_CLAIM_TENANTID = "tenantId";
+  public static final String JWT_CLAIM_UID = "uid";
+  public static final String JWT_CLAIM_AUTHORITIES = "authorities";
+  // 根商户ID
+  public static final Integer ROOT_SHOP_FID = 1;
+
+  public static final String HEADER_AUTHORIZATION = "Authorization";
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/util/DateUtil.java b/backend/src/main/java/com/supwisdom/dlpay/framework/util/DateUtil.java
new file mode 100644
index 0000000..3f445b2
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/util/DateUtil.java
@@ -0,0 +1,361 @@
+package com.supwisdom.dlpay.framework.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.Timestamp;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.TimeZone;
+
+public class DateUtil {
+  private static final Logger logger = LoggerFactory.getLogger(DateUtil.class);
+  public static final String DATE_FMT = "yyyyMMdd";
+  public static final String TIME_FMT = "HHmmss";
+  public static final String DATETIME_FMT = "yyyyMMddHHmmss";
+
+  /**
+   * Description: 返回一个当前时间 @return String 格式:yyyyMMddHHmmss @exception Modify
+   * History:
+   */
+  public static String getNow() {
+    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+    return sdf.format(new Date());
+  }
+
+
+  /**
+   * Description: 根据类型返回一个当前时间 @param partten String @return String 格式:partten
+   */
+  public static String getNow(String partten) {
+    SimpleDateFormat sdf = new SimpleDateFormat(partten);
+    return sdf.format(new Date());
+  }
+  /*
+  *
+  * */
+  public static String getNowInterDay(int intervalday) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
+      Date d = new Date();
+      Calendar calendar = Calendar.getInstance();
+      calendar.setTimeInMillis(d.getTime());
+      calendar.add(Calendar.DATE, intervalday);
+      return sdf.format(calendar.getTime());
+    } catch (Exception e) {
+      e.printStackTrace();
+      return null;
+    }
+  }
+
+  /**
+   * Description: 得到一个特殊的时间 @param startTime String 格式:yyyyMMddHHmmss @param
+   * interval int 秒 @return String 格式:partten @exception Modify History:
+   */
+  public static String getNewTime(String startTime, int interval) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+      Date d = sdf.parse(startTime);
+      Calendar calendar = Calendar.getInstance();
+      calendar.setTimeInMillis(d.getTime());
+      calendar.add(Calendar.SECOND, interval);
+      return sdf.format(calendar.getTime());
+    } catch (ParseException e) {
+      return startTime;
+    }
+  }
+
+  /**
+   * Description: 得到一个特殊的时间 @param startTime String 格式:partten @param
+   * interval int 秒 @return String 格式:partten @exception Modify History:
+   */
+  public static String getNewTime(String startTime, int interval, String partten) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat(partten);
+      Date d = sdf.parse(startTime);
+      Calendar calendar = Calendar.getInstance();
+      calendar.setTimeInMillis(d.getTime());
+      calendar.add(Calendar.SECOND, interval);
+      return sdf.format(calendar.getTime());
+    } catch (ParseException e) {
+      return startTime;
+    }
+  }
+
+  public static String getNewDay(String startDay, int intervalday) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
+      Date d = sdf.parse(startDay);
+      Calendar calendar = Calendar.getInstance();
+      calendar.setTimeInMillis(d.getTime());
+      calendar.add(Calendar.DATE, intervalday);
+      return sdf.format(calendar.getTime());
+    } catch (ParseException e) {
+      return startDay;
+    }
+  }
+
+  /**
+   * 得到两个日期相差的天数 格式 yyyyMMdd @return diffdays = secondDay - firstDay
+   */
+  public static long getIntervalDay(String firstDay, String secondDay) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
+      Date f = sdf.parse(firstDay);
+      Date s = sdf.parse(secondDay);
+      long time = s.getTime() - f.getTime();
+      return time / (24 * 60 * 60 * 1000);
+    } catch (ParseException e) {
+      return 0;
+    }
+  }
+
+  /**
+   * Description: 比较两个时间字符串的前后关系 @param firstTime String 格式:yyyyMMddHHmmss
+   *
+   * @param secondTime String 格式: yyyyMMddHHmmss @return int |
+   *                   firstTime=second int=0 | firstTime>secondTime int>0 |
+   *                   firstTime<secondTime int<0 @exception Modify History:
+   */
+  public static int compareDatetime(String firstTime, String secondTime) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+      Date f = sdf.parse(firstTime);
+      Date s = sdf.parse(secondTime);
+      return f.compareTo(s);
+    } catch (ParseException e) {
+      return 0;
+    }
+  }
+
+  /**
+   * Description: 比较两个时间字符串的前后关系 @param firstTime String 格式:pattern
+   *
+   * @param secondTime String 格式: yyyyMMddHHmmss @return int |
+   *                   firstTime=second int=0 | firstTime>secondTime int>0 |
+   *                   firstTime<secondTime int<0 @exception Modify History:
+   */
+  public static int compareDatetime(String firstTime, String secondTime, String pattern) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat(pattern);
+      Date f = sdf.parse(firstTime);
+      Date s = sdf.parse(secondTime);
+      return f.compareTo(s);
+    } catch (ParseException e) {
+      return 0;
+    }
+  }
+
+  /**
+   * Description: 比较两个时间字符串的时间差 @param firstTime String 格式:yyyyMMddHHmmss
+   *
+   * @param secondTime String 格式: yyyyMMddHHmmss @param second int 格式 @return
+   *                   int | firstTime+seconds=secondTime int=0 | firstTime+seconds>secondTime
+   *                   int>0 | firstTime+seconds<secondTime int<0 @exception Modify History:
+   */
+  public static int compareDatetime(String firstTime, String secondTime, int seconds) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+      Date f = sdf.parse(firstTime);
+      Date s = sdf.parse(secondTime);
+      Calendar calendar = Calendar.getInstance();
+      calendar.setTimeInMillis(f.getTime());
+      calendar.add(Calendar.SECOND, seconds);
+      Date temp = calendar.getTime();
+      return temp.compareTo(s);
+    } catch (Exception e) {
+      return 0;
+    }
+  }
+
+  /**
+   * Description: 对time重新格式化
+   */
+  public static String reformatDatetime(String time, String fromPattern, String toPattern) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat(fromPattern);
+      Date d = sdf.parse(time);
+      Calendar calendar = Calendar.getInstance();
+      calendar.setTimeInMillis(d.getTime());
+      sdf = new SimpleDateFormat(toPattern);
+      return sdf.format(calendar.getTime());
+    } catch (Exception e) {
+      e.printStackTrace();
+      return time;
+    }
+  }
+
+  /**
+   * 获得两个字符串日期之间的时间差(单位毫秒) 格式 yyyyMMddHHmmss
+   */
+  public static long getInterval(String startTime, String endTime) {
+    long duration = 0;
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+      duration = sdf.parse(endTime).getTime() - sdf.parse(startTime).getTime();
+    } catch (ParseException e) {
+      logger.error("Hi guys,there is an error when you try to parse the date string");
+    }
+    return duration;
+  }
+
+  /**
+   * 获得两个字符串日期之间的时间差(单位毫秒)
+   */
+  public static long getIntervalTime(String startTime, String endTime, String pattern) {
+    long duration = 0;
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat(pattern);
+      duration = sdf.parse(endTime).getTime() - sdf.parse(startTime).getTime();
+    } catch (ParseException e) {
+      logger.error("Hi guys,there is an error when you try to parse the date string");
+    }
+    return duration;
+  }
+
+  /**
+   * 转换成日期格式
+   * 短格式:20140401 -> 2014-04-01
+   * 中格式:201404011200 -> 2014-04-01 12:00
+   * 长格式:20140401123025 -> 2014-04-01 12:30:25
+   **/
+  public static String parseToDateFormat(String str) {
+    switch (str.length()) {
+      case 8:
+        str = str.substring(0, 4) + "-" + str.substring(4, 6) + "-" + str.substring(6, 8);
+        break;
+      case 12:
+        str = str.substring(0, 4) + "-" + str.substring(4, 6) + "-" + str.substring(6, 8) + " " + str.substring(8, 10) + ":" + str.substring(10, 12);
+        break;
+      case 14:
+        str = str.substring(0, 4) + "-" + str.substring(4, 6) + "-" + str.substring(6, 8) + " " + str.substring(8, 10) + ":" + str.substring(10, 12) + ":" + str.substring(12, 14);
+        break;
+      default:
+        break;
+    }
+    return str;
+  }
+
+  /**
+   * 解日期格式
+   * 短格式:2014-04-01 -> 20140401
+   * 中格式:2014-04-01 12:00 -> 201404011200
+   * 长格式:2014-04-01 12:30:25 -> 20140401123025
+   **/
+  public static String unParseToDateFormat(String str) {
+    return str.replaceAll("-", "").replaceAll(" ", "").replaceAll(":", "");
+  }
+
+  /**
+   * 检验时间格式
+   */
+  public static boolean checkDatetimeValid(String datetime, String pattern) {
+    if (null == datetime) return false;
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat(pattern);
+      Date d = sdf.parse(datetime);
+      return datetime.trim().equals(sdf.format(d));
+    } catch (Exception e) {
+    }
+    return false;
+  }
+
+  /**
+   * 获取指定日期是星期几 格式 yyyyMMdd
+   * MON|TUE|WED|THU|FRI|SAT|SUN
+   * 1		2		3		4		5		6		7
+   */
+  public static int getWeekday(String datestr) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
+      Calendar calendar = Calendar.getInstance();
+      boolean isFirstSunday = (calendar.getFirstDayOfWeek() == Calendar.SUNDAY); //一周第一天是否为星期天
+      Date d = sdf.parse(datestr);
+      calendar.setTimeInMillis(d.getTime());
+      int weekDay = calendar.get(calendar.DAY_OF_WEEK);
+      if (isFirstSunday) {
+        weekDay = weekDay - 1;
+        if (weekDay == 0) {
+          weekDay = 7;
+        }
+      }
+      return weekDay;
+    } catch (Exception e) {
+      return -1;
+    }
+  }
+
+  /**
+   * 获取指定日期
+   */
+  public static Date getSpecifyDate(String datestr, String pattern) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat(pattern);
+      Date result = sdf.parse(datestr);
+      return result;
+    } catch (Exception e) {
+      return new Date();
+    }
+  }
+
+  public static Integer getLastDayOfMonth(Integer year, Integer month) {
+    Calendar cal = Calendar.getInstance();
+    cal.set(Calendar.YEAR, year);
+    cal.set(Calendar.MONTH, month - 1);
+    cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DATE));
+    String str = new SimpleDateFormat("yyyyMMdd ").format(cal.getTime()).toString();
+    Integer result = Integer.parseInt(str.substring(0, 4) + str.substring(4, 6) + str.substring(6, 8));
+    return result;
+  }
+
+  private static Date set(Date date, int calendarField, int amount) {
+    Calendar c = Calendar.getInstance();
+    c.setLenient(false);
+    c.setTime(date);
+    c.add(calendarField, amount);
+    return c.getTime();
+  }
+
+
+  public static Date setMinutes(Date date, int amount) {
+    return set(date, Calendar.MINUTE, amount);
+  }
+
+
+  public static long getNowSecond() {
+    Calendar calendar = Calendar.getInstance();
+    return calendar.getTimeInMillis() / 1000;
+  }
+
+
+  public static String getUTCTime(Long timeInMillisSecond) {
+    Calendar time = Calendar.getInstance();
+    SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
+    fmt.setTimeZone(TimeZone.getTimeZone("UTC"));
+    time.setTimeInMillis(timeInMillisSecond);
+    return fmt.format(time.getTime());
+  }
+
+  public static String getUTCTime() {
+    return getUTCTime(System.currentTimeMillis());
+  }
+
+  public static int compareDay(Timestamp d1, Timestamp d2) {
+    Calendar cd1 = Calendar.getInstance();
+    cd1.setTimeInMillis(d1.getTime());
+    Calendar cd2 = Calendar.getInstance();
+    cd2.setTimeInMillis(d2.getTime());
+
+    if (cd1.get(Calendar.YEAR) != cd2.get(Calendar.YEAR)) {
+      return cd1.compareTo(cd2);
+    }
+
+    return Integer.compare(cd1.get(Calendar.DAY_OF_YEAR), cd2.get(Calendar.DAY_OF_YEAR));
+  }
+
+  public static Boolean sameDay(Timestamp d1, Timestamp d2) {
+    return (compareDay(d1, d2) == 0);
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/util/Dictionary.java b/backend/src/main/java/com/supwisdom/dlpay/framework/util/Dictionary.java
new file mode 100644
index 0000000..9eac02d
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/util/Dictionary.java
@@ -0,0 +1,17 @@
+package com.supwisdom.dlpay.framework.util;
+
+public class Dictionary {
+  // dictionary 表字典
+  public static final String REVERSE_FLAG = "reverseflagList";
+  public static final String DTL_STATUS = "dtlStatusList";
+  public static final String IDTYPE = "idtypeList";
+  public static final String SEX = "sexList";
+  public static final String ACCOUNT_STATUS = "accountStatusList";
+  public static final String DTLTYPES = "dtltypeList";
+
+  /////////////////////////////////////
+  public static final String SOURCE_TYPE = "sourcetypeList";
+  public static final String TRANS_CODE = "transcodeList";
+  public static final String PAY_TYPE = "paytypelist";
+  public static final String ALL_SUBJECT = "allSubjectList";
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/util/MD5.java b/backend/src/main/java/com/supwisdom/dlpay/framework/util/MD5.java
new file mode 100644
index 0000000..bcf4445
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/util/MD5.java
@@ -0,0 +1,105 @@
+package com.supwisdom.dlpay.framework.util;
+
+import org.apache.commons.codec.binary.Base64;
+
+import java.security.MessageDigest;
+
+public class MD5 {
+	//十六进制下数字到字符的映射数组  
+    private final static String[] hexDigits = {"0", "1", "2", "3", "4",
+        "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};  
+      
+    /** * 把inputString加密     */  
+    public static String generatePassword(String data, String salt){
+    	String enyptString = data+"{"+salt+"}";
+        return encodeByMD5(enyptString);  
+    }  
+      
+      /** 
+       * 验证输入的密码是否正确 
+     * @param password    加密后的密码 
+     * @param inputString    输入的字符串 
+     * @return    验证结果,TRUE:正确 FALSE:错误 
+     */  
+    public static boolean validatePassword(String password, String inputString){
+        if(password.equals(encodeByMD5(inputString))){  
+            return true;  
+        } else{  
+            return false;  
+        }  
+    }  
+    /**  对字符串进行MD5加密     */
+    public static String encodeByMD5(String originString){
+        if (originString != null){  
+            try{  
+                //创建具有指定算法名称的信息摘要  
+                MessageDigest md = MessageDigest.getInstance("MD5");
+                //使用指定的字节数组对摘要进行最后更新,然后完成摘要计算  
+                byte[] results = md.digest(originString.getBytes("UTF-8"));
+                //将得到的字节数组变成字符串返回  
+                String resultString = byteArrayToHexString(results);
+                return resultString.toUpperCase();  
+            } catch(Exception ex){
+                ex.printStackTrace();  
+            }  
+        }  
+        return null;  
+    }  
+      
+    /**  
+     * 转换字节数组为十六进制字符串 
+     * @param     字节数组 
+     * @return    十六进制字符串 
+     */  
+    private static String byteArrayToHexString(byte[] b){
+        StringBuffer resultSb = new StringBuffer();
+        for (int i = 0; i < b.length; i++){  
+            resultSb.append(byteToHexString(b[i]));  
+        }  
+        return resultSb.toString();  
+    }  
+      
+    /** 将一个字节转化成十六进制形式的字符串     */  
+    private static String byteToHexString(byte b){
+        int n = b;  
+        if (n < 0)  
+            n = 256 + n;  
+        int d1 = n / 16;  
+        int d2 = n % 16;  
+        return hexDigits[d1] + hexDigits[d2];  
+    }
+
+    public static String encodeByMD5ForDlpay(String originString) {
+        if (originString != null) {
+            try {
+                //创建具有指定算法名称的信息摘要
+                MessageDigest md = MessageDigest.getInstance("MD5");
+                //使用指定的字节数组对摘要进行最后更新,然后完成摘要计算
+                byte[] results = md.digest(originString.getBytes("GBK"));
+                //将得到的字节数组变成字符串返回
+                String resultString = byteArrayToHexString(results);
+                return resultString;
+            } catch (Exception ex) {
+                ex.printStackTrace();
+            }
+        }
+        return null;
+    }
+    /** 对字符串进行MD5加密 */
+    public static String encodeByMD5ToURLSafeBase64(String originString) {
+        if (originString != null) {
+            try {
+                // 创建具有指定算法名称的信息摘要
+                MessageDigest md = MessageDigest.getInstance("MD5");
+                // 使用指定的字节数组对摘要进行最后更新,然后完成摘要计算
+                byte[] results = md.digest(originString.getBytes("utf-8"));
+                // 将得到的字节数组变成字符串返回
+                String resultString = Base64.encodeBase64URLSafeString(results);
+                return resultString.toUpperCase();
+            } catch (Exception ex) {
+                ex.printStackTrace();
+            }
+        }
+        return null;
+    }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/util/MoneyUtil.java b/backend/src/main/java/com/supwisdom/dlpay/framework/util/MoneyUtil.java
new file mode 100644
index 0000000..d384340
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/util/MoneyUtil.java
@@ -0,0 +1,35 @@
+package com.supwisdom.dlpay.framework.util;
+
+import java.text.DecimalFormat;
+
+public class MoneyUtil {
+
+  public static int YuanToFen(double yuan) {
+    return (int) (Math.round(yuan * 100));
+  }
+
+  public static boolean moneyEqual(double x1, double x2) {
+    return YuanToFen(x1) == YuanToFen(x2);
+  }
+
+  public static int moneyCompare(double x1, double x2) {
+    return YuanToFen(x1) - YuanToFen(x2);
+  }
+
+  public static double formatYuan(double yuan) {
+    DecimalFormat df = new DecimalFormat("##0.00");
+    double money = ((double) YuanToFen(yuan)) / 100;
+    return Double.valueOf(df.format(money));
+  }
+
+  public static String formatYuanToString(Double yuan) {
+    if (null == yuan) return "0.00";
+    DecimalFormat df = new DecimalFormat("##0.00");
+    double money = ((double) YuanToFen(yuan)) / 100;
+    return df.format(money);
+  }
+  public static double FenToYuan(int fen) {
+    return formatYuan(fen / 100.0);
+  }
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/util/NumberUtil.java b/backend/src/main/java/com/supwisdom/dlpay/framework/util/NumberUtil.java
new file mode 100644
index 0000000..a8d4390
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/util/NumberUtil.java
@@ -0,0 +1,65 @@
+package com.supwisdom.dlpay.framework.util;
+
+public class NumberUtil {
+
+  /**
+   * 判断是否是非负整数
+   *
+   * @param str
+   * @return 是 -> true;  否 -> false 如 1 -> true; -1 -> false
+   */
+  public static boolean isDigits(String str) {
+    if (null == str || "".equals(str.trim())) return false;
+    java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("[0-9]*");
+    java.util.regex.Matcher match = pattern.matcher(str.trim());
+    return match.matches();
+  }
+
+  /**
+   * 判断是否是整数(包含正负)
+   *
+   * @param str
+   * @return 是 -> true;  否 -> false  如 -1 -> true; -1.0 -> false
+   */
+  public static boolean isNumber(String str) {
+    if (null == str) return false;
+    try {
+      Integer.valueOf(str);
+      return true;
+    } catch (NumberFormatException ex) {
+      return false;
+    }
+  }
+
+  /**
+   * 判断是否是数字
+   *
+   * @param str
+   * @return 是 -> true;  否 -> false  如 -1 -> true; asd.1sa -> false
+   */
+  public static boolean isDecimal(String str) {
+    if (null == str) return false;
+    try {
+      Double.valueOf(str.trim());
+      return true;
+    } catch (NumberFormatException ex) {
+      return false;
+    }
+  }
+
+  /**
+   * 判断是否是金额(包含正负,最多2位小数)
+   *
+   * @param str
+   * @return 是 -> true;  否 -> false  如 -1.10 -> true; -1.011 -> false
+   */
+  public static boolean isAmount(String str) {
+    if (!isDecimal(str)) return false;
+    if (str.trim().indexOf(".") != -1) {
+      String suffix = str.trim().substring(str.trim().indexOf(".") + 1);
+      if (suffix.length() > 2) return false; //小数位多余2位
+    }
+    return true;
+  }
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/util/PageResult.java b/backend/src/main/java/com/supwisdom/dlpay/framework/util/PageResult.java
new file mode 100644
index 0000000..2c2694c
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/util/PageResult.java
@@ -0,0 +1,87 @@
+package com.supwisdom.dlpay.framework.util;
+
+import org.springframework.data.domain.Page;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PageResult<T> {
+
+  private int code; //状态码, 0表示成功
+
+  private String msg;  //提示信息
+
+  private long count; // 总数量
+
+  private List<T> data; // 当前数据
+
+  public PageResult() {
+  }
+
+  public PageResult(List<T> rows) {
+    this.data = rows;
+    this.count = rows.size();
+    this.code = 0;
+    this.msg = "";
+  }
+
+  public PageResult(long total, List<T> rows) {
+    this.count = total;
+    this.data = rows;
+    this.code = 0;
+    this.msg = "";
+  }
+
+  public PageResult(int code, String msg) {
+    this.code = code;
+    this.msg = msg;
+    this.count = 0;
+    this.data = new ArrayList<>(0);
+  }
+
+  public PageResult(Page<T> page){
+    if(null == page) {
+      this.code = 99;
+      this.msg = "无数据";
+      this.count = 0;
+      this.data = new ArrayList<>(0);
+    } else{
+      this.code = 0;
+      this.msg = "";
+      this.count = page.getTotalElements();
+      this.data = page.getContent();
+    }
+  }
+
+  public int getCode() {
+    return code;
+  }
+
+  public void setCode(int code) {
+    this.code = code;
+  }
+
+  public String getMsg() {
+    return msg;
+  }
+
+  public void setMsg(String msg) {
+    this.msg = msg;
+  }
+
+  public long getCount() {
+    return count;
+  }
+
+  public void setCount(long count) {
+    this.count = count;
+  }
+
+  public List<T> getData() {
+    return data;
+  }
+
+  public void setData(List<T> data) {
+    this.data = data;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/util/RandomUtils.java b/backend/src/main/java/com/supwisdom/dlpay/framework/util/RandomUtils.java
new file mode 100644
index 0000000..74a0534
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/util/RandomUtils.java
@@ -0,0 +1,108 @@
+package com.supwisdom.dlpay.framework.util;
+
+import org.apache.commons.codec.binary.Base32;
+import org.apache.commons.codec.binary.Hex;
+
+import java.security.SecureRandom;
+import java.util.Random;
+import java.util.UUID;
+
+/**
+ * 获取随机数类
+ * @author yanhua_mao
+ *
+ */
+public class RandomUtils {
+	
+	/**
+	 * 获取随机数
+	 * @param length 位数
+	 * @return 返回随机数
+	 */
+	public static final String randomNumber(int length) {
+		char[] numbersAndLetters = null;
+		Random randGen = null;
+		if (length < 1) {
+			return null;
+		}
+		if (randGen == null) {
+			randGen = new Random();
+			numbersAndLetters = ("0123456789").toCharArray();
+		}
+		char[] randBuffer = new char[length];
+		for (int i = 0; i < randBuffer.length; i++) {
+			randBuffer[i] = numbersAndLetters[randGen.nextInt(9)];
+		}
+		return new String(randBuffer);
+	}
+	
+	/** 
+     * 产生一个随机的字符串 
+     *  
+     * @param 字符串长度 
+     * @return 
+     */  
+    public static String getRandomString(int length) {  
+        String base = "abcdefghijklmnopqrstuvwxyz234567";  
+        Random random = new Random();  
+        StringBuffer sb = new StringBuffer();  
+        for (int i = 0; i < length; i++) {  
+            int number = random.nextInt(base.length());  
+            sb.append(base.charAt(number));  
+        }  
+        return sb.toString();  
+    }
+    
+    public static String getSecureRandom(){
+    	SecureRandom a = new SecureRandom();
+		byte[] phonesecret = new byte[10];
+		a.nextBytes(phonesecret);
+		
+		Base32 c = new Base32();
+		String s = c.encodeAsString(phonesecret);
+		String savesecret = Hex.encodeHexString(phonesecret);
+    	String result = s + "_"+savesecret;
+		return result;
+    }
+
+	public static String getSecureRandomHex() {
+		SecureRandom a = new SecureRandom();
+		byte[] phonesecret = new byte[10];
+		a.nextBytes(phonesecret);
+
+		String savesecret = Hex.encodeHexString(phonesecret);
+		return savesecret;
+	}
+	/**
+	 * 
+	 * @param length
+	 * @return
+	 */
+	public static String getCharAndNumr(int length)     
+	{     
+	    String val = "";     
+	             
+	    Random random = new Random();     
+	    for(int i = 0; i < length; i++)     
+	    {     
+	        String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num"; // 输出字母还是数字     
+	                 
+	        if("char".equalsIgnoreCase(charOrNum)) // 字符串     
+	        {     
+	            int choice = random.nextInt(2) % 2 == 0 ? 65 : 97; //取得大写字母还是小写字母     
+	            val += (char) (choice + random.nextInt(26));     
+	        }     
+	        else if("num".equalsIgnoreCase(charOrNum)) // 数字     
+	        {     
+	            val += String.valueOf(random.nextInt(10));     
+	        }     
+	    }     
+	             
+	    return val;     
+	}
+
+	public static String getUUIDStr() {
+		return UUID.randomUUID().toString().replaceAll("-", "");
+	}
+    
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/util/Signature.java b/backend/src/main/java/com/supwisdom/dlpay/framework/util/Signature.java
new file mode 100644
index 0000000..a0173a0
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/util/Signature.java
@@ -0,0 +1,22 @@
+package com.supwisdom.dlpay.framework.util;
+
+import com.supwisdom.dlpay.api.util.HMACUtil;
+import com.supwisdom.dlpay.framework.tenant.TenantContext;
+
+public class Signature {
+  public static final String SPY_TAC = "DLzJi044R7QHhJCDhpjZId8d";
+  private static final String ROOT_STATIC_KEY = "WfFTw42/2JnHP1Qjs4hnMstgANbhRvbXL84rNg==";
+
+  private static String deliveryKey(String tenantId, String factor) {
+    return HMACUtil.sha256HMAC(tenantId + factor, ROOT_STATIC_KEY);
+  }
+
+  public static String generateTac(String factor, String data) {
+    String tenant = TenantContext.getTenantSchema();
+    if (tenant == null) {
+      throw new IllegalArgumentException("TenantID 未定义");
+    }
+//    System.out.println("factor="+factor+",data="+data+",tenant="+tenant);
+    return HMACUtil.sha256HMAC(deliveryKey(tenant, factor), data).substring(0, 24);
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/util/StringUtil.java b/backend/src/main/java/com/supwisdom/dlpay/framework/util/StringUtil.java
new file mode 100644
index 0000000..08546cd
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/util/StringUtil.java
@@ -0,0 +1,275 @@
+package com.supwisdom.dlpay.framework.util;
+
+
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class StringUtil {
+  /**
+   * 判断字符串是否为空
+   */
+  public static boolean isEmpty(String str) {
+    if (null == str || "".equals(str) || "".equals(str.trim())) {
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * 判断List是否为空
+   */
+  public static boolean isEmpty(List list) {
+    if (null == list || list.size() <= 0) {
+      return true;
+    }
+    return false;
+  }
+
+  public static boolean equalsIgnoreCase(String str1, String str2) {
+    return str1 == null ? str2 == null : str1.equalsIgnoreCase(str2);
+  }
+
+  public static boolean equals(String str1, String str2) {
+    return str1 == null ? str2 == null : str1.equals(str2);
+  }
+
+  /**
+   * 手机号验证
+   *
+   * @param str
+   * @return
+   */
+  public static boolean isMobile(String str) {
+    Pattern p = Pattern.compile("^1[0-9]{10}$"); // 验证手机号
+    Matcher m = p.matcher(str);
+    return m.matches();
+  }
+
+  /**
+   * 邮箱格式验证
+   */
+  public static boolean isEmail(String email) {
+    boolean ret = true;
+    Pattern pattern = Pattern.compile("^([a-zA-Z0-9]+[_|\\.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|\\.]?)*[a-zA-Z0-9]+\\.[a-zA-Z]{2,3}$");
+    final Matcher mat = pattern.matcher(email);
+    if (!mat.find()) {
+      ret = false;
+    }
+    return ret;
+  }
+
+  /**
+   * 身份证格式验证
+   *
+   * @param str
+   * @return
+   */
+  public static boolean isIdentity(String str) {
+    // 中国公民身份证格式:长度为15或18位,最后一位可以为字母
+    Pattern pattern = Pattern.compile("(\\d{14}[0-9a-zA-Z])|(\\d{17}[0-9a-zA-Z])");
+    final Matcher m = pattern.matcher(str);
+    return m.matches();
+  }
+
+  /**
+   * 除去数组中的空值和签名参数
+   *
+   * @param sArray 签名参数组
+   * @return 去掉空值与签名参数后的新签名参数组
+   */
+  public static Map<String, String> paraFilter(Map<String, String> sArray) {
+    Map<String, String> result = new HashMap<String, String>();
+    if (sArray == null || sArray.size() <= 0) {
+      return result;
+    }
+    for (String key : sArray.keySet()) {
+      String value = sArray.get(key);
+      if (null == value || "".equals(value.trim()) || "null".equalsIgnoreCase(value.trim()) || key.equalsIgnoreCase("sign") || key.equalsIgnoreCase("sign_type")) {
+        continue;
+      }
+      result.put(key, value);
+    }
+    return result;
+  }
+
+  /**
+   * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
+   *
+   * @param params 需要排序并参与字符拼接的参数组
+   * @return 拼接后字符串
+   */
+  public static String createLinkString(Map<String, String> params) {
+
+    List<String> keys = new ArrayList<String>(params.keySet());
+    Collections.sort(keys);
+
+    String prestr = "";
+
+    for (int i = 0; i < keys.size(); i++) {
+      String key = keys.get(i);
+      String value = params.get(key);
+
+      if (i == keys.size() - 1) {//拼接时,不包括最后一个&字符
+        prestr = prestr + key + "=" + value;
+      } else {
+        prestr = prestr + key + "=" + value + "&";
+      }
+    }
+
+    return prestr;
+  }
+
+  public static boolean isCharAndNum(String inputStr) {
+    //有问题
+    /*Pattern p = Pattern.compile("\\w+");
+    Matcher m = p.matcher(inputStr);
+    if (m.matches()) {
+      // 除字母和数字外还包含其它字符
+      return false;
+    } else {
+      return true;
+    }*/
+    return true;
+  }
+
+  /**
+   * 手机号遮掩中间4位
+   */
+  public static String phoneReplace(String phone) {
+    if(phone==null){
+      return phone;
+    }
+    return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
+  }
+
+  /**
+   * 邮箱只显示@前面的首位和末位
+   */
+  public static String emailReplace(String s) {
+    return s.replaceAll("(\\w?)(\\w+)(\\w)(@\\w+\\.[a-z]+(\\.[a-z]+)?)", "$1****$3$4");
+  }
+
+  /**
+   * 名字显示姓
+   */
+  public static String nameReplace(String s) {
+    return s.replaceAll("([\\d\\D]{1})(.*)", "$1**");
+  }
+
+
+  public static String urlAppend(String url, String path) {
+    if (url.endsWith("/")) {
+      if (path.startsWith("/")) {
+        return url + path.substring(1);
+      }
+      return url + path;
+    } else {
+      if (path.startsWith("/")) {
+        return url + path;
+      }
+      return url + "/" + path;
+    }
+  }
+
+  public static void transforToBean(List<String> fields, List<String> columns, Object bean) throws Exception {
+    if (null == fields) throw new Exception("fields is null");
+    if (null == columns) throw new Exception("columns is null");
+    if (fields.size() < columns.size()) throw new Exception("错误的列定义");
+    Map<String, String> data = new HashMap<>(0);
+    for (int i = 0; i < fields.size(); i++) {
+      data.put(fields.get(i), columns.get(i));
+    }
+    org.apache.commons.beanutils.BeanUtils.populate(bean, data);
+  }
+
+  /**
+   * 企业营业执照编号有15位和18位,自2016年7月1日后,“三证合一、一照一码”后统一为18位社会信用代码
+   * */
+  public static boolean checkBusinessLicenseNo(String blno) {
+    if (isEmpty(blno)) return false;
+    String businessLicenseNo = blno.trim();
+    if (businessLicenseNo.length() != 15 && businessLicenseNo.length() != 18) return false; //历史企业注册码15位,社会信用代码长度为18位
+
+    if (businessLicenseNo.length() == 15) {
+      Pattern pattern = Pattern.compile("^[0-9A-Za-z]\\w{14}$");
+      Matcher match = pattern.matcher(businessLicenseNo);
+      return match.matches(); //15位注册码为历史问题,只校验格式
+    }
+
+    //18位社会信用代码判断
+    Pattern pattern = Pattern.compile("^([0-9ABCDEFGHJKLMNPQRTUWXY]{2})([0-9]{6})([0-9ABCDEFGHJKLMNPQRTUWXY]{9})([0-9Y])$");
+    Matcher match = pattern.matcher(businessLicenseNo);
+    if (!match.matches()) return false; //社会信用代码校验错误!
+
+    //验证最后一位
+    String codeStr = "0123456789ABCDEFGHJKLMNPQRTUWXY"; //字符字典(字符下标为代码字符数值)
+    int[] ws = {1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28}; //权值
+    String lastChar = businessLicenseNo.substring(businessLicenseNo.length() - 1);
+//    String prefixStr = businessLicenseNo.substring(0, businessLicenseNo.length() - 1);
+    int sum = 0;
+    for (int i = 0; i < 17; i++) {
+      sum += (codeStr.indexOf(businessLicenseNo.charAt(i)) * ws[i]);
+    }
+    int c18 = 31 - (sum % 31);
+    if (c18 == 31) {
+      if ("0".equals(lastChar)) return true; //第18位为0
+    } else {
+      //0~30 查字典
+      String cs18=String.valueOf(codeStr.charAt(c18)); //c18代表的字符
+      if(cs18.equals(lastChar)) return true;
+    }
+    return false;
+  }
+
+  public static boolean isNumber(String inputStr) {
+    char[] ch = inputStr.toCharArray();
+    for (int i = 0; i < ch.length; i++) {
+      if ((ch[i] < '0') || (ch[i] > '9')) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  public static boolean isCardphyid(String str) {
+    if (null != str && str.matches("^[A-F0-9]+$")) {
+      return true;
+    }
+    return false;
+  }
+
+  private static char getBankCardCheckCode(String nonCheckCodeBankCard) {
+    if (nonCheckCodeBankCard == null || nonCheckCodeBankCard.trim().length() == 0 || !nonCheckCodeBankCard.matches("\\d+")) {
+      //如果传的不是数字返回N
+      return 'N';
+    }
+    char[] chs = nonCheckCodeBankCard.trim().toCharArray();
+    int luhmSum = 0;
+    for (int i = chs.length - 1, j = 0; i >= 0; i--, j++) {
+      int k = chs[i] - '0';
+      if (j % 2 == 0) {
+        k *= 2;
+        k = k / 10 + k % 10;
+      }
+      luhmSum += k;
+    }
+    return (luhmSum % 10 == 0) ? '0' : (char) ((10 - luhmSum % 10) + '0');
+  }
+
+  /**
+   * 判断是否是银行卡号
+   * */
+  public static boolean isBankcardno(String bankCard) {
+    if (bankCard.length() < 15 || bankCard.length() > 19) {
+      return false;
+    }
+    char bit = getBankCardCheckCode(bankCard.substring(0, bankCard.length() - 1));
+    if (bit == 'N') {
+      return false;
+    }
+    return bankCard.charAt(bankCard.length() - 1) == bit;
+  }
+
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/util/Subject.java b/backend/src/main/java/com/supwisdom/dlpay/framework/util/Subject.java
new file mode 100644
index 0000000..e80344a
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/util/Subject.java
@@ -0,0 +1,140 @@
+package com.supwisdom.dlpay.framework.util;
+
+public class Subject {
+  //======================= 资产类 =====================//
+  /**
+   * 现金
+   */
+  public static final String SUBJNO_CASH = "1001";
+
+  /**
+   * 银行存款
+   */
+  public static final String SUBJNO_BANK_DEPOSIT = "1002";
+
+  /**
+   * 应收票据 - 支票
+   */
+  public static final String SUBJNO_CHEQUE = "112101";
+
+  /**
+   * 应收票据 - 经费本
+   */
+  public static final String SUBJNO_FUNDS = "112102";
+
+  /**
+   * 应收账款 - 现金充值款
+   */
+  public static final String SUBJNO_RECHARGE_CASH = "112201";
+
+  /**
+   * 应收账款 - 其他第三方充值款
+   */
+  public static final String SUBJNO_RECHARGE_OTHER = "112209";
+
+  /**
+   * 应收账款 - 支付宝充值款
+   */
+  public static final String SUBJNO_RECHARGE_ALIPAY = "112210";
+
+  /**
+   * 应收账款 - 微信充值款
+   */
+  public static final String SUBJNO_RECHARGE_WECHAT = "112211";
+
+  /**
+   * 应收账款 - 银联充值款
+   */
+  public static final String SUBJNO_RECHARGE_UNIONPAY = "112212";
+
+  /**
+   * 应收账款 - 一卡通充值款
+   */
+  public static final String SUBJNO_RECHARGE_YKT = "112213";
+
+
+  /**
+   * 应收账款 - 支付宝支付款
+   */
+  public static final String SUBJNO_PAY_ALIPAY = "112230";
+
+  /**
+   * 应收账款 - 微信支付款
+   */
+  public static final String SUBJNO_PAY_WECHAT = "112231";
+
+  /**
+   * 应收账款 - 银联支付款
+   */
+  public static final String SUBJNO_PAY_UNIONPAY = "112232";
+
+  /**
+   * 应收账款 - 一卡通支付款
+   */
+  public static final String SUBJNO_PAY_YKT = "112233";
+
+  /**
+   * 应收账款 - 市民卡支付款
+   */
+  public static final String SUBJNO_PAY_CITIZEN_CARD = "112234";
+
+
+  //======================= 负债类 =====================//
+
+  /**
+   * 用户押金
+   */
+  public static final String SUBJNO_FOREGIFT = "2001";
+
+  /**
+   * 商户营业款
+   */
+  public static final String SUBJNO_MACHANT_INCOME = "2004";
+
+
+  /**
+   * 应付账款 - 个人存款
+   */
+  public static final String SUBJNO_PERSONAL_DEPOSIT = "220201";
+
+
+  /**
+   * 应付账款 - 销户退款
+   */
+  public static final String SUBJNO_PERSONAL_REFUND = "220211";
+
+
+
+  //======================= 损益类 =====================//
+  /**
+   * 手续费收入 - 支付宝充值手续费
+   */
+  public static final String SUBJNO_SERVICEFEE_ALIPAY = "602101";
+
+  /**
+   * 手续费收入 - 微信充值手续费
+   */
+  public static final String SUBJNO_SERVICEFEE_WECHAT = "602102";
+
+  /**
+   * 手续费收入 - 银联充值手续费
+   */
+  public static final String SUBJNO_SERVICEFEE_UNIONPAY = "602103";
+
+
+  /**
+   * 销售费用 - (消费)折扣优惠款
+   */
+  public static final String SUBJNO_CONSUME_DISCOUNT = "660101";
+
+  /**
+   * 销售费用 - (消费)积分抵扣款
+   */
+  public static final String SUBJNO_CONSUME_POINTS = "660102";
+
+  /**
+   * 管理费收入 (搭伙费收入)
+   */
+  public static final String SUBJNO_MANAGEFEE = "6602";
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/util/SysparaUtil.java b/backend/src/main/java/com/supwisdom/dlpay/framework/util/SysparaUtil.java
new file mode 100644
index 0000000..f87b96b
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/util/SysparaUtil.java
@@ -0,0 +1,27 @@
+package com.supwisdom.dlpay.framework.util;
+
+public class SysparaUtil {
+  public static final String VALUETYPE_AMOUNT = "amount"; //金额类型 Double
+  public static final String VALUETYPE_SWITCH = "switch"; //开关类型 Boolean 0/1
+  public static final String VALUETYPE_DATE = "date"; //日期类型 yyyyMMdd
+  public static final String VALUETYPE_DATETIME = "datetime"; //日期+时间 yyyyMMddHHmmss
+  public static final String VALUETYPE_DECIMAL = "decimal"; //数值
+  public static final String VALUETYPE_NUMBER = "number "; //整数,包含正负
+  public static final String VALUETYPE_STRING = "string"; //字符串
+
+
+  public static final int BALANCE_LIMIT = 1; //系统默认最大余额限制的ID
+  public static final int NOPASS_LIMIT = 2; //默认免密额度
+  public static final int DAY_PAY_LIMIT = 3; //默认日累计额度
+  public static final int MOBILE_LOGIN_EXPIRE_IN_SECONDS = 4; //手机端用户过期时间(秒)
+  public static final int SHOP_NEEDCHECK_SWITCH = 5; //商户是否需要审核:1-需要;0-不需要
+
+  public static final int DLCARDMANAGER_APPID = 2019; //与卡管系统对接的应用ID
+  public static final int DLCARDMANAGER_SECRET = 2020; //与卡管系统对接的应用appkey
+  public static final int DLCARDMANAGER_DESKEY = 2021; //与卡管系统对接的业务参数deskey
+
+  public static final double DEFAULT_BALANCE_LIMIT = 10000.0; // 系统默认最大余额限制
+  public static final double DEFAULT_NOPASS_LIMIT = 100.0; // 默认免密额度
+  public static final double DEFAULT_DAY_PAY_LIMIT = 200.0; // 默认日累计额度
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/util/TradeDict.java b/backend/src/main/java/com/supwisdom/dlpay/framework/util/TradeDict.java
new file mode 100644
index 0000000..9c2162f
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/util/TradeDict.java
@@ -0,0 +1,113 @@
+package com.supwisdom.dlpay.framework.util;
+
+public class TradeDict {
+  /**
+   * 状态:
+   * normal -- 正常
+   * closed -- 注销
+   * locked -- 锁定、冻结
+   * frozen -- 冻结
+   * lost -- 挂失
+   * unuse -- 未使用,未启用
+   * abnormal -- 异常
+   */
+  public static final String STATUS_NORMAL = "normal";
+  public static final String STATUS_CLOSED = "closed";
+  public static final String STATUS_LOCKED = "locked";
+  public static final String STATUS_LOST = "lost";
+  public static final String STATUS_FROZEN = "frozen";
+  public static final String STATUS_UNUSE = "unuse";
+  public static final String STATUS_ABNORMAL = "abnormal";
+  public static final String STATUS_UNCHECK = "uncheck";
+  public static final String STATUS_REJECT = "reject";
+
+  public static final String STATUS_YES = "yes";
+  public static final String STATUS_NO = "no";
+  /**
+   * JWT 状态
+   */
+  public static final String JWT_STATUS_NORMAL = "normal";
+  public static final String JWT_STATUS_BLACKLIST = "blacklist";
+
+  /**
+   * 流水状态
+   * temp -- 临时流水
+   * init -- 初始化流水
+   * success -- 交易成功
+   * fail -- 交易失败
+   * cancel -- 交易取消
+   */
+  public static final String DTL_STATUS_TEMP = "temp";
+  public static final String DTL_STATUS_INIT = "init";
+  public static final String DTL_STATUS_SUCCESS = "success";
+  public static final String DTL_STATUS_FAIL = "fail";
+  public static final String DTL_STATUS_CANCEL = "cancel";
+  public static final String DTL_STATUS_WIP = "wip";
+  public static final String DTL_STATUS_NONE = "none";
+
+
+  /**
+   * 操作员类型
+   * person - 个人
+   * shop - 商户
+   * operator - 操作员
+   */
+
+  public static final String OPERTYPE_PERSON = "person";
+  public static final String OPERTYPE_SHOP = "shop";
+  public static final String OPERTYPE_OPER = "operator";
+
+  /**
+   * 收支
+   * out - 支出
+   * in - 收入
+   */
+  public static final String TRADE_FLAG_OUT = "out";
+  public static final String TRADE_FLAG_IN = "in";
+  /**
+   * 交易借方
+   */
+  public static final String TRANS_DIRECTION_CREDIT = "credit";
+  public static final String TRANS_DIRECTION_DEBIT = "debit";
+
+
+  /**
+   * 支付方式
+   */
+  public static final String PAYTYPE_CASH = "cash";
+  public static final String PAYTYPE_BALANCE = "balance";
+  public static final String PAYTYPE_ALIPAY = "alipay"; //市民卡
+  public static final String PAYTYPE_WECHAT = "wechat"; //市民卡
+  public static final String PAYTYPE_CITIZEN_CARD = "citizenCard"; //市民卡
+  public static final String PAYTYPE_YKT_CARD = "yktpay"; //一卡通
+  public static final String PAYTYPE_OTHER_THIRDPART = "thirdpart"; //其他第三方
+
+  public static final String PAYTYPE_APP = "APP"; //app内支付
+  public static final String PAYTYPE_H5 = "H5"; //H5内支付
+  public static final String PAYTYPE_JS = "JS"; //JS内支付
+  public static final String PAYTYPE_SM = "SM"; //SM内支付
+
+  public static final String SOURCETYPE_DEFAULT = "thirduid"; //第三方id默认
+  /**
+   * feetype
+   * - 消费:折扣、搭伙费(管理费)
+   * - 充值:优惠、服务费(手续费)
+   */
+
+  public static final String FEETYPE_CONSUME_MEALER = "mealer"; //收管理费
+  public static final String FEETYPE_CONSUME_DISCOUNT = "discount"; //折扣款抵扣
+
+  public static final String PAYTYPE_RECHARGE_COUPON = "coupon";  // 充值优惠
+  public static final String PAYTYPE_RECHARGE_SERVICEFEE = "servicefee"; //收服务费
+
+  /**
+   * refund flag
+   * - none : 无
+   * - cancel : 冲正
+   * - refund : 手工撤销
+   */
+  public static final String REVERSE_FLAG_NONE = "none";
+  public static final String REVERSE_FLAG_WIP = "wip";
+  public static final String REVERSE_FLAG_CANCEL = "cancel";
+  public static final String REVERSE_FLAG_REFUND = "refund";
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/framework/util/TradeErrorCode.java b/backend/src/main/java/com/supwisdom/dlpay/framework/util/TradeErrorCode.java
new file mode 100644
index 0000000..84a951e
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/framework/util/TradeErrorCode.java
@@ -0,0 +1,148 @@
+package com.supwisdom.dlpay.framework.util;
+
+public class TradeErrorCode {
+
+  public static final int OK = 0;
+
+  /**
+   * 10000 开始表示交易数据错误
+   */
+  public static final int INPUT_DATA_ERROR = 10000;
+
+
+  /**
+   * 账户不存在
+   */
+  public static final int ACCOUNT_NOT_EXISTS = 10001;
+
+  /**
+   * 账户余额不足
+   */
+  public static final int SHORT_BALANCE_ERROR = 10002;
+
+  /**
+   * 个人账户状态异常
+   */
+  public static final int PERSON_STATUS_ERROR = 10003;
+
+  /**
+   * 交易记录不存在
+   */
+  public static final int TRANSACTION_NOT_EXISTS = 10004;
+
+  /**
+   * 交易状态已完成
+   */
+  public static final int TRANSACTION_IS_FINISHED = 10005;
+
+  /**
+   * 交易状态错误,正在处理
+   */
+  public static final int TRANSACTION_HAS_BEEN_PROCESS = 100030;
+
+  /**
+   * 交易已冲正
+   */
+  public static final int TRANSACTION_HAS_CANCELED = 10006;
+  /**
+   * 商户不存在
+   */
+  public static final int SHOP_NOT_EXISTS = 10007;
+
+  /**
+   * 科目不存在
+   */
+  public static final int SUBJECT_NOT_EXISTS = 10008;
+
+  /**
+   * 外部流水号重复
+   */
+  public static final int OUTTRADENO_ALREADY_EXISTS = 10009;
+
+  /**
+   * 账户TAC校验异常
+   */
+  public static final int ACCOUNT_TAC_ERROR = 10010;
+
+  /**
+   * 账户余额超上限
+   */
+  public static final int OVERFLOW_BALANCE_ERROR = 10011;
+
+  /**
+   * 未指定明确的交易结束状态
+   */
+  public static final int TRANSDTL_STATUS_ERROR = 10012;
+
+  /**
+   * 非初始化流水
+   */
+  public static final int TRANSDTL_STATUS_NOT_INIT = 10013;
+
+  /**
+   * 账户交易繁忙
+   */
+  public static final int ACCOUNT_TRADE_BUSY = 10014;
+
+  /**
+   * 非等待锁查询超时异常
+   */
+  public static final int LOCK_READ_TIMEOUT = 10015;
+
+  /**
+   * 费用类别未定义
+   */
+  public static final int FEETYPE_NOT_EXISTS = 10018;
+
+  /**
+   * 费用类别不支持
+   */
+  public static final int FEETYPE_NOT_NOSUPPORT = 10019;
+
+
+  /**
+   * 请求参数错误
+   */
+  public static final int REQUEST_PARAM_ERROR = 20000;
+
+  /**
+   * 请求参数错误签名错误
+   */
+  public static final int REQUEST_SIGN_ERROR = 20001;
+
+  /**
+   * 注册用户已经存在
+   */
+  public static final int REGISTER_USER_EXIST = 20003;
+
+
+  //================= 业务处理错误 ==================//
+
+  public static final int BUSINESS_DEAL_ERROR = 30000; //业务处理错误
+
+  public static final int BUSINESS_SHOP_EXISTS = 30001; //商户已经存在
+
+  public static final int BUSINESS_APPID_NOTFOUND = 30002; //APPID没找到
+
+  public static final int BUSINESS_PAYTYPE_NOSUPPORT = 30003; //支付方式不支持
+
+  public static final int SQL_EXCEPTION = 30004; // SQL处理异常
+
+  public static final int NO_DEAL_EEROR = 30005; // 无交易记录
+
+  //============= 交易错误 ============//
+  public static final int CARD_NOT_EXISTS = 40000; //卡不存在
+
+  public static final int CARD_IS_CLOSED = 40001; //卡已注销
+
+  public static final int ACCOUNT_IS_LOSS = 40002; //卡已挂失
+
+  public static final int ACCOUNT_IS_FROZEN = 40003; //卡已挂失
+
+  public static final int ACCOUNT_IS_LOCKED = 40004; //卡已锁定
+
+  public static final int WAIT_QUERY_RESULT = 55555; //查询结果
+
+
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/system/service/DictionaryDataService.java b/backend/src/main/java/com/supwisdom/dlpay/system/service/DictionaryDataService.java
new file mode 100644
index 0000000..d416e56
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/system/service/DictionaryDataService.java
@@ -0,0 +1,19 @@
+package com.supwisdom.dlpay.system.service;
+
+import com.supwisdom.dlpay.framework.domain.TDictionary;
+import com.supwisdom.dlpay.framework.domain.TSubject;
+import com.supwisdom.dlpay.framework.domain.TTranscode;
+
+import java.util.List;
+
+public interface DictionaryDataService {
+  List<TDictionary> getDictionaryByDictType(String dicttype);
+
+  void refreshCache(String dicttype);
+
+  void refreshCache();
+
+  List<TTranscode> getTransCode();
+
+  List<TSubject> getAllSubject();
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/system/service/DictionaryProxy.java b/backend/src/main/java/com/supwisdom/dlpay/system/service/DictionaryProxy.java
new file mode 100644
index 0000000..4b45b79
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/system/service/DictionaryProxy.java
@@ -0,0 +1,54 @@
+package com.supwisdom.dlpay.system.service;
+
+import com.supwisdom.dlpay.framework.domain.DictionaryTable;
+import com.supwisdom.dlpay.framework.util.Dictionary;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Component
+public class DictionaryProxy {
+  private final DictionaryDataService dictionaryDataService;
+
+  private final ParamService paramService;
+
+  public DictionaryProxy(DictionaryDataService dictionaryDataService, ParamService paramService) {
+    this.dictionaryDataService = dictionaryDataService;
+    this.paramService = paramService;
+  }
+
+  public Map<String, Object> getDictionaryAsMap(String dictType) {
+    List list = getDictionaryAsList(dictType);
+    Map<String, Object> result = new HashMap<>();
+    for (Object item : list) {
+      if (item instanceof DictionaryTable) {
+        DictionaryTable it = (DictionaryTable) item;
+        result.put(it.getDictKey(), it.getDictValue());
+      }
+    }
+    return result;
+  }
+
+  @SuppressWarnings("UNCHECKED_CAST")
+  public <T> List<T> getDictionaryAsList(String dictType) {
+    if (Dictionary.SOURCE_TYPE.equals(dictType)) {
+      return (List<T>) paramService.getAllSourceType();
+    } else if (Dictionary.TRANS_CODE.equals(dictType)) {
+      return (List<T>) dictionaryDataService.getTransCode();
+    } else if (Dictionary.ALL_SUBJECT.equals(dictType)) {
+      return (List<T>) dictionaryDataService.getAllSubject();
+    } else {
+      return (List<T>) dictionaryDataService.getDictionaryByDictType(dictType);
+    }
+  }
+
+  public void refreshDictionary() {
+    dictionaryDataService.refreshCache();
+  }
+
+  public void refreshDictionary(String dicttype) {
+    dictionaryDataService.refreshCache(dicttype);
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/system/service/ParamService.java b/backend/src/main/java/com/supwisdom/dlpay/system/service/ParamService.java
new file mode 100644
index 0000000..f0c8c17
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/system/service/ParamService.java
@@ -0,0 +1,18 @@
+package com.supwisdom.dlpay.system.service;
+
+import com.supwisdom.dlpay.api.domain.TSourceType;
+import com.supwisdom.dlpay.framework.domain.TApiClient;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+
+public interface ParamService {
+
+  @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class, readOnly = true)
+  TApiClient getApiClientByAppid(String appid);
+
+  @Transactional(readOnly = true)
+  List<TSourceType> getAllSourceType();
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/system/service/impl/DictionaryDataServiceImpl.java b/backend/src/main/java/com/supwisdom/dlpay/system/service/impl/DictionaryDataServiceImpl.java
new file mode 100644
index 0000000..8ec3017
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/system/service/impl/DictionaryDataServiceImpl.java
@@ -0,0 +1,84 @@
+package com.supwisdom.dlpay.system.service.impl;
+
+import com.supwisdom.dlpay.framework.dao.DictionaryDao;
+import com.supwisdom.dlpay.framework.dao.SubjectDao;
+import com.supwisdom.dlpay.framework.dao.TranscodeDao;
+import com.supwisdom.dlpay.framework.domain.TDictionary;
+import com.supwisdom.dlpay.framework.domain.TSubject;
+import com.supwisdom.dlpay.framework.domain.TTranscode;
+import com.supwisdom.dlpay.system.service.DictionaryDataService;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+public class DictionaryDataServiceImpl implements DictionaryDataService {
+  private final DictionaryDao dictionaryDao;
+  private final TranscodeDao transcodeDao;
+  private final SubjectDao subjectDao;
+
+  public DictionaryDataServiceImpl(DictionaryDao dictionaryDao,
+                                   TranscodeDao transcodeDao,
+                                   SubjectDao subjectDao) {
+    this.dictionaryDao = dictionaryDao;
+    this.transcodeDao = transcodeDao;
+    this.subjectDao = subjectDao;
+  }
+
+  @Override
+  @Cacheable(cacheNames = "dictionary_cache", key = "@tenantHolder.genKey(#dicttype)")
+  public List<TDictionary> getDictionaryByDictType(String dicttype) {
+    List<TDictionary> list = dictionaryDao.findAllByDicttype(dicttype);
+    if (!list.isEmpty()) {
+      return list;
+    }
+    return new ArrayList<>();
+  }
+
+
+  private TDictionary findExistsOrNew(List<TDictionary> list, TDictionary item) {
+    for (TDictionary i : list) {
+      if (item.getDicttype().equals(i.getDicttype()) && item.getDictval().equals(i.getDictval())) {
+        i.setDicttypename(item.getDicttypename());
+        i.setDictcaption(item.getDictcaption());
+        return i;
+      }
+    }
+    TDictionary newDict = new TDictionary();
+    newDict.setDicttype(item.getDicttype());
+    newDict.setDictval(item.getDictval());
+    newDict.setDictcaption(item.getDictcaption());
+    newDict.setDicttypename(item.getDicttypename());
+    return newDict;
+  }
+
+  @Override
+  @CacheEvict(cacheNames = "dictionary_cache", key = "@tenantHolder.genKey(#dicttype)")
+  public void refreshCache(String dicttype) {
+  }
+
+  @Override
+  @CacheEvict(cacheNames = "dictionary_cache", key = "@tenantHolder.getId() + '.*'", allEntries = true)
+  public void refreshCache() {
+
+  }
+
+  @Override
+  @Cacheable(cacheNames = "dictionary_cache", key = "@tenantHolder.genKey('transcode')")
+  public List<TTranscode> getTransCode() {
+    List<TTranscode> list = transcodeDao.findAll();
+    if (!list.isEmpty()) {
+      return list;
+    }
+    return new ArrayList<>();
+  }
+
+  @Override
+  @Cacheable(cacheNames = "dictionary_cache", key = "@tenantHolder.genKey('allsubject')")
+  public List<TSubject> getAllSubject() {
+    return subjectDao.findAllDisplaySubjects();
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/system/service/impl/ParamServiceImpl.java b/backend/src/main/java/com/supwisdom/dlpay/system/service/impl/ParamServiceImpl.java
new file mode 100644
index 0000000..b8d15ef
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/system/service/impl/ParamServiceImpl.java
@@ -0,0 +1,37 @@
+package com.supwisdom.dlpay.system.service.impl;
+
+import com.supwisdom.dlpay.api.dao.SourceTypeDao;
+import com.supwisdom.dlpay.api.domain.TSourceType;
+import com.supwisdom.dlpay.framework.dao.ApiClientDao;
+import com.supwisdom.dlpay.framework.domain.TApiClient;
+import com.supwisdom.dlpay.framework.util.StringUtil;
+import com.supwisdom.dlpay.system.service.ParamService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+
+@Service
+public class ParamServiceImpl implements ParamService {
+  @Autowired
+  private ApiClientDao apiClientDao;
+  @Autowired
+  private SourceTypeDao sourceTypeDao;
+
+  @Override
+  public TApiClient getApiClientByAppid(String appid) {
+    if (!StringUtil.isEmpty(appid)) {
+      return apiClientDao.findByAppid(appid.trim());
+    }
+    return null;
+  }
+
+  @Override
+  @Cacheable(cacheNames = "dictionary_cache", key = "@tenantHolder.genKey('sourcetype')")
+  public List<TSourceType> getAllSourceType() {
+    return sourceTypeDao.findAll();
+  }
+
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/util/AesUtil.java b/backend/src/main/java/com/supwisdom/dlpay/util/AesUtil.java
new file mode 100644
index 0000000..c3e938b
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/util/AesUtil.java
@@ -0,0 +1,161 @@
+package com.supwisdom.dlpay.util;
+
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.binary.Hex;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+public class AesUtil {
+
+	public static String encrypt(String rawdata, String secret) {
+		Cipher cipher;
+		try {
+			cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+			SecretKeySpec key = new SecretKeySpec(secret.getBytes("UTF-8"), "AES");
+			cipher.init(Cipher.ENCRYPT_MODE, key);
+			byte[] encdata = cipher.doFinal(rawdata.getBytes("UTF-8"));
+			String hexdata = Base64.encodeBase64String(encdata);
+			
+			return hexdata;
+		} catch (NoSuchAlgorithmException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (NoSuchPaddingException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (UnsupportedEncodingException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (InvalidKeyException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (IllegalBlockSizeException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (BadPaddingException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		}
+		return null;
+		
+	}
+	public static String encryptCFB(String rawdata, String secret ,String ivstr,String KEY_ALG_MODE) {
+		Cipher cipher;
+		try {
+			//byte[] iv = new byte[] { 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF };
+			byte[] iv = Hex.decodeHex(ivstr.toCharArray());
+			IvParameterSpec ivSpec = new IvParameterSpec(iv);
+			cipher = Cipher.getInstance(KEY_ALG_MODE);
+			SecretKeySpec key = new SecretKeySpec(Base64.decodeBase64(secret), "AES");
+			cipher.init(Cipher.ENCRYPT_MODE, key,ivSpec);
+			byte[] encdata = cipher.doFinal(rawdata.getBytes("UTF-8"));
+			String hexdata = Base64.encodeBase64String(encdata);
+
+			return hexdata;
+		} catch (NoSuchAlgorithmException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (NoSuchPaddingException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (UnsupportedEncodingException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (InvalidKeyException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (IllegalBlockSizeException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (BadPaddingException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (InvalidAlgorithmParameterException e) {
+			e.printStackTrace();
+		} catch (DecoderException e) {
+			e.printStackTrace();
+		}
+		return null;
+
+	}
+	
+	public static String decrypt(String encdata, String secret){
+		Cipher cipher;
+		try {
+			cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+			SecretKeySpec key = new SecretKeySpec(secret.getBytes("UTF-8"), "AES");
+			cipher.init(Cipher.DECRYPT_MODE, key);
+			byte[] decordedValue = Base64.decodeBase64(encdata);
+			byte[] decdata = cipher.doFinal(decordedValue);
+			String rawdata = new String(decdata);
+			
+			return rawdata;
+		} catch (NoSuchAlgorithmException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (NoSuchPaddingException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (UnsupportedEncodingException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (InvalidKeyException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (IllegalBlockSizeException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (BadPaddingException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		}
+		return null;
+	}
+
+	public static String decryptCFB(String encdata, String secret,String ivstr,String KEY_ALG_MODE){
+		Cipher cipher;
+		try {
+//			byte[] iv = new byte[] { 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF };
+			byte[] iv = Hex.decodeHex(ivstr.toCharArray());
+			IvParameterSpec ivSpec = new IvParameterSpec(iv);
+			cipher = Cipher.getInstance(KEY_ALG_MODE);
+			SecretKeySpec key = new SecretKeySpec(Base64.decodeBase64(secret), "AES");
+			cipher.init(Cipher.DECRYPT_MODE, key,ivSpec);
+			byte[] decordedValue = Base64.decodeBase64(encdata);
+			byte[] decdata = cipher.doFinal(decordedValue);
+			String rawdata = new String(decdata);
+
+			return rawdata;
+		} catch (NoSuchAlgorithmException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (NoSuchPaddingException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (InvalidKeyException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (IllegalBlockSizeException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (BadPaddingException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (InvalidAlgorithmParameterException e) {
+			e.printStackTrace();
+		} catch (DecoderException e) {
+			e.printStackTrace();
+		}
+		return null;
+	}
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/util/Code.java b/backend/src/main/java/com/supwisdom/dlpay/util/Code.java
new file mode 100644
index 0000000..b3ca93f
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/util/Code.java
@@ -0,0 +1,39 @@
+package com.supwisdom.dlpay.util;
+
+/**
+ * Created by shuwei on 2019/4/9.
+ */
+public enum Code {
+    SUCCESS("0","OK"),
+    PARAM_ERROR("1","参数错误"),
+    SYS_ERROR("2","系统异常"),
+    PAYTYPE_NOT_SUPPORT("10","支付方式不支持"),
+    PAYTYPE_CONFIG_ERROR("11","支付方式未配置或配置错误"),
+    REQUEST_ERROR("20","响应内容错误"),
+    RESPNOSE_CONTENT_ERROR("21","响应内容错误")
+    ;
+
+    private String code;
+    private String retmsg;
+
+    private Code(String code, String retmsg){
+        this.code = code;
+        this.retmsg = retmsg;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public void setCode(String code) {
+        this.code = code;
+    }
+
+    public String getRetmsg() {
+        return retmsg;
+    }
+
+    public void setRetmsg(String retmsg) {
+        this.retmsg = retmsg;
+    }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/util/ConstantUtil.java b/backend/src/main/java/com/supwisdom/dlpay/util/ConstantUtil.java
new file mode 100644
index 0000000..4f6d410
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/util/ConstantUtil.java
@@ -0,0 +1,107 @@
+package com.supwisdom.dlpay.util;
+
+/**
+ * Created by shuwei on 2019/4/9.
+ */
+public class ConstantUtil {
+
+  public static final String ENABLE_YES = "yes";
+  public static final String ENABLE_NO = "no";
+
+  public static final String SEX_MALE = "male";
+  public static final String SEX_FEMALE = "female";
+
+  /**
+   * TDictionary的dicttype
+   * */
+  public static final int DICTTYPE_NO1 = 1; //冲正状态字典
+  public static final int DICTTYPE_NO2 = 2; //流水状态字典
+
+  /*
+  * 页面ID
+  * */
+
+  public static final String PAGE_USERXIEYI = "xieyi";//用户协议页面
+  public static final String PAGE_BANKXIEYI = "bankxieyi";//银行协议页面
+
+  /**
+   * 卡库同步的Cardtype
+   * */
+  public static final String CARDTYPE_CITIZENCARD = "citizencard";
+  public static final String CARDTYPE_BANKCARD = "bankcard";
+
+  public static final String QUERYTYPE_NEED_QUERY = "query";
+  public static final String QUERYTYPE_QUERY_FINISH = "finish";
+  public static final int QUERY_MAX_COUNT = 10;
+
+  /**
+   * 对账文件状态
+   * */
+  public static final String CHKFILE_STATUS_INIT = "init"; //初始化
+  public static final String CHKFILE_STATUS_UNCHECK = "uncheck"; //待对账
+  public static final String CHKFILE_STATUS_CHECKOUT = "checkout"; //校对完成
+  public static final String CHKFILE_STATUS_FINISH = "finish"; //对账完成
+  public static final String CHKFILE_STATUS_ERROR = "error"; //保存数据异常
+
+  /**
+   * 对账文件结果状态
+   * */
+  public static final String CHKFILE_RESULT_NONE = "none"; //未对账 -
+  public static final String CHKFILE_RESULT_EQUAL = "equal"; //对账一致,平
+  public static final String CHKFILE_RESULT_UNEQUAL = "unequal"; //对账不一致,不平
+  public static final String CHKFILE_RESULT_ERROR = "error"; //对账明细有异常
+
+  /**
+   * 对账明细状态
+   * */
+  public static final String CHKDTL_CHKRESULT_UNCHECK= "uncheck"; //未检查
+  public static final String CHKDTL_CHKRESULT_EQUAL= "equal"; //一致
+  public static final String CHKDTL_CHKRESULT_NOTEXIST= "notexist"; //交易流水不存在
+  public static final String CHKDTL_CHKRESULT_NOCHARGE = "nocharge"; //支付未记账
+  public static final String CHKDTL_CHKRESULT_DIFF = "diff"; //金额不相等
+  public static final String CHKDTL_CHKRESULT_ERROR = "error"; //记账日期或支付方式错误
+  public static final String CHKDTL_CHKRESULT_SURPLUS = "surplus"; //本地多余成功流水
+
+  /**
+   * 对账明细解决状态
+   * */
+  public static final String CHKDTL_RESOLVED_NONE = "none"; //已处理
+  public static final String CHKDTL_RESOLVED_ADD ="add"; //补账
+  public static final String CHKDTL_RESOLVED_FAIL = "fail"; //补账失败
+  public static final String CHKDTL_RESOLVED_EQUAL = "equal";  //一致,相等
+  public static final String CHKDTL_RESOLVED_HANGUP="hangup"; //挂起
+
+
+  /**
+   * 市民卡挂失申请表状态
+   * */
+  public static final String CITIZENCARD_LOSSAPPLY_STATUS_APPLY = "apply"; //申请挂失
+  public static final String CITIZENCARD_LOSSAPPLY_STATUS_SUCCESS = "success"; //调用挂失接口成功
+  public static final String CITIZENCARD_LOSSAPPLY_STATUS_FAIL = "fail"; //调用挂失接口失败
+
+  public static final String CITIZENCARD_SYSTEM_URL = "dlsmk.cardsystem.url"; //大理市民卡管系统url
+
+  /**
+   * 商户审批
+   * */
+  public static final String OPERCHK_CHKTYPE_SHOP = "SHOP";
+
+  public static final String OPERCHK_CHKMODE_ADD = "新增";
+  public static final String OPERCHK_CHKMODE_MODIFY = "修改";
+  public static final String OPERCHK_CHKMODE_DELETE = "删除";
+
+  public static final String CHKFILE_DELIMITER = "|";
+
+  public static final String PHONE_PLATFORM_ALL = "all";
+  public static final String PHONE_PLATFORM_ANDROID = "android";
+  public static final String PHONE_PLATFORM_IOS = "ios";
+
+  public static final String PHONE_NOTICE_PUSHMODE_ATONCE = "atonce"; //立即发布
+  public static final String PHONE_NOTICE_PUSHMODE_DELAY = "delay"; //延时发布
+
+  /**
+   * kafka消息类型
+   * */
+  public static final String KAFKA_MAGTYPE_NOTICE = "dlsmk_phone_notice";
+  public static final String KAFKA_MAGTYPE_CONSUME = "dlsmk_card_consume";
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/util/QrCodeTotpUtil.java b/backend/src/main/java/com/supwisdom/dlpay/util/QrCodeTotpUtil.java
new file mode 100644
index 0000000..6cc621c
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/util/QrCodeTotpUtil.java
@@ -0,0 +1,59 @@
+package com.supwisdom.dlpay.util;
+
+public class QrCodeTotpUtil {
+    public static String generateTOTP(String seed){
+        long X = 5;
+        long T0 = 0;
+        String steps = "0";
+        long time = System.currentTimeMillis() / 1000;
+        long T = (time - T0) / X;
+        steps = Long.toHexString(T).toUpperCase();
+        while (steps.length() < 16) {
+            steps = "0" + steps;
+        }
+        String totp = TOTP.generateTOTP(seed, steps, "8", "HmacSHA256");
+        return totp;
+    }
+
+    public static String generateTOTP(String seed,String returnDigits){
+        long X = 5;
+        long T0 = 0;
+        String steps = "0";
+        long time = System.currentTimeMillis() / 1000;
+        long T = (time - T0) / X;
+        steps = Long.toHexString(T).toUpperCase();
+        while (steps.length() < 16) {
+            steps = "0" + steps;
+        }
+        String totp = TOTP.generateTOTP(seed, steps, returnDigits, "HmacSHA256");
+        return totp;
+    }
+
+    public static boolean verifyCode(String totp, String secret, int offset) {
+
+        String second = "5";
+        long T0 = 0;
+        String[] keys = new String[offset * 2 + 1];
+        long time = System.currentTimeMillis() / 1000;
+        for (int i = 0; i < keys.length; i++) {
+            String steps = "0";
+            try {
+                long T = (time - T0) / Long.parseLong(second);
+                steps = Long.toHexString(T + (i - offset)).toUpperCase();
+                while (steps.length() < 16) {
+                    steps = "0" + steps;
+                }
+                String key = TOTP.generateTOTP(secret, steps, "8", "HmacSHA256");
+                keys[i] = key;
+            } catch (final Exception e) {
+                System.out.println("Error : " + e);
+            }
+        }
+        for (String key : keys) {
+            if (key.equals(totp)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/util/QrcodeRawData.java b/backend/src/main/java/com/supwisdom/dlpay/util/QrcodeRawData.java
new file mode 100644
index 0000000..44ac288
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/util/QrcodeRawData.java
@@ -0,0 +1,22 @@
+package com.supwisdom.dlpay.util;
+
+public class QrcodeRawData {
+  private String totp;
+  private String userid;
+
+  public String getTotp() {
+    return totp;
+  }
+
+  public void setTotp(String totp) {
+    this.totp = totp;
+  }
+
+  public String getUserid() {
+    return userid;
+  }
+
+  public void setUserid(String userid) {
+    this.userid = userid;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/util/RSAKeysGenerate.java b/backend/src/main/java/com/supwisdom/dlpay/util/RSAKeysGenerate.java
new file mode 100644
index 0000000..82d2b82
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/util/RSAKeysGenerate.java
@@ -0,0 +1,179 @@
+package com.supwisdom.dlpay.util;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang.StringUtils;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+import javax.crypto.Cipher;
+import java.io.ByteArrayOutputStream;
+import java.security.*;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.HashMap;
+import java.util.Map;
+
+public class RSAKeysGenerate {
+
+  public static final String KEY_ALGORITHM = "RSA";
+  private static final String PUBLIC_KEY = "RSAPublicKey";
+  private static final String PRIVATE_KEY = "RSAPrivateKey";
+  private static final String KEY_ALG_MODE = "RSA/ECB/PKCS1Padding";
+  /** 密钥大小 */
+  private static final int KEY_SIZE = 1024;
+  /** *//**
+   * RSA最大加密明文大小
+   */
+  private static final int MAX_ENCRYPT_BLOCK = 117;
+
+  /** *//**
+   * RSA最大解密密文大小
+   */
+  private static final int MAX_DECRYPT_BLOCK = 128;
+  /** 默认的安全服务提供者 */
+  private static final Provider DEFAULT_PROVIDER = new BouncyCastleProvider();
+  public static void main(String[] args) {
+    Map<String, Object> keyMap;
+    try {
+      keyMap = initKey();
+      String publicKey = getPublicKey(keyMap);
+      System.out.println(publicKey);
+      String privateKey = getPrivateKey(keyMap);
+      System.out.println(privateKey);
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+  }
+
+  public static String getPublicKey(Map<String, Object> keyMap) throws Exception {
+    Key key = (Key) keyMap.get(PUBLIC_KEY);
+    byte[] publicKey = key.getEncoded();
+    return encryptBASE64(key.getEncoded());
+  }
+
+  public static String getPrivateKey(Map<String, Object> keyMap) throws Exception {
+    Key key = (Key) keyMap.get(PRIVATE_KEY);
+    byte[] privateKey = key.getEncoded();
+    return encryptBASE64(key.getEncoded());
+  }
+
+
+  public static String encryptBASE64(byte[] key) throws Exception {
+    return Base64.encodeBase64String(key);
+  }
+
+  public static Map<String, Object> initKey() throws Exception {
+    KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
+    keyPairGen.initialize(1024);
+    KeyPair keyPair = keyPairGen.generateKeyPair();
+    RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
+    RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
+    Map<String, Object> keyMap = new HashMap<String, Object>(2);
+    keyMap.put(PUBLIC_KEY, publicKey);
+    keyMap.put(PRIVATE_KEY, privateKey);
+    return keyMap;
+  }
+
+  public static PrivateKey getPrivateKey(final String key) throws Exception {
+    byte[] keyBytes = null;
+    keyBytes = com.sun.jersey.core.util.Base64.decode(key);
+    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
+    KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
+    PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
+    return privateKey;
+  }
+  public static PublicKey getPublicKey(final String key) throws Exception {
+    byte[] keyBytes;
+    keyBytes = com.sun.jersey.core.util.Base64.decode(key);
+    X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
+    KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
+    PublicKey publicKey = keyFactory.generatePublic(keySpec);
+    return publicKey;
+  }
+
+  public static byte[] decrypt(PrivateKey privateKey, byte[] data) throws Exception {
+    Cipher cipher = Cipher.getInstance(KEY_ALG_MODE, DEFAULT_PROVIDER);
+    cipher.init(Cipher.DECRYPT_MODE, privateKey);
+
+    int inputLen = data.length;
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    int offSet = 0;
+    byte[] cache;
+    int i = 0;
+    // 对数据分段解密
+    while (inputLen - offSet > 0) {
+      if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
+        cache = cipher.doFinal(data, offSet, MAX_DECRYPT_BLOCK);
+      } else {
+        cache = cipher.doFinal(data, offSet, inputLen - offSet);
+      }
+      out.write(cache, 0, cache.length);
+      i++;
+      offSet = i * MAX_DECRYPT_BLOCK;
+    }
+    byte[] decryptedData = out.toByteArray();
+    out.close();
+    return decryptedData;
+
+  }
+  public static String decryptbyte(PrivateKey privateKey, String encrypttext) {
+    if (privateKey == null || StringUtils.isBlank(encrypttext)) {
+      return null;
+    }
+    try {
+      byte[] en_data = Base64.decodeBase64(encrypttext);
+
+
+      byte[] data = decrypt(privateKey, en_data);
+      return new String(data);
+    } catch (Exception ex) {
+      ex.printStackTrace();
+    }
+    return null;
+  }
+  /**
+   * 使用指定的公钥加密数据。
+   *
+   * @param publicKey 给定的公钥。
+   * @param data 要加密的数据。
+   * @return 加密后的数据。
+   */
+  public static byte[] encrypt(PublicKey publicKey, byte[] data) throws Exception {
+    Cipher cipher = Cipher.getInstance(KEY_ALG_MODE, DEFAULT_PROVIDER);
+    cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+
+    int inputLen = data.length;
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    int offSet = 0;
+    byte[] cache;
+    int i = 0;
+    // 对数据分段加密
+    while (inputLen - offSet > 0) {
+      if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
+        cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
+      } else {
+        cache = cipher.doFinal(data, offSet, inputLen - offSet);
+      }
+      out.write(cache, 0, cache.length);
+      i++;
+      offSet = i * MAX_ENCRYPT_BLOCK;
+    }
+    byte[] encryptedData = out.toByteArray();
+    out.close();
+    return encryptedData;
+  }
+  public static byte[] encryptbyte(PublicKey publicKey, String plaintext) {
+    if (publicKey == null || plaintext == null) {
+      return null;
+    }
+    byte[] data = plaintext.getBytes();
+    try {
+      byte[] en_data = encrypt(publicKey, data);
+      return en_data;
+    } catch (Exception ex) {
+      ex.printStackTrace();
+    }
+    return null;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/util/TOTP.java b/backend/src/main/java/com/supwisdom/dlpay/util/TOTP.java
new file mode 100644
index 0000000..bcab4d3
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/util/TOTP.java
@@ -0,0 +1,264 @@
+package com.supwisdom.dlpay.util;
+
+/**
+ Copyright (c) 2011 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, is permitted pursuant to, and subject to the license
+ terms contained in, the Simplified BSD License set forth in Section
+ 4.c of the IETF Trust's Legal Provisions Relating to IETF Documents
+ (http://trustee.ietf.org/license-info).
+ */
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+
+/**
+ * This is an example implementation of the OATH TOTP algorithm. Visit
+ * www.openauthentication.org for more information.
+ * 
+ * @author Johan Rydell, PortWise, Inc.
+ */
+public class TOTP {
+	private static final int[] DIGITS_POWER
+	// 0 1 2 3 4 5 6 7 8
+	= { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000 };
+
+	private TOTP() {
+	}
+
+	/**
+	 * This method uses the JCE to provide the crypto algorithm. HMAC computes a
+	 * Hashed Message Authentication Code with the crypto hash algorithm as a
+	 * parameter.
+	 * 
+	 * @param crypto
+	 *            : the crypto algorithm (HmacSHA1, HmacSHA256, HmacSHA512)
+	 * @param keyBytes
+	 *            : the bytes to use for the HMAC key
+	 * @param text
+	 *            : the message or text to be authenticated
+	 */
+	private static byte[] hmac_sha(String crypto, byte[] keyBytes, byte[] text) {
+		try {
+			Mac hmac;
+			hmac = Mac.getInstance(crypto);
+
+			SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");
+			hmac.init(macKey);
+
+			return hmac.doFinal(text);
+		} catch (GeneralSecurityException gse) {
+			throw new UndeclaredThrowableException(gse);
+		}
+	}
+
+	/**
+	 * This method converts a HEX string to Byte[]
+	 * 
+	 * @param hex
+	 *            : the HEX string
+	 * 
+	 * @return: a byte array
+	 */
+	private static byte[] hexStr2Bytes(String hex) {
+		// Adding one byte to get the right conversion
+		// Values starting with "0" can be converted
+		byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();
+
+		// Copy all the REAL bytes, not the "first"
+		byte[] ret = new byte[bArray.length - 1];
+
+		for (int i = 0; i < ret.length; i++)
+			ret[i] = bArray[i + 1];
+
+		return ret;
+	}
+
+	/**
+	 * This method generates a TOTP value for the given set of parameters.
+	 * 
+	 * @param key
+	 *            : the shared secret, HEX encoded
+	 * @param time
+	 *            : a value that reflects a time
+	 * @param returnDigits
+	 *            : number of digits to return
+	 * 
+	 * @return: a numeric String in base 10 that includes
+	 *          {@link truncationDigits} digits
+	 */
+	public static String generateTOTP(String key, String time,
+			String returnDigits) {
+		return generateTOTP(key, time, returnDigits, "HmacSHA1");
+	}
+
+	/**
+	 * This method generates a TOTP value for the given set of parameters.
+	 * 
+	 * @param key
+	 *            : the shared secret, HEX encoded
+	 * @param time
+	 *            : a value that reflects a time
+	 * @param returnDigits
+	 *            : number of digits to return
+	 * 
+	 * @return: a numeric String in base 10 that includes
+	 *          {@link truncationDigits} digits
+	 */
+	public static String generateTOTP256(String key, String time,
+			String returnDigits) {
+		return generateTOTP(key, time, returnDigits, "HmacSHA256");
+	}
+
+	/**
+	 * This method generates a TOTP value for the given set of parameters.
+	 * 
+	 * @param key
+	 *            : the shared secret, HEX encoded
+	 * @param time
+	 *            : a value that reflects a time
+	 * @param returnDigits
+	 *            : number of digits to return
+	 * 
+	 * @return: a numeric String in base 10 that includes
+	 *          {@link truncationDigits} digits
+	 */
+	public static String generateTOTP512(String key, String time,
+			String returnDigits) {
+		return generateTOTP(key, time, returnDigits, "HmacSHA512");
+	}
+
+	/**
+	 * This method generates a TOTP value for the given set of parameters.
+	 * 
+	 * @param key
+	 *            : the shared secret, HEX encoded
+	 * @param time
+	 *            : a value that reflects a time
+	 * @param returnDigits
+	 *            : number of digits to return
+	 * @param crypto
+	 *            : the crypto function to use
+	 * 
+	 * @return: a numeric String in base 10 that includes
+	 *          {@link truncationDigits} digits
+	 */
+	public static String generateTOTP(String key, String time,
+			String returnDigits, String crypto) {
+		int codeDigits = Integer.decode(returnDigits).intValue();
+		String result = null;
+
+		// Using the counter
+		// First 8 bytes are for the movingFactor
+		// Compliant with base RFC 4226 (HOTP)
+		while (time.length() < 16)
+			time = "0" + time;
+
+		// Get the HEX in a Byte[]
+		byte[] msg = hexStr2Bytes(time);
+		byte[] k = hexStr2Bytes(key);
+		byte[] hash = hmac_sha(crypto, k, msg);
+
+		// put selected bytes into result int
+		int offset = hash[hash.length - 1] & 0xf;
+
+		int binary = ((hash[offset] & 0x7f) << 24)
+				| ((hash[offset + 1] & 0xff) << 16)
+				| ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);
+
+		int otp = binary % DIGITS_POWER[codeDigits];
+
+		result = Integer.toString(otp);
+
+		while (result.length() < codeDigits) {
+			result = "0" + result;
+		}
+
+		return result;
+	}
+
+	public static void main(String[] args) {
+		long X = 30;
+		long T0 = 0;
+		String steps = "0";
+		long time = System.currentTimeMillis() / 1000;
+		System.out.println(time);
+		long T = (time - T0) / X;
+		steps = Long.toHexString(T).toUpperCase();
+		while (steps.length() < 16) {
+			steps = "0" + steps;
+		}
+		String seed = "9e8bb3d2df73741c041aef39e37ee015fc98233720a1350fcd67d5c3027896ac";
+		String totp = TOTP.generateTOTP(seed, steps, "8", "HmacSHA256");
+		System.out.println(totp);
+
+		/*
+
+
+		SecureRandom a = new SecureRandom();
+		byte[] b = a.generateSeed(8);
+		// Seed for HMAC-SHA1 - 20 bytes
+		String seed = "3132333435363738393031323334353637383930";
+
+		// Seed for HMAC-SHA256 - 32 bytes
+		//String seed32 = "3132333435363738393031323334353637383930313233343536373839303132";
+		String seed32 = "e5b72370f61687c8075b8383266f4fbcbb3b55da178eed76329666600c134093";
+
+		// Seed for HMAC-SHA512 - 64 bytes
+		String seed64 = "3132333435363738393031323334353637383930"
+				+ "3132333435363738393031323334353637383930"
+				+ "3132333435363738393031323334353637383930" + "31323334";
+		long T0 = 0;
+		long X = 30;
+		long[] testTime = { 59L, 1111111109L, 1111111111L, 1234567890L,
+				2000000000L, 20000000000L };
+
+		String steps = "0";
+		DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+		df.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+		try {
+			System.out.println("+---------------+-----------------------+"
+					+ "------------------+--------+--------+");
+			System.out.println("|  Time(sec)    |   Time (UTC format)   "
+					+ "| Value of T(Hex)  |  TOTP  | Mode   |");
+			System.out.println("+---------------+-----------------------+"
+					+ "------------------+--------+--------+");
+
+			for (int i = 0; i < testTime.length; i++) {
+				long T = (testTime[i] - T0) / X;
+				steps = Long.toHexString(T).toUpperCase();
+
+				while (steps.length() < 16)
+					steps = "0" + steps;
+
+				String fmtTime = String.format("%1$-11s", testTime[i]);
+				String utcTime = df.format(new Date(testTime[i] * 1000));
+				System.out.print("|  " + fmtTime + "  |  " + utcTime + "  | "
+						+ steps + " |");
+				System.out.println(generateTOTP(seed, steps, "8", "HmacSHA1")
+						+ "| SHA1   |");
+				System.out.print("|  " + fmtTime + "  |  " + utcTime + "  | "
+						+ steps + " |");
+				System.out.println(generateTOTP(seed32, steps, "8",
+						"HmacSHA256") + "| SHA256 |");
+				System.out.print("|  " + fmtTime + "  |  " + utcTime + "  | "
+						+ steps + " |");
+				System.out.println(generateTOTP(seed64, steps, "8",
+						"HmacSHA512") + "| SHA512 |");
+
+				System.out.println("+---------------+-----------------------+"
+						+ "------------------+--------+--------+");
+			}
+		} catch (final Exception e) {
+			System.out.println("Error : " + e);
+		}
+
+		*/
+	}
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt
new file mode 100644
index 0000000..f489996
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt
@@ -0,0 +1,167 @@
+package com.supwisdom.dlpay
+
+import com.supwisdom.dlpay.framework.tenant.TenantCacheKeyGen
+import io.lettuce.core.ReadFrom
+import net.javacrumbs.shedlock.core.LockProvider
+import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider
+import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.boot.SpringApplication
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.autoconfigure.data.redis.RedisProperties
+import org.springframework.boot.builder.SpringApplicationBuilder
+import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.boot.web.servlet.FilterRegistrationBean
+import org.springframework.boot.web.servlet.ServletComponentScan
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer
+import org.springframework.cache.annotation.EnableCaching
+import org.springframework.cache.interceptor.KeyGenerator
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.data.redis.connection.RedisConnectionFactory
+import org.springframework.data.redis.connection.RedisPassword
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration
+import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
+import org.springframework.data.redis.core.RedisTemplate
+import org.springframework.data.redis.repository.configuration.EnableRedisRepositories
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
+import org.springframework.data.redis.serializer.StringRedisSerializer
+import org.springframework.http.client.SimpleClientHttpRequestFactory
+import org.springframework.scheduling.annotation.EnableScheduling
+import org.springframework.stereotype.Component
+import org.springframework.web.client.RestTemplate
+import java.net.InetSocketAddress
+import java.net.Proxy
+
+
+@Configuration
+@EnableRedisRepositories
+class AppConfig {
+
+    @Autowired
+    private lateinit var redis: RedisProperties
+
+    @Bean
+    fun redisConnectionFactory(): RedisConnectionFactory {
+        val clientConfig = LettuceClientConfiguration.builder()
+                .readFrom(ReadFrom.SLAVE_PREFERRED)
+                .build()
+        val serverConfig = RedisStandaloneConfiguration(redis.host, redis.port)
+        if (redis.password.isNotEmpty()) {
+            serverConfig.password = RedisPassword.of(redis.password)
+        }
+        serverConfig.database = redis.database
+        return LettuceConnectionFactory(serverConfig, clientConfig)
+    }
+
+    @Bean("tenantCacheKey")
+    fun createTenantCacheableKey(): KeyGenerator {
+        return TenantCacheKeyGen()
+    }
+
+    @Bean
+    fun lockProvider(connectionFactory: RedisConnectionFactory): LockProvider {
+        return RedisLockProvider(connectionFactory, "prod")
+    }
+}
+
+@Configuration
+class JwtAuthFilterRegistration {
+    @Autowired
+    private lateinit var apiJwtAuthenticationFilter: ApiJwtAuthenticationFilter
+
+    @Autowired
+    private lateinit var mobileSecurityFilter: MobileSecurityFilter
+
+    @Bean
+    fun registerJwtFilter(): FilterRegistrationBean<ApiJwtAuthenticationFilter> {
+        val registration = FilterRegistrationBean<ApiJwtAuthenticationFilter>()
+        registration.filter = apiJwtAuthenticationFilter
+        registration.addUrlPatterns("/api/*")
+        registration.order = 1
+        return registration
+    }
+
+    @Bean
+    fun registerMobileFilter(): FilterRegistrationBean<MobileSecurityFilter> {
+        val registrationBean = FilterRegistrationBean<MobileSecurityFilter>()
+        registrationBean.filter = mobileSecurityFilter
+        registrationBean.addUrlPatterns("/mobile/*")
+        registrationBean.order = 2
+        return registrationBean
+    }
+}
+
+@Configuration
+class HttpSessionConfig {
+    @Bean
+    fun sessionRedisTemplate(
+            connectionFactory: RedisConnectionFactory): RedisTemplate<Any, Any> {
+        val template = RedisTemplate<Any, Any>()
+        template.keySerializer = StringRedisSerializer()
+        template.hashKeySerializer = StringRedisSerializer()
+
+        template.setDefaultSerializer(GenericJackson2JsonRedisSerializer())
+        template.setConnectionFactory(connectionFactory)
+        return template
+    }
+}
+
+@Configuration
+class RestTemplateConfig {
+    @Component
+    @ConfigurationProperties("resttemplate.proxy")
+    class RestTemplateProxyProperties {
+        @Value("\${type:}")
+        lateinit var type: String
+        @Value("\${host:}")
+        lateinit var host: String
+        @Value("\${port:0}")
+        var port: Int = 0
+    }
+
+    @Bean
+    fun simpleClientHttpRequestFactory(proxyProperties: RestTemplateProxyProperties):
+            SimpleClientHttpRequestFactory {
+        val factory = SimpleClientHttpRequestFactory()
+        factory.setConnectTimeout(15000)
+        factory.setReadTimeout(5000)
+        if (proxyProperties.type.isNotEmpty()) {
+            val proxyType = when (proxyProperties.type) {
+                "http" -> Proxy.Type.HTTP
+                "socks5" -> Proxy.Type.SOCKS
+                else -> Proxy.Type.DIRECT
+            }
+            if (proxyType != Proxy.Type.DIRECT) {
+                factory.setProxy(Proxy(proxyType,
+                        InetSocketAddress(proxyProperties.host, proxyProperties.port)))
+            }
+        }
+        return factory
+    }
+
+    @Bean
+    fun restTemplate(factory: SimpleClientHttpRequestFactory): RestTemplate {
+        return RestTemplate(factory)
+    }
+}
+
+@EnableSchedulerLock(defaultLockAtMostFor = "PT30M")
+@SpringBootApplication
+@EnableDiscoveryClient
+@EnableScheduling
+@EnableCaching
+@ServletComponentScan
+class PayApiApplication : SpringBootServletInitializer() {
+
+    override fun configure(builder: SpringApplicationBuilder): SpringApplicationBuilder {
+        return builder.sources(PayApiApplication::class.java)
+    }
+}
+
+fun main(args: Array<String>) {
+    SpringApplication.run(PayApiApplication::class.java, * args)
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/agent/service/citizencard_service.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/agent/service/citizencard_service.kt
new file mode 100644
index 0000000..76d4f46
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/agent/service/citizencard_service.kt
@@ -0,0 +1,66 @@
+package com.supwisdom.dlpay.agent.service
+
+import com.supwisdom.dlpay.agent.AgentCode
+import com.supwisdom.dlpay.agent.AgentPayService
+import com.supwisdom.dlpay.agent.AgentResponse
+import com.supwisdom.dlpay.agent.DtlStatus
+import com.supwisdom.dlpay.agent.citizencard.DlpayResp
+import com.supwisdom.dlpay.agent.citizencard.YnrccRespCode
+import com.supwisdom.dlpay.agent.citizencard.YnrccUtil
+import com.supwisdom.dlpay.api.domain.TTransactionMain
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.stereotype.Component
+
+interface CitizencardPayService {
+    fun bindCard(bankcardno: String, username: String, idtype: String, idno: String,
+            phone: String): DlpayResp
+
+    fun signCard(bankcardno: String, username: String, idtype: String, idno: String, phone: String, transtype: String,captcha:String): DlpayResp
+
+    fun cardPay(shopaccno: String, userid: String, accdate: String, amount: Int, scenario: String, refno: String): DlpayResp
+
+    fun cardPayRefund(refno: String, accdate: String, orignRefno: String, amount: Int): DlpayResp
+
+    fun queryResult(orignRefno: String): DlpayResp
+
+    fun getChkfilename(checkdate: String, merchantBankcardno: String?): DlpayResp
+
+    fun makeSureCheckResult(trxdate: String, transcnt: Int, transamt: Long): DlpayResp
+
+    fun bankCardLoss(bankcardno: String, username: String, idtype: String, idno: String): DlpayResp
+}
+
+@Component("citizenCardAgent")
+class CitizenCardPayAgent : AgentPayService<DtlStatus> {
+    @Autowired
+    private lateinit var citizencardPayService: CitizencardPayService
+
+    private fun agentCode(code: String, msg: String?): org.springframework.data.util.Pair<AgentCode, YnrccRespCode> {
+        return YnrccUtil.errcode.firstOrNull {
+            it.second.code == code
+        }?.let {
+            org.springframework.data.util.Pair.of(it.first, YnrccRespCode(
+                    code, it.second.msg.replace("{message}", msg ?: "未知")
+            ))
+        } ?: org.springframework.data.util.Pair.of(AgentCode.COMMON_ERROR,
+                YnrccRespCode(code, msg ?: "未知"))
+    }
+
+    override fun query(transaction: TTransactionMain): AgentResponse<DtlStatus> {
+        val resp = citizencardPayService.queryResult(transaction.refno)
+
+        return AgentResponse<DtlStatus>().also {
+            val code = agentCode(resp.code, resp.message)
+            it.code = code.first
+            it.agentCode = code.second.code
+            it.agentMsg = code.first.message() + "-" + code.second.msg
+            it.agentRefno = resp.bankjourno
+            it.dtlStatus = when (resp.status) {
+                YnrccUtil.DTL_STATUS_SUCCESS -> DtlStatus.SUCCESS
+                YnrccUtil.DTL_STATUS_REFUND -> DtlStatus.REFUND
+                YnrccUtil.DTL_STATUS_PART_REFUND -> DtlStatus.PARTIAL_REFUND
+                else -> DtlStatus.FAIL
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/agent/service/impl/citizencard_service_impl.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/agent/service/impl/citizencard_service_impl.kt
new file mode 100644
index 0000000..bc1ecca
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/agent/service/impl/citizencard_service_impl.kt
@@ -0,0 +1,505 @@
+package com.supwisdom.dlpay.agent.service.impl
+
+import com.google.gson.Gson
+import com.sun.jersey.api.client.Client
+import com.sun.jersey.api.client.ClientResponse
+import com.sun.jersey.core.util.MultivaluedMapImpl
+import com.supwisdom.dlpay.agent.citizencard.DlpayResp
+import com.supwisdom.dlpay.api.service.CardService
+import com.supwisdom.dlpay.api.service.SourceTypeService
+import com.supwisdom.dlpay.api.service.UserService
+import com.supwisdom.dlpay.api.types.IDTypes
+import com.supwisdom.dlpay.agent.service.CitizencardPayService
+import com.supwisdom.dlpay.framework.service.SystemUtilService
+import com.supwisdom.dlpay.framework.util.*
+import com.supwisdom.dlpay.agent.citizencard.YnrccUtil
+import mu.KotlinLogging
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.stereotype.Service
+import javax.ws.rs.core.MediaType
+
+@Service
+class CitizencardPayServiceImpl : CitizencardPayService {
+    @Autowired
+    lateinit var sourceTypeService: SourceTypeService
+    @Autowired
+    lateinit var systemUtilService: SystemUtilService
+    @Autowired
+    lateinit var cardService: CardService
+    @Autowired
+    lateinit var userService: UserService
+
+    private val logger = KotlinLogging.logger { }
+
+    private fun checkCitizencardConfig(config: Map<String, String?>, resp: DlpayResp): Boolean {
+        val agentUrl = config[YnrccUtil.YNRCC_ANGENT_URL]
+        val signKey = config[YnrccUtil.YNRCC_SIGNKEY]
+        if (agentUrl.isNullOrEmpty()) {
+            resp.code = YnrccUtil.PARAM_CONFIG_ERROR
+            resp.message = "系统参数未配置[农商行前置地址前缀]"
+            logger.error(resp.message)
+            return false
+        }
+        if (signKey.isNullOrEmpty()) {
+            resp.code = YnrccUtil.PARAM_CONFIG_ERROR
+            resp.message = "系统参数未配置[农商行前置签名秘钥]"
+            logger.error(resp.message)
+            return false
+        }
+        return true
+    }
+
+    override fun bindCard(bankcardno: String, username: String, idtype: String, idno: String, phone: String): DlpayResp {
+        var resp = DlpayResp()
+        val config = sourceTypeService.getChargePaytypeConfig(TradeDict.PAYTYPE_CITIZEN_CARD, true)
+        if (!checkCitizencardConfig(config, resp)) {
+            return resp
+        }
+        val idType = IDTypes.findByValue(idtype)
+        if (idType < 0) {
+            resp.code = YnrccUtil.PARAM_VALUE_ERROR
+            resp.message = "证件类型未识别[$idtype]"
+            logger.error(resp.message)
+            return resp
+        }
+        val systime = systemUtilService.sysdatetime
+        val refno = systemUtilService.refno
+        val params = hashMapOf<String, String>()
+        params["transcode"] = YnrccUtil.BANKCARD_BIND_TRANSCODE
+        params["transdate"] = systime.hostdate
+        params["transtime"] = systime.hosttime
+        params["categorie"] = YnrccUtil.DLPAY_CATEGORIE
+        params["refno"] = refno
+        params["bankcardno"] = bankcardno
+        params["username"] = username
+        params["idtype"] = idType.toString()
+        params["idno"] = idno
+        params["phone"] = phone
+        params["sign_type"] = "MD5"
+        val sign = MD5.encodeByMD5(StringUtil.createLinkString(StringUtil.paraFilter(params)) + config[YnrccUtil.YNRCC_SIGNKEY]!!.trim())
+        params["sign"] = sign
+
+        val postData = MultivaluedMapImpl()
+        params.forEach { (t, u) -> postData.add(t, u) }
+
+        val url = config[YnrccUtil.YNRCC_ANGENT_URL]!!.trim() + "/bindcard"
+        logger.error("url=[$url], params=[" + Gson().toJson(params) + "]")
+        val client = Client.create()
+        client.setConnectTimeout(YnrccUtil.AGENT_CONNECT_TIMEOUT * 1000)
+        client.setReadTimeout(YnrccUtil.AGENT_READ_TIMEOUT * 1000)
+        val respClient = client.resource(url).type(MediaType.APPLICATION_FORM_URLENCODED_TYPE).accept(MediaType.APPLICATION_JSON).post(ClientResponse::class.java, postData)
+        return if (200 == respClient.status) {
+            val jsonStr = respClient.getEntity(String::class.java)
+            logger.error("refno=[$refno], url=[$url], return=[$jsonStr]")
+            resp = Gson().fromJson(jsonStr, DlpayResp::class.java)
+            resp
+        } else {
+            resp.code = "99"
+            resp.message = "请求前置返回失败[httpStatus=$respClient.status]"
+            logger.error(resp.message)
+            resp
+        }
+    }
+
+    override fun signCard(bankcardno: String, username: String, idtype: String, idno: String, phone: String, transtype: String,captcha:String): DlpayResp {
+        var resp = DlpayResp()
+        val config = sourceTypeService.getChargePaytypeConfig(TradeDict.PAYTYPE_CITIZEN_CARD, true)
+        if (!checkCitizencardConfig(config, resp)) {
+            return resp
+        }
+        val idType = IDTypes.findByValue(idtype)
+        if (idType < 0) {
+            resp.code = YnrccUtil.PARAM_VALUE_ERROR
+            resp.message = "证件类型未识别[$idtype]"
+            logger.error(resp.message)
+            return resp
+        }
+        if (YnrccUtil.TRANSTYPE_SIGNCARD != transtype && YnrccUtil.TRANSTYPE_UNSIGNCARD != transtype) {
+            resp.code = YnrccUtil.PARAM_VALUE_ERROR
+            resp.message = "签约类型错误[$transtype]"
+            logger.error(resp.message)
+            return resp
+        }
+
+        val systime = systemUtilService.sysdatetime
+        val refno = systemUtilService.refno
+        val params = hashMapOf<String, String>()
+        params["transcode"] = YnrccUtil.BANKCARD_SIGN_TRANSCODE
+        params["transdate"] = systime.hostdate
+        params["transtime"] = systime.hosttime
+        params["refno"] = refno
+        params["categorie"] = YnrccUtil.DLPAY_CATEGORIE
+        params["transtype"] = transtype
+        params["bankcardno"] = bankcardno
+        params["username"] = username
+        params["idtype"] = idType.toString()
+        params["idno"] = idno
+        params["phone"] = phone
+        params["captcha"] = captcha
+        params["sign_type"] = "MD5"
+        val sign = MD5.encodeByMD5(StringUtil.createLinkString(StringUtil.paraFilter(params)) + config[YnrccUtil.YNRCC_SIGNKEY]!!.trim())
+        params["sign"] = sign
+
+        val postData = MultivaluedMapImpl()
+        params.forEach { (t, u) -> postData.add(t, u) }
+
+        val url = config[YnrccUtil.YNRCC_ANGENT_URL]!!.trim() + "/signcard"
+        logger.error("url=[$url], params=[" + Gson().toJson(params) + "]")
+        val client = Client.create()
+        client.setConnectTimeout(YnrccUtil.AGENT_CONNECT_TIMEOUT * 1000)
+        client.setReadTimeout(YnrccUtil.AGENT_READ_TIMEOUT * 1000)
+        val respClient = client.resource(url).type(MediaType.APPLICATION_FORM_URLENCODED_TYPE).accept(MediaType.APPLICATION_JSON).post(ClientResponse::class.java, postData)
+        return if (200 == respClient.status) {
+            val jsonStr = respClient.getEntity(String::class.java)
+            logger.error("refno=[$refno], url=[$url], return=[$jsonStr]")
+            resp = Gson().fromJson(jsonStr, DlpayResp::class.java)
+            resp
+        } else {
+            resp.code = "99"
+            resp.message = "请求前置返回失败[httpStatus=$respClient.status]"
+            logger.error(resp.message)
+            resp
+        }
+    }
+
+    override fun cardPay(shopaccno: String, userid: String, accdate: String, amount: Int, scenario: String, refno: String): DlpayResp {
+        var resp = DlpayResp()
+        val config = sourceTypeService.getConsumePaytypeConfig(TradeDict.PAYTYPE_CITIZEN_CARD, shopaccno, false, false)
+        if (!checkCitizencardConfig(config, resp)) {
+            return resp
+        }
+        val merchantBankcardno = config[YnrccUtil.YNRCC_MERCHANT_BANKCARDNO]
+        val merchantBankaccname = config[YnrccUtil.YNRCC_MERCHANT_BANKACCNAME]
+        if (StringUtil.isEmpty(merchantBankcardno) || StringUtil.isEmpty(merchantBankaccname)) {
+            resp.code = YnrccUtil.PARAM_CONFIG_ERROR
+            resp.message = "系统参数未配置[商户收款银行账号]"
+            logger.error(resp.message)
+            return resp
+        }
+        val person = userService.findOnePersonByUserid(userid)
+        val idType = IDTypes.findByValue(person.idtype)
+        if (idType < 0) {
+            resp.code = YnrccUtil.PARAM_VALUE_ERROR
+            resp.message = "证件类型未识别[${person.idtype}]"
+            logger.error(resp.message)
+            return resp
+        }
+        val userBankcard = cardService.getBankcardByUserid(person.userid)
+        if (null == userBankcard) {
+            resp.code = "99"
+            resp.message = "用户[${person.userid}]未绑定银行卡"
+            logger.error(resp.message)
+            return resp
+        } else if (TradeDict.STATUS_NORMAL != userBankcard.status) {
+            resp.code = "99"
+            resp.message = "用户[${person.userid}]绑定银行卡状态异常"
+            logger.error(resp.message)
+            return resp
+        } else if (!userBankcard.signed) {
+            resp.code = "99"
+            resp.message = "用户[${person.userid}]绑定银行卡未签约"
+            logger.error(resp.message)
+            return resp
+        }
+        val bankcardno = userBankcard.cardno
+
+        val systime = systemUtilService.sysdatetime
+        val params = hashMapOf<String, String>()
+        params["transcode"] = YnrccUtil.BANKCARD_PAY_TRANSCODE
+        params["transdate"] = accdate
+        params["transtime"] = systime.hosttime
+        params["refno"] = refno
+        params["categorie"] = YnrccUtil.DLPAY_CATEGORIE
+        params["bankcardno"] = bankcardno
+        params["username"] = person.name
+        params["idtype"] = idType.toString()
+        params["idno"] = person.idno
+        params["merchant_bankcardno"] = merchantBankcardno!!
+        params["merchant_bankaccname"] = merchantBankaccname!!
+        params["amount"] = amount.toString()
+        params["scenario"] = scenario
+        params["description"] = "市民卡代扣消费"
+        params["sign_type"] = "MD5"
+        val sign = MD5.encodeByMD5(StringUtil.createLinkString(StringUtil.paraFilter(params)) + config[YnrccUtil.YNRCC_SIGNKEY]!!.trim())
+        params["sign"] = sign
+
+        val postData = MultivaluedMapImpl()
+        params.forEach { (t, u) -> postData.add(t, u) }
+
+        val url = config[YnrccUtil.YNRCC_ANGENT_URL]!!.trim() + "/cardpay"
+        logger.error("url=[$url], params=[" + Gson().toJson(params) + "]")
+        try {
+            val client = Client.create()
+            client.setConnectTimeout(YnrccUtil.AGENT_CONNECT_TIMEOUT * 1000)
+            client.setReadTimeout(YnrccUtil.AGENT_READ_TIMEOUT * 1000)
+            val respClient = client.resource(url).type(MediaType.APPLICATION_FORM_URLENCODED_TYPE).accept(MediaType.APPLICATION_JSON).post(ClientResponse::class.java, postData)
+            return if (200 == respClient.status) {
+                val jsonStr = respClient.getEntity(String::class.java)
+                logger.error("refno=[$refno], url=[$url], return=[$jsonStr]")
+                resp = Gson().fromJson(jsonStr, DlpayResp::class.java)
+                resp
+            } else {
+                resp.code = "99"
+                resp.message = "请求前置返回失败[httpStatus=$respClient.status]"
+                logger.error(resp.message)
+                resp
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+            resp.code = YnrccUtil.CODE_EXCEPTION
+            resp.message = "请求前置抛出异常"
+            logger.error(resp.message)
+            return resp
+        }
+    }
+
+    override fun cardPayRefund(refno: String, accdate: String, orignRefno: String, amount: Int): DlpayResp {
+        var resp = DlpayResp()
+        val config = sourceTypeService.getChargePaytypeConfig(TradeDict.PAYTYPE_CITIZEN_CARD, true)
+        if (!checkCitizencardConfig(config, resp)) {
+            return resp
+        }
+
+        val systime = systemUtilService.sysdatetime
+        val params = hashMapOf<String, String>()
+        params["transcode"] = YnrccUtil.BANKCARD_PAYREFUND_TRANSCODE
+        params["transdate"] = accdate //我们的记账日期
+        params["transtime"] = systime.hosttime
+        params["refno"] = refno
+        params["refundRefno"] = orignRefno
+        params["amount"] = amount.toString()
+        params["description"] = "市民卡代扣消费退款"
+        params["sign_type"] = "MD5"
+        val sign = MD5.encodeByMD5(StringUtil.createLinkString(StringUtil.paraFilter(params)) + config[YnrccUtil.YNRCC_SIGNKEY]!!.trim())
+        params["sign"] = sign
+
+        val postData = MultivaluedMapImpl()
+        params.forEach { (t, u) -> postData.add(t, u) }
+
+        val url = config[YnrccUtil.YNRCC_ANGENT_URL]!!.trim() + "/cardpayrefund"
+        logger.error("url=[$url], params=[" + Gson().toJson(params) + "]")
+        try {
+            val client = Client.create()
+            client.setConnectTimeout(YnrccUtil.AGENT_CONNECT_TIMEOUT * 1000)
+            client.setReadTimeout(YnrccUtil.AGENT_READ_TIMEOUT * 1000)
+            val respClient = client.resource(url).type(MediaType.APPLICATION_FORM_URLENCODED_TYPE).accept(MediaType.APPLICATION_JSON).post(ClientResponse::class.java, postData)
+            return if (200 == respClient.status) {
+                val jsonStr = respClient.getEntity(String::class.java)
+                logger.error("refno=[$refno],refundRefno=[$orignRefno], url=[$url], return=[$jsonStr]")
+                resp = Gson().fromJson(jsonStr, DlpayResp::class.java)
+                resp
+            } else {
+                resp.code = "99"
+                resp.message = "请求前置返回失败[httpStatus=$respClient.status]"
+                logger.error(resp.message)
+                resp
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+            resp.code = YnrccUtil.CODE_EXCEPTION
+            resp.message = "请求前置抛出异常"
+            logger.error(resp.message)
+            return resp
+        }
+    }
+
+    override fun queryResult(orignRefno: String): DlpayResp {
+        var resp = DlpayResp()
+        val config = sourceTypeService.getChargePaytypeConfig(TradeDict.PAYTYPE_CITIZEN_CARD, true)
+        if (!checkCitizencardConfig(config, resp)) {
+            return resp
+        }
+
+        val systime = systemUtilService.sysdatetime
+        val refno = systemUtilService.refno
+        val params = hashMapOf<String, String>()
+        params["transcode"] = YnrccUtil.BANKCARD_QUERYRESULT_TRANSCODE
+        params["transdate"] = systime.hostdate
+        params["transtime"] = systime.hosttime
+        params["refno"] = refno
+        params["orignRefno"] = orignRefno
+        params["sign_type"] = "MD5"
+        val sign = MD5.encodeByMD5(StringUtil.createLinkString(StringUtil.paraFilter(params)) + config[YnrccUtil.YNRCC_SIGNKEY]!!.trim())
+        params["sign"] = sign
+
+        val postData = MultivaluedMapImpl()
+        params.forEach { (t, u) -> postData.add(t, u) }
+
+        val url = config[YnrccUtil.YNRCC_ANGENT_URL]!!.trim() + "/queryresult"
+        logger.error("url=[$url], params=[" + Gson().toJson(params) + "]")
+        try {
+            val client = Client.create()
+            client.setConnectTimeout(YnrccUtil.AGENT_CONNECT_TIMEOUT * 1000)
+            client.setReadTimeout(YnrccUtil.AGENT_READ_TIMEOUT * 1000)
+            val respClient = client.resource(url).type(MediaType.APPLICATION_FORM_URLENCODED_TYPE).accept(MediaType.APPLICATION_JSON).post(ClientResponse::class.java, postData)
+            return if (200 == respClient.status) {
+                val jsonStr = respClient.getEntity(String::class.java)
+                logger.error("refno=[$orignRefno], url=[$url], return=[$jsonStr]")
+                resp = Gson().fromJson(jsonStr, DlpayResp::class.java)
+                resp
+            } else {
+                resp.code = "99"
+                resp.message = "请求前置返回失败[httpStatus=$respClient.status]"
+                logger.error(resp.message)
+                resp
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+            resp.code = YnrccUtil.CODE_EXCEPTION
+            resp.message = "请求前置抛出异常"
+            logger.error(resp.message)
+            return resp
+        }
+    }
+
+    override fun getChkfilename(checkdate: String, merchantBankcardno: String?): DlpayResp {
+        var resp = DlpayResp()
+        val config = sourceTypeService.getChargePaytypeConfig(TradeDict.PAYTYPE_CITIZEN_CARD, true)
+        if (!checkCitizencardConfig(config, resp)) {
+            return resp
+        }
+        val systime = systemUtilService.sysdatetime
+        val refno = systemUtilService.refno
+        val params = hashMapOf<String, String>()
+        params["transcode"] = YnrccUtil.BANKCARD_CHKFILE_TRANSCODE
+        params["transdate"] = systime.hostdate
+        params["transtime"] = systime.hosttime
+        params["refno"] = refno
+        params["checkdate"] = checkdate
+        params["merchant_bankcardno"] = merchantBankcardno ?: ""
+        params["sign_type"] = "MD5"
+        val sign = MD5.encodeByMD5(StringUtil.createLinkString(StringUtil.paraFilter(params)) + config[YnrccUtil.YNRCC_SIGNKEY]!!.trim())
+        params["sign"] = sign
+
+        val postData = MultivaluedMapImpl()
+        params.forEach { (t, u) -> postData.add(t, u) }
+
+        val url = config[YnrccUtil.YNRCC_ANGENT_URL]!!.trim() + "/checkdtl"
+        logger.error("url=[$url], params=[" + Gson().toJson(params) + "]")
+        val client = Client.create()
+        client.setConnectTimeout(YnrccUtil.AGENT_CONNECT_TIMEOUT * 1000)
+        client.setReadTimeout(YnrccUtil.AGENT_READ_TIMEOUT * 1000)
+        val respClient = client.resource(url).type(MediaType.APPLICATION_FORM_URLENCODED_TYPE).accept(MediaType.APPLICATION_JSON).post(ClientResponse::class.java, postData)
+        return if (200 == respClient.status) {
+            val jsonStr = respClient.getEntity(String::class.java)
+            logger.error("refno=[$refno],chkdate=[$checkdate], url=[$url], return=[$jsonStr]")
+            resp = Gson().fromJson(jsonStr, DlpayResp::class.java)
+            resp
+        } else {
+            resp.code = "99"
+            resp.message = "请求前置返回失败[httpStatus=$respClient.status]"
+            logger.error(resp.message)
+            resp
+        }
+    }
+
+    override fun makeSureCheckResult(trxdate: String, transcnt: Int, transamt: Long): DlpayResp {
+        var resp = DlpayResp()
+        val config = sourceTypeService.getChargePaytypeConfig(TradeDict.PAYTYPE_CITIZEN_CARD, true)
+        if (!checkCitizencardConfig(config, resp)) {
+            return resp
+        }
+
+        val systime = systemUtilService.sysdatetime
+        val refno = systemUtilService.refno
+        val params = hashMapOf<String, String>()
+        params["transcode"] = YnrccUtil.BANKCARD_CHKMAKESURE_TRANSCODE
+        params["transdate"] = systime.hostdate
+        params["transtime"] = systime.hosttime
+        params["refno"] = refno
+        params["stltrxdate"] = trxdate
+        params["stlamt"] = transamt.toString()
+        params["jnlcount"] = transcnt.toString()
+        params["sign_type"] = "MD5"
+        val sign = MD5.encodeByMD5(StringUtil.createLinkString(StringUtil.paraFilter(params)) + config[YnrccUtil.YNRCC_SIGNKEY]!!.trim())
+        params["sign"] = sign
+
+        val postData = MultivaluedMapImpl()
+        params.forEach { (t, u) -> postData.add(t, u) }
+
+        val url = config[YnrccUtil.YNRCC_ANGENT_URL]!!.trim() + "/checkmakesure"
+        logger.error("url=[$url], params=[" + Gson().toJson(params) + "]")
+        try {
+            val client = Client.create()
+            client.setConnectTimeout(YnrccUtil.AGENT_CONNECT_TIMEOUT * 1000)
+            client.setReadTimeout(YnrccUtil.AGENT_READ_TIMEOUT * 1000)
+            val respClient = client.resource(url).type(MediaType.APPLICATION_FORM_URLENCODED_TYPE).accept(MediaType.APPLICATION_JSON).post(ClientResponse::class.java, postData)
+            return if (200 == respClient.status) {
+                val jsonStr = respClient.getEntity(String::class.java)
+                logger.error("refno=[$refno],trxdate=[$trxdate], url=[$url], return=[$jsonStr]")
+                resp = Gson().fromJson(jsonStr, DlpayResp::class.java)
+                resp
+            } else {
+                resp.code = "99"
+                resp.message = "请求前置返回失败[httpStatus=$respClient.status]"
+                logger.error(resp.message)
+                resp
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+            resp.code = YnrccUtil.CODE_EXCEPTION
+            resp.message = "请求前置抛出异常"
+            logger.error(resp.message)
+            return resp
+        }
+    }
+
+    override fun bankCardLoss(bankcardno: String, username: String, idtype: String, idno: String): DlpayResp {
+        var resp = DlpayResp()
+        val config = sourceTypeService.getChargePaytypeConfig(TradeDict.PAYTYPE_CITIZEN_CARD, true)
+        if (!checkCitizencardConfig(config, resp)) {
+            return resp
+        }
+        val idType = IDTypes.findByValue(idtype)
+        if (idType < 0) {
+            resp.code = YnrccUtil.PARAM_VALUE_ERROR
+            resp.message = "证件类型未识别[$idtype]"
+            logger.error(resp.message)
+            return resp
+        }
+
+        val systime = systemUtilService.sysdatetime
+        val refno = systemUtilService.refno
+        val params = hashMapOf<String, String>()
+        params["transcode"] = YnrccUtil.BANKCARD_BANKCARDLOSS_TRANSCODE
+        params["transdate"] = systime.hostdate
+        params["transtime"] = systime.hosttime
+        params["refno"] = refno
+        params["bankcardno"] = bankcardno
+        params["username"] = username
+        params["idtype"] = idType.toString()
+        params["idno"] = idno
+        params["sign_type"] = "MD5"
+        val sign = MD5.encodeByMD5(StringUtil.createLinkString(StringUtil.paraFilter(params)) + config[YnrccUtil.YNRCC_SIGNKEY]!!.trim())
+        params["sign"] = sign
+
+        val postData = MultivaluedMapImpl()
+        params.forEach { (t, u) -> postData.add(t, u) }
+
+        val url = config[YnrccUtil.YNRCC_ANGENT_URL]!!.trim() + "/bankcardloss"
+        logger.error("url=[$url], params=[" + Gson().toJson(params) + "]")
+        try {
+            val client = Client.create()
+            client.setConnectTimeout(YnrccUtil.AGENT_CONNECT_TIMEOUT * 1000)
+            client.setReadTimeout(YnrccUtil.AGENT_READ_TIMEOUT * 1000)
+            val respClient = client.resource(url).type(MediaType.APPLICATION_FORM_URLENCODED_TYPE).accept(MediaType.APPLICATION_JSON).post(ClientResponse::class.java, postData)
+            return if (200 == respClient.status) {
+                val jsonStr = respClient.getEntity(String::class.java)
+                logger.error("refno=[$refno],bankcardno=[$bankcardno], url=[$url], return=[$jsonStr]")
+                resp = Gson().fromJson(jsonStr, DlpayResp::class.java)
+                resp
+            } else {
+                resp.code = "99"
+                resp.message = "请求前置返回失败[httpStatus=$respClient.status]"
+                logger.error(resp.message)
+                resp
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+            resp.code = YnrccUtil.CODE_EXCEPTION
+            resp.message = "请求前置抛出异常"
+            logger.error(resp.message)
+            return resp
+        }
+    }
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/card_service.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/card_service.kt
new file mode 100644
index 0000000..aa492a0
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/card_service.kt
@@ -0,0 +1,9 @@
+package com.supwisdom.dlpay.api.service
+
+import com.supwisdom.dlpay.api.domain.TCard
+import org.springframework.transaction.annotation.Transactional
+
+interface CardService {
+    @Transactional(rollbackFor = arrayOf(Exception::class), readOnly = true)
+    fun getBankcardByUserid(userid: String): TCard?
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/card_service_impl.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/card_service_impl.kt
new file mode 100644
index 0000000..2b257b7
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/card_service_impl.kt
@@ -0,0 +1,23 @@
+package com.supwisdom.dlpay.api.service.impl
+
+import com.supwisdom.dlpay.api.dao.CardDao
+import com.supwisdom.dlpay.api.domain.TCard
+import com.supwisdom.dlpay.api.service.CardService
+import com.supwisdom.dlpay.util.ConstantUtil
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.stereotype.Service
+import javax.persistence.EntityManager
+import javax.persistence.PersistenceContext
+
+@Service
+class CardServiceImpl : CardService {
+    @Autowired
+    lateinit var cardDao: CardDao
+
+    @PersistenceContext
+    private lateinit var entityManager: EntityManager
+
+    override fun getBankcardByUserid(userid: String): TCard? {
+        return cardDao.findCardByUseridAndCardtype(userid, ConstantUtil.CARDTYPE_BANKCARD)
+    }
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/qrcode_srvice_impl.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/qrcode_srvice_impl.kt
new file mode 100644
index 0000000..761a5b0
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/qrcode_srvice_impl.kt
@@ -0,0 +1,64 @@
+package com.supwisdom.dlpay.api.service.impl
+
+import com.google.gson.Gson
+import com.supwisdom.dlpay.api.bean.ApiResponse
+import com.supwisdom.dlpay.api.service.QRCodeService
+import com.supwisdom.dlpay.framework.service.SystemUtilService
+import com.supwisdom.dlpay.framework.util.MD5
+import com.supwisdom.dlpay.framework.util.TradeDict
+import com.supwisdom.dlpay.mobile.service.MobileApiService
+import com.supwisdom.dlpay.util.AesUtil
+import com.supwisdom.dlpay.util.QrCodeTotpUtil
+import com.supwisdom.dlpay.util.QrcodeRawData
+import com.supwisdom.dlpay.util.RSAKeysGenerate
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.data.redis.core.RedisTemplate
+import org.springframework.stereotype.Service
+import java.time.Duration
+
+@Service
+class QRCodeServiceImpl:QRCodeService{
+    @Autowired
+    private lateinit var systemUtilService: SystemUtilService
+    @Autowired
+    private lateinit var mobileApiService: MobileApiService
+    @Autowired
+    lateinit var redisTemplate: RedisTemplate<String, String>
+
+    override fun encodeCode(uid:String): ApiResponse {
+        var resp = ApiResponse()
+        val rootkey = systemUtilService.getBusinessValue("aes.cfb.rootkey")
+        val iv = systemUtilService.getBusinessValue("aes.cfb.iv")
+        if (rootkey.isNullOrEmpty()||iv.isNullOrEmpty()) {
+            resp.retcode = 1
+            resp.retmsg = "秘钥未配置"
+            return resp
+        }
+        val muser = mobileApiService.findUserById(uid)
+        if(muser==null||TradeDict.STATUS_NORMAL!=muser.status){
+            resp.retcode = 1
+            resp.retmsg = "用户不存在或状态异常"
+            return resp
+        }
+        if(muser.userid.isNullOrEmpty()){
+            resp.retcode = 1
+            resp.retmsg = "用户未绑定身份"
+            return resp
+        }
+        val totp = QrCodeTotpUtil.generateTOTP(muser.secertkey)
+        val rowdata = QrcodeRawData()
+        rowdata.userid = muser.userid
+        rowdata.setTotp(totp)
+        val orgData = Gson().toJson(rowdata)
+        val publicKey = RSAKeysGenerate.getPublicKey(muser.rsapublic)
+        val encdata = org.apache.commons.codec.binary.Base64.encodeBase64String(RSAKeysGenerate.encryptbyte(publicKey, orgData))
+        val qrcode = AesUtil.encryptCFB("$uid:$encdata", rootkey, iv, "AES/CFB/NoPadding")
+
+        val key = MD5.encodeByMD5ToURLSafeBase64(qrcode)
+        redisTemplate.opsForValue().set(key,qrcode, Duration.ofSeconds(20))
+        resp.retcode = 0
+        resp.retmsg = key
+        return resp
+    }
+}
+
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/user_service_impl.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/user_service_impl.kt
new file mode 100644
index 0000000..45c0d3f
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/user_service_impl.kt
@@ -0,0 +1,66 @@
+package com.supwisdom.dlpay.api.service.impl
+
+import com.supwisdom.dlpay.api.dao.AccountDao
+import com.supwisdom.dlpay.api.dao.PersonDao
+import com.supwisdom.dlpay.api.dao.PersondtlDao
+import com.supwisdom.dlpay.api.dao.PointsAccountDao
+import com.supwisdom.dlpay.api.domain.TAccount
+import com.supwisdom.dlpay.api.domain.TPerson
+import com.supwisdom.dlpay.api.domain.TPersondtl
+import com.supwisdom.dlpay.api.domain.TPointsAccount
+import com.supwisdom.dlpay.api.service.UserService
+import com.supwisdom.dlpay.exception.TransactionProcessException
+import com.supwisdom.dlpay.framework.util.*
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.data.domain.PageRequest
+import org.springframework.data.domain.Sort
+import org.springframework.stereotype.Service
+
+/**
+ * Created by shuwei on 2019/4/15.
+ */
+@Service
+class UserServiceImpl : UserService {
+    @Autowired
+    private lateinit var personDao: PersonDao
+    @Autowired
+    private lateinit var accountDao: AccountDao
+    @Autowired
+    private lateinit var pointsAccountDao: PointsAccountDao
+    @Autowired
+    private lateinit var persondtlDao: PersondtlDao
+
+    override fun findAccountByUserid(userid: String, subjno: String?): TAccount? {
+        return if (!StringUtil.isEmpty(subjno)) {
+            accountDao.findByUseridAndSubjno(userid, subjno)
+        } else {
+            accountDao.findByUseridAndSubjno(userid, Subject.SUBJNO_PERSONAL_DEPOSIT)
+        }
+    }
+
+    override fun findPointsAccountByUserid(userid: String): TPointsAccount? {
+        return pointsAccountDao.findByUserid(userid)
+    }
+
+    override fun findOnePersonByUserid(userid: String): TPerson {
+        return personDao.findByUserid(userid)
+                ?: throw TransactionProcessException(TradeErrorCode.ACCOUNT_NOT_EXISTS, "用户<$userid>不存在")
+    }
+
+    override fun findPersondtlByUserid(userid: String,pageno:Int): PageResult<TPersondtl> {
+        var pageable = PageRequest.of(pageno - 1, 10, Sort.Direction.DESC, "transdate","transtime")
+        return PageResult<TPersondtl>(persondtlDao.findByUseridAndStatus(userid,TradeDict.DTL_STATUS_SUCCESS,pageable))
+    }
+
+    override fun findPersondtlDetailByUserid(userid: String, billno: String): TPersondtl? {
+        var dtl = persondtlDao.findById(billno)
+        if(dtl.isPresent){
+            if(userid!=dtl.get().userid){
+                return null
+            }
+            return dtl.get()
+        }else{
+            return null
+        }
+    }
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/qrcode_service.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/qrcode_service.kt
new file mode 100644
index 0000000..f379574
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/qrcode_service.kt
@@ -0,0 +1,7 @@
+package com.supwisdom.dlpay.api.service
+
+import com.supwisdom.dlpay.api.bean.ApiResponse
+
+interface QRCodeService {
+    fun encodeCode(uid: String): ApiResponse
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/user_service.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/user_service.kt
new file mode 100644
index 0000000..302db65
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/user_service.kt
@@ -0,0 +1,27 @@
+package com.supwisdom.dlpay.api.service
+
+import com.supwisdom.dlpay.api.domain.*
+import com.supwisdom.dlpay.framework.util.PageResult
+import org.springframework.transaction.annotation.Propagation
+import org.springframework.transaction.annotation.Transactional
+
+/**
+ * Created by shuwei on 2019/4/15.
+ */
+interface UserService {
+    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = arrayOf(Exception::class), readOnly = true)
+    fun findAccountByUserid(userid: String, subjno:String?): TAccount?
+
+    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = arrayOf(Exception::class), readOnly = true)
+    fun findPointsAccountByUserid(userid: String): TPointsAccount?
+
+    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = arrayOf(Exception::class), readOnly = true)
+    fun findOnePersonByUserid(userid: String): TPerson
+
+    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = arrayOf(Exception::class), readOnly = true)
+    fun findPersondtlByUserid(userid:String, pageno :Int) : PageResult<TPersondtl>
+
+    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = arrayOf(Exception::class), readOnly = true)
+    fun findPersondtlDetailByUserid(userid:String, billno :String) : TPersondtl?
+
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/AuthLoginHandler.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/AuthLoginHandler.kt
new file mode 100644
index 0000000..c795cbd
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/AuthLoginHandler.kt
@@ -0,0 +1,145 @@
+package com.supwisdom.dlpay.mobile
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.supwisdom.dlpay.api.bean.JsonResult
+import com.supwisdom.dlpay.api.service.UserService
+import com.supwisdom.dlpay.framework.core.JwtConfig
+import com.supwisdom.dlpay.framework.core.JwtTokenUtil
+import com.supwisdom.dlpay.framework.domain.JwtRedis
+import com.supwisdom.dlpay.framework.redisrepo.ApiJwtRepository
+import com.supwisdom.dlpay.framework.service.SystemUtilService
+import com.supwisdom.dlpay.framework.util.*
+import com.supwisdom.dlpay.mobile.dao.MobileUserDao
+import com.supwisdom.dlpay.mobile.domain.TBMobileUser
+import com.supwisdom.dlpay.mobile.exception.UserLoginFailException
+import com.supwisdom.dlpay.mobile.service.MobileApiService
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.http.HttpStatus
+import org.springframework.security.authentication.BadCredentialsException
+import org.springframework.security.authentication.LockedException
+import org.springframework.security.core.Authentication
+import org.springframework.security.core.AuthenticationException
+import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler
+import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler
+import org.springframework.stereotype.Component
+import java.io.IOException
+import javax.servlet.ServletException
+import javax.servlet.http.HttpServletRequest
+import javax.servlet.http.HttpServletResponse
+
+@Component("authLoginSuccessHandler")
+class AuthLoginSuccessHandler : SimpleUrlAuthenticationSuccessHandler() {
+    @Autowired
+    lateinit var mobileApiService: MobileApiService
+    @Autowired
+    lateinit var objectMapper: ObjectMapper
+    @Autowired
+    lateinit var jwtConfig: JwtConfig
+    @Autowired
+    lateinit var apiJwtRepository: ApiJwtRepository
+    @Autowired
+    lateinit var systemUtilService: SystemUtilService
+    @Autowired
+    lateinit var userService: UserService
+
+    override fun onAuthenticationSuccess(request: HttpServletRequest, response: HttpServletResponse, authentication: Authentication) {
+        val platform = request.getParameter("platform")
+        logger.error(platform)
+        val temp = authentication.principal as TBMobileUser
+        val user = mobileApiService.findUserById(temp.uid)
+        val exp = systemUtilService.getSysparaValueAsInt(SysparaUtil.MOBILE_LOGIN_EXPIRE_IN_SECONDS,60*60*24*3)
+        jwtConfig.expiration = exp.toLong()
+        if (user != null) {
+            //TODO 从数据取jwtConfig.expiration
+            val token = JwtTokenUtil(jwtConfig).generateToken(
+                    mapOf("uid" to user.uid, "issuer" to "payapi",
+                            "audience" to user.loginid,
+                            Constants.JWT_CLAIM_TENANTID to "mobile",
+                            Constants.JWT_CLAIM_AUTHORITIES to temp.authorities))
+            val jwt = JwtRedis().apply {
+                jti = token.jti
+                uid = user.loginid
+                status = TradeDict.JWT_STATUS_NORMAL
+                expiration = token.expiration.valueInMillis
+            }.apply {
+                //删除之前的token
+                if (!user.jti.isNullOrEmpty()) {
+                    apiJwtRepository.deleteById(user.jti!!)
+                }
+                apiJwtRepository.save(this)
+            }
+            if (user.loginpwderror > 0) {
+                user.loginpwderror = 0
+                user.loginpwderrortime = null
+            }
+            user.lastlogin = DateUtil.getNow()
+            user.lastloginplatform = platform
+            user.jti = jwt.jti
+            mobileApiService.saveUser(user)
+            var payseted = false
+            if(!user.paypwd.isNullOrEmpty()){
+                payseted = true
+            }
+            var name = ""
+            var signed=""
+            if (!user.userid.isNullOrEmpty()) {
+                val person = userService.findOnePersonByUserid(user.userid!!)
+                var card = mobileApiService.findCardByUserid(user.userid!!)
+                name = person.name
+                if(card!=null&&card.signed){
+                    signed = TradeDict.STATUS_YES
+                }
+            }
+            response.status = HttpStatus.OK.value()
+            response.contentType = "application/json;charset=UTF-8"
+            response.writer.write(objectMapper.writeValueAsString(JsonResult.ok()
+                    .put("token", token.jwtToken)
+                    ?.put("expire",token.expiration.valueInMillis)
+                    ?.put("now",System.currentTimeMillis())
+                    ?.put("tenantid", "mobile")
+                    ?.put("name", name)
+                    ?.put("uid", user.uid)
+                    ?.put("phone", StringUtil.phoneReplace(user.phone))
+                    ?.put("paypwdset",payseted)
+                    ?.put("signed", signed)
+                    ?.put("userid",if(user.userid.isNullOrEmpty()) "" else user.userid)))
+        } else {
+            throw UserLoginFailException("登录错误")
+        }
+    }
+}
+
+
+@Component("authLoginFailHandler")
+class AuthLoginFailHandler : SimpleUrlAuthenticationFailureHandler() {
+    @Autowired
+    lateinit var mobileUserDao: MobileUserDao
+
+    @Autowired
+    lateinit var objectMapper: ObjectMapper
+
+    @Throws(IOException::class, ServletException::class)
+    override fun onAuthenticationFailure(request: HttpServletRequest,
+                                         response: HttpServletResponse, exception: AuthenticationException) {
+        logger.error("登录失败:" + exception.message + "|" + exception.javaClass)
+        val errmsg = when (exception) {
+            is BadCredentialsException -> "手机号或密码错误"
+            is LockedException -> "账户被锁定"
+            else -> exception.message!!
+        }
+        val temp = request.getParameter("username")
+        if(!temp.isNullOrEmpty()) {
+            mobileUserDao.findByLoginid(temp)?.let {
+                if (it.loginpwderror == 0) {
+                    it.loginpwderror = 0
+                    it.loginpwderrortime = System.currentTimeMillis()
+                }
+                it.loginpwderror += 1
+                mobileUserDao.save(it)
+            }
+        }
+        response.status = HttpStatus.OK.value()
+        response.contentType = "application/json;charset=UTF-8"
+        response.writer.write(objectMapper.writeValueAsString(JsonResult.error(errmsg)))
+    }
+}
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/MobileApi.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/MobileApi.kt
new file mode 100644
index 0000000..2649949
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/MobileApi.kt
@@ -0,0 +1,957 @@
+package com.supwisdom.dlpay.mobile
+
+import com.supwisdom.dlpay.agent.citizencard.YnrccUtil
+import com.supwisdom.dlpay.agent.service.CitizencardPayService
+import com.supwisdom.dlpay.api.bean.JsonResult
+import com.supwisdom.dlpay.api.service.QRCodeService
+import com.supwisdom.dlpay.api.service.UserService
+import com.supwisdom.dlpay.api.util.MobileNumberCheck
+import com.supwisdom.dlpay.framework.core.JwtConfig
+import com.supwisdom.dlpay.framework.core.JwtTokenUtil
+import com.supwisdom.dlpay.framework.domain.JwtRedis
+import com.supwisdom.dlpay.framework.redisrepo.ApiJwtRepository
+import com.supwisdom.dlpay.framework.service.SystemUtilService
+import com.supwisdom.dlpay.framework.util.*
+import com.supwisdom.dlpay.framework.util.Dictionary
+import com.supwisdom.dlpay.mobile.domain.TBMobileUser
+import com.supwisdom.dlpay.mobile.service.MobileApiService
+import com.supwisdom.dlpay.system.service.DictionaryProxy
+import com.supwisdom.dlpay.util.ConstantUtil
+import com.supwisdom.dlpay.util.RSAKeysGenerate
+import mu.KotlinLogging
+import org.apache.commons.lang.StringUtils
+import org.jose4j.jwt.ReservedClaimNames
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.data.redis.core.RedisTemplate
+import org.springframework.http.HttpStatus
+import org.springframework.http.ResponseEntity
+import org.springframework.security.core.GrantedAuthority
+import org.springframework.security.core.authority.AuthorityUtils
+import org.springframework.security.core.context.SecurityContextHolder
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
+import org.springframework.web.bind.annotation.RequestHeader
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestParam
+import org.springframework.web.bind.annotation.RestController
+import org.springframework.web.multipart.MultipartFile
+import java.time.Duration
+import java.util.*
+
+
+@RestController
+@RequestMapping("/mobileapi/i")
+class ApiInit {
+    @Autowired
+    lateinit var mobileApiService: MobileApiService
+    @Autowired
+    lateinit var redisTemplate: RedisTemplate<String, String>
+    @Autowired
+    lateinit var jwtConfig: JwtConfig
+    @Autowired
+    lateinit var apiJwtRepository: ApiJwtRepository
+    @Autowired
+    lateinit var systemUtilService: SystemUtilService
+    val logger = KotlinLogging.logger { }
+
+    @RequestMapping("/time")
+    fun time(): JsonResult {
+        return JsonResult.ok("OK").put("now", System.currentTimeMillis())!!
+    }
+
+    @RequestMapping("/test")
+    fun test(uid: String): JsonResult {
+        return JsonResult.ok("OK").put("uid", uid)!!
+    }
+
+    @RequestMapping("/uxy")
+    fun xieyi(): JsonResult {
+        var page = mobileApiService.findPageById(ConstantUtil.PAGE_USERXIEYI)
+        if (page != null) {
+            return JsonResult.ok("OK").put("page", page.pageContent)!!
+        }
+        return JsonResult.error("页面未配置")
+    }
+
+    /**
+     * 注册或找回时使用的验证码生成接口
+     * */
+    @RequestMapping("/code")
+    fun code(@RequestParam phone: String, @RequestParam type: String?): JsonResult {
+        if (phone.isEmpty() || !MobileNumberCheck.isPhone(phone)) {
+            return JsonResult.error("手机号不合法")
+        }
+        val user = mobileApiService.findUserByPhone(phone)
+        if (type.isNullOrEmpty() || "new" == type) {
+            if (user != null && !user.loginpwd.isEmpty()) {
+                return JsonResult.error("该手机号已注册,请登录或找回密码")
+            }
+        } else {
+            if ("find" == type) {
+                if (user == null) {
+                    return JsonResult.error("手机号不存在,请注册")
+                }
+            } else {
+                return JsonResult.error("类型错误")
+            }
+        }
+        //如果已经发送了
+        val temp = redisTemplate.opsForValue().get(phone.plus("_after"))
+        if (temp.isNullOrEmpty()) {
+            val code = RandomUtils.randomNumber(6)
+            logger.error { code }
+            //TODO 先发送成功,再放入redis
+            redisTemplate.opsForValue().set(phone, code, Duration.ofMinutes(5))
+            val rs = mobileApiService.sendSms(phone, code)
+            if ("0" != rs.retcode) {
+                return JsonResult.error(rs.retmsg)
+            }
+            redisTemplate.opsForValue().set(phone, code, Duration.ofMinutes(5))
+            redisTemplate.opsForValue().set(phone.plus("_after"), code, Duration.ofMinutes(2))
+        }
+        return JsonResult.ok("验证码已发送")
+    }
+
+    /**
+     * 注册或找回时使用的验证码校验接口
+     * */
+    @RequestMapping("/checkcode")
+    fun check(@RequestParam phone: String,
+              @RequestParam code: String,
+              @RequestParam platform: String?,
+              @RequestParam uuid: String?): JsonResult {
+        val temp = redisTemplate.opsForValue().get(phone)
+        if (!temp.isNullOrEmpty()) {
+            if (temp != code) {
+                return JsonResult.error("验证码错误")
+            }
+            var user = mobileApiService.findUserByPhone(phone)
+            if (user == null) {
+                user = TBMobileUser()
+                //登录ID和phone一致
+                user.loginid = phone
+                user.phone = phone
+                user.registerplatform = platform
+                user.registerdate = DateUtil.getNow()
+                user.devuid = uuid
+                user.status = TradeDict.STATUS_NORMAL
+                user.paypwderror = 0
+                user.loginpwderror = 0
+                user = mobileApiService.saveUser(user)
+            }
+            user.status = TradeDict.STATUS_NORMAL
+            user.registerplatform = platform
+            if(!user.registerplatform.isNullOrEmpty()){
+                user.lastloginplatform = user.registerplatform!!.split(",")[1]
+            }
+            user.devuid = uuid
+            user = mobileApiService.saveUser(user)
+            val newCode = RandomUtils.getRandomString(30)
+            redisTemplate.opsForValue().set(user.uid, newCode, Duration.ofHours(1))
+            redisTemplate.delete(phone)
+            return JsonResult.ok("OK").put("uid", user.uid)?.put("randcode", newCode)!!
+        } else {
+            return JsonResult.error(-1, "验证码无效或已过期,请重新获取")
+        }
+    }
+
+    /**
+     * 注册
+     * */
+    @RequestMapping("/register")
+    fun register(@RequestParam id: String,
+                 @RequestParam pwd: String,
+                 @RequestParam repwd: String,
+                 @RequestParam random: String): JsonResult {
+        if (random.isEmpty()) {
+            return JsonResult.error("信息有误,请返回上一步")
+        }
+        if (pwd.isEmpty() || repwd.isEmpty() || pwd.length < 6) {
+            return JsonResult.error("请重新设置密码,密码不能小于6位字符")
+        }
+        if (!pwd.matches(Regex("^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,32}$"))) {
+            return JsonResult.error("密码必须包含数字和字符,长度6~32位")
+        }
+        if (pwd != repwd) {
+            return JsonResult.error("两次密码不一致")
+        }
+        val user: TBMobileUser? = mobileApiService.findUserById(id) ?: return JsonResult.error("用户不存在,请注册")
+        val code = redisTemplate.opsForValue().get(id)
+        if (random != code) {
+            return JsonResult.error("信息有误,请返回上一步,并重新发送验证码")
+        }
+        val encoder = BCryptPasswordEncoder()
+        user!!.loginpwd = encoder.encode(pwd)
+        val exp = systemUtilService.getSysparaValueAsInt(SysparaUtil.MOBILE_LOGIN_EXPIRE_IN_SECONDS, 60 * 60 * 24 * 3)
+        jwtConfig.expiration = exp.toLong()
+        val authorities: Collection<GrantedAuthority> = AuthorityUtils.createAuthorityList("ROLE_USER")
+        user.auths = authorities
+        val token = JwtTokenUtil(jwtConfig).generateToken(
+                mapOf("uid" to user.uid, "issuer" to "payapi",
+                        "audience" to user.loginid,
+                        Constants.JWT_CLAIM_TENANTID to "mobile",
+                        Constants.JWT_CLAIM_AUTHORITIES to user.authorities))
+        val jwt = JwtRedis().apply {
+            jti = token.jti
+            uid = user.loginid
+            status = TradeDict.JWT_STATUS_NORMAL
+            expiration = token.expiration.valueInMillis
+        }.apply {
+            //删除之前的token
+            if (!user.jti.isNullOrEmpty()) {
+                apiJwtRepository.deleteById(user.jti!!)
+            }
+            apiJwtRepository.save(this)
+        }
+        if (user.loginpwderror > 0) {
+            user.loginpwderror = 0
+            user.loginpwderrortime = null
+        }
+        user.lastlogin = DateUtil.getNow()
+        user.jti = jwt.jti
+        val keyMap = RSAKeysGenerate.initKey()
+        val publicKey = RSAKeysGenerate.getPublicKey(keyMap)
+        val privateKey = RSAKeysGenerate.getPrivateKey(keyMap)
+        user.rsaprivate = privateKey
+        user.rsapublic = publicKey
+        user.secertkey = RandomUtils.getSecureRandomHex()
+        mobileApiService.saveUser(user)
+        redisTemplate.delete(user.uid)
+        var payseted = false
+        if (!user.paypwd.isNullOrEmpty()) {
+            payseted = true
+        }
+        var signed = ""
+        if (!user.userid.isNullOrEmpty()) {
+            val card = mobileApiService.findCardByUserid(user.userid!!)
+            if (card != null && card.signed) {
+                signed = TradeDict.STATUS_YES
+            }
+        }
+        return JsonResult.ok("OK").put("token", token.jwtToken)
+                ?.put("userid", if (user.userid.isNullOrEmpty()) "" else user.userid)
+                ?.put("expire", token.expiration.valueInMillis)
+                ?.put("now", System.currentTimeMillis())
+                ?.put("phone", StringUtil.phoneReplace(user.phone))
+                ?.put("paypwdset", payseted)
+                ?.put("signed", signed)
+                ?.put("tenantid", "mobile")!!
+    }
+
+    /**
+     *
+     * 小程序认证接口
+     *
+     * */
+
+
+}
+
+
+@RestController
+@RequestMapping("/mobileapi/v1")
+class ApiV1 {
+    @Autowired
+    lateinit var mobileApiService: MobileApiService
+    @Autowired
+    lateinit var userService: UserService
+    @Autowired
+    lateinit var redisTemplate: RedisTemplate<String, String>
+    @Autowired
+    lateinit var dictionaryProxy: DictionaryProxy
+    @Autowired
+    lateinit var citizencardPayService: CitizencardPayService
+    @Autowired
+    lateinit var apiJwtRepository: ApiJwtRepository
+    @Autowired
+    lateinit var jwtConfig: JwtConfig
+    @Autowired
+    lateinit var qrcodeService:QRCodeService
+    val logger = KotlinLogging.logger { }
+
+    @RequestMapping("/idtypes")
+    fun idtypes(): JsonResult {
+        var dict = dictionaryProxy.getDictionaryAsMap(Dictionary.IDTYPE)
+        return JsonResult.ok("OK").put("idtypes", dict)!!
+    }
+
+    @RequestMapping("/logout")
+    fun logout(@RequestHeader("Authorization") auth: String?): ResponseEntity<Any> {
+        if (auth == null) {
+            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()
+        }
+        val jwt = auth.substring(jwtConfig.tokenHeader.length)
+        val claims = JwtTokenUtil(jwtConfig).verifyToken(jwt)
+        SecurityContextHolder.clearContext()
+        apiJwtRepository.deleteById(claims[ReservedClaimNames.JWT_ID].toString())
+        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()
+    }
+
+    /**
+     * 用户信息
+     * */
+    @RequestMapping("/infor")
+    fun getUserInfor(): JsonResult {
+        val p = SecurityContextHolder.getContext().authentication
+        var user = mobileApiService.findUserById(p.name)
+                ?: return JsonResult.error("用户不存在,请注册")
+        var tk= ""
+        if (!user.jti.isNullOrEmpty()) {
+            var opt = apiJwtRepository.findById(user.jti!!)
+            if(opt.isPresent){
+                var jwt =  opt.get()
+                val cur = System.currentTimeMillis()
+                //token 小于12个小时,则更新它
+                if(jwt.expiration-cur<1000*60*60*12){
+                    val token = JwtTokenUtil(jwtConfig).generateToken(
+                            mapOf("uid" to user.uid, "issuer" to "payapi",
+                                    "audience" to user.loginid,
+                                    Constants.JWT_CLAIM_TENANTID to "mobile",
+                                    Constants.JWT_CLAIM_AUTHORITIES to p.authorities))
+                    jwt = JwtRedis().apply {
+                        jti = token.jti
+                        uid = user.loginid
+                        status = TradeDict.JWT_STATUS_NORMAL
+                        expiration = token.expiration.valueInMillis
+                    }.apply {
+                        //删除之前的token
+                        if (!user.jti.isNullOrEmpty()) {
+                            apiJwtRepository.deleteById(user.jti!!)
+                        }
+                        apiJwtRepository.save(this)
+                    }
+                    user.jti = jwt.jti
+                    mobileApiService.saveUser(user)
+                    tk = token.jwtToken
+                }
+            }
+        }
+        return JsonResult.ok("OK").put("now", System.currentTimeMillis())
+                ?.put("token", tk)!!
+    }
+    /**
+     * 验证码生成,内部校验
+     * */
+    @RequestMapping("/code")
+    fun code(): JsonResult {
+        val p = SecurityContextHolder.getContext().authentication
+        val user = mobileApiService.findUserById(p.name) ?: return JsonResult.error("用户不存在,请注册")
+        if (user.phone.isNullOrEmpty()) {
+            return JsonResult.error("用户不存在,请注册")
+        }
+        val temp = redisTemplate.opsForValue().get(user.phone!!)
+        if (temp.isNullOrEmpty()) {
+            val code = RandomUtils.randomNumber(6)
+            logger.error { code }
+            //TODO delete it in product
+            redisTemplate.opsForValue().set(user.loginid, code, Duration.ofMinutes(5))
+            val rs = mobileApiService.sendSms(user.phone!!, code)
+            if ("0" != rs.retcode) {
+                return JsonResult.error(rs.retmsg)
+            }
+            redisTemplate.opsForValue().set(user.loginid, code, Duration.ofMinutes(5))
+        }
+        return JsonResult.ok("验证码已发送")
+    }
+
+    @RequestMapping("/checkcode")
+    fun check(@RequestParam code: String, @RequestParam personid: String?
+    ): JsonResult {
+        val p = SecurityContextHolder.getContext().authentication
+        val user = mobileApiService.findUserById(p.name)
+                ?: return JsonResult.error("用户不存在,请注册")
+        if (user.phone.isNullOrEmpty()) {
+            return JsonResult.error("手机号不存在,请注册")
+        }
+        val temp = redisTemplate.opsForValue().get(user.phone!!)
+        if (!temp.isNullOrEmpty()) {
+            if (temp != code) {
+                return JsonResult.error("验证码错误")
+            }
+            val newCode = RandomUtils.getRandomString(30)
+            redisTemplate.opsForValue().set(user.uid, newCode, Duration.ofHours(1))
+            redisTemplate.delete(user.phone!!)
+            if (!personid.isNullOrEmpty()) {
+                //绑定用户
+                val person = userService.findOnePersonByUserid(personid)
+                user.bindtime = DateUtil.getNow()
+                user.userid = person.userid
+                mobileApiService.saveUser(user)
+            }
+            return JsonResult.ok("OK").put("randcode", newCode)!!
+        } else {
+            return JsonResult.error(-1, "验证码无效或已过期,请重新获取")
+        }
+    }
+
+    /**
+     * 绑卡
+     * */
+    @RequestMapping("/bindcard")
+    fun bindcard(cardno: String, name: String, idtype: String, idno: String): JsonResult {
+        val p = SecurityContextHolder.getContext().authentication
+        val user = mobileApiService.findUserById(p.name)
+                ?: return JsonResult.error("用户不存在,请注册")
+        if (user.phone.isNullOrEmpty()) {
+            return JsonResult.error("手机号不存在,请注册")
+        }
+        val phone = user.phone!!
+        val card = mobileApiService.findCardByNo(cardno)
+                ?: return JsonResult.error("银行卡号有误")
+        if (card.userid.isNullOrEmpty() || card.status != TradeDict.STATUS_NORMAL) {
+            return JsonResult.error("银行卡号信息有误")
+        }
+        val person = userService.findOnePersonByUserid(card.userid)
+        if (person.name != name) {
+            return JsonResult.error("绑定信息有误[姓名]")
+        }
+        if (person.idtype != idtype || person.idno != idno) {
+            return JsonResult.error("绑定信息有误[证件类型/证件号]")
+        }
+        val exsitUser = mobileApiService.findUserById(card.userid)
+        if (exsitUser != null) {
+            return JsonResult.error("该银行卡号已被绑定,若您本人绑定,请先解除绑定,若非本人,请联系客服")
+        }
+        var signed = ""
+        //call api
+        var resp = citizencardPayService.bindCard(cardno, name, idtype, idno, phone)
+        if (resp.code != "0000") {
+            return JsonResult.error(resp.message)
+        }
+        var needupdate = false
+        if (resp.sinstatus == YnrccUtil.TRANSTYPE_SIGNCARD ) {
+            signed = TradeDict.STATUS_YES
+            if(!card.signed){
+                card.signed = true
+                mobileApiService.saveCard(card)
+            }
+            user.signedtime = DateUtil.getNow();
+            mobileApiService.saveUser(user)
+            needupdate = true;
+
+        }
+        if( user.userid.isNullOrEmpty()){
+            user.userid = person.userid
+            user.bindtime = DateUtil.getNow()
+            needupdate=true
+        }
+        if(needupdate){
+            mobileApiService.saveUser(user)
+        }
+        var payseted = false
+        if (!user.paypwd.isNullOrEmpty()) {
+            payseted = true
+        }
+
+        return JsonResult.ok("OK").put("personid", card.userid)
+                ?.put("paypwdset", payseted)
+                ?.put("phonex", StringUtil.phoneReplace(phone))
+                ?.put("signed", signed)!!
+
+
+    }
+    /**
+     * 绑卡
+     * */
+    @RequestMapping("/bindcardcode")
+    fun bindcardcode(): JsonResult {
+        val p = SecurityContextHolder.getContext().authentication
+        val user = mobileApiService.findUserById(p.name)
+                ?: return JsonResult.error("用户不存在,请注册")
+        if (user.phone.isNullOrEmpty()) {
+            return JsonResult.error("手机号不存在,请注册")
+        }
+        var card = mobileApiService.findCardByUserid(user.userid!!)
+                ?: return JsonResult.error("卡片不存在,请重新绑定")
+        //call sign api
+        val person = userService.findOnePersonByUserid(card.userid)
+        var signed=""
+        //call api
+        var resp = citizencardPayService.bindCard(card.cardno, person.name, person.idtype, person.idno, user.phone!!)
+        if (resp.code != "0000") {
+            return JsonResult.error(resp.message)
+        }
+
+        if (resp.sinstatus == YnrccUtil.TRANSTYPE_SIGNCARD ) {
+            signed = TradeDict.STATUS_YES
+            if(!card.signed){
+                card.signed = true
+                mobileApiService.saveCard(card)
+            }
+            user.signedtime = DateUtil.getNow();
+            mobileApiService.saveUser(user)
+        }
+
+        return JsonResult.ok("OK")
+                ?.put("signed", signed)!!
+
+    }
+
+    /**
+     * 支付密码
+     * */
+    @RequestMapping("/paypwd")
+    fun paypwd(pwd: String, repwd: String, oldpwd: String?, type: String, randcode: String?): JsonResult {
+        val p = SecurityContextHolder.getContext().authentication
+        val user = mobileApiService.findUserById(p.name)
+                ?: return JsonResult.error("用户不存在,请注册")
+        if (pwd != repwd) {
+            return JsonResult.error("两次密码不一致,请确认")
+        }
+        if (pwd.length != 6) {
+            return JsonResult.error("支付密码为6位数字")
+        }
+        if (!StringUtils.isNumeric(pwd)) {
+            return JsonResult.error("支付密码为6位数字")
+        }
+        val encoder = BCryptPasswordEncoder()
+        if (user.paypwd.isNullOrEmpty()) {
+            user.paypwd = encoder.encode(pwd)
+            mobileApiService.saveUser(user)
+            return JsonResult.ok("OK")
+                    ?.put("paypwdset", true)!!
+        } else {
+            when (type) {
+                "new" -> return JsonResult.error("支付密码已设置")
+                "renew" -> {
+                    if (oldpwd.isNullOrEmpty()) {
+                        return JsonResult.error("原支付密码错误")
+                    }
+                    val paypwdtimes = user.checkPaypwdtime()
+                    if (paypwdtimes == -1) {
+                        return JsonResult.error("密码错误次数过多,请30分钟后再试")
+                    } else if (paypwdtimes == 1) {
+                        mobileApiService.saveUser(user)
+                    }
+                    if (!encoder.matches(oldpwd, user.paypwd)) {
+                        user.updatePaypwderror(false).also {
+                            if (it) mobileApiService.saveUser(user)
+                        }
+                        return JsonResult.error("原支付密码错误")
+                    } else {
+                        user.updatePaypwderror(true).also {
+                            if (it) mobileApiService.saveUser(user)
+                        }
+                    }
+                    user.paypwd = encoder.encode(pwd)
+                    mobileApiService.saveUser(user)
+                    return JsonResult.ok("OK")
+                            ?.put("paypwdset", true)!!
+                }
+                "find" -> {
+                    if (randcode.isNullOrEmpty()) {
+                        return JsonResult.error("信息有误,请返回并重新设置")
+                    }
+                    val code = redisTemplate.opsForValue().get(user.uid)
+                    if (randcode != code) {
+                        return JsonResult.error(-1, "长时间未操作,请返回上一步,并重新发送验证码")
+                    }
+                    user.paypwd = encoder.encode(pwd)
+                    mobileApiService.saveUser(user)
+                    redisTemplate.delete(user.uid)
+                    return JsonResult.ok("OK").put("paypwdset", true)!!
+                }
+                else -> return JsonResult.error("请求错误")
+            }
+        }
+    }
+
+    /**
+     *
+     * 银行协议
+     * */
+    @RequestMapping("/bxy")
+    fun xieyi(): JsonResult {
+        val page = mobileApiService.findPageById(ConstantUtil.PAGE_BANKXIEYI)
+        if (page != null) {
+            val p = SecurityContextHolder.getContext().authentication
+            val user = mobileApiService.findUserById(p.name)
+                    ?: return JsonResult.error("用户不存在,请注册")
+            var signed = ""
+            if (!user.userid.isNullOrEmpty()) {
+                var card = mobileApiService.findCardByUserid(user.userid!!)
+                        ?: return JsonResult.error(-1, "卡片不存在,请重新绑定")
+                if (card.signed) {
+                    signed = TradeDict.STATUS_YES
+                }
+            } else {
+                return JsonResult.error(-1, "请先绑定银行卡")
+            }
+            return JsonResult.ok("OK").put("page", page.pageContent)?.put("signed", signed)!!
+        }
+        return JsonResult.error("页面未配置")
+    }
+
+    /**
+     *
+     * 签约银行协议
+     * */
+    @RequestMapping("/signbxy")
+    fun signbxy(agree: String): JsonResult {
+        val p = SecurityContextHolder.getContext().authentication
+        val user = mobileApiService.findUserById(p.name)
+                ?: return JsonResult.error("手机号不存在,请注册")
+        if (user.phone.isNullOrEmpty()) {
+            return JsonResult.error("用户不存在,请注册")
+        }
+        var signed: String
+        if (!user.userid.isNullOrEmpty()) {
+            var card = mobileApiService.findCardByUserid(user.userid!!)
+                    ?: return JsonResult.error("卡片不存在,请重新绑定")
+            //call sign api
+            val person = userService.findOnePersonByUserid(card.userid)
+            val captcha = agree//此处为验证码,暂由此参数代替
+            var resp = citizencardPayService.signCard(card.cardno, person.name, person.idtype, person.idno, user.phone!!, YnrccUtil.TRANSTYPE_SIGNCARD,captcha)
+            if (resp.code != "0000") {
+                return JsonResult.error(resp.message)
+            }
+            card.signed = true
+            user.signedtime = DateUtil.getNow()
+            mobileApiService.saveCard(card)
+            mobileApiService.saveUser(user)
+            signed = TradeDict.STATUS_YES
+        } else {
+            return JsonResult.error("请先绑定银行卡")
+        }
+        return JsonResult.ok("ok")
+                .put("signed", signed)!!
+    }
+
+    /**
+     * 查询账单
+     * */
+    @RequestMapping("/bills")
+    fun bills(pageno: Int,platform: String?,type:String?): JsonResult {
+        val p = SecurityContextHolder.getContext().authentication
+        val user = mobileApiService.findUserById(p.name)
+                ?: return JsonResult.error("用户不存在,请注册")
+        val c = Calendar.getInstance()
+        val timeOfDay = c.get(Calendar.HOUR_OF_DAY)
+        var t = ""
+        when (timeOfDay) {
+            in 0..7 -> t = "早上好"
+            in 8..12 -> t = "上午好"
+            in 13..17 -> t = "下午好"
+            in 18..23 -> t = "晚上好"
+        }
+        if (user.userid.isNullOrEmpty()) {
+            return JsonResult.ok("OK").put("t", t)!!
+        }
+        val no = if (pageno <= 0) 1 else pageno
+        val today = DateUtil.getNow("yyyyMMdd")
+        val yester = DateUtil.getNowInterDay(-1)
+        val acc = userService.findAccountByUserid(user.userid!!, null)
+        val point = userService.findPointsAccountByUserid(user.userid!!)
+        var amount = 0.0;
+        var pi = 0L
+        if (acc != null && acc.availbal != null) {
+            amount = acc.availbal
+        }
+        if (point != null && point.points != null) {
+            pi = point.points
+        }
+        var payseted = false
+        if (!user.paypwd.isNullOrEmpty()) {
+            payseted = true
+        }
+        val person = userService.findOnePersonByUserid(user.userid!!)
+        val card = mobileApiService.findCardByUserid(user.userid!!)
+        var needrebind = false
+        var signed = ""
+        if (card == null) {
+            //卡片已补办,或绑定失败,需要重新绑定
+            needrebind = true
+        } else {
+            if (card.signed) {
+                signed = TradeDict.STATUS_YES
+            }
+        }
+        var version:String?=""
+        var minversion:String?=""
+        var versionmsg:String?=""
+        var versionurl:String?=""
+        if(!platform.isNullOrEmpty()){
+            var map= mutableMapOf<String,Any>()
+            var key=""
+            if(platform.toLowerCase().contains("android")){
+                key="androidapp"
+                if(!type.isNullOrEmpty()){
+                    key+= "_$type"
+                }
+                dictionaryProxy.refreshDictionary(key)
+                map = dictionaryProxy.getDictionaryAsMap(key)
+                if(map["androidversion"]!=null){
+                    version = map["androidversion"] as String
+                }
+                if(map["androidminversion"]!=null){
+                    minversion = map["androidminversion"] as String
+                }
+                if(map["androidversionmsg"]!=null){
+                    versionmsg = map["androidversionmsg"] as String
+                }
+                if(map["androidversionurl"]!=null){
+                    versionurl = map["androidversionurl"] as String
+                }
+
+            }else if(platform.toLowerCase().contains("iphone")){
+                key="iosapp"
+                if(!type.isNullOrEmpty()){
+                    key+= "_$type"
+                }
+                dictionaryProxy.refreshDictionary(key)
+                map = dictionaryProxy.getDictionaryAsMap(key)
+                if(map["iosversion"]!=null){
+                    version = map["iosversion"] as String
+                }
+                if(map["iosminversion"]!=null){
+                    minversion = map["iosminversion"] as String
+                }
+                if(map["iosversionmsg"]!=null){
+                    versionmsg = map["iosversionmsg"] as String
+                }
+                if(map["iosversionurl"]!=null){
+                    versionurl = map["iosversionurl"] as String
+                }
+            }
+
+        }
+
+        var name = person.name
+        val page = userService.findPersondtlByUserid(user.userid!!, no)
+        return JsonResult.ok("OK").put("page", page)
+                ?.put("today", today)
+                ?.put("yesterday", yester)
+                ?.put("point", pi)
+                ?.put("amount", amount)
+                ?.put("paypwdset", payseted)
+                ?.put("name", name)
+                ?.put("needrebind", needrebind)
+                ?.put("signed", signed)
+                ?.put("version",version)
+                ?.put("minversion",minversion)
+                ?.put("versionmsg",versionmsg)
+                ?.put("versionurl",versionurl)
+                ?.put("userid", if (user.userid.isNullOrEmpty()) "" else user.userid)!!.put("t", t)!!
+    }
+
+    /**
+     * 账单明细
+     * */
+    @RequestMapping("/billdetail")
+    fun billdetail(billid: String): JsonResult {
+        val p = SecurityContextHolder.getContext().authentication
+        val user: TBMobileUser? = mobileApiService.findUserById(p.name)
+                ?: return JsonResult.error("用户不存在,请注册")
+        if (user!!.userid.isNullOrEmpty()) {
+            return JsonResult.ok("OK")
+        }
+        val dtl = userService.findPersondtlDetailByUserid(user.userid!!, billid)
+        return JsonResult.ok("OK").put("dtl", dtl)!!
+    }
+
+    /**
+     * 密码修改
+     * */
+    @RequestMapping("/pwdset")
+    fun pwdset(oldpwd: String, newpwd: String, renewpwd: String): JsonResult {
+        val p = SecurityContextHolder.getContext().authentication
+        val user = mobileApiService.findUserById(p.name)
+                ?: return JsonResult.error("用户不存在,请注册")
+        if (newpwd != renewpwd) {
+            return JsonResult.error("两次密码不一致,请确认")
+        }
+        if (!newpwd.matches(Regex("^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,32}$"))) {
+            return JsonResult.error("密码必须包含数字和字符,长度6~32位")
+        }
+        if (newpwd.isEmpty() || newpwd.length < 6) {
+            return JsonResult.error("请重新设置密码,密码不能小于6位字符")
+        }
+        val pwdtimes = user.checkLoginpwdtime()
+        if (pwdtimes == -1) {
+            if (!user.jti.isNullOrEmpty()) {
+                apiJwtRepository.deleteById(user.jti!!)
+            }
+            return JsonResult.error(-1, "原密码错误次数过多,将退出系统,请重新登录系统或点击忘记密码功能找回密码")
+        } else if (pwdtimes == 1) {
+            mobileApiService.saveUser(user)
+        }
+        val encoder = BCryptPasswordEncoder()
+        if (!encoder.matches(oldpwd, user.loginpwd)) {
+            user.updateLoginpwderror(false).also {
+                if (it) mobileApiService.saveUser(user)
+            }
+            return JsonResult.error("原密码错误")
+        } else {
+            user.updateLoginpwderror(true)
+        }
+        user.loginpwd = encoder.encode(newpwd)
+        mobileApiService.saveUser(user)
+        return JsonResult.ok("密码修改成功")
+    }
+
+    /**
+     *
+     * 市民卡挂失
+     * */
+    @RequestMapping("/cardinfor")
+    fun cardinfor(): JsonResult {
+        val p = SecurityContextHolder.getContext().authentication
+        val user = mobileApiService.findUserById(p.name)
+                ?: return JsonResult.error("用户不存在,请注册")
+        if (user.userid.isNullOrEmpty()) {
+            return JsonResult.error(-1, "银行卡未绑定,请先绑定")
+        }
+        val person = userService.findOnePersonByUserid(user.userid!!)
+        val card = mobileApiService.findCardByUserid(user.userid!!)
+                ?: return JsonResult.error(-1, "银行卡未绑定,请先绑定")
+        var status = ""
+        //normal/loss/frozen/locked
+        when (card.transStatus) {
+            TradeDict.STATUS_NORMAL -> status = "正常"
+            TradeDict.STATUS_LOST -> status = "已挂失"
+            TradeDict.STATUS_LOCKED -> status = "已锁定"
+            TradeDict.STATUS_FROZEN -> status = "已冻结"
+        }
+        return JsonResult.ok("ok").put("name", person.name)
+                ?.put("cardno", card.cardno)
+                ?.put("cardstatus", status)!!
+    }
+
+    /**
+     *
+     * 市民卡挂失
+     * */
+    @RequestMapping("/cardlost")
+    fun cardlost(paypwd: String): JsonResult {
+        val p = SecurityContextHolder.getContext().authentication
+        val user = mobileApiService.findUserById(p.name)
+                ?: return JsonResult.error("用户不存在,请注册")
+        val paypwdtimes = user.checkPaypwdtime()
+        if (paypwdtimes == -1) {
+            return JsonResult.error("支付密码错误次数过多,请30分钟后再试")
+        } else if (paypwdtimes == 1) {
+            mobileApiService.saveUser(user)
+        }
+        val encoder = BCryptPasswordEncoder()
+        if (!encoder.matches(paypwd, user.paypwd)) {
+            user.updatePaypwderror(false).also {
+                if (it) mobileApiService.saveUser(user)
+            }
+            return JsonResult.error("支付密码错误")
+        } else {
+            user.updatePaypwderror(true).also {
+                if (it) mobileApiService.saveUser(user)
+            }
+        }
+        var card = mobileApiService.findCardByUserid(user.userid!!)
+                ?: return JsonResult.error(-1, "银行卡未绑定,请先绑定")
+        if (card.transStatus != TradeDict.STATUS_NORMAL) {
+            return JsonResult.error("卡状态非正常,不能挂失")
+        }
+        card.transStatus = TradeDict.STATUS_LOST
+        mobileApiService.saveCard(card)
+        return JsonResult.ok("ok")
+    }
+
+    /**
+     *
+     * 二维码在线生成
+     * */
+    @RequestMapping("/qrcode")
+    fun qrcode(): JsonResult {
+        val p = SecurityContextHolder.getContext().authentication
+        val user = mobileApiService.findUserById(p.name)
+                ?: return JsonResult.error("用户不存在,请注册")
+        val resp = qrcodeService.encodeCode(user.uid)
+        return if(resp.retcode==0){
+            JsonResult.ok("ok").put("qrcode", resp.retmsg)!!
+        }else{
+            JsonResult.error(resp.retmsg)
+        }
+    }
+
+    /**
+     *
+     * 解除银行协议
+     * */
+    @RequestMapping("/unsignbxy")
+    fun unSignbxy(): JsonResult {
+        val p = SecurityContextHolder.getContext().authentication
+        val user = mobileApiService.findUserById(p.name)
+                ?: return JsonResult.error("用户不存在,请注册")
+        var signed: String
+        if (!user.userid.isNullOrEmpty()) {
+            var card = mobileApiService.findCardByUserid(user.userid!!)
+                    ?: return JsonResult.error("银行卡不存在,不能解除代扣协议")
+            //call sign api
+            val person = userService.findOnePersonByUserid(card.userid)
+            val captcha = ""//此处为验证码,暂由此参数代替
+            var resp = citizencardPayService.signCard(card.cardno, person.name, person.idtype, person.idno, user.phone!!, YnrccUtil.TRANSTYPE_UNSIGNCARD,captcha)
+            if (resp.code != "0000") {
+                return JsonResult.error(resp.message)
+            }
+            card.signed = false
+            mobileApiService.saveCard(card)
+            signed = TradeDict.STATUS_NO
+        } else {
+            return JsonResult.error("未绑定银行卡,不能解除代扣协议")
+        }
+        return JsonResult.ok("ok")
+                .put("signed", signed)!!
+    }
+
+    /**
+     * 解除绑定
+     * */
+    @RequestMapping("/unbindcard")
+    fun unbindcard(paypwd: String): JsonResult {
+        val p = SecurityContextHolder.getContext().authentication
+        val user = mobileApiService.findUserById(p.name)
+                ?: return JsonResult.error("用户不存在,请注册")
+        if(user.paypwd.isNullOrEmpty()){
+            return JsonResult.error("支付密码未设置,请先设置")
+        }
+        val paypwdtimes = user.checkPaypwdtime()
+        if (paypwdtimes == -1) {
+            return JsonResult.error("支付密码错误次数过多,请30分钟后再试")
+        } else if (paypwdtimes == 1) {
+            mobileApiService.saveUser(user)
+        }
+        val encoder = BCryptPasswordEncoder()
+        if (!encoder.matches(paypwd, user.paypwd)) {
+            user.updatePaypwderror(false).also {
+                if (it) mobileApiService.saveUser(user)
+            }
+            return JsonResult.error("支付密码错误")
+        } else {
+            user.updatePaypwderror(true).also {
+                if (it) mobileApiService.saveUser(user)
+            }
+        }
+        var card = mobileApiService.findCardByUserid(user.userid!!)
+                ?: return JsonResult.error(-1, "银行卡未绑定,无需解绑")
+        card.signed = false
+        user.userid = null
+        mobileApiService.saveCard(card)
+        mobileApiService.saveUser(user)
+        return JsonResult.ok("OK")
+                ?.put("signed", TradeDict.STATUS_NO)!!
+    }
+
+    /**
+     * 上传头像
+     * */
+    @RequestMapping("/uploadphoto")
+    fun uploadPhoto(file: MultipartFile): JsonResult {
+        val p = SecurityContextHolder.getContext().authentication
+        val user = mobileApiService.findUserById(p.name)
+                ?: return JsonResult.error("用户不存在,请注册")
+        /**
+         * TODO
+         * 1.已有头像更新
+         * 2.没有头像上传
+         * 3.返回图片ID
+         *
+        */
+        return JsonResult.ok("OK")
+    }
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/dao/ApiDao.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/dao/ApiDao.kt
new file mode 100644
index 0000000..27f63ad
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/dao/ApiDao.kt
@@ -0,0 +1,12 @@
+package com.supwisdom.dlpay.mobile.dao
+
+import com.supwisdom.dlpay.mobile.domain.TBMsg
+import com.supwisdom.dlpay.mobile.domain.TBPages
+import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface PagesDao : JpaRepository<TBPages, String>
+
+@Repository
+interface MsgDao : JpaRepository<TBMsg, String>
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/dao/MobileUserDao.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/dao/MobileUserDao.kt
new file mode 100644
index 0000000..c7d8a0e
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/dao/MobileUserDao.kt
@@ -0,0 +1,16 @@
+package com.supwisdom.dlpay.mobile.dao
+
+import com.supwisdom.dlpay.mobile.domain.TBMobileUser
+import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface MobileUserDao : JpaRepository<TBMobileUser, String> {
+    fun findByLoginid(loginid: String): TBMobileUser?
+
+    fun findByPhone(phone: String): TBMobileUser?
+
+    fun findByUseridAndStatus(userid:String,status:String):List<TBMobileUser>?
+}
+
+
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/domain/TBMobileUser.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/domain/TBMobileUser.kt
new file mode 100644
index 0000000..339e607
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/domain/TBMobileUser.kt
@@ -0,0 +1,240 @@
+package com.supwisdom.dlpay.mobile.domain
+
+import com.supwisdom.dlpay.api.util.DateUtil
+import com.supwisdom.dlpay.framework.util.TradeDict
+import org.hibernate.annotations.GenericGenerator
+import org.springframework.security.core.GrantedAuthority
+import org.springframework.security.core.userdetails.UserDetails
+import javax.persistence.*
+import javax.validation.constraints.NotNull
+
+@Entity
+@Table(name = "TB_MOBILE_USER",indexes = [Index(name = "mobile_user_loginid_idx", columnList = "loginid", unique = true)])
+class TBMobileUser : UserDetails {
+    override fun getAuthorities(): Collection<GrantedAuthority>? {
+        return this.auths
+    }
+
+    override fun isEnabled(): Boolean {
+        return TradeDict.STATUS_CLOSED != this.status
+    }
+
+    override fun getUsername(): String {
+        return this.loginid
+    }
+
+    override fun isCredentialsNonExpired(): Boolean {
+        return true
+    }
+
+    override fun getPassword(): String {
+        return this.loginpwd
+    }
+
+    override fun isAccountNonExpired(): Boolean {
+        if(expiredate.isNullOrEmpty()){
+            return true
+        }
+        return this.expiredate!! >= DateUtil.getNow("yyyyMMdd")
+    }
+
+    override fun isAccountNonLocked(): Boolean {
+        return TradeDict.STATUS_LOCKED != this.status
+    }
+
+    @Transient
+    var auths: Collection<GrantedAuthority>? = null
+
+    @Id
+    @GenericGenerator(name = "idGenerator", strategy = "uuid")
+    @GeneratedValue(generator = "idGenerator")
+    @Column(name = "uid", nullable = false, length = 32)
+    var uid: String = ""
+    /**
+     * 登录id
+     * */
+
+    @Column(name = "loginid", length = 64)
+    @NotNull
+    var loginid: String = ""
+
+    /**
+     * 登录密码
+     * */
+    @Column(name = "loginpwd", length = 64)
+    var loginpwd: String = ""
+
+    /**
+     * 设备uuid
+     * */
+    @Column(name = "devuid", length = 64)
+    var devuid: String? = null
+
+    /**
+     * 注册时间
+     * */
+    @Column(name = "registerdate", length = 16)
+    var registerdate: String? = null
+
+    /**
+     * 关联tb_person
+     * */
+    @Column(name = "userid", length = 32)
+    var userid: String? = null
+
+    /**
+     * 银行卡绑定时间
+     * */
+    @Column(name = "bindtime", length = 14)
+    var bindtime: String? = null
+
+    /**
+     * 注册手机类型
+     * */
+    @Column(name = "registerplatform", length = 100)
+    var registerplatform: String? = null
+
+    /**
+     * 最后登录时间
+     * */
+    @Column(name = "lastlogin", length = 16)
+    var lastlogin: String? = null
+
+    /**
+     * 最后登录手机类型
+     * */
+    @Column(name = "lastloginplatform", length = 100)
+    var lastloginplatform: String? = null
+
+    /**
+     * 状态
+     * */
+    @Column(name = "status", length = 16)
+    var status: String? = null
+
+    /**
+     * 支付密码
+     * */
+    @Column(name = "paypwd", length = 64)
+    var paypwd: String? = null
+
+    /**
+     * 登录密码错误次数
+     * */
+    @Column(name = "loginpwderror", length = 4)
+    var loginpwderror: Int = 0
+
+    /**
+     * 登录密码错误时间
+     * */
+    @Column(name = "loginpwderrortime", length = 16)
+    var loginpwderrortime: Long? = 0
+
+    /**
+     * 支付密码错误次数
+     * */
+    @Column(name = "paypwderror", length = 4)
+    var paypwderror: Int = 0
+
+    /**
+     * 支付密码错误时间
+     * */
+    @Column(name = "paypwderrortime", length = 16)
+    var paypwderrortime: Long? = 0
+
+    /**
+     * jti
+     * */
+    @Column(name = "jti", length = 64)
+    var jti: String? = null
+
+    /**
+     * 签约时间
+     * */
+    @Column(name = "signedtime", length = 20)
+    var signedtime: String? = null
+
+    /**
+     * 头像
+     * */
+    @Column(name = "ulogo", length = 100)
+    var ulogo: String? = null
+    @Column(name = "rsaprivate", length = 1000)
+    var rsaprivate: String? = null
+    @Column(name = "rsapublic", length = 1000)
+    var rsapublic: String? = null
+    @Column(name = "secertkey", length = 64)
+    var secertkey: String? = null
+
+    @Column(name = "tenantid", length = 32)
+    var tenantid:String? = null
+
+    @Column(name = "expiredate", length = 8)
+    var expiredate: String? = null
+
+    @Column(name = "email", length = 100)
+    var email: String? = null
+
+    @Column(name = "phone", length = 15)
+    var phone: String? = null
+
+    fun checkLoginpwdtime():Int{
+        if (this.loginpwderror >= 5 && (System.currentTimeMillis() - this.loginpwderrortime!!) < 1000 * 60 * 30) {
+            return -1
+        } else if (this.loginpwderror >= 5 && (System.currentTimeMillis() - this.loginpwderrortime!!) > 1000 * 60 * 30) {
+            //更新时间
+            this.loginpwderror = 0
+            this.loginpwderrortime = null
+            return 1
+        }
+        return 0
+    }
+
+    fun updateLoginpwderror(ok: Boolean): Boolean {
+        return if (ok) {
+            if (this.loginpwderror > 0) {
+                this.loginpwderror = 0
+                this.loginpwderrortime = null
+                true
+            } else {
+                false
+            }
+        } else {
+            if (this.loginpwderror == 0) {
+                this.loginpwderrortime = System.currentTimeMillis()
+            }
+            this.loginpwderror += 1
+            true
+        }
+    }
+
+    fun checkPaypwdtime():Int{
+        if (this.paypwderror >= 5 && (System.currentTimeMillis() - this.paypwderrortime!!) < 1000 * 60 * 30) {
+            return -1
+        } else if (this.paypwderror >= 5 && (System.currentTimeMillis() - this.paypwderrortime!!) > 1000 * 60 * 30) {
+            //更新时间
+            this.paypwderror = 0
+            this.paypwderrortime = null
+            return 1
+        }
+        return 0
+    }
+
+    fun updatePaypwderror(ok: Boolean): Boolean {
+        return if (ok) {
+            if (this.paypwderror > 0) {
+                this.paypwderror = 0
+                this.paypwderrortime = null
+                true
+            } else {
+                false
+            }
+        } else {
+            if (this.paypwderror == 0) {
+                this.paypwderrortime = System.currentTimeMillis()
+            }
+            this.paypwderror += 1
+            true
+        }
+    }
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/domain/TBMsg.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/domain/TBMsg.kt
new file mode 100644
index 0000000..53ec2f0
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/domain/TBMsg.kt
@@ -0,0 +1,41 @@
+package com.supwisdom.dlpay.mobile.domain
+
+import org.hibernate.annotations.GenericGenerator
+import javax.persistence.*
+
+@Entity
+@Table(name = "TB_MSG")
+class TBMsg{
+    @Id
+    @GenericGenerator(name = "idGenerator", strategy = "uuid")
+    @GeneratedValue(generator = "idGenerator")
+    @Column(name = "msgid", nullable = false, length = 32)
+    var msgid: String = ""
+
+    @Column(name = "userid", length = 32)
+    var userid:String = ""
+
+    @Column(name = "title", length = 100)
+    var title: String? = null
+
+    @Column(name = "refno", length = 32)
+    var refno: String? = null
+
+    @Column(name = "content", length = 400)
+    var content: String? = null
+
+    @Column(name = "lastupdate", length = 14)
+    var lastupdate: String? = null
+
+    @Column(name = "extras", length = 1000)
+    var extras: String? = null
+
+    @Column(name = "pushresult", length = 500)
+    var pushresult: String? = null
+
+    @Column(name = "pusheduids", length = 400)
+    var pusheduids:String? = ""
+
+    @Column(name = "tenantid", length = 32)
+    var tenantid:String? = null
+}
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/domain/TBPages.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/domain/TBPages.kt
new file mode 100644
index 0000000..6c9b74a
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/domain/TBPages.kt
@@ -0,0 +1,26 @@
+package com.supwisdom.dlpay.mobile.domain
+
+import javax.persistence.Column
+import javax.persistence.Entity
+import javax.persistence.Id
+import javax.persistence.Table
+
+@Entity
+@Table(name = "TB_PAGES")
+class TBPages{
+    @Id
+    @Column(name = "pageid", nullable = false, length = 32)
+    var pageid: String = ""
+
+    @Column(name = "pagecontent",columnDefinition = "TEXT")
+    var pageContent:String = ""
+
+    @Column(name = "pagedes", length = 200)
+    var pagedes: String? = null
+
+    @Column(name = "lastupdate", length = 14)
+    var lastupdate: String? = null
+
+    @Column(name = "tenantid", length = 32)
+    var tenantid:String? = null
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/exception/UserLoginFailException.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/exception/UserLoginFailException.kt
new file mode 100644
index 0000000..67d6243
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/exception/UserLoginFailException.kt
@@ -0,0 +1,7 @@
+package com.supwisdom.dlpay.mobile.exception
+
+import org.springframework.security.core.AuthenticationException
+
+class UserLoginFailException(msg: String) : AuthenticationException(msg) {
+    private val serialVersionUID = 1170189980006964105L
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/service/MobileApiService.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/service/MobileApiService.kt
new file mode 100644
index 0000000..55d717c
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/service/MobileApiService.kt
@@ -0,0 +1,30 @@
+package com.supwisdom.dlpay.mobile.service
+
+import com.supwisdom.dlpay.api.bean.BaseResp
+import com.supwisdom.dlpay.api.domain.TCard
+import com.supwisdom.dlpay.mobile.domain.TBMobileUser
+import com.supwisdom.dlpay.mobile.domain.TBPages
+
+interface MobileApiService {
+    fun saveUser(user: TBMobileUser): TBMobileUser
+
+    fun findPageById(pageid: String): TBPages?
+
+    fun findUserByPhone(phone: String): TBMobileUser?
+
+    fun findUserById(id: String): TBMobileUser?
+
+    fun findCardByNo(cardno: String):TCard?
+
+    fun findCardByUserid(userid :String) :TCard?
+
+    fun findCityCardByUserid(userid :String) :TCard?
+
+    fun saveCard(card:TCard):TCard
+
+    fun sendSms(phone:String,code:String):BaseResp
+
+    fun findByUseridAndStatus(userid:String,status:String):List<TBMobileUser>?
+
+    fun findCardByUseridAndCardphyid(userid: String, cardtype: String, cardphyid: String?): TCard?
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/service/MobileUserService.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/service/MobileUserService.kt
new file mode 100644
index 0000000..f8b5d8d
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/service/MobileUserService.kt
@@ -0,0 +1,9 @@
+package com.supwisdom.dlpay.mobile.service
+
+import com.supwisdom.dlpay.mobile.domain.TBMobileUser
+import org.springframework.security.core.userdetails.UserDetailsService
+
+interface MobileUserService : UserDetailsService {
+
+    fun getByUid(uid: String): TBMobileUser?
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/service/impl/MobileApiServiceImpl.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/service/impl/MobileApiServiceImpl.kt
new file mode 100644
index 0000000..3f95fc7
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/service/impl/MobileApiServiceImpl.kt
@@ -0,0 +1,183 @@
+package com.supwisdom.dlpay.mobile.service.impl
+
+import com.mascloud.sdkclient.Client
+import com.supwisdom.dlpay.api.bean.BaseResp
+import com.supwisdom.dlpay.api.dao.CardDao
+import com.supwisdom.dlpay.api.domain.TCard
+import com.supwisdom.dlpay.framework.service.SystemUtilService
+import com.supwisdom.dlpay.mobile.dao.MobileUserDao
+import com.supwisdom.dlpay.mobile.dao.PagesDao
+import com.supwisdom.dlpay.mobile.domain.TBMobileUser
+import com.supwisdom.dlpay.mobile.domain.TBPages
+import com.supwisdom.dlpay.mobile.service.MobileApiService
+import com.supwisdom.dlpay.util.ConstantUtil
+import mu.KotlinLogging
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.stereotype.Service
+import java.util.*
+
+@Service
+class MobileApiServiceImpl : MobileApiService {
+    @Autowired
+    lateinit var mobileUserDao: MobileUserDao
+
+    @Autowired
+    lateinit var cardDao: CardDao
+
+    @Autowired
+    lateinit var pagesDao: PagesDao
+
+    @Autowired
+    lateinit var systemUtilService: SystemUtilService
+    companion object {
+        var isMsgLogined: Boolean = false
+    }
+    val logger = KotlinLogging.logger { }
+
+    override fun saveUser(user: TBMobileUser): TBMobileUser {
+        return mobileUserDao.save(user)
+    }
+
+    override fun findPageById(pageid: String): TBPages? {
+        return pagesDao.findById(pageid).let {
+            if (it.isPresent) it.get() else null
+        }
+    }
+
+    override fun findUserByPhone(phone: String): TBMobileUser? {
+        return mobileUserDao.findByPhone(phone)
+    }
+
+    override fun findUserById(id: String): TBMobileUser? {
+        return mobileUserDao.findById(id).let {
+            if (it.isPresent) it.get() else null
+        }
+    }
+
+    override fun findCardByNo(cardno: String): TCard? {
+        return cardDao.findCardByCardnoAndCardtype(cardno, ConstantUtil.CARDTYPE_BANKCARD)
+    }
+
+    override fun findCardByUserid(userid: String): TCard? {
+        return cardDao.findCardByUseridAndCardtype(userid, ConstantUtil.CARDTYPE_BANKCARD)
+    }
+
+    override fun findCityCardByUserid(userid: String): TCard? {
+        return cardDao.findCardByUseridAndCardtype(userid, ConstantUtil.CARDTYPE_CITIZENCARD)
+    }
+
+    override fun saveCard(card: TCard): TCard {
+        return cardDao.save(card)
+    }
+
+    override fun sendSms(phone: String, code: String): BaseResp {
+        var resp = BaseResp()
+        var url = systemUtilService.getBusinessValue("sms.url")
+        if (url.isNullOrEmpty()) {
+            resp.retcode = "1"
+            resp.retmsg = "短信业务参数未配置"
+            logger.error { "短信参数:sms.url 未在业务表中配置" }
+            return resp
+        }
+        var account = systemUtilService.getBusinessValue("sms.account")
+        if (account.isNullOrEmpty()) {
+            resp.retcode = "1"
+            resp.retmsg = "短信业务参数未配置"
+            logger.error { "短信参数:sms.account 未在业务表中配置" }
+            return resp
+        }
+        var pwd = systemUtilService.getBusinessValue("sms.pwd")
+        if (pwd.isNullOrEmpty()) {
+            resp.retcode = "1"
+            resp.retmsg = "短信业务参数未配置"
+            logger.error { "短信参数:sms.pwd 未在业务表中配置" }
+            return resp
+        }
+        var ecname = systemUtilService.getBusinessValue("sms.ecname")
+        if (ecname.isNullOrEmpty()) {
+            resp.retcode = "1"
+            resp.retmsg = "短信业务参数未配置"
+            logger.error { "短信参数:sms.ecname 未在业务表中配置" }
+            return resp
+        }
+        var addserial = systemUtilService.getBusinessValue("sms.addserial")
+        if (addserial.isNullOrEmpty()) {
+            addserial=""
+        }
+        var sign = systemUtilService.getBusinessValue("sms.sign")
+        if (sign.isNullOrEmpty()) {
+            resp.retcode = "1"
+            resp.retmsg = "短信业务参数未配置"
+            logger.error { "短信参数:sms.sign 未在业务表中配置" }
+            return resp
+        }
+        var tempid = systemUtilService.getBusinessValue("sms.tempid")
+        if (tempid.isNullOrEmpty()) {
+            resp.retcode = "1"
+            resp.retmsg = "短信业务参数未配置"
+            logger.error { "短信参数:sms.tempid 未在业务表中配置" }
+            return resp
+        }
+        var priority = systemUtilService.getBusinessValue("sms.priority")
+        if (priority.isNullOrEmpty()) {
+            priority = "1"
+        }
+        var client = Client.getInstance()
+        // 正式环境IP,登录验证URL,用户名,密码,集团客户名称
+        if(!isMsgLogined){
+            var ret = client.login(url, account, pwd, ecname)
+            if (!ret) {
+                logger.error { "无法登陆短信平台,身份验证失" }
+                resp.retcode = "1"
+                resp.retmsg = "无法登陆短信平台"
+                return resp
+            }
+            isMsgLogined = true
+        }
+        var sendResult = client.sendTSMS(arrayOf(phone),
+                tempid, arrayOf(code), addserial, priority.toInt(), sign, UUID.randomUUID().toString())
+        println("推送结果: $sendResult")
+        return if (sendResult == 1 || sendResult == 110) {
+            resp.retcode = "0"
+            resp.retmsg = "成功"
+            resp
+        } else if (sendResult == 104) {
+            resp.retcode = "1"
+            resp.retmsg = "手机号错误"
+            resp
+        } else if (sendResult == 105) {
+            //105 需要重新登录
+            var ret = client.login(url, account, pwd, ecname)
+            if (!ret) {
+                logger.error { "无法登陆短信平台,身份验证失" }
+                resp.retcode = "1"
+                resp.retmsg = "无法登陆短信平台"
+                return resp
+            }
+            resp.retcode = "1"
+            resp.retmsg = "短信发送失败,请稍后再试"
+            resp
+        } else if (sendResult == 106 || sendResult == 108 || sendResult == 111) {
+            logger.error { "106:网关签名为空, 用户需要填写网关签名编号;108:JMS异常,用户侧网络问题。需要检查防火墙配置和网络连通性,看是否能够与云MAS平台正常连接;111:扩展码错误,扩展码只能是15位以内数字或空字符串" }
+            resp.retcode = "1"
+            resp.retmsg = "短信发送失败,请稍后再试"
+            resp
+        } else {
+            resp.retcode = "1"
+            resp.retmsg = "短信发送失败,请稍后再试"
+            resp
+        }
+    }
+
+    override fun findByUseridAndStatus(userid: String, status: String): List<TBMobileUser>? {
+        return mobileUserDao.findByUseridAndStatus(userid, status)
+    }
+
+    override fun findCardByUseridAndCardphyid(userid: String, cardtype: String, cardphyid: String?): TCard? {
+        return if (cardphyid.isNullOrEmpty()) {
+            cardDao.findCardByUseridAndCardtype(userid, cardtype)
+        } else {
+            cardDao.findBankcardByCitizencard(userid, cardtype, cardphyid)
+        }
+    }
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/service/impl/MobileUserServiceImpl.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/service/impl/MobileUserServiceImpl.kt
new file mode 100644
index 0000000..90dfd62
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/mobile/service/impl/MobileUserServiceImpl.kt
@@ -0,0 +1,55 @@
+package com.supwisdom.dlpay.mobile.service.impl
+
+import com.supwisdom.dlpay.mobile.dao.MobileUserDao
+import com.supwisdom.dlpay.mobile.domain.TBMobileUser
+import com.supwisdom.dlpay.mobile.exception.UserLoginFailException
+import com.supwisdom.dlpay.mobile.service.MobileUserService
+import mu.KotlinLogging
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.security.core.GrantedAuthority
+import org.springframework.security.core.authority.AuthorityUtils
+import org.springframework.security.core.userdetails.UserDetails
+import org.springframework.security.core.userdetails.UsernameNotFoundException
+import org.springframework.stereotype.Service
+
+@Service
+class MobileUserServiceImpl : MobileUserService {
+    companion object {
+        const val TIME_INTERVAL = 1000 * 60 * 30
+    }
+    val logger = KotlinLogging.logger { }
+    @Autowired
+    lateinit var mobileUserDao: MobileUserDao
+
+    override fun loadUserByUsername(username: String?): UserDetails {
+        logger.error("username:$username")
+        if (username.isNullOrEmpty()) {
+            throw UsernameNotFoundException("用户不存在")
+        }
+        val temp = mobileUserDao.findByLoginid(username)
+        if (temp != null) {
+            if (temp.loginpwd.isEmpty()) {
+                throw UserLoginFailException("用户注册后未设置登录密码,请找回密码或重新注册")
+            }
+            if (temp.loginpwderror >= 5 && (System.currentTimeMillis() - temp.loginpwderrortime!!) < TIME_INTERVAL) {
+                throw UserLoginFailException("密码错误次数过多,请30分钟后再试")
+            } else if (temp.loginpwderror >= 5 && (System.currentTimeMillis() - temp.loginpwderrortime!!) > TIME_INTERVAL) {
+                //更新时间
+                temp.loginpwderror = 0
+                temp.loginpwderrortime = null
+                mobileUserDao.save(temp)
+            }
+            val authorities: Collection<GrantedAuthority> = AuthorityUtils.createAuthorityList("ROLE_USER")
+            temp.auths = authorities
+        } else {
+            throw UsernameNotFoundException("用户不存在")
+        }
+        return temp
+    }
+
+    override fun getByUid(uid: String): TBMobileUser? {
+        return mobileUserDao.findById(uid).let {
+            if (it.isPresent) it.get() else null
+        }
+    }
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/oauth.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/oauth.kt
new file mode 100644
index 0000000..08f1e79
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/oauth.kt
@@ -0,0 +1,102 @@
+package com.supwisdom.dlpay
+
+import com.supwisdom.dlpay.framework.core.PasswordBCryptConfig
+import com.supwisdom.dlpay.system.service.ParamService
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.data.redis.connection.RedisConnectionFactory
+import org.springframework.security.authentication.AuthenticationManager
+import org.springframework.security.core.GrantedAuthority
+import org.springframework.security.core.authority.SimpleGrantedAuthority
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
+import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer
+import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter
+import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer
+import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer
+import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer
+import org.springframework.security.oauth2.provider.ClientDetails
+import org.springframework.security.oauth2.provider.ClientDetailsService
+import org.springframework.security.oauth2.provider.client.BaseClientDetails
+import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore
+import java.security.SecureRandom
+import java.util.*
+
+
+class OAuthDetailService : ClientDetailsService {
+    @Autowired
+    private lateinit var paramService: ParamService;
+    override fun loadClientByClientId(clientId: String?): ClientDetails {
+        val details = BaseClientDetails()
+        if (clientId.isNullOrEmpty()) {
+            return details
+        }
+        details.clientId = clientId
+        paramService.getApiClientByAppid(clientId)?.let {
+            details.setAuthorizedGrantTypes(Arrays.asList("password","authorization_code","refresh_token"))
+            details.setScope(Arrays.asList("read"))
+            details.setResourceIds(Arrays.asList("oauth2-resource"))
+            val authorities = HashSet<GrantedAuthority>()
+            authorities.add(SimpleGrantedAuthority("ROLE_THIRD_ADMIN"))
+            details.authorities = authorities
+            details.setAutoApproveScopes(Arrays.asList("true"))
+            details.clientSecret = it.bcryptSecret
+            details.accessTokenValiditySeconds = 3600
+            details.refreshTokenValiditySeconds=43200
+            if(!it.thirdurl.isNullOrEmpty()){
+                val redir = HashSet<String>()
+                when {
+                    it.thirdurl.contains(",") -> redir.addAll(it.thirdurl.split(","))
+                    it.thirdurl.contains(";") -> redir.addAll(it.thirdurl.split(";"))
+                    else -> redir.add(it.thirdurl)
+                }
+                details.registeredRedirectUri = redir
+            }
+
+        }
+        return details
+    }
+}
+
+
+@Configuration
+class OAuth2Config {
+
+    @Configuration
+    @EnableAuthorizationServer
+
+    class AuthorizationServerConfigure : AuthorizationServerConfigurerAdapter() {
+
+        @Autowired
+        private lateinit var redisConnectionFactory: RedisConnectionFactory
+
+        @Autowired
+        private lateinit var authenticationManager: AuthenticationManager
+
+        @Autowired
+        lateinit var passwordBCryptConfig: PasswordBCryptConfig
+
+        override fun configure(security: AuthorizationServerSecurityConfigurer?) {
+            security?.allowFormAuthenticationForClients()
+            security?.passwordEncoder(pwdEncoder())
+        }
+        @Bean
+        fun pwdEncoder(): BCryptPasswordEncoder {
+            return if (passwordBCryptConfig.seed.isBlank()) {
+                BCryptPasswordEncoder()
+            } else {
+                BCryptPasswordEncoder(passwordBCryptConfig.length,
+                        SecureRandom(passwordBCryptConfig.seed.toByteArray()))
+            }
+        }
+
+        override fun configure(clients: ClientDetailsServiceConfigurer?) {
+            clients?.withClientDetails(OAuthDetailService())
+        }
+
+        override fun configure(endpoints: AuthorizationServerEndpointsConfigurer?) {
+            endpoints?.tokenStore(RedisTokenStore(redisConnectionFactory))
+                    ?.authenticationManager(authenticationManager)
+        }
+    }
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/PortalApi.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/PortalApi.kt
new file mode 100644
index 0000000..3ea8bbc
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/PortalApi.kt
@@ -0,0 +1,14 @@
+package com.supwisdom.dlpay.portal
+
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@RequestMapping("/portalapi")
+class PortalApi{
+    @RequestMapping("/test")
+    fun test(): ResponseEntity<Any>{
+        return ResponseEntity.ok("测试")
+    }
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/security.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/security.kt
new file mode 100644
index 0000000..e8464f1
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/security.kt
@@ -0,0 +1,455 @@
+package com.supwisdom.dlpay
+
+import com.supwisdom.dlpay.framework.core.JwtConfig
+import com.supwisdom.dlpay.framework.core.JwtTokenUtil
+import com.supwisdom.dlpay.framework.core.PasswordBCryptConfig
+import com.supwisdom.dlpay.framework.redisrepo.ApiJwtRepository
+import com.supwisdom.dlpay.framework.security.MyAuthenticationFailureHandler
+import com.supwisdom.dlpay.framework.security.ValidateCodeSecurityConfig
+import com.supwisdom.dlpay.framework.service.OperatorDetailService
+import com.supwisdom.dlpay.framework.tenant.TenantContext
+import com.supwisdom.dlpay.framework.util.Constants
+import com.supwisdom.dlpay.framework.util.TradeDict
+import com.supwisdom.dlpay.mobile.AuthLoginFailHandler
+import com.supwisdom.dlpay.mobile.AuthLoginSuccessHandler
+import com.supwisdom.dlpay.mobile.service.MobileUserService
+import org.jose4j.jwt.ReservedClaimNames
+import org.jose4j.jwt.consumer.InvalidJwtException
+import org.jose4j.lang.JoseException
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.annotation.Order
+import org.springframework.http.HttpStatus
+import org.springframework.security.authentication.AuthenticationManager
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
+import org.springframework.security.config.http.SessionCreationPolicy
+import org.springframework.security.core.authority.SimpleGrantedAuthority
+import org.springframework.security.core.context.SecurityContextHolder
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
+import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher
+import org.springframework.stereotype.Component
+import org.springframework.web.cors.CorsConfiguration
+import org.springframework.web.cors.CorsConfigurationSource
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource
+import org.springframework.web.filter.OncePerRequestFilter
+import java.security.SecureRandom
+import java.util.*
+import javax.servlet.FilterChain
+import javax.servlet.http.HttpServletRequest
+import javax.servlet.http.HttpServletResponse
+import javax.sql.DataSource
+
+
+@Component
+class ApiJwtAuthenticationFilter : OncePerRequestFilter() {
+    @Autowired
+    lateinit var jwtConfig: JwtConfig
+
+    @Autowired
+    lateinit var apiJwtRepository: ApiJwtRepository
+
+    private var jwtUtil: JwtTokenUtil? = null
+
+    private fun getUtil(): JwtTokenUtil {
+        if (jwtUtil == null) {
+            jwtUtil = JwtTokenUtil((jwtConfig))
+        }
+        return jwtUtil as JwtTokenUtil
+    }
+
+    override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
+        if (!jwtConfig.multiTenant) {
+            TenantContext.setTenantSchema(Constants.DEFAULT_TENANTID)
+        }
+        request.getHeader(jwtConfig.header)?.let { authHeader ->
+            try {
+                val jwt = if (authHeader.startsWith(jwtConfig.tokenHeader)) {
+                    authHeader.substring(jwtConfig.tokenHeader.length)
+                } else {
+                    throw JoseException("JWT Header error")
+                }
+                val claims = getUtil().verifyToken(jwt)
+                apiJwtRepository.findById(claims[ReservedClaimNames.JWT_ID].toString()).let {
+                    if (!it.isPresent) {
+                        throw JoseException("JWT has not been register")
+                    }
+                    // token 已被设为黑名单
+                    if (it.get().status != TradeDict.JWT_STATUS_NORMAL) {
+                        throw JoseException("JWT status error : ${it.get().status}")
+                    }
+                }
+                if (jwtConfig.multiTenant) {
+                    val tenantId = request.getHeader(Constants.HEADER_TETANTID)
+                    if (tenantId == null) {
+                        response.status = HttpStatus.UNAUTHORIZED.value()
+                        return
+                    }
+                    if (claims[Constants.JWT_CLAIM_TENANTID] != tenantId) {
+                        response.status = HttpStatus.UNAUTHORIZED.value()
+                        return
+                    }
+                    TenantContext.setTenantSchema(tenantId)
+                }
+                val auth = UsernamePasswordAuthenticationToken(claims[Constants.JWT_CLAIM_UID], null,
+                        (claims[Constants.JWT_CLAIM_AUTHORITIES] as ArrayList<*>)
+                                .map { SimpleGrantedAuthority(it as String) })
+                SecurityContextHolder.getContext().authentication = auth
+            } catch (e: InvalidJwtException) {
+                SecurityContextHolder.clearContext()
+                if (e.hasExpired()) {
+                    // jwt 过期后返回 401
+                    apiJwtRepository.deleteById(e.jwtContext.jwtClaims.jwtId)
+                }
+                response.status = HttpStatus.UNAUTHORIZED.value()
+                return
+            } catch (e: JoseException) {
+                SecurityContextHolder.clearContext()
+                // jwt 失效后返回 401
+                response.status = HttpStatus.UNAUTHORIZED.value()
+                response.contentType = "application/json;charset=UTF-8"
+                return
+            } catch (e: Exception) {
+                SecurityContextHolder.clearContext()
+                // jwt 失效后返回 401
+                response.status = HttpStatus.UNAUTHORIZED.value()
+                response.contentType = "application/json;charset=UTF-8"
+                return
+            }
+        }
+        filterChain.doFilter(request, response)
+    }
+}
+
+@Component
+class MobileSecurityFilter : OncePerRequestFilter() {
+    @Autowired
+    lateinit var jwtConfig: JwtConfig
+
+    @Autowired
+    lateinit var apiJwtRepository: ApiJwtRepository
+
+    private var jwtUtil: JwtTokenUtil? = null
+
+    private fun getUtil(): JwtTokenUtil {
+        if (jwtUtil == null) {
+            jwtUtil = JwtTokenUtil((jwtConfig))
+        }
+        return jwtUtil as JwtTokenUtil
+    }
+
+    override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
+        var context: String? = request.contextPath
+        if (context == null || "" == context.trim { it <= ' ' }) {
+            context = "/"
+        }
+        if (request.requestURI.isEmpty()) {
+            filterChain.doFilter(request, response)
+            return
+        }
+        var url = request.requestURI
+        if ("/" != context) {
+            url = url.replace(context, "")
+        }
+        logger.info(url)
+        if (!url.startsWith("/mobileapi/v1/")) {
+            filterChain.doFilter(request, response)
+            return
+        }
+        request.getHeader(jwtConfig.header)?.let { authHeader ->
+            try {
+                val jwt = if (authHeader.startsWith(jwtConfig.tokenHeader)) {
+                    authHeader.substring(jwtConfig.tokenHeader.length)
+                } else {
+                    throw JoseException("JWT Header error")
+                }
+                val claims = getUtil().verifyToken(jwt)
+                apiJwtRepository.findById(claims[ReservedClaimNames.JWT_ID].toString()).let {
+                    if (!it.isPresent) {
+                        throw JoseException("JWT has not been register")
+                    }
+                    // token 已被设为黑名单
+                    if (it.get().status != TradeDict.JWT_STATUS_NORMAL) {
+                        throw JoseException("JWT status error : ${it.get().status}")
+                    }
+                }
+                if (jwtConfig.multiTenant) {
+                    val tenantId = request.getHeader(Constants.HEADER_TETANTID)
+                    if (tenantId == null) {
+                        response.status = HttpStatus.UNAUTHORIZED.value()
+                        return
+                    }
+                    if (claims[Constants.JWT_CLAIM_TENANTID] != tenantId) {
+                        response.status = HttpStatus.UNAUTHORIZED.value()
+                        return
+                    }
+                    TenantContext.setTenantSchema(tenantId)
+                }
+                val auth = UsernamePasswordAuthenticationToken(claims[Constants.JWT_CLAIM_UID], null,
+                        (claims[Constants.JWT_CLAIM_AUTHORITIES] as ArrayList<*>)
+                                .map { SimpleGrantedAuthority(it as String) })
+                SecurityContextHolder.getContext().authentication = auth
+            } catch (e: InvalidJwtException) {
+                SecurityContextHolder.clearContext()
+                if (e.hasExpired()) {
+                    // jwt 过期后返回 401
+                    apiJwtRepository.deleteById(e.jwtContext.jwtClaims.jwtId)
+                }
+                response.status = HttpStatus.UNAUTHORIZED.value()
+                return
+            } catch (e: JoseException) {
+                SecurityContextHolder.clearContext()
+                // jwt 失效后返回 401
+                response.status = HttpStatus.UNAUTHORIZED.value()
+                response.contentType = "application/json;charset=UTF-8"
+                return
+            } catch (e: Exception) {
+                SecurityContextHolder.clearContext()
+                // jwt 失效后返回 401
+                response.status = HttpStatus.UNAUTHORIZED.value()
+                response.contentType = "application/json;charset=UTF-8"
+                return
+            }
+        }
+        response.setHeader("Access-Control-Allow-Origin", "*");
+        response.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS");
+        response.setHeader("Access-Control-Allow-Headers", "*");
+        response.setHeader("Access-Control-Allow-Credentials", "true")
+        filterChain.doFilter(request, response)
+    }
+}
+
+@Component
+class PortalApiSecurityFilter : OncePerRequestFilter() {
+    @Autowired
+    lateinit var jwtConfig: JwtConfig
+
+    @Autowired
+    lateinit var apiJwtRepository: ApiJwtRepository
+
+    private var jwtUtil: JwtTokenUtil? = null
+
+    private fun getUtil(): JwtTokenUtil {
+        if (jwtUtil == null) {
+            jwtUtil = JwtTokenUtil((jwtConfig))
+        }
+        return jwtUtil as JwtTokenUtil
+    }
+
+    override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
+        var context: String? = request.contextPath
+        if (context == null || "" == context.trim { it <= ' ' }) {
+            context = "/"
+        }
+        if (request.requestURI.isEmpty()) {
+            filterChain.doFilter(request, response)
+            return
+        }
+        var url = request.requestURI
+        if ("/" != context) {
+            url = url.replace(context, "")
+        }
+        logger.info(url)
+        request.getHeader(jwtConfig.header)?.let { authHeader ->
+            try {
+                val jwt = if (authHeader.startsWith(jwtConfig.tokenHeader)) {
+                    authHeader.substring(jwtConfig.tokenHeader.length)
+                } else {
+                    throw JoseException("JWT Header error")
+                }
+                val claims = getUtil().verifyToken(jwt)
+                apiJwtRepository.findById(claims[ReservedClaimNames.JWT_ID].toString()).let {
+                    if (!it.isPresent) {
+                        throw JoseException("JWT has not been register")
+                    }
+                    // token 已被设为黑名单
+                    if (it.get().status != TradeDict.JWT_STATUS_NORMAL) {
+                        throw JoseException("JWT status error : ${it.get().status}")
+                    }
+                }
+                if (jwtConfig.multiTenant) {
+                    val tenantId = request.getHeader(Constants.HEADER_TETANTID)
+                    if (tenantId == null) {
+                        response.status = HttpStatus.UNAUTHORIZED.value()
+                        return
+                    }
+                    if (claims[Constants.JWT_CLAIM_TENANTID] != tenantId) {
+                        response.status = HttpStatus.UNAUTHORIZED.value()
+                        return
+                    }
+                    TenantContext.setTenantSchema(tenantId)
+                }
+                val auth = UsernamePasswordAuthenticationToken(claims[Constants.JWT_CLAIM_UID], null,
+                        (claims[Constants.JWT_CLAIM_AUTHORITIES] as ArrayList<*>)
+                                .map { SimpleGrantedAuthority(it as String) })
+                SecurityContextHolder.getContext().authentication = auth
+            } catch (e: InvalidJwtException) {
+                SecurityContextHolder.clearContext()
+                if (e.hasExpired()) {
+                    // jwt 过期后返回 401
+                    apiJwtRepository.deleteById(e.jwtContext.jwtClaims.jwtId)
+                }
+                response.status = HttpStatus.UNAUTHORIZED.value()
+                return
+            } catch (e: JoseException) {
+                SecurityContextHolder.clearContext()
+                // jwt 失效后返回 401
+                response.status = HttpStatus.UNAUTHORIZED.value()
+                response.contentType = "application/json;charset=UTF-8"
+                return
+            } catch (e: Exception) {
+                SecurityContextHolder.clearContext()
+                // jwt 失效后返回 401
+                response.status = HttpStatus.UNAUTHORIZED.value()
+                response.contentType = "application/json;charset=UTF-8"
+                return
+            }
+        }
+        response.setHeader("Access-Control-Allow-Origin", "*");
+        response.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS");
+        response.setHeader("Access-Control-Allow-Headers", "*");
+        response.setHeader("Access-Control-Allow-Credentials", "true")
+        filterChain.doFilter(request, response)
+    }
+}
+
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+class WebSecurityConfig {
+
+    companion object {
+
+        @Configuration
+        @Order(2)
+        class MobileApiSecurityConfigurationAdapter : WebSecurityConfigurerAdapter() {
+            @Autowired
+            lateinit var failureHandler: AuthLoginFailHandler
+            @Autowired
+            lateinit var successHandler: AuthLoginSuccessHandler
+            @Autowired
+            lateinit var passwordBCryptConfig: PasswordBCryptConfig
+
+            @Autowired
+            lateinit var userDetailsService: MobileUserService
+            @Autowired
+            lateinit var mobileSecurityFilter: MobileSecurityFilter
+
+
+            override fun configure(auth: AuthenticationManagerBuilder) {
+                auth.authenticationProvider(userProvider())
+            }
+
+            @Bean
+            override fun authenticationManager(): AuthenticationManager {
+                return super.authenticationManagerBean()
+            }
+
+            @Bean
+            fun userProvider(): DaoAuthenticationProvider {
+                return DaoAuthenticationProvider().apply {
+                    setUserDetailsService(userDetailsService)
+                    setPasswordEncoder(userPasswordEncoder())
+                }
+            }
+
+            @Bean
+            fun userPasswordEncoder(): BCryptPasswordEncoder {
+                return if (passwordBCryptConfig.seed.isBlank()) {
+                    BCryptPasswordEncoder()
+                } else {
+                    BCryptPasswordEncoder(passwordBCryptConfig.length,
+                            SecureRandom(passwordBCryptConfig.seed.toByteArray()))
+                }
+            }
+
+            override fun configure(http: HttpSecurity) {
+                http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+                        .and()
+                        .cors()
+                        .and()
+                        .antMatcher("/mobileapi/**")
+                        .addFilterAfter(mobileSecurityFilter,
+                                UsernamePasswordAuthenticationFilter::class.java)
+                        .authorizeRequests().antMatchers("/mobileapi/i/**", "/mobileapi/login")
+                        .permitAll().anyRequest().authenticated()
+                        .and()
+                        .formLogin()
+                        .loginProcessingUrl("/mobileapi/login")
+                        .failureHandler(failureHandler)
+                        .successHandler(successHandler)
+                        .and().csrf().disable()
+            }
+
+            @Bean
+            fun corsConfigurationSource(): CorsConfigurationSource {
+                //手机端支持跨域请求
+                val configuration = CorsConfiguration()
+                configuration.allowedOrigins = listOf("*")
+                configuration.allowedMethods = listOf("GET", "POST")
+                configuration.allowedHeaders = listOf("*")
+                val source = UrlBasedCorsConfigurationSource()
+                source.registerCorsConfiguration("/mobileapi/**", configuration)
+                return source
+            }
+        }
+
+        @Configuration
+        @Order(3)
+        class PortalApiSecurityConfigurationAdapter : WebSecurityConfigurerAdapter() {
+            @Autowired
+            lateinit var passwordBCryptConfig: PasswordBCryptConfig
+
+            @Autowired
+            lateinit var userDetailsService: MobileUserService
+            @Autowired
+            lateinit var portalApiSecurityFilter: PortalApiSecurityFilter
+
+
+            override fun configure(auth: AuthenticationManagerBuilder) {
+                auth.authenticationProvider(portalUserProvider())
+            }
+
+            @Bean
+            fun portalUserProvider(): DaoAuthenticationProvider {
+                return DaoAuthenticationProvider().apply {
+                    setUserDetailsService(userDetailsService)
+                    setPasswordEncoder(portalUserPasswordEncoder())
+                }
+            }
+
+            @Bean
+            fun portalUserPasswordEncoder(): BCryptPasswordEncoder {
+                return if (passwordBCryptConfig.seed.isBlank()) {
+                    BCryptPasswordEncoder()
+                } else {
+                    BCryptPasswordEncoder(passwordBCryptConfig.length,
+                            SecureRandom(passwordBCryptConfig.seed.toByteArray()))
+                }
+            }
+
+            override fun configure(http: HttpSecurity) {
+                http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+                        .and()
+                        .cors()
+                        .and()
+                        .antMatcher("/portalapi/**")
+                        .addFilterAfter(portalApiSecurityFilter,
+                                UsernamePasswordAuthenticationFilter::class.java)
+                        .authorizeRequests().antMatchers( "/mobileapi/login")
+                        .permitAll().anyRequest().authenticated()
+                        .and()
+                        .formLogin()
+                        .loginProcessingUrl("/mobileapi/login")
+                        .and().csrf().disable()
+            }
+        }
+    }
+}
diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties
new file mode 100644
index 0000000..091b5cf
--- /dev/null
+++ b/backend/src/main/resources/application.properties
@@ -0,0 +1,44 @@
+#######################################springboot配置 start#################################
+spring.application.name=supwisdom.payapi
+spring.cloud.consul.discovery.health-check-path=${management.context-path}/api/common/version
+spring.cloud.consul.discovery.health-check-interval=10s
+spring.cloud.consul.discovery.instanceId=${spring.application.name}:${spring.application.instance_id:${random.value}}
+# 单库数据库配置
+spring.jpa.show-sql=true
+spring.datasource.hikari.connection-timeout=60000
+spring.datasource.hikari.maximum-pool-size=5
+spring.jpa.hibernate.ddl-auto=update
+# logging
+logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n
+logging.level.org.hibernate.SQL=debug
+#################### JSP PAGE ####################
+#spring.mvc.view.prefix=/pages/
+#spring.mvc.view.suffix=.jsp
+server.servlet.context-path=/payapi
+#################### thymeleaf ####################
+spring.mvc.static-path-pattern=/static/**
+spring.thymeleaf.prefix=classpath:/templates/
+spring.thymeleaf.suffix=.html
+spring.thymeleaf.encoding=UTF-8
+spring.thymeleaf.mode=HTML5
+spring.thymeleaf.cache=false
+spring.thymeleaf.enabled=true
+################## 全局字符编码设置 ######################
+spring.http.encoding.force=true
+spring.http.encoding.charset=UTF-8
+spring.http.encoding.enabled=true
+server.tomcat.uri-encoding=UTF-8
+##################################################
+## quartz task scheduler
+shopbalance.updater.cron=*/10 * * * * ?
+dayend.settletask.cron=0 3/30 2-3 * * ?
+query.third.transdtl.result.cron=7 0/1 * * * ?
+payapi.sourcetype.checker.scheduler=7 3/10 * * * ?
+citizencard.dolosstask.cron=-
+send.delay.notice.task.cron=29 0/1 * * * ?
+################################################
+# user password
+auth.password.bcrypt.length=10
+###################################################
+spring.redis.database=0
+multi-tenant.datasource.base-package=com.supwisdom.dlpay
\ No newline at end of file
diff --git a/backend/src/main/resources/data-postgresql.sql b/backend/src/main/resources/data-postgresql.sql
new file mode 100644
index 0000000..4298c90
--- /dev/null
+++ b/backend/src/main/resources/data-postgresql.sql
@@ -0,0 +1,16 @@
+--pg--
+INSERT INTO "tb_period" ("id", "period_year", "period_month", "startdate", "enddate", "settleflag" , "tenantid")
+VALUES ('8a53b7826c65b925016c65bfa7c3001c',to_number(to_char(CURRENT_TIMESTAMP,'yyyy'),'9999'),to_number(to_char(CURRENT_TIMESTAMP,'MM'),'99'), to_char(CURRENT_TIMESTAMP,'yyyyMM')||'01', to_char((to_date(to_char(CURRENT_TIMESTAMP+'1 month','yyyyMM')||'01','yyyyMMdd')-1)::Timestamp,'yyyyMMdd'), 0, '{tenantid}');
+
+insert into TB_SETTLECTL(BOOKSETNO,PERIODYEAR,PERIODMONTH,STATDATE,SETTLEDATE,STATUS,updtime, "tenantid")
+values (1,to_number(to_char(CURRENT_TIMESTAMP,'yyyy'),'9999'),to_number(to_char(CURRENT_TIMESTAMP,'MM'),'99'),to_number(to_char(CURRENT_TIMESTAMP,'yyyyMMdd'),'99999999'),to_number(to_char(CURRENT_TIMESTAMP,'yyyyMMdd'),'99999999'),0,to_char(CURRENT_TIMESTAMP,'yyyyMMddhh24miss'), '{tenantid}');
+
+insert into TB_VOUCHERNOCTL(VOUCHERTYPE,PERIODMONTH,VOUCHERNO,"tenantid")
+values (1,to_number(to_char(CURRENT_TIMESTAMP,'MM'),'99'),0, , '{tenantid}');
+
+update TB_SUBJECT set opendate = to_number(to_char(CURRENT_TIMESTAMP,'yyyymmdd'),'99999999');
+
+CREATE SEQUENCE seq_refno;
+
+---------  end of script
+commit;
diff --git a/backend/src/main/resources/data.sql b/backend/src/main/resources/data.sql
new file mode 100644
index 0000000..3bd9798
--- /dev/null
+++ b/backend/src/main/resources/data.sql
@@ -0,0 +1,743 @@
+INSERT INTO tb_tenantconfig(cfgid, tenantid, datacenter_id)
+values ('main', '{tenantid}', '01');
+
+INSERT INTO tb_operator(operid, closedate, opendate, opercode, opername, operpwd, opertype, status, thirdadmin, tenantid)
+VALUES ('LOR2IwRkbOjp+sVG9KR2BpHZbwGKepS4', '20500101', '20190101', 'system', '系统管理员', '$2a$10$Ex9xp11.vCaD8D0a7ahiUOKqDij1TcCUBwRAmrqXeDvAkmzLibn4.', 'oper', 'normal', 'no', '{tenantid}');
+
+
+INSERT INTO tb_role(roleid, createtime, editflag, lastsaved, rolecode, roledesc, rolename, tenantid)
+VALUES ('d1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '20190101000000', 0, '', 'ROLE_ADMIN', '超级管理员', '超级管理员', '{tenantid}');
+
+INSERT INTO tb_oper_role(id, operid, roleid, tenantid)
+VALUES ('1', 'LOR2IwRkbOjp+sVG9KR2BpHZbwGKepS4', 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (1, NULL, 0, NULL, 'layui-icon-home', '#', '主页', 1, -1, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (2, NULL, 1, NULL, 'layui-icon-home', '/home/console', '控制台', 1, 1, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (3, NULL, 0, NULL, 'layui-icon-set', '#', '系统中心', 2, -1, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (5, NULL, 1, NULL, 'layui-icon-set', '/role/index', '角色管理', 2, 3, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (6, NULL, 1, NULL, 'layui-icon-set', '/operator/index', '操作员管理', 4, 3, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (4, NULL, 1, NULL, 'layui-icon-set', '/function/index', '功能维护', 1, 3, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (7, NULL, 0, NULL, 'layui-icon-release', '#', '商户中心', 4, -1, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (9, NULL, 1, NULL, '', '/operator/logs', '操作日志', 4, 3, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (10, NULL, 0, NULL, 'layui-icon-util', '#', '参数管理', 3, -1, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (11, NULL, 1, NULL, 'layui-icon-util', '/param/syspara', '全局参数配置', 1, 10, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (12, NULL, 1, NULL, 'layui-icon-util', '/param/businesspara', '业务参数配置', 2, 10, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (17, NULL, 1, NULL, 'layui-icon-util', '/param/apiclientpara', '应用参数配置', 3, 10, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (18, NULL, 1, NULL, 'layui-icon-util', '/param/sourcetype', '支付能力配置', 4, 10, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (19, NULL, 0, NULL, 'layui-icon-user', '#', '用户中心', 5, -1, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (20, NULL, 0, NULL, 'layui-icon-rmb', '#', '报表中心', 6, -1, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (21, NULL, 1, NULL, '', '/user/index', '用户管理', 1, 19, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (23, NULL, 1, NULL, '', '/shop/index', '商户管理', 1, 7, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (24, NULL, 1, NULL, '', '/user/point', '积分管理', 3, 19, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (22, NULL, 1, NULL, '', '/user/acc', '账户管理', 2, 19, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (25, NULL, 1, NULL, '', '/shop/config', '商户支付能力配置', 3, 7, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (26, NULL, 0, NULL, 'layui-icon-tabs', '#', '流水管理', 3, -1, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (27, NULL, 1, NULL, '', '/dtl/userdtl', '个人流水查询', 1, 26, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (28, NULL, 1, NULL, '', '/dtl/shopdtl', '商户流水查询', 2, 26, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (29, NULL, 1, NULL, '', '/report/subjectday', '科目汇总表', 1, 20, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (30, NULL, 1, NULL, '', '/report/subjectdetail', '科目明细账', 2, 20, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (31, NULL, 1, NULL, '', '/report/shopbusiness', '商户营业情况表', 3, 20, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (32, NULL, 1, NULL, '', '/settlelog/index', '结算日志', 5, 3, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (33, NULL, 0, NULL, 'layui-icon-component', '#', '对账中心', 8, -1, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (34, NULL, 1, NULL, '', '/thirdchk/chkstatus', '对账状态查询', 331, 33, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (35, NULL, 1, NULL, '', '/thirdchk/chkfile', '对账明细查询', 332, 33, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (36, NULL, 1, NULL, '', '/shop/shopcheck', '商户审批', 2, 7, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (37, NULL, 1, NULL, '', '/user/card', '市民卡查询', 1, 19, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (38, NULL, 1, NULL, '', '/report/shoptodaybusiness', '商户当天统计表', 4, 20, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (39, NULL, 1, NULL, '', '/notice/index', '手机通知公告', 6, 3, '{tenantid}');
+
+
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b7955772c0032', 1, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b7955772d0033', 2, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b7955772d0034', 3, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b7955772d0035', 5, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b79557730003b', 6, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b795577310041', 4, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b79557732004a', 9, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b79557732004c', 7, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b79557732004d', 23, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b795577330052', 25, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b79557733005a', 20, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b79557733005b', 29, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b79557733005d', 30, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b79557733005f', 31, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b795577340061', 19, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b795577340062', 21, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b795577350068', 24, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b79557735006a', 22, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b79557735006c', 26, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b79557735006d', 27, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b79557735006f', 28, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b795577350071', 10, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b795577350072', 11, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b795577360074', 17, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b79557736007b', 12, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b795577370081', 18, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816c8468e8016c846d7a570017', 32, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8081816c8468e8016c846d7a570017', 33, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8082816c8468e8016c846d7a570017', 34, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8083816c8468e8016c846d7a570017', 35, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816db87e27016db88be41a0014', 36, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816db87e27016db88be41f0015', 37, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('4028ee9f6e5d95d8016e5d99e8d50012', 38, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816f8d8258016f8d85e4d70005', 39, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+
+
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (4, '1', 4, '添加功能', '/function/add', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (5, '', 4, '添加父功能', '/function/loadadd', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (6, '1', 6, '查询', '/operator/index', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (7, '', 6, '添加查询', '/operator/load4add', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (8, '', 6, '添加和修改', '/operator/add', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (9, '', 6, '重置密码', '/operator/resetpwd', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (10, '', 4, '查询', '/function/index', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (11, '', 4, '删除', '/function/delfunc', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (12, '', 4, '添加子功能', '/function/loadsubadd', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (13, '', 4, '维护资源', '/function/loadres', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (14, '', 4, '添加资源', '/function/addres', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (15, '', 4, '删除资源', '/function/delres', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (16, '', 5, '查询', '/role/index', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (17, '', 5, '添加', '/role/add', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (19, '', 5, '分配功能', '/role/addfunc', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (20, '', 5, '加载分配功能', '/role/loadfunc', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (21, '', 5, '删除角色', '/role/del', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (22, '', 6, '修改状态', '/operator/updatestate', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (24, '', 9, '查询', '/operator/logslist', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (26, '', 11, '修改', '/param/sysparaupdate', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (27, '', 11, '查询', '/param/syspara', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (28, '', 12, '查询', '/param/businesspara', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (29, '', 12, '删除', '/param/deletebusinesspara', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (30, '', 12, '修改', '/param/businessparaupdate', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (32, '', 12, '新增', '/param/addbusinesspara', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (33, '', 12, '新增跳转', '/param/load4addbuspara', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (34, '', 17, '查询', '/param/apiclientpara', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (35, '', 17, '新增修改跳转', '/param/load4addapiclient', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (36, '', 17, '修改状态', '/param/updateapiclientstate', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (37, '', 17, '删除', '/param/deleteapiclient', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (39, '', 17, '修改', '/param/updateapiclientpara', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (40, '', 17, '新增', '/param/addapiclientpara', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (41, '', 18, '查询', '/param/sourcetype', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (42, '', 21, '查询', '/user/list', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (43, '', 18, '新增跳转', '/param/load4addsourcetype', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (44, '', 18, '切换状态', '/param/updatesourcetypestate', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (45, '', 18, '修改', '/param/updatesourcetypename', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (46, '', 18, '删除', '/param/deletesourcetype', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (48, '', 18, '配置跳转', '/param/load4sourcetypeconfig', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (49, '', 18, '配置参数', '/param/addsourcetypeconfig', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (50, '', 22, '查询', '/user/account', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (52, '', 24, '查询', '/user/pointlist', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (53, '', 23, '删除', '/shop/deleteshop', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (54, '', 21, '添加', '/user/add', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (56, '', 23, '商户详情', '/shop/getshopinfo', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (57, '', 23, '查询', '/shop/index', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (58, '', 23, '新增跳转', '/shop/load4addshop', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (59, '', 21, '删除', '/user/del', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (60, '', 21, '注销账户', '/user/delacc', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (61, '', 21, '删除积分账户', '/user/delpoint', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (63, '', 25, '商户树', '/shop/config', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (64, '', 25, '查询', '/shop/shopsourcetypelist', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (65, '', 25, '切换状态', '/shop/updatesourcetypestat', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (66, '', 25, '新增跳转', '/shop/load4addsourcetype', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (67, '', 25, '新增', '/shop/addshopsourcetype', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (68, '', 25, '配置跳转', '/shop/load4sourcetypepara', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (69, '', 25, '配置', '/shop/addsourcetypepara', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (70, '', 27, '查询', '/dtl/userdtl', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (71, '', 28, '查询', '/dtl/shopdtl', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (72, '', 29, '查询', '/report/subjectday', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (73, '', 30, '查询', '/report/subjectdetail', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (74, '', 31, '查询', '/report/shopbusiness', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (76, '', 18, '新增', '/param/addsourcetype', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (77, '', 32, '查询', '/settlelog/index', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (78, '', 32, '查询', '/settlelog/dosettle', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (79, '', 34, '查询', '/thirdchk/chkstatus', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (80, '', 34, '强制重对切换', '/thirdchk/forcerecheck', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (81, '', 35, '查询', '/thirdchk/chkfile', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (82, '', 35, '查看详情', '/thirdchk/chkdtl', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (83, '', 23, '新增商户', '/shop/addshop', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (84, '', 23, '修改跳转', '/shop/load4updateshop', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (85, '', 23, '修改', '/shop/updateshop', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (86, '', 23, '审批意见', '/shop/getshopchk', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (87, '', 36, '查询', '/shop/shopcheck', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (88, '', 36, '审核跳转', '/shop/opercheck', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (89, '', 36, '审核', '/shop/docheckshop', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (90, '', 37, '查询', '/user/card', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (91, '', 38, '查询', '/report/shoptodaybusiness', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (92, '', 37, '新增跳转', '/user/load4addcard', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (93, '', 37, '新增', '/user/cardadd', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (94, '', 37, '修改跳转', '/user/load4modifycard', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (95, '', 37, '修改', '/user/cardupdate', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (96, '', 39, '查询', '/notice/index', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (97, '', 39, '新增修改跳转', '/notice/load4add', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (98, '', 39, '新增修改', '/notice/edit', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (99, '', 39, '删除', '/notice/delete', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (100, '', 39, '发布跳转', '/notice/load4publish', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (101, '', 39, '发布', '/notice/publish', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (102, '', 39, '发布明细', '/notice/load4detail', '{tenantid}');
+
+
+INSERT INTO "tb_permission" ("id", "resid", "role_func_id", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b795577300036', 16, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_permission" ("id", "resid", "role_func_id", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b795577300037', 17, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_permission" ("id", "resid", "role_func_id", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b795577300038', 19, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_permission" ("id", "resid", "role_func_id", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b795577300039', 20, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_permission" ("id", "resid", "role_func_id", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b79557730003a', 21, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_permission" ("id", "resid", "role_func_id", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b79557730003c', 6, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_permission" ("id", "resid", "role_func_id", "roleid", tenantid)
+VALUES ('ff8080816b7947ed016b79557730003d', 7, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b79557731003e', 8, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b79557731003f', 9, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577310040', 22, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577310042', 4, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577310043', 5, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577310044', 10, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577310045', 11, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577310046', 12, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577320047', 13, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577320048', 14, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577320049', 15, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b79557732004b', 24, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b79557732004e', 53, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b79557732004f', 57, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577320050', 56, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577320051', 58, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577330053', 63, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+ VALUES ('ff8080816b7947ed016b795577330054', 64, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577330055', 65, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577330056', 66, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+ VALUES ('ff8080816b7947ed016b795577330057', 68, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+ VALUES ('ff8080816b7947ed016b795577330058', 67, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+ VALUES ('ff8080816b7947ed016b795577330059', 69, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b79557733005c', 72, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b79557733005e', 73, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577340060', 74, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577340063', 54, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577340064', 60, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577340065', 61, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+ VALUES ('ff8080816b7947ed016b795577350066', 42, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577350067', 59, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577350069', 52, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+ VALUES ('ff8080816b7947ed016b79557735006b', 50, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b79557735006e', 70, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577350070', 71, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577360073', 27, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577360075', 39, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577360076', 34, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577360077', 40, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577360078', 37, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577360079', 36, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b79557736007a', 35, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b79557736007c', 33, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b79557736007d', 32, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b79557736007e', 28, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+ VALUES ('ff8080816b7947ed016b79557737007f', 30, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577370080', 29, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577370082', 46, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577370083', 48, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577370084', 41, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577370085', 44, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+ VALUES ('ff8080816b7947ed016b795577370086', 49, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577370087', 45, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816b7947ed016b795577370088', 43, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816bbb130c016bbb6ea2f700c9', 76, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816bbb130c016bbb6ea2f600b3', 26, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816c8468e8016c846d7a5d0018', 77, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816c85279d016c852aa829000d', 78, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816c985e76016c98659ad00015', 79, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816c985e76016c98659ad10016', 80, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816c985e76016c98659ad10017', 81, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816c99abee016c99b1cb1c0004', 82, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('402868516daf75de016dafb36670002a', 83, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816db54261016db543aad40009', 84, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('402868516daf15d2016daf1fcb290017', 85, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816db36bbd016db36ff8580010', 86, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816db36bbd016db36ff8580011', 87, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816db87e27016db94468da0089', 88, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816db87e27016db94468da008a', 89, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816db87e27016db9446fda008b', 90, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('4028ee9f6e5d95d8016e5d99e8dd0013', 91, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816ec5cfd5016ec5d16d470006', 92, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816ec5cfd5016ec5d16d470007', 93, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816ecaebf8016ecaeedcec0004', 94, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816ecafb4e016ecafd58400005', 95, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816f8d8258016f8d85e4e20006', 96, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816fa2c2b7016fa2d809780030', 97, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816fa2c2b7016fa2d80978002e', 98, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816fa2c2b7016fa2d809780031', 99, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816fa2c2b7016fa2d809780032', 100, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816fa2c2b7016fa2d80978002f', 101, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816fa2c2b7016fa2d80977002d', 102, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+
+
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (1, '1001', 1, 'y', 1, NULL, 20190430, 1, '库存现金', 1, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (2, '1002', 1, 'y', 1, NULL, 20190430, 1, '银行存款', 1, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (3, '1121', 1, 'n', 0, NULL, 20190430, 1, '应收票据', 1, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (4, '112101', 1, 'n', 1, '1121', 20190430, 2, '支票', 1, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (5, '112102', 1, 'n', 1, '1121', 20190430, 2, '经费本', 1, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (6, '1122', 1, 'y', 0, NULL, 20190430, 1, '应收账款', 1, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (7, '112201', 1, 'y', 1, '1122', 20190430, 2, '现金充值款', 1, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (8, '112209', 1, 'y', 1, '1122', 20190617, 2, '其他第三方充值款', 1, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (9, '112210', 1, 'y', 1, '1122', 20190430, 2, '支付宝充值款', 1, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (10, '112211', 1, 'y', 1, '1122', 20190430, 2, '微信充值款', 1, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (11, '112212', 1, 'n', 1, '1122', 20190430, 2, '银联充值款', 1, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (12, '112213', 1, 'y', 1, '1122', 20190430, 2, '一卡通充值款', 1, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (13, '112230', 1, 'y', 1, '1122', 20190430, 2, '支付宝支付款', 1, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (14, '112231', 1, 'y', 1, '1122', 20190430, 2, '微信支付款', 1, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (15, '112232', 1, 'n', 1, '1122', 20190430, 2, '银联支付款', 1, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (16, '112233', 1, 'y', 1, '1122', 20190430, 2, '一卡通支付款', 1, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (17, '112234', 1, 'y', 1, '1122', NULL, 2, '市民卡支付款', 1, '{tenantid}');
+--负债
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (18, '2001', 2, 'n', 1, NULL, 20190430, 1, '用户押金', 2, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (19, '2004', 2, 'y', 1, NULL, 20190430, 1, '商户营业款', 2, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (20, '2202', 2, 'y', 0, NULL, 20190430, 1, '应付账款', 2, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (21, '220201', 2, 'y', 1, '2202', 20190430, 2, '个人存款', 2, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (22, '220211', 2, 'n', 1, '2202', 20190430, 2, '销户退款', 2, '{tenantid}');
+--损益
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (23, '6021', 2, 'y', 0, NULL, 20190430, 1, '手续费收入', 6, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (24, '602101', 2, 'y', 1, '6021', 20190430, 2, '支付宝充值手续费', 6, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (25, '602102', 2, 'y', 1, '6021', 20190430, 2, '微信充值手续费', 6, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (26, '602103', 2, 'n', 1, '6021', 20190430, 2, '银联充值手续费', 6, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (27, '6601', 2, 'y', 0, NULL, 20190430, 1, '销售费用', 6, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (28, '660101', 2, 'y', 1, '6601', 20190430, 2, '折扣优惠款', 6, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (29, '660102', 2, 'y', 1, '6601', 20190430, 2, '积分抵扣款', 6, '{tenantid}');
+INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
+VALUES (30, '6602', 2, 'y', 1, NULL, 20190430, 1, '管理费收入', 6, '{tenantid}');
+
+
+
+INSERT INTO "tb_sourcetype" ("sourcetype_id", "sourcetype", "checkable", "paydesc", "enable", "charge_enable", "consume_enable", "anonymous_enable", "reversable", "pay_subjno", "deposite_subjno", "tplusn", "start_chktime", "tenantid")
+VALUES ('D3820D49DACA49199E36537F6719665F', 'citizenCard', 't', '大理市民卡', 't', 'f', 't', 'f', 't', '112234', '-', 1, null, '{tenantid}');
+INSERT INTO "tb_sourcetype" ("sourcetype_id", "sourcetype", "checkable", "paydesc", "enable", "charge_enable", "consume_enable", "anonymous_enable", "reversable", "pay_subjno", "deposite_subjno", "tplusn", "start_chktime", "tenantid")
+VALUES ('0997477F40904AD1A2E37FD15345CE00', 'balance', 'f', '账户余额', 't', 'f', 't', 'f', 't', '-', '-', 0, null, '{tenantid}');
+INSERT INTO "tb_sourcetype" ("sourcetype_id", "sourcetype", "checkable", "paydesc", "enable", "charge_enable", "consume_enable", "anonymous_enable", "reversable", "pay_subjno", "deposite_subjno", "tplusn", "start_chktime", "tenantid")
+VALUES ('F0CA47ADC0F24DFCA0D95DF4136CC2D0', 'thirdpart', 'f', '其他第三方支付', 't', 't', 'f', 'f', 'f','-','-', 0, null, '{tenantid}');
+INSERT INTO "tb_sourcetype" ("sourcetype_id", "sourcetype", "checkable", "paydesc", "enable", "charge_enable", "consume_enable", "anonymous_enable", "reversable", "pay_subjno", "deposite_subjno", "tplusn", "start_chktime", "tenantid")
+VALUES ('F5B344726FA24BD896E70DEE3D3F46CA', 'swyktv5', 't', '一卡通支付', 't', 't', 't', 't', 't','-','-', 1, null, '{tenantid}');
+INSERT INTO "tb_sourcetype" ("sourcetype_id", "sourcetype", "checkable", "paydesc", "enable", "charge_enable", "consume_enable", "anonymous_enable", "reversable", "pay_subjno", "deposite_subjno", "tplusn", "start_chktime", "tenantid")
+VALUES ('28EE54CD3B044CC197D6C5B0E309F8B8', 'alipay', 't', '支付宝', 't', 't', 't', 't', 'f', '112230','112210', 1, '103000', '{tenantid}');
+INSERT INTO "tb_sourcetype" ("sourcetype_id", "sourcetype", "checkable", "paydesc", "enable", "charge_enable", "consume_enable", "anonymous_enable", "reversable", "pay_subjno", "deposite_subjno", "tplusn", "start_chktime", "tenantid")
+VALUES ('DAEF88B54B684347B2B83940C38C7671', 'wechat', 't', '微信支付', 't', 't', 't', 't', 'f', '112231','112211', 1, '103000', '{tenantid}');
+
+-- 支付方式
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('1', 'swyktv5','appid','模板','100005', 't', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('2', 'swyktv5','appkey','模板','adc4ac6822fd462780f878b86cb94688', 't', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('3', 'swyktv5','orderurl','模板','http://172.28.201.101:9116/epayapi/services/thirdparty/common/pay', 't', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('4', 'swyktv5','reverseurl','模板','http://172.28.201.101:9116/epayapi/services/thirdparty/common/payreverse', 't', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('5', 'alipay', 'alipay.gateway', '支付宝网关地址', 'https://openapi.alipay.com/gateway.do', 't', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('6', 'alipay', 'enable.paymethod', '支付渠道', 'balance,moneyFund,debitCardExpress,bankPay,pcredit', 'f', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('7', 'alipay', 'notify.url', '异步通知地址', 'http://ykt.supwisdom.com:9116/epay/zfb/newnotify', 't', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('8', 'alipay', 'timeout.express', '支付超时时间', '5m', 't', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('9', 'alipay', 'alipay.public.key', '支付宝商户应用的支付宝公钥', NULL, 'f', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('10', 'alipay', 'alipay.rsa.private.key', '支付宝商户应用RSA私钥', NULL, 'f', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('11', 'alipay', 'alipay.rsa.public.key', '支付宝商户应用RSA公钥', NULL, 'f', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('12', 'alipay', 'alipay.appid', '支付宝应用APPID', NULL, 'f', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('13', 'citizenCard', 'ynrcc.agent.url', '农商行前置地址前缀', NULL, 't', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('15', 'citizenCard', 'ynrcc.agent.signkey', '农商行前置签名秘钥', NULL, 't', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('16', 'citizenCard', 'merchant.bankcardno', '商户银行卡号', NULL, 'f', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('17', 'citizenCard', 'merchant.bankaccname', '银行开户名', NULL, 'f', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('18', 'wechat', 'wechat.appid', '微信APPID', null, 'f', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('19', 'wechat', 'wechat.mechid', '微信商户号', NULL, 'f', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('20', 'wechat', 'wechat.mechkey', '微信商户支付秘钥', NULL, 'f', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('21', 'wechat', 'wechat.notify', '微信支付结果通知地址', NULL, 't', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('22', 'wechat', 'wechat.refund.certpath', '微信退款证书路径', NULL, 'f', '{tenantid}');
+INSERT INTO TB_SOURCETYPE_CONFIG (ID, SOURCETYPE,CONFIGID,CONFIG_NAME,CONFIG_VALUE,GLOBALFLAG, "tenantid")
+VALUES ('23', 'wechat', 'wechat.refund.certpwd', '微信退款证书密码', NULL, 'f', '{tenantid}');
+
+INSERT INTO "tb_syspara" ("paraid", "displayflag", "editflag", "lastsaved", "paraname", "paraunit", "paraval", "remark", "valuetype", "tenantid")
+VALUES (1, 'yes', 1, '20190514165658', '账户最大余额默认值', '元', '10000.0', '账户开户时的默认账户最大余额', 'amount', '{tenantid}');
+INSERT INTO "tb_syspara" ("paraid", "displayflag", "editflag", "lastsaved", "paraname", "paraunit", "paraval", "remark", "valuetype", "tenantid")
+VALUES (2, 'yes', 1, '20190514165658', '默认免密额度', '元', '100.0', '账户开户时的默认免密额度', 'amount', '{tenantid}');
+INSERT INTO "tb_syspara" ("paraid", "displayflag", "editflag", "lastsaved", "paraname", "paraunit", "paraval", "remark", "valuetype", "tenantid")
+VALUES (3, 'yes', 1, '20190514165658', '默认日累计额度', '元', '200.0', '账户余额支付时默认的日累计额度', 'amount', '{tenantid}');
+INSERT INTO "tb_syspara" ("paraid", "displayflag", "editflag", "lastsaved", "paraname", "paraunit", "paraval", "remark", "valuetype", "tenantid")
+VALUES (4, 'yes', 1, '20190514165658', '手机端用户过期时间', '秒', null, '手机端用户过期时间(秒)', 'decimal', '{tenantid}');
+INSERT INTO "tb_syspara" ("paraid", "displayflag", "editflag", "lastsaved", "paraname", "paraunit", "paraval", "remark", "valuetype", "tenantid")
+VALUES (5, 'yes', 1, '20190514165658', '商户信息维护是否需要审核', '', '0', '商户的新增、修改、删除是否需要审核:1-需要;0-不需要', 'switch', '{tenantid}');
+
+INSERT INTO "tb_syspara" ("paraid", "displayflag", "editflag", "lastsaved", "paraname", "paraunit", "paraval", "remark", "valuetype", "tenantid")
+VALUES (2019, 'yes', 1, '20190514165658', '与卡管系统对接的应用ID', null, null, '与卡管系统对接的app_id', 'string', '{tenantid}');
+INSERT INTO "tb_syspara" ("paraid", "displayflag", "editflag", "lastsaved", "paraname", "paraunit", "paraval", "remark", "valuetype", "tenantid")
+VALUES (2020, 'yes', 1, '20190514165658', '与卡管系统对接的签名秘钥', null, null, '与卡管系统对接的appkey', 'string', '{tenantid}');
+INSERT INTO "tb_syspara" ("paraid", "displayflag", "editflag", "lastsaved", "paraname", "paraunit", "paraval", "remark", "valuetype", "tenantid")
+VALUES (2021, 'yes', 1, '20190514165658', '与卡管系统对接的业务参数加密秘钥', null, null, '与卡管系统对接中业务参数加密的deskey', 'string', '{tenantid}');
+
+
+INSERT INTO "tb_task_lock" ("taskcode", "remark", "taskstatus", "tasktime", "tenantid")
+VALUES ('DAYENDSETTLETASK', '日终结算', '0', '20190619100600', '{tenantid}');
+
+INSERT INTO "tb_shop" ("shopid", "shopname", "shoptype", "fshopid", "status", "check_status", "opendate", "tenantid")
+VALUES (1, '支付中心', 'root', 0, 'normal', 'normal', '20190517', '{tenantid}');
+
+INSERT INTO "tb_transcode" ("transcode_id", "transcode", "transname", "tenantid")
+VALUES (1, 3010, '市民卡代扣', '{tenantid}');
+INSERT INTO "tb_transcode" ("transcode_id", "transcode", "transname", "tenantid")
+VALUES (2, 1002, '支付码聚合付', '{tenantid}');
+INSERT INTO "tb_transcode" ("transcode_id", "transcode", "transname", "tenantid")
+VALUES (3, 3500, '账户充值', '{tenantid}');
+
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (1, 'cancel', 'reverseFlagList', '被撤销', '冲正状态', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (2, 'refund', 'reverseFlagList', '被退款', '冲正状态', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (3, 'none', 'reverseFlagList', '-', '冲正状态', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (4, 'init', 'dtlStatusList', '初始化', '流水状态', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (5, 'success', 'dtlStatusList', '交易成功', '流水状态', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (6, 'fail', 'dtlStatusList', '交易失败', '流水状态', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (7, 'wip', 'dtlStatusList','待支付', '流水状态', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (8, 'cancel','dtlStatusList', '交易取消', '流水状态', '{tenantid}');
+
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (9, 'idcard', 'idtypeList', '身份证', '证件类型', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (10, 'residence_booklet', 'idtypeList', '户口簿', '证件类型', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (11, 'passport', 'idtypeList', '护照', '证件类型', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (12, 'hk_macau_pass', 'idtypeList', '港澳居民来往内地通行证', '证件类型', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (13, 'taiwan_pass', 'idtypeList', '台湾同胞来往内地通行证', '证件类型', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (14, 'foreigner_residence_permit', 'idtypeList', '外国人居留证', '证件类型', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (15, 'military_idcard', 'idtypeList', '军官证', '证件类型', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (16, 'soldier_idcard', 'idtypeList', '士兵证', '证件类型', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (17, 'driving_license', 'idtypeList', '驾照', '证件类型', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (18, 'student_idcard', 'idtypeList', '学工号', '证件类型', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (19, 'unknown', 'idtypeList', '其他', '证件类型', '{tenantid}');
+
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (20, 'male', 'sexList', '男', '性别', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (21, 'female', 'sexList', '女', '性别', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (22, 'unknown', 'sexList', '未知', '性别', '{tenantid}');
+
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (23, 'normal', 'accountStatusList', '正常', '账户状态', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (24, 'closed', 'accountStatusList', '注销', '账户状态', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (25, 'locked', 'accountStatusList', '锁定', '账户状态', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (26, 'unknown', 'accountStatusList', '异常', '账户状态', '{tenantid}');
+
+
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (27, 'water', 'dtltypeList', '生活用水', '流水类型', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (28, 'carbus', 'dtltypeList', '乘车', '流水类型', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (29, 'canteen', 'dtltypeList', '食堂就餐', '流水类型', '{tenantid}');
+INSERT INTO "tb_dictionary" ("id", "dictval", "dicttype", "dictcaption", "dicttypename", "tenantid")
+VALUES (30, 'shopmarket', 'dtltypeList', '商超消费', '流水类型', '{tenantid}');
+
+
+
+INSERT INTO TB_QRCODE_PATTERN(ID, PATTERN, SOURCETYPE, TENANTID)
+VALUES(1, '28\d{16}', 'alipay', '{tenantid}');
+INSERT INTO TB_QRCODE_PATTERN(ID, PATTERN, SOURCETYPE, TENANTID)
+VALUES(2, '13\d{16}', 'wechat', '{tenantid}');
+----------------------------------------------------
+commit;
\ No newline at end of file
diff --git a/backend/src/main/resources/templates/index.html b/backend/src/main/resources/templates/index.html
new file mode 100644
index 0000000..566549b
--- /dev/null
+++ b/backend/src/main/resources/templates/index.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Title</title>
+</head>
+<body>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/backend/src/main/webapp/WEB-INF/web.xml b/backend/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..bdc24e9
--- /dev/null
+++ b/backend/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
+         version="4.0">
+
+</web-app>
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..95d82f4
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,11 @@
+plugins {
+    id 'java'
+    id 'org.jetbrains.kotlin.jvm' version '1.3.61' apply false
+    id 'org.springframework.boot' version '2.1.6.RELEASE' apply false
+    id 'org.jetbrains.kotlin.plugin.spring' version '1.3.31' apply false
+    id 'io.spring.dependency-management' version '1.0.8.RELEASE' apply false
+}
+
+group 'com.supwisdom.dlpay'
+version '1.0-SNAPSHOT'
+
diff --git a/config/application-devel-pg.properties b/config/application-devel-pg.properties
new file mode 100644
index 0000000..4346bdc
--- /dev/null
+++ b/config/application-devel-pg.properties
@@ -0,0 +1,58 @@
+spring.main.banner-mode=off
+# create and drop tables and sequences, loads import.sql
+spring.jpa.hibernate.ddl-auto=update
+spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
+spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
+# Postgresql settings
+spring.datasource.platform=postgresql
+#spring.datasource.url=jdbc:postgresql://ykt.supwisdom.com:15432/payapidev
+spring.datasource.url=jdbc:postgresql://172.28.201.70:15432/payapidev
+spring.datasource.username=payapi
+spring.datasource.password=123456
+spring.datasource.continue-on-error=true
+spring.datasource.initialization-mode=always
+# Redis settings
+#spring.redis.host=ykt.supwisdom.com
+spring.redis.host=172.28.201.70
+spring.redis.port=6379
+spring.redis.password=
+spring.redis.database=2
+# jwt settings
+jwt.secret=Zj5taLomEbrM0lk+NMQZbHfSxaDU1wekjT+kiC3YzDw=
+# timeout seconds
+jwt.expiration=3600
+auth.password.bcrypt.seed=
+spring.jackson.serialization.fail-on-empty-beans=false
+logging.level.org.springframework.web=DEBUG
+security.request.sign=false
+##################################################
+## quartz task scheduler
+shopbalance.updater.cron=-
+##################多租户配置 end################################
+#############################################
+spring.cloud.consul.enabled=false
+spring.cloud.consul.host=172.28.201.70
+spring.cloud.consul.port=8500
+
+#============== kafka ===================
+# 指定kafka 代理地址,可以多个
+spring.kafka.bootstrap-servers=172.28.201.101:9192
+#=============== provider  =======================
+spring.kafka.producer.retries=3
+# 每次批量发送消息的数量
+spring.kafka.producer.batch-size=16384
+spring.kafka.producer.buffer-memory=33554432
+# 指定消息key和消息体的编解码方式
+spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
+spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
+
+#===============kafka consumer  =======================
+# 指定默认消费者group id
+spring.kafka.listen.auto.start=false
+spring.kafka.consumer.group-id=epaymessager1
+spring.kafka.consumer.auto-offset-reset=earliest
+spring.kafka.consumer.enable-auto-commit=true
+spring.kafka.consumer.auto-commit-interval=100
+# 指定消息key和消息体的编解码方式
+spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
+spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
\ No newline at end of file
diff --git a/frontend/.editorconfig b/frontend/.editorconfig
new file mode 100644
index 0000000..3454886
--- /dev/null
+++ b/frontend/.editorconfig
@@ -0,0 +1,14 @@
+# https://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+insert_final_newline = false
+trim_trailing_whitespace = false
diff --git a/frontend/.env.development b/frontend/.env.development
new file mode 100644
index 0000000..de583d0
--- /dev/null
+++ b/frontend/.env.development
@@ -0,0 +1,5 @@
+# just a flag
+ENV = 'development'
+
+# base api
+VUE_APP_BASE_API = '/dev-api'
diff --git a/frontend/.env.production b/frontend/.env.production
new file mode 100644
index 0000000..80c8103
--- /dev/null
+++ b/frontend/.env.production
@@ -0,0 +1,6 @@
+# just a flag
+ENV = 'production'
+
+# base api
+VUE_APP_BASE_API = '/prod-api'
+
diff --git a/frontend/.env.staging b/frontend/.env.staging
new file mode 100644
index 0000000..a8793a0
--- /dev/null
+++ b/frontend/.env.staging
@@ -0,0 +1,8 @@
+NODE_ENV = production
+
+# just a flag
+ENV = 'staging'
+
+# base api
+VUE_APP_BASE_API = '/stage-api'
+
diff --git a/frontend/.eslintignore b/frontend/.eslintignore
new file mode 100644
index 0000000..e6529fc
--- /dev/null
+++ b/frontend/.eslintignore
@@ -0,0 +1,4 @@
+build/*.js
+src/assets
+public
+dist
diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js
new file mode 100644
index 0000000..c977505
--- /dev/null
+++ b/frontend/.eslintrc.js
@@ -0,0 +1,198 @@
+module.exports = {
+  root: true,
+  parserOptions: {
+    parser: 'babel-eslint',
+    sourceType: 'module'
+  },
+  env: {
+    browser: true,
+    node: true,
+    es6: true,
+  },
+  extends: ['plugin:vue/recommended', 'eslint:recommended'],
+
+  // add your custom rules here
+  //it is base on https://github.com/vuejs/eslint-config-vue
+  rules: {
+    "vue/max-attributes-per-line": [2, {
+      "singleline": 10,
+      "multiline": {
+        "max": 1,
+        "allowFirstLine": false
+      }
+    }],
+    "vue/singleline-html-element-content-newline": "off",
+    "vue/multiline-html-element-content-newline":"off",
+    "vue/name-property-casing": ["error", "PascalCase"],
+    "vue/no-v-html": "off",
+    'accessor-pairs': 2,
+    'arrow-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'block-spacing': [2, 'always'],
+    'brace-style': [2, '1tbs', {
+      'allowSingleLine': true
+    }],
+    'camelcase': [0, {
+      'properties': 'always'
+    }],
+    'comma-dangle': [2, 'never'],
+    'comma-spacing': [2, {
+      'before': false,
+      'after': true
+    }],
+    'comma-style': [2, 'last'],
+    'constructor-super': 2,
+    'curly': [2, 'multi-line'],
+    'dot-location': [2, 'property'],
+    'eol-last': 2,
+    'eqeqeq': ["error", "always", {"null": "ignore"}],
+    'generator-star-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'handle-callback-err': [2, '^(err|error)$'],
+    'indent': [2, 2, {
+      'SwitchCase': 1
+    }],
+    'jsx-quotes': [2, 'prefer-single'],
+    'key-spacing': [2, {
+      'beforeColon': false,
+      'afterColon': true
+    }],
+    'keyword-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'new-cap': [2, {
+      'newIsCap': true,
+      'capIsNew': false
+    }],
+    'new-parens': 2,
+    'no-array-constructor': 2,
+    'no-caller': 2,
+    'no-console': 'off',
+    'no-class-assign': 2,
+    'no-cond-assign': 2,
+    'no-const-assign': 2,
+    'no-control-regex': 0,
+    'no-delete-var': 2,
+    'no-dupe-args': 2,
+    'no-dupe-class-members': 2,
+    'no-dupe-keys': 2,
+    'no-duplicate-case': 2,
+    'no-empty-character-class': 2,
+    'no-empty-pattern': 2,
+    'no-eval': 2,
+    'no-ex-assign': 2,
+    'no-extend-native': 2,
+    'no-extra-bind': 2,
+    'no-extra-boolean-cast': 2,
+    'no-extra-parens': [2, 'functions'],
+    'no-fallthrough': 2,
+    'no-floating-decimal': 2,
+    'no-func-assign': 2,
+    'no-implied-eval': 2,
+    'no-inner-declarations': [2, 'functions'],
+    'no-invalid-regexp': 2,
+    'no-irregular-whitespace': 2,
+    'no-iterator': 2,
+    'no-label-var': 2,
+    'no-labels': [2, {
+      'allowLoop': false,
+      'allowSwitch': false
+    }],
+    'no-lone-blocks': 2,
+    'no-mixed-spaces-and-tabs': 2,
+    'no-multi-spaces': 2,
+    'no-multi-str': 2,
+    'no-multiple-empty-lines': [2, {
+      'max': 1
+    }],
+    'no-native-reassign': 2,
+    'no-negated-in-lhs': 2,
+    'no-new-object': 2,
+    'no-new-require': 2,
+    'no-new-symbol': 2,
+    'no-new-wrappers': 2,
+    'no-obj-calls': 2,
+    'no-octal': 2,
+    'no-octal-escape': 2,
+    'no-path-concat': 2,
+    'no-proto': 2,
+    'no-redeclare': 2,
+    'no-regex-spaces': 2,
+    'no-return-assign': [2, 'except-parens'],
+    'no-self-assign': 2,
+    'no-self-compare': 2,
+    'no-sequences': 2,
+    'no-shadow-restricted-names': 2,
+    'no-spaced-func': 2,
+    'no-sparse-arrays': 2,
+    'no-this-before-super': 2,
+    'no-throw-literal': 2,
+    'no-trailing-spaces': 2,
+    'no-undef': 2,
+    'no-undef-init': 2,
+    'no-unexpected-multiline': 2,
+    'no-unmodified-loop-condition': 2,
+    'no-unneeded-ternary': [2, {
+      'defaultAssignment': false
+    }],
+    'no-unreachable': 2,
+    'no-unsafe-finally': 2,
+    'no-unused-vars': [2, {
+      'vars': 'all',
+      'args': 'none'
+    }],
+    'no-useless-call': 2,
+    'no-useless-computed-key': 2,
+    'no-useless-constructor': 2,
+    'no-useless-escape': 0,
+    'no-whitespace-before-property': 2,
+    'no-with': 2,
+    'one-var': [2, {
+      'initialized': 'never'
+    }],
+    'operator-linebreak': [2, 'after', {
+      'overrides': {
+        '?': 'before',
+        ':': 'before'
+      }
+    }],
+    'padded-blocks': [2, 'never'],
+    'quotes': [2, 'single', {
+      'avoidEscape': true,
+      'allowTemplateLiterals': true
+    }],
+    'semi': [2, 'never'],
+    'semi-spacing': [2, {
+      'before': false,
+      'after': true
+    }],
+    'space-before-blocks': [2, 'always'],
+    'space-before-function-paren': [2, 'never'],
+    'space-in-parens': [2, 'never'],
+    'space-infix-ops': 2,
+    'space-unary-ops': [2, {
+      'words': true,
+      'nonwords': false
+    }],
+    'spaced-comment': [2, 'always', {
+      'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
+    }],
+    'template-curly-spacing': [2, 'never'],
+    'use-isnan': 2,
+    'valid-typeof': 2,
+    'wrap-iife': [2, 'any'],
+    'yield-star-spacing': [2, 'both'],
+    'yoda': [2, 'never'],
+    'prefer-const': 2,
+    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
+    'object-curly-spacing': [2, 'always', {
+      objectsInObjects: false
+    }],
+    'array-bracket-spacing': [2, 'never']
+  }
+}
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..78a752d
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,23 @@
+.DS_Store
+node_modules/
+dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+**/*.log
+
+tests/**/coverage/
+tests/e2e/reports
+selenium-debug.log
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.local
+
+package-lock.json
+yarn.lock
diff --git a/frontend/.travis.yml b/frontend/.travis.yml
new file mode 100644
index 0000000..f4be7a0
--- /dev/null
+++ b/frontend/.travis.yml
@@ -0,0 +1,5 @@
+language: node_js
+node_js: 10
+script: npm run test
+notifications:
+  email: false
diff --git a/frontend/LICENSE b/frontend/LICENSE
new file mode 100644
index 0000000..6151575
--- /dev/null
+++ b/frontend/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017-present PanJiaChen
+
+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.
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..5067b8d
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,222 @@
+<p align="center">
+  <img width="320" src="https://wpimg.wallstcn.com/ecc53a42-d79b-42e2-8852-5126b810a4c8.svg">
+</p>
+
+<p align="center">
+  <a href="https://github.com/vuejs/vue">
+    <img src="https://img.shields.io/badge/vue-2.6.10-brightgreen.svg" alt="vue">
+  </a>
+  <a href="https://github.com/ElemeFE/element">
+    <img src="https://img.shields.io/badge/element--ui-2.7.0-brightgreen.svg" alt="element-ui">
+  </a>
+  <a href="https://travis-ci.org/PanJiaChen/vue-element-admin" rel="nofollow">
+    <img src="https://travis-ci.org/PanJiaChen/vue-element-admin.svg?branch=master" alt="Build Status">
+  </a>
+  <a href="https://github.com/PanJiaChen/vue-element-admin/blob/master/LICENSE">
+    <img src="https://img.shields.io/github/license/mashape/apistatus.svg" alt="license">
+  </a>
+  <a href="https://github.com/PanJiaChen/vue-element-admin/releases">
+    <img src="https://img.shields.io/github/release/PanJiaChen/vue-element-admin.svg" alt="GitHub release">
+  </a>
+  <a href="https://gitter.im/vue-element-admin/discuss">
+    <img src="https://badges.gitter.im/Join%20Chat.svg" alt="gitter">
+  </a>
+  <a href="https://panjiachen.github.io/vue-element-admin-site/donate">
+    <img src="https://img.shields.io/badge/%24-donate-ff69b4.svg" alt="donate">
+  </a>
+</p>
+
+English | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md) | [Spanish](./README.es.md)
+
+## Introduction
+
+[vue-element-admin](https://panjiachen.github.io/vue-element-admin) is a production-ready front-end solution for admin interfaces. It is based on [vue](https://github.com/vuejs/vue) and uses the UI Toolkit [element-ui](https://github.com/ElemeFE/element).
+
+[vue-element-admin](https://panjiachen.github.io/vue-element-admin) is based on the newest development stack of vue and it has a built-in i18n solution, typical templates for enterprise applications, and lots of awesome features. It helps you build large and complex Single-Page Applications. I believe whatever your needs are, this project will help you.
+
+- [Preview](https://panjiachen.github.io/vue-element-admin)
+
+- [Documentation](https://panjiachen.github.io/vue-element-admin-site/)
+
+- [Gitter](https://gitter.im/vue-element-admin/discuss)
+
+- [Donate](https://panjiachen.github.io/vue-element-admin-site/donate/)
+
+- [Wiki](https://github.com/PanJiaChen/vue-element-admin/wiki)
+
+- [Gitee](https://panjiachen.gitee.io/vue-element-admin/) 国内用户可访问该地址在线预览
+
+- Base template recommends using: [vue-admin-template](https://github.com/PanJiaChen/vue-admin-template)
+- Desktop: [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
+- Typescript: [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour))
+- [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312)
+
+**After the `v4.1.0+` version, the default master branch will not support i18n. Please use [i18n Branch](https://github.com/PanJiaChen/vue-element-admin/tree/i18n), it will keep up with the master update**
+
+**The current version is `v4.0+` build on `vue-cli`. If you find a problem, please put [issue](https://github.com/PanJiaChen/vue-element-admin/issues/new). If you want to use the old version , you can switch branch to [tag/3.11.0](https://github.com/PanJiaChen/vue-element-admin/tree/tag/3.11.0), it does not rely on `vue-cli`**
+
+**This project does not support low version browsers (e.g. IE). Please add polyfill by yourself.**
+
+## Preparation
+
+You need to install [node](https://nodejs.org/) and [git](https://git-scm.com/) locally. The project is based on [ES2015+](https://es6.ruanyifeng.com/), [vue](https://cn.vuejs.org/index.html), [vuex](https://vuex.vuejs.org/zh-cn/), [vue-router](https://router.vuejs.org/zh-cn/), [vue-cli](https://github.com/vuejs/vue-cli) , [axios](https://github.com/axios/axios) and [element-ui](https://github.com/ElemeFE/element), all request data is simulated using [Mock.js](https://github.com/nuysoft/Mock).
+Understanding and learning this knowledge in advance will greatly help the use of this project.
+
+[![Edit on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/PanJiaChen/vue-element-admin/tree/CodeSandbox)
+
+<p align="center">
+  <img width="900" src="https://wpimg.wallstcn.com/a5894c1b-f6af-456e-82df-1151da0839bf.png">
+</p>
+
+## Sponsors
+
+Become a sponsor and get your logo on our README on GitHub with a link to your site. [[Become a sponsor]](https://www.patreon.com/panjiachen)
+
+<a href="https://flatlogic.com/admin-dashboards?from=vue-element-admin"><img width="150px" src="https://wpimg.wallstcn.com/9c0b719b-5551-4c1e-b776-63994632d94a.png" /></a><p>Admin Dashboard Templates made with Vue, React and Angular.</p>
+
+## Features
+
+```
+- Login / Logout
+
+- Permission Authentication
+  - Page permission
+  - Directive permission
+  - Permission configuration page
+  - Two-step login
+
+- Multi-environment build
+  - Develop (dev)
+  - sit
+  - Stage Test (stage)
+  - Production (prod)
+
+- Global Features
+  - I18n
+  - Multiple dynamic themes
+  - Dynamic sidebar (supports multi-level routing)
+  - Dynamic breadcrumb
+  - Tags-view (Tab page Support right-click operation)
+  - Svg Sprite
+  - Mock data
+  - Screenfull
+  - Responsive Sidebar
+
+- Editor
+  - Rich Text Editor
+  - Markdown Editor
+  - JSON Editor
+
+- Excel
+  - Export Excel
+  - Upload Excel
+  - Visualization Excel
+  - Export zip
+
+- Table
+  - Dynamic Table
+  - Drag And Drop Table
+  - Inline Edit Table
+
+- Error Page
+  - 401
+  - 404
+
+- Components
+  - Avatar Upload
+  - Back To Top
+  - Drag Dialog
+  - Drag Select
+  - Drag Kanban
+  - Drag List
+  - SplitPane
+  - Dropzone
+  - Sticky
+  - CountTo
+
+- Advanced Example
+- Error Log
+- Dashboard
+- Guide Page
+- ECharts
+- Clipboard
+- Markdown to html
+```
+
+## Getting started
+
+```bash
+# clone the project
+git clone https://github.com/PanJiaChen/vue-element-admin.git
+
+# enter the project directory
+cd vue-element-admin
+
+# install dependency
+npm install
+
+# develop
+npm run dev
+```
+
+This will automatically open http://localhost:9527
+
+## Build
+
+```bash
+# build for test environment
+npm run build:stage
+
+# build for production environment
+npm run build:prod
+```
+
+## Advanced
+
+```bash
+# preview the release environment effect
+npm run preview
+
+# preview the release environment effect + static resource analysis
+npm run preview -- --report
+
+# code format check
+npm run lint
+
+# code format check and auto fix
+npm run lint -- --fix
+```
+
+Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information
+
+## Changelog
+
+Detailed changes for each release are documented in the [release notes](https://github.com/PanJiaChen/vue-element-admin/releases).
+
+## Online Demo
+
+[Preview](https://panjiachen.github.io/vue-element-admin)
+
+## Donate
+
+If you find this project useful, you can buy author a glass of juice :tropical_drink:
+
+![donate](https://wpimg.wallstcn.com/bd273f0d-83a0-4ef2-92e1-9ac8ed3746b9.png)
+
+[Paypal Me](https://www.paypal.me/panfree23)
+
+[Buy me a coffee](https://www.buymeacoffee.com/Pan)
+
+## Browsers support
+
+Modern browsers and Internet Explorer 10+.
+
+| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](https://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](https://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](https://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](https://godban.github.io/browsers-support-badges/)</br>Safari |
+| --------- | --------- | --------- | --------- |
+| IE10, IE11, Edge | last 2 versions | last 2 versions | last 2 versions |
+
+## License
+
+[MIT](https://github.com/PanJiaChen/vue-element-admin/blob/master/LICENSE)
+
+Copyright (c) 2017-present PanJiaChen
diff --git a/frontend/babel.config.js b/frontend/babel.config.js
new file mode 100644
index 0000000..fb82b27
--- /dev/null
+++ b/frontend/babel.config.js
@@ -0,0 +1,14 @@
+module.exports = {
+  presets: [
+    // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
+    '@vue/cli-plugin-babel/preset'
+  ],
+  'env': {
+    'development': {
+      // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
+      // This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
+      // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html
+      'plugins': ['dynamic-import-node']
+    }
+  }
+}
diff --git a/frontend/jest.config.js b/frontend/jest.config.js
new file mode 100644
index 0000000..143cdc8
--- /dev/null
+++ b/frontend/jest.config.js
@@ -0,0 +1,24 @@
+module.exports = {
+  moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
+  transform: {
+    '^.+\\.vue$': 'vue-jest',
+    '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
+      'jest-transform-stub',
+    '^.+\\.jsx?$': 'babel-jest'
+  },
+  moduleNameMapper: {
+    '^@/(.*)$': '<rootDir>/src/$1'
+  },
+  snapshotSerializers: ['jest-serializer-vue'],
+  testMatch: [
+    '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
+  ],
+  collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
+  coverageDirectory: '<rootDir>/tests/unit/coverage',
+  // 'collectCoverage': true,
+  'coverageReporters': [
+    'lcov',
+    'text-summary'
+  ],
+  testURL: 'http://localhost/'
+}
diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json
new file mode 100644
index 0000000..958df04
--- /dev/null
+++ b/frontend/jsconfig.json
@@ -0,0 +1,9 @@
+{ 
+  "compilerOptions": {
+    "baseUrl": "./",
+    "paths": {
+        "@/*": ["src/*"]
+    }
+  },
+  "exclude": ["node_modules", "dist"]
+}
\ No newline at end of file
diff --git a/frontend/mock/article.js b/frontend/mock/article.js
new file mode 100644
index 0000000..23d8ba5
--- /dev/null
+++ b/frontend/mock/article.js
@@ -0,0 +1,116 @@
+const Mock = require('mockjs')
+
+const List = []
+const count = 100
+
+const baseContent = '<p>I am testing data, I am testing data.</p><p><img src="https://wpimg.wallstcn.com/4c69009c-0fd4-4153-b112-6cb53d1cf943"></p>'
+const image_uri = 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3'
+
+for (let i = 0; i < count; i++) {
+  List.push(Mock.mock({
+    id: '@increment',
+    timestamp: +Mock.Random.date('T'),
+    author: '@first',
+    reviewer: '@first',
+    title: '@title(5, 10)',
+    content_short: 'mock data',
+    content: baseContent,
+    forecast: '@float(0, 100, 2, 2)',
+    importance: '@integer(1, 3)',
+    'type|1': ['CN', 'US', 'JP', 'EU'],
+    'status|1': ['published', 'draft'],
+    display_time: '@datetime',
+    comment_disabled: true,
+    pageviews: '@integer(300, 5000)',
+    image_uri,
+    platforms: ['a-platform']
+  }))
+}
+
+module.exports = [
+  {
+    url: '/vue-element-admin/article/list',
+    type: 'get',
+    response: config => {
+      const { importance, type, title, page = 1, limit = 20, sort } = config.query
+
+      let mockList = List.filter(item => {
+        if (importance && item.importance !== +importance) return false
+        if (type && item.type !== type) return false
+        if (title && item.title.indexOf(title) < 0) return false
+        return true
+      })
+
+      if (sort === '-id') {
+        mockList = mockList.reverse()
+      }
+
+      const pageList = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1))
+
+      return {
+        code: 20000,
+        data: {
+          total: mockList.length,
+          items: pageList
+        }
+      }
+    }
+  },
+
+  {
+    url: '/vue-element-admin/article/detail',
+    type: 'get',
+    response: config => {
+      const { id } = config.query
+      for (const article of List) {
+        if (article.id === +id) {
+          return {
+            code: 20000,
+            data: article
+          }
+        }
+      }
+    }
+  },
+
+  {
+    url: '/vue-element-admin/article/pv',
+    type: 'get',
+    response: _ => {
+      return {
+        code: 20000,
+        data: {
+          pvData: [
+            { key: 'PC', pv: 1024 },
+            { key: 'mobile', pv: 1024 },
+            { key: 'ios', pv: 1024 },
+            { key: 'android', pv: 1024 }
+          ]
+        }
+      }
+    }
+  },
+
+  {
+    url: '/vue-element-admin/article/create',
+    type: 'post',
+    response: _ => {
+      return {
+        code: 20000,
+        data: 'success'
+      }
+    }
+  },
+
+  {
+    url: '/vue-element-admin/article/update',
+    type: 'post',
+    response: _ => {
+      return {
+        code: 20000,
+        data: 'success'
+      }
+    }
+  }
+]
+
diff --git a/frontend/mock/index.js b/frontend/mock/index.js
new file mode 100644
index 0000000..2eed65d
--- /dev/null
+++ b/frontend/mock/index.js
@@ -0,0 +1,60 @@
+const Mock = require('mockjs')
+const { param2Obj } = require('./utils')
+
+const user = require('./user')
+const role = require('./role')
+const article = require('./article')
+const search = require('./remote-search')
+
+const mocks = [
+  ...user,
+  ...role,
+  ...article,
+  ...search
+]
+
+// for front mock
+// please use it cautiously, it will redefine XMLHttpRequest,
+// which will cause many of your third-party libraries to be invalidated(like progress event).
+function mockXHR() {
+  // mock patch
+  // https://github.com/nuysoft/Mock/issues/300
+  Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
+  Mock.XHR.prototype.send = function() {
+    if (this.custom.xhr) {
+      this.custom.xhr.withCredentials = this.withCredentials || false
+
+      if (this.responseType) {
+        this.custom.xhr.responseType = this.responseType
+      }
+    }
+    this.proxy_send(...arguments)
+  }
+
+  function XHR2ExpressReqWrap(respond) {
+    return function(options) {
+      let result = null
+      if (respond instanceof Function) {
+        const { body, type, url } = options
+        // https://expressjs.com/en/4x/api.html#req
+        result = respond({
+          method: type,
+          body: JSON.parse(body),
+          query: param2Obj(url)
+        })
+      } else {
+        result = respond
+      }
+      return Mock.mock(result)
+    }
+  }
+
+  for (const i of mocks) {
+    Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
+  }
+}
+
+module.exports = {
+  mocks,
+  mockXHR
+}
diff --git a/frontend/mock/mock-server.js b/frontend/mock/mock-server.js
new file mode 100644
index 0000000..8941ec0
--- /dev/null
+++ b/frontend/mock/mock-server.js
@@ -0,0 +1,81 @@
+const chokidar = require('chokidar')
+const bodyParser = require('body-parser')
+const chalk = require('chalk')
+const path = require('path')
+const Mock = require('mockjs')
+
+const mockDir = path.join(process.cwd(), 'mock')
+
+function registerRoutes(app) {
+  let mockLastIndex
+  const { mocks } = require('./index.js')
+  const mocksForServer = mocks.map(route => {
+    return responseFake(route.url, route.type, route.response)
+  })
+  for (const mock of mocksForServer) {
+    app[mock.type](mock.url, mock.response)
+    mockLastIndex = app._router.stack.length
+  }
+  const mockRoutesLength = Object.keys(mocksForServer).length
+  return {
+    mockRoutesLength: mockRoutesLength,
+    mockStartIndex: mockLastIndex - mockRoutesLength
+  }
+}
+
+function unregisterRoutes() {
+  Object.keys(require.cache).forEach(i => {
+    if (i.includes(mockDir)) {
+      delete require.cache[require.resolve(i)]
+    }
+  })
+}
+
+// for mock server
+const responseFake = (url, type, respond) => {
+  return {
+    url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`),
+    type: type || 'get',
+    response(req, res) {
+      console.log('request invoke:' + req.path)
+      res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
+    }
+  }
+}
+
+module.exports = app => {
+  // parse app.body
+  // https://expressjs.com/en/4x/api.html#req.body
+  app.use(bodyParser.json())
+  app.use(bodyParser.urlencoded({
+    extended: true
+  }))
+
+  const mockRoutes = registerRoutes(app)
+  var mockRoutesLength = mockRoutes.mockRoutesLength
+  var mockStartIndex = mockRoutes.mockStartIndex
+
+  // watch files, hot reload mock server
+  chokidar.watch(mockDir, {
+    ignored: /mock-server/,
+    ignoreInitial: true
+  }).on('all', (event, path) => {
+    if (event === 'change' || event === 'add') {
+      try {
+        // remove mock routes stack
+        app._router.stack.splice(mockStartIndex, mockRoutesLength)
+
+        // clear routes cache
+        unregisterRoutes()
+
+        const mockRoutes = registerRoutes(app)
+        mockRoutesLength = mockRoutes.mockRoutesLength
+        mockStartIndex = mockRoutes.mockStartIndex
+
+        console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed  ${path}`))
+      } catch (error) {
+        console.log(chalk.redBright(error))
+      }
+    }
+  })
+}
diff --git a/frontend/mock/remote-search.js b/frontend/mock/remote-search.js
new file mode 100644
index 0000000..8fc4926
--- /dev/null
+++ b/frontend/mock/remote-search.js
@@ -0,0 +1,51 @@
+const Mock = require('mockjs')
+
+const NameList = []
+const count = 100
+
+for (let i = 0; i < count; i++) {
+  NameList.push(Mock.mock({
+    name: '@first'
+  }))
+}
+NameList.push({ name: 'mock-Pan' })
+
+module.exports = [
+  // username search
+  {
+    url: '/vue-element-admin/search/user',
+    type: 'get',
+    response: config => {
+      const { name } = config.query
+      const mockNameList = NameList.filter(item => {
+        const lowerCaseName = item.name.toLowerCase()
+        return !(name && lowerCaseName.indexOf(name.toLowerCase()) < 0)
+      })
+      return {
+        code: 20000,
+        data: { items: mockNameList }
+      }
+    }
+  },
+
+  // transaction list
+  {
+    url: '/vue-element-admin/transaction/list',
+    type: 'get',
+    response: _ => {
+      return {
+        code: 20000,
+        data: {
+          total: 20,
+          'items|20': [{
+            order_no: '@guid()',
+            timestamp: +Mock.Random.date('T'),
+            username: '@name()',
+            price: '@float(1000, 15000, 0, 2)',
+            'status|1': ['success', 'pending']
+          }]
+        }
+      }
+    }
+  }
+]
diff --git a/frontend/mock/role/index.js b/frontend/mock/role/index.js
new file mode 100644
index 0000000..4643f00
--- /dev/null
+++ b/frontend/mock/role/index.js
@@ -0,0 +1,98 @@
+const Mock = require('mockjs')
+const { deepClone } = require('../utils')
+const { asyncRoutes, constantRoutes } = require('./routes.js')
+
+const routes = deepClone([...constantRoutes, ...asyncRoutes])
+
+const roles = [
+  {
+    key: 'admin',
+    name: 'admin',
+    description: 'Super Administrator. Have access to view all pages.',
+    routes: routes
+  },
+  {
+    key: 'editor',
+    name: 'editor',
+    description: 'Normal Editor. Can see all pages except permission page',
+    routes: routes.filter(i => i.path !== '/permission')// just a mock
+  },
+  {
+    key: 'visitor',
+    name: 'visitor',
+    description: 'Just a visitor. Can only see the home page and the document page',
+    routes: [{
+      path: '',
+      redirect: 'dashboard',
+      children: [
+        {
+          path: 'dashboard',
+          name: 'Dashboard',
+          meta: { title: 'dashboard', icon: 'dashboard' }
+        }
+      ]
+    }]
+  }
+]
+
+module.exports = [
+  // mock get all routes form server
+  {
+    url: '/vue-element-admin/routes',
+    type: 'get',
+    response: _ => {
+      return {
+        code: 20000,
+        data: routes
+      }
+    }
+  },
+
+  // mock get all roles form server
+  {
+    url: '/vue-element-admin/roles',
+    type: 'get',
+    response: _ => {
+      return {
+        code: 20000,
+        data: roles
+      }
+    }
+  },
+
+  // add role
+  {
+    url: '/vue-element-admin/role',
+    type: 'post',
+    response: {
+      code: 20000,
+      data: {
+        key: Mock.mock('@integer(300, 5000)')
+      }
+    }
+  },
+
+  // update role
+  {
+    url: '/vue-element-admin/role/[A-Za-z0-9]',
+    type: 'put',
+    response: {
+      code: 20000,
+      data: {
+        status: 'success'
+      }
+    }
+  },
+
+  // delete role
+  {
+    url: '/vue-element-admin/role/[A-Za-z0-9]',
+    type: 'delete',
+    response: {
+      code: 20000,
+      data: {
+        status: 'success'
+      }
+    }
+  }
+]
diff --git a/frontend/mock/role/routes.js b/frontend/mock/role/routes.js
new file mode 100644
index 0000000..d33f162
--- /dev/null
+++ b/frontend/mock/role/routes.js
@@ -0,0 +1,530 @@
+// Just a mock data
+
+const constantRoutes = [
+  {
+    path: '/redirect',
+    component: 'layout/Layout',
+    hidden: true,
+    children: [
+      {
+        path: '/redirect/:path*',
+        component: 'views/redirect/index'
+      }
+    ]
+  },
+  {
+    path: '/login',
+    component: 'views/login/index',
+    hidden: true
+  },
+  {
+    path: '/auth-redirect',
+    component: 'views/login/auth-redirect',
+    hidden: true
+  },
+  {
+    path: '/404',
+    component: 'views/error-page/404',
+    hidden: true
+  },
+  {
+    path: '/401',
+    component: 'views/error-page/401',
+    hidden: true
+  },
+  {
+    path: '',
+    component: 'layout/Layout',
+    redirect: 'dashboard',
+    children: [
+      {
+        path: 'dashboard',
+        component: 'views/dashboard/index',
+        name: 'Dashboard',
+        meta: { title: 'Dashboard', icon: 'dashboard', affix: true }
+      }
+    ]
+  },
+  {
+    path: '/documentation',
+    component: 'layout/Layout',
+    children: [
+      {
+        path: 'index',
+        component: 'views/documentation/index',
+        name: 'Documentation',
+        meta: { title: 'Documentation', icon: 'documentation', affix: true }
+      }
+    ]
+  },
+  {
+    path: '/guide',
+    component: 'layout/Layout',
+    redirect: '/guide/index',
+    children: [
+      {
+        path: 'index',
+        component: 'views/guide/index',
+        name: 'Guide',
+        meta: { title: 'Guide', icon: 'guide', noCache: true }
+      }
+    ]
+  }
+]
+
+const asyncRoutes = [
+  {
+    path: '/permission',
+    component: 'layout/Layout',
+    redirect: '/permission/index',
+    alwaysShow: true,
+    meta: {
+      title: 'Permission',
+      icon: 'lock',
+      roles: ['admin', 'editor']
+    },
+    children: [
+      {
+        path: 'page',
+        component: 'views/permission/page',
+        name: 'PagePermission',
+        meta: {
+          title: 'Page Permission',
+          roles: ['admin']
+        }
+      },
+      {
+        path: 'directive',
+        component: 'views/permission/directive',
+        name: 'DirectivePermission',
+        meta: {
+          title: 'Directive Permission'
+        }
+      },
+      {
+        path: 'role',
+        component: 'views/permission/role',
+        name: 'RolePermission',
+        meta: {
+          title: 'Role Permission',
+          roles: ['admin']
+        }
+      }
+    ]
+  },
+
+  {
+    path: '/icon',
+    component: 'layout/Layout',
+    children: [
+      {
+        path: 'index',
+        component: 'views/icons/index',
+        name: 'Icons',
+        meta: { title: 'Icons', icon: 'icon', noCache: true }
+      }
+    ]
+  },
+
+  {
+    path: '/components',
+    component: 'layout/Layout',
+    redirect: 'noRedirect',
+    name: 'ComponentDemo',
+    meta: {
+      title: 'Components',
+      icon: 'component'
+    },
+    children: [
+      {
+        path: 'tinymce',
+        component: 'views/components-demo/tinymce',
+        name: 'TinymceDemo',
+        meta: { title: 'Tinymce' }
+      },
+      {
+        path: 'markdown',
+        component: 'views/components-demo/markdown',
+        name: 'MarkdownDemo',
+        meta: { title: 'Markdown' }
+      },
+      {
+        path: 'json-editor',
+        component: 'views/components-demo/json-editor',
+        name: 'JsonEditorDemo',
+        meta: { title: 'Json Editor' }
+      },
+      {
+        path: 'split-pane',
+        component: 'views/components-demo/split-pane',
+        name: 'SplitpaneDemo',
+        meta: { title: 'SplitPane' }
+      },
+      {
+        path: 'avatar-upload',
+        component: 'views/components-demo/avatar-upload',
+        name: 'AvatarUploadDemo',
+        meta: { title: 'Avatar Upload' }
+      },
+      {
+        path: 'dropzone',
+        component: 'views/components-demo/dropzone',
+        name: 'DropzoneDemo',
+        meta: { title: 'Dropzone' }
+      },
+      {
+        path: 'sticky',
+        component: 'views/components-demo/sticky',
+        name: 'StickyDemo',
+        meta: { title: 'Sticky' }
+      },
+      {
+        path: 'count-to',
+        component: 'views/components-demo/count-to',
+        name: 'CountToDemo',
+        meta: { title: 'Count To' }
+      },
+      {
+        path: 'mixin',
+        component: 'views/components-demo/mixin',
+        name: 'ComponentMixinDemo',
+        meta: { title: 'componentMixin' }
+      },
+      {
+        path: 'back-to-top',
+        component: 'views/components-demo/back-to-top',
+        name: 'BackToTopDemo',
+        meta: { title: 'Back To Top' }
+      },
+      {
+        path: 'drag-dialog',
+        component: 'views/components-demo/drag-dialog',
+        name: 'DragDialogDemo',
+        meta: { title: 'Drag Dialog' }
+      },
+      {
+        path: 'drag-select',
+        component: 'views/components-demo/drag-select',
+        name: 'DragSelectDemo',
+        meta: { title: 'Drag Select' }
+      },
+      {
+        path: 'dnd-list',
+        component: 'views/components-demo/dnd-list',
+        name: 'DndListDemo',
+        meta: { title: 'Dnd List' }
+      },
+      {
+        path: 'drag-kanban',
+        component: 'views/components-demo/drag-kanban',
+        name: 'DragKanbanDemo',
+        meta: { title: 'Drag Kanban' }
+      }
+    ]
+  },
+  {
+    path: '/charts',
+    component: 'layout/Layout',
+    redirect: 'noRedirect',
+    name: 'Charts',
+    meta: {
+      title: 'Charts',
+      icon: 'chart'
+    },
+    children: [
+      {
+        path: 'keyboard',
+        component: 'views/charts/keyboard',
+        name: 'KeyboardChart',
+        meta: { title: 'Keyboard Chart', noCache: true }
+      },
+      {
+        path: 'line',
+        component: 'views/charts/line',
+        name: 'LineChart',
+        meta: { title: 'Line Chart', noCache: true }
+      },
+      {
+        path: 'mixchart',
+        component: 'views/charts/mixChart',
+        name: 'MixChart',
+        meta: { title: 'Mix Chart', noCache: true }
+      }
+    ]
+  },
+  {
+    path: '/nested',
+    component: 'layout/Layout',
+    redirect: '/nested/menu1/menu1-1',
+    name: 'Nested',
+    meta: {
+      title: 'Nested',
+      icon: 'nested'
+    },
+    children: [
+      {
+        path: 'menu1',
+        component: 'views/nested/menu1/index',
+        name: 'Menu1',
+        meta: { title: 'Menu1' },
+        redirect: '/nested/menu1/menu1-1',
+        children: [
+          {
+            path: 'menu1-1',
+            component: 'views/nested/menu1/menu1-1',
+            name: 'Menu1-1',
+            meta: { title: 'Menu1-1' }
+          },
+          {
+            path: 'menu1-2',
+            component: 'views/nested/menu1/menu1-2',
+            name: 'Menu1-2',
+            redirect: '/nested/menu1/menu1-2/menu1-2-1',
+            meta: { title: 'Menu1-2' },
+            children: [
+              {
+                path: 'menu1-2-1',
+                component: 'views/nested/menu1/menu1-2/menu1-2-1',
+                name: 'Menu1-2-1',
+                meta: { title: 'Menu1-2-1' }
+              },
+              {
+                path: 'menu1-2-2',
+                component: 'views/nested/menu1/menu1-2/menu1-2-2',
+                name: 'Menu1-2-2',
+                meta: { title: 'Menu1-2-2' }
+              }
+            ]
+          },
+          {
+            path: 'menu1-3',
+            component: 'views/nested/menu1/menu1-3',
+            name: 'Menu1-3',
+            meta: { title: 'Menu1-3' }
+          }
+        ]
+      },
+      {
+        path: 'menu2',
+        name: 'Menu2',
+        component: 'views/nested/menu2/index',
+        meta: { title: 'Menu2' }
+      }
+    ]
+  },
+
+  {
+    path: '/example',
+    component: 'layout/Layout',
+    redirect: '/example/list',
+    name: 'Example',
+    meta: {
+      title: 'Example',
+      icon: 'example'
+    },
+    children: [
+      {
+        path: 'create',
+        component: 'views/example/create',
+        name: 'CreateArticle',
+        meta: { title: 'Create Article', icon: 'edit' }
+      },
+      {
+        path: 'edit/:id(\\d+)',
+        component: 'views/example/edit',
+        name: 'EditArticle',
+        meta: { title: 'Edit Article', noCache: true },
+        hidden: true
+      },
+      {
+        path: 'list',
+        component: 'views/example/list',
+        name: 'ArticleList',
+        meta: { title: 'Article List', icon: 'list' }
+      }
+    ]
+  },
+
+  {
+    path: '/tab',
+    component: 'layout/Layout',
+    children: [
+      {
+        path: 'index',
+        component: 'views/tab/index',
+        name: 'Tab',
+        meta: { title: 'Tab', icon: 'tab' }
+      }
+    ]
+  },
+
+  {
+    path: '/error',
+    component: 'layout/Layout',
+    redirect: 'noRedirect',
+    name: 'ErrorPages',
+    meta: {
+      title: 'Error Pages',
+      icon: '404'
+    },
+    children: [
+      {
+        path: '401',
+        component: 'views/error-page/401',
+        name: 'Page401',
+        meta: { title: 'Page 401', noCache: true }
+      },
+      {
+        path: '404',
+        component: 'views/error-page/404',
+        name: 'Page404',
+        meta: { title: 'Page 404', noCache: true }
+      }
+    ]
+  },
+
+  {
+    path: '/error-log',
+    component: 'layout/Layout',
+    redirect: 'noRedirect',
+    children: [
+      {
+        path: 'log',
+        component: 'views/error-log/index',
+        name: 'ErrorLog',
+        meta: { title: 'Error Log', icon: 'bug' }
+      }
+    ]
+  },
+
+  {
+    path: '/excel',
+    component: 'layout/Layout',
+    redirect: '/excel/export-excel',
+    name: 'Excel',
+    meta: {
+      title: 'Excel',
+      icon: 'excel'
+    },
+    children: [
+      {
+        path: 'export-excel',
+        component: 'views/excel/export-excel',
+        name: 'ExportExcel',
+        meta: { title: 'Export Excel' }
+      },
+      {
+        path: 'export-selected-excel',
+        component: 'views/excel/select-excel',
+        name: 'SelectExcel',
+        meta: { title: 'Select Excel' }
+      },
+      {
+        path: 'export-merge-header',
+        component: 'views/excel/merge-header',
+        name: 'MergeHeader',
+        meta: { title: 'Merge Header' }
+      },
+      {
+        path: 'upload-excel',
+        component: 'views/excel/upload-excel',
+        name: 'UploadExcel',
+        meta: { title: 'Upload Excel' }
+      }
+    ]
+  },
+
+  {
+    path: '/zip',
+    component: 'layout/Layout',
+    redirect: '/zip/download',
+    alwaysShow: true,
+    meta: { title: 'Zip', icon: 'zip' },
+    children: [
+      {
+        path: 'download',
+        component: 'views/zip/index',
+        name: 'ExportZip',
+        meta: { title: 'Export Zip' }
+      }
+    ]
+  },
+
+  {
+    path: '/pdf',
+    component: 'layout/Layout',
+    redirect: '/pdf/index',
+    children: [
+      {
+        path: 'index',
+        component: 'views/pdf/index',
+        name: 'PDF',
+        meta: { title: 'PDF', icon: 'pdf' }
+      }
+    ]
+  },
+  {
+    path: '/pdf/download',
+    component: 'views/pdf/download',
+    hidden: true
+  },
+
+  {
+    path: '/theme',
+    component: 'layout/Layout',
+    redirect: 'noRedirect',
+    children: [
+      {
+        path: 'index',
+        component: 'views/theme/index',
+        name: 'Theme',
+        meta: { title: 'Theme', icon: 'theme' }
+      }
+    ]
+  },
+
+  {
+    path: '/clipboard',
+    component: 'layout/Layout',
+    redirect: 'noRedirect',
+    children: [
+      {
+        path: 'index',
+        component: 'views/clipboard/index',
+        name: 'ClipboardDemo',
+        meta: { title: 'Clipboard Demo', icon: 'clipboard' }
+      }
+    ]
+  },
+
+  {
+    path: '/i18n',
+    component: 'layout/Layout',
+    children: [
+      {
+        path: 'index',
+        component: 'views/i18n-demo/index',
+        name: 'I18n',
+        meta: { title: 'I18n', icon: 'international' }
+      }
+    ]
+  },
+
+  {
+    path: 'external-link',
+    component: 'layout/Layout',
+    children: [
+      {
+        path: 'https://github.com/PanJiaChen/vue-element-admin',
+        meta: { title: 'External Link', icon: 'link' }
+      }
+    ]
+  },
+
+  { path: '*', redirect: '/404', hidden: true }
+]
+
+module.exports = {
+  constantRoutes,
+  asyncRoutes
+}
diff --git a/frontend/mock/user.js b/frontend/mock/user.js
new file mode 100644
index 0000000..d82e079
--- /dev/null
+++ b/frontend/mock/user.js
@@ -0,0 +1,84 @@
+
+const tokens = {
+  admin: {
+    token: 'admin-token'
+  },
+  editor: {
+    token: 'editor-token'
+  }
+}
+
+const users = {
+  'admin-token': {
+    roles: ['admin'],
+    introduction: 'I am a super administrator',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: 'Super Admin'
+  },
+  'editor-token': {
+    roles: ['editor'],
+    introduction: 'I am an editor',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: 'Normal Editor'
+  }
+}
+
+module.exports = [
+  // user login
+  {
+    url: '/vue-element-admin/user/login',
+    type: 'post',
+    response: config => {
+      const { username } = config.body
+      const token = tokens[username]
+
+      // mock error
+      if (!token) {
+        return {
+          code: 60204,
+          message: 'Account and password are incorrect.'
+        }
+      }
+
+      return {
+        code: 20000,
+        data: token
+      }
+    }
+  },
+
+  // get user info
+  {
+    url: '/vue-element-admin/user/info\.*',
+    type: 'get',
+    response: config => {
+      const { token } = config.query
+      const info = users[token]
+
+      // mock error
+      if (!info) {
+        return {
+          code: 50008,
+          message: 'Login failed, unable to get user details.'
+        }
+      }
+
+      return {
+        code: 20000,
+        data: info
+      }
+    }
+  },
+
+  // user logout
+  {
+    url: '/vue-element-admin/user/logout',
+    type: 'post',
+    response: _ => {
+      return {
+        code: 20000,
+        data: 'success'
+      }
+    }
+  }
+]
diff --git a/frontend/mock/utils.js b/frontend/mock/utils.js
new file mode 100644
index 0000000..f909a29
--- /dev/null
+++ b/frontend/mock/utils.js
@@ -0,0 +1,48 @@
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+function param2Obj(url) {
+  const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
+  if (!search) {
+    return {}
+  }
+  const obj = {}
+  const searchArr = search.split('&')
+  searchArr.forEach(v => {
+    const index = v.indexOf('=')
+    if (index !== -1) {
+      const name = v.substring(0, index)
+      const val = v.substring(index + 1, v.length)
+      obj[name] = val
+    }
+  })
+  return obj
+}
+
+/**
+ * This is just a simple version of deep copy
+ * Has a lot of edge cases bug
+ * If you want to use a perfect deep copy, use lodash's _.cloneDeep
+ * @param {Object} source
+ * @returns {Object}
+ */
+function deepClone(source) {
+  if (!source && typeof source !== 'object') {
+    throw new Error('error arguments', 'deepClone')
+  }
+  const targetObj = source.constructor === Array ? [] : {}
+  Object.keys(source).forEach(keys => {
+    if (source[keys] && typeof source[keys] === 'object') {
+      targetObj[keys] = deepClone(source[keys])
+    } else {
+      targetObj[keys] = source[keys]
+    }
+  })
+  return targetObj
+}
+
+module.exports = {
+  param2Obj,
+  deepClone
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..02f68e2
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,111 @@
+{
+  "name": "vue-element-admin",
+  "version": "4.4.0",
+  "description": "A magical vue admin. An out-of-box UI solution for enterprise applications. Newest development stack of vue. Lots of awesome features",
+  "author": "Pan <panfree23@gmail.com>",
+  "scripts": {
+    "dev": "vue-cli-service serve",
+    "lint": "eslint --ext .js,.vue src",
+    "build:prod": "vue-cli-service build",
+    "build:stage": "vue-cli-service build --mode staging",
+    "preview": "node build/index.js --preview",
+    "new": "plop",
+    "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml",
+    "test:unit": "jest --clearCache && vue-cli-service test:unit",
+    "test:ci": "npm run lint && npm run test:unit"
+  },
+  "dependencies": {
+    "axios": "0.18.1",
+    "clipboard": "2.0.4",
+    "codemirror": "5.45.0",
+    "core-js": "3.6.5",
+    "driver.js": "0.9.5",
+    "dropzone": "5.5.1",
+    "echarts": "4.2.1",
+    "element-ui": "2.13.2",
+    "file-saver": "2.0.1",
+    "fuse.js": "3.4.4",
+    "js-cookie": "2.2.0",
+    "jsonlint": "1.6.3",
+    "jszip": "3.2.1",
+    "normalize.css": "7.0.0",
+    "nprogress": "0.2.0",
+    "path-to-regexp": "2.4.0",
+    "screenfull": "4.2.0",
+    "script-loader": "0.7.2",
+    "sortablejs": "1.8.4",
+    "tui-editor": "1.3.3",
+    "vue": "2.6.10",
+    "vue-count-to": "1.0.13",
+    "vue-router": "3.0.2",
+    "vue-splitpane": "1.0.4",
+    "vuedraggable": "2.20.0",
+    "vuex": "3.1.0",
+    "xlsx": "0.14.1"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "4.4.4",
+    "@vue/cli-plugin-eslint": "4.4.4",
+    "@vue/cli-plugin-unit-jest": "4.4.4",
+    "@vue/cli-service": "4.4.4",
+    "@vue/test-utils": "1.0.0-beta.29",
+    "autoprefixer": "9.5.1",
+    "babel-eslint": "10.1.0",
+    "babel-jest": "23.6.0",
+    "babel-plugin-dynamic-import-node": "2.3.3",
+    "chalk": "2.4.2",
+    "chokidar": "2.1.5",
+    "connect": "3.6.6",
+    "eslint": "6.7.2",
+    "eslint-plugin-vue": "6.2.2",
+    "html-webpack-plugin": "3.2.0",
+    "husky": "1.3.1",
+    "lint-staged": "8.1.5",
+    "mockjs": "1.0.1-beta3",
+    "plop": "2.3.0",
+    "runjs": "4.3.2",
+    "sass": "1.26.2",
+    "sass-loader": "8.0.2",
+    "script-ext-html-webpack-plugin": "2.1.3",
+    "serve-static": "1.13.2",
+    "svg-sprite-loader": "4.1.3",
+    "svgo": "1.2.0",
+    "vue-template-compiler": "2.6.10"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions"
+  ],
+  "bugs": {
+    "url": "https://github.com/PanJiaChen/vue-element-admin/issues"
+  },
+  "engines": {
+    "node": ">=8.9",
+    "npm": ">= 3.0.0"
+  },
+  "keywords": [
+    "vue",
+    "admin",
+    "dashboard",
+    "element-ui",
+    "boilerplate",
+    "admin-template",
+    "management-system"
+  ],
+  "license": "MIT",
+  "lint-staged": {
+    "src/**/*.{js,vue}": [
+      "eslint --fix",
+      "git add"
+    ]
+  },
+  "husky": {
+    "hooks": {
+      "pre-commit": "lint-staged"
+    }
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/PanJiaChen/vue-element-admin.git"
+  }
+}
diff --git a/frontend/plop-templates/component/index.hbs b/frontend/plop-templates/component/index.hbs
new file mode 100644
index 0000000..7661055
--- /dev/null
+++ b/frontend/plop-templates/component/index.hbs
@@ -0,0 +1,26 @@
+{{#if template}}
+<template>
+  <div />
+</template>
+{{/if}}
+
+{{#if script}}
+<script>
+export default {
+  name: '{{ properCase name }}',
+  props: {},
+  data() {
+    return {}
+  },
+  created() {},
+  mounted() {},
+  methods: {}
+}
+</script>
+{{/if}}
+
+{{#if style}}
+<style lang="scss" scoped>
+
+</style>
+{{/if}}
diff --git a/frontend/plop-templates/component/prompt.js b/frontend/plop-templates/component/prompt.js
new file mode 100644
index 0000000..3723e8e
--- /dev/null
+++ b/frontend/plop-templates/component/prompt.js
@@ -0,0 +1,55 @@
+const { notEmpty } = require('../utils.js')
+
+module.exports = {
+  description: 'generate vue component',
+  prompts: [{
+    type: 'input',
+    name: 'name',
+    message: 'component name please',
+    validate: notEmpty('name')
+  },
+  {
+    type: 'checkbox',
+    name: 'blocks',
+    message: 'Blocks:',
+    choices: [{
+      name: '<template>',
+      value: 'template',
+      checked: true
+    },
+    {
+      name: '<script>',
+      value: 'script',
+      checked: true
+    },
+    {
+      name: 'style',
+      value: 'style',
+      checked: true
+    }
+    ],
+    validate(value) {
+      if (value.indexOf('script') === -1 && value.indexOf('template') === -1) {
+        return 'Components require at least a <script> or <template> tag.'
+      }
+      return true
+    }
+  }
+  ],
+  actions: data => {
+    const name = '{{properCase name}}'
+    const actions = [{
+      type: 'add',
+      path: `src/components/${name}/index.vue`,
+      templateFile: 'plop-templates/component/index.hbs',
+      data: {
+        name: name,
+        template: data.blocks.includes('template'),
+        script: data.blocks.includes('script'),
+        style: data.blocks.includes('style')
+      }
+    }]
+
+    return actions
+  }
+}
diff --git a/frontend/plop-templates/store/index.hbs b/frontend/plop-templates/store/index.hbs
new file mode 100644
index 0000000..4f8e2dc
--- /dev/null
+++ b/frontend/plop-templates/store/index.hbs
@@ -0,0 +1,16 @@
+{{#if state}}
+const state = {}
+{{/if}}
+
+{{#if mutations}}
+const mutations = {}
+{{/if}}
+
+{{#if actions}}
+const actions = {}
+{{/if}}
+
+export default {
+  namespaced: true,
+  {{options}}
+}
diff --git a/frontend/plop-templates/store/prompt.js b/frontend/plop-templates/store/prompt.js
new file mode 100644
index 0000000..bcbc11d
--- /dev/null
+++ b/frontend/plop-templates/store/prompt.js
@@ -0,0 +1,62 @@
+const { notEmpty } = require('../utils.js')
+
+module.exports = {
+  description: 'generate store',
+  prompts: [{
+    type: 'input',
+    name: 'name',
+    message: 'store name please',
+    validate: notEmpty('name')
+  },
+  {
+    type: 'checkbox',
+    name: 'blocks',
+    message: 'Blocks:',
+    choices: [{
+      name: 'state',
+      value: 'state',
+      checked: true
+    },
+    {
+      name: 'mutations',
+      value: 'mutations',
+      checked: true
+    },
+    {
+      name: 'actions',
+      value: 'actions',
+      checked: true
+    }
+    ],
+    validate(value) {
+      if (!value.includes('state') || !value.includes('mutations')) {
+        return 'store require at least state and mutations'
+      }
+      return true
+    }
+  }
+  ],
+  actions(data) {
+    const name = '{{name}}'
+    const { blocks } = data
+    const options = ['state', 'mutations']
+    const joinFlag = `,
+  `
+    if (blocks.length === 3) {
+      options.push('actions')
+    }
+
+    const actions = [{
+      type: 'add',
+      path: `src/store/modules/${name}.js`,
+      templateFile: 'plop-templates/store/index.hbs',
+      data: {
+        options: options.join(joinFlag),
+        state: blocks.includes('state'),
+        mutations: blocks.includes('mutations'),
+        actions: blocks.includes('actions')
+      }
+    }]
+    return actions
+  }
+}
diff --git a/frontend/plop-templates/utils.js b/frontend/plop-templates/utils.js
new file mode 100644
index 0000000..0498753
--- /dev/null
+++ b/frontend/plop-templates/utils.js
@@ -0,0 +1,2 @@
+exports.notEmpty = name => v =>
+  !v || v.trim() === '' ? `${name} is required` : true
diff --git a/frontend/plop-templates/view/index.hbs b/frontend/plop-templates/view/index.hbs
new file mode 100644
index 0000000..7661055
--- /dev/null
+++ b/frontend/plop-templates/view/index.hbs
@@ -0,0 +1,26 @@
+{{#if template}}
+<template>
+  <div />
+</template>
+{{/if}}
+
+{{#if script}}
+<script>
+export default {
+  name: '{{ properCase name }}',
+  props: {},
+  data() {
+    return {}
+  },
+  created() {},
+  mounted() {},
+  methods: {}
+}
+</script>
+{{/if}}
+
+{{#if style}}
+<style lang="scss" scoped>
+
+</style>
+{{/if}}
diff --git a/frontend/plop-templates/view/prompt.js b/frontend/plop-templates/view/prompt.js
new file mode 100644
index 0000000..1d490ee
--- /dev/null
+++ b/frontend/plop-templates/view/prompt.js
@@ -0,0 +1,55 @@
+const { notEmpty } = require('../utils.js')
+
+module.exports = {
+  description: 'generate a view',
+  prompts: [{
+    type: 'input',
+    name: 'name',
+    message: 'view name please',
+    validate: notEmpty('name')
+  },
+  {
+    type: 'checkbox',
+    name: 'blocks',
+    message: 'Blocks:',
+    choices: [{
+      name: '<template>',
+      value: 'template',
+      checked: true
+    },
+    {
+      name: '<script>',
+      value: 'script',
+      checked: true
+    },
+    {
+      name: 'style',
+      value: 'style',
+      checked: true
+    }
+    ],
+    validate(value) {
+      if (value.indexOf('script') === -1 && value.indexOf('template') === -1) {
+        return 'View require at least a <script> or <template> tag.'
+      }
+      return true
+    }
+  }
+  ],
+  actions: data => {
+    const name = '{{name}}'
+    const actions = [{
+      type: 'add',
+      path: `src/views/${name}/index.vue`,
+      templateFile: 'plop-templates/view/index.hbs',
+      data: {
+        name: name,
+        template: data.blocks.includes('template'),
+        script: data.blocks.includes('script'),
+        style: data.blocks.includes('style')
+      }
+    }]
+
+    return actions
+  }
+}
diff --git a/frontend/plopfile.js b/frontend/plopfile.js
new file mode 100644
index 0000000..57387bf
--- /dev/null
+++ b/frontend/plopfile.js
@@ -0,0 +1,9 @@
+const viewGenerator = require('./plop-templates/view/prompt')
+const componentGenerator = require('./plop-templates/component/prompt')
+const storeGenerator = require('./plop-templates/store/prompt.js')
+
+module.exports = function(plop) {
+  plop.setGenerator('view', viewGenerator)
+  plop.setGenerator('component', componentGenerator)
+  plop.setGenerator('store', storeGenerator)
+}
diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js
new file mode 100644
index 0000000..961986e
--- /dev/null
+++ b/frontend/postcss.config.js
@@ -0,0 +1,5 @@
+module.exports = {
+  plugins: {
+    autoprefixer: {}
+  }
+}
diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico
new file mode 100644
index 0000000..34b63ac
--- /dev/null
+++ b/frontend/public/favicon.ico
Binary files differ
diff --git a/frontend/public/index.html b/frontend/public/index.html
new file mode 100644
index 0000000..e918500
--- /dev/null
+++ b/frontend/public/index.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="renderer" content="webkit">
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title><%= webpackConfig.name %></title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
new file mode 100644
index 0000000..ec9032c
--- /dev/null
+++ b/frontend/src/App.vue
@@ -0,0 +1,11 @@
+<template>
+  <div id="app">
+    <router-view />
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'App'
+}
+</script>
diff --git a/frontend/src/api/article.js b/frontend/src/api/article.js
new file mode 100644
index 0000000..407bda1
--- /dev/null
+++ b/frontend/src/api/article.js
@@ -0,0 +1,41 @@
+import request from '@/utils/request'
+
+export function fetchList(query) {
+  return request({
+    url: '/vue-element-admin/article/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function fetchArticle(id) {
+  return request({
+    url: '/vue-element-admin/article/detail',
+    method: 'get',
+    params: { id }
+  })
+}
+
+export function fetchPv(pv) {
+  return request({
+    url: '/vue-element-admin/article/pv',
+    method: 'get',
+    params: { pv }
+  })
+}
+
+export function createArticle(data) {
+  return request({
+    url: '/vue-element-admin/article/create',
+    method: 'post',
+    data
+  })
+}
+
+export function updateArticle(data) {
+  return request({
+    url: '/vue-element-admin/article/update',
+    method: 'post',
+    data
+  })
+}
diff --git a/frontend/src/api/qiniu.js b/frontend/src/api/qiniu.js
new file mode 100644
index 0000000..a037584
--- /dev/null
+++ b/frontend/src/api/qiniu.js
@@ -0,0 +1,8 @@
+import request from '@/utils/request'
+
+export function getToken() {
+  return request({
+    url: '/qiniu/upload/token', // 假地址 自行替换
+    method: 'get'
+  })
+}
diff --git a/frontend/src/api/remote-search.js b/frontend/src/api/remote-search.js
new file mode 100644
index 0000000..02e42b4
--- /dev/null
+++ b/frontend/src/api/remote-search.js
@@ -0,0 +1,17 @@
+import request from '@/utils/request'
+
+export function searchUser(name) {
+  return request({
+    url: '/vue-element-admin/search/user',
+    method: 'get',
+    params: { name }
+  })
+}
+
+export function transactionList(query) {
+  return request({
+    url: '/vue-element-admin/transaction/list',
+    method: 'get',
+    params: query
+  })
+}
diff --git a/frontend/src/api/role.js b/frontend/src/api/role.js
new file mode 100644
index 0000000..959bbd2
--- /dev/null
+++ b/frontend/src/api/role.js
@@ -0,0 +1,38 @@
+import request from '@/utils/request'
+
+export function getRoutes() {
+  return request({
+    url: '/vue-element-admin/routes',
+    method: 'get'
+  })
+}
+
+export function getRoles() {
+  return request({
+    url: '/vue-element-admin/roles',
+    method: 'get'
+  })
+}
+
+export function addRole(data) {
+  return request({
+    url: '/vue-element-admin/role',
+    method: 'post',
+    data
+  })
+}
+
+export function updateRole(id, data) {
+  return request({
+    url: `/vue-element-admin/role/${id}`,
+    method: 'put',
+    data
+  })
+}
+
+export function deleteRole(id) {
+  return request({
+    url: `/vue-element-admin/role/${id}`,
+    method: 'delete'
+  })
+}
diff --git a/frontend/src/api/user.js b/frontend/src/api/user.js
new file mode 100644
index 0000000..b8b8741
--- /dev/null
+++ b/frontend/src/api/user.js
@@ -0,0 +1,24 @@
+import request from '@/utils/request'
+
+export function login(data) {
+  return request({
+    url: '/vue-element-admin/user/login',
+    method: 'post',
+    data
+  })
+}
+
+export function getInfo(token) {
+  return request({
+    url: '/vue-element-admin/user/info',
+    method: 'get',
+    params: { token }
+  })
+}
+
+export function logout() {
+  return request({
+    url: '/vue-element-admin/user/logout',
+    method: 'post'
+  })
+}
diff --git a/frontend/src/assets/401_images/401.gif b/frontend/src/assets/401_images/401.gif
new file mode 100644
index 0000000..cd6e0d9
--- /dev/null
+++ b/frontend/src/assets/401_images/401.gif
Binary files differ
diff --git a/frontend/src/assets/404_images/404.png b/frontend/src/assets/404_images/404.png
new file mode 100644
index 0000000..3d8e230
--- /dev/null
+++ b/frontend/src/assets/404_images/404.png
Binary files differ
diff --git a/frontend/src/assets/404_images/404_cloud.png b/frontend/src/assets/404_images/404_cloud.png
new file mode 100644
index 0000000..c6281d0
--- /dev/null
+++ b/frontend/src/assets/404_images/404_cloud.png
Binary files differ
diff --git a/frontend/src/assets/custom-theme/fonts/element-icons.ttf b/frontend/src/assets/custom-theme/fonts/element-icons.ttf
new file mode 100644
index 0000000..570a3e1
--- /dev/null
+++ b/frontend/src/assets/custom-theme/fonts/element-icons.ttf
Binary files differ
diff --git a/frontend/src/assets/custom-theme/fonts/element-icons.woff b/frontend/src/assets/custom-theme/fonts/element-icons.woff
new file mode 100644
index 0000000..c2bcc00
--- /dev/null
+++ b/frontend/src/assets/custom-theme/fonts/element-icons.woff
Binary files differ
diff --git a/frontend/src/assets/custom-theme/index.css b/frontend/src/assets/custom-theme/index.css
new file mode 100644
index 0000000..e8b4e08
--- /dev/null
+++ b/frontend/src/assets/custom-theme/index.css
@@ -0,0 +1 @@
+@charset "UTF-8";.custom-theme .fade-in-linear-enter-active,.custom-theme .fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.custom-theme .fade-in-linear-enter,.custom-theme .fade-in-linear-leave,.custom-theme .fade-in-linear-leave-active{opacity:0}.custom-theme .el-fade-in-linear-enter-active,.custom-theme .el-fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.custom-theme .el-fade-in-linear-enter,.custom-theme .el-fade-in-linear-leave,.custom-theme .el-fade-in-linear-leave-active{opacity:0}.custom-theme .el-fade-in-enter-active,.custom-theme .el-fade-in-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.custom-theme .el-fade-in-enter,.custom-theme .el-fade-in-leave-active{opacity:0}.custom-theme .el-zoom-in-center-enter-active,.custom-theme .el-zoom-in-center-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.custom-theme .el-zoom-in-center-enter,.custom-theme .el-zoom-in-center-leave-active{opacity:0;-webkit-transform:scaleX(0);transform:scaleX(0)}.custom-theme .el-zoom-in-top-enter-active,.custom-theme .el-zoom-in-top-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;-webkit-transform-origin:center top;transform-origin:center top}.custom-theme .el-zoom-in-top-enter,.custom-theme .el-zoom-in-top-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.custom-theme .el-zoom-in-bottom-enter-active,.custom-theme .el-zoom-in-bottom-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;-webkit-transform-origin:center bottom;transform-origin:center bottom}.custom-theme .el-zoom-in-bottom-enter,.custom-theme .el-zoom-in-bottom-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.custom-theme .el-zoom-in-left-enter-active,.custom-theme .el-zoom-in-left-leave-active{opacity:1;-webkit-transform:scale(1,1);transform:scale(1,1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;-webkit-transform-origin:top left;transform-origin:top left}.custom-theme .el-zoom-in-left-enter,.custom-theme .el-zoom-in-left-leave-active{opacity:0;-webkit-transform:scale(.45,.45);transform:scale(.45,.45)}.custom-theme .collapse-transition{-webkit-transition:.3s height ease-in-out,.3s padding-top ease-in-out,.3s padding-bottom ease-in-out;transition:.3s height ease-in-out,.3s padding-top ease-in-out,.3s padding-bottom ease-in-out}.custom-theme .horizontal-collapse-transition{-webkit-transition:.3s width ease-in-out,.3s padding-left ease-in-out,.3s padding-right ease-in-out;transition:.3s width ease-in-out,.3s padding-left ease-in-out,.3s padding-right ease-in-out}.custom-theme .el-list-enter-active,.custom-theme .el-list-leave-active{-webkit-transition:all 1s;transition:all 1s}.custom-theme .el-list-enter,.custom-theme .el-list-leave-active{opacity:0;-webkit-transform:translateY(-30px);transform:translateY(-30px)}.custom-theme .el-opacity-transition{-webkit-transition:opacity .3s cubic-bezier(.55,0,.1,1);transition:opacity .3s cubic-bezier(.55,0,.1,1)}@font-face{font-family:element-icons;src:url(fonts/element-icons.woff?t=1508751886602) format("woff"),url(fonts/element-icons.ttf?t=1508751886602) format("truetype");font-weight:400;font-style:normal}.custom-theme [class*=" el-icon-"],.custom-theme [class^=el-icon-]{font-family:element-icons!important;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;vertical-align:baseline;display:inline-block;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.custom-theme .el-icon-upload:before{content:"\e60d"}.custom-theme .el-icon-error:before{content:"\e62c"}.custom-theme .el-icon-success:before{content:"\e62d"}.custom-theme .el-icon-warning:before{content:"\e62e"}.custom-theme .el-icon-sort-down:before{content:"\e630"}.custom-theme .el-icon-sort-up:before{content:"\e631"}.custom-theme .el-icon-arrow-left:before{content:"\e600"}.custom-theme .el-icon-circle-plus:before{content:"\e601"}.custom-theme .el-icon-circle-plus-outline:before{content:"\e602"}.custom-theme .el-icon-arrow-down:before{content:"\e603"}.custom-theme .el-icon-arrow-right:before{content:"\e604"}.custom-theme .el-icon-arrow-up:before{content:"\e605"}.custom-theme .el-icon-back:before{content:"\e606"}.custom-theme .el-icon-circle-close:before{content:"\e607"}.custom-theme .el-icon-date:before{content:"\e608"}.custom-theme .el-icon-circle-close-outline:before{content:"\e609"}.custom-theme .el-icon-caret-left:before{content:"\e60a"}.custom-theme .el-icon-caret-bottom:before{content:"\e60b"}.custom-theme .el-icon-caret-top:before{content:"\e60c"}.custom-theme .el-icon-caret-right:before{content:"\e60e"}.custom-theme .el-icon-close:before{content:"\e60f"}.custom-theme .el-icon-d-arrow-left:before{content:"\e610"}.custom-theme .el-icon-check:before{content:"\e611"}.custom-theme .el-icon-delete:before{content:"\e612"}.custom-theme .el-icon-d-arrow-right:before{content:"\e613"}.custom-theme .el-icon-document:before{content:"\e614"}.custom-theme .el-icon-d-caret:before{content:"\e615"}.custom-theme .el-icon-edit-outline:before{content:"\e616"}.custom-theme .el-icon-download:before{content:"\e617"}.custom-theme .el-icon-goods:before{content:"\e618"}.custom-theme .el-icon-search:before{content:"\e619"}.custom-theme .el-icon-info:before{content:"\e61a"}.custom-theme .el-icon-message:before{content:"\e61b"}.custom-theme .el-icon-edit:before{content:"\e61c"}.custom-theme .el-icon-location:before{content:"\e61d"}.custom-theme .el-icon-loading:before{content:"\e61e"}.custom-theme .el-icon-location-outline:before{content:"\e61f"}.custom-theme .el-icon-menu:before{content:"\e620"}.custom-theme .el-icon-minus:before{content:"\e621"}.custom-theme .el-icon-bell:before{content:"\e622"}.custom-theme .el-icon-mobile-phone:before{content:"\e624"}.custom-theme .el-icon-news:before{content:"\e625"}.custom-theme .el-icon-more:before{content:"\e646"}.custom-theme .el-icon-more-outline:before{content:"\e626"}.custom-theme .el-icon-phone:before{content:"\e627"}.custom-theme .el-icon-phone-outline:before{content:"\e628"}.custom-theme .el-icon-picture:before{content:"\e629"}.custom-theme .el-icon-picture-outline:before{content:"\e62a"}.custom-theme .el-icon-plus:before{content:"\e62b"}.custom-theme .el-icon-printer:before{content:"\e62f"}.custom-theme .el-icon-rank:before{content:"\e632"}.custom-theme .el-icon-refresh:before{content:"\e633"}.custom-theme .el-icon-question:before{content:"\e634"}.custom-theme .el-icon-remove:before{content:"\e635"}.custom-theme .el-icon-share:before{content:"\e636"}.custom-theme .el-icon-star-on:before{content:"\e637"}.custom-theme .el-icon-setting:before{content:"\e638"}.custom-theme .el-icon-circle-check:before{content:"\e639"}.custom-theme .el-icon-service:before{content:"\e63a"}.custom-theme .el-icon-sold-out:before{content:"\e63b"}.custom-theme .el-icon-remove-outline:before{content:"\e63c"}.custom-theme .el-icon-star-off:before{content:"\e63d"}.custom-theme .el-icon-circle-check-outline:before{content:"\e63e"}.custom-theme .el-icon-tickets:before{content:"\e63f"}.custom-theme .el-icon-sort:before{content:"\e640"}.custom-theme .el-icon-zoom-in:before{content:"\e641"}.custom-theme .el-icon-time:before{content:"\e642"}.custom-theme .el-icon-view:before{content:"\e643"}.custom-theme .el-icon-upload2:before{content:"\e644"}.custom-theme .el-icon-zoom-out:before{content:"\e645"}.custom-theme .el-icon-loading{-webkit-animation:rotating 2s linear infinite;animation:rotating 2s linear infinite}.custom-theme .el-icon--right{margin-left:5px}.custom-theme .el-icon--left{margin-right:5px}@-webkit-keyframes rotating{0%{-webkit-transform:rotateZ(0);transform:rotateZ(0)}100%{-webkit-transform:rotateZ(360deg);transform:rotateZ(360deg)}}@keyframes rotating{0%{-webkit-transform:rotateZ(0);transform:rotateZ(0)}100%{-webkit-transform:rotateZ(360deg);transform:rotateZ(360deg)}}.custom-theme .el-popper .popper__arrow,.custom-theme .el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.custom-theme .el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03));filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.custom-theme .el-popper .popper__arrow::after{content:" ";border-width:6px}.custom-theme .el-popper[x-placement^=top]{margin-bottom:12px}.custom-theme .el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#e6ebf5;border-bottom-width:0}.custom-theme .el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#fff;border-bottom-width:0}.custom-theme .el-popper[x-placement^=bottom]{margin-top:12px}.custom-theme .el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#e6ebf5}.custom-theme .el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#fff}.custom-theme .el-popper[x-placement^=right]{margin-left:12px}.custom-theme .el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#e6ebf5;border-left-width:0}.custom-theme .el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#fff;border-left-width:0}.custom-theme .el-popper[x-placement^=left]{margin-right:12px}.custom-theme .el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#e6ebf5}.custom-theme .el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#fff}.custom-theme .el-select-dropdown{position:absolute;z-index:1001;border:solid 1px #dfe4ed;border-radius:4px;background-color:#fff;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);-webkit-box-sizing:border-box;box-sizing:border-box;margin:5px 0}.custom-theme .el-select-dropdown.is-multiple .el-select-dropdown__item.selected{color:#262729;background-color:#fff}.custom-theme .el-select-dropdown.is-multiple .el-select-dropdown__item.selected.hover{background-color:#f5f7fa}.custom-theme .el-select-dropdown.is-multiple .el-select-dropdown__item.selected::after{position:absolute;right:20px;font-family:element-icons;content:"\E611";font-size:12px;font-weight:700;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.custom-theme .el-select-dropdown .el-scrollbar.is-empty .el-select-dropdown__list{padding:0}.custom-theme .el-select-dropdown .popper__arrow{-webkit-transform:translateX(-400%);transform:translateX(-400%)}.custom-theme .el-select-dropdown.is-arrow-fixed .popper__arrow{-webkit-transform:translateX(-200%);transform:translateX(-200%)}.custom-theme .el-select-dropdown__empty{padding:10px 0;margin:0;text-align:center;color:#999;font-size:14px}.custom-theme .el-select-dropdown__wrap{max-height:274px}.custom-theme .el-select-dropdown__list{list-style:none;padding:6px 0;margin:0;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-input{position:relative;font-size:14px;display:inline-block;width:100%}.custom-theme .el-input::-webkit-scrollbar{z-index:11;width:6px}.custom-theme .el-input::-webkit-scrollbar:horizontal{height:6px}.custom-theme .el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.custom-theme .el-input::-webkit-scrollbar-corner{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.custom-theme .el-input__inner{-webkit-appearance:none;background-color:#fff;background-image:none;border-radius:4px;border:1px solid #d8dce5;-webkit-box-sizing:border-box;box-sizing:border-box;color:#5a5e66;display:inline-block;font-size:inherit;height:40px;line-height:1;outline:0;padding:0 15px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.custom-theme .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input__inner:hover{border-color:#b4bccc}.custom-theme .el-input__inner:focus{outline:0;border-color:#262729}.custom-theme .el-input__suffix{position:absolute;height:100%;right:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s;pointer-events:none}.custom-theme .el-input__suffix-inner{pointer-events:all}.custom-theme .el-input__prefix{position:absolute;height:100%;left:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s}.custom-theme .el-input__icon{height:100%;width:25px;text-align:center;-webkit-transition:all .3s;transition:all .3s;line-height:40px}.custom-theme .el-input__icon:after{content:'';height:100%;width:0;display:inline-block;vertical-align:middle}.custom-theme .el-input__validateIcon{pointer-events:none}.custom-theme .el-input.is-active .el-input__inner{outline:0;border-color:#262729}.custom-theme .el-input.is-disabled .el-input__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-input.is-disabled .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__icon{cursor:not-allowed}.custom-theme .el-input--suffix .el-input__inner{padding-right:30px}.custom-theme .el-input--prefix .el-input__inner{padding-left:30px}.custom-theme .el-input--medium{font-size:14px}.custom-theme .el-input--medium .el-input__inner{height:36px}.custom-theme .el-input--medium .el-input__icon{line-height:36px}.custom-theme .el-input--small{font-size:13px}.custom-theme .el-input--small .el-input__inner{height:32px}.custom-theme .el-input--small .el-input__icon{line-height:32px}.custom-theme .el-input--mini{font-size:12px}.custom-theme .el-input--mini .el-input__inner{height:28px}.custom-theme .el-input--mini .el-input__icon{line-height:28px}.custom-theme .el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate}.custom-theme .el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.custom-theme .el-input-group__append,.custom-theme .el-input-group__prepend{background-color:#f5f7fa;color:#0a76a4;vertical-align:middle;display:table-cell;position:relative;border:1px solid #d8dce5;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.custom-theme .el-input-group__append:focus,.custom-theme .el-input-group__prepend:focus{outline:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-select,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-select{display:inline-block;margin:-20px}.custom-theme .el-input-group__append button.el-button,.custom-theme .el-input-group__append div.el-select .el-input__inner,.custom-theme .el-input-group__append div.el-select:hover .el-input__inner,.custom-theme .el-input-group__prepend button.el-button,.custom-theme .el-input-group__prepend div.el-select .el-input__inner,.custom-theme .el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-input,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-input{font-size:inherit}.custom-theme .el-input-group__prepend{border-right:0;border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-input-group__append{border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--prepend .el-input__inner{border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--append .el-input__inner{border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-textarea{display:inline-block;width:100%;vertical-align:bottom}.custom-theme .el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;font-size:14px;color:#5a5e66;background-color:#fff;background-image:none;border:1px solid #d8dce5;border-radius:4px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.custom-theme .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:hover{border-color:#b4bccc}.custom-theme .el-textarea__inner:focus{outline:0;border-color:#262729}.custom-theme .el-textarea.is-disabled .el-textarea__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-tag{background-color:rgba(38,39,41,.1);display:inline-block;padding:0 10px;height:32px;line-height:30px;font-size:12px;color:#262729;border-radius:4px;-webkit-box-sizing:border-box;box-sizing:border-box;border:1px solid rgba(38,39,41,.2);white-space:nowrap}.custom-theme .el-tag .el-icon-close{border-radius:50%;text-align:center;position:relative;cursor:pointer;font-size:12px;height:18px;width:18px;line-height:18px;vertical-align:middle;top:-1px;right:-5px;color:#262729}.custom-theme .el-tag .el-icon-close::before{display:block}.custom-theme .el-tag .el-icon-close:hover{background-color:#262729;color:#fff}.custom-theme .el-tag--info{background-color:rgba(10,118,164,.1);border-color:rgba(10,118,164,.2);color:#0a76a4}.custom-theme .el-tag--info.is-hit{border-color:#0a76a4}.custom-theme .el-tag--info .el-tag__close{color:#0a76a4}.custom-theme .el-tag--info .el-tag__close:hover{background-color:#0a76a4;color:#fff}.custom-theme .el-tag--success{background-color:rgba(64,145,103,.1);border-color:rgba(64,145,103,.2);color:#409167}.custom-theme .el-tag--success.is-hit{border-color:#409167}.custom-theme .el-tag--success .el-tag__close{color:#409167}.custom-theme .el-tag--success .el-tag__close:hover{background-color:#409167;color:#fff}.custom-theme .el-tag--warning{background-color:rgba(157,164,8,.1);border-color:rgba(157,164,8,.2);color:#9da408}.custom-theme .el-tag--warning.is-hit{border-color:#9da408}.custom-theme .el-tag--warning .el-tag__close{color:#9da408}.custom-theme .el-tag--warning .el-tag__close:hover{background-color:#9da408;color:#fff}.custom-theme .el-tag--danger{background-color:rgba(179,69,14,.1);border-color:rgba(179,69,14,.2);color:#b3450e}.custom-theme .el-tag--danger.is-hit{border-color:#b3450e}.custom-theme .el-tag--danger .el-tag__close{color:#b3450e}.custom-theme .el-tag--danger .el-tag__close:hover{background-color:#b3450e;color:#fff}.custom-theme .el-tag--medium{height:28px;line-height:26px}.custom-theme .el-tag--medium .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.custom-theme .el-tag--small{height:24px;padding:0 8px;line-height:22px}.custom-theme .el-tag--small .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.custom-theme .el-tag--mini{height:20px;padding:0 5px;line-height:19px}.custom-theme .el-tag--mini .el-icon-close{margin-left:-3px;-webkit-transform:scale(.7);transform:scale(.7)}.custom-theme .el-select-dropdown__item{font-size:14px;padding:0 20px;position:relative;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#5a5e66;height:34px;line-height:34px;-webkit-box-sizing:border-box;box-sizing:border-box;cursor:pointer}.custom-theme .el-select-dropdown__item.is-disabled{color:#b4bccc;cursor:not-allowed}.custom-theme .el-select-dropdown__item.is-disabled:hover{background-color:#fff}.custom-theme .el-select-dropdown__item.hover,.custom-theme .el-select-dropdown__item:hover{background-color:#f5f7fa}.custom-theme .el-select-dropdown__item.selected{color:#262729;font-weight:700}.custom-theme .el-select-dropdown__item span{line-height:34px!important}.custom-theme .el-select-group{margin:0;padding:0}.custom-theme .el-select-group__wrap{position:relative;list-style:none;margin:0;padding:0}.custom-theme .el-select-group__wrap:not(:last-of-type){padding-bottom:24px}.custom-theme .el-select-group__wrap:not(:last-of-type)::after{content:'';position:absolute;display:block;left:20px;right:20px;bottom:12px;height:1px;background:#dfe4ed}.custom-theme .el-select-group__title{padding-left:20px;font-size:12px;color:#0a76a4;line-height:30px}.custom-theme .el-select-group .el-select-dropdown__item{padding-left:20px}.custom-theme .el-scrollbar{overflow:hidden;position:relative}.custom-theme .el-scrollbar:active>.el-scrollbar__bar,.custom-theme .el-scrollbar:focus>.el-scrollbar__bar,.custom-theme .el-scrollbar:hover>.el-scrollbar__bar{opacity:1;-webkit-transition:opacity 340ms ease-out;transition:opacity 340ms ease-out}.custom-theme .el-scrollbar__wrap{overflow:scroll;height:100%}.custom-theme .el-scrollbar__wrap--hidden-default::-webkit-scrollbar{width:0;height:0}.custom-theme .el-scrollbar__thumb{position:relative;display:block;width:0;height:0;cursor:pointer;border-radius:inherit;background-color:rgba(135,141,153,.3);-webkit-transition:.3s background-color;transition:.3s background-color}.custom-theme .el-scrollbar__thumb:hover{background-color:rgba(135,141,153,.5)}.custom-theme .el-scrollbar__bar{position:absolute;right:2px;bottom:2px;z-index:1;border-radius:4px;opacity:0;-webkit-transition:opacity 120ms ease-out;transition:opacity 120ms ease-out}.custom-theme .el-scrollbar__bar.is-vertical{width:6px;top:2px}.custom-theme .el-scrollbar__bar.is-vertical>div{width:100%}.custom-theme .el-scrollbar__bar.is-horizontal{height:6px;left:2px}.custom-theme .el-scrollbar__bar.is-horizontal>div{height:100%}.custom-theme .el-select{display:inline-block;position:relative}.custom-theme .el-select:hover .el-input__inner{border-color:#b4bccc}.custom-theme .el-select .el-input__inner{cursor:pointer;padding-right:35px}.custom-theme .el-select .el-input__inner:focus{border-color:#262729}.custom-theme .el-select .el-input .el-select__caret{color:#b4bccc;font-size:14px;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;-webkit-transform:rotateZ(180deg);transform:rotateZ(180deg);line-height:16px;cursor:pointer}.custom-theme .el-select .el-input .el-select__caret.is-reverse{-webkit-transform:rotateZ(0);transform:rotateZ(0)}.custom-theme .el-select .el-input .el-select__caret.is-show-close{font-size:14px;text-align:center;-webkit-transform:rotateZ(180deg);transform:rotateZ(180deg);border-radius:100%;color:#b4bccc;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1)}.custom-theme .el-select .el-input .el-select__caret.is-show-close:hover{color:#878d99}.custom-theme .el-select .el-input.is-disabled .el-input__inner{cursor:not-allowed}.custom-theme .el-select .el-input.is-disabled .el-input__inner:hover{border-color:#dfe4ed}.custom-theme .el-select>.el-input{display:block}.custom-theme .el-select__input{border:none;outline:0;padding:0;margin-left:15px;color:#666;font-size:14px;vertical-align:baseline;-webkit-appearance:none;-moz-appearance:none;appearance:none;height:28px;background-color:transparent}.custom-theme .el-select__input.is-mini{height:14px}.custom-theme .el-select__close{cursor:pointer;position:absolute;top:8px;z-index:1000;right:25px;color:#b4bccc;line-height:18px;font-size:14px}.custom-theme .el-select__close:hover{color:#878d99}.custom-theme .el-select__tags{position:absolute;line-height:normal;white-space:normal;z-index:1;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.custom-theme .el-select .el-tag__close{margin-top:-2px}.custom-theme .el-select .el-tag{-webkit-box-sizing:border-box;box-sizing:border-box;border-color:transparent;margin:3px 0 3px 6px;background-color:#f0f2f5}.custom-theme .el-select .el-tag__close.el-icon-close{background-color:#b4bccc;right:-7px;color:#fff}.custom-theme .el-select .el-tag__close.el-icon-close:hover{background-color:#878d99}.custom-theme .el-select .el-tag__close.el-icon-close::before{display:block;-webkit-transform:translate(0,.5px);transform:translate(0,.5px)}.custom-theme .el-select__tag{display:inline-block;height:24px;line-height:24px;font-size:14px;border-radius:4px;color:#fff;background-color:#262729}.custom-theme .el-select__tag .el-icon-close{font-size:14px}.custom-theme .el-pagination{white-space:nowrap;padding:2px 5px;color:#2d2f33;font-weight:700}.custom-theme .el-pagination::after,.custom-theme .el-pagination::before{display:table;content:""}.custom-theme .el-pagination::after{clear:both}.custom-theme .el-pagination button,.custom-theme .el-pagination span:not([class*=suffix]){display:inline-block;font-size:13px;min-width:35.5px;height:28px;line-height:28px;vertical-align:top;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-pagination .el-input__inner{text-align:center}.custom-theme .el-pagination .el-input__suffix{right:0;-webkit-transform:scale(.8);transform:scale(.8)}.custom-theme .el-pagination .el-select .el-input{width:100px;margin:0 5px}.custom-theme .el-pagination .el-select .el-input .el-input__inner{padding-right:25px;border-radius:3px;height:28px}.custom-theme .el-pagination button{border:none;padding:0 6px;background:0 0}.custom-theme .el-pagination button:focus{outline:0}.custom-theme .el-pagination button:hover{color:#262729}.custom-theme .el-pagination button.disabled{color:#b4bccc;background-color:#fff;cursor:not-allowed}.custom-theme .el-pagination .btn-next,.custom-theme .el-pagination .btn-prev{background:center center no-repeat;background-size:16px;background-color:#fff;cursor:pointer;margin:0;color:#2d2f33}.custom-theme .el-pagination .btn-next .el-icon,.custom-theme .el-pagination .btn-prev .el-icon{display:block;font-size:12px}.custom-theme .el-pagination .btn-prev{padding-right:12px}.custom-theme .el-pagination .btn-next{padding-left:12px}.custom-theme .el-pagination--small .btn-next,.custom-theme .el-pagination--small .btn-prev,.custom-theme .el-pagination--small .el-pager li,.custom-theme .el-pagination--small .el-pager li:last-child{border-color:transparent;font-size:12px;line-height:22px;height:22px;min-width:22px}.custom-theme .el-pagination--small .arrow.disabled{visibility:hidden}.custom-theme .el-pagination__sizes{margin:0 10px 0 0;font-weight:400;color:#5a5e66}.custom-theme .el-pagination__sizes .el-input .el-input__inner{font-size:13px;padding-left:8px}.custom-theme .el-pagination__sizes .el-input .el-input__inner:hover{border-color:#262729}.custom-theme .el-pagination__total{margin-right:10px;font-weight:400;color:#5a5e66}.custom-theme .el-pagination__jump{margin-left:24px;font-weight:400;color:#5a5e66}.custom-theme .el-pagination__jump .el-input__inner{padding:0 3px}.custom-theme .el-pagination__rightwrapper{float:right}.custom-theme .el-pagination__editor{line-height:18px;padding:0 2px;height:28px;text-align:center;margin:0 2px;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:3px;-moz-appearance:textfield}.custom-theme .el-pagination__editor.el-input{width:50px}.custom-theme .el-pagination__editor.el-input .el-input__inner{height:28px}.custom-theme .el-pagination__editor .el-input__inner::-webkit-inner-spin-button,.custom-theme .el-pagination__editor .el-input__inner::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.custom-theme .el-pager{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;list-style:none;display:inline-block;vertical-align:top;font-size:0;padding:0;margin:0}.custom-theme .el-pager .el-icon-more::before{vertical-align:-4px}.custom-theme .el-pager li{padding:0 4px;background:#fff;vertical-align:top;display:inline-block;font-size:13px;min-width:35.5px;height:28px;line-height:28px;cursor:pointer;-webkit-box-sizing:border-box;box-sizing:border-box;text-align:center;margin:0}.custom-theme .el-pager li.btn-quicknext,.custom-theme .el-pager li.btn-quickprev{line-height:28px;color:#2d2f33}.custom-theme .el-pager li.btn-quickprev:hover{cursor:pointer}.custom-theme .el-pager li.btn-quicknext:hover{cursor:pointer}.custom-theme .el-pager li.active+li{border-left:0}.custom-theme .el-pager li:hover{color:#262729}.custom-theme .el-pager li.active{color:#262729;cursor:default}.custom-theme .v-modal-enter{-webkit-animation:v-modal-in .2s ease;animation:v-modal-in .2s ease}.custom-theme .v-modal-leave{-webkit-animation:v-modal-out .2s ease forwards;animation:v-modal-out .2s ease forwards}@-webkit-keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-in{0%{opacity:0}}@-webkit-keyframes v-modal-out{100%{opacity:0}}@keyframes v-modal-out{100%{opacity:0}}.custom-theme .v-modal{position:fixed;left:0;top:0;width:100%;height:100%;opacity:.5;background:#000}.custom-theme .el-dialog{position:relative;margin:0 auto 50px;background:#fff;border-radius:2px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,.3);box-shadow:0 1px 3px rgba(0,0,0,.3);-webkit-box-sizing:border-box;box-sizing:border-box;width:50%}.custom-theme .el-dialog.is-fullscreen{width:100%;margin-top:0;margin-bottom:0;height:100%;overflow:auto}.custom-theme .el-dialog__wrapper{position:fixed;top:0;right:0;bottom:0;left:0;overflow:auto;margin:0}.custom-theme .el-dialog__header{padding:15px;padding-bottom:10px}.custom-theme .el-dialog__headerbtn{position:absolute;top:15px;right:15px;padding:0;background:0 0;border:none;outline:0;cursor:pointer;font-size:16px}.custom-theme .el-dialog__headerbtn .el-dialog__close{color:#0a76a4}.custom-theme .el-dialog__headerbtn:focus .el-dialog__close,.custom-theme .el-dialog__headerbtn:hover .el-dialog__close{color:#262729}.custom-theme .el-dialog__title{line-height:24px;font-size:18px;color:#2d2f33}.custom-theme .el-dialog__body{padding:30px 20px;color:#5a5e66;line-height:24px;font-size:14px}.custom-theme .el-dialog__footer{padding:15px;padding-top:10px;text-align:right;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-dialog--center{text-align:center}.custom-theme .el-dialog--center .el-dialog__header{padding-top:30px}.custom-theme .el-dialog--center .el-dialog__body{text-align:initial;padding:25px 27px 30px}.custom-theme .el-dialog--center .el-dialog__footer{text-align:inherit;padding-bottom:30px}.custom-theme .dialog-fade-enter-active{-webkit-animation:dialog-fade-in .3s;animation:dialog-fade-in .3s}.custom-theme .dialog-fade-leave-active{-webkit-animation:dialog-fade-out .3s;animation:dialog-fade-out .3s}@-webkit-keyframes dialog-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@keyframes dialog-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes dialog-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}@keyframes dialog-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}.custom-theme .el-input{position:relative;font-size:14px;display:inline-block;width:100%}.custom-theme .el-input::-webkit-scrollbar{z-index:11;width:6px}.custom-theme .el-input::-webkit-scrollbar:horizontal{height:6px}.custom-theme .el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.custom-theme .el-input::-webkit-scrollbar-corner{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.custom-theme .el-input__inner{-webkit-appearance:none;background-color:#fff;background-image:none;border-radius:4px;border:1px solid #d8dce5;-webkit-box-sizing:border-box;box-sizing:border-box;color:#5a5e66;display:inline-block;font-size:inherit;height:40px;line-height:1;outline:0;padding:0 15px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.custom-theme .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input__inner:hover{border-color:#b4bccc}.custom-theme .el-input__inner:focus{outline:0;border-color:#262729}.custom-theme .el-input__suffix{position:absolute;height:100%;right:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s;pointer-events:none}.custom-theme .el-input__suffix-inner{pointer-events:all}.custom-theme .el-input__prefix{position:absolute;height:100%;left:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s}.custom-theme .el-input__icon{height:100%;width:25px;text-align:center;-webkit-transition:all .3s;transition:all .3s;line-height:40px}.custom-theme .el-input__icon:after{content:'';height:100%;width:0;display:inline-block;vertical-align:middle}.custom-theme .el-input__validateIcon{pointer-events:none}.custom-theme .el-input.is-active .el-input__inner{outline:0;border-color:#262729}.custom-theme .el-input.is-disabled .el-input__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-input.is-disabled .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__icon{cursor:not-allowed}.custom-theme .el-input--suffix .el-input__inner{padding-right:30px}.custom-theme .el-input--prefix .el-input__inner{padding-left:30px}.custom-theme .el-input--medium{font-size:14px}.custom-theme .el-input--medium .el-input__inner{height:36px}.custom-theme .el-input--medium .el-input__icon{line-height:36px}.custom-theme .el-input--small{font-size:13px}.custom-theme .el-input--small .el-input__inner{height:32px}.custom-theme .el-input--small .el-input__icon{line-height:32px}.custom-theme .el-input--mini{font-size:12px}.custom-theme .el-input--mini .el-input__inner{height:28px}.custom-theme .el-input--mini .el-input__icon{line-height:28px}.custom-theme .el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate}.custom-theme .el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.custom-theme .el-input-group__append,.custom-theme .el-input-group__prepend{background-color:#f5f7fa;color:#0a76a4;vertical-align:middle;display:table-cell;position:relative;border:1px solid #d8dce5;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.custom-theme .el-input-group__append:focus,.custom-theme .el-input-group__prepend:focus{outline:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-select,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-select{display:inline-block;margin:-20px}.custom-theme .el-input-group__append button.el-button,.custom-theme .el-input-group__append div.el-select .el-input__inner,.custom-theme .el-input-group__append div.el-select:hover .el-input__inner,.custom-theme .el-input-group__prepend button.el-button,.custom-theme .el-input-group__prepend div.el-select .el-input__inner,.custom-theme .el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-input,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-input{font-size:inherit}.custom-theme .el-input-group__prepend{border-right:0;border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-input-group__append{border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--prepend .el-input__inner{border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--append .el-input__inner{border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-textarea{display:inline-block;width:100%;vertical-align:bottom}.custom-theme .el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;font-size:14px;color:#5a5e66;background-color:#fff;background-image:none;border:1px solid #d8dce5;border-radius:4px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.custom-theme .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:hover{border-color:#b4bccc}.custom-theme .el-textarea__inner:focus{outline:0;border-color:#262729}.custom-theme .el-textarea.is-disabled .el-textarea__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-scrollbar{overflow:hidden;position:relative}.custom-theme .el-scrollbar:active>.el-scrollbar__bar,.custom-theme .el-scrollbar:focus>.el-scrollbar__bar,.custom-theme .el-scrollbar:hover>.el-scrollbar__bar{opacity:1;-webkit-transition:opacity 340ms ease-out;transition:opacity 340ms ease-out}.custom-theme .el-scrollbar__wrap{overflow:scroll;height:100%}.custom-theme .el-scrollbar__wrap--hidden-default::-webkit-scrollbar{width:0;height:0}.custom-theme .el-scrollbar__thumb{position:relative;display:block;width:0;height:0;cursor:pointer;border-radius:inherit;background-color:rgba(135,141,153,.3);-webkit-transition:.3s background-color;transition:.3s background-color}.custom-theme .el-scrollbar__thumb:hover{background-color:rgba(135,141,153,.5)}.custom-theme .el-scrollbar__bar{position:absolute;right:2px;bottom:2px;z-index:1;border-radius:4px;opacity:0;-webkit-transition:opacity 120ms ease-out;transition:opacity 120ms ease-out}.custom-theme .el-scrollbar__bar.is-vertical{width:6px;top:2px}.custom-theme .el-scrollbar__bar.is-vertical>div{width:100%}.custom-theme .el-scrollbar__bar.is-horizontal{height:6px;left:2px}.custom-theme .el-scrollbar__bar.is-horizontal>div{height:100%}.custom-theme .el-popper .popper__arrow,.custom-theme .el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.custom-theme .el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03));filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.custom-theme .el-popper .popper__arrow::after{content:" ";border-width:6px}.custom-theme .el-popper[x-placement^=top]{margin-bottom:12px}.custom-theme .el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#e6ebf5;border-bottom-width:0}.custom-theme .el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#fff;border-bottom-width:0}.custom-theme .el-popper[x-placement^=bottom]{margin-top:12px}.custom-theme .el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#e6ebf5}.custom-theme .el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#fff}.custom-theme .el-popper[x-placement^=right]{margin-left:12px}.custom-theme .el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#e6ebf5;border-left-width:0}.custom-theme .el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#fff;border-left-width:0}.custom-theme .el-popper[x-placement^=left]{margin-right:12px}.custom-theme .el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#e6ebf5}.custom-theme .el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#fff}.custom-theme .el-autocomplete{position:relative;display:inline-block}.custom-theme .el-autocomplete-suggestion{margin:5px 0;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);border-radius:4px}.custom-theme .el-autocomplete-suggestion.el-popper .popper__arrow{left:24px!important}.custom-theme .el-autocomplete-suggestion__wrap{max-height:280px;padding:10px 0;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:auto;background-color:#fff;border:1px solid #dfe4ed;border-radius:4px}.custom-theme .el-autocomplete-suggestion__list{margin:0;padding:0}.custom-theme .el-autocomplete-suggestion li{padding:0 20px;margin:0;line-height:34px;cursor:pointer;color:#5a5e66;font-size:14px;list-style:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.custom-theme .el-autocomplete-suggestion li:hover{background-color:#f5f7fa}.custom-theme .el-autocomplete-suggestion li.highlighted{background-color:#f5f7fa}.custom-theme .el-autocomplete-suggestion li.divider{margin-top:6px;border-top:1px solid #000}.custom-theme .el-autocomplete-suggestion li.divider:last-child{margin-bottom:-6px}.custom-theme .el-autocomplete-suggestion.is-loading li{text-align:center;height:100px;line-height:100px;font-size:20px;color:#999}.custom-theme .el-autocomplete-suggestion.is-loading li::after{display:inline-block;content:"";height:100%;vertical-align:middle}.custom-theme .el-autocomplete-suggestion.is-loading li:hover{background-color:#fff}.custom-theme .el-autocomplete-suggestion.is-loading .el-icon-loading{vertical-align:middle}.custom-theme .el-button{display:inline-block;line-height:1;white-space:nowrap;cursor:pointer;background:#fff;border:1px solid #d8dce5;border-color:#d8dce5;color:#5a5e66;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;-webkit-transition:.1s;transition:.1s;font-weight:500;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;padding:12px 20px;font-size:14px;border-radius:4px}.custom-theme .el-button+.el-button{margin-left:10px}.custom-theme .el-button.is-round{padding:12px 20px}.custom-theme .el-button:focus,.custom-theme .el-button:hover{color:#262729;border-color:#bebebf;background-color:#e9e9ea}.custom-theme .el-button:active{color:#222325;border-color:#222325;outline:0}.custom-theme .el-button::-moz-focus-inner{border:0}.custom-theme .el-button [class*=el-icon-]+span{margin-left:5px}.custom-theme .el-button.is-plain:focus,.custom-theme .el-button.is-plain:hover{background:#fff;border-color:#262729;color:#262729}.custom-theme .el-button.is-plain:active{background:#fff;border-color:#222325;color:#222325;outline:0}.custom-theme .el-button.is-active{color:#222325;border-color:#222325}.custom-theme .el-button.is-disabled,.custom-theme .el-button.is-disabled:focus,.custom-theme .el-button.is-disabled:hover{color:#b4bccc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#e6ebf5}.custom-theme .el-button.is-disabled.el-button--text{background-color:transparent}.custom-theme .el-button.is-disabled.is-plain,.custom-theme .el-button.is-disabled.is-plain:focus,.custom-theme .el-button.is-disabled.is-plain:hover{background-color:#fff;border-color:#e6ebf5;color:#b4bccc}.custom-theme .el-button.is-loading{position:relative;pointer-events:none}.custom-theme .el-button.is-loading:before{pointer-events:none;content:'';position:absolute;left:-1px;top:-1px;right:-1px;bottom:-1px;border-radius:inherit;background-color:rgba(255,255,255,.35)}.custom-theme .el-button.is-round{border-radius:20px;padding:12px 23px}.custom-theme .el-button--primary{color:#fff;background-color:#262729;border-color:#262729}.custom-theme .el-button--primary:focus,.custom-theme .el-button--primary:hover{background:#515254;border-color:#515254;color:#fff}.custom-theme .el-button--primary:active{background:#222325;border-color:#222325;color:#fff;outline:0}.custom-theme .el-button--primary.is-active{background:#222325;border-color:#222325;color:#fff}.custom-theme .el-button--primary.is-disabled,.custom-theme .el-button--primary.is-disabled:active,.custom-theme .el-button--primary.is-disabled:focus,.custom-theme .el-button--primary.is-disabled:hover{color:#fff;background-color:#939394;border-color:#939394}.custom-theme .el-button--primary.is-plain{color:#262729;background:#e9e9ea;border-color:#a8a9a9}.custom-theme .el-button--primary.is-plain:focus,.custom-theme .el-button--primary.is-plain:hover{background:#262729;border-color:#262729;color:#fff}.custom-theme .el-button--primary.is-plain:active{background:#222325;border-color:#222325;color:#fff;outline:0}.custom-theme .el-button--primary.is-plain.is-disabled,.custom-theme .el-button--primary.is-plain.is-disabled:active,.custom-theme .el-button--primary.is-plain.is-disabled:focus,.custom-theme .el-button--primary.is-plain.is-disabled:hover{color:#7d7d7f;background-color:#e9e9ea;border-color:#d4d4d4}.custom-theme .el-button--success{color:#fff;background-color:#409167;border-color:#409167}.custom-theme .el-button--success:focus,.custom-theme .el-button--success:hover{background:#66a785;border-color:#66a785;color:#fff}.custom-theme .el-button--success:active{background:#3a835d;border-color:#3a835d;color:#fff;outline:0}.custom-theme .el-button--success.is-active{background:#3a835d;border-color:#3a835d;color:#fff}.custom-theme .el-button--success.is-disabled,.custom-theme .el-button--success.is-disabled:active,.custom-theme .el-button--success.is-disabled:focus,.custom-theme .el-button--success.is-disabled:hover{color:#fff;background-color:#a0c8b3;border-color:#a0c8b3}.custom-theme .el-button--success.is-plain{color:#409167;background:#ecf4f0;border-color:#b3d3c2}.custom-theme .el-button--success.is-plain:focus,.custom-theme .el-button--success.is-plain:hover{background:#409167;border-color:#409167;color:#fff}.custom-theme .el-button--success.is-plain:active{background:#3a835d;border-color:#3a835d;color:#fff;outline:0}.custom-theme .el-button--success.is-plain.is-disabled,.custom-theme .el-button--success.is-plain.is-disabled:active,.custom-theme .el-button--success.is-plain.is-disabled:focus,.custom-theme .el-button--success.is-plain.is-disabled:hover{color:#8cbda4;background-color:#ecf4f0;border-color:#d9e9e1}.custom-theme .el-button--warning{color:#fff;background-color:#9da408;border-color:#9da408}.custom-theme .el-button--warning:focus,.custom-theme .el-button--warning:hover{background:#b1b639;border-color:#b1b639;color:#fff}.custom-theme .el-button--warning:active{background:#8d9407;border-color:#8d9407;color:#fff;outline:0}.custom-theme .el-button--warning.is-active{background:#8d9407;border-color:#8d9407;color:#fff}.custom-theme .el-button--warning.is-disabled,.custom-theme .el-button--warning.is-disabled:active,.custom-theme .el-button--warning.is-disabled:focus,.custom-theme .el-button--warning.is-disabled:hover{color:#fff;background-color:#ced284;border-color:#ced284}.custom-theme .el-button--warning.is-plain{color:#9da408;background:#f5f6e6;border-color:#d8db9c}.custom-theme .el-button--warning.is-plain:focus,.custom-theme .el-button--warning.is-plain:hover{background:#9da408;border-color:#9da408;color:#fff}.custom-theme .el-button--warning.is-plain:active{background:#8d9407;border-color:#8d9407;color:#fff;outline:0}.custom-theme .el-button--warning.is-plain.is-disabled,.custom-theme .el-button--warning.is-plain.is-disabled:active,.custom-theme .el-button--warning.is-plain.is-disabled:focus,.custom-theme .el-button--warning.is-plain.is-disabled:hover{color:#c4c86b;background-color:#f5f6e6;border-color:#ebedce}.custom-theme .el-button--danger{color:#fff;background-color:#b3450e;border-color:#b3450e}.custom-theme .el-button--danger:focus,.custom-theme .el-button--danger:hover{background:#c26a3e;border-color:#c26a3e;color:#fff}.custom-theme .el-button--danger:active{background:#a13e0d;border-color:#a13e0d;color:#fff;outline:0}.custom-theme .el-button--danger.is-active{background:#a13e0d;border-color:#a13e0d;color:#fff}.custom-theme .el-button--danger.is-disabled,.custom-theme .el-button--danger.is-disabled:active,.custom-theme .el-button--danger.is-disabled:focus,.custom-theme .el-button--danger.is-disabled:hover{color:#fff;background-color:#d9a287;border-color:#d9a287}.custom-theme .el-button--danger.is-plain{color:#b3450e;background:#f7ece7;border-color:#e1b59f}.custom-theme .el-button--danger.is-plain:focus,.custom-theme .el-button--danger.is-plain:hover{background:#b3450e;border-color:#b3450e;color:#fff}.custom-theme .el-button--danger.is-plain:active{background:#a13e0d;border-color:#a13e0d;color:#fff;outline:0}.custom-theme .el-button--danger.is-plain.is-disabled,.custom-theme .el-button--danger.is-plain.is-disabled:active,.custom-theme .el-button--danger.is-plain.is-disabled:focus,.custom-theme .el-button--danger.is-plain.is-disabled:hover{color:#d18f6e;background-color:#f7ece7;border-color:#f0dacf}.custom-theme .el-button--info{color:#fff;background-color:#0a76a4;border-color:#0a76a4}.custom-theme .el-button--info:focus,.custom-theme .el-button--info:hover{background:#3b91b6;border-color:#3b91b6;color:#fff}.custom-theme .el-button--info:active{background:#096a94;border-color:#096a94;color:#fff;outline:0}.custom-theme .el-button--info.is-active{background:#096a94;border-color:#096a94;color:#fff}.custom-theme .el-button--info.is-disabled,.custom-theme .el-button--info.is-disabled:active,.custom-theme .el-button--info.is-disabled:focus,.custom-theme .el-button--info.is-disabled:hover{color:#fff;background-color:#85bbd2;border-color:#85bbd2}.custom-theme .el-button--info.is-plain{color:#0a76a4;background:#e7f1f6;border-color:#9dc8db}.custom-theme .el-button--info.is-plain:focus,.custom-theme .el-button--info.is-plain:hover{background:#0a76a4;border-color:#0a76a4;color:#fff}.custom-theme .el-button--info.is-plain:active{background:#096a94;border-color:#096a94;color:#fff;outline:0}.custom-theme .el-button--info.is-plain.is-disabled,.custom-theme .el-button--info.is-plain.is-disabled:active,.custom-theme .el-button--info.is-plain.is-disabled:focus,.custom-theme .el-button--info.is-plain.is-disabled:hover{color:#6cadc8;background-color:#e7f1f6;border-color:#cee4ed}.custom-theme .el-button--medium{padding:10px 20px;font-size:14px;border-radius:4px}.custom-theme .el-button--medium.is-round{padding:10px 20px}.custom-theme .el-button--small{padding:9px 15px;font-size:12px;border-radius:3px}.custom-theme .el-button--small.is-round{padding:9px 15px}.custom-theme .el-button--mini{padding:7px 15px;font-size:12px;border-radius:3px}.custom-theme .el-button--mini.is-round{padding:7px 15px}.custom-theme .el-button--text{border:none;color:#262729;background:0 0;padding-left:0;padding-right:0}.custom-theme .el-button--text:focus,.custom-theme .el-button--text:hover{color:#515254;border-color:transparent;background-color:transparent}.custom-theme .el-button--text:active{color:#222325;border-color:transparent;background-color:transparent}.custom-theme .el-button-group{display:inline-block;vertical-align:middle}.custom-theme .el-button-group::after,.custom-theme .el-button-group::before{display:table;content:""}.custom-theme .el-button-group::after{clear:both}.custom-theme .el-button-group .el-button{float:left;position:relative}.custom-theme .el-button-group .el-button+.el-button{margin-left:0}.custom-theme .el-button-group .el-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-button-group .el-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-button-group .el-button:not(:first-child):not(:last-child){border-radius:0}.custom-theme .el-button-group .el-button:not(:last-child){margin-right:-1px}.custom-theme .el-button-group .el-button:active,.custom-theme .el-button-group .el-button:focus,.custom-theme .el-button-group .el-button:hover{z-index:1}.custom-theme .el-button-group .el-button.is-active{z-index:1}.custom-theme .el-button-group .el-button--primary:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--primary:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--primary:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--success:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--success:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--success:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--warning:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--warning:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--warning:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--danger:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--danger:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--danger:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--info:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--info:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--info:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-popper .popper__arrow,.custom-theme .el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.custom-theme .el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03));filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.custom-theme .el-popper .popper__arrow::after{content:" ";border-width:6px}.custom-theme .el-popper[x-placement^=top]{margin-bottom:12px}.custom-theme .el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#e6ebf5;border-bottom-width:0}.custom-theme .el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#fff;border-bottom-width:0}.custom-theme .el-popper[x-placement^=bottom]{margin-top:12px}.custom-theme .el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#e6ebf5}.custom-theme .el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#fff}.custom-theme .el-popper[x-placement^=right]{margin-left:12px}.custom-theme .el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#e6ebf5;border-left-width:0}.custom-theme .el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#fff;border-left-width:0}.custom-theme .el-popper[x-placement^=left]{margin-right:12px}.custom-theme .el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#e6ebf5}.custom-theme .el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#fff}.custom-theme .el-dropdown{display:inline-block;position:relative;color:#5a5e66;font-size:14px}.custom-theme .el-dropdown .el-button-group{display:block}.custom-theme .el-dropdown .el-button-group .el-button{float:none}.custom-theme .el-dropdown .el-dropdown__caret-button{padding-left:5px;padding-right:5px;position:relative;border-left:none}.custom-theme .el-dropdown .el-dropdown__caret-button::before{content:'';position:absolute;display:block;width:1px;top:5px;bottom:5px;left:0;background:rgba(255,255,255,.5)}.custom-theme .el-dropdown .el-dropdown__caret-button:hover::before{top:0;bottom:0}.custom-theme .el-dropdown .el-dropdown__caret-button .el-dropdown__icon{padding-left:0}.custom-theme .el-dropdown__icon{font-size:12px;margin:0 3px}.custom-theme .el-dropdown-menu{position:absolute;top:0;left:0;z-index:10;padding:10px 0;margin:5px 0;background-color:#fff;border:1px solid #e6ebf5;border-radius:4px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.custom-theme .el-dropdown-menu__item{list-style:none;line-height:36px;padding:0 20px;margin:0;font-size:14px;color:#5a5e66;cursor:pointer}.custom-theme .el-dropdown-menu__item:not(.is-disabled):hover{background-color:#e9e9ea;color:#515254}.custom-theme .el-dropdown-menu__item--divided{position:relative;margin-top:6px;border-top:1px solid #e6ebf5}.custom-theme .el-dropdown-menu__item--divided:before{content:'';height:6px;display:block;margin:0 -20px;background-color:#fff}.custom-theme .el-dropdown-menu__item.is-disabled{cursor:default;color:#bbb;pointer-events:none}.custom-theme .el-dropdown-menu--medium{padding:6px 0}.custom-theme .el-dropdown-menu--medium .el-dropdown-menu__item{line-height:30px;padding:0 17px;font-size:14px}.custom-theme .el-dropdown-menu--medium .el-dropdown-menu__item.el-dropdown-menu__item--divided{margin-top:6px}.custom-theme .el-dropdown-menu--medium .el-dropdown-menu__item.el-dropdown-menu__item--divided:before{height:6px;margin:0 -17px}.custom-theme .el-dropdown-menu--small{padding:6px 0}.custom-theme .el-dropdown-menu--small .el-dropdown-menu__item{line-height:27px;padding:0 15px;font-size:13px}.custom-theme .el-dropdown-menu--small .el-dropdown-menu__item.el-dropdown-menu__item--divided{margin-top:4px}.custom-theme .el-dropdown-menu--small .el-dropdown-menu__item.el-dropdown-menu__item--divided:before{height:4px;margin:0 -15px}.custom-theme .el-dropdown-menu--mini{padding:3px 0}.custom-theme .el-dropdown-menu--mini .el-dropdown-menu__item{line-height:24px;padding:0 10px;font-size:12px}.custom-theme .el-dropdown-menu--mini .el-dropdown-menu__item.el-dropdown-menu__item--divided{margin-top:3px}.custom-theme .el-dropdown-menu--mini .el-dropdown-menu__item.el-dropdown-menu__item--divided:before{height:3px;margin:0 -10px}.custom-theme .el-menu{border-right:solid 1px #e6e6e6;list-style:none;position:relative;margin:0;padding-left:0;background-color:#fff}.custom-theme .el-menu::after,.custom-theme .el-menu::before{display:table;content:""}.custom-theme .el-menu::after{clear:both}.custom-theme .el-menu li{list-style:none}.custom-theme .el-menu--horizontal{border-right:none;border-bottom:solid 1px #e6e6e6}.custom-theme .el-menu--horizontal .el-menu-item{float:left;height:60px;line-height:60px;margin:0;cursor:pointer;position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;border-bottom:2px solid transparent;color:#878d99}.custom-theme .el-menu--horizontal .el-menu-item a,.custom-theme .el-menu--horizontal .el-menu-item a:hover{color:inherit}.custom-theme .el-menu--horizontal .el-menu-item:focus,.custom-theme .el-menu--horizontal .el-menu-item:hover{background-color:#fff}.custom-theme .el-menu--horizontal .el-submenu{float:left;position:relative}.custom-theme .el-menu--horizontal .el-submenu:focus{outline:0}.custom-theme .el-menu--horizontal .el-submenu:focus>.el-submenu__title{color:#2d2f33}.custom-theme .el-menu--horizontal .el-submenu>.el-menu{position:absolute;top:65px;left:0;border:none;padding:5px 0;background-color:#fff;z-index:100;min-width:100%;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);border-radius:2px}.custom-theme .el-menu--horizontal .el-submenu .el-submenu__title{height:60px;line-height:60px;border-bottom:2px solid transparent;color:#878d99}.custom-theme .el-menu--horizontal .el-submenu .el-submenu__title:hover{background-color:#fff}.custom-theme .el-menu--horizontal .el-submenu .el-menu-item{background-color:#fff;float:none;height:36px;line-height:36px;padding:0 10px}.custom-theme .el-menu--horizontal .el-submenu .el-submenu__icon-arrow{position:static;vertical-align:middle;margin-left:8px;margin-top:-3px}.custom-theme .el-menu--horizontal .el-menu-item:focus,.custom-theme .el-menu--horizontal .el-menu-item:hover,.custom-theme .el-menu--horizontal .el-submenu__title:hover{outline:0;color:#2d2f33}.custom-theme .el-menu--horizontal>.el-menu-item.is-active,.custom-theme .el-menu--horizontal>.el-submenu.is-active .el-submenu__title{border-bottom:2px solid #262729;color:#2d2f33}.custom-theme .el-menu--collapse{width:64px}.custom-theme .el-menu--collapse>.el-menu-item [class^=el-icon-],.custom-theme .el-menu--collapse>.el-submenu>.el-submenu__title [class^=el-icon-]{margin:0;vertical-align:middle;width:24px;text-align:center}.custom-theme .el-menu--collapse>.el-menu-item .el-submenu__icon-arrow,.custom-theme .el-menu--collapse>.el-submenu>.el-submenu__title .el-submenu__icon-arrow{display:none}.custom-theme .el-menu--collapse>.el-menu-item span,.custom-theme .el-menu--collapse>.el-submenu>.el-submenu__title span{height:0;width:0;overflow:hidden;visibility:hidden;display:inline-block}.custom-theme .el-menu--collapse>.el-menu-item.is-active i{color:inherit}.custom-theme .el-menu--collapse .el-menu .el-submenu{min-width:200px}.custom-theme .el-menu--collapse .el-submenu{position:relative}.custom-theme .el-menu--collapse .el-submenu .el-menu{position:absolute;margin-left:5px;top:0;left:100%;z-index:10;border:1px solid #dfe4ed;border-radius:2px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.custom-theme .el-menu--collapse .el-submenu.is-opened>.el-submenu__title .el-submenu__icon-arrow{-webkit-transform:none;transform:none}.custom-theme .el-menu-item{height:56px;line-height:56px;font-size:14px;color:#2d2f33;padding:0 20px;cursor:pointer;position:relative;-webkit-transition:border-color .3s,background-color .3s,color .3s;transition:border-color .3s,background-color .3s,color .3s;-webkit-box-sizing:border-box;box-sizing:border-box;white-space:nowrap}.custom-theme .el-menu-item [class^=el-icon-]{margin-right:5px;width:24px;text-align:center;font-size:18px}.custom-theme .el-menu-item *{vertical-align:middle}.custom-theme .el-menu-item:first-child{margin-left:0}.custom-theme .el-menu-item:last-child{margin-right:0}.custom-theme .el-menu-item:focus,.custom-theme .el-menu-item:hover{outline:0;background-color:#e9e9ea}.custom-theme .el-menu-item i{color:#878d99}.custom-theme .el-menu-item.is-active{color:#262729}.custom-theme .el-menu-item.is-active i{color:inherit}.custom-theme .el-submenu__title{position:relative;height:56px;line-height:56px;font-size:14px;color:#2d2f33;padding:0 20px;cursor:pointer;position:relative;-webkit-transition:border-color .3s,background-color .3s,color .3s;transition:border-color .3s,background-color .3s,color .3s;-webkit-box-sizing:border-box;box-sizing:border-box;white-space:nowrap}.custom-theme .el-submenu__title *{vertical-align:middle}.custom-theme .el-submenu__title i{color:#878d99}.custom-theme .el-submenu__title:hover{background-color:#e9e9ea}.custom-theme .el-submenu .el-menu{border:none}.custom-theme .el-submenu .el-menu-item{height:50px;line-height:50px;padding:0 45px;min-width:200px}.custom-theme .el-submenu__icon-arrow{position:absolute;top:50%;right:20px;margin-top:-7px;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;font-size:12px}.custom-theme .el-submenu.is-active .el-submenu__title{border-bottom-color:#262729}.custom-theme .el-submenu.is-opened>.el-submenu__title .el-submenu__icon-arrow{-webkit-transform:rotateZ(180deg);transform:rotateZ(180deg)}.custom-theme .el-submenu [class^=el-icon-]{vertical-align:middle;margin-right:5px;width:24px;text-align:center;font-size:18px}.custom-theme .el-menu-item-group>ul{padding:0}.custom-theme .el-menu-item-group__title{padding:7px 0 7px 20px;line-height:normal;font-size:12px;color:#878d99}.custom-theme .horizontal-collapse-transition .el-submenu__title .el-submenu__icon-arrow{-webkit-transition:.2s;transition:.2s;opacity:0}.custom-theme .el-input{position:relative;font-size:14px;display:inline-block;width:100%}.custom-theme .el-input::-webkit-scrollbar{z-index:11;width:6px}.custom-theme .el-input::-webkit-scrollbar:horizontal{height:6px}.custom-theme .el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.custom-theme .el-input::-webkit-scrollbar-corner{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.custom-theme .el-input__inner{-webkit-appearance:none;background-color:#fff;background-image:none;border-radius:4px;border:1px solid #d8dce5;-webkit-box-sizing:border-box;box-sizing:border-box;color:#5a5e66;display:inline-block;font-size:inherit;height:40px;line-height:1;outline:0;padding:0 15px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.custom-theme .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input__inner:hover{border-color:#b4bccc}.custom-theme .el-input__inner:focus{outline:0;border-color:#262729}.custom-theme .el-input__suffix{position:absolute;height:100%;right:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s;pointer-events:none}.custom-theme .el-input__suffix-inner{pointer-events:all}.custom-theme .el-input__prefix{position:absolute;height:100%;left:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s}.custom-theme .el-input__icon{height:100%;width:25px;text-align:center;-webkit-transition:all .3s;transition:all .3s;line-height:40px}.custom-theme .el-input__icon:after{content:'';height:100%;width:0;display:inline-block;vertical-align:middle}.custom-theme .el-input__validateIcon{pointer-events:none}.custom-theme .el-input.is-active .el-input__inner{outline:0;border-color:#262729}.custom-theme .el-input.is-disabled .el-input__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-input.is-disabled .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__icon{cursor:not-allowed}.custom-theme .el-input--suffix .el-input__inner{padding-right:30px}.custom-theme .el-input--prefix .el-input__inner{padding-left:30px}.custom-theme .el-input--medium{font-size:14px}.custom-theme .el-input--medium .el-input__inner{height:36px}.custom-theme .el-input--medium .el-input__icon{line-height:36px}.custom-theme .el-input--small{font-size:13px}.custom-theme .el-input--small .el-input__inner{height:32px}.custom-theme .el-input--small .el-input__icon{line-height:32px}.custom-theme .el-input--mini{font-size:12px}.custom-theme .el-input--mini .el-input__inner{height:28px}.custom-theme .el-input--mini .el-input__icon{line-height:28px}.custom-theme .el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate}.custom-theme .el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.custom-theme .el-input-group__append,.custom-theme .el-input-group__prepend{background-color:#f5f7fa;color:#0a76a4;vertical-align:middle;display:table-cell;position:relative;border:1px solid #d8dce5;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.custom-theme .el-input-group__append:focus,.custom-theme .el-input-group__prepend:focus{outline:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-select,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-select{display:inline-block;margin:-20px}.custom-theme .el-input-group__append button.el-button,.custom-theme .el-input-group__append div.el-select .el-input__inner,.custom-theme .el-input-group__append div.el-select:hover .el-input__inner,.custom-theme .el-input-group__prepend button.el-button,.custom-theme .el-input-group__prepend div.el-select .el-input__inner,.custom-theme .el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-input,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-input{font-size:inherit}.custom-theme .el-input-group__prepend{border-right:0;border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-input-group__append{border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--prepend .el-input__inner{border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--append .el-input__inner{border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-textarea{display:inline-block;width:100%;vertical-align:bottom}.custom-theme .el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;font-size:14px;color:#5a5e66;background-color:#fff;background-image:none;border:1px solid #d8dce5;border-radius:4px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.custom-theme .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:hover{border-color:#b4bccc}.custom-theme .el-textarea__inner:focus{outline:0;border-color:#262729}.custom-theme .el-textarea.is-disabled .el-textarea__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-input{position:relative;font-size:14px;display:inline-block;width:100%}.custom-theme .el-input::-webkit-scrollbar{z-index:11;width:6px}.custom-theme .el-input::-webkit-scrollbar:horizontal{height:6px}.custom-theme .el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.custom-theme .el-input::-webkit-scrollbar-corner{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.custom-theme .el-input__inner{-webkit-appearance:none;background-color:#fff;background-image:none;border-radius:4px;border:1px solid #d8dce5;-webkit-box-sizing:border-box;box-sizing:border-box;color:#5a5e66;display:inline-block;font-size:inherit;height:40px;line-height:1;outline:0;padding:0 15px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.custom-theme .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input__inner:hover{border-color:#b4bccc}.custom-theme .el-input__inner:focus{outline:0;border-color:#262729}.custom-theme .el-input__suffix{position:absolute;height:100%;right:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s;pointer-events:none}.custom-theme .el-input__suffix-inner{pointer-events:all}.custom-theme .el-input__prefix{position:absolute;height:100%;left:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s}.custom-theme .el-input__icon{height:100%;width:25px;text-align:center;-webkit-transition:all .3s;transition:all .3s;line-height:40px}.custom-theme .el-input__icon:after{content:'';height:100%;width:0;display:inline-block;vertical-align:middle}.custom-theme .el-input__validateIcon{pointer-events:none}.custom-theme .el-input.is-active .el-input__inner{outline:0;border-color:#262729}.custom-theme .el-input.is-disabled .el-input__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-input.is-disabled .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__icon{cursor:not-allowed}.custom-theme .el-input--suffix .el-input__inner{padding-right:30px}.custom-theme .el-input--prefix .el-input__inner{padding-left:30px}.custom-theme .el-input--medium{font-size:14px}.custom-theme .el-input--medium .el-input__inner{height:36px}.custom-theme .el-input--medium .el-input__icon{line-height:36px}.custom-theme .el-input--small{font-size:13px}.custom-theme .el-input--small .el-input__inner{height:32px}.custom-theme .el-input--small .el-input__icon{line-height:32px}.custom-theme .el-input--mini{font-size:12px}.custom-theme .el-input--mini .el-input__inner{height:28px}.custom-theme .el-input--mini .el-input__icon{line-height:28px}.custom-theme .el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate}.custom-theme .el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.custom-theme .el-input-group__append,.custom-theme .el-input-group__prepend{background-color:#f5f7fa;color:#0a76a4;vertical-align:middle;display:table-cell;position:relative;border:1px solid #d8dce5;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.custom-theme .el-input-group__append:focus,.custom-theme .el-input-group__prepend:focus{outline:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-select,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-select{display:inline-block;margin:-20px}.custom-theme .el-input-group__append button.el-button,.custom-theme .el-input-group__append div.el-select .el-input__inner,.custom-theme .el-input-group__append div.el-select:hover .el-input__inner,.custom-theme .el-input-group__prepend button.el-button,.custom-theme .el-input-group__prepend div.el-select .el-input__inner,.custom-theme .el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-input,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-input{font-size:inherit}.custom-theme .el-input-group__prepend{border-right:0;border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-input-group__append{border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--prepend .el-input__inner{border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--append .el-input__inner{border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-textarea{display:inline-block;width:100%;vertical-align:bottom}.custom-theme .el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;font-size:14px;color:#5a5e66;background-color:#fff;background-image:none;border:1px solid #d8dce5;border-radius:4px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.custom-theme .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:hover{border-color:#b4bccc}.custom-theme .el-textarea__inner:focus{outline:0;border-color:#262729}.custom-theme .el-textarea.is-disabled .el-textarea__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-input-number{position:relative;display:inline-block;width:180px;line-height:38px}.custom-theme .el-input-number .el-input{display:block}.custom-theme .el-input-number .el-input__inner{-webkit-appearance:none;padding-left:50px;padding-right:50px;text-align:center}.custom-theme .el-input-number__decrease,.custom-theme .el-input-number__increase{position:absolute;z-index:1;top:1px;width:40px;height:auto;text-align:center;background:#f5f7fa;color:#5a5e66;cursor:pointer;font-size:13px}.custom-theme .el-input-number__decrease:hover,.custom-theme .el-input-number__increase:hover{color:#262729}.custom-theme .el-input-number__decrease:hover:not(.is-disabled)~.el-input .el-input__inner:not(.is-disabled),.custom-theme .el-input-number__increase:hover:not(.is-disabled)~.el-input .el-input__inner:not(.is-disabled){border-color:#262729}.custom-theme .el-input-number__decrease.is-disabled,.custom-theme .el-input-number__increase.is-disabled{color:#b4bccc;cursor:not-allowed}.custom-theme .el-input-number__increase{right:1px;border-radius:0 4px 4px 0;border-left:1px solid #d8dce5}.custom-theme .el-input-number__decrease{left:1px;border-radius:4px 0 0 4px;border-right:1px solid #d8dce5}.custom-theme .el-input-number.is-disabled .el-input-number__decrease,.custom-theme .el-input-number.is-disabled .el-input-number__increase{border-color:#dfe4ed;color:#dfe4ed}.custom-theme .el-input-number.is-disabled .el-input-number__decrease:hover,.custom-theme .el-input-number.is-disabled .el-input-number__increase:hover{color:#dfe4ed;cursor:not-allowed}.custom-theme .el-input-number--medium{width:200px;line-height:34px}.custom-theme .el-input-number--medium .el-input-number__decrease,.custom-theme .el-input-number--medium .el-input-number__increase{width:36px;font-size:14px}.custom-theme .el-input-number--medium .el-input__inner{padding-left:43px;padding-right:43px}.custom-theme .el-input-number--small{width:130px;line-height:30px}.custom-theme .el-input-number--small .el-input-number__decrease,.custom-theme .el-input-number--small .el-input-number__increase{width:32px;font-size:13px}.custom-theme .el-input-number--small .el-input-number__decrease [class*=el-icon],.custom-theme .el-input-number--small .el-input-number__increase [class*=el-icon]{-webkit-transform:scale(.9);transform:scale(.9)}.custom-theme .el-input-number--small .el-input__inner{padding-left:39px;padding-right:39px}.custom-theme .el-input-number--mini{width:130px;line-height:26px}.custom-theme .el-input-number--mini .el-input-number__decrease,.custom-theme .el-input-number--mini .el-input-number__increase{width:28px;font-size:12px}.custom-theme .el-input-number--mini .el-input-number__decrease [class*=el-icon],.custom-theme .el-input-number--mini .el-input-number__increase [class*=el-icon]{-webkit-transform:scale(.8);transform:scale(.8)}.custom-theme .el-input-number--mini .el-input__inner{padding-left:35px;padding-right:35px}.custom-theme .el-input-number.is-without-controls .el-input__inner{padding-left:15px;padding-right:15px}.custom-theme .el-input-number.is-controls-right .el-input__inner{padding-left:15px;padding-right:50px}.custom-theme .el-input-number.is-controls-right .el-input-number__decrease,.custom-theme .el-input-number.is-controls-right .el-input-number__increase{height:auto;line-height:19px}.custom-theme .el-input-number.is-controls-right .el-input-number__decrease [class*=el-icon],.custom-theme .el-input-number.is-controls-right .el-input-number__increase [class*=el-icon]{-webkit-transform:scale(.8);transform:scale(.8)}.custom-theme .el-input-number.is-controls-right .el-input-number__increase{border-radius:0 4px 0 0;border-bottom:1px solid #d8dce5}.custom-theme .el-input-number.is-controls-right .el-input-number__decrease{right:1px;bottom:1px;top:auto;left:auto;border-right:none;border-left:1px solid #d8dce5;border-radius:0 0 4px 0}.custom-theme .el-input-number.is-controls-right[class*=medium] [class*=decrease],.custom-theme .el-input-number.is-controls-right[class*=medium] [class*=increase]{line-height:17px}.custom-theme .el-input-number.is-controls-right[class*=small] [class*=decrease],.custom-theme .el-input-number.is-controls-right[class*=small] [class*=increase]{line-height:15px}.custom-theme .el-input-number.is-controls-right[class*=mini] [class*=decrease],.custom-theme .el-input-number.is-controls-right[class*=mini] [class*=increase]{line-height:13px}.custom-theme .el-radio{color:#5a5e66;font-weight:500;line-height:1;position:relative;cursor:pointer;display:inline-block;white-space:nowrap;outline:0;font-size:14px;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.custom-theme .el-radio.is-bordered{padding:10px 20px 10px 10px;border-radius:4px;border:1px solid #d8dce5}.custom-theme .el-radio.is-bordered.is-checked{border-color:#262729}.custom-theme .el-radio.is-bordered.is-disabled{cursor:not-allowed;border-color:#e6ebf5}.custom-theme .el-radio.is-bordered+.el-radio.is-bordered{margin-left:10px}.custom-theme .el-radio--medium.is-bordered{padding:8px 20px 8px 10px;border-radius:4px}.custom-theme .el-radio--medium.is-bordered .el-radio__label{font-size:14px}.custom-theme .el-radio--medium.is-bordered .el-radio__inner{height:14px;width:14px}.custom-theme .el-radio--small.is-bordered{padding:6px 15px 6px 10px;border-radius:3px}.custom-theme .el-radio--small.is-bordered .el-radio__label{font-size:12px}.custom-theme .el-radio--small.is-bordered .el-radio__inner{height:12px;width:12px}.custom-theme .el-radio--mini.is-bordered{padding:4px 15px 4px 10px;border-radius:3px}.custom-theme .el-radio--mini.is-bordered .el-radio__label{font-size:12px}.custom-theme .el-radio--mini.is-bordered .el-radio__inner{height:12px;width:12px}.custom-theme .el-radio+.el-radio{margin-left:30px}.custom-theme .el-radio__input{white-space:nowrap;cursor:pointer;outline:0;display:inline-block;line-height:1;position:relative;vertical-align:middle}.custom-theme .el-radio__input.is-disabled .el-radio__inner{background-color:#f5f7fa;border-color:#dfe4ed;cursor:not-allowed}.custom-theme .el-radio__input.is-disabled .el-radio__inner::after{cursor:not-allowed;background-color:#f5f7fa}.custom-theme .el-radio__input.is-disabled .el-radio__inner+.el-radio__label{cursor:not-allowed}.custom-theme .el-radio__input.is-disabled.is-checked .el-radio__inner{background-color:#f5f7fa;border-color:#dfe4ed}.custom-theme .el-radio__input.is-disabled.is-checked .el-radio__inner::after{background-color:#b4bccc}.custom-theme .el-radio__input.is-disabled+span.el-radio__label{color:#b4bccc;cursor:not-allowed}.custom-theme .el-radio__input.is-checked .el-radio__inner{border-color:#262729;background:#262729}.custom-theme .el-radio__input.is-checked .el-radio__inner::after{-webkit-transform:translate(-50%,-50%) scale(1);transform:translate(-50%,-50%) scale(1)}.custom-theme .el-radio__input.is-checked+.el-radio__label{color:#262729}.custom-theme .el-radio__input.is-focus .el-radio__inner{border-color:#262729}.custom-theme .el-radio__inner{border:1px solid #d8dce5;border-radius:100%;width:14px;height:14px;background-color:#fff;position:relative;cursor:pointer;display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-radio__inner:hover{border-color:#262729}.custom-theme .el-radio__inner::after{width:4px;height:4px;border-radius:100%;background-color:#fff;content:"";position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%) scale(0);transform:translate(-50%,-50%) scale(0);-webkit-transition:-webkit-transform .15s cubic-bezier(.71,-.46,.88,.6);transition:-webkit-transform .15s cubic-bezier(.71,-.46,.88,.6);transition:transform .15s cubic-bezier(.71,-.46,.88,.6);transition:transform .15s cubic-bezier(.71,-.46,.88,.6),-webkit-transform .15s cubic-bezier(.71,-.46,.88,.6)}.custom-theme .el-radio__original{opacity:0;outline:0;position:absolute;z-index:-1;top:0;left:0;right:0;bottom:0;margin:0}.custom-theme .el-radio:focus:not(.is-focus):not(:active) .el-radio__inner{-webkit-box-shadow:0 0 2px 2px #262729;box-shadow:0 0 2px 2px #262729}.custom-theme .el-radio__label{font-size:14px;padding-left:10px}.custom-theme .el-radio-group{display:inline-block;line-height:1;vertical-align:middle;font-size:0}.custom-theme .el-radio-button{position:relative;display:inline-block;outline:0}.custom-theme .el-radio-button__inner{display:inline-block;line-height:1;white-space:nowrap;vertical-align:middle;background:#fff;border:1px solid #d8dce5;font-weight:500;border-left:0;color:#5a5e66;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;position:relative;cursor:pointer;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);padding:12px 20px;font-size:14px;border-radius:0}.custom-theme .el-radio-button__inner.is-round{padding:12px 20px}.custom-theme .el-radio-button__inner:hover{color:#262729}.custom-theme .el-radio-button__inner [class*=el-icon-]{line-height:.9}.custom-theme .el-radio-button__inner [class*=el-icon-]+span{margin-left:5px}.custom-theme .el-radio-button__orig-radio{opacity:0;outline:0;position:absolute;z-index:-1;left:-999px}.custom-theme .el-radio-button__orig-radio:checked+.el-radio-button__inner{color:#fff;background-color:#262729;border-color:#262729;-webkit-box-shadow:-1px 0 0 0 #262729;box-shadow:-1px 0 0 0 #262729}.custom-theme .el-radio-button__orig-radio:disabled+.el-radio-button__inner{color:#b4bccc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#e6ebf5;-webkit-box-shadow:none;box-shadow:none}.custom-theme .el-radio-button__orig-radio:disabled:checked+.el-radio-button__inner{background-color:#edf2fc}.custom-theme .el-radio-button:first-child .el-radio-button__inner{border-left:1px solid #d8dce5;border-radius:4px 0 0 4px;-webkit-box-shadow:none!important;box-shadow:none!important}.custom-theme .el-radio-button:last-child .el-radio-button__inner{border-radius:0 4px 4px 0}.custom-theme .el-radio-button:first-child:last-child .el-radio-button__inner{border-radius:4px}.custom-theme .el-radio-button--medium .el-radio-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.custom-theme .el-radio-button--medium .el-radio-button__inner.is-round{padding:10px 20px}.custom-theme .el-radio-button--small .el-radio-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.custom-theme .el-radio-button--small .el-radio-button__inner.is-round{padding:9px 15px}.custom-theme .el-radio-button--mini .el-radio-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.custom-theme .el-radio-button--mini .el-radio-button__inner.is-round{padding:7px 15px}.custom-theme .el-radio-button:focus:not(.is-focus):not(:active){-webkit-box-shadow:0 0 2px 2px #262729;box-shadow:0 0 2px 2px #262729}.custom-theme .el-checkbox{color:#5a5e66;font-weight:500;font-size:14px;position:relative;cursor:pointer;display:inline-block;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.custom-theme .el-checkbox.is-bordered{padding:9px 20px 9px 10px;border-radius:4px;border:1px solid #d8dce5}.custom-theme .el-checkbox.is-bordered.is-checked{border-color:#262729}.custom-theme .el-checkbox.is-bordered.is-disabled{border-color:#e6ebf5;cursor:not-allowed}.custom-theme .el-checkbox.is-bordered+.el-checkbox.is-bordered{margin-left:10px}.custom-theme .el-checkbox.is-bordered.el-checkbox--medium{padding:7px 20px 7px 10px;border-radius:4px}.custom-theme .el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__label{line-height:17px;font-size:14px}.custom-theme .el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__inner{height:14px;width:14px}.custom-theme .el-checkbox.is-bordered.el-checkbox--small{padding:3px 15px 7px 10px;border-radius:3px}.custom-theme .el-checkbox.is-bordered.el-checkbox--small .el-checkbox__label{line-height:15px;font-size:12px}.custom-theme .el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner{height:12px;width:12px}.custom-theme .el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner::after{height:6px;width:2px}.custom-theme .el-checkbox.is-bordered.el-checkbox--mini{padding:1px 15px 5px 10px;border-radius:3px}.custom-theme .el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__label{line-height:12px;font-size:12px}.custom-theme .el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner{height:12px;width:12px}.custom-theme .el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner::after{height:6px;width:2px}.custom-theme .el-checkbox__input{white-space:nowrap;cursor:pointer;outline:0;display:inline-block;line-height:1;position:relative;vertical-align:middle}.custom-theme .el-checkbox__input.is-disabled .el-checkbox__inner{background-color:#edf2fc;border-color:#d8dce5;cursor:not-allowed}.custom-theme .el-checkbox__input.is-disabled .el-checkbox__inner::after{cursor:not-allowed;border-color:#b4bccc}.custom-theme .el-checkbox__input.is-disabled .el-checkbox__inner+.el-checkbox__label{cursor:not-allowed}.custom-theme .el-checkbox__input.is-disabled.is-checked .el-checkbox__inner{background-color:#edf2fc;border-color:#d8dce5}.custom-theme .el-checkbox__input.is-disabled.is-checked .el-checkbox__inner::after{border-color:#b4bccc}.custom-theme .el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner{background-color:#edf2fc;border-color:#d8dce5}.custom-theme .el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner::before{background-color:#b4bccc;border-color:#b4bccc}.custom-theme .el-checkbox__input.is-disabled+span.el-checkbox__label{color:#b4bccc;cursor:not-allowed}.custom-theme .el-checkbox__input.is-checked .el-checkbox__inner{background-color:#262729;border-color:#262729}.custom-theme .el-checkbox__input.is-checked .el-checkbox__inner::after{-webkit-transform:rotate(45deg) scaleY(1);transform:rotate(45deg) scaleY(1)}.custom-theme .el-checkbox__input.is-checked+.el-checkbox__label{color:#262729}.custom-theme .el-checkbox__input.is-focus .el-checkbox__inner{border-color:#262729}.custom-theme .el-checkbox__input.is-indeterminate .el-checkbox__inner{background-color:#262729;border-color:#262729}.custom-theme .el-checkbox__input.is-indeterminate .el-checkbox__inner::before{content:'';position:absolute;display:block;background-color:#fff;height:2px;-webkit-transform:scale(.5);transform:scale(.5);left:0;right:0;top:5px}.custom-theme .el-checkbox__input.is-indeterminate .el-checkbox__inner::after{display:none}.custom-theme .el-checkbox__inner{display:inline-block;position:relative;border:1px solid #d8dce5;border-radius:2px;-webkit-box-sizing:border-box;box-sizing:border-box;width:14px;height:14px;background-color:#fff;z-index:1;-webkit-transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46);transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46)}.custom-theme .el-checkbox__inner:hover{border-color:#262729}.custom-theme .el-checkbox__inner::after{-webkit-box-sizing:content-box;box-sizing:content-box;content:"";border:1px solid #fff;border-left:0;border-top:0;height:7px;left:4px;position:absolute;top:1px;-webkit-transform:rotate(45deg) scaleY(0);transform:rotate(45deg) scaleY(0);width:3px;-webkit-transition:-webkit-transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms;transition:-webkit-transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms;transition:transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms;transition:transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms,-webkit-transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms;-webkit-transform-origin:center;transform-origin:center}.custom-theme .el-checkbox__original{opacity:0;outline:0;position:absolute;margin:0;width:0;height:0;left:-999px}.custom-theme .el-checkbox__label{display:inline-block;padding-left:10px;line-height:19px;font-size:14px}.custom-theme .el-checkbox+.el-checkbox{margin-left:30px}.custom-theme .el-checkbox-button{position:relative;display:inline-block}.custom-theme .el-checkbox-button__inner{display:inline-block;line-height:1;font-weight:500;white-space:nowrap;vertical-align:middle;cursor:pointer;background:#fff;border:1px solid #d8dce5;border-left:0;color:#5a5e66;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;position:relative;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;padding:12px 20px;font-size:14px;border-radius:0}.custom-theme .el-checkbox-button__inner.is-round{padding:12px 20px}.custom-theme .el-checkbox-button__inner:hover{color:#262729}.custom-theme .el-checkbox-button__inner [class*=el-icon-]{line-height:.9}.custom-theme .el-checkbox-button__inner [class*=el-icon-]+span{margin-left:5px}.custom-theme .el-checkbox-button__original{opacity:0;outline:0;position:absolute;margin:0;left:-999px}.custom-theme .el-checkbox-button.is-checked .el-checkbox-button__inner{color:#fff;background-color:#262729;border-color:#262729;-webkit-box-shadow:-1px 0 0 0 #7d7d7f;box-shadow:-1px 0 0 0 #7d7d7f}.custom-theme .el-checkbox-button.is-disabled .el-checkbox-button__inner{color:#b4bccc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#e6ebf5;-webkit-box-shadow:none;box-shadow:none}.custom-theme .el-checkbox-button:first-child .el-checkbox-button__inner{border-left:1px solid #d8dce5;border-radius:4px 0 0 4px;-webkit-box-shadow:none!important;box-shadow:none!important}.custom-theme .el-checkbox-button.is-focus .el-checkbox-button__inner{border-color:#262729}.custom-theme .el-checkbox-button:last-child .el-checkbox-button__inner{border-radius:0 4px 4px 0}.custom-theme .el-checkbox-button--medium .el-checkbox-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.custom-theme .el-checkbox-button--medium .el-checkbox-button__inner.is-round{padding:10px 20px}.custom-theme .el-checkbox-button--small .el-checkbox-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.custom-theme .el-checkbox-button--small .el-checkbox-button__inner.is-round{padding:9px 15px}.custom-theme .el-checkbox-button--mini .el-checkbox-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.custom-theme .el-checkbox-button--mini .el-checkbox-button__inner.is-round{padding:7px 15px}.custom-theme .el-checkbox-group{font-size:0}.custom-theme .el-switch{display:inline-block;position:relative;font-size:14px;line-height:20px;height:20px;vertical-align:middle}.custom-theme .el-switch.is-disabled .el-switch__core,.custom-theme .el-switch.is-disabled .el-switch__label{cursor:not-allowed}.custom-theme .el-switch__label{-webkit-transition:.2s;transition:.2s;height:20px;display:inline-block;font-size:14px;font-weight:500;cursor:pointer;vertical-align:middle;color:#2d2f33}.custom-theme .el-switch__label.is-active{color:#262729}.custom-theme .el-switch__label--left{margin-right:10px}.custom-theme .el-switch__label--right{margin-left:10px}.custom-theme .el-switch__label *{line-height:1;font-size:14px;display:inline-block}.custom-theme .el-switch__input{position:absolute;width:0;height:0;opacity:0;margin:0}.custom-theme .el-switch__input:focus~.el-switch__core{outline:1px solid #262729}.custom-theme .el-switch__core{margin:0;display:inline-block;position:relative;width:40px;height:20px;border:1px solid #d8dce5;outline:0;border-radius:10px;-webkit-box-sizing:border-box;box-sizing:border-box;background:#d8dce5;cursor:pointer;-webkit-transition:border-color .3s,background-color .3s;transition:border-color .3s,background-color .3s;vertical-align:middle}.custom-theme .el-switch__core .el-switch__button{position:absolute;top:1px;left:1px;border-radius:100%;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;width:16px;height:16px;background-color:#fff}.custom-theme .el-switch.is-checked .el-switch__core{border-color:#262729;background-color:#262729}.custom-theme .el-switch.is-disabled{opacity:.6}.custom-theme .el-switch--wide .el-switch__label.el-switch__label--left span{left:10px}.custom-theme .el-switch--wide .el-switch__label.el-switch__label--right span{right:10px}.custom-theme .el-switch .label-fade-enter,.custom-theme .el-switch .label-fade-leave-active{opacity:0}.custom-theme .el-popper .popper__arrow,.custom-theme .el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.custom-theme .el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03));filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.custom-theme .el-popper .popper__arrow::after{content:" ";border-width:6px}.custom-theme .el-popper[x-placement^=top]{margin-bottom:12px}.custom-theme .el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#e6ebf5;border-bottom-width:0}.custom-theme .el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#fff;border-bottom-width:0}.custom-theme .el-popper[x-placement^=bottom]{margin-top:12px}.custom-theme .el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#e6ebf5}.custom-theme .el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#fff}.custom-theme .el-popper[x-placement^=right]{margin-left:12px}.custom-theme .el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#e6ebf5;border-left-width:0}.custom-theme .el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#fff;border-left-width:0}.custom-theme .el-popper[x-placement^=left]{margin-right:12px}.custom-theme .el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#e6ebf5}.custom-theme .el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#fff}.custom-theme .el-select-dropdown{position:absolute;z-index:1001;border:solid 1px #dfe4ed;border-radius:4px;background-color:#fff;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);-webkit-box-sizing:border-box;box-sizing:border-box;margin:5px 0}.custom-theme .el-select-dropdown.is-multiple .el-select-dropdown__item.selected{color:#262729;background-color:#fff}.custom-theme .el-select-dropdown.is-multiple .el-select-dropdown__item.selected.hover{background-color:#f5f7fa}.custom-theme .el-select-dropdown.is-multiple .el-select-dropdown__item.selected::after{position:absolute;right:20px;font-family:element-icons;content:"\E611";font-size:12px;font-weight:700;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.custom-theme .el-select-dropdown .el-scrollbar.is-empty .el-select-dropdown__list{padding:0}.custom-theme .el-select-dropdown .popper__arrow{-webkit-transform:translateX(-400%);transform:translateX(-400%)}.custom-theme .el-select-dropdown.is-arrow-fixed .popper__arrow{-webkit-transform:translateX(-200%);transform:translateX(-200%)}.custom-theme .el-select-dropdown__empty{padding:10px 0;margin:0;text-align:center;color:#999;font-size:14px}.custom-theme .el-select-dropdown__wrap{max-height:274px}.custom-theme .el-select-dropdown__list{list-style:none;padding:6px 0;margin:0;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-input{position:relative;font-size:14px;display:inline-block;width:100%}.custom-theme .el-input::-webkit-scrollbar{z-index:11;width:6px}.custom-theme .el-input::-webkit-scrollbar:horizontal{height:6px}.custom-theme .el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.custom-theme .el-input::-webkit-scrollbar-corner{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.custom-theme .el-input__inner{-webkit-appearance:none;background-color:#fff;background-image:none;border-radius:4px;border:1px solid #d8dce5;-webkit-box-sizing:border-box;box-sizing:border-box;color:#5a5e66;display:inline-block;font-size:inherit;height:40px;line-height:1;outline:0;padding:0 15px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.custom-theme .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input__inner:hover{border-color:#b4bccc}.custom-theme .el-input__inner:focus{outline:0;border-color:#262729}.custom-theme .el-input__suffix{position:absolute;height:100%;right:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s;pointer-events:none}.custom-theme .el-input__suffix-inner{pointer-events:all}.custom-theme .el-input__prefix{position:absolute;height:100%;left:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s}.custom-theme .el-input__icon{height:100%;width:25px;text-align:center;-webkit-transition:all .3s;transition:all .3s;line-height:40px}.custom-theme .el-input__icon:after{content:'';height:100%;width:0;display:inline-block;vertical-align:middle}.custom-theme .el-input__validateIcon{pointer-events:none}.custom-theme .el-input.is-active .el-input__inner{outline:0;border-color:#262729}.custom-theme .el-input.is-disabled .el-input__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-input.is-disabled .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__icon{cursor:not-allowed}.custom-theme .el-input--suffix .el-input__inner{padding-right:30px}.custom-theme .el-input--prefix .el-input__inner{padding-left:30px}.custom-theme .el-input--medium{font-size:14px}.custom-theme .el-input--medium .el-input__inner{height:36px}.custom-theme .el-input--medium .el-input__icon{line-height:36px}.custom-theme .el-input--small{font-size:13px}.custom-theme .el-input--small .el-input__inner{height:32px}.custom-theme .el-input--small .el-input__icon{line-height:32px}.custom-theme .el-input--mini{font-size:12px}.custom-theme .el-input--mini .el-input__inner{height:28px}.custom-theme .el-input--mini .el-input__icon{line-height:28px}.custom-theme .el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate}.custom-theme .el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.custom-theme .el-input-group__append,.custom-theme .el-input-group__prepend{background-color:#f5f7fa;color:#0a76a4;vertical-align:middle;display:table-cell;position:relative;border:1px solid #d8dce5;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.custom-theme .el-input-group__append:focus,.custom-theme .el-input-group__prepend:focus{outline:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-select,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-select{display:inline-block;margin:-20px}.custom-theme .el-input-group__append button.el-button,.custom-theme .el-input-group__append div.el-select .el-input__inner,.custom-theme .el-input-group__append div.el-select:hover .el-input__inner,.custom-theme .el-input-group__prepend button.el-button,.custom-theme .el-input-group__prepend div.el-select .el-input__inner,.custom-theme .el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-input,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-input{font-size:inherit}.custom-theme .el-input-group__prepend{border-right:0;border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-input-group__append{border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--prepend .el-input__inner{border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--append .el-input__inner{border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-textarea{display:inline-block;width:100%;vertical-align:bottom}.custom-theme .el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;font-size:14px;color:#5a5e66;background-color:#fff;background-image:none;border:1px solid #d8dce5;border-radius:4px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.custom-theme .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:hover{border-color:#b4bccc}.custom-theme .el-textarea__inner:focus{outline:0;border-color:#262729}.custom-theme .el-textarea.is-disabled .el-textarea__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-tag{background-color:rgba(38,39,41,.1);display:inline-block;padding:0 10px;height:32px;line-height:30px;font-size:12px;color:#262729;border-radius:4px;-webkit-box-sizing:border-box;box-sizing:border-box;border:1px solid rgba(38,39,41,.2);white-space:nowrap}.custom-theme .el-tag .el-icon-close{border-radius:50%;text-align:center;position:relative;cursor:pointer;font-size:12px;height:18px;width:18px;line-height:18px;vertical-align:middle;top:-1px;right:-5px;color:#262729}.custom-theme .el-tag .el-icon-close::before{display:block}.custom-theme .el-tag .el-icon-close:hover{background-color:#262729;color:#fff}.custom-theme .el-tag--info{background-color:rgba(10,118,164,.1);border-color:rgba(10,118,164,.2);color:#0a76a4}.custom-theme .el-tag--info.is-hit{border-color:#0a76a4}.custom-theme .el-tag--info .el-tag__close{color:#0a76a4}.custom-theme .el-tag--info .el-tag__close:hover{background-color:#0a76a4;color:#fff}.custom-theme .el-tag--success{background-color:rgba(64,145,103,.1);border-color:rgba(64,145,103,.2);color:#409167}.custom-theme .el-tag--success.is-hit{border-color:#409167}.custom-theme .el-tag--success .el-tag__close{color:#409167}.custom-theme .el-tag--success .el-tag__close:hover{background-color:#409167;color:#fff}.custom-theme .el-tag--warning{background-color:rgba(157,164,8,.1);border-color:rgba(157,164,8,.2);color:#9da408}.custom-theme .el-tag--warning.is-hit{border-color:#9da408}.custom-theme .el-tag--warning .el-tag__close{color:#9da408}.custom-theme .el-tag--warning .el-tag__close:hover{background-color:#9da408;color:#fff}.custom-theme .el-tag--danger{background-color:rgba(179,69,14,.1);border-color:rgba(179,69,14,.2);color:#b3450e}.custom-theme .el-tag--danger.is-hit{border-color:#b3450e}.custom-theme .el-tag--danger .el-tag__close{color:#b3450e}.custom-theme .el-tag--danger .el-tag__close:hover{background-color:#b3450e;color:#fff}.custom-theme .el-tag--medium{height:28px;line-height:26px}.custom-theme .el-tag--medium .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.custom-theme .el-tag--small{height:24px;padding:0 8px;line-height:22px}.custom-theme .el-tag--small .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.custom-theme .el-tag--mini{height:20px;padding:0 5px;line-height:19px}.custom-theme .el-tag--mini .el-icon-close{margin-left:-3px;-webkit-transform:scale(.7);transform:scale(.7)}.custom-theme .el-select-dropdown__item{font-size:14px;padding:0 20px;position:relative;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#5a5e66;height:34px;line-height:34px;-webkit-box-sizing:border-box;box-sizing:border-box;cursor:pointer}.custom-theme .el-select-dropdown__item.is-disabled{color:#b4bccc;cursor:not-allowed}.custom-theme .el-select-dropdown__item.is-disabled:hover{background-color:#fff}.custom-theme .el-select-dropdown__item.hover,.custom-theme .el-select-dropdown__item:hover{background-color:#f5f7fa}.custom-theme .el-select-dropdown__item.selected{color:#262729;font-weight:700}.custom-theme .el-select-dropdown__item span{line-height:34px!important}.custom-theme .el-select-group{margin:0;padding:0}.custom-theme .el-select-group__wrap{position:relative;list-style:none;margin:0;padding:0}.custom-theme .el-select-group__wrap:not(:last-of-type){padding-bottom:24px}.custom-theme .el-select-group__wrap:not(:last-of-type)::after{content:'';position:absolute;display:block;left:20px;right:20px;bottom:12px;height:1px;background:#dfe4ed}.custom-theme .el-select-group__title{padding-left:20px;font-size:12px;color:#0a76a4;line-height:30px}.custom-theme .el-select-group .el-select-dropdown__item{padding-left:20px}.custom-theme .el-scrollbar{overflow:hidden;position:relative}.custom-theme .el-scrollbar:active>.el-scrollbar__bar,.custom-theme .el-scrollbar:focus>.el-scrollbar__bar,.custom-theme .el-scrollbar:hover>.el-scrollbar__bar{opacity:1;-webkit-transition:opacity 340ms ease-out;transition:opacity 340ms ease-out}.custom-theme .el-scrollbar__wrap{overflow:scroll;height:100%}.custom-theme .el-scrollbar__wrap--hidden-default::-webkit-scrollbar{width:0;height:0}.custom-theme .el-scrollbar__thumb{position:relative;display:block;width:0;height:0;cursor:pointer;border-radius:inherit;background-color:rgba(135,141,153,.3);-webkit-transition:.3s background-color;transition:.3s background-color}.custom-theme .el-scrollbar__thumb:hover{background-color:rgba(135,141,153,.5)}.custom-theme .el-scrollbar__bar{position:absolute;right:2px;bottom:2px;z-index:1;border-radius:4px;opacity:0;-webkit-transition:opacity 120ms ease-out;transition:opacity 120ms ease-out}.custom-theme .el-scrollbar__bar.is-vertical{width:6px;top:2px}.custom-theme .el-scrollbar__bar.is-vertical>div{width:100%}.custom-theme .el-scrollbar__bar.is-horizontal{height:6px;left:2px}.custom-theme .el-scrollbar__bar.is-horizontal>div{height:100%}.custom-theme .el-select{display:inline-block;position:relative}.custom-theme .el-select:hover .el-input__inner{border-color:#b4bccc}.custom-theme .el-select .el-input__inner{cursor:pointer;padding-right:35px}.custom-theme .el-select .el-input__inner:focus{border-color:#262729}.custom-theme .el-select .el-input .el-select__caret{color:#b4bccc;font-size:14px;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;-webkit-transform:rotateZ(180deg);transform:rotateZ(180deg);line-height:16px;cursor:pointer}.custom-theme .el-select .el-input .el-select__caret.is-reverse{-webkit-transform:rotateZ(0);transform:rotateZ(0)}.custom-theme .el-select .el-input .el-select__caret.is-show-close{font-size:14px;text-align:center;-webkit-transform:rotateZ(180deg);transform:rotateZ(180deg);border-radius:100%;color:#b4bccc;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1)}.custom-theme .el-select .el-input .el-select__caret.is-show-close:hover{color:#878d99}.custom-theme .el-select .el-input.is-disabled .el-input__inner{cursor:not-allowed}.custom-theme .el-select .el-input.is-disabled .el-input__inner:hover{border-color:#dfe4ed}.custom-theme .el-select>.el-input{display:block}.custom-theme .el-select__input{border:none;outline:0;padding:0;margin-left:15px;color:#666;font-size:14px;vertical-align:baseline;-webkit-appearance:none;-moz-appearance:none;appearance:none;height:28px;background-color:transparent}.custom-theme .el-select__input.is-mini{height:14px}.custom-theme .el-select__close{cursor:pointer;position:absolute;top:8px;z-index:1000;right:25px;color:#b4bccc;line-height:18px;font-size:14px}.custom-theme .el-select__close:hover{color:#878d99}.custom-theme .el-select__tags{position:absolute;line-height:normal;white-space:normal;z-index:1;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.custom-theme .el-select .el-tag__close{margin-top:-2px}.custom-theme .el-select .el-tag{-webkit-box-sizing:border-box;box-sizing:border-box;border-color:transparent;margin:3px 0 3px 6px;background-color:#f0f2f5}.custom-theme .el-select .el-tag__close.el-icon-close{background-color:#b4bccc;right:-7px;color:#fff}.custom-theme .el-select .el-tag__close.el-icon-close:hover{background-color:#878d99}.custom-theme .el-select .el-tag__close.el-icon-close::before{display:block;-webkit-transform:translate(0,.5px);transform:translate(0,.5px)}.custom-theme .el-select__tag{display:inline-block;height:24px;line-height:24px;font-size:14px;border-radius:4px;color:#fff;background-color:#262729}.custom-theme .el-select__tag .el-icon-close{font-size:14px}.custom-theme .el-button{display:inline-block;line-height:1;white-space:nowrap;cursor:pointer;background:#fff;border:1px solid #d8dce5;border-color:#d8dce5;color:#5a5e66;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;-webkit-transition:.1s;transition:.1s;font-weight:500;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;padding:12px 20px;font-size:14px;border-radius:4px}.custom-theme .el-button+.el-button{margin-left:10px}.custom-theme .el-button.is-round{padding:12px 20px}.custom-theme .el-button:focus,.custom-theme .el-button:hover{color:#262729;border-color:#bebebf;background-color:#e9e9ea}.custom-theme .el-button:active{color:#222325;border-color:#222325;outline:0}.custom-theme .el-button::-moz-focus-inner{border:0}.custom-theme .el-button [class*=el-icon-]+span{margin-left:5px}.custom-theme .el-button.is-plain:focus,.custom-theme .el-button.is-plain:hover{background:#fff;border-color:#262729;color:#262729}.custom-theme .el-button.is-plain:active{background:#fff;border-color:#222325;color:#222325;outline:0}.custom-theme .el-button.is-active{color:#222325;border-color:#222325}.custom-theme .el-button.is-disabled,.custom-theme .el-button.is-disabled:focus,.custom-theme .el-button.is-disabled:hover{color:#b4bccc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#e6ebf5}.custom-theme .el-button.is-disabled.el-button--text{background-color:transparent}.custom-theme .el-button.is-disabled.is-plain,.custom-theme .el-button.is-disabled.is-plain:focus,.custom-theme .el-button.is-disabled.is-plain:hover{background-color:#fff;border-color:#e6ebf5;color:#b4bccc}.custom-theme .el-button.is-loading{position:relative;pointer-events:none}.custom-theme .el-button.is-loading:before{pointer-events:none;content:'';position:absolute;left:-1px;top:-1px;right:-1px;bottom:-1px;border-radius:inherit;background-color:rgba(255,255,255,.35)}.custom-theme .el-button.is-round{border-radius:20px;padding:12px 23px}.custom-theme .el-button--primary{color:#fff;background-color:#262729;border-color:#262729}.custom-theme .el-button--primary:focus,.custom-theme .el-button--primary:hover{background:#515254;border-color:#515254;color:#fff}.custom-theme .el-button--primary:active{background:#222325;border-color:#222325;color:#fff;outline:0}.custom-theme .el-button--primary.is-active{background:#222325;border-color:#222325;color:#fff}.custom-theme .el-button--primary.is-disabled,.custom-theme .el-button--primary.is-disabled:active,.custom-theme .el-button--primary.is-disabled:focus,.custom-theme .el-button--primary.is-disabled:hover{color:#fff;background-color:#939394;border-color:#939394}.custom-theme .el-button--primary.is-plain{color:#262729;background:#e9e9ea;border-color:#a8a9a9}.custom-theme .el-button--primary.is-plain:focus,.custom-theme .el-button--primary.is-plain:hover{background:#262729;border-color:#262729;color:#fff}.custom-theme .el-button--primary.is-plain:active{background:#222325;border-color:#222325;color:#fff;outline:0}.custom-theme .el-button--primary.is-plain.is-disabled,.custom-theme .el-button--primary.is-plain.is-disabled:active,.custom-theme .el-button--primary.is-plain.is-disabled:focus,.custom-theme .el-button--primary.is-plain.is-disabled:hover{color:#7d7d7f;background-color:#e9e9ea;border-color:#d4d4d4}.custom-theme .el-button--success{color:#fff;background-color:#409167;border-color:#409167}.custom-theme .el-button--success:focus,.custom-theme .el-button--success:hover{background:#66a785;border-color:#66a785;color:#fff}.custom-theme .el-button--success:active{background:#3a835d;border-color:#3a835d;color:#fff;outline:0}.custom-theme .el-button--success.is-active{background:#3a835d;border-color:#3a835d;color:#fff}.custom-theme .el-button--success.is-disabled,.custom-theme .el-button--success.is-disabled:active,.custom-theme .el-button--success.is-disabled:focus,.custom-theme .el-button--success.is-disabled:hover{color:#fff;background-color:#a0c8b3;border-color:#a0c8b3}.custom-theme .el-button--success.is-plain{color:#409167;background:#ecf4f0;border-color:#b3d3c2}.custom-theme .el-button--success.is-plain:focus,.custom-theme .el-button--success.is-plain:hover{background:#409167;border-color:#409167;color:#fff}.custom-theme .el-button--success.is-plain:active{background:#3a835d;border-color:#3a835d;color:#fff;outline:0}.custom-theme .el-button--success.is-plain.is-disabled,.custom-theme .el-button--success.is-plain.is-disabled:active,.custom-theme .el-button--success.is-plain.is-disabled:focus,.custom-theme .el-button--success.is-plain.is-disabled:hover{color:#8cbda4;background-color:#ecf4f0;border-color:#d9e9e1}.custom-theme .el-button--warning{color:#fff;background-color:#9da408;border-color:#9da408}.custom-theme .el-button--warning:focus,.custom-theme .el-button--warning:hover{background:#b1b639;border-color:#b1b639;color:#fff}.custom-theme .el-button--warning:active{background:#8d9407;border-color:#8d9407;color:#fff;outline:0}.custom-theme .el-button--warning.is-active{background:#8d9407;border-color:#8d9407;color:#fff}.custom-theme .el-button--warning.is-disabled,.custom-theme .el-button--warning.is-disabled:active,.custom-theme .el-button--warning.is-disabled:focus,.custom-theme .el-button--warning.is-disabled:hover{color:#fff;background-color:#ced284;border-color:#ced284}.custom-theme .el-button--warning.is-plain{color:#9da408;background:#f5f6e6;border-color:#d8db9c}.custom-theme .el-button--warning.is-plain:focus,.custom-theme .el-button--warning.is-plain:hover{background:#9da408;border-color:#9da408;color:#fff}.custom-theme .el-button--warning.is-plain:active{background:#8d9407;border-color:#8d9407;color:#fff;outline:0}.custom-theme .el-button--warning.is-plain.is-disabled,.custom-theme .el-button--warning.is-plain.is-disabled:active,.custom-theme .el-button--warning.is-plain.is-disabled:focus,.custom-theme .el-button--warning.is-plain.is-disabled:hover{color:#c4c86b;background-color:#f5f6e6;border-color:#ebedce}.custom-theme .el-button--danger{color:#fff;background-color:#b3450e;border-color:#b3450e}.custom-theme .el-button--danger:focus,.custom-theme .el-button--danger:hover{background:#c26a3e;border-color:#c26a3e;color:#fff}.custom-theme .el-button--danger:active{background:#a13e0d;border-color:#a13e0d;color:#fff;outline:0}.custom-theme .el-button--danger.is-active{background:#a13e0d;border-color:#a13e0d;color:#fff}.custom-theme .el-button--danger.is-disabled,.custom-theme .el-button--danger.is-disabled:active,.custom-theme .el-button--danger.is-disabled:focus,.custom-theme .el-button--danger.is-disabled:hover{color:#fff;background-color:#d9a287;border-color:#d9a287}.custom-theme .el-button--danger.is-plain{color:#b3450e;background:#f7ece7;border-color:#e1b59f}.custom-theme .el-button--danger.is-plain:focus,.custom-theme .el-button--danger.is-plain:hover{background:#b3450e;border-color:#b3450e;color:#fff}.custom-theme .el-button--danger.is-plain:active{background:#a13e0d;border-color:#a13e0d;color:#fff;outline:0}.custom-theme .el-button--danger.is-plain.is-disabled,.custom-theme .el-button--danger.is-plain.is-disabled:active,.custom-theme .el-button--danger.is-plain.is-disabled:focus,.custom-theme .el-button--danger.is-plain.is-disabled:hover{color:#d18f6e;background-color:#f7ece7;border-color:#f0dacf}.custom-theme .el-button--info{color:#fff;background-color:#0a76a4;border-color:#0a76a4}.custom-theme .el-button--info:focus,.custom-theme .el-button--info:hover{background:#3b91b6;border-color:#3b91b6;color:#fff}.custom-theme .el-button--info:active{background:#096a94;border-color:#096a94;color:#fff;outline:0}.custom-theme .el-button--info.is-active{background:#096a94;border-color:#096a94;color:#fff}.custom-theme .el-button--info.is-disabled,.custom-theme .el-button--info.is-disabled:active,.custom-theme .el-button--info.is-disabled:focus,.custom-theme .el-button--info.is-disabled:hover{color:#fff;background-color:#85bbd2;border-color:#85bbd2}.custom-theme .el-button--info.is-plain{color:#0a76a4;background:#e7f1f6;border-color:#9dc8db}.custom-theme .el-button--info.is-plain:focus,.custom-theme .el-button--info.is-plain:hover{background:#0a76a4;border-color:#0a76a4;color:#fff}.custom-theme .el-button--info.is-plain:active{background:#096a94;border-color:#096a94;color:#fff;outline:0}.custom-theme .el-button--info.is-plain.is-disabled,.custom-theme .el-button--info.is-plain.is-disabled:active,.custom-theme .el-button--info.is-plain.is-disabled:focus,.custom-theme .el-button--info.is-plain.is-disabled:hover{color:#6cadc8;background-color:#e7f1f6;border-color:#cee4ed}.custom-theme .el-button--medium{padding:10px 20px;font-size:14px;border-radius:4px}.custom-theme .el-button--medium.is-round{padding:10px 20px}.custom-theme .el-button--small{padding:9px 15px;font-size:12px;border-radius:3px}.custom-theme .el-button--small.is-round{padding:9px 15px}.custom-theme .el-button--mini{padding:7px 15px;font-size:12px;border-radius:3px}.custom-theme .el-button--mini.is-round{padding:7px 15px}.custom-theme .el-button--text{border:none;color:#262729;background:0 0;padding-left:0;padding-right:0}.custom-theme .el-button--text:focus,.custom-theme .el-button--text:hover{color:#515254;border-color:transparent;background-color:transparent}.custom-theme .el-button--text:active{color:#222325;border-color:transparent;background-color:transparent}.custom-theme .el-button-group{display:inline-block;vertical-align:middle}.custom-theme .el-button-group::after,.custom-theme .el-button-group::before{display:table;content:""}.custom-theme .el-button-group::after{clear:both}.custom-theme .el-button-group .el-button{float:left;position:relative}.custom-theme .el-button-group .el-button+.el-button{margin-left:0}.custom-theme .el-button-group .el-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-button-group .el-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-button-group .el-button:not(:first-child):not(:last-child){border-radius:0}.custom-theme .el-button-group .el-button:not(:last-child){margin-right:-1px}.custom-theme .el-button-group .el-button:active,.custom-theme .el-button-group .el-button:focus,.custom-theme .el-button-group .el-button:hover{z-index:1}.custom-theme .el-button-group .el-button.is-active{z-index:1}.custom-theme .el-button-group .el-button--primary:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--primary:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--primary:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--success:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--success:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--success:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--warning:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--warning:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--warning:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--danger:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--danger:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--danger:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--info:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--info:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--info:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-checkbox{color:#5a5e66;font-weight:500;font-size:14px;position:relative;cursor:pointer;display:inline-block;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.custom-theme .el-checkbox.is-bordered{padding:9px 20px 9px 10px;border-radius:4px;border:1px solid #d8dce5}.custom-theme .el-checkbox.is-bordered.is-checked{border-color:#262729}.custom-theme .el-checkbox.is-bordered.is-disabled{border-color:#e6ebf5;cursor:not-allowed}.custom-theme .el-checkbox.is-bordered+.el-checkbox.is-bordered{margin-left:10px}.custom-theme .el-checkbox.is-bordered.el-checkbox--medium{padding:7px 20px 7px 10px;border-radius:4px}.custom-theme .el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__label{line-height:17px;font-size:14px}.custom-theme .el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__inner{height:14px;width:14px}.custom-theme .el-checkbox.is-bordered.el-checkbox--small{padding:3px 15px 7px 10px;border-radius:3px}.custom-theme .el-checkbox.is-bordered.el-checkbox--small .el-checkbox__label{line-height:15px;font-size:12px}.custom-theme .el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner{height:12px;width:12px}.custom-theme .el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner::after{height:6px;width:2px}.custom-theme .el-checkbox.is-bordered.el-checkbox--mini{padding:1px 15px 5px 10px;border-radius:3px}.custom-theme .el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__label{line-height:12px;font-size:12px}.custom-theme .el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner{height:12px;width:12px}.custom-theme .el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner::after{height:6px;width:2px}.custom-theme .el-checkbox__input{white-space:nowrap;cursor:pointer;outline:0;display:inline-block;line-height:1;position:relative;vertical-align:middle}.custom-theme .el-checkbox__input.is-disabled .el-checkbox__inner{background-color:#edf2fc;border-color:#d8dce5;cursor:not-allowed}.custom-theme .el-checkbox__input.is-disabled .el-checkbox__inner::after{cursor:not-allowed;border-color:#b4bccc}.custom-theme .el-checkbox__input.is-disabled .el-checkbox__inner+.el-checkbox__label{cursor:not-allowed}.custom-theme .el-checkbox__input.is-disabled.is-checked .el-checkbox__inner{background-color:#edf2fc;border-color:#d8dce5}.custom-theme .el-checkbox__input.is-disabled.is-checked .el-checkbox__inner::after{border-color:#b4bccc}.custom-theme .el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner{background-color:#edf2fc;border-color:#d8dce5}.custom-theme .el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner::before{background-color:#b4bccc;border-color:#b4bccc}.custom-theme .el-checkbox__input.is-disabled+span.el-checkbox__label{color:#b4bccc;cursor:not-allowed}.custom-theme .el-checkbox__input.is-checked .el-checkbox__inner{background-color:#262729;border-color:#262729}.custom-theme .el-checkbox__input.is-checked .el-checkbox__inner::after{-webkit-transform:rotate(45deg) scaleY(1);transform:rotate(45deg) scaleY(1)}.custom-theme .el-checkbox__input.is-checked+.el-checkbox__label{color:#262729}.custom-theme .el-checkbox__input.is-focus .el-checkbox__inner{border-color:#262729}.custom-theme .el-checkbox__input.is-indeterminate .el-checkbox__inner{background-color:#262729;border-color:#262729}.custom-theme .el-checkbox__input.is-indeterminate .el-checkbox__inner::before{content:'';position:absolute;display:block;background-color:#fff;height:2px;-webkit-transform:scale(.5);transform:scale(.5);left:0;right:0;top:5px}.custom-theme .el-checkbox__input.is-indeterminate .el-checkbox__inner::after{display:none}.custom-theme .el-checkbox__inner{display:inline-block;position:relative;border:1px solid #d8dce5;border-radius:2px;-webkit-box-sizing:border-box;box-sizing:border-box;width:14px;height:14px;background-color:#fff;z-index:1;-webkit-transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46);transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46)}.custom-theme .el-checkbox__inner:hover{border-color:#262729}.custom-theme .el-checkbox__inner::after{-webkit-box-sizing:content-box;box-sizing:content-box;content:"";border:1px solid #fff;border-left:0;border-top:0;height:7px;left:4px;position:absolute;top:1px;-webkit-transform:rotate(45deg) scaleY(0);transform:rotate(45deg) scaleY(0);width:3px;-webkit-transition:-webkit-transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms;transition:-webkit-transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms;transition:transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms;transition:transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms,-webkit-transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms;-webkit-transform-origin:center;transform-origin:center}.custom-theme .el-checkbox__original{opacity:0;outline:0;position:absolute;margin:0;width:0;height:0;left:-999px}.custom-theme .el-checkbox__label{display:inline-block;padding-left:10px;line-height:19px;font-size:14px}.custom-theme .el-checkbox+.el-checkbox{margin-left:30px}.custom-theme .el-checkbox-button{position:relative;display:inline-block}.custom-theme .el-checkbox-button__inner{display:inline-block;line-height:1;font-weight:500;white-space:nowrap;vertical-align:middle;cursor:pointer;background:#fff;border:1px solid #d8dce5;border-left:0;color:#5a5e66;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;position:relative;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;padding:12px 20px;font-size:14px;border-radius:0}.custom-theme .el-checkbox-button__inner.is-round{padding:12px 20px}.custom-theme .el-checkbox-button__inner:hover{color:#262729}.custom-theme .el-checkbox-button__inner [class*=el-icon-]{line-height:.9}.custom-theme .el-checkbox-button__inner [class*=el-icon-]+span{margin-left:5px}.custom-theme .el-checkbox-button__original{opacity:0;outline:0;position:absolute;margin:0;left:-999px}.custom-theme .el-checkbox-button.is-checked .el-checkbox-button__inner{color:#fff;background-color:#262729;border-color:#262729;-webkit-box-shadow:-1px 0 0 0 #7d7d7f;box-shadow:-1px 0 0 0 #7d7d7f}.custom-theme .el-checkbox-button.is-disabled .el-checkbox-button__inner{color:#b4bccc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#e6ebf5;-webkit-box-shadow:none;box-shadow:none}.custom-theme .el-checkbox-button:first-child .el-checkbox-button__inner{border-left:1px solid #d8dce5;border-radius:4px 0 0 4px;-webkit-box-shadow:none!important;box-shadow:none!important}.custom-theme .el-checkbox-button.is-focus .el-checkbox-button__inner{border-color:#262729}.custom-theme .el-checkbox-button:last-child .el-checkbox-button__inner{border-radius:0 4px 4px 0}.custom-theme .el-checkbox-button--medium .el-checkbox-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.custom-theme .el-checkbox-button--medium .el-checkbox-button__inner.is-round{padding:10px 20px}.custom-theme .el-checkbox-button--small .el-checkbox-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.custom-theme .el-checkbox-button--small .el-checkbox-button__inner.is-round{padding:9px 15px}.custom-theme .el-checkbox-button--mini .el-checkbox-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.custom-theme .el-checkbox-button--mini .el-checkbox-button__inner.is-round{padding:7px 15px}.custom-theme .el-checkbox-group{font-size:0}.custom-theme .el-tag{background-color:rgba(38,39,41,.1);display:inline-block;padding:0 10px;height:32px;line-height:30px;font-size:12px;color:#262729;border-radius:4px;-webkit-box-sizing:border-box;box-sizing:border-box;border:1px solid rgba(38,39,41,.2);white-space:nowrap}.custom-theme .el-tag .el-icon-close{border-radius:50%;text-align:center;position:relative;cursor:pointer;font-size:12px;height:18px;width:18px;line-height:18px;vertical-align:middle;top:-1px;right:-5px;color:#262729}.custom-theme .el-tag .el-icon-close::before{display:block}.custom-theme .el-tag .el-icon-close:hover{background-color:#262729;color:#fff}.custom-theme .el-tag--info{background-color:rgba(10,118,164,.1);border-color:rgba(10,118,164,.2);color:#0a76a4}.custom-theme .el-tag--info.is-hit{border-color:#0a76a4}.custom-theme .el-tag--info .el-tag__close{color:#0a76a4}.custom-theme .el-tag--info .el-tag__close:hover{background-color:#0a76a4;color:#fff}.custom-theme .el-tag--success{background-color:rgba(64,145,103,.1);border-color:rgba(64,145,103,.2);color:#409167}.custom-theme .el-tag--success.is-hit{border-color:#409167}.custom-theme .el-tag--success .el-tag__close{color:#409167}.custom-theme .el-tag--success .el-tag__close:hover{background-color:#409167;color:#fff}.custom-theme .el-tag--warning{background-color:rgba(157,164,8,.1);border-color:rgba(157,164,8,.2);color:#9da408}.custom-theme .el-tag--warning.is-hit{border-color:#9da408}.custom-theme .el-tag--warning .el-tag__close{color:#9da408}.custom-theme .el-tag--warning .el-tag__close:hover{background-color:#9da408;color:#fff}.custom-theme .el-tag--danger{background-color:rgba(179,69,14,.1);border-color:rgba(179,69,14,.2);color:#b3450e}.custom-theme .el-tag--danger.is-hit{border-color:#b3450e}.custom-theme .el-tag--danger .el-tag__close{color:#b3450e}.custom-theme .el-tag--danger .el-tag__close:hover{background-color:#b3450e;color:#fff}.custom-theme .el-tag--medium{height:28px;line-height:26px}.custom-theme .el-tag--medium .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.custom-theme .el-tag--small{height:24px;padding:0 8px;line-height:22px}.custom-theme .el-tag--small .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.custom-theme .el-tag--mini{height:20px;padding:0 5px;line-height:19px}.custom-theme .el-tag--mini .el-icon-close{margin-left:-3px;-webkit-transform:scale(.7);transform:scale(.7)}.custom-theme .el-table{position:relative;overflow:hidden;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-box-flex:1;-ms-flex:1;flex:1;width:100%;max-width:100%;background-color:#fff;font-size:14px;color:#5a5e66}.custom-theme .el-table__empty-block{position:relative;min-height:60px;text-align:center;width:100%;height:100%}.custom-theme .el-table__empty-text{position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);color:color(#262729 s(16%) l(44%))}.custom-theme .el-table__expand-column .cell{padding:0;text-align:center}.custom-theme .el-table__expand-icon{position:relative;cursor:pointer;color:#666;font-size:12px;-webkit-transition:-webkit-transform .2s ease-in-out;transition:-webkit-transform .2s ease-in-out;transition:transform .2s ease-in-out;transition:transform .2s ease-in-out,-webkit-transform .2s ease-in-out;height:20px}.custom-theme .el-table__expand-icon--expanded{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.custom-theme .el-table__expand-icon>.el-icon{position:absolute;left:50%;top:50%;margin-left:-5px;margin-top:-5px}.custom-theme .el-table__expanded-cell{background-color:#fff}.custom-theme .el-table__expanded-cell[class*=cell]{padding:20px 50px}.custom-theme .el-table__expanded-cell:hover{background-color:#f5f7fa!important}.custom-theme .el-table--fit{border-right:0;border-bottom:0}.custom-theme .el-table--fit td.gutter,.custom-theme .el-table--fit th.gutter{border-right-width:1px}.custom-theme .el-table thead{color:#878d99;font-weight:500}.custom-theme .el-table thead.is-group th{background:#f5f7fa}.custom-theme .el-table td,.custom-theme .el-table th{padding:12px 0;min-width:0;-webkit-box-sizing:border-box;box-sizing:border-box;text-overflow:ellipsis;vertical-align:middle;position:relative}.custom-theme .el-table td.is-center,.custom-theme .el-table th.is-center{text-align:center}.custom-theme .el-table td.is-left,.custom-theme .el-table th.is-left{text-align:left}.custom-theme .el-table td.is-right,.custom-theme .el-table th.is-right{text-align:right}.custom-theme .el-table td.gutter,.custom-theme .el-table th.gutter{width:15px;border-right-width:0;border-bottom-width:0;padding:0}.custom-theme .el-table td.is-hidden>*,.custom-theme .el-table th.is-hidden>*{visibility:hidden}.custom-theme .el-table--medium td,.custom-theme .el-table--medium th{padding:10px 0}.custom-theme .el-table--small{font-size:12px}.custom-theme .el-table--small td,.custom-theme .el-table--small th{padding:8px 0}.custom-theme .el-table--mini{font-size:12px}.custom-theme .el-table--mini td,.custom-theme .el-table--mini th{padding:6px 0}.custom-theme .el-table tr{background-color:#fff}.custom-theme .el-table tr input[type=checkbox]{margin:0}.custom-theme .el-table td,.custom-theme .el-table th.is-leaf{border-bottom:1px solid #e6ebf5}.custom-theme .el-table th.is-sortable{cursor:pointer}.custom-theme .el-table th{white-space:nowrap;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-align:left}.custom-theme .el-table th div{display:inline-block;padding-left:10px;padding-right:10px;line-height:40px;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.custom-theme .el-table th>.cell{position:relative;word-wrap:normal;text-overflow:ellipsis;display:inline-block;vertical-align:middle;width:100%;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-table th>.cell.highlight{color:#262729}.custom-theme .el-table th.required>div::before{display:inline-block;content:"";width:8px;height:8px;border-radius:50%;background:#ff4d51;margin-right:5px;vertical-align:middle}.custom-theme .el-table td div{-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-table td.gutter{width:0}.custom-theme .el-table .cell{-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;text-overflow:ellipsis;white-space:normal;word-break:break-all;line-height:23px;padding-left:10px;padding-right:10px}.custom-theme .el-table .cell.el-tooltip{white-space:nowrap;min-width:50px}.custom-theme .el-table td:first-child .cell,.custom-theme .el-table th:first-child .cell{padding-left:0}.custom-theme .el-table--border,.custom-theme .el-table--group{border:1px solid #e6ebf5}.custom-theme .el-table--border::after,.custom-theme .el-table--group::after,.custom-theme .el-table::before{content:'';position:absolute;background-color:#e6ebf5;z-index:1}.custom-theme .el-table--border::after,.custom-theme .el-table--group::after{top:0;right:0;width:1px;height:100%}.custom-theme .el-table::before{left:0;bottom:0;width:100%;height:1px}.custom-theme .el-table--border{border-right:none;border-bottom:none}.custom-theme .el-table--border td,.custom-theme .el-table--border th{border-right:1px solid #e6ebf5}.custom-theme .el-table--border td:first-child .cell,.custom-theme .el-table--border th:first-child .cell{padding-left:10px}.custom-theme .el-table--border .has-gutter td:nth-last-of-type(2),.custom-theme .el-table--border .has-gutter th:nth-last-of-type(2){border-right:none}.custom-theme .el-table--border th.gutter:last-of-type{border-bottom:1px solid #e6ebf5;border-bottom-width:1px}.custom-theme .el-table--border th{border-bottom:1px solid #e6ebf5}.custom-theme .el-table--hidden{visibility:hidden}.custom-theme .el-table__fixed,.custom-theme .el-table__fixed-right{position:absolute;top:0;left:0;overflow-x:hidden;-webkit-box-shadow:0 0 10px rgba(0,0,0,.12);box-shadow:0 0 10px rgba(0,0,0,.12)}.custom-theme .el-table__fixed-right::before,.custom-theme .el-table__fixed::before{content:'';position:absolute;left:0;bottom:0;width:100%;height:1px;background-color:#e6ebf5;z-index:4}.custom-theme .el-table__fixed-right-patch{position:absolute;top:-1px;right:0;background-color:#fff;border-bottom:1px solid #e6ebf5}.custom-theme .el-table__fixed-right{top:0;left:auto;right:0}.custom-theme .el-table__fixed-right .el-table__fixed-body-wrapper,.custom-theme .el-table__fixed-right .el-table__fixed-footer-wrapper,.custom-theme .el-table__fixed-right .el-table__fixed-header-wrapper{left:auto;right:0}.custom-theme .el-table__fixed-header-wrapper{position:absolute;left:0;top:0;z-index:3}.custom-theme .el-table__fixed-footer-wrapper{position:absolute;left:0;bottom:0;z-index:3}.custom-theme .el-table__fixed-footer-wrapper tbody td{border-top:1px solid #e6ebf5;background-color:#f5f7fa;color:#5a5e66}.custom-theme .el-table__fixed-body-wrapper{position:absolute;left:0;top:37px;overflow:hidden;z-index:3}.custom-theme .el-table__body-wrapper,.custom-theme .el-table__footer-wrapper,.custom-theme .el-table__header-wrapper{width:100%}.custom-theme .el-table__footer-wrapper{margin-top:-1px}.custom-theme .el-table__footer-wrapper td{border-top:1px solid #e6ebf5}.custom-theme .el-table__body,.custom-theme .el-table__footer,.custom-theme .el-table__header{table-layout:fixed}.custom-theme .el-table__footer-wrapper,.custom-theme .el-table__header-wrapper{overflow:hidden}.custom-theme .el-table__footer-wrapper tbody td,.custom-theme .el-table__header-wrapper tbody td{background-color:#f5f7fa;color:#5a5e66}.custom-theme .el-table__body-wrapper{overflow:auto;position:relative}.custom-theme .el-table__body-wrapper.is-scroll-none~.el-table__fixed,.custom-theme .el-table__body-wrapper.is-scroll-none~.el-table__fixed-right{-webkit-box-shadow:none;box-shadow:none}.custom-theme .el-table__body-wrapper.is-scroll-left~.el-table__fixed{-webkit-box-shadow:none;box-shadow:none}.custom-theme .el-table__body-wrapper.is-scroll-right~.el-table__fixed-right{-webkit-box-shadow:none;box-shadow:none}.custom-theme .el-table__body-wrapper .el-table--border.is-scroll-right~.el-table__fixed-right{border-left:1px solid #e6ebf5}.custom-theme .el-table__body-wrapper .el-table--border.is-scroll-left~.el-table__fixed{border-right:1px solid #e6ebf5}.custom-theme .el-table .caret-wrapper{position:relative;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:13px;width:24px;cursor:pointer;overflow:initial}.custom-theme .el-table .sort-caret{color:#0a76a4;width:14px;overflow:hidden;font-size:13px}.custom-theme .el-table .ascending .sort-caret.ascending{color:#262729}.custom-theme .el-table .descending .sort-caret.descending{color:#262729}.custom-theme .el-table .hidden-columns{visibility:hidden;position:absolute;z-index:-1}.custom-theme .el-table--striped .el-table__body tr.el-table__row--striped td{background:#fafafa}.custom-theme .el-table--striped .el-table__body tr.el-table__row--striped.current-row td{background-color:#e9e9ea}.custom-theme .el-table__body tr.hover-row.current-row>td,.custom-theme .el-table__body tr.hover-row.el-table__row--striped.current-row>td,.custom-theme .el-table__body tr.hover-row.el-table__row--striped>td,.custom-theme .el-table__body tr.hover-row>td{background-color:#e9e9ea}.custom-theme .el-table__body tr.current-row>td{background-color:#e9e9ea}.custom-theme .el-table__column-resize-proxy{position:absolute;left:200px;top:0;bottom:0;width:0;border-left:1px solid #e6ebf5;z-index:10}.custom-theme .el-table__column-filter-trigger{display:inline-block;line-height:34px;cursor:pointer}.custom-theme .el-table__column-filter-trigger i{color:#0a76a4;font-size:12px;-webkit-transform:scale(.75);transform:scale(.75)}.custom-theme .el-table--enable-row-transition .el-table__body td{-webkit-transition:background-color .25s ease;transition:background-color .25s ease}.custom-theme .el-table--enable-row-hover .el-table__body tr:hover>td{background-color:#f5f7fa}.custom-theme .el-table--fluid-height .el-table__fixed,.custom-theme .el-table--fluid-height .el-table__fixed-right{bottom:0;overflow:hidden}.custom-theme .el-checkbox{color:#5a5e66;font-weight:500;font-size:14px;position:relative;cursor:pointer;display:inline-block;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.custom-theme .el-checkbox.is-bordered{padding:9px 20px 9px 10px;border-radius:4px;border:1px solid #d8dce5}.custom-theme .el-checkbox.is-bordered.is-checked{border-color:#262729}.custom-theme .el-checkbox.is-bordered.is-disabled{border-color:#e6ebf5;cursor:not-allowed}.custom-theme .el-checkbox.is-bordered+.el-checkbox.is-bordered{margin-left:10px}.custom-theme .el-checkbox.is-bordered.el-checkbox--medium{padding:7px 20px 7px 10px;border-radius:4px}.custom-theme .el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__label{line-height:17px;font-size:14px}.custom-theme .el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__inner{height:14px;width:14px}.custom-theme .el-checkbox.is-bordered.el-checkbox--small{padding:3px 15px 7px 10px;border-radius:3px}.custom-theme .el-checkbox.is-bordered.el-checkbox--small .el-checkbox__label{line-height:15px;font-size:12px}.custom-theme .el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner{height:12px;width:12px}.custom-theme .el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner::after{height:6px;width:2px}.custom-theme .el-checkbox.is-bordered.el-checkbox--mini{padding:1px 15px 5px 10px;border-radius:3px}.custom-theme .el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__label{line-height:12px;font-size:12px}.custom-theme .el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner{height:12px;width:12px}.custom-theme .el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner::after{height:6px;width:2px}.custom-theme .el-checkbox__input{white-space:nowrap;cursor:pointer;outline:0;display:inline-block;line-height:1;position:relative;vertical-align:middle}.custom-theme .el-checkbox__input.is-disabled .el-checkbox__inner{background-color:#edf2fc;border-color:#d8dce5;cursor:not-allowed}.custom-theme .el-checkbox__input.is-disabled .el-checkbox__inner::after{cursor:not-allowed;border-color:#b4bccc}.custom-theme .el-checkbox__input.is-disabled .el-checkbox__inner+.el-checkbox__label{cursor:not-allowed}.custom-theme .el-checkbox__input.is-disabled.is-checked .el-checkbox__inner{background-color:#edf2fc;border-color:#d8dce5}.custom-theme .el-checkbox__input.is-disabled.is-checked .el-checkbox__inner::after{border-color:#b4bccc}.custom-theme .el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner{background-color:#edf2fc;border-color:#d8dce5}.custom-theme .el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner::before{background-color:#b4bccc;border-color:#b4bccc}.custom-theme .el-checkbox__input.is-disabled+span.el-checkbox__label{color:#b4bccc;cursor:not-allowed}.custom-theme .el-checkbox__input.is-checked .el-checkbox__inner{background-color:#262729;border-color:#262729}.custom-theme .el-checkbox__input.is-checked .el-checkbox__inner::after{-webkit-transform:rotate(45deg) scaleY(1);transform:rotate(45deg) scaleY(1)}.custom-theme .el-checkbox__input.is-checked+.el-checkbox__label{color:#262729}.custom-theme .el-checkbox__input.is-focus .el-checkbox__inner{border-color:#262729}.custom-theme .el-checkbox__input.is-indeterminate .el-checkbox__inner{background-color:#262729;border-color:#262729}.custom-theme .el-checkbox__input.is-indeterminate .el-checkbox__inner::before{content:'';position:absolute;display:block;background-color:#fff;height:2px;-webkit-transform:scale(.5);transform:scale(.5);left:0;right:0;top:5px}.custom-theme .el-checkbox__input.is-indeterminate .el-checkbox__inner::after{display:none}.custom-theme .el-checkbox__inner{display:inline-block;position:relative;border:1px solid #d8dce5;border-radius:2px;-webkit-box-sizing:border-box;box-sizing:border-box;width:14px;height:14px;background-color:#fff;z-index:1;-webkit-transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46);transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46)}.custom-theme .el-checkbox__inner:hover{border-color:#262729}.custom-theme .el-checkbox__inner::after{-webkit-box-sizing:content-box;box-sizing:content-box;content:"";border:1px solid #fff;border-left:0;border-top:0;height:7px;left:4px;position:absolute;top:1px;-webkit-transform:rotate(45deg) scaleY(0);transform:rotate(45deg) scaleY(0);width:3px;-webkit-transition:-webkit-transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms;transition:-webkit-transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms;transition:transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms;transition:transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms,-webkit-transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms;-webkit-transform-origin:center;transform-origin:center}.custom-theme .el-checkbox__original{opacity:0;outline:0;position:absolute;margin:0;width:0;height:0;left:-999px}.custom-theme .el-checkbox__label{display:inline-block;padding-left:10px;line-height:19px;font-size:14px}.custom-theme .el-checkbox+.el-checkbox{margin-left:30px}.custom-theme .el-checkbox-button{position:relative;display:inline-block}.custom-theme .el-checkbox-button__inner{display:inline-block;line-height:1;font-weight:500;white-space:nowrap;vertical-align:middle;cursor:pointer;background:#fff;border:1px solid #d8dce5;border-left:0;color:#5a5e66;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;position:relative;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;padding:12px 20px;font-size:14px;border-radius:0}.custom-theme .el-checkbox-button__inner.is-round{padding:12px 20px}.custom-theme .el-checkbox-button__inner:hover{color:#262729}.custom-theme .el-checkbox-button__inner [class*=el-icon-]{line-height:.9}.custom-theme .el-checkbox-button__inner [class*=el-icon-]+span{margin-left:5px}.custom-theme .el-checkbox-button__original{opacity:0;outline:0;position:absolute;margin:0;left:-999px}.custom-theme .el-checkbox-button.is-checked .el-checkbox-button__inner{color:#fff;background-color:#262729;border-color:#262729;-webkit-box-shadow:-1px 0 0 0 #7d7d7f;box-shadow:-1px 0 0 0 #7d7d7f}.custom-theme .el-checkbox-button.is-disabled .el-checkbox-button__inner{color:#b4bccc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#e6ebf5;-webkit-box-shadow:none;box-shadow:none}.custom-theme .el-checkbox-button:first-child .el-checkbox-button__inner{border-left:1px solid #d8dce5;border-radius:4px 0 0 4px;-webkit-box-shadow:none!important;box-shadow:none!important}.custom-theme .el-checkbox-button.is-focus .el-checkbox-button__inner{border-color:#262729}.custom-theme .el-checkbox-button:last-child .el-checkbox-button__inner{border-radius:0 4px 4px 0}.custom-theme .el-checkbox-button--medium .el-checkbox-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.custom-theme .el-checkbox-button--medium .el-checkbox-button__inner.is-round{padding:10px 20px}.custom-theme .el-checkbox-button--small .el-checkbox-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.custom-theme .el-checkbox-button--small .el-checkbox-button__inner.is-round{padding:9px 15px}.custom-theme .el-checkbox-button--mini .el-checkbox-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.custom-theme .el-checkbox-button--mini .el-checkbox-button__inner.is-round{padding:7px 15px}.custom-theme .el-checkbox-group{font-size:0}.custom-theme .el-tag{background-color:rgba(38,39,41,.1);display:inline-block;padding:0 10px;height:32px;line-height:30px;font-size:12px;color:#262729;border-radius:4px;-webkit-box-sizing:border-box;box-sizing:border-box;border:1px solid rgba(38,39,41,.2);white-space:nowrap}.custom-theme .el-tag .el-icon-close{border-radius:50%;text-align:center;position:relative;cursor:pointer;font-size:12px;height:18px;width:18px;line-height:18px;vertical-align:middle;top:-1px;right:-5px;color:#262729}.custom-theme .el-tag .el-icon-close::before{display:block}.custom-theme .el-tag .el-icon-close:hover{background-color:#262729;color:#fff}.custom-theme .el-tag--info{background-color:rgba(10,118,164,.1);border-color:rgba(10,118,164,.2);color:#0a76a4}.custom-theme .el-tag--info.is-hit{border-color:#0a76a4}.custom-theme .el-tag--info .el-tag__close{color:#0a76a4}.custom-theme .el-tag--info .el-tag__close:hover{background-color:#0a76a4;color:#fff}.custom-theme .el-tag--success{background-color:rgba(64,145,103,.1);border-color:rgba(64,145,103,.2);color:#409167}.custom-theme .el-tag--success.is-hit{border-color:#409167}.custom-theme .el-tag--success .el-tag__close{color:#409167}.custom-theme .el-tag--success .el-tag__close:hover{background-color:#409167;color:#fff}.custom-theme .el-tag--warning{background-color:rgba(157,164,8,.1);border-color:rgba(157,164,8,.2);color:#9da408}.custom-theme .el-tag--warning.is-hit{border-color:#9da408}.custom-theme .el-tag--warning .el-tag__close{color:#9da408}.custom-theme .el-tag--warning .el-tag__close:hover{background-color:#9da408;color:#fff}.custom-theme .el-tag--danger{background-color:rgba(179,69,14,.1);border-color:rgba(179,69,14,.2);color:#b3450e}.custom-theme .el-tag--danger.is-hit{border-color:#b3450e}.custom-theme .el-tag--danger .el-tag__close{color:#b3450e}.custom-theme .el-tag--danger .el-tag__close:hover{background-color:#b3450e;color:#fff}.custom-theme .el-tag--medium{height:28px;line-height:26px}.custom-theme .el-tag--medium .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.custom-theme .el-tag--small{height:24px;padding:0 8px;line-height:22px}.custom-theme .el-tag--small .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.custom-theme .el-tag--mini{height:20px;padding:0 5px;line-height:19px}.custom-theme .el-tag--mini .el-icon-close{margin-left:-3px;-webkit-transform:scale(.7);transform:scale(.7)}.custom-theme .el-table-column--selection .cell{padding-left:14px;padding-right:14px}.custom-theme .el-table-filter{border:solid 1px #e6ebf5;border-radius:2px;background-color:#fff;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);-webkit-box-sizing:border-box;box-sizing:border-box;margin:2px 0}.custom-theme .el-table-filter__list{padding:5px 0;margin:0;list-style:none;min-width:100px}.custom-theme .el-table-filter__list-item{line-height:36px;padding:0 10px;cursor:pointer;font-size:14px}.custom-theme .el-table-filter__list-item:hover{background-color:#e9e9ea;color:#515254}.custom-theme .el-table-filter__list-item.is-active{background-color:#262729;color:#fff}.custom-theme .el-table-filter__content{min-width:100px}.custom-theme .el-table-filter__bottom{border-top:1px solid #e6ebf5;padding:8px}.custom-theme .el-table-filter__bottom button{background:0 0;border:none;color:#5a5e66;cursor:pointer;font-size:13px;padding:0 3px}.custom-theme .el-table-filter__bottom button:hover{color:#262729}.custom-theme .el-table-filter__bottom button:focus{outline:0}.custom-theme .el-table-filter__bottom button.is-disabled{color:#b4bccc;cursor:not-allowed}.custom-theme .el-table-filter__checkbox-group{padding:10px}.custom-theme .el-table-filter__checkbox-group label.el-checkbox{display:block;margin-bottom:8px;margin-left:5px}.custom-theme .el-table-filter__checkbox-group .el-checkbox:last-child{margin-bottom:0}.custom-theme .el-date-table{font-size:12px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.custom-theme .el-date-table.is-week-mode .el-date-table__row:hover div{background-color:#edf2fc}.custom-theme .el-date-table.is-week-mode .el-date-table__row:hover td.available:hover{color:#5a5e66}.custom-theme .el-date-table.is-week-mode .el-date-table__row:hover td:first-child div{margin-left:5px;border-top-left-radius:15px;border-bottom-left-radius:15px}.custom-theme .el-date-table.is-week-mode .el-date-table__row:hover td:last-child div{margin-right:5px;border-top-right-radius:15px;border-bottom-right-radius:15px}.custom-theme .el-date-table.is-week-mode .el-date-table__row.current div{background-color:#edf2fc}.custom-theme .el-date-table td{width:32px;height:30px;padding:4px 0;-webkit-box-sizing:border-box;box-sizing:border-box;text-align:center;cursor:pointer;position:relative}.custom-theme .el-date-table td div{height:30px;padding:3px 0;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-date-table td span{width:24px;height:24px;display:block;margin:0 auto;line-height:24px;position:absolute;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);border-radius:50%}.custom-theme .el-date-table td.next-month,.custom-theme .el-date-table td.prev-month{color:#b4bccc}.custom-theme .el-date-table td.today{position:relative}.custom-theme .el-date-table td.today span{color:#262729}.custom-theme .el-date-table td.today.end-date span,.custom-theme .el-date-table td.today.start-date span{color:#fff}.custom-theme .el-date-table td.available:hover{color:#262729}.custom-theme .el-date-table td.in-range div{background-color:#edf2fc}.custom-theme .el-date-table td.in-range div:hover{background-color:#edf2fc}.custom-theme .el-date-table td.current:not(.disabled) span{color:#fff;background-color:#262729}.custom-theme .el-date-table td.end-date div,.custom-theme .el-date-table td.start-date div{color:#fff}.custom-theme .el-date-table td.end-date span,.custom-theme .el-date-table td.start-date span{background-color:#262729}.custom-theme .el-date-table td.start-date div{margin-left:5px;border-top-left-radius:15px;border-bottom-left-radius:15px}.custom-theme .el-date-table td.end-date div{margin-right:5px;border-top-right-radius:15px;border-bottom-right-radius:15px}.custom-theme .el-date-table td.disabled div{background-color:#f5f7fa;opacity:1;cursor:not-allowed;color:#b4bccc}.custom-theme .el-date-table td.week{font-size:80%;color:#5a5e66}.custom-theme .el-date-table th{padding:5px;color:#5a5e66;font-weight:400;border-bottom:solid 1px #e6ebf5}.custom-theme .el-month-table{font-size:12px;margin:-1px;border-collapse:collapse}.custom-theme .el-month-table td{text-align:center;padding:20px 3px;cursor:pointer}.custom-theme .el-month-table td.disabled .cell{background-color:#f5f7fa;cursor:not-allowed;color:#b4bccc}.custom-theme .el-month-table td.disabled .cell:hover{color:#b4bccc}.custom-theme .el-month-table td .cell{width:48px;height:32px;display:block;line-height:32px;color:#5a5e66;margin:0 auto}.custom-theme .el-month-table td .cell:hover{color:#262729}.custom-theme .el-month-table td.current:not(.disabled) .cell{color:#262729}.custom-theme .el-year-table{font-size:12px;margin:-1px;border-collapse:collapse}.custom-theme .el-year-table .el-icon{color:#2d2f33}.custom-theme .el-year-table td{text-align:center;padding:20px 3px;cursor:pointer}.custom-theme .el-year-table td.disabled .cell{background-color:#f5f7fa;cursor:not-allowed;color:#b4bccc}.custom-theme .el-year-table td.disabled .cell:hover{color:#b4bccc}.custom-theme .el-year-table td .cell{width:48px;height:32px;display:block;line-height:32px;color:#5a5e66;margin:0 auto}.custom-theme .el-year-table td .cell:hover{color:#262729}.custom-theme .el-year-table td.current:not(.disabled) .cell{color:#262729}.custom-theme .el-time-spinner.has-seconds .el-time-spinner__wrapper{width:33.3%}.custom-theme .el-time-spinner.has-seconds .el-time-spinner__wrapper:nth-child(2){margin-left:1%}.custom-theme .el-time-spinner__wrapper{max-height:190px;overflow:auto;display:inline-block;width:50%;vertical-align:top;position:relative}.custom-theme .el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default){padding-bottom:15px}.custom-theme .el-time-spinner__wrapper.is-arrow{-webkit-box-sizing:border-box;box-sizing:border-box;text-align:center;overflow:hidden}.custom-theme .el-time-spinner__wrapper.is-arrow .el-time-spinner__list{-webkit-transform:translateY(-32px);transform:translateY(-32px)}.custom-theme .el-time-spinner__wrapper.is-arrow .el-time-spinner__item:hover:not(.disabled):not(.active){background:#fff;cursor:default}.custom-theme .el-time-spinner__arrow{font-size:12px;color:#878d99;position:absolute;left:0;width:100%;z-index:1;text-align:center;height:30px;line-height:30px;cursor:pointer}.custom-theme .el-time-spinner__arrow:hover{color:#262729}.custom-theme .el-time-spinner__arrow.el-icon-arrow-up{top:10px}.custom-theme .el-time-spinner__arrow.el-icon-arrow-down{bottom:10px}.custom-theme .el-time-spinner__input.el-input{width:70%}.custom-theme .el-time-spinner__input.el-input .el-input__inner{padding:0;text-align:center}.custom-theme .el-time-spinner__list{padding:0;margin:0;list-style:none;text-align:center}.custom-theme .el-time-spinner__list::after,.custom-theme .el-time-spinner__list::before{content:'';display:block;width:100%;height:80px}.custom-theme .el-time-spinner__item{height:32px;line-height:32px;font-size:12px;color:#5a5e66}.custom-theme .el-time-spinner__item:hover:not(.disabled):not(.active){background:#f5f7fa;cursor:pointer}.custom-theme .el-time-spinner__item.active:not(.disabled){color:#2d2f33;font-weight:700}.custom-theme .el-time-spinner__item.disabled{color:#b4bccc;cursor:not-allowed}.custom-theme .fade-in-linear-enter-active,.custom-theme .fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.custom-theme .fade-in-linear-enter,.custom-theme .fade-in-linear-leave,.custom-theme .fade-in-linear-leave-active{opacity:0}.custom-theme .el-fade-in-linear-enter-active,.custom-theme .el-fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.custom-theme .el-fade-in-linear-enter,.custom-theme .el-fade-in-linear-leave,.custom-theme .el-fade-in-linear-leave-active{opacity:0}.custom-theme .el-fade-in-enter-active,.custom-theme .el-fade-in-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.custom-theme .el-fade-in-enter,.custom-theme .el-fade-in-leave-active{opacity:0}.custom-theme .el-zoom-in-center-enter-active,.custom-theme .el-zoom-in-center-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.custom-theme .el-zoom-in-center-enter,.custom-theme .el-zoom-in-center-leave-active{opacity:0;-webkit-transform:scaleX(0);transform:scaleX(0)}.custom-theme .el-zoom-in-top-enter-active,.custom-theme .el-zoom-in-top-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;-webkit-transform-origin:center top;transform-origin:center top}.custom-theme .el-zoom-in-top-enter,.custom-theme .el-zoom-in-top-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.custom-theme .el-zoom-in-bottom-enter-active,.custom-theme .el-zoom-in-bottom-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;-webkit-transform-origin:center bottom;transform-origin:center bottom}.custom-theme .el-zoom-in-bottom-enter,.custom-theme .el-zoom-in-bottom-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.custom-theme .el-zoom-in-left-enter-active,.custom-theme .el-zoom-in-left-leave-active{opacity:1;-webkit-transform:scale(1,1);transform:scale(1,1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;-webkit-transform-origin:top left;transform-origin:top left}.custom-theme .el-zoom-in-left-enter,.custom-theme .el-zoom-in-left-leave-active{opacity:0;-webkit-transform:scale(.45,.45);transform:scale(.45,.45)}.custom-theme .collapse-transition{-webkit-transition:.3s height ease-in-out,.3s padding-top ease-in-out,.3s padding-bottom ease-in-out;transition:.3s height ease-in-out,.3s padding-top ease-in-out,.3s padding-bottom ease-in-out}.custom-theme .horizontal-collapse-transition{-webkit-transition:.3s width ease-in-out,.3s padding-left ease-in-out,.3s padding-right ease-in-out;transition:.3s width ease-in-out,.3s padding-left ease-in-out,.3s padding-right ease-in-out}.custom-theme .el-list-enter-active,.custom-theme .el-list-leave-active{-webkit-transition:all 1s;transition:all 1s}.custom-theme .el-list-enter,.custom-theme .el-list-leave-active{opacity:0;-webkit-transform:translateY(-30px);transform:translateY(-30px)}.custom-theme .el-opacity-transition{-webkit-transition:opacity .3s cubic-bezier(.55,0,.1,1);transition:opacity .3s cubic-bezier(.55,0,.1,1)}.custom-theme .el-date-editor{position:relative;display:inline-block;text-align:left}.custom-theme .el-date-editor.el-input,.custom-theme .el-date-editor.el-input__inner{width:220px}.custom-theme .el-date-editor--daterange.el-input,.custom-theme .el-date-editor--daterange.el-input__inner,.custom-theme .el-date-editor--timerange.el-input,.custom-theme .el-date-editor--timerange.el-input__inner{width:350px}.custom-theme .el-date-editor--datetimerange.el-input,.custom-theme .el-date-editor--datetimerange.el-input__inner{width:400px}.custom-theme .el-date-editor .el-range__icon{font-size:14px;margin-left:-5px;color:#b4bccc;float:left;line-height:32px}.custom-theme .el-date-editor .el-range-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:none;outline:0;display:inline-block;height:100%;margin:0;padding:0;width:39%;text-align:center;font-size:14px;color:#5a5e66}.custom-theme .el-date-editor .el-range-input::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-date-editor .el-range-input:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-date-editor .el-range-input::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-date-editor .el-range-input::placeholder{color:#b4bccc}.custom-theme .el-date-editor .el-range-separator{display:inline-block;height:100%;padding:0 5px;margin:0;text-align:center;line-height:32px;font-size:14px;width:5%;color:#2d2f33}.custom-theme .el-date-editor .el-range__close-icon{font-size:14px;color:#b4bccc;width:25px;display:inline-block;float:right;line-height:32px}.custom-theme .el-range-editor.el-input__inner{padding:3px 10px}.custom-theme .el-range-editor.is-active{border-color:#262729}.custom-theme .el-range-editor.is-active:hover{border-color:#262729}.custom-theme .el-range-editor--medium.el-input__inner{height:36px}.custom-theme .el-range-editor--medium .el-range-separator{line-height:28px;font-size:14px}.custom-theme .el-range-editor--medium .el-range-input{font-size:14px}.custom-theme .el-range-editor--medium .el-range__close-icon,.custom-theme .el-range-editor--medium .el-range__icon{line-height:28px}.custom-theme .el-range-editor--small.el-input__inner{height:32px}.custom-theme .el-range-editor--small .el-range-separator{line-height:24px;font-size:13px}.custom-theme .el-range-editor--small .el-range-input{font-size:13px}.custom-theme .el-range-editor--small .el-range__close-icon,.custom-theme .el-range-editor--small .el-range__icon{line-height:24px}.custom-theme .el-range-editor--mini.el-input__inner{height:28px}.custom-theme .el-range-editor--mini .el-range-separator{line-height:20px;font-size:12px}.custom-theme .el-range-editor--mini .el-range-input{font-size:12px}.custom-theme .el-range-editor--mini .el-range__close-icon,.custom-theme .el-range-editor--mini .el-range__icon{line-height:20px}.custom-theme .el-range-editor.is-disabled{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-range-editor.is-disabled:focus,.custom-theme .el-range-editor.is-disabled:hover{border-color:#dfe4ed}.custom-theme .el-range-editor.is-disabled input{background-color:#f5f7fa;color:#b4bccc;cursor:not-allowed}.custom-theme .el-range-editor.is-disabled input::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-range-editor.is-disabled input:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-range-editor.is-disabled input::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-range-editor.is-disabled input::placeholder{color:#b4bccc}.custom-theme .el-range-editor.is-disabled .el-range-separator{color:#b4bccc}.custom-theme .el-picker-panel{color:#5a5e66;border:1px solid #dfe4ed;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);background:#fff;border-radius:4px;line-height:30px;margin:5px 0}.custom-theme .el-picker-panel__body-wrapper::after,.custom-theme .el-picker-panel__body::after{content:"";display:table;clear:both}.custom-theme .el-picker-panel__content{position:relative;margin:15px}.custom-theme .el-picker-panel__footer{border-top:1px solid #e4e4e4;padding:4px;text-align:right;background-color:#fff;position:relative;font-size:0}.custom-theme .el-picker-panel__shortcut{display:block;width:100%;border:0;background-color:transparent;line-height:28px;font-size:14px;color:#5a5e66;padding-left:12px;text-align:left;outline:0;cursor:pointer}.custom-theme .el-picker-panel__shortcut:hover{color:#262729}.custom-theme .el-picker-panel__shortcut.active{background-color:#e6f1fe;color:#262729}.custom-theme .el-picker-panel__btn{border:1px solid #dcdcdc;color:#333;line-height:24px;border-radius:2px;padding:0 20px;cursor:pointer;background-color:transparent;outline:0;font-size:12px}.custom-theme .el-picker-panel__btn[disabled]{color:#ccc;cursor:not-allowed}.custom-theme .el-picker-panel__icon-btn{font-size:12px;color:#2d2f33;border:0;background:0 0;cursor:pointer;outline:0;margin-top:8px}.custom-theme .el-picker-panel__icon-btn:hover{color:#262729}.custom-theme .el-picker-panel__icon-btn.is-disabled{color:#bbb}.custom-theme .el-picker-panel__icon-btn.is-disabled:hover{cursor:not-allowed}.custom-theme .el-picker-panel__link-btn{vertical-align:middle}.custom-theme .el-picker-panel .popper__arrow{-webkit-transform:translateX(-400%);transform:translateX(-400%)}.custom-theme .el-picker-panel [slot=sidebar],.custom-theme .el-picker-panel__sidebar{position:absolute;top:0;bottom:0;width:110px;border-right:1px solid #e4e4e4;-webkit-box-sizing:border-box;box-sizing:border-box;padding-top:6px;background-color:#fff;overflow:auto}.custom-theme .el-picker-panel [slot=sidebar]+.el-picker-panel__body,.custom-theme .el-picker-panel__sidebar+.el-picker-panel__body{margin-left:110px}.custom-theme .el-date-picker{width:322px}.custom-theme .el-date-picker.has-sidebar.has-time{width:434px}.custom-theme .el-date-picker.has-sidebar{width:438px}.custom-theme .el-date-picker.has-time .el-picker-panel__body-wrapper{position:relative}.custom-theme .el-date-picker .el-picker-panel__content{width:292px}.custom-theme .el-date-picker table{table-layout:fixed;width:100%}.custom-theme .el-date-picker__editor-wrap{position:relative;display:table-cell;padding:0 5px}.custom-theme .el-date-picker__time-header{position:relative;border-bottom:1px solid #e4e4e4;font-size:12px;padding:8px 5px 5px 5px;display:table;width:100%;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-date-picker__header{margin:12px;text-align:center}.custom-theme .el-date-picker__header--bordered{margin-bottom:0;padding-bottom:12px;border-bottom:solid 1px #e6ebf5}.custom-theme .el-date-picker__header--bordered+.el-picker-panel__content{margin-top:0}.custom-theme .el-date-picker__header-label{font-size:16px;font-weight:500;padding:0 5px;line-height:22px;text-align:center;cursor:pointer;color:#5a5e66}.custom-theme .el-date-picker__header-label:hover{color:#262729}.custom-theme .el-date-picker__header-label.active{color:#262729}.custom-theme .el-date-picker__prev-btn{float:left}.custom-theme .el-date-picker__next-btn{float:right}.custom-theme .el-date-picker__time-wrap{padding:10px;text-align:center}.custom-theme .el-date-picker__time-label{float:left;cursor:pointer;line-height:30px;margin-left:10px}.custom-theme .el-date-range-picker{width:646px}.custom-theme .el-date-range-picker.has-sidebar{width:756px}.custom-theme .el-date-range-picker table{table-layout:fixed;width:100%}.custom-theme .el-date-range-picker .el-picker-panel__body{min-width:513px}.custom-theme .el-date-range-picker .el-picker-panel__content{margin:0}.custom-theme .el-date-range-picker__header{position:relative;text-align:center;height:28px}.custom-theme .el-date-range-picker__header [class*=arrow-left]{float:left}.custom-theme .el-date-range-picker__header [class*=arrow-right]{float:right}.custom-theme .el-date-range-picker__header div{font-size:16px;font-weight:500;margin-right:50px}.custom-theme .el-date-range-picker__content{float:left;width:50%;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:16px}.custom-theme .el-date-range-picker__content.is-left{border-right:1px solid #e4e4e4}.custom-theme .el-date-range-picker__content.is-right .el-date-range-picker__header div{margin-left:50px;margin-right:50px}.custom-theme .el-date-range-picker__editors-wrap{-webkit-box-sizing:border-box;box-sizing:border-box;display:table-cell}.custom-theme .el-date-range-picker__editors-wrap.is-right{text-align:right}.custom-theme .el-date-range-picker__time-header{position:relative;border-bottom:1px solid #e4e4e4;font-size:12px;padding:8px 5px 5px 5px;display:table;width:100%;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-date-range-picker__time-header>.el-icon-arrow-right{font-size:20px;vertical-align:middle;display:table-cell;color:#2d2f33}.custom-theme .el-date-range-picker__time-picker-wrap{position:relative;display:table-cell;padding:0 5px}.custom-theme .el-date-range-picker__time-picker-wrap .el-picker-panel{position:absolute;top:13px;right:0;z-index:1;background:#fff}.custom-theme .el-time-range-picker{width:354px;overflow:visible}.custom-theme .el-time-range-picker__content{position:relative;text-align:center;padding:10px}.custom-theme .el-time-range-picker__cell{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:4px 7px 7px;width:50%;display:inline-block}.custom-theme .el-time-range-picker__header{margin-bottom:5px;text-align:center;font-size:14px}.custom-theme .el-time-range-picker__body{border-radius:2px;border:1px solid #dfe4ed}.custom-theme .el-time-panel{margin:5px 0;border:solid 1px #dfe4ed;background-color:#fff;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);border-radius:2px;position:absolute;width:180px;left:0;z-index:1000;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.custom-theme .el-time-panel__content{font-size:0;position:relative;overflow:hidden}.custom-theme .el-time-panel__content::after,.custom-theme .el-time-panel__content::before{content:"";top:50%;position:absolute;margin-top:-15px;height:32px;z-index:-1;left:0;right:0;-webkit-box-sizing:border-box;box-sizing:border-box;padding-top:6px;text-align:left;border-top:1px solid #dfe4ed;border-bottom:1px solid #dfe4ed}.custom-theme .el-time-panel__content::after{left:50%;margin-left:12%;margin-right:12%}.custom-theme .el-time-panel__content::before{padding-left:50%;margin-right:12%;margin-left:12%}.custom-theme .el-time-panel__content.has-seconds::after{left:calc(100% / 3 * 2)}.custom-theme .el-time-panel__content.has-seconds::before{padding-left:calc(100% / 3)}.custom-theme .el-time-panel__footer{border-top:1px solid #e4e4e4;padding:4px;height:36px;line-height:25px;text-align:right;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-time-panel__btn{border:none;line-height:28px;padding:0 5px;margin:0 5px;cursor:pointer;background-color:transparent;outline:0;font-size:12px;color:#2d2f33}.custom-theme .el-time-panel__btn.confirm{font-weight:800;color:#262729}.custom-theme .el-time-panel .popper__arrow{-webkit-transform:translateX(-400%);transform:translateX(-400%)}.custom-theme .el-input{position:relative;font-size:14px;display:inline-block;width:100%}.custom-theme .el-input::-webkit-scrollbar{z-index:11;width:6px}.custom-theme .el-input::-webkit-scrollbar:horizontal{height:6px}.custom-theme .el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.custom-theme .el-input::-webkit-scrollbar-corner{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.custom-theme .el-input__inner{-webkit-appearance:none;background-color:#fff;background-image:none;border-radius:4px;border:1px solid #d8dce5;-webkit-box-sizing:border-box;box-sizing:border-box;color:#5a5e66;display:inline-block;font-size:inherit;height:40px;line-height:1;outline:0;padding:0 15px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.custom-theme .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input__inner:hover{border-color:#b4bccc}.custom-theme .el-input__inner:focus{outline:0;border-color:#262729}.custom-theme .el-input__suffix{position:absolute;height:100%;right:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s;pointer-events:none}.custom-theme .el-input__suffix-inner{pointer-events:all}.custom-theme .el-input__prefix{position:absolute;height:100%;left:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s}.custom-theme .el-input__icon{height:100%;width:25px;text-align:center;-webkit-transition:all .3s;transition:all .3s;line-height:40px}.custom-theme .el-input__icon:after{content:'';height:100%;width:0;display:inline-block;vertical-align:middle}.custom-theme .el-input__validateIcon{pointer-events:none}.custom-theme .el-input.is-active .el-input__inner{outline:0;border-color:#262729}.custom-theme .el-input.is-disabled .el-input__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-input.is-disabled .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__icon{cursor:not-allowed}.custom-theme .el-input--suffix .el-input__inner{padding-right:30px}.custom-theme .el-input--prefix .el-input__inner{padding-left:30px}.custom-theme .el-input--medium{font-size:14px}.custom-theme .el-input--medium .el-input__inner{height:36px}.custom-theme .el-input--medium .el-input__icon{line-height:36px}.custom-theme .el-input--small{font-size:13px}.custom-theme .el-input--small .el-input__inner{height:32px}.custom-theme .el-input--small .el-input__icon{line-height:32px}.custom-theme .el-input--mini{font-size:12px}.custom-theme .el-input--mini .el-input__inner{height:28px}.custom-theme .el-input--mini .el-input__icon{line-height:28px}.custom-theme .el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate}.custom-theme .el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.custom-theme .el-input-group__append,.custom-theme .el-input-group__prepend{background-color:#f5f7fa;color:#0a76a4;vertical-align:middle;display:table-cell;position:relative;border:1px solid #d8dce5;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.custom-theme .el-input-group__append:focus,.custom-theme .el-input-group__prepend:focus{outline:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-select,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-select{display:inline-block;margin:-20px}.custom-theme .el-input-group__append button.el-button,.custom-theme .el-input-group__append div.el-select .el-input__inner,.custom-theme .el-input-group__append div.el-select:hover .el-input__inner,.custom-theme .el-input-group__prepend button.el-button,.custom-theme .el-input-group__prepend div.el-select .el-input__inner,.custom-theme .el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-input,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-input{font-size:inherit}.custom-theme .el-input-group__prepend{border-right:0;border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-input-group__append{border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--prepend .el-input__inner{border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--append .el-input__inner{border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-textarea{display:inline-block;width:100%;vertical-align:bottom}.custom-theme .el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;font-size:14px;color:#5a5e66;background-color:#fff;background-image:none;border:1px solid #d8dce5;border-radius:4px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.custom-theme .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:hover{border-color:#b4bccc}.custom-theme .el-textarea__inner:focus{outline:0;border-color:#262729}.custom-theme .el-textarea.is-disabled .el-textarea__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-scrollbar{overflow:hidden;position:relative}.custom-theme .el-scrollbar:active>.el-scrollbar__bar,.custom-theme .el-scrollbar:focus>.el-scrollbar__bar,.custom-theme .el-scrollbar:hover>.el-scrollbar__bar{opacity:1;-webkit-transition:opacity 340ms ease-out;transition:opacity 340ms ease-out}.custom-theme .el-scrollbar__wrap{overflow:scroll;height:100%}.custom-theme .el-scrollbar__wrap--hidden-default::-webkit-scrollbar{width:0;height:0}.custom-theme .el-scrollbar__thumb{position:relative;display:block;width:0;height:0;cursor:pointer;border-radius:inherit;background-color:rgba(135,141,153,.3);-webkit-transition:.3s background-color;transition:.3s background-color}.custom-theme .el-scrollbar__thumb:hover{background-color:rgba(135,141,153,.5)}.custom-theme .el-scrollbar__bar{position:absolute;right:2px;bottom:2px;z-index:1;border-radius:4px;opacity:0;-webkit-transition:opacity 120ms ease-out;transition:opacity 120ms ease-out}.custom-theme .el-scrollbar__bar.is-vertical{width:6px;top:2px}.custom-theme .el-scrollbar__bar.is-vertical>div{width:100%}.custom-theme .el-scrollbar__bar.is-horizontal{height:6px;left:2px}.custom-theme .el-scrollbar__bar.is-horizontal>div{height:100%}.custom-theme .el-popper .popper__arrow,.custom-theme .el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.custom-theme .el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03));filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.custom-theme .el-popper .popper__arrow::after{content:" ";border-width:6px}.custom-theme .el-popper[x-placement^=top]{margin-bottom:12px}.custom-theme .el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#e6ebf5;border-bottom-width:0}.custom-theme .el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#fff;border-bottom-width:0}.custom-theme .el-popper[x-placement^=bottom]{margin-top:12px}.custom-theme .el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#e6ebf5}.custom-theme .el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#fff}.custom-theme .el-popper[x-placement^=right]{margin-left:12px}.custom-theme .el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#e6ebf5;border-left-width:0}.custom-theme .el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#fff;border-left-width:0}.custom-theme .el-popper[x-placement^=left]{margin-right:12px}.custom-theme .el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#e6ebf5}.custom-theme .el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#fff}.custom-theme .fade-in-linear-enter-active,.custom-theme .fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.custom-theme .fade-in-linear-enter,.custom-theme .fade-in-linear-leave,.custom-theme .fade-in-linear-leave-active{opacity:0}.custom-theme .el-fade-in-linear-enter-active,.custom-theme .el-fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.custom-theme .el-fade-in-linear-enter,.custom-theme .el-fade-in-linear-leave,.custom-theme .el-fade-in-linear-leave-active{opacity:0}.custom-theme .el-fade-in-enter-active,.custom-theme .el-fade-in-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.custom-theme .el-fade-in-enter,.custom-theme .el-fade-in-leave-active{opacity:0}.custom-theme .el-zoom-in-center-enter-active,.custom-theme .el-zoom-in-center-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.custom-theme .el-zoom-in-center-enter,.custom-theme .el-zoom-in-center-leave-active{opacity:0;-webkit-transform:scaleX(0);transform:scaleX(0)}.custom-theme .el-zoom-in-top-enter-active,.custom-theme .el-zoom-in-top-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;-webkit-transform-origin:center top;transform-origin:center top}.custom-theme .el-zoom-in-top-enter,.custom-theme .el-zoom-in-top-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.custom-theme .el-zoom-in-bottom-enter-active,.custom-theme .el-zoom-in-bottom-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;-webkit-transform-origin:center bottom;transform-origin:center bottom}.custom-theme .el-zoom-in-bottom-enter,.custom-theme .el-zoom-in-bottom-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.custom-theme .el-zoom-in-left-enter-active,.custom-theme .el-zoom-in-left-leave-active{opacity:1;-webkit-transform:scale(1,1);transform:scale(1,1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;-webkit-transform-origin:top left;transform-origin:top left}.custom-theme .el-zoom-in-left-enter,.custom-theme .el-zoom-in-left-leave-active{opacity:0;-webkit-transform:scale(.45,.45);transform:scale(.45,.45)}.custom-theme .collapse-transition{-webkit-transition:.3s height ease-in-out,.3s padding-top ease-in-out,.3s padding-bottom ease-in-out;transition:.3s height ease-in-out,.3s padding-top ease-in-out,.3s padding-bottom ease-in-out}.custom-theme .horizontal-collapse-transition{-webkit-transition:.3s width ease-in-out,.3s padding-left ease-in-out,.3s padding-right ease-in-out;transition:.3s width ease-in-out,.3s padding-left ease-in-out,.3s padding-right ease-in-out}.custom-theme .el-list-enter-active,.custom-theme .el-list-leave-active{-webkit-transition:all 1s;transition:all 1s}.custom-theme .el-list-enter,.custom-theme .el-list-leave-active{opacity:0;-webkit-transform:translateY(-30px);transform:translateY(-30px)}.custom-theme .el-opacity-transition{-webkit-transition:opacity .3s cubic-bezier(.55,0,.1,1);transition:opacity .3s cubic-bezier(.55,0,.1,1)}.custom-theme .el-date-editor{position:relative;display:inline-block;text-align:left}.custom-theme .el-date-editor.el-input,.custom-theme .el-date-editor.el-input__inner{width:220px}.custom-theme .el-date-editor--daterange.el-input,.custom-theme .el-date-editor--daterange.el-input__inner,.custom-theme .el-date-editor--timerange.el-input,.custom-theme .el-date-editor--timerange.el-input__inner{width:350px}.custom-theme .el-date-editor--datetimerange.el-input,.custom-theme .el-date-editor--datetimerange.el-input__inner{width:400px}.custom-theme .el-date-editor .el-range__icon{font-size:14px;margin-left:-5px;color:#b4bccc;float:left;line-height:32px}.custom-theme .el-date-editor .el-range-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:none;outline:0;display:inline-block;height:100%;margin:0;padding:0;width:39%;text-align:center;font-size:14px;color:#5a5e66}.custom-theme .el-date-editor .el-range-input::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-date-editor .el-range-input:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-date-editor .el-range-input::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-date-editor .el-range-input::placeholder{color:#b4bccc}.custom-theme .el-date-editor .el-range-separator{display:inline-block;height:100%;padding:0 5px;margin:0;text-align:center;line-height:32px;font-size:14px;width:5%;color:#2d2f33}.custom-theme .el-date-editor .el-range__close-icon{font-size:14px;color:#b4bccc;width:25px;display:inline-block;float:right;line-height:32px}.custom-theme .el-range-editor.el-input__inner{padding:3px 10px}.custom-theme .el-range-editor.is-active{border-color:#262729}.custom-theme .el-range-editor.is-active:hover{border-color:#262729}.custom-theme .el-range-editor--medium.el-input__inner{height:36px}.custom-theme .el-range-editor--medium .el-range-separator{line-height:28px;font-size:14px}.custom-theme .el-range-editor--medium .el-range-input{font-size:14px}.custom-theme .el-range-editor--medium .el-range__close-icon,.custom-theme .el-range-editor--medium .el-range__icon{line-height:28px}.custom-theme .el-range-editor--small.el-input__inner{height:32px}.custom-theme .el-range-editor--small .el-range-separator{line-height:24px;font-size:13px}.custom-theme .el-range-editor--small .el-range-input{font-size:13px}.custom-theme .el-range-editor--small .el-range__close-icon,.custom-theme .el-range-editor--small .el-range__icon{line-height:24px}.custom-theme .el-range-editor--mini.el-input__inner{height:28px}.custom-theme .el-range-editor--mini .el-range-separator{line-height:20px;font-size:12px}.custom-theme .el-range-editor--mini .el-range-input{font-size:12px}.custom-theme .el-range-editor--mini .el-range__close-icon,.custom-theme .el-range-editor--mini .el-range__icon{line-height:20px}.custom-theme .el-range-editor.is-disabled{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-range-editor.is-disabled:focus,.custom-theme .el-range-editor.is-disabled:hover{border-color:#dfe4ed}.custom-theme .el-range-editor.is-disabled input{background-color:#f5f7fa;color:#b4bccc;cursor:not-allowed}.custom-theme .el-range-editor.is-disabled input::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-range-editor.is-disabled input:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-range-editor.is-disabled input::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-range-editor.is-disabled input::placeholder{color:#b4bccc}.custom-theme .el-range-editor.is-disabled .el-range-separator{color:#b4bccc}.custom-theme .el-picker-panel{color:#5a5e66;border:1px solid #dfe4ed;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);background:#fff;border-radius:4px;line-height:30px;margin:5px 0}.custom-theme .el-picker-panel__body-wrapper::after,.custom-theme .el-picker-panel__body::after{content:"";display:table;clear:both}.custom-theme .el-picker-panel__content{position:relative;margin:15px}.custom-theme .el-picker-panel__footer{border-top:1px solid #e4e4e4;padding:4px;text-align:right;background-color:#fff;position:relative;font-size:0}.custom-theme .el-picker-panel__shortcut{display:block;width:100%;border:0;background-color:transparent;line-height:28px;font-size:14px;color:#5a5e66;padding-left:12px;text-align:left;outline:0;cursor:pointer}.custom-theme .el-picker-panel__shortcut:hover{color:#262729}.custom-theme .el-picker-panel__shortcut.active{background-color:#e6f1fe;color:#262729}.custom-theme .el-picker-panel__btn{border:1px solid #dcdcdc;color:#333;line-height:24px;border-radius:2px;padding:0 20px;cursor:pointer;background-color:transparent;outline:0;font-size:12px}.custom-theme .el-picker-panel__btn[disabled]{color:#ccc;cursor:not-allowed}.custom-theme .el-picker-panel__icon-btn{font-size:12px;color:#2d2f33;border:0;background:0 0;cursor:pointer;outline:0;margin-top:8px}.custom-theme .el-picker-panel__icon-btn:hover{color:#262729}.custom-theme .el-picker-panel__icon-btn.is-disabled{color:#bbb}.custom-theme .el-picker-panel__icon-btn.is-disabled:hover{cursor:not-allowed}.custom-theme .el-picker-panel__link-btn{vertical-align:middle}.custom-theme .el-picker-panel .popper__arrow{-webkit-transform:translateX(-400%);transform:translateX(-400%)}.custom-theme .el-picker-panel [slot=sidebar],.custom-theme .el-picker-panel__sidebar{position:absolute;top:0;bottom:0;width:110px;border-right:1px solid #e4e4e4;-webkit-box-sizing:border-box;box-sizing:border-box;padding-top:6px;background-color:#fff;overflow:auto}.custom-theme .el-picker-panel [slot=sidebar]+.el-picker-panel__body,.custom-theme .el-picker-panel__sidebar+.el-picker-panel__body{margin-left:110px}.custom-theme .el-date-picker{width:322px}.custom-theme .el-date-picker.has-sidebar.has-time{width:434px}.custom-theme .el-date-picker.has-sidebar{width:438px}.custom-theme .el-date-picker.has-time .el-picker-panel__body-wrapper{position:relative}.custom-theme .el-date-picker .el-picker-panel__content{width:292px}.custom-theme .el-date-picker table{table-layout:fixed;width:100%}.custom-theme .el-date-picker__editor-wrap{position:relative;display:table-cell;padding:0 5px}.custom-theme .el-date-picker__time-header{position:relative;border-bottom:1px solid #e4e4e4;font-size:12px;padding:8px 5px 5px 5px;display:table;width:100%;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-date-picker__header{margin:12px;text-align:center}.custom-theme .el-date-picker__header--bordered{margin-bottom:0;padding-bottom:12px;border-bottom:solid 1px #e6ebf5}.custom-theme .el-date-picker__header--bordered+.el-picker-panel__content{margin-top:0}.custom-theme .el-date-picker__header-label{font-size:16px;font-weight:500;padding:0 5px;line-height:22px;text-align:center;cursor:pointer;color:#5a5e66}.custom-theme .el-date-picker__header-label:hover{color:#262729}.custom-theme .el-date-picker__header-label.active{color:#262729}.custom-theme .el-date-picker__prev-btn{float:left}.custom-theme .el-date-picker__next-btn{float:right}.custom-theme .el-date-picker__time-wrap{padding:10px;text-align:center}.custom-theme .el-date-picker__time-label{float:left;cursor:pointer;line-height:30px;margin-left:10px}.custom-theme .el-scrollbar{overflow:hidden;position:relative}.custom-theme .el-scrollbar:active>.el-scrollbar__bar,.custom-theme .el-scrollbar:focus>.el-scrollbar__bar,.custom-theme .el-scrollbar:hover>.el-scrollbar__bar{opacity:1;-webkit-transition:opacity 340ms ease-out;transition:opacity 340ms ease-out}.custom-theme .el-scrollbar__wrap{overflow:scroll;height:100%}.custom-theme .el-scrollbar__wrap--hidden-default::-webkit-scrollbar{width:0;height:0}.custom-theme .el-scrollbar__thumb{position:relative;display:block;width:0;height:0;cursor:pointer;border-radius:inherit;background-color:rgba(135,141,153,.3);-webkit-transition:.3s background-color;transition:.3s background-color}.custom-theme .el-scrollbar__thumb:hover{background-color:rgba(135,141,153,.5)}.custom-theme .el-scrollbar__bar{position:absolute;right:2px;bottom:2px;z-index:1;border-radius:4px;opacity:0;-webkit-transition:opacity 120ms ease-out;transition:opacity 120ms ease-out}.custom-theme .el-scrollbar__bar.is-vertical{width:6px;top:2px}.custom-theme .el-scrollbar__bar.is-vertical>div{width:100%}.custom-theme .el-scrollbar__bar.is-horizontal{height:6px;left:2px}.custom-theme .el-scrollbar__bar.is-horizontal>div{height:100%}.custom-theme .el-popper .popper__arrow,.custom-theme .el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.custom-theme .el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03));filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.custom-theme .el-popper .popper__arrow::after{content:" ";border-width:6px}.custom-theme .el-popper[x-placement^=top]{margin-bottom:12px}.custom-theme .el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#e6ebf5;border-bottom-width:0}.custom-theme .el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#fff;border-bottom-width:0}.custom-theme .el-popper[x-placement^=bottom]{margin-top:12px}.custom-theme .el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#e6ebf5}.custom-theme .el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#fff}.custom-theme .el-popper[x-placement^=right]{margin-left:12px}.custom-theme .el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#e6ebf5;border-left-width:0}.custom-theme .el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#fff;border-left-width:0}.custom-theme .el-popper[x-placement^=left]{margin-right:12px}.custom-theme .el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#e6ebf5}.custom-theme .el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#fff}.custom-theme .time-select{margin:5px 0;min-width:0}.custom-theme .time-select .el-picker-panel__content{max-height:200px;margin:0}.custom-theme .time-select-item{padding:8px 10px;font-size:14px;line-height:20px}.custom-theme .time-select-item.selected:not(.disabled){color:#262729;font-weight:700}.custom-theme .time-select-item.disabled{color:#dfe4ed;cursor:not-allowed}.custom-theme .time-select-item:hover{background-color:#f5f7fa;font-weight:700;cursor:pointer}.custom-theme .fade-in-linear-enter-active,.custom-theme .fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.custom-theme .fade-in-linear-enter,.custom-theme .fade-in-linear-leave,.custom-theme .fade-in-linear-leave-active{opacity:0}.custom-theme .el-fade-in-linear-enter-active,.custom-theme .el-fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.custom-theme .el-fade-in-linear-enter,.custom-theme .el-fade-in-linear-leave,.custom-theme .el-fade-in-linear-leave-active{opacity:0}.custom-theme .el-fade-in-enter-active,.custom-theme .el-fade-in-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.custom-theme .el-fade-in-enter,.custom-theme .el-fade-in-leave-active{opacity:0}.custom-theme .el-zoom-in-center-enter-active,.custom-theme .el-zoom-in-center-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.custom-theme .el-zoom-in-center-enter,.custom-theme .el-zoom-in-center-leave-active{opacity:0;-webkit-transform:scaleX(0);transform:scaleX(0)}.custom-theme .el-zoom-in-top-enter-active,.custom-theme .el-zoom-in-top-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;-webkit-transform-origin:center top;transform-origin:center top}.custom-theme .el-zoom-in-top-enter,.custom-theme .el-zoom-in-top-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.custom-theme .el-zoom-in-bottom-enter-active,.custom-theme .el-zoom-in-bottom-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;-webkit-transform-origin:center bottom;transform-origin:center bottom}.custom-theme .el-zoom-in-bottom-enter,.custom-theme .el-zoom-in-bottom-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.custom-theme .el-zoom-in-left-enter-active,.custom-theme .el-zoom-in-left-leave-active{opacity:1;-webkit-transform:scale(1,1);transform:scale(1,1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;-webkit-transform-origin:top left;transform-origin:top left}.custom-theme .el-zoom-in-left-enter,.custom-theme .el-zoom-in-left-leave-active{opacity:0;-webkit-transform:scale(.45,.45);transform:scale(.45,.45)}.custom-theme .collapse-transition{-webkit-transition:.3s height ease-in-out,.3s padding-top ease-in-out,.3s padding-bottom ease-in-out;transition:.3s height ease-in-out,.3s padding-top ease-in-out,.3s padding-bottom ease-in-out}.custom-theme .horizontal-collapse-transition{-webkit-transition:.3s width ease-in-out,.3s padding-left ease-in-out,.3s padding-right ease-in-out;transition:.3s width ease-in-out,.3s padding-left ease-in-out,.3s padding-right ease-in-out}.custom-theme .el-list-enter-active,.custom-theme .el-list-leave-active{-webkit-transition:all 1s;transition:all 1s}.custom-theme .el-list-enter,.custom-theme .el-list-leave-active{opacity:0;-webkit-transform:translateY(-30px);transform:translateY(-30px)}.custom-theme .el-opacity-transition{-webkit-transition:opacity .3s cubic-bezier(.55,0,.1,1);transition:opacity .3s cubic-bezier(.55,0,.1,1)}.custom-theme .el-date-editor{position:relative;display:inline-block;text-align:left}.custom-theme .el-date-editor.el-input,.custom-theme .el-date-editor.el-input__inner{width:220px}.custom-theme .el-date-editor--daterange.el-input,.custom-theme .el-date-editor--daterange.el-input__inner,.custom-theme .el-date-editor--timerange.el-input,.custom-theme .el-date-editor--timerange.el-input__inner{width:350px}.custom-theme .el-date-editor--datetimerange.el-input,.custom-theme .el-date-editor--datetimerange.el-input__inner{width:400px}.custom-theme .el-date-editor .el-range__icon{font-size:14px;margin-left:-5px;color:#b4bccc;float:left;line-height:32px}.custom-theme .el-date-editor .el-range-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:none;outline:0;display:inline-block;height:100%;margin:0;padding:0;width:39%;text-align:center;font-size:14px;color:#5a5e66}.custom-theme .el-date-editor .el-range-input::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-date-editor .el-range-input:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-date-editor .el-range-input::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-date-editor .el-range-input::placeholder{color:#b4bccc}.custom-theme .el-date-editor .el-range-separator{display:inline-block;height:100%;padding:0 5px;margin:0;text-align:center;line-height:32px;font-size:14px;width:5%;color:#2d2f33}.custom-theme .el-date-editor .el-range__close-icon{font-size:14px;color:#b4bccc;width:25px;display:inline-block;float:right;line-height:32px}.custom-theme .el-range-editor.el-input__inner{padding:3px 10px}.custom-theme .el-range-editor.is-active{border-color:#262729}.custom-theme .el-range-editor.is-active:hover{border-color:#262729}.custom-theme .el-range-editor--medium.el-input__inner{height:36px}.custom-theme .el-range-editor--medium .el-range-separator{line-height:28px;font-size:14px}.custom-theme .el-range-editor--medium .el-range-input{font-size:14px}.custom-theme .el-range-editor--medium .el-range__close-icon,.custom-theme .el-range-editor--medium .el-range__icon{line-height:28px}.custom-theme .el-range-editor--small.el-input__inner{height:32px}.custom-theme .el-range-editor--small .el-range-separator{line-height:24px;font-size:13px}.custom-theme .el-range-editor--small .el-range-input{font-size:13px}.custom-theme .el-range-editor--small .el-range__close-icon,.custom-theme .el-range-editor--small .el-range__icon{line-height:24px}.custom-theme .el-range-editor--mini.el-input__inner{height:28px}.custom-theme .el-range-editor--mini .el-range-separator{line-height:20px;font-size:12px}.custom-theme .el-range-editor--mini .el-range-input{font-size:12px}.custom-theme .el-range-editor--mini .el-range__close-icon,.custom-theme .el-range-editor--mini .el-range__icon{line-height:20px}.custom-theme .el-range-editor.is-disabled{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-range-editor.is-disabled:focus,.custom-theme .el-range-editor.is-disabled:hover{border-color:#dfe4ed}.custom-theme .el-range-editor.is-disabled input{background-color:#f5f7fa;color:#b4bccc;cursor:not-allowed}.custom-theme .el-range-editor.is-disabled input::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-range-editor.is-disabled input:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-range-editor.is-disabled input::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-range-editor.is-disabled input::placeholder{color:#b4bccc}.custom-theme .el-range-editor.is-disabled .el-range-separator{color:#b4bccc}.custom-theme .el-time-spinner.has-seconds .el-time-spinner__wrapper{width:33.3%}.custom-theme .el-time-spinner.has-seconds .el-time-spinner__wrapper:nth-child(2){margin-left:1%}.custom-theme .el-time-spinner__wrapper{max-height:190px;overflow:auto;display:inline-block;width:50%;vertical-align:top;position:relative}.custom-theme .el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default){padding-bottom:15px}.custom-theme .el-time-spinner__wrapper.is-arrow{-webkit-box-sizing:border-box;box-sizing:border-box;text-align:center;overflow:hidden}.custom-theme .el-time-spinner__wrapper.is-arrow .el-time-spinner__list{-webkit-transform:translateY(-32px);transform:translateY(-32px)}.custom-theme .el-time-spinner__wrapper.is-arrow .el-time-spinner__item:hover:not(.disabled):not(.active){background:#fff;cursor:default}.custom-theme .el-time-spinner__arrow{font-size:12px;color:#878d99;position:absolute;left:0;width:100%;z-index:1;text-align:center;height:30px;line-height:30px;cursor:pointer}.custom-theme .el-time-spinner__arrow:hover{color:#262729}.custom-theme .el-time-spinner__arrow.el-icon-arrow-up{top:10px}.custom-theme .el-time-spinner__arrow.el-icon-arrow-down{bottom:10px}.custom-theme .el-time-spinner__input.el-input{width:70%}.custom-theme .el-time-spinner__input.el-input .el-input__inner{padding:0;text-align:center}.custom-theme .el-time-spinner__list{padding:0;margin:0;list-style:none;text-align:center}.custom-theme .el-time-spinner__list::after,.custom-theme .el-time-spinner__list::before{content:'';display:block;width:100%;height:80px}.custom-theme .el-time-spinner__item{height:32px;line-height:32px;font-size:12px;color:#5a5e66}.custom-theme .el-time-spinner__item:hover:not(.disabled):not(.active){background:#f5f7fa;cursor:pointer}.custom-theme .el-time-spinner__item.active:not(.disabled){color:#2d2f33;font-weight:700}.custom-theme .el-time-spinner__item.disabled{color:#b4bccc;cursor:not-allowed}.custom-theme .el-time-panel{margin:5px 0;border:solid 1px #dfe4ed;background-color:#fff;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);border-radius:2px;position:absolute;width:180px;left:0;z-index:1000;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.custom-theme .el-time-panel__content{font-size:0;position:relative;overflow:hidden}.custom-theme .el-time-panel__content::after,.custom-theme .el-time-panel__content::before{content:"";top:50%;position:absolute;margin-top:-15px;height:32px;z-index:-1;left:0;right:0;-webkit-box-sizing:border-box;box-sizing:border-box;padding-top:6px;text-align:left;border-top:1px solid #dfe4ed;border-bottom:1px solid #dfe4ed}.custom-theme .el-time-panel__content::after{left:50%;margin-left:12%;margin-right:12%}.custom-theme .el-time-panel__content::before{padding-left:50%;margin-right:12%;margin-left:12%}.custom-theme .el-time-panel__content.has-seconds::after{left:calc(100% / 3 * 2)}.custom-theme .el-time-panel__content.has-seconds::before{padding-left:calc(100% / 3)}.custom-theme .el-time-panel__footer{border-top:1px solid #e4e4e4;padding:4px;height:36px;line-height:25px;text-align:right;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-time-panel__btn{border:none;line-height:28px;padding:0 5px;margin:0 5px;cursor:pointer;background-color:transparent;outline:0;font-size:12px;color:#2d2f33}.custom-theme .el-time-panel__btn.confirm{font-weight:800;color:#262729}.custom-theme .el-time-panel .popper__arrow{-webkit-transform:translateX(-400%);transform:translateX(-400%)}.custom-theme .el-input{position:relative;font-size:14px;display:inline-block;width:100%}.custom-theme .el-input::-webkit-scrollbar{z-index:11;width:6px}.custom-theme .el-input::-webkit-scrollbar:horizontal{height:6px}.custom-theme .el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.custom-theme .el-input::-webkit-scrollbar-corner{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.custom-theme .el-input__inner{-webkit-appearance:none;background-color:#fff;background-image:none;border-radius:4px;border:1px solid #d8dce5;-webkit-box-sizing:border-box;box-sizing:border-box;color:#5a5e66;display:inline-block;font-size:inherit;height:40px;line-height:1;outline:0;padding:0 15px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.custom-theme .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input__inner:hover{border-color:#b4bccc}.custom-theme .el-input__inner:focus{outline:0;border-color:#262729}.custom-theme .el-input__suffix{position:absolute;height:100%;right:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s;pointer-events:none}.custom-theme .el-input__suffix-inner{pointer-events:all}.custom-theme .el-input__prefix{position:absolute;height:100%;left:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s}.custom-theme .el-input__icon{height:100%;width:25px;text-align:center;-webkit-transition:all .3s;transition:all .3s;line-height:40px}.custom-theme .el-input__icon:after{content:'';height:100%;width:0;display:inline-block;vertical-align:middle}.custom-theme .el-input__validateIcon{pointer-events:none}.custom-theme .el-input.is-active .el-input__inner{outline:0;border-color:#262729}.custom-theme .el-input.is-disabled .el-input__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-input.is-disabled .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__icon{cursor:not-allowed}.custom-theme .el-input--suffix .el-input__inner{padding-right:30px}.custom-theme .el-input--prefix .el-input__inner{padding-left:30px}.custom-theme .el-input--medium{font-size:14px}.custom-theme .el-input--medium .el-input__inner{height:36px}.custom-theme .el-input--medium .el-input__icon{line-height:36px}.custom-theme .el-input--small{font-size:13px}.custom-theme .el-input--small .el-input__inner{height:32px}.custom-theme .el-input--small .el-input__icon{line-height:32px}.custom-theme .el-input--mini{font-size:12px}.custom-theme .el-input--mini .el-input__inner{height:28px}.custom-theme .el-input--mini .el-input__icon{line-height:28px}.custom-theme .el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate}.custom-theme .el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.custom-theme .el-input-group__append,.custom-theme .el-input-group__prepend{background-color:#f5f7fa;color:#0a76a4;vertical-align:middle;display:table-cell;position:relative;border:1px solid #d8dce5;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.custom-theme .el-input-group__append:focus,.custom-theme .el-input-group__prepend:focus{outline:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-select,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-select{display:inline-block;margin:-20px}.custom-theme .el-input-group__append button.el-button,.custom-theme .el-input-group__append div.el-select .el-input__inner,.custom-theme .el-input-group__append div.el-select:hover .el-input__inner,.custom-theme .el-input-group__prepend button.el-button,.custom-theme .el-input-group__prepend div.el-select .el-input__inner,.custom-theme .el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-input,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-input{font-size:inherit}.custom-theme .el-input-group__prepend{border-right:0;border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-input-group__append{border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--prepend .el-input__inner{border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--append .el-input__inner{border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-textarea{display:inline-block;width:100%;vertical-align:bottom}.custom-theme .el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;font-size:14px;color:#5a5e66;background-color:#fff;background-image:none;border:1px solid #d8dce5;border-radius:4px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.custom-theme .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:hover{border-color:#b4bccc}.custom-theme .el-textarea__inner:focus{outline:0;border-color:#262729}.custom-theme .el-textarea.is-disabled .el-textarea__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-scrollbar{overflow:hidden;position:relative}.custom-theme .el-scrollbar:active>.el-scrollbar__bar,.custom-theme .el-scrollbar:focus>.el-scrollbar__bar,.custom-theme .el-scrollbar:hover>.el-scrollbar__bar{opacity:1;-webkit-transition:opacity 340ms ease-out;transition:opacity 340ms ease-out}.custom-theme .el-scrollbar__wrap{overflow:scroll;height:100%}.custom-theme .el-scrollbar__wrap--hidden-default::-webkit-scrollbar{width:0;height:0}.custom-theme .el-scrollbar__thumb{position:relative;display:block;width:0;height:0;cursor:pointer;border-radius:inherit;background-color:rgba(135,141,153,.3);-webkit-transition:.3s background-color;transition:.3s background-color}.custom-theme .el-scrollbar__thumb:hover{background-color:rgba(135,141,153,.5)}.custom-theme .el-scrollbar__bar{position:absolute;right:2px;bottom:2px;z-index:1;border-radius:4px;opacity:0;-webkit-transition:opacity 120ms ease-out;transition:opacity 120ms ease-out}.custom-theme .el-scrollbar__bar.is-vertical{width:6px;top:2px}.custom-theme .el-scrollbar__bar.is-vertical>div{width:100%}.custom-theme .el-scrollbar__bar.is-horizontal{height:6px;left:2px}.custom-theme .el-scrollbar__bar.is-horizontal>div{height:100%}.custom-theme .el-popper .popper__arrow,.custom-theme .el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.custom-theme .el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03));filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.custom-theme .el-popper .popper__arrow::after{content:" ";border-width:6px}.custom-theme .el-popper[x-placement^=top]{margin-bottom:12px}.custom-theme .el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#e6ebf5;border-bottom-width:0}.custom-theme .el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#fff;border-bottom-width:0}.custom-theme .el-popper[x-placement^=bottom]{margin-top:12px}.custom-theme .el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#e6ebf5}.custom-theme .el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#fff}.custom-theme .el-popper[x-placement^=right]{margin-left:12px}.custom-theme .el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#e6ebf5;border-left-width:0}.custom-theme .el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#fff;border-left-width:0}.custom-theme .el-popper[x-placement^=left]{margin-right:12px}.custom-theme .el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#e6ebf5}.custom-theme .el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#fff}.custom-theme .el-popper .popper__arrow,.custom-theme .el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.custom-theme .el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03));filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.custom-theme .el-popper .popper__arrow::after{content:" ";border-width:6px}.custom-theme .el-popper[x-placement^=top]{margin-bottom:12px}.custom-theme .el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#e6ebf5;border-bottom-width:0}.custom-theme .el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#fff;border-bottom-width:0}.custom-theme .el-popper[x-placement^=bottom]{margin-top:12px}.custom-theme .el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#e6ebf5}.custom-theme .el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#fff}.custom-theme .el-popper[x-placement^=right]{margin-left:12px}.custom-theme .el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#e6ebf5;border-left-width:0}.custom-theme .el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#fff;border-left-width:0}.custom-theme .el-popper[x-placement^=left]{margin-right:12px}.custom-theme .el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#e6ebf5}.custom-theme .el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#fff}.custom-theme .el-popover{position:absolute;background:#fff;min-width:150px;border-radius:4px;border:1px solid #e6ebf5;padding:12px;z-index:2000;color:#5a5e66;line-height:1.4;text-align:justify;word-break:break-all;font-size:14px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.custom-theme .el-popover--plain{padding:18px 20px}.custom-theme .el-popover__title{color:#2d2f33;font-size:16px;line-height:1;margin-bottom:12px}.custom-theme .el-tooltip__popper{position:absolute;border-radius:4px;padding:10px;z-index:2000;font-size:12px;line-height:1.2}.custom-theme .el-tooltip__popper .popper__arrow,.custom-theme .el-tooltip__popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.custom-theme .el-tooltip__popper .popper__arrow{border-width:6px}.custom-theme .el-tooltip__popper .popper__arrow::after{content:" ";border-width:5px}.custom-theme .el-tooltip__popper[x-placement^=top]{margin-bottom:12px}.custom-theme .el-tooltip__popper[x-placement^=top] .popper__arrow{bottom:-6px;border-top-color:#2d2f33;border-bottom-width:0}.custom-theme .el-tooltip__popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-5px;border-top-color:#2d2f33;border-bottom-width:0}.custom-theme .el-tooltip__popper[x-placement^=bottom]{margin-top:12px}.custom-theme .el-tooltip__popper[x-placement^=bottom] .popper__arrow{top:-6px;border-top-width:0;border-bottom-color:#2d2f33}.custom-theme .el-tooltip__popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-5px;border-top-width:0;border-bottom-color:#2d2f33}.custom-theme .el-tooltip__popper[x-placement^=right]{margin-left:12px}.custom-theme .el-tooltip__popper[x-placement^=right] .popper__arrow{left:-6px;border-right-color:#2d2f33;border-left-width:0}.custom-theme .el-tooltip__popper[x-placement^=right] .popper__arrow::after{bottom:-5px;left:1px;border-right-color:#2d2f33;border-left-width:0}.custom-theme .el-tooltip__popper[x-placement^=left]{margin-right:12px}.custom-theme .el-tooltip__popper[x-placement^=left] .popper__arrow{right:-6px;border-right-width:0;border-left-color:#2d2f33}.custom-theme .el-tooltip__popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-5px;margin-left:-5px;border-right-width:0;border-left-color:#2d2f33}.custom-theme .el-tooltip__popper.is-dark{background:#2d2f33;color:#fff}.custom-theme .el-tooltip__popper.is-light{background:#fff;border:1px solid #2d2f33}.custom-theme .el-tooltip__popper.is-light[x-placement^=top] .popper__arrow{border-top-color:#2d2f33}.custom-theme .el-tooltip__popper.is-light[x-placement^=top] .popper__arrow::after{border-top-color:#fff}.custom-theme .el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow{border-bottom-color:#2d2f33}.custom-theme .el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow::after{border-bottom-color:#fff}.custom-theme .el-tooltip__popper.is-light[x-placement^=left] .popper__arrow{border-left-color:#2d2f33}.custom-theme .el-tooltip__popper.is-light[x-placement^=left] .popper__arrow::after{border-left-color:#fff}.custom-theme .el-tooltip__popper.is-light[x-placement^=right] .popper__arrow{border-right-color:#2d2f33}.custom-theme .el-tooltip__popper.is-light[x-placement^=right] .popper__arrow::after{border-right-color:#fff}.custom-theme .v-modal-enter{-webkit-animation:v-modal-in .2s ease;animation:v-modal-in .2s ease}.custom-theme .v-modal-leave{-webkit-animation:v-modal-out .2s ease forwards;animation:v-modal-out .2s ease forwards}@keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-out{100%{opacity:0}}.custom-theme .v-modal{position:fixed;left:0;top:0;width:100%;height:100%;opacity:.5;background:#000}.custom-theme .el-button{display:inline-block;line-height:1;white-space:nowrap;cursor:pointer;background:#fff;border:1px solid #d8dce5;border-color:#d8dce5;color:#5a5e66;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;-webkit-transition:.1s;transition:.1s;font-weight:500;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;padding:12px 20px;font-size:14px;border-radius:4px}.custom-theme .el-button+.el-button{margin-left:10px}.custom-theme .el-button.is-round{padding:12px 20px}.custom-theme .el-button:focus,.custom-theme .el-button:hover{color:#262729;border-color:#bebebf;background-color:#e9e9ea}.custom-theme .el-button:active{color:#222325;border-color:#222325;outline:0}.custom-theme .el-button::-moz-focus-inner{border:0}.custom-theme .el-button [class*=el-icon-]+span{margin-left:5px}.custom-theme .el-button.is-plain:focus,.custom-theme .el-button.is-plain:hover{background:#fff;border-color:#262729;color:#262729}.custom-theme .el-button.is-plain:active{background:#fff;border-color:#222325;color:#222325;outline:0}.custom-theme .el-button.is-active{color:#222325;border-color:#222325}.custom-theme .el-button.is-disabled,.custom-theme .el-button.is-disabled:focus,.custom-theme .el-button.is-disabled:hover{color:#b4bccc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#e6ebf5}.custom-theme .el-button.is-disabled.el-button--text{background-color:transparent}.custom-theme .el-button.is-disabled.is-plain,.custom-theme .el-button.is-disabled.is-plain:focus,.custom-theme .el-button.is-disabled.is-plain:hover{background-color:#fff;border-color:#e6ebf5;color:#b4bccc}.custom-theme .el-button.is-loading{position:relative;pointer-events:none}.custom-theme .el-button.is-loading:before{pointer-events:none;content:'';position:absolute;left:-1px;top:-1px;right:-1px;bottom:-1px;border-radius:inherit;background-color:rgba(255,255,255,.35)}.custom-theme .el-button.is-round{border-radius:20px;padding:12px 23px}.custom-theme .el-button--primary{color:#fff;background-color:#262729;border-color:#262729}.custom-theme .el-button--primary:focus,.custom-theme .el-button--primary:hover{background:#515254;border-color:#515254;color:#fff}.custom-theme .el-button--primary:active{background:#222325;border-color:#222325;color:#fff;outline:0}.custom-theme .el-button--primary.is-active{background:#222325;border-color:#222325;color:#fff}.custom-theme .el-button--primary.is-disabled,.custom-theme .el-button--primary.is-disabled:active,.custom-theme .el-button--primary.is-disabled:focus,.custom-theme .el-button--primary.is-disabled:hover{color:#fff;background-color:#939394;border-color:#939394}.custom-theme .el-button--primary.is-plain{color:#262729;background:#e9e9ea;border-color:#a8a9a9}.custom-theme .el-button--primary.is-plain:focus,.custom-theme .el-button--primary.is-plain:hover{background:#262729;border-color:#262729;color:#fff}.custom-theme .el-button--primary.is-plain:active{background:#222325;border-color:#222325;color:#fff;outline:0}.custom-theme .el-button--primary.is-plain.is-disabled,.custom-theme .el-button--primary.is-plain.is-disabled:active,.custom-theme .el-button--primary.is-plain.is-disabled:focus,.custom-theme .el-button--primary.is-plain.is-disabled:hover{color:#7d7d7f;background-color:#e9e9ea;border-color:#d4d4d4}.custom-theme .el-button--success{color:#fff;background-color:#409167;border-color:#409167}.custom-theme .el-button--success:focus,.custom-theme .el-button--success:hover{background:#66a785;border-color:#66a785;color:#fff}.custom-theme .el-button--success:active{background:#3a835d;border-color:#3a835d;color:#fff;outline:0}.custom-theme .el-button--success.is-active{background:#3a835d;border-color:#3a835d;color:#fff}.custom-theme .el-button--success.is-disabled,.custom-theme .el-button--success.is-disabled:active,.custom-theme .el-button--success.is-disabled:focus,.custom-theme .el-button--success.is-disabled:hover{color:#fff;background-color:#a0c8b3;border-color:#a0c8b3}.custom-theme .el-button--success.is-plain{color:#409167;background:#ecf4f0;border-color:#b3d3c2}.custom-theme .el-button--success.is-plain:focus,.custom-theme .el-button--success.is-plain:hover{background:#409167;border-color:#409167;color:#fff}.custom-theme .el-button--success.is-plain:active{background:#3a835d;border-color:#3a835d;color:#fff;outline:0}.custom-theme .el-button--success.is-plain.is-disabled,.custom-theme .el-button--success.is-plain.is-disabled:active,.custom-theme .el-button--success.is-plain.is-disabled:focus,.custom-theme .el-button--success.is-plain.is-disabled:hover{color:#8cbda4;background-color:#ecf4f0;border-color:#d9e9e1}.custom-theme .el-button--warning{color:#fff;background-color:#9da408;border-color:#9da408}.custom-theme .el-button--warning:focus,.custom-theme .el-button--warning:hover{background:#b1b639;border-color:#b1b639;color:#fff}.custom-theme .el-button--warning:active{background:#8d9407;border-color:#8d9407;color:#fff;outline:0}.custom-theme .el-button--warning.is-active{background:#8d9407;border-color:#8d9407;color:#fff}.custom-theme .el-button--warning.is-disabled,.custom-theme .el-button--warning.is-disabled:active,.custom-theme .el-button--warning.is-disabled:focus,.custom-theme .el-button--warning.is-disabled:hover{color:#fff;background-color:#ced284;border-color:#ced284}.custom-theme .el-button--warning.is-plain{color:#9da408;background:#f5f6e6;border-color:#d8db9c}.custom-theme .el-button--warning.is-plain:focus,.custom-theme .el-button--warning.is-plain:hover{background:#9da408;border-color:#9da408;color:#fff}.custom-theme .el-button--warning.is-plain:active{background:#8d9407;border-color:#8d9407;color:#fff;outline:0}.custom-theme .el-button--warning.is-plain.is-disabled,.custom-theme .el-button--warning.is-plain.is-disabled:active,.custom-theme .el-button--warning.is-plain.is-disabled:focus,.custom-theme .el-button--warning.is-plain.is-disabled:hover{color:#c4c86b;background-color:#f5f6e6;border-color:#ebedce}.custom-theme .el-button--danger{color:#fff;background-color:#b3450e;border-color:#b3450e}.custom-theme .el-button--danger:focus,.custom-theme .el-button--danger:hover{background:#c26a3e;border-color:#c26a3e;color:#fff}.custom-theme .el-button--danger:active{background:#a13e0d;border-color:#a13e0d;color:#fff;outline:0}.custom-theme .el-button--danger.is-active{background:#a13e0d;border-color:#a13e0d;color:#fff}.custom-theme .el-button--danger.is-disabled,.custom-theme .el-button--danger.is-disabled:active,.custom-theme .el-button--danger.is-disabled:focus,.custom-theme .el-button--danger.is-disabled:hover{color:#fff;background-color:#d9a287;border-color:#d9a287}.custom-theme .el-button--danger.is-plain{color:#b3450e;background:#f7ece7;border-color:#e1b59f}.custom-theme .el-button--danger.is-plain:focus,.custom-theme .el-button--danger.is-plain:hover{background:#b3450e;border-color:#b3450e;color:#fff}.custom-theme .el-button--danger.is-plain:active{background:#a13e0d;border-color:#a13e0d;color:#fff;outline:0}.custom-theme .el-button--danger.is-plain.is-disabled,.custom-theme .el-button--danger.is-plain.is-disabled:active,.custom-theme .el-button--danger.is-plain.is-disabled:focus,.custom-theme .el-button--danger.is-plain.is-disabled:hover{color:#d18f6e;background-color:#f7ece7;border-color:#f0dacf}.custom-theme .el-button--info{color:#fff;background-color:#0a76a4;border-color:#0a76a4}.custom-theme .el-button--info:focus,.custom-theme .el-button--info:hover{background:#3b91b6;border-color:#3b91b6;color:#fff}.custom-theme .el-button--info:active{background:#096a94;border-color:#096a94;color:#fff;outline:0}.custom-theme .el-button--info.is-active{background:#096a94;border-color:#096a94;color:#fff}.custom-theme .el-button--info.is-disabled,.custom-theme .el-button--info.is-disabled:active,.custom-theme .el-button--info.is-disabled:focus,.custom-theme .el-button--info.is-disabled:hover{color:#fff;background-color:#85bbd2;border-color:#85bbd2}.custom-theme .el-button--info.is-plain{color:#0a76a4;background:#e7f1f6;border-color:#9dc8db}.custom-theme .el-button--info.is-plain:focus,.custom-theme .el-button--info.is-plain:hover{background:#0a76a4;border-color:#0a76a4;color:#fff}.custom-theme .el-button--info.is-plain:active{background:#096a94;border-color:#096a94;color:#fff;outline:0}.custom-theme .el-button--info.is-plain.is-disabled,.custom-theme .el-button--info.is-plain.is-disabled:active,.custom-theme .el-button--info.is-plain.is-disabled:focus,.custom-theme .el-button--info.is-plain.is-disabled:hover{color:#6cadc8;background-color:#e7f1f6;border-color:#cee4ed}.custom-theme .el-button--medium{padding:10px 20px;font-size:14px;border-radius:4px}.custom-theme .el-button--medium.is-round{padding:10px 20px}.custom-theme .el-button--small{padding:9px 15px;font-size:12px;border-radius:3px}.custom-theme .el-button--small.is-round{padding:9px 15px}.custom-theme .el-button--mini{padding:7px 15px;font-size:12px;border-radius:3px}.custom-theme .el-button--mini.is-round{padding:7px 15px}.custom-theme .el-button--text{border:none;color:#262729;background:0 0;padding-left:0;padding-right:0}.custom-theme .el-button--text:focus,.custom-theme .el-button--text:hover{color:#515254;border-color:transparent;background-color:transparent}.custom-theme .el-button--text:active{color:#222325;border-color:transparent;background-color:transparent}.custom-theme .el-button-group{display:inline-block;vertical-align:middle}.custom-theme .el-button-group::after,.custom-theme .el-button-group::before{display:table;content:""}.custom-theme .el-button-group::after{clear:both}.custom-theme .el-button-group .el-button{float:left;position:relative}.custom-theme .el-button-group .el-button+.el-button{margin-left:0}.custom-theme .el-button-group .el-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-button-group .el-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-button-group .el-button:not(:first-child):not(:last-child){border-radius:0}.custom-theme .el-button-group .el-button:not(:last-child){margin-right:-1px}.custom-theme .el-button-group .el-button:active,.custom-theme .el-button-group .el-button:focus,.custom-theme .el-button-group .el-button:hover{z-index:1}.custom-theme .el-button-group .el-button.is-active{z-index:1}.custom-theme .el-button-group .el-button--primary:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--primary:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--primary:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--success:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--success:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--success:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--warning:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--warning:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--warning:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--danger:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--danger:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--danger:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--info:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--info:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--info:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-input{position:relative;font-size:14px;display:inline-block;width:100%}.custom-theme .el-input::-webkit-scrollbar{z-index:11;width:6px}.custom-theme .el-input::-webkit-scrollbar:horizontal{height:6px}.custom-theme .el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.custom-theme .el-input::-webkit-scrollbar-corner{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.custom-theme .el-input__inner{-webkit-appearance:none;background-color:#fff;background-image:none;border-radius:4px;border:1px solid #d8dce5;-webkit-box-sizing:border-box;box-sizing:border-box;color:#5a5e66;display:inline-block;font-size:inherit;height:40px;line-height:1;outline:0;padding:0 15px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.custom-theme .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input__inner:hover{border-color:#b4bccc}.custom-theme .el-input__inner:focus{outline:0;border-color:#262729}.custom-theme .el-input__suffix{position:absolute;height:100%;right:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s;pointer-events:none}.custom-theme .el-input__suffix-inner{pointer-events:all}.custom-theme .el-input__prefix{position:absolute;height:100%;left:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s}.custom-theme .el-input__icon{height:100%;width:25px;text-align:center;-webkit-transition:all .3s;transition:all .3s;line-height:40px}.custom-theme .el-input__icon:after{content:'';height:100%;width:0;display:inline-block;vertical-align:middle}.custom-theme .el-input__validateIcon{pointer-events:none}.custom-theme .el-input.is-active .el-input__inner{outline:0;border-color:#262729}.custom-theme .el-input.is-disabled .el-input__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-input.is-disabled .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__icon{cursor:not-allowed}.custom-theme .el-input--suffix .el-input__inner{padding-right:30px}.custom-theme .el-input--prefix .el-input__inner{padding-left:30px}.custom-theme .el-input--medium{font-size:14px}.custom-theme .el-input--medium .el-input__inner{height:36px}.custom-theme .el-input--medium .el-input__icon{line-height:36px}.custom-theme .el-input--small{font-size:13px}.custom-theme .el-input--small .el-input__inner{height:32px}.custom-theme .el-input--small .el-input__icon{line-height:32px}.custom-theme .el-input--mini{font-size:12px}.custom-theme .el-input--mini .el-input__inner{height:28px}.custom-theme .el-input--mini .el-input__icon{line-height:28px}.custom-theme .el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate}.custom-theme .el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.custom-theme .el-input-group__append,.custom-theme .el-input-group__prepend{background-color:#f5f7fa;color:#0a76a4;vertical-align:middle;display:table-cell;position:relative;border:1px solid #d8dce5;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.custom-theme .el-input-group__append:focus,.custom-theme .el-input-group__prepend:focus{outline:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-select,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-select{display:inline-block;margin:-20px}.custom-theme .el-input-group__append button.el-button,.custom-theme .el-input-group__append div.el-select .el-input__inner,.custom-theme .el-input-group__append div.el-select:hover .el-input__inner,.custom-theme .el-input-group__prepend button.el-button,.custom-theme .el-input-group__prepend div.el-select .el-input__inner,.custom-theme .el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-input,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-input{font-size:inherit}.custom-theme .el-input-group__prepend{border-right:0;border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-input-group__append{border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--prepend .el-input__inner{border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--append .el-input__inner{border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-textarea{display:inline-block;width:100%;vertical-align:bottom}.custom-theme .el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;font-size:14px;color:#5a5e66;background-color:#fff;background-image:none;border:1px solid #d8dce5;border-radius:4px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.custom-theme .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:hover{border-color:#b4bccc}.custom-theme .el-textarea__inner:focus{outline:0;border-color:#262729}.custom-theme .el-textarea.is-disabled .el-textarea__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-message-box{display:inline-block;width:420px;padding-bottom:10px;vertical-align:middle;background-color:#fff;border-radius:4px;border:1px solid #e6ebf5;font-size:18px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);text-align:left;overflow:hidden;-webkit-backface-visibility:hidden;backface-visibility:hidden}.custom-theme .el-message-box__wrapper{position:fixed;top:0;bottom:0;left:0;right:0;text-align:center}.custom-theme .el-message-box__wrapper::after{content:"";display:inline-block;height:100%;width:0;vertical-align:middle}.custom-theme .el-message-box__header{position:relative;padding:15px;padding-bottom:10px}.custom-theme .el-message-box__title{padding-left:0;margin-bottom:0;font-size:18px;line-height:1;color:#2d2f33}.custom-theme .el-message-box__headerbtn{position:absolute;top:15px;right:15px;padding:0;border:none;outline:0;background:0 0;font-size:16px;cursor:pointer}.custom-theme .el-message-box__headerbtn .el-message-box__close{color:#0a76a4}.custom-theme .el-message-box__headerbtn:focus .el-message-box__close,.custom-theme .el-message-box__headerbtn:hover .el-message-box__close{color:#262729}.custom-theme .el-message-box__content{position:relative;padding:10px 15px;color:#5a5e66;font-size:14px}.custom-theme .el-message-box__input{padding-top:15px}.custom-theme .el-message-box__input input.invalid{border-color:#b3450e}.custom-theme .el-message-box__input input.invalid:focus{border-color:#b3450e}.custom-theme .el-message-box__status{position:absolute;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);font-size:24px!important}.custom-theme .el-message-box__status::before{padding-left:1px}.custom-theme .el-message-box__status+.el-message-box__message{padding-left:36px;padding-right:12px}.custom-theme .el-message-box__status.el-icon-success{color:#409167}.custom-theme .el-message-box__status.el-icon-info{color:#0a76a4}.custom-theme .el-message-box__status.el-icon-warning{color:#9da408}.custom-theme .el-message-box__status.el-icon-error{color:#b3450e}.custom-theme .el-message-box__message{margin:0}.custom-theme .el-message-box__message p{margin:0;line-height:24px}.custom-theme .el-message-box__errormsg{color:#b3450e;font-size:12px;min-height:18px;margin-top:2px}.custom-theme .el-message-box__btns{padding:5px 15px 0;text-align:right}.custom-theme .el-message-box__btns button:nth-child(2){margin-left:10px}.custom-theme .el-message-box__btns-reverse{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.custom-theme .el-message-box--center{padding-bottom:30px}.custom-theme .el-message-box--center .el-message-box__header{padding-top:30px}.custom-theme .el-message-box--center .el-message-box__title{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.custom-theme .el-message-box--center .el-message-box__status{position:relative;top:auto;padding-right:5px;text-align:center;-webkit-transform:translateY(-1px);transform:translateY(-1px)}.custom-theme .el-message-box--center .el-message-box__message{margin-left:0}.custom-theme .el-message-box--center .el-message-box__btns,.custom-theme .el-message-box--center .el-message-box__content{text-align:center}.custom-theme .el-message-box--center .el-message-box__content{padding-left:27px;padding-right:27px}.custom-theme .msgbox-fade-enter-active{-webkit-animation:msgbox-fade-in .3s;animation:msgbox-fade-in .3s}.custom-theme .msgbox-fade-leave-active{-webkit-animation:msgbox-fade-out .3s;animation:msgbox-fade-out .3s}@-webkit-keyframes msgbox-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@keyframes msgbox-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes msgbox-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}@keyframes msgbox-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}.custom-theme .el-breadcrumb{font-size:14px;line-height:1}.custom-theme .el-breadcrumb::after,.custom-theme .el-breadcrumb::before{display:table;content:""}.custom-theme .el-breadcrumb::after{clear:both}.custom-theme .el-breadcrumb__separator{margin:0 9px;font-weight:700;color:#b4bccc}.custom-theme .el-breadcrumb__separator[class*=icon]{margin:0 6px;font-weight:400}.custom-theme .el-breadcrumb__item{float:left}.custom-theme .el-breadcrumb__inner,.custom-theme .el-breadcrumb__inner a{font-weight:700;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1);color:#2d2f33}.custom-theme .el-breadcrumb__inner a:hover,.custom-theme .el-breadcrumb__inner:hover{color:#262729;cursor:pointer}.custom-theme .el-breadcrumb__item:last-child .el-breadcrumb__inner,.custom-theme .el-breadcrumb__item:last-child .el-breadcrumb__inner a,.custom-theme .el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover,.custom-theme .el-breadcrumb__item:last-child .el-breadcrumb__inner:hover{font-weight:400;color:#5a5e66;cursor:text}.custom-theme .el-breadcrumb__item:last-child .el-breadcrumb__separator{display:none}.custom-theme .el-form--label-left .el-form-item__label{text-align:left}.custom-theme .el-form--label-top .el-form-item__label{float:none;display:inline-block;text-align:left;padding:0 0 10px 0}.custom-theme .el-form--inline .el-form-item{display:inline-block;margin-right:10px;vertical-align:top}.custom-theme .el-form--inline .el-form-item__label{float:none;display:inline-block}.custom-theme .el-form--inline .el-form-item__content{display:inline-block;vertical-align:top}.custom-theme .el-form--inline.el-form--label-top .el-form-item__content{display:block}.custom-theme .el-form-item{margin-bottom:22px}.custom-theme .el-form-item::after,.custom-theme .el-form-item::before{display:table;content:""}.custom-theme .el-form-item::after{clear:both}.custom-theme .el-form-item .el-form-item{margin-bottom:0}.custom-theme .el-form-item .el-input__validateIcon{display:none}.custom-theme .el-form-item--medium .el-form-item__label{line-height:36px}.custom-theme .el-form-item--medium .el-form-item__content{line-height:36px}.custom-theme .el-form-item--small .el-form-item__label{line-height:32px}.custom-theme .el-form-item--small .el-form-item__content{line-height:32px}.custom-theme .el-form-item--small.el-form-item{margin-bottom:18px}.custom-theme .el-form-item--small .el-form-item__error{padding-top:2px}.custom-theme .el-form-item--mini .el-form-item__label{line-height:28px}.custom-theme .el-form-item--mini .el-form-item__content{line-height:28px}.custom-theme .el-form-item--mini.el-form-item{margin-bottom:18px}.custom-theme .el-form-item--mini .el-form-item__error{padding-top:1px}.custom-theme .el-form-item__label{text-align:right;vertical-align:middle;float:left;font-size:14px;color:#5a5e66;line-height:40px;padding:0 12px 0 0;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-form-item__content{line-height:40px;position:relative;font-size:14px}.custom-theme .el-form-item__content::after,.custom-theme .el-form-item__content::before{display:table;content:""}.custom-theme .el-form-item__content::after{clear:both}.custom-theme .el-form-item__error{color:#b3450e;font-size:12px;line-height:1;padding-top:4px;position:absolute;top:100%;left:0}.custom-theme .el-form-item__error--inline{position:relative;top:auto;left:auto;display:inline-block;margin-left:10px}.custom-theme .el-form-item.is-required .el-form-item__label:before{content:'*';color:#b3450e;margin-right:4px}.custom-theme .el-form-item.is-error .el-input__inner,.custom-theme .el-form-item.is-error .el-input__inner:focus,.custom-theme .el-form-item.is-error .el-textarea__inner,.custom-theme .el-form-item.is-error .el-textarea__inner:focus{border-color:#b3450e}.custom-theme .el-form-item.is-error .el-input-group__append .el-input__inner,.custom-theme .el-form-item.is-error .el-input-group__prepend .el-input__inner{border-color:transparent}.custom-theme .el-form-item.is-error .el-input__validateIcon{color:#b3450e}.custom-theme .el-form-item.is-success .el-input__inner,.custom-theme .el-form-item.is-success .el-input__inner:focus,.custom-theme .el-form-item.is-success .el-textarea__inner,.custom-theme .el-form-item.is-success .el-textarea__inner:focus{border-color:#409167}.custom-theme .el-form-item.is-success .el-input-group__append .el-input__inner,.custom-theme .el-form-item.is-success .el-input-group__prepend .el-input__inner{border-color:transparent}.custom-theme .el-form-item.is-success .el-input__validateIcon{color:#409167}.custom-theme .el-form-item--feedback .el-input__validateIcon{display:inline-block}.custom-theme .el-tabs__header{padding:0;position:relative;margin:0 0 15px}.custom-theme .el-tabs__active-bar{position:absolute;bottom:0;left:0;height:2px;background-color:#262729;z-index:1;-webkit-transition:-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:transform .3s cubic-bezier(.645,.045,.355,1);transition:transform .3s cubic-bezier(.645,.045,.355,1),-webkit-transform .3s cubic-bezier(.645,.045,.355,1);list-style:none}.custom-theme .el-tabs__new-tab{float:right;border:1px solid #d3dce6;height:18px;width:18px;line-height:18px;margin:12px 0 9px 10px;border-radius:3px;text-align:center;font-size:12px;color:#d3dce6;cursor:pointer;-webkit-transition:all .15s;transition:all .15s}.custom-theme .el-tabs__new-tab .el-icon-plus{-webkit-transform:scale(.8,.8);transform:scale(.8,.8)}.custom-theme .el-tabs__new-tab:hover{color:#262729}.custom-theme .el-tabs__nav-wrap{overflow:hidden;margin-bottom:-1px;position:relative}.custom-theme .el-tabs__nav-wrap::after{content:"";position:absolute;left:0;bottom:0;width:100%;height:2px;background-color:#dfe4ed;z-index:1}.custom-theme .el-tabs__nav-wrap.is-scrollable{padding:0 20px;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-tabs__nav-scroll{overflow:hidden}.custom-theme .el-tabs__nav-next,.custom-theme .el-tabs__nav-prev{position:absolute;cursor:pointer;line-height:44px;font-size:12px;color:#878d99}.custom-theme .el-tabs__nav-next{right:0}.custom-theme .el-tabs__nav-prev{left:0}.custom-theme .el-tabs__nav{white-space:nowrap;position:relative;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;float:left;z-index:2}.custom-theme .el-tabs__item{padding:0 20px;height:40px;-webkit-box-sizing:border-box;box-sizing:border-box;line-height:40px;display:inline-block;list-style:none;font-size:14px;font-weight:500;color:#2d2f33;position:relative}.custom-theme .el-tabs__item:focus,.custom-theme .el-tabs__item:focus:active{outline:0}.custom-theme .el-tabs__item .el-icon-close{border-radius:50%;text-align:center;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);margin-left:5px}.custom-theme .el-tabs__item .el-icon-close:before{-webkit-transform:scale(.9);transform:scale(.9);display:inline-block}.custom-theme .el-tabs__item .el-icon-close:hover{background-color:#b4bccc;color:#fff}.custom-theme .el-tabs__item.is-active{color:#262729}.custom-theme .el-tabs__item:hover{color:#262729;cursor:pointer}.custom-theme .el-tabs__item.is-disabled{color:#b4bccc;cursor:default}.custom-theme .el-tabs__content{overflow:hidden;position:relative}.custom-theme .el-tabs--card>.el-tabs__header{border-bottom:1px solid #dfe4ed}.custom-theme .el-tabs--card>.el-tabs__header .el-tabs__nav-wrap::after{content:none}.custom-theme .el-tabs--card>.el-tabs__header .el-tabs__nav{border:1px solid #dfe4ed;border-bottom:none;border-radius:4px 4px 0 0}.custom-theme .el-tabs--card>.el-tabs__header .el-tabs__active-bar{display:none}.custom-theme .el-tabs--card>.el-tabs__header .el-tabs__item .el-icon-close{position:relative;font-size:12px;width:0;height:14px;vertical-align:middle;line-height:15px;overflow:hidden;top:-1px;right:-2px;-webkit-transform-origin:100% 50%;transform-origin:100% 50%}.custom-theme .el-tabs--card>.el-tabs__header .el-tabs__item{border-bottom:1px solid transparent;border-left:1px solid #dfe4ed;-webkit-transition:color .3s cubic-bezier(.645,.045,.355,1),padding .3s cubic-bezier(.645,.045,.355,1);transition:color .3s cubic-bezier(.645,.045,.355,1),padding .3s cubic-bezier(.645,.045,.355,1)}.custom-theme .el-tabs--card>.el-tabs__header .el-tabs__item:first-child{border-left:none}.custom-theme .el-tabs--card>.el-tabs__header .el-tabs__item.is-closable:hover{padding-left:13px;padding-right:13px}.custom-theme .el-tabs--card>.el-tabs__header .el-tabs__item.is-closable:hover .el-icon-close{width:14px}.custom-theme .el-tabs--card>.el-tabs__header .el-tabs__item.is-active{border-bottom-color:#fff}.custom-theme .el-tabs--card>.el-tabs__header .el-tabs__item.is-active.is-closable{padding-left:20px;padding-right:20px}.custom-theme .el-tabs--card>.el-tabs__header .el-tabs__item.is-active.is-closable .el-icon-close{width:14px}.custom-theme .el-tabs--border-card{background:#fff;border:1px solid #d8dce5;-webkit-box-shadow:0 2px 4px 0 rgba(0,0,0,.12),0 0 6px 0 rgba(0,0,0,.04);box-shadow:0 2px 4px 0 rgba(0,0,0,.12),0 0 6px 0 rgba(0,0,0,.04)}.custom-theme .el-tabs--border-card>.el-tabs__content{padding:15px}.custom-theme .el-tabs--border-card>.el-tabs__header{background-color:#f5f7fa;border-bottom:1px solid #dfe4ed;margin:0}.custom-theme .el-tabs--border-card>.el-tabs__header .el-tabs__nav-wrap::after{content:none}.custom-theme .el-tabs--border-card>.el-tabs__header .el-tabs__item{-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);border:1px solid transparent;margin:-1px -1px 0;color:#878d99}.custom-theme .el-tabs--border-card>.el-tabs__header .el-tabs__item.is-active{color:#262729;background-color:#fff;border-right-color:#d8dce5;border-left-color:#d8dce5}.custom-theme .el-tabs--border-card>.el-tabs__header .el-tabs__item:hover{color:#262729}.custom-theme .el-tabs--bottom:not(.el-tabs--border-card):not(.el-tabs--card) .el-tabs__item:nth-child(2),.custom-theme .el-tabs--top:not(.el-tabs--border-card):not(.el-tabs--card) .el-tabs__item:nth-child(2){padding-left:0}.custom-theme .el-tabs--bottom .el-tabs__header{margin-bottom:0;margin-top:10px}.custom-theme .el-tabs--bottom.el-tabs--border-card .el-tabs__header{border-bottom:0;border-top:1px solid #d8dce5}.custom-theme .el-tabs--bottom.el-tabs--border-card .el-tabs__nav-wrap{margin-top:-1px;margin-bottom:0}.custom-theme .el-tabs--bottom.el-tabs--border-card .el-tabs__item{border:1px solid transparent;margin:0 -1px -1px -1px}.custom-theme .el-tabs--left,.custom-theme .el-tabs--right{overflow:hidden}.custom-theme .el-tabs--left .el-tabs__header,.custom-theme .el-tabs--left .el-tabs__nav-scroll,.custom-theme .el-tabs--left .el-tabs__nav-wrap,.custom-theme .el-tabs--right .el-tabs__header,.custom-theme .el-tabs--right .el-tabs__nav-scroll,.custom-theme .el-tabs--right .el-tabs__nav-wrap{height:100%}.custom-theme .el-tabs--left .el-tabs__active-bar,.custom-theme .el-tabs--right .el-tabs__active-bar{top:0;bottom:auto;width:2px;height:auto}.custom-theme .el-tabs--left .el-tabs__nav-wrap,.custom-theme .el-tabs--right .el-tabs__nav-wrap{margin-bottom:0}.custom-theme .el-tabs--left .el-tabs__nav-wrap.is-scrollable,.custom-theme .el-tabs--right .el-tabs__nav-wrap.is-scrollable{padding:30px 0}.custom-theme .el-tabs--left .el-tabs__nav-wrap::after,.custom-theme .el-tabs--right .el-tabs__nav-wrap::after{height:100%;width:2px;bottom:auto;top:0}.custom-theme .el-tabs--left .el-tabs__nav,.custom-theme .el-tabs--right .el-tabs__nav{float:none}.custom-theme .el-tabs--left .el-tabs__item,.custom-theme .el-tabs--right .el-tabs__item{display:block}.custom-theme .el-tabs--left .el-tabs__nav-next,.custom-theme .el-tabs--left .el-tabs__nav-prev,.custom-theme .el-tabs--right .el-tabs__nav-next,.custom-theme .el-tabs--right .el-tabs__nav-prev{height:30px;line-height:30px;width:100%;text-align:center;cursor:pointer}.custom-theme .el-tabs--left .el-tabs__nav-next i,.custom-theme .el-tabs--left .el-tabs__nav-prev i,.custom-theme .el-tabs--right .el-tabs__nav-next i,.custom-theme .el-tabs--right .el-tabs__nav-prev i{-webkit-transform:rotateZ(90deg);transform:rotateZ(90deg)}.custom-theme .el-tabs--left .el-tabs__nav-prev,.custom-theme .el-tabs--right .el-tabs__nav-prev{left:auto;top:0}.custom-theme .el-tabs--left .el-tabs__nav-next,.custom-theme .el-tabs--right .el-tabs__nav-next{right:auto;bottom:0}.custom-theme .el-tabs--left .el-tabs__header{float:left;margin-bottom:0;margin-right:10px}.custom-theme .el-tabs--left .el-tabs__nav-wrap{margin-right:-1px}.custom-theme .el-tabs--left .el-tabs__nav-wrap::after{left:auto;right:0}.custom-theme .el-tabs--left .el-tabs__active-bar{right:0;left:auto}.custom-theme .el-tabs--left .el-tabs__item{text-align:right}.custom-theme .el-tabs--left.el-tabs--card .el-tabs__active-bar{display:none}.custom-theme .el-tabs--left.el-tabs--card .el-tabs__item{border-left:none;border-right:1px solid #dfe4ed;border-bottom:none;border-top:1px solid #dfe4ed}.custom-theme .el-tabs--left.el-tabs--card .el-tabs__item:first-child{border-right:1px solid #dfe4ed;border-top:none}.custom-theme .el-tabs--left.el-tabs--card .el-tabs__item.is-active{border:1px solid #dfe4ed;border-right-color:#fff;border-left:none;border-bottom:none}.custom-theme .el-tabs--left.el-tabs--card .el-tabs__item.is-active:first-child{border-top:none}.custom-theme .el-tabs--left.el-tabs--card .el-tabs__item.is-active:last-child{border-bottom:none}.custom-theme .el-tabs--left.el-tabs--card .el-tabs__nav{border-radius:4px 0 0 4px;border-bottom:1px solid #dfe4ed;border-right:none}.custom-theme .el-tabs--left.el-tabs--card .el-tabs__new-tab{float:none}.custom-theme .el-tabs--left.el-tabs--border-card .el-tabs__header{border-right:1px solid #dfe4ed}.custom-theme .el-tabs--left.el-tabs--border-card .el-tabs__item{border:1px solid transparent;margin:-1px 0 -1px -1px}.custom-theme .el-tabs--left.el-tabs--border-card .el-tabs__item.is-active{border-color:transparent;border-top-color:#d1dbe5;border-bottom-color:#d1dbe5}.custom-theme .el-tabs--right .el-tabs__header{float:right;margin-bottom:0;margin-left:10px}.custom-theme .el-tabs--right .el-tabs__nav-wrap{margin-left:-1px}.custom-theme .el-tabs--right .el-tabs__nav-wrap::after{left:0;right:auto}.custom-theme .el-tabs--right .el-tabs__active-bar{left:0}.custom-theme .el-tabs--right.el-tabs--card .el-tabs__active-bar{display:none}.custom-theme .el-tabs--right.el-tabs--card .el-tabs__item{border-bottom:none;border-top:1px solid #dfe4ed}.custom-theme .el-tabs--right.el-tabs--card .el-tabs__item:first-child{border-left:1px solid #dfe4ed;border-top:none}.custom-theme .el-tabs--right.el-tabs--card .el-tabs__item.is-active{border:1px solid #dfe4ed;border-left-color:#fff;border-right:none;border-bottom:none}.custom-theme .el-tabs--right.el-tabs--card .el-tabs__item.is-active:first-child{border-top:none}.custom-theme .el-tabs--right.el-tabs--card .el-tabs__item.is-active:last-child{border-bottom:none}.custom-theme .el-tabs--right.el-tabs--card .el-tabs__nav{border-radius:0 4px 4px 0;border-bottom:1px solid #dfe4ed;border-left:none}.custom-theme .el-tabs--right.el-tabs--border-card .el-tabs__header{border-left:1px solid #dfe4ed}.custom-theme .el-tabs--right.el-tabs--border-card .el-tabs__item{border:1px solid transparent;margin:-1px -1px -1px 0}.custom-theme .el-tabs--right.el-tabs--border-card .el-tabs__item.is-active{border-color:transparent;border-top-color:#d1dbe5;border-bottom-color:#d1dbe5}.custom-theme .slideInLeft-transition,.custom-theme .slideInRight-transition{display:inline-block}.custom-theme .slideInRight-enter{-webkit-animation:slideInRight-enter .3s;animation:slideInRight-enter .3s}.custom-theme .slideInRight-leave{position:absolute;left:0;right:0;-webkit-animation:slideInRight-leave .3s;animation:slideInRight-leave .3s}.custom-theme .slideInLeft-enter{-webkit-animation:slideInLeft-enter .3s;animation:slideInLeft-enter .3s}.custom-theme .slideInLeft-leave{position:absolute;left:0;right:0;-webkit-animation:slideInLeft-leave .3s;animation:slideInLeft-leave .3s}@-webkit-keyframes slideInRight-enter{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%)}to{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes slideInRight-enter{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%)}to{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@-webkit-keyframes slideInRight-leave{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%);opacity:0}}@keyframes slideInRight-leave{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%);opacity:0}}@-webkit-keyframes slideInLeft-enter{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%)}to{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes slideInLeft-enter{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%)}to{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@-webkit-keyframes slideInLeft-leave{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%);opacity:0}}@keyframes slideInLeft-leave{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%);opacity:0}}.custom-theme .el-tag{background-color:rgba(38,39,41,.1);display:inline-block;padding:0 10px;height:32px;line-height:30px;font-size:12px;color:#262729;border-radius:4px;-webkit-box-sizing:border-box;box-sizing:border-box;border:1px solid rgba(38,39,41,.2);white-space:nowrap}.custom-theme .el-tag .el-icon-close{border-radius:50%;text-align:center;position:relative;cursor:pointer;font-size:12px;height:18px;width:18px;line-height:18px;vertical-align:middle;top:-1px;right:-5px;color:#262729}.custom-theme .el-tag .el-icon-close::before{display:block}.custom-theme .el-tag .el-icon-close:hover{background-color:#262729;color:#fff}.custom-theme .el-tag--info{background-color:rgba(10,118,164,.1);border-color:rgba(10,118,164,.2);color:#0a76a4}.custom-theme .el-tag--info.is-hit{border-color:#0a76a4}.custom-theme .el-tag--info .el-tag__close{color:#0a76a4}.custom-theme .el-tag--info .el-tag__close:hover{background-color:#0a76a4;color:#fff}.custom-theme .el-tag--success{background-color:rgba(64,145,103,.1);border-color:rgba(64,145,103,.2);color:#409167}.custom-theme .el-tag--success.is-hit{border-color:#409167}.custom-theme .el-tag--success .el-tag__close{color:#409167}.custom-theme .el-tag--success .el-tag__close:hover{background-color:#409167;color:#fff}.custom-theme .el-tag--warning{background-color:rgba(157,164,8,.1);border-color:rgba(157,164,8,.2);color:#9da408}.custom-theme .el-tag--warning.is-hit{border-color:#9da408}.custom-theme .el-tag--warning .el-tag__close{color:#9da408}.custom-theme .el-tag--warning .el-tag__close:hover{background-color:#9da408;color:#fff}.custom-theme .el-tag--danger{background-color:rgba(179,69,14,.1);border-color:rgba(179,69,14,.2);color:#b3450e}.custom-theme .el-tag--danger.is-hit{border-color:#b3450e}.custom-theme .el-tag--danger .el-tag__close{color:#b3450e}.custom-theme .el-tag--danger .el-tag__close:hover{background-color:#b3450e;color:#fff}.custom-theme .el-tag--medium{height:28px;line-height:26px}.custom-theme .el-tag--medium .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.custom-theme .el-tag--small{height:24px;padding:0 8px;line-height:22px}.custom-theme .el-tag--small .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.custom-theme .el-tag--mini{height:20px;padding:0 5px;line-height:19px}.custom-theme .el-tag--mini .el-icon-close{margin-left:-3px;-webkit-transform:scale(.7);transform:scale(.7)}.custom-theme .el-tree{cursor:default;background:#fff;color:#5a5e66}.custom-theme .el-tree__empty-block{position:relative;min-height:60px;text-align:center;width:100%;height:100%}.custom-theme .el-tree__empty-text{position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);color:#623615}.custom-theme .el-tree-node{white-space:nowrap}.custom-theme .el-tree-node__content{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:26px;cursor:pointer}.custom-theme .el-tree-node__content>.el-tree-node__expand-icon{padding:6px}.custom-theme .el-tree-node__content>.el-checkbox{margin-right:8px}.custom-theme .el-tree-node__content:hover{background-color:#f5f7fa}.custom-theme .el-tree-node__expand-icon{cursor:pointer;color:#b4bccc;font-size:12px;-webkit-transform:rotate(0);transform:rotate(0);-webkit-transition:-webkit-transform .3s ease-in-out;transition:-webkit-transform .3s ease-in-out;transition:transform .3s ease-in-out;transition:transform .3s ease-in-out,-webkit-transform .3s ease-in-out}.custom-theme .el-tree-node__expand-icon.expanded{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.custom-theme .el-tree-node__expand-icon.is-leaf{color:transparent;cursor:default}.custom-theme .el-tree-node__label{font-size:14px}.custom-theme .el-tree-node__loading-icon{margin-right:8px;font-size:14px;color:#b4bccc}.custom-theme .el-tree-node>.el-tree-node__children{overflow:hidden;background-color:transparent}.custom-theme .el-tree-node.is-expanded>.el-tree-node__children{display:block}.custom-theme .el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content{background-color:#eee}.custom-theme .el-alert{width:100%;padding:8px 16px;margin:0;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;position:relative;background-color:#fff;overflow:hidden;opacity:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-transition:opacity .2s;transition:opacity .2s}.custom-theme .el-alert.is-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.custom-theme .el-alert--success{background-color:#ecf4f0;color:#409167}.custom-theme .el-alert--success .el-alert__description{color:#409167}.custom-theme .el-alert--info{background-color:#e7f1f6;color:#0a76a4}.custom-theme .el-alert--info .el-alert__description{color:#0a76a4}.custom-theme .el-alert--warning{background-color:#f5f6e6;color:#9da408}.custom-theme .el-alert--warning .el-alert__description{color:#9da408}.custom-theme .el-alert--error{background-color:#f7ece7;color:#b3450e}.custom-theme .el-alert--error .el-alert__description{color:#b3450e}.custom-theme .el-alert__content{display:table-cell;padding:0 8px}.custom-theme .el-alert__icon{font-size:16px;width:16px}.custom-theme .el-alert__icon.is-big{font-size:28px;width:28px}.custom-theme .el-alert__title{font-size:13px;line-height:18px}.custom-theme .el-alert__title.is-bold{font-weight:700}.custom-theme .el-alert .el-alert__description{font-size:12px;margin:5px 0 0 0}.custom-theme .el-alert__closebtn{font-size:12px;color:#b4bccc;opacity:1;position:absolute;top:12px;right:15px;cursor:pointer}.custom-theme .el-alert__closebtn.is-customed{font-style:normal;font-size:13px;top:9px}.custom-theme .el-alert-fade-enter,.custom-theme .el-alert-fade-leave-active{opacity:0}.custom-theme .el-notification{display:-webkit-box;display:-ms-flexbox;display:flex;width:330px;padding:14px 26px 14px 13px;border-radius:8px;-webkit-box-sizing:border-box;box-sizing:border-box;border:1px solid #e6ebf5;position:fixed;background-color:#fff;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);-webkit-transition:opacity .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;transition:opacity .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;transition:opacity .3s,transform .3s,left .3s,right .3s,top .4s,bottom .3s;transition:opacity .3s,transform .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;overflow:hidden}.custom-theme .el-notification.right{right:16px}.custom-theme .el-notification.left{left:16px}.custom-theme .el-notification__group{margin-left:13px}.custom-theme .el-notification__title{font-weight:700;font-size:16px;color:#2d2f33;margin:0}.custom-theme .el-notification__content{font-size:14px;line-height:21px;margin:6px 0 0 0;color:#5a5e66;text-align:justify}.custom-theme .el-notification__content p{margin:0}.custom-theme .el-notification__icon{height:24px;width:24px;font-size:24px;-webkit-transform:translateY(4px);transform:translateY(4px)}.custom-theme .el-notification__closeBtn{position:absolute;top:15px;right:15px;cursor:pointer;color:#878d99;font-size:16px}.custom-theme .el-notification__closeBtn:hover{color:#5a5e66}.custom-theme .el-notification .el-icon-success{color:#409167}.custom-theme .el-notification .el-icon-error{color:#b3450e}.custom-theme .el-notification .el-icon-info{color:#0a76a4}.custom-theme .el-notification .el-icon-warning{color:#9da408}.custom-theme .el-notification-fade-enter.right{right:0;-webkit-transform:translateX(100%);transform:translateX(100%)}.custom-theme .el-notification-fade-enter.left{left:0;-webkit-transform:translateX(-100%);transform:translateX(-100%)}.custom-theme .el-notification-fade-leave-active{opacity:0}.custom-theme .el-input{position:relative;font-size:14px;display:inline-block;width:100%}.custom-theme .el-input::-webkit-scrollbar{z-index:11;width:6px}.custom-theme .el-input::-webkit-scrollbar:horizontal{height:6px}.custom-theme .el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.custom-theme .el-input::-webkit-scrollbar-corner{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.custom-theme .el-input__inner{-webkit-appearance:none;background-color:#fff;background-image:none;border-radius:4px;border:1px solid #d8dce5;-webkit-box-sizing:border-box;box-sizing:border-box;color:#5a5e66;display:inline-block;font-size:inherit;height:40px;line-height:1;outline:0;padding:0 15px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.custom-theme .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input__inner:hover{border-color:#b4bccc}.custom-theme .el-input__inner:focus{outline:0;border-color:#262729}.custom-theme .el-input__suffix{position:absolute;height:100%;right:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s;pointer-events:none}.custom-theme .el-input__suffix-inner{pointer-events:all}.custom-theme .el-input__prefix{position:absolute;height:100%;left:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s}.custom-theme .el-input__icon{height:100%;width:25px;text-align:center;-webkit-transition:all .3s;transition:all .3s;line-height:40px}.custom-theme .el-input__icon:after{content:'';height:100%;width:0;display:inline-block;vertical-align:middle}.custom-theme .el-input__validateIcon{pointer-events:none}.custom-theme .el-input.is-active .el-input__inner{outline:0;border-color:#262729}.custom-theme .el-input.is-disabled .el-input__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-input.is-disabled .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__icon{cursor:not-allowed}.custom-theme .el-input--suffix .el-input__inner{padding-right:30px}.custom-theme .el-input--prefix .el-input__inner{padding-left:30px}.custom-theme .el-input--medium{font-size:14px}.custom-theme .el-input--medium .el-input__inner{height:36px}.custom-theme .el-input--medium .el-input__icon{line-height:36px}.custom-theme .el-input--small{font-size:13px}.custom-theme .el-input--small .el-input__inner{height:32px}.custom-theme .el-input--small .el-input__icon{line-height:32px}.custom-theme .el-input--mini{font-size:12px}.custom-theme .el-input--mini .el-input__inner{height:28px}.custom-theme .el-input--mini .el-input__icon{line-height:28px}.custom-theme .el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate}.custom-theme .el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.custom-theme .el-input-group__append,.custom-theme .el-input-group__prepend{background-color:#f5f7fa;color:#0a76a4;vertical-align:middle;display:table-cell;position:relative;border:1px solid #d8dce5;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.custom-theme .el-input-group__append:focus,.custom-theme .el-input-group__prepend:focus{outline:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-select,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-select{display:inline-block;margin:-20px}.custom-theme .el-input-group__append button.el-button,.custom-theme .el-input-group__append div.el-select .el-input__inner,.custom-theme .el-input-group__append div.el-select:hover .el-input__inner,.custom-theme .el-input-group__prepend button.el-button,.custom-theme .el-input-group__prepend div.el-select .el-input__inner,.custom-theme .el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-input,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-input{font-size:inherit}.custom-theme .el-input-group__prepend{border-right:0;border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-input-group__append{border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--prepend .el-input__inner{border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--append .el-input__inner{border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-textarea{display:inline-block;width:100%;vertical-align:bottom}.custom-theme .el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;font-size:14px;color:#5a5e66;background-color:#fff;background-image:none;border:1px solid #d8dce5;border-radius:4px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.custom-theme .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:hover{border-color:#b4bccc}.custom-theme .el-textarea__inner:focus{outline:0;border-color:#262729}.custom-theme .el-textarea.is-disabled .el-textarea__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-input-number{position:relative;display:inline-block;width:180px;line-height:38px}.custom-theme .el-input-number .el-input{display:block}.custom-theme .el-input-number .el-input__inner{-webkit-appearance:none;padding-left:50px;padding-right:50px;text-align:center}.custom-theme .el-input-number__decrease,.custom-theme .el-input-number__increase{position:absolute;z-index:1;top:1px;width:40px;height:auto;text-align:center;background:#f5f7fa;color:#5a5e66;cursor:pointer;font-size:13px}.custom-theme .el-input-number__decrease:hover,.custom-theme .el-input-number__increase:hover{color:#262729}.custom-theme .el-input-number__decrease:hover:not(.is-disabled)~.el-input .el-input__inner:not(.is-disabled),.custom-theme .el-input-number__increase:hover:not(.is-disabled)~.el-input .el-input__inner:not(.is-disabled){border-color:#262729}.custom-theme .el-input-number__decrease.is-disabled,.custom-theme .el-input-number__increase.is-disabled{color:#b4bccc;cursor:not-allowed}.custom-theme .el-input-number__increase{right:1px;border-radius:0 4px 4px 0;border-left:1px solid #d8dce5}.custom-theme .el-input-number__decrease{left:1px;border-radius:4px 0 0 4px;border-right:1px solid #d8dce5}.custom-theme .el-input-number.is-disabled .el-input-number__decrease,.custom-theme .el-input-number.is-disabled .el-input-number__increase{border-color:#dfe4ed;color:#dfe4ed}.custom-theme .el-input-number.is-disabled .el-input-number__decrease:hover,.custom-theme .el-input-number.is-disabled .el-input-number__increase:hover{color:#dfe4ed;cursor:not-allowed}.custom-theme .el-input-number--medium{width:200px;line-height:34px}.custom-theme .el-input-number--medium .el-input-number__decrease,.custom-theme .el-input-number--medium .el-input-number__increase{width:36px;font-size:14px}.custom-theme .el-input-number--medium .el-input__inner{padding-left:43px;padding-right:43px}.custom-theme .el-input-number--small{width:130px;line-height:30px}.custom-theme .el-input-number--small .el-input-number__decrease,.custom-theme .el-input-number--small .el-input-number__increase{width:32px;font-size:13px}.custom-theme .el-input-number--small .el-input-number__decrease [class*=el-icon],.custom-theme .el-input-number--small .el-input-number__increase [class*=el-icon]{-webkit-transform:scale(.9);transform:scale(.9)}.custom-theme .el-input-number--small .el-input__inner{padding-left:39px;padding-right:39px}.custom-theme .el-input-number--mini{width:130px;line-height:26px}.custom-theme .el-input-number--mini .el-input-number__decrease,.custom-theme .el-input-number--mini .el-input-number__increase{width:28px;font-size:12px}.custom-theme .el-input-number--mini .el-input-number__decrease [class*=el-icon],.custom-theme .el-input-number--mini .el-input-number__increase [class*=el-icon]{-webkit-transform:scale(.8);transform:scale(.8)}.custom-theme .el-input-number--mini .el-input__inner{padding-left:35px;padding-right:35px}.custom-theme .el-input-number.is-without-controls .el-input__inner{padding-left:15px;padding-right:15px}.custom-theme .el-input-number.is-controls-right .el-input__inner{padding-left:15px;padding-right:50px}.custom-theme .el-input-number.is-controls-right .el-input-number__decrease,.custom-theme .el-input-number.is-controls-right .el-input-number__increase{height:auto;line-height:19px}.custom-theme .el-input-number.is-controls-right .el-input-number__decrease [class*=el-icon],.custom-theme .el-input-number.is-controls-right .el-input-number__increase [class*=el-icon]{-webkit-transform:scale(.8);transform:scale(.8)}.custom-theme .el-input-number.is-controls-right .el-input-number__increase{border-radius:0 4px 0 0;border-bottom:1px solid #d8dce5}.custom-theme .el-input-number.is-controls-right .el-input-number__decrease{right:1px;bottom:1px;top:auto;left:auto;border-right:none;border-left:1px solid #d8dce5;border-radius:0 0 4px 0}.custom-theme .el-input-number.is-controls-right[class*=medium] [class*=decrease],.custom-theme .el-input-number.is-controls-right[class*=medium] [class*=increase]{line-height:17px}.custom-theme .el-input-number.is-controls-right[class*=small] [class*=decrease],.custom-theme .el-input-number.is-controls-right[class*=small] [class*=increase]{line-height:15px}.custom-theme .el-input-number.is-controls-right[class*=mini] [class*=decrease],.custom-theme .el-input-number.is-controls-right[class*=mini] [class*=increase]{line-height:13px}.custom-theme .el-tooltip__popper{position:absolute;border-radius:4px;padding:10px;z-index:2000;font-size:12px;line-height:1.2}.custom-theme .el-tooltip__popper .popper__arrow,.custom-theme .el-tooltip__popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.custom-theme .el-tooltip__popper .popper__arrow{border-width:6px}.custom-theme .el-tooltip__popper .popper__arrow::after{content:" ";border-width:5px}.custom-theme .el-tooltip__popper[x-placement^=top]{margin-bottom:12px}.custom-theme .el-tooltip__popper[x-placement^=top] .popper__arrow{bottom:-6px;border-top-color:#2d2f33;border-bottom-width:0}.custom-theme .el-tooltip__popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-5px;border-top-color:#2d2f33;border-bottom-width:0}.custom-theme .el-tooltip__popper[x-placement^=bottom]{margin-top:12px}.custom-theme .el-tooltip__popper[x-placement^=bottom] .popper__arrow{top:-6px;border-top-width:0;border-bottom-color:#2d2f33}.custom-theme .el-tooltip__popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-5px;border-top-width:0;border-bottom-color:#2d2f33}.custom-theme .el-tooltip__popper[x-placement^=right]{margin-left:12px}.custom-theme .el-tooltip__popper[x-placement^=right] .popper__arrow{left:-6px;border-right-color:#2d2f33;border-left-width:0}.custom-theme .el-tooltip__popper[x-placement^=right] .popper__arrow::after{bottom:-5px;left:1px;border-right-color:#2d2f33;border-left-width:0}.custom-theme .el-tooltip__popper[x-placement^=left]{margin-right:12px}.custom-theme .el-tooltip__popper[x-placement^=left] .popper__arrow{right:-6px;border-right-width:0;border-left-color:#2d2f33}.custom-theme .el-tooltip__popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-5px;margin-left:-5px;border-right-width:0;border-left-color:#2d2f33}.custom-theme .el-tooltip__popper.is-dark{background:#2d2f33;color:#fff}.custom-theme .el-tooltip__popper.is-light{background:#fff;border:1px solid #2d2f33}.custom-theme .el-tooltip__popper.is-light[x-placement^=top] .popper__arrow{border-top-color:#2d2f33}.custom-theme .el-tooltip__popper.is-light[x-placement^=top] .popper__arrow::after{border-top-color:#fff}.custom-theme .el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow{border-bottom-color:#2d2f33}.custom-theme .el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow::after{border-bottom-color:#fff}.custom-theme .el-tooltip__popper.is-light[x-placement^=left] .popper__arrow{border-left-color:#2d2f33}.custom-theme .el-tooltip__popper.is-light[x-placement^=left] .popper__arrow::after{border-left-color:#fff}.custom-theme .el-tooltip__popper.is-light[x-placement^=right] .popper__arrow{border-right-color:#2d2f33}.custom-theme .el-tooltip__popper.is-light[x-placement^=right] .popper__arrow::after{border-right-color:#fff}.custom-theme .el-slider::after,.custom-theme .el-slider::before{display:table;content:""}.custom-theme .el-slider::after{clear:both}.custom-theme .el-slider__runway{width:100%;height:6px;margin:16px 0;background-color:#dfe4ed;border-radius:3px;position:relative;cursor:pointer;vertical-align:middle}.custom-theme .el-slider__runway.show-input{margin-right:160px;width:auto}.custom-theme .el-slider__runway.disabled{cursor:default}.custom-theme .el-slider__runway.disabled .el-slider__bar{background-color:#b4bccc}.custom-theme .el-slider__runway.disabled .el-slider__button{border-color:#b4bccc}.custom-theme .el-slider__runway.disabled .el-slider__button-wrapper.hover,.custom-theme .el-slider__runway.disabled .el-slider__button-wrapper:hover{cursor:not-allowed}.custom-theme .el-slider__runway.disabled .el-slider__button-wrapper.dragging{cursor:not-allowed}.custom-theme .el-slider__runway.disabled .el-slider__button.dragging,.custom-theme .el-slider__runway.disabled .el-slider__button.hover,.custom-theme .el-slider__runway.disabled .el-slider__button:hover{-webkit-transform:scale(1);transform:scale(1)}.custom-theme .el-slider__runway.disabled .el-slider__button.hover,.custom-theme .el-slider__runway.disabled .el-slider__button:hover{cursor:not-allowed}.custom-theme .el-slider__runway.disabled .el-slider__button.dragging{cursor:not-allowed}.custom-theme .el-slider__input{float:right;margin-top:3px}.custom-theme .el-slider__bar{height:6px;background-color:#262729;border-top-left-radius:3px;border-bottom-left-radius:3px;position:absolute}.custom-theme .el-slider__button-wrapper{height:36px;width:36px;position:absolute;z-index:1001;top:-15px;-webkit-transform:translateX(-50%);transform:translateX(-50%);background-color:transparent;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.custom-theme .el-slider__button-wrapper::after{display:inline-block;content:"";height:100%;vertical-align:middle}.custom-theme .el-slider__button-wrapper .el-tooltip{vertical-align:middle;display:inline-block}.custom-theme .el-slider__button-wrapper.hover,.custom-theme .el-slider__button-wrapper:hover{cursor:-webkit-grab;cursor:grab}.custom-theme .el-slider__button-wrapper.dragging{cursor:-webkit-grabbing;cursor:grabbing}.custom-theme .el-slider__button{width:16px;height:16px;border:solid 2px #262729;background-color:#fff;border-radius:50%;-webkit-transition:.2s;transition:.2s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.custom-theme .el-slider__button.dragging,.custom-theme .el-slider__button.hover,.custom-theme .el-slider__button:hover{-webkit-transform:scale(1.2);transform:scale(1.2)}.custom-theme .el-slider__button.hover,.custom-theme .el-slider__button:hover{cursor:-webkit-grab;cursor:grab}.custom-theme .el-slider__button.dragging{cursor:-webkit-grabbing;cursor:grabbing}.custom-theme .el-slider__stop{position:absolute;height:6px;width:6px;border-radius:100%;background-color:#fff;-webkit-transform:translateX(-50%);transform:translateX(-50%)}.custom-theme .el-slider.is-vertical{position:relative}.custom-theme .el-slider.is-vertical .el-slider__runway{width:4px;height:100%;margin:0 16px}.custom-theme .el-slider.is-vertical .el-slider__bar{width:4px;height:auto;border-radius:0 0 3px 3px}.custom-theme .el-slider.is-vertical .el-slider__button-wrapper{top:auto;left:-15px;-webkit-transform:translateY(50%);transform:translateY(50%)}.custom-theme .el-slider.is-vertical .el-slider__stop{-webkit-transform:translateY(50%);transform:translateY(50%)}.custom-theme .el-slider.is-vertical.el-slider--with-input{padding-bottom:58px}.custom-theme .el-slider.is-vertical.el-slider--with-input .el-slider__input{overflow:visible;float:none;position:absolute;bottom:22px;width:36px;margin-top:15px}.custom-theme .el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input__inner{text-align:center;padding-left:5px;padding-right:5px}.custom-theme .el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__decrease,.custom-theme .el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__increase{top:32px;margin-top:-1px;border:1px solid #d8dce5;line-height:20px;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.custom-theme .el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__decrease{width:18px;right:18px;border-bottom-left-radius:4px}.custom-theme .el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__increase{width:19px;border-bottom-right-radius:4px}.custom-theme .el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__increase~.el-input .el-input__inner{border-bottom-left-radius:0;border-bottom-right-radius:0}.custom-theme .el-slider.is-vertical.el-slider--with-input .el-slider__input:hover .el-input-number__decrease,.custom-theme .el-slider.is-vertical.el-slider--with-input .el-slider__input:hover .el-input-number__increase{border-color:#b4bccc}.custom-theme .el-slider.is-vertical.el-slider--with-input .el-slider__input:active .el-input-number__decrease,.custom-theme .el-slider.is-vertical.el-slider--with-input .el-slider__input:active .el-input-number__increase{border-color:#262729}.custom-theme .el-loading-parent--relative{position:relative!important}.custom-theme .el-loading-parent--hidden{overflow:hidden!important}.custom-theme .el-loading-mask{position:absolute;z-index:10000;background-color:rgba(255,255,255,.9);margin:0;top:0;right:0;bottom:0;left:0;-webkit-transition:opacity .3s;transition:opacity .3s}.custom-theme .el-loading-mask.is-fullscreen{position:fixed}.custom-theme .el-loading-mask.is-fullscreen .el-loading-spinner{margin-top:-25px}.custom-theme .el-loading-mask.is-fullscreen .el-loading-spinner .circular{height:50px;width:50px}.custom-theme .el-loading-spinner{top:50%;margin-top:-21px;width:100%;text-align:center;position:absolute}.custom-theme .el-loading-spinner .el-loading-text{color:#262729;margin:3px 0;font-size:14px}.custom-theme .el-loading-spinner .circular{height:42px;width:42px;-webkit-animation:loading-rotate 2s linear infinite;animation:loading-rotate 2s linear infinite}.custom-theme .el-loading-spinner .path{-webkit-animation:loading-dash 1.5s ease-in-out infinite;animation:loading-dash 1.5s ease-in-out infinite;stroke-dasharray:90,150;stroke-dashoffset:0;stroke-width:2;stroke:#262729;stroke-linecap:round}.custom-theme .el-loading-spinner i{color:#262729}.custom-theme .el-loading-fade-enter,.custom-theme .el-loading-fade-leave-active{opacity:0}@-webkit-keyframes loading-rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes loading-rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}100%{stroke-dasharray:90,150;stroke-dashoffset:-120px}}@keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}100%{stroke-dasharray:90,150;stroke-dashoffset:-120px}}.custom-theme .el-row{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-row::after,.custom-theme .el-row::before{display:table;content:""}.custom-theme .el-row::after{clear:both}.custom-theme .el-row--flex{display:-webkit-box;display:-ms-flexbox;display:flex}.custom-theme .el-row--flex:after,.custom-theme .el-row--flex:before{display:none}.custom-theme .el-row--flex.is-justify-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.custom-theme .el-row--flex.is-justify-end{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.custom-theme .el-row--flex.is-justify-space-between{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.custom-theme .el-row--flex.is-justify-space-around{-ms-flex-pack:distribute;justify-content:space-around}.custom-theme .el-row--flex.is-align-middle{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.custom-theme .el-row--flex.is-align-bottom{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}.custom-theme [class*=el-col-]{float:left;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-col-0{display:none}.custom-theme .el-col-1{width:4.16667%}.custom-theme .el-col-offset-1{margin-left:4.16667%}.custom-theme .el-col-pull-1{position:relative;right:4.16667%}.custom-theme .el-col-push-1{position:relative;left:4.16667%}.custom-theme .el-col-2{width:8.33333%}.custom-theme .el-col-offset-2{margin-left:8.33333%}.custom-theme .el-col-pull-2{position:relative;right:8.33333%}.custom-theme .el-col-push-2{position:relative;left:8.33333%}.custom-theme .el-col-3{width:12.5%}.custom-theme .el-col-offset-3{margin-left:12.5%}.custom-theme .el-col-pull-3{position:relative;right:12.5%}.custom-theme .el-col-push-3{position:relative;left:12.5%}.custom-theme .el-col-4{width:16.66667%}.custom-theme .el-col-offset-4{margin-left:16.66667%}.custom-theme .el-col-pull-4{position:relative;right:16.66667%}.custom-theme .el-col-push-4{position:relative;left:16.66667%}.custom-theme .el-col-5{width:20.83333%}.custom-theme .el-col-offset-5{margin-left:20.83333%}.custom-theme .el-col-pull-5{position:relative;right:20.83333%}.custom-theme .el-col-push-5{position:relative;left:20.83333%}.custom-theme .el-col-6{width:25%}.custom-theme .el-col-offset-6{margin-left:25%}.custom-theme .el-col-pull-6{position:relative;right:25%}.custom-theme .el-col-push-6{position:relative;left:25%}.custom-theme .el-col-7{width:29.16667%}.custom-theme .el-col-offset-7{margin-left:29.16667%}.custom-theme .el-col-pull-7{position:relative;right:29.16667%}.custom-theme .el-col-push-7{position:relative;left:29.16667%}.custom-theme .el-col-8{width:33.33333%}.custom-theme .el-col-offset-8{margin-left:33.33333%}.custom-theme .el-col-pull-8{position:relative;right:33.33333%}.custom-theme .el-col-push-8{position:relative;left:33.33333%}.custom-theme .el-col-9{width:37.5%}.custom-theme .el-col-offset-9{margin-left:37.5%}.custom-theme .el-col-pull-9{position:relative;right:37.5%}.custom-theme .el-col-push-9{position:relative;left:37.5%}.custom-theme .el-col-10{width:41.66667%}.custom-theme .el-col-offset-10{margin-left:41.66667%}.custom-theme .el-col-pull-10{position:relative;right:41.66667%}.custom-theme .el-col-push-10{position:relative;left:41.66667%}.custom-theme .el-col-11{width:45.83333%}.custom-theme .el-col-offset-11{margin-left:45.83333%}.custom-theme .el-col-pull-11{position:relative;right:45.83333%}.custom-theme .el-col-push-11{position:relative;left:45.83333%}.custom-theme .el-col-12{width:50%}.custom-theme .el-col-offset-12{margin-left:50%}.custom-theme .el-col-pull-12{position:relative;right:50%}.custom-theme .el-col-push-12{position:relative;left:50%}.custom-theme .el-col-13{width:54.16667%}.custom-theme .el-col-offset-13{margin-left:54.16667%}.custom-theme .el-col-pull-13{position:relative;right:54.16667%}.custom-theme .el-col-push-13{position:relative;left:54.16667%}.custom-theme .el-col-14{width:58.33333%}.custom-theme .el-col-offset-14{margin-left:58.33333%}.custom-theme .el-col-pull-14{position:relative;right:58.33333%}.custom-theme .el-col-push-14{position:relative;left:58.33333%}.custom-theme .el-col-15{width:62.5%}.custom-theme .el-col-offset-15{margin-left:62.5%}.custom-theme .el-col-pull-15{position:relative;right:62.5%}.custom-theme .el-col-push-15{position:relative;left:62.5%}.custom-theme .el-col-16{width:66.66667%}.custom-theme .el-col-offset-16{margin-left:66.66667%}.custom-theme .el-col-pull-16{position:relative;right:66.66667%}.custom-theme .el-col-push-16{position:relative;left:66.66667%}.custom-theme .el-col-17{width:70.83333%}.custom-theme .el-col-offset-17{margin-left:70.83333%}.custom-theme .el-col-pull-17{position:relative;right:70.83333%}.custom-theme .el-col-push-17{position:relative;left:70.83333%}.custom-theme .el-col-18{width:75%}.custom-theme .el-col-offset-18{margin-left:75%}.custom-theme .el-col-pull-18{position:relative;right:75%}.custom-theme .el-col-push-18{position:relative;left:75%}.custom-theme .el-col-19{width:79.16667%}.custom-theme .el-col-offset-19{margin-left:79.16667%}.custom-theme .el-col-pull-19{position:relative;right:79.16667%}.custom-theme .el-col-push-19{position:relative;left:79.16667%}.custom-theme .el-col-20{width:83.33333%}.custom-theme .el-col-offset-20{margin-left:83.33333%}.custom-theme .el-col-pull-20{position:relative;right:83.33333%}.custom-theme .el-col-push-20{position:relative;left:83.33333%}.custom-theme .el-col-21{width:87.5%}.custom-theme .el-col-offset-21{margin-left:87.5%}.custom-theme .el-col-pull-21{position:relative;right:87.5%}.custom-theme .el-col-push-21{position:relative;left:87.5%}.custom-theme .el-col-22{width:91.66667%}.custom-theme .el-col-offset-22{margin-left:91.66667%}.custom-theme .el-col-pull-22{position:relative;right:91.66667%}.custom-theme .el-col-push-22{position:relative;left:91.66667%}.custom-theme .el-col-23{width:95.83333%}.custom-theme .el-col-offset-23{margin-left:95.83333%}.custom-theme .el-col-pull-23{position:relative;right:95.83333%}.custom-theme .el-col-push-23{position:relative;left:95.83333%}.custom-theme .el-col-24{width:100%}.custom-theme .el-col-offset-24{margin-left:100%}.custom-theme .el-col-pull-24{position:relative;right:100%}.custom-theme .el-col-push-24{position:relative;left:100%}@media only screen and (max-width:768px){.custom-theme .el-col-xs-0{display:none}.custom-theme .el-col-xs-1{width:4.16667%}.custom-theme .el-col-xs-offset-1{margin-left:4.16667%}.custom-theme .el-col-xs-pull-1{position:relative;right:4.16667%}.custom-theme .el-col-xs-push-1{position:relative;left:4.16667%}.custom-theme .el-col-xs-2{width:8.33333%}.custom-theme .el-col-xs-offset-2{margin-left:8.33333%}.custom-theme .el-col-xs-pull-2{position:relative;right:8.33333%}.custom-theme .el-col-xs-push-2{position:relative;left:8.33333%}.custom-theme .el-col-xs-3{width:12.5%}.custom-theme .el-col-xs-offset-3{margin-left:12.5%}.custom-theme .el-col-xs-pull-3{position:relative;right:12.5%}.custom-theme .el-col-xs-push-3{position:relative;left:12.5%}.custom-theme .el-col-xs-4{width:16.66667%}.custom-theme .el-col-xs-offset-4{margin-left:16.66667%}.custom-theme .el-col-xs-pull-4{position:relative;right:16.66667%}.custom-theme .el-col-xs-push-4{position:relative;left:16.66667%}.custom-theme .el-col-xs-5{width:20.83333%}.custom-theme .el-col-xs-offset-5{margin-left:20.83333%}.custom-theme .el-col-xs-pull-5{position:relative;right:20.83333%}.custom-theme .el-col-xs-push-5{position:relative;left:20.83333%}.custom-theme .el-col-xs-6{width:25%}.custom-theme .el-col-xs-offset-6{margin-left:25%}.custom-theme .el-col-xs-pull-6{position:relative;right:25%}.custom-theme .el-col-xs-push-6{position:relative;left:25%}.custom-theme .el-col-xs-7{width:29.16667%}.custom-theme .el-col-xs-offset-7{margin-left:29.16667%}.custom-theme .el-col-xs-pull-7{position:relative;right:29.16667%}.custom-theme .el-col-xs-push-7{position:relative;left:29.16667%}.custom-theme .el-col-xs-8{width:33.33333%}.custom-theme .el-col-xs-offset-8{margin-left:33.33333%}.custom-theme .el-col-xs-pull-8{position:relative;right:33.33333%}.custom-theme .el-col-xs-push-8{position:relative;left:33.33333%}.custom-theme .el-col-xs-9{width:37.5%}.custom-theme .el-col-xs-offset-9{margin-left:37.5%}.custom-theme .el-col-xs-pull-9{position:relative;right:37.5%}.custom-theme .el-col-xs-push-9{position:relative;left:37.5%}.custom-theme .el-col-xs-10{width:41.66667%}.custom-theme .el-col-xs-offset-10{margin-left:41.66667%}.custom-theme .el-col-xs-pull-10{position:relative;right:41.66667%}.custom-theme .el-col-xs-push-10{position:relative;left:41.66667%}.custom-theme .el-col-xs-11{width:45.83333%}.custom-theme .el-col-xs-offset-11{margin-left:45.83333%}.custom-theme .el-col-xs-pull-11{position:relative;right:45.83333%}.custom-theme .el-col-xs-push-11{position:relative;left:45.83333%}.custom-theme .el-col-xs-12{width:50%}.custom-theme .el-col-xs-offset-12{margin-left:50%}.custom-theme .el-col-xs-pull-12{position:relative;right:50%}.custom-theme .el-col-xs-push-12{position:relative;left:50%}.custom-theme .el-col-xs-13{width:54.16667%}.custom-theme .el-col-xs-offset-13{margin-left:54.16667%}.custom-theme .el-col-xs-pull-13{position:relative;right:54.16667%}.custom-theme .el-col-xs-push-13{position:relative;left:54.16667%}.custom-theme .el-col-xs-14{width:58.33333%}.custom-theme .el-col-xs-offset-14{margin-left:58.33333%}.custom-theme .el-col-xs-pull-14{position:relative;right:58.33333%}.custom-theme .el-col-xs-push-14{position:relative;left:58.33333%}.custom-theme .el-col-xs-15{width:62.5%}.custom-theme .el-col-xs-offset-15{margin-left:62.5%}.custom-theme .el-col-xs-pull-15{position:relative;right:62.5%}.custom-theme .el-col-xs-push-15{position:relative;left:62.5%}.custom-theme .el-col-xs-16{width:66.66667%}.custom-theme .el-col-xs-offset-16{margin-left:66.66667%}.custom-theme .el-col-xs-pull-16{position:relative;right:66.66667%}.custom-theme .el-col-xs-push-16{position:relative;left:66.66667%}.custom-theme .el-col-xs-17{width:70.83333%}.custom-theme .el-col-xs-offset-17{margin-left:70.83333%}.custom-theme .el-col-xs-pull-17{position:relative;right:70.83333%}.custom-theme .el-col-xs-push-17{position:relative;left:70.83333%}.custom-theme .el-col-xs-18{width:75%}.custom-theme .el-col-xs-offset-18{margin-left:75%}.custom-theme .el-col-xs-pull-18{position:relative;right:75%}.custom-theme .el-col-xs-push-18{position:relative;left:75%}.custom-theme .el-col-xs-19{width:79.16667%}.custom-theme .el-col-xs-offset-19{margin-left:79.16667%}.custom-theme .el-col-xs-pull-19{position:relative;right:79.16667%}.custom-theme .el-col-xs-push-19{position:relative;left:79.16667%}.custom-theme .el-col-xs-20{width:83.33333%}.custom-theme .el-col-xs-offset-20{margin-left:83.33333%}.custom-theme .el-col-xs-pull-20{position:relative;right:83.33333%}.custom-theme .el-col-xs-push-20{position:relative;left:83.33333%}.custom-theme .el-col-xs-21{width:87.5%}.custom-theme .el-col-xs-offset-21{margin-left:87.5%}.custom-theme .el-col-xs-pull-21{position:relative;right:87.5%}.custom-theme .el-col-xs-push-21{position:relative;left:87.5%}.custom-theme .el-col-xs-22{width:91.66667%}.custom-theme .el-col-xs-offset-22{margin-left:91.66667%}.custom-theme .el-col-xs-pull-22{position:relative;right:91.66667%}.custom-theme .el-col-xs-push-22{position:relative;left:91.66667%}.custom-theme .el-col-xs-23{width:95.83333%}.custom-theme .el-col-xs-offset-23{margin-left:95.83333%}.custom-theme .el-col-xs-pull-23{position:relative;right:95.83333%}.custom-theme .el-col-xs-push-23{position:relative;left:95.83333%}.custom-theme .el-col-xs-24{width:100%}.custom-theme .el-col-xs-offset-24{margin-left:100%}.custom-theme .el-col-xs-pull-24{position:relative;right:100%}.custom-theme .el-col-xs-push-24{position:relative;left:100%}}@media only screen and (min-width:768px){.custom-theme .el-col-sm-0{display:none}.custom-theme .el-col-sm-1{width:4.16667%}.custom-theme .el-col-sm-offset-1{margin-left:4.16667%}.custom-theme .el-col-sm-pull-1{position:relative;right:4.16667%}.custom-theme .el-col-sm-push-1{position:relative;left:4.16667%}.custom-theme .el-col-sm-2{width:8.33333%}.custom-theme .el-col-sm-offset-2{margin-left:8.33333%}.custom-theme .el-col-sm-pull-2{position:relative;right:8.33333%}.custom-theme .el-col-sm-push-2{position:relative;left:8.33333%}.custom-theme .el-col-sm-3{width:12.5%}.custom-theme .el-col-sm-offset-3{margin-left:12.5%}.custom-theme .el-col-sm-pull-3{position:relative;right:12.5%}.custom-theme .el-col-sm-push-3{position:relative;left:12.5%}.custom-theme .el-col-sm-4{width:16.66667%}.custom-theme .el-col-sm-offset-4{margin-left:16.66667%}.custom-theme .el-col-sm-pull-4{position:relative;right:16.66667%}.custom-theme .el-col-sm-push-4{position:relative;left:16.66667%}.custom-theme .el-col-sm-5{width:20.83333%}.custom-theme .el-col-sm-offset-5{margin-left:20.83333%}.custom-theme .el-col-sm-pull-5{position:relative;right:20.83333%}.custom-theme .el-col-sm-push-5{position:relative;left:20.83333%}.custom-theme .el-col-sm-6{width:25%}.custom-theme .el-col-sm-offset-6{margin-left:25%}.custom-theme .el-col-sm-pull-6{position:relative;right:25%}.custom-theme .el-col-sm-push-6{position:relative;left:25%}.custom-theme .el-col-sm-7{width:29.16667%}.custom-theme .el-col-sm-offset-7{margin-left:29.16667%}.custom-theme .el-col-sm-pull-7{position:relative;right:29.16667%}.custom-theme .el-col-sm-push-7{position:relative;left:29.16667%}.custom-theme .el-col-sm-8{width:33.33333%}.custom-theme .el-col-sm-offset-8{margin-left:33.33333%}.custom-theme .el-col-sm-pull-8{position:relative;right:33.33333%}.custom-theme .el-col-sm-push-8{position:relative;left:33.33333%}.custom-theme .el-col-sm-9{width:37.5%}.custom-theme .el-col-sm-offset-9{margin-left:37.5%}.custom-theme .el-col-sm-pull-9{position:relative;right:37.5%}.custom-theme .el-col-sm-push-9{position:relative;left:37.5%}.custom-theme .el-col-sm-10{width:41.66667%}.custom-theme .el-col-sm-offset-10{margin-left:41.66667%}.custom-theme .el-col-sm-pull-10{position:relative;right:41.66667%}.custom-theme .el-col-sm-push-10{position:relative;left:41.66667%}.custom-theme .el-col-sm-11{width:45.83333%}.custom-theme .el-col-sm-offset-11{margin-left:45.83333%}.custom-theme .el-col-sm-pull-11{position:relative;right:45.83333%}.custom-theme .el-col-sm-push-11{position:relative;left:45.83333%}.custom-theme .el-col-sm-12{width:50%}.custom-theme .el-col-sm-offset-12{margin-left:50%}.custom-theme .el-col-sm-pull-12{position:relative;right:50%}.custom-theme .el-col-sm-push-12{position:relative;left:50%}.custom-theme .el-col-sm-13{width:54.16667%}.custom-theme .el-col-sm-offset-13{margin-left:54.16667%}.custom-theme .el-col-sm-pull-13{position:relative;right:54.16667%}.custom-theme .el-col-sm-push-13{position:relative;left:54.16667%}.custom-theme .el-col-sm-14{width:58.33333%}.custom-theme .el-col-sm-offset-14{margin-left:58.33333%}.custom-theme .el-col-sm-pull-14{position:relative;right:58.33333%}.custom-theme .el-col-sm-push-14{position:relative;left:58.33333%}.custom-theme .el-col-sm-15{width:62.5%}.custom-theme .el-col-sm-offset-15{margin-left:62.5%}.custom-theme .el-col-sm-pull-15{position:relative;right:62.5%}.custom-theme .el-col-sm-push-15{position:relative;left:62.5%}.custom-theme .el-col-sm-16{width:66.66667%}.custom-theme .el-col-sm-offset-16{margin-left:66.66667%}.custom-theme .el-col-sm-pull-16{position:relative;right:66.66667%}.custom-theme .el-col-sm-push-16{position:relative;left:66.66667%}.custom-theme .el-col-sm-17{width:70.83333%}.custom-theme .el-col-sm-offset-17{margin-left:70.83333%}.custom-theme .el-col-sm-pull-17{position:relative;right:70.83333%}.custom-theme .el-col-sm-push-17{position:relative;left:70.83333%}.custom-theme .el-col-sm-18{width:75%}.custom-theme .el-col-sm-offset-18{margin-left:75%}.custom-theme .el-col-sm-pull-18{position:relative;right:75%}.custom-theme .el-col-sm-push-18{position:relative;left:75%}.custom-theme .el-col-sm-19{width:79.16667%}.custom-theme .el-col-sm-offset-19{margin-left:79.16667%}.custom-theme .el-col-sm-pull-19{position:relative;right:79.16667%}.custom-theme .el-col-sm-push-19{position:relative;left:79.16667%}.custom-theme .el-col-sm-20{width:83.33333%}.custom-theme .el-col-sm-offset-20{margin-left:83.33333%}.custom-theme .el-col-sm-pull-20{position:relative;right:83.33333%}.custom-theme .el-col-sm-push-20{position:relative;left:83.33333%}.custom-theme .el-col-sm-21{width:87.5%}.custom-theme .el-col-sm-offset-21{margin-left:87.5%}.custom-theme .el-col-sm-pull-21{position:relative;right:87.5%}.custom-theme .el-col-sm-push-21{position:relative;left:87.5%}.custom-theme .el-col-sm-22{width:91.66667%}.custom-theme .el-col-sm-offset-22{margin-left:91.66667%}.custom-theme .el-col-sm-pull-22{position:relative;right:91.66667%}.custom-theme .el-col-sm-push-22{position:relative;left:91.66667%}.custom-theme .el-col-sm-23{width:95.83333%}.custom-theme .el-col-sm-offset-23{margin-left:95.83333%}.custom-theme .el-col-sm-pull-23{position:relative;right:95.83333%}.custom-theme .el-col-sm-push-23{position:relative;left:95.83333%}.custom-theme .el-col-sm-24{width:100%}.custom-theme .el-col-sm-offset-24{margin-left:100%}.custom-theme .el-col-sm-pull-24{position:relative;right:100%}.custom-theme .el-col-sm-push-24{position:relative;left:100%}}@media only screen and (min-width:992px){.custom-theme .el-col-md-0{display:none}.custom-theme .el-col-md-1{width:4.16667%}.custom-theme .el-col-md-offset-1{margin-left:4.16667%}.custom-theme .el-col-md-pull-1{position:relative;right:4.16667%}.custom-theme .el-col-md-push-1{position:relative;left:4.16667%}.custom-theme .el-col-md-2{width:8.33333%}.custom-theme .el-col-md-offset-2{margin-left:8.33333%}.custom-theme .el-col-md-pull-2{position:relative;right:8.33333%}.custom-theme .el-col-md-push-2{position:relative;left:8.33333%}.custom-theme .el-col-md-3{width:12.5%}.custom-theme .el-col-md-offset-3{margin-left:12.5%}.custom-theme .el-col-md-pull-3{position:relative;right:12.5%}.custom-theme .el-col-md-push-3{position:relative;left:12.5%}.custom-theme .el-col-md-4{width:16.66667%}.custom-theme .el-col-md-offset-4{margin-left:16.66667%}.custom-theme .el-col-md-pull-4{position:relative;right:16.66667%}.custom-theme .el-col-md-push-4{position:relative;left:16.66667%}.custom-theme .el-col-md-5{width:20.83333%}.custom-theme .el-col-md-offset-5{margin-left:20.83333%}.custom-theme .el-col-md-pull-5{position:relative;right:20.83333%}.custom-theme .el-col-md-push-5{position:relative;left:20.83333%}.custom-theme .el-col-md-6{width:25%}.custom-theme .el-col-md-offset-6{margin-left:25%}.custom-theme .el-col-md-pull-6{position:relative;right:25%}.custom-theme .el-col-md-push-6{position:relative;left:25%}.custom-theme .el-col-md-7{width:29.16667%}.custom-theme .el-col-md-offset-7{margin-left:29.16667%}.custom-theme .el-col-md-pull-7{position:relative;right:29.16667%}.custom-theme .el-col-md-push-7{position:relative;left:29.16667%}.custom-theme .el-col-md-8{width:33.33333%}.custom-theme .el-col-md-offset-8{margin-left:33.33333%}.custom-theme .el-col-md-pull-8{position:relative;right:33.33333%}.custom-theme .el-col-md-push-8{position:relative;left:33.33333%}.custom-theme .el-col-md-9{width:37.5%}.custom-theme .el-col-md-offset-9{margin-left:37.5%}.custom-theme .el-col-md-pull-9{position:relative;right:37.5%}.custom-theme .el-col-md-push-9{position:relative;left:37.5%}.custom-theme .el-col-md-10{width:41.66667%}.custom-theme .el-col-md-offset-10{margin-left:41.66667%}.custom-theme .el-col-md-pull-10{position:relative;right:41.66667%}.custom-theme .el-col-md-push-10{position:relative;left:41.66667%}.custom-theme .el-col-md-11{width:45.83333%}.custom-theme .el-col-md-offset-11{margin-left:45.83333%}.custom-theme .el-col-md-pull-11{position:relative;right:45.83333%}.custom-theme .el-col-md-push-11{position:relative;left:45.83333%}.custom-theme .el-col-md-12{width:50%}.custom-theme .el-col-md-offset-12{margin-left:50%}.custom-theme .el-col-md-pull-12{position:relative;right:50%}.custom-theme .el-col-md-push-12{position:relative;left:50%}.custom-theme .el-col-md-13{width:54.16667%}.custom-theme .el-col-md-offset-13{margin-left:54.16667%}.custom-theme .el-col-md-pull-13{position:relative;right:54.16667%}.custom-theme .el-col-md-push-13{position:relative;left:54.16667%}.custom-theme .el-col-md-14{width:58.33333%}.custom-theme .el-col-md-offset-14{margin-left:58.33333%}.custom-theme .el-col-md-pull-14{position:relative;right:58.33333%}.custom-theme .el-col-md-push-14{position:relative;left:58.33333%}.custom-theme .el-col-md-15{width:62.5%}.custom-theme .el-col-md-offset-15{margin-left:62.5%}.custom-theme .el-col-md-pull-15{position:relative;right:62.5%}.custom-theme .el-col-md-push-15{position:relative;left:62.5%}.custom-theme .el-col-md-16{width:66.66667%}.custom-theme .el-col-md-offset-16{margin-left:66.66667%}.custom-theme .el-col-md-pull-16{position:relative;right:66.66667%}.custom-theme .el-col-md-push-16{position:relative;left:66.66667%}.custom-theme .el-col-md-17{width:70.83333%}.custom-theme .el-col-md-offset-17{margin-left:70.83333%}.custom-theme .el-col-md-pull-17{position:relative;right:70.83333%}.custom-theme .el-col-md-push-17{position:relative;left:70.83333%}.custom-theme .el-col-md-18{width:75%}.custom-theme .el-col-md-offset-18{margin-left:75%}.custom-theme .el-col-md-pull-18{position:relative;right:75%}.custom-theme .el-col-md-push-18{position:relative;left:75%}.custom-theme .el-col-md-19{width:79.16667%}.custom-theme .el-col-md-offset-19{margin-left:79.16667%}.custom-theme .el-col-md-pull-19{position:relative;right:79.16667%}.custom-theme .el-col-md-push-19{position:relative;left:79.16667%}.custom-theme .el-col-md-20{width:83.33333%}.custom-theme .el-col-md-offset-20{margin-left:83.33333%}.custom-theme .el-col-md-pull-20{position:relative;right:83.33333%}.custom-theme .el-col-md-push-20{position:relative;left:83.33333%}.custom-theme .el-col-md-21{width:87.5%}.custom-theme .el-col-md-offset-21{margin-left:87.5%}.custom-theme .el-col-md-pull-21{position:relative;right:87.5%}.custom-theme .el-col-md-push-21{position:relative;left:87.5%}.custom-theme .el-col-md-22{width:91.66667%}.custom-theme .el-col-md-offset-22{margin-left:91.66667%}.custom-theme .el-col-md-pull-22{position:relative;right:91.66667%}.custom-theme .el-col-md-push-22{position:relative;left:91.66667%}.custom-theme .el-col-md-23{width:95.83333%}.custom-theme .el-col-md-offset-23{margin-left:95.83333%}.custom-theme .el-col-md-pull-23{position:relative;right:95.83333%}.custom-theme .el-col-md-push-23{position:relative;left:95.83333%}.custom-theme .el-col-md-24{width:100%}.custom-theme .el-col-md-offset-24{margin-left:100%}.custom-theme .el-col-md-pull-24{position:relative;right:100%}.custom-theme .el-col-md-push-24{position:relative;left:100%}}@media only screen and (min-width:1200px){.custom-theme .el-col-lg-0{display:none}.custom-theme .el-col-lg-1{width:4.16667%}.custom-theme .el-col-lg-offset-1{margin-left:4.16667%}.custom-theme .el-col-lg-pull-1{position:relative;right:4.16667%}.custom-theme .el-col-lg-push-1{position:relative;left:4.16667%}.custom-theme .el-col-lg-2{width:8.33333%}.custom-theme .el-col-lg-offset-2{margin-left:8.33333%}.custom-theme .el-col-lg-pull-2{position:relative;right:8.33333%}.custom-theme .el-col-lg-push-2{position:relative;left:8.33333%}.custom-theme .el-col-lg-3{width:12.5%}.custom-theme .el-col-lg-offset-3{margin-left:12.5%}.custom-theme .el-col-lg-pull-3{position:relative;right:12.5%}.custom-theme .el-col-lg-push-3{position:relative;left:12.5%}.custom-theme .el-col-lg-4{width:16.66667%}.custom-theme .el-col-lg-offset-4{margin-left:16.66667%}.custom-theme .el-col-lg-pull-4{position:relative;right:16.66667%}.custom-theme .el-col-lg-push-4{position:relative;left:16.66667%}.custom-theme .el-col-lg-5{width:20.83333%}.custom-theme .el-col-lg-offset-5{margin-left:20.83333%}.custom-theme .el-col-lg-pull-5{position:relative;right:20.83333%}.custom-theme .el-col-lg-push-5{position:relative;left:20.83333%}.custom-theme .el-col-lg-6{width:25%}.custom-theme .el-col-lg-offset-6{margin-left:25%}.custom-theme .el-col-lg-pull-6{position:relative;right:25%}.custom-theme .el-col-lg-push-6{position:relative;left:25%}.custom-theme .el-col-lg-7{width:29.16667%}.custom-theme .el-col-lg-offset-7{margin-left:29.16667%}.custom-theme .el-col-lg-pull-7{position:relative;right:29.16667%}.custom-theme .el-col-lg-push-7{position:relative;left:29.16667%}.custom-theme .el-col-lg-8{width:33.33333%}.custom-theme .el-col-lg-offset-8{margin-left:33.33333%}.custom-theme .el-col-lg-pull-8{position:relative;right:33.33333%}.custom-theme .el-col-lg-push-8{position:relative;left:33.33333%}.custom-theme .el-col-lg-9{width:37.5%}.custom-theme .el-col-lg-offset-9{margin-left:37.5%}.custom-theme .el-col-lg-pull-9{position:relative;right:37.5%}.custom-theme .el-col-lg-push-9{position:relative;left:37.5%}.custom-theme .el-col-lg-10{width:41.66667%}.custom-theme .el-col-lg-offset-10{margin-left:41.66667%}.custom-theme .el-col-lg-pull-10{position:relative;right:41.66667%}.custom-theme .el-col-lg-push-10{position:relative;left:41.66667%}.custom-theme .el-col-lg-11{width:45.83333%}.custom-theme .el-col-lg-offset-11{margin-left:45.83333%}.custom-theme .el-col-lg-pull-11{position:relative;right:45.83333%}.custom-theme .el-col-lg-push-11{position:relative;left:45.83333%}.custom-theme .el-col-lg-12{width:50%}.custom-theme .el-col-lg-offset-12{margin-left:50%}.custom-theme .el-col-lg-pull-12{position:relative;right:50%}.custom-theme .el-col-lg-push-12{position:relative;left:50%}.custom-theme .el-col-lg-13{width:54.16667%}.custom-theme .el-col-lg-offset-13{margin-left:54.16667%}.custom-theme .el-col-lg-pull-13{position:relative;right:54.16667%}.custom-theme .el-col-lg-push-13{position:relative;left:54.16667%}.custom-theme .el-col-lg-14{width:58.33333%}.custom-theme .el-col-lg-offset-14{margin-left:58.33333%}.custom-theme .el-col-lg-pull-14{position:relative;right:58.33333%}.custom-theme .el-col-lg-push-14{position:relative;left:58.33333%}.custom-theme .el-col-lg-15{width:62.5%}.custom-theme .el-col-lg-offset-15{margin-left:62.5%}.custom-theme .el-col-lg-pull-15{position:relative;right:62.5%}.custom-theme .el-col-lg-push-15{position:relative;left:62.5%}.custom-theme .el-col-lg-16{width:66.66667%}.custom-theme .el-col-lg-offset-16{margin-left:66.66667%}.custom-theme .el-col-lg-pull-16{position:relative;right:66.66667%}.custom-theme .el-col-lg-push-16{position:relative;left:66.66667%}.custom-theme .el-col-lg-17{width:70.83333%}.custom-theme .el-col-lg-offset-17{margin-left:70.83333%}.custom-theme .el-col-lg-pull-17{position:relative;right:70.83333%}.custom-theme .el-col-lg-push-17{position:relative;left:70.83333%}.custom-theme .el-col-lg-18{width:75%}.custom-theme .el-col-lg-offset-18{margin-left:75%}.custom-theme .el-col-lg-pull-18{position:relative;right:75%}.custom-theme .el-col-lg-push-18{position:relative;left:75%}.custom-theme .el-col-lg-19{width:79.16667%}.custom-theme .el-col-lg-offset-19{margin-left:79.16667%}.custom-theme .el-col-lg-pull-19{position:relative;right:79.16667%}.custom-theme .el-col-lg-push-19{position:relative;left:79.16667%}.custom-theme .el-col-lg-20{width:83.33333%}.custom-theme .el-col-lg-offset-20{margin-left:83.33333%}.custom-theme .el-col-lg-pull-20{position:relative;right:83.33333%}.custom-theme .el-col-lg-push-20{position:relative;left:83.33333%}.custom-theme .el-col-lg-21{width:87.5%}.custom-theme .el-col-lg-offset-21{margin-left:87.5%}.custom-theme .el-col-lg-pull-21{position:relative;right:87.5%}.custom-theme .el-col-lg-push-21{position:relative;left:87.5%}.custom-theme .el-col-lg-22{width:91.66667%}.custom-theme .el-col-lg-offset-22{margin-left:91.66667%}.custom-theme .el-col-lg-pull-22{position:relative;right:91.66667%}.custom-theme .el-col-lg-push-22{position:relative;left:91.66667%}.custom-theme .el-col-lg-23{width:95.83333%}.custom-theme .el-col-lg-offset-23{margin-left:95.83333%}.custom-theme .el-col-lg-pull-23{position:relative;right:95.83333%}.custom-theme .el-col-lg-push-23{position:relative;left:95.83333%}.custom-theme .el-col-lg-24{width:100%}.custom-theme .el-col-lg-offset-24{margin-left:100%}.custom-theme .el-col-lg-pull-24{position:relative;right:100%}.custom-theme .el-col-lg-push-24{position:relative;left:100%}}@media only screen and (min-width:1920px){.custom-theme .el-col-xl-0{display:none}.custom-theme .el-col-xl-1{width:4.16667%}.custom-theme .el-col-xl-offset-1{margin-left:4.16667%}.custom-theme .el-col-xl-pull-1{position:relative;right:4.16667%}.custom-theme .el-col-xl-push-1{position:relative;left:4.16667%}.custom-theme .el-col-xl-2{width:8.33333%}.custom-theme .el-col-xl-offset-2{margin-left:8.33333%}.custom-theme .el-col-xl-pull-2{position:relative;right:8.33333%}.custom-theme .el-col-xl-push-2{position:relative;left:8.33333%}.custom-theme .el-col-xl-3{width:12.5%}.custom-theme .el-col-xl-offset-3{margin-left:12.5%}.custom-theme .el-col-xl-pull-3{position:relative;right:12.5%}.custom-theme .el-col-xl-push-3{position:relative;left:12.5%}.custom-theme .el-col-xl-4{width:16.66667%}.custom-theme .el-col-xl-offset-4{margin-left:16.66667%}.custom-theme .el-col-xl-pull-4{position:relative;right:16.66667%}.custom-theme .el-col-xl-push-4{position:relative;left:16.66667%}.custom-theme .el-col-xl-5{width:20.83333%}.custom-theme .el-col-xl-offset-5{margin-left:20.83333%}.custom-theme .el-col-xl-pull-5{position:relative;right:20.83333%}.custom-theme .el-col-xl-push-5{position:relative;left:20.83333%}.custom-theme .el-col-xl-6{width:25%}.custom-theme .el-col-xl-offset-6{margin-left:25%}.custom-theme .el-col-xl-pull-6{position:relative;right:25%}.custom-theme .el-col-xl-push-6{position:relative;left:25%}.custom-theme .el-col-xl-7{width:29.16667%}.custom-theme .el-col-xl-offset-7{margin-left:29.16667%}.custom-theme .el-col-xl-pull-7{position:relative;right:29.16667%}.custom-theme .el-col-xl-push-7{position:relative;left:29.16667%}.custom-theme .el-col-xl-8{width:33.33333%}.custom-theme .el-col-xl-offset-8{margin-left:33.33333%}.custom-theme .el-col-xl-pull-8{position:relative;right:33.33333%}.custom-theme .el-col-xl-push-8{position:relative;left:33.33333%}.custom-theme .el-col-xl-9{width:37.5%}.custom-theme .el-col-xl-offset-9{margin-left:37.5%}.custom-theme .el-col-xl-pull-9{position:relative;right:37.5%}.custom-theme .el-col-xl-push-9{position:relative;left:37.5%}.custom-theme .el-col-xl-10{width:41.66667%}.custom-theme .el-col-xl-offset-10{margin-left:41.66667%}.custom-theme .el-col-xl-pull-10{position:relative;right:41.66667%}.custom-theme .el-col-xl-push-10{position:relative;left:41.66667%}.custom-theme .el-col-xl-11{width:45.83333%}.custom-theme .el-col-xl-offset-11{margin-left:45.83333%}.custom-theme .el-col-xl-pull-11{position:relative;right:45.83333%}.custom-theme .el-col-xl-push-11{position:relative;left:45.83333%}.custom-theme .el-col-xl-12{width:50%}.custom-theme .el-col-xl-offset-12{margin-left:50%}.custom-theme .el-col-xl-pull-12{position:relative;right:50%}.custom-theme .el-col-xl-push-12{position:relative;left:50%}.custom-theme .el-col-xl-13{width:54.16667%}.custom-theme .el-col-xl-offset-13{margin-left:54.16667%}.custom-theme .el-col-xl-pull-13{position:relative;right:54.16667%}.custom-theme .el-col-xl-push-13{position:relative;left:54.16667%}.custom-theme .el-col-xl-14{width:58.33333%}.custom-theme .el-col-xl-offset-14{margin-left:58.33333%}.custom-theme .el-col-xl-pull-14{position:relative;right:58.33333%}.custom-theme .el-col-xl-push-14{position:relative;left:58.33333%}.custom-theme .el-col-xl-15{width:62.5%}.custom-theme .el-col-xl-offset-15{margin-left:62.5%}.custom-theme .el-col-xl-pull-15{position:relative;right:62.5%}.custom-theme .el-col-xl-push-15{position:relative;left:62.5%}.custom-theme .el-col-xl-16{width:66.66667%}.custom-theme .el-col-xl-offset-16{margin-left:66.66667%}.custom-theme .el-col-xl-pull-16{position:relative;right:66.66667%}.custom-theme .el-col-xl-push-16{position:relative;left:66.66667%}.custom-theme .el-col-xl-17{width:70.83333%}.custom-theme .el-col-xl-offset-17{margin-left:70.83333%}.custom-theme .el-col-xl-pull-17{position:relative;right:70.83333%}.custom-theme .el-col-xl-push-17{position:relative;left:70.83333%}.custom-theme .el-col-xl-18{width:75%}.custom-theme .el-col-xl-offset-18{margin-left:75%}.custom-theme .el-col-xl-pull-18{position:relative;right:75%}.custom-theme .el-col-xl-push-18{position:relative;left:75%}.custom-theme .el-col-xl-19{width:79.16667%}.custom-theme .el-col-xl-offset-19{margin-left:79.16667%}.custom-theme .el-col-xl-pull-19{position:relative;right:79.16667%}.custom-theme .el-col-xl-push-19{position:relative;left:79.16667%}.custom-theme .el-col-xl-20{width:83.33333%}.custom-theme .el-col-xl-offset-20{margin-left:83.33333%}.custom-theme .el-col-xl-pull-20{position:relative;right:83.33333%}.custom-theme .el-col-xl-push-20{position:relative;left:83.33333%}.custom-theme .el-col-xl-21{width:87.5%}.custom-theme .el-col-xl-offset-21{margin-left:87.5%}.custom-theme .el-col-xl-pull-21{position:relative;right:87.5%}.custom-theme .el-col-xl-push-21{position:relative;left:87.5%}.custom-theme .el-col-xl-22{width:91.66667%}.custom-theme .el-col-xl-offset-22{margin-left:91.66667%}.custom-theme .el-col-xl-pull-22{position:relative;right:91.66667%}.custom-theme .el-col-xl-push-22{position:relative;left:91.66667%}.custom-theme .el-col-xl-23{width:95.83333%}.custom-theme .el-col-xl-offset-23{margin-left:95.83333%}.custom-theme .el-col-xl-pull-23{position:relative;right:95.83333%}.custom-theme .el-col-xl-push-23{position:relative;left:95.83333%}.custom-theme .el-col-xl-24{width:100%}.custom-theme .el-col-xl-offset-24{margin-left:100%}.custom-theme .el-col-xl-pull-24{position:relative;right:100%}.custom-theme .el-col-xl-push-24{position:relative;left:100%}}.custom-theme .el-progress{position:relative;line-height:1}.custom-theme .el-progress__text{font-size:14px;color:#5a5e66;display:inline-block;vertical-align:middle;margin-left:10px;line-height:1}.custom-theme .el-progress__text i{vertical-align:middle;display:block}.custom-theme .el-progress--circle{display:inline-block}.custom-theme .el-progress--circle .el-progress__text{position:absolute;top:50%;left:0;width:100%;text-align:center;margin:0;-webkit-transform:translate(0,-50%);transform:translate(0,-50%)}.custom-theme .el-progress--circle .el-progress__text i{vertical-align:middle;display:inline-block}.custom-theme .el-progress--without-text .el-progress__text{display:none}.custom-theme .el-progress--without-text .el-progress-bar{padding-right:0;margin-right:0;display:block}.custom-theme .el-progress--text-inside .el-progress-bar{padding-right:0;margin-right:0}.custom-theme .el-progress.is-success .el-progress-bar__inner{background-color:#409167}.custom-theme .el-progress.is-success .el-progress__text{color:#409167}.custom-theme .el-progress.is-exception .el-progress-bar__inner{background-color:#b3450e}.custom-theme .el-progress.is-exception .el-progress__text{color:#b3450e}.custom-theme .el-progress-bar{padding-right:50px;display:inline-block;vertical-align:middle;width:100%;margin-right:-55px;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-progress-bar__outer{height:6px;border-radius:100px;background-color:#e6ebf5;overflow:hidden;position:relative;vertical-align:middle}.custom-theme .el-progress-bar__inner{position:absolute;left:0;top:0;height:100%;background-color:#262729;text-align:right;border-radius:100px;line-height:1;white-space:nowrap}.custom-theme .el-progress-bar__inner::after{display:inline-block;content:"";height:100%;vertical-align:middle}.custom-theme .el-progress-bar__innerText{display:inline-block;vertical-align:middle;color:#fff;font-size:12px;margin:0 5px}@-webkit-keyframes progress{0%{background-position:0 0}100%{background-position:32px 0}}@keyframes progress{0%{background-position:0 0}100%{background-position:32px 0}}.custom-theme .el-upload{display:inline-block;text-align:center;cursor:pointer}.custom-theme .el-upload__input{display:none}.custom-theme .el-upload__tip{font-size:12px;color:#5a5e66;margin-top:7px}.custom-theme .el-upload iframe{position:absolute;z-index:-1;top:0;left:0;opacity:0}.custom-theme .el-upload--picture-card{background-color:#fbfdff;border:1px dashed #c0ccda;border-radius:6px;-webkit-box-sizing:border-box;box-sizing:border-box;width:148px;height:148px;cursor:pointer;line-height:146px;vertical-align:top}.custom-theme .el-upload--picture-card i{font-size:28px;color:#8c939d}.custom-theme .el-upload--picture-card:hover{border-color:#262729;color:#262729}.custom-theme .el-upload-dragger{background-color:#fff;border:1px dashed #d9d9d9;border-radius:6px;-webkit-box-sizing:border-box;box-sizing:border-box;width:360px;height:180px;text-align:center;cursor:pointer;position:relative;overflow:hidden}.custom-theme .el-upload-dragger .el-icon-upload{font-size:67px;color:#b4bccc;margin:40px 0 16px;line-height:50px}.custom-theme .el-upload-dragger+.el-upload__tip{text-align:center}.custom-theme .el-upload-dragger~.el-upload__files{border-top:1px solid #d8dce5;margin-top:7px;padding-top:5px}.custom-theme .el-upload-dragger .el-upload__text{color:#5a5e66;font-size:14px;text-align:center}.custom-theme .el-upload-dragger .el-upload__text em{color:#262729;font-style:normal}.custom-theme .el-upload-dragger:hover{border-color:#262729}.custom-theme .el-upload-dragger.is-dragover{background-color:rgba(32,159,255,.06);border:2px dashed #262729}.custom-theme .el-upload-list{margin:0;padding:0;list-style:none}.custom-theme .el-upload-list__item{-webkit-transition:all .5s cubic-bezier(.55,0,.1,1);transition:all .5s cubic-bezier(.55,0,.1,1);font-size:14px;color:#5a5e66;line-height:1.8;margin-top:5px;position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;width:100%}.custom-theme .el-upload-list__item .el-progress{position:absolute;top:20px;width:100%}.custom-theme .el-upload-list__item .el-progress__text{position:absolute;right:0;top:-13px}.custom-theme .el-upload-list__item .el-progress-bar{margin-right:0;padding-right:0}.custom-theme .el-upload-list__item:first-child{margin-top:10px}.custom-theme .el-upload-list__item .el-icon-upload-success{color:#409167}.custom-theme .el-upload-list__item .el-icon-close{display:none;position:absolute;top:5px;right:5px;cursor:pointer;opacity:.75;color:#5a5e66}.custom-theme .el-upload-list__item .el-icon-close:hover{opacity:1}.custom-theme .el-upload-list__item .el-icon-close-tip{display:none;position:absolute;top:5px;right:0;cursor:pointer;opacity:1;color:#262729;-webkit-transform:translate(15%,0);transform:translate(15%,0)}.custom-theme .el-upload-list__item:hover{background-color:#f5f7fa}.custom-theme .el-upload-list__item:hover .el-icon-close{display:inline-block}.custom-theme .el-upload-list__item:hover .el-progress__text{display:none}.custom-theme .el-upload-list__item.is-success .el-upload-list__item-status-label{display:block}.custom-theme .el-upload-list__item.is-success .el-upload-list__item-name:focus,.custom-theme .el-upload-list__item.is-success .el-upload-list__item-name:hover{color:#262729;cursor:pointer}.custom-theme .el-upload-list__item.is-success:focus .el-icon-close-tip{display:inline-block}.custom-theme .el-upload-list__item.is-success:active,.custom-theme .el-upload-list__item.is-success:focus:not(.focusing){outline-width:0}.custom-theme .el-upload-list__item.is-success:active .el-icon-close-tip,.custom-theme .el-upload-list__item.is-success:focus:not(.focusing) .el-icon-close-tip{display:none}.custom-theme .el-upload-list__item.is-success:focus .el-upload-list__item-status-label,.custom-theme .el-upload-list__item.is-success:hover .el-upload-list__item-status-label{display:none}.custom-theme .el-upload-list.is-disabled .el-upload-list__item:hover .el-upload-list__item-status-label{display:block}.custom-theme .el-upload-list__item-name{color:#5a5e66;display:block;margin-right:40px;overflow:hidden;padding-left:4px;text-overflow:ellipsis;-webkit-transition:color .3s;transition:color .3s;white-space:nowrap}.custom-theme .el-upload-list__item-name [class^=el-icon]{height:100%;margin-right:7px;color:#878d99;line-height:inherit}.custom-theme .el-upload-list__item-status-label{position:absolute;right:5px;top:0;line-height:inherit;display:none}.custom-theme .el-upload-list__item-delete{position:absolute;right:10px;top:0;font-size:12px;color:#5a5e66;display:none}.custom-theme .el-upload-list__item-delete:hover{color:#262729}.custom-theme .el-upload-list--picture-card{margin:0;display:inline;vertical-align:top}.custom-theme .el-upload-list--picture-card .el-upload-list__item{overflow:hidden;background-color:#fff;border:1px solid #c0ccda;border-radius:6px;-webkit-box-sizing:border-box;box-sizing:border-box;width:148px;height:148px;margin:0 8px 8px 0;display:inline-block}.custom-theme .el-upload-list--picture-card .el-upload-list__item .el-icon-check,.custom-theme .el-upload-list--picture-card .el-upload-list__item .el-icon-circle-check{color:#fff}.custom-theme .el-upload-list--picture-card .el-upload-list__item .el-icon-close{display:none}.custom-theme .el-upload-list--picture-card .el-upload-list__item:hover .el-upload-list__item-status-label{display:none}.custom-theme .el-upload-list--picture-card .el-upload-list__item:hover .el-progress__text{display:block}.custom-theme .el-upload-list--picture-card .el-upload-list__item-name{display:none}.custom-theme .el-upload-list--picture-card .el-upload-list__item-thumbnail{width:100%;height:100%}.custom-theme .el-upload-list--picture-card .el-upload-list__item-status-label{position:absolute;right:-15px;top:-6px;width:40px;height:24px;background:#13ce66;text-align:center;-webkit-transform:rotate(45deg);transform:rotate(45deg);-webkit-box-shadow:0 0 1pc 1px rgba(0,0,0,.2);box-shadow:0 0 1pc 1px rgba(0,0,0,.2)}.custom-theme .el-upload-list--picture-card .el-upload-list__item-status-label i{font-size:12px;margin-top:11px;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.custom-theme .el-upload-list--picture-card .el-upload-list__item-actions{position:absolute;width:100%;height:100%;left:0;top:0;cursor:default;text-align:center;color:#fff;opacity:0;font-size:20px;background-color:rgba(0,0,0,.5);-webkit-transition:opacity .3s;transition:opacity .3s}.custom-theme .el-upload-list--picture-card .el-upload-list__item-actions::after{display:inline-block;content:"";height:100%;vertical-align:middle}.custom-theme .el-upload-list--picture-card .el-upload-list__item-actions span{display:none;cursor:pointer}.custom-theme .el-upload-list--picture-card .el-upload-list__item-actions span+span{margin-left:15px}.custom-theme .el-upload-list--picture-card .el-upload-list__item-actions .el-upload-list__item-delete{position:static;font-size:inherit;color:inherit}.custom-theme .el-upload-list--picture-card .el-upload-list__item-actions:hover{opacity:1}.custom-theme .el-upload-list--picture-card .el-upload-list__item-actions:hover span{display:inline-block}.custom-theme .el-upload-list--picture-card .el-progress{top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);bottom:auto;width:126px}.custom-theme .el-upload-list--picture-card .el-progress .el-progress__text{top:50%}.custom-theme .el-upload-list--picture .el-upload-list__item{overflow:hidden;background-color:#fff;border:1px solid #c0ccda;border-radius:6px;-webkit-box-sizing:border-box;box-sizing:border-box;margin-top:10px;padding:10px 10px 10px 90px;height:92px}.custom-theme .el-upload-list--picture .el-upload-list__item .el-icon-check,.custom-theme .el-upload-list--picture .el-upload-list__item .el-icon-circle-check{color:#fff}.custom-theme .el-upload-list--picture .el-upload-list__item:hover .el-upload-list__item-status-label{background:0 0;-webkit-box-shadow:none;box-shadow:none;top:-2px;right:-12px}.custom-theme .el-upload-list--picture .el-upload-list__item:hover .el-progress__text{display:block}.custom-theme .el-upload-list--picture .el-upload-list__item.is-success .el-upload-list__item-name{line-height:70px;margin-top:0}.custom-theme .el-upload-list--picture .el-upload-list__item.is-success .el-upload-list__item-name i{display:none}.custom-theme .el-upload-list--picture .el-upload-list__item-thumbnail{vertical-align:middle;display:inline-block;width:70px;height:70px;float:left;position:relative;z-index:1;margin-left:-80px}.custom-theme .el-upload-list--picture .el-upload-list__item-name{display:block;margin-top:20px}.custom-theme .el-upload-list--picture .el-upload-list__item-name i{font-size:70px;line-height:1;position:absolute;left:9px;top:10px}.custom-theme .el-upload-list--picture .el-upload-list__item-status-label{position:absolute;right:-17px;top:-7px;width:46px;height:26px;background:#13ce66;text-align:center;-webkit-transform:rotate(45deg);transform:rotate(45deg);-webkit-box-shadow:0 1px 1px #ccc;box-shadow:0 1px 1px #ccc}.custom-theme .el-upload-list--picture .el-upload-list__item-status-label i{font-size:12px;margin-top:12px;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.custom-theme .el-upload-list--picture .el-progress{position:relative;top:-7px}.custom-theme .el-upload-cover{position:absolute;left:0;top:0;width:100%;height:100%;overflow:hidden;z-index:10;cursor:default}.custom-theme .el-upload-cover::after{display:inline-block;content:"";height:100%;vertical-align:middle}.custom-theme .el-upload-cover img{display:block;width:100%;height:100%}.custom-theme .el-upload-cover__label{position:absolute;right:-15px;top:-6px;width:40px;height:24px;background:#13ce66;text-align:center;-webkit-transform:rotate(45deg);transform:rotate(45deg);-webkit-box-shadow:0 0 1pc 1px rgba(0,0,0,.2);box-shadow:0 0 1pc 1px rgba(0,0,0,.2)}.custom-theme .el-upload-cover__label i{font-size:12px;margin-top:11px;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);color:#fff}.custom-theme .el-upload-cover__progress{display:inline-block;vertical-align:middle;position:static;width:243px}.custom-theme .el-upload-cover__progress+.el-upload__inner{opacity:0}.custom-theme .el-upload-cover__content{position:absolute;top:0;left:0;width:100%;height:100%}.custom-theme .el-upload-cover__interact{position:absolute;bottom:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.72);text-align:center}.custom-theme .el-upload-cover__interact .btn{display:inline-block;color:#fff;font-size:14px;cursor:pointer;vertical-align:middle;-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s;transition:transform .3s cubic-bezier(.23,1,.32,1) .1s,opacity .3s cubic-bezier(.23,1,.32,1) .1s,-webkit-transform .3s cubic-bezier(.23,1,.32,1) .1s;margin-top:60px}.custom-theme .el-upload-cover__interact .btn i{margin-top:0}.custom-theme .el-upload-cover__interact .btn span{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.custom-theme .el-upload-cover__interact .btn:not(:first-child){margin-left:35px}.custom-theme .el-upload-cover__interact .btn:hover{-webkit-transform:translateY(-13px);transform:translateY(-13px)}.custom-theme .el-upload-cover__interact .btn:hover span{opacity:1}.custom-theme .el-upload-cover__interact .btn i{color:#fff;display:block;font-size:24px;line-height:inherit;margin:0 auto 5px}.custom-theme .el-upload-cover__title{position:absolute;bottom:0;left:0;background-color:#fff;height:36px;width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:400;text-align:left;padding:0 10px;margin:0;line-height:36px;font-size:14px;color:#2d2f33}.custom-theme .el-upload-cover+.el-upload__inner{opacity:0;position:relative;z-index:1}.custom-theme .el-progress{position:relative;line-height:1}.custom-theme .el-progress__text{font-size:14px;color:#5a5e66;display:inline-block;vertical-align:middle;margin-left:10px;line-height:1}.custom-theme .el-progress__text i{vertical-align:middle;display:block}.custom-theme .el-progress--circle{display:inline-block}.custom-theme .el-progress--circle .el-progress__text{position:absolute;top:50%;left:0;width:100%;text-align:center;margin:0;-webkit-transform:translate(0,-50%);transform:translate(0,-50%)}.custom-theme .el-progress--circle .el-progress__text i{vertical-align:middle;display:inline-block}.custom-theme .el-progress--without-text .el-progress__text{display:none}.custom-theme .el-progress--without-text .el-progress-bar{padding-right:0;margin-right:0;display:block}.custom-theme .el-progress--text-inside .el-progress-bar{padding-right:0;margin-right:0}.custom-theme .el-progress.is-success .el-progress-bar__inner{background-color:#409167}.custom-theme .el-progress.is-success .el-progress__text{color:#409167}.custom-theme .el-progress.is-exception .el-progress-bar__inner{background-color:#b3450e}.custom-theme .el-progress.is-exception .el-progress__text{color:#b3450e}.custom-theme .el-progress-bar{padding-right:50px;display:inline-block;vertical-align:middle;width:100%;margin-right:-55px;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-progress-bar__outer{height:6px;border-radius:100px;background-color:#e6ebf5;overflow:hidden;position:relative;vertical-align:middle}.custom-theme .el-progress-bar__inner{position:absolute;left:0;top:0;height:100%;background-color:#262729;text-align:right;border-radius:100px;line-height:1;white-space:nowrap}.custom-theme .el-progress-bar__inner::after{display:inline-block;content:"";height:100%;vertical-align:middle}.custom-theme .el-progress-bar__innerText{display:inline-block;vertical-align:middle;color:#fff;font-size:12px;margin:0 5px}@keyframes progress{0%{background-position:0 0}100%{background-position:32px 0}}.custom-theme .el-time-spinner{width:100%;white-space:nowrap}.custom-theme .el-spinner{display:inline-block;vertical-align:middle}.custom-theme .el-spinner-inner{-webkit-animation:rotate 2s linear infinite;animation:rotate 2s linear infinite;width:50px;height:50px}.custom-theme .el-spinner-inner .path{stroke:#ececec;stroke-linecap:round;-webkit-animation:dash 1.5s ease-in-out infinite;animation:dash 1.5s ease-in-out infinite}@-webkit-keyframes rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}100%{stroke-dasharray:90,150;stroke-dashoffset:-124}}@keyframes dash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}100%{stroke-dasharray:90,150;stroke-dashoffset:-124}}.custom-theme .el-message{min-width:380px;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;border-width:1px;border-style:solid;border-color:#e6ebf5;position:fixed;left:50%;top:20px;-webkit-transform:translateX(-50%);transform:translateX(-50%);background-color:#edf2fc;-webkit-transition:opacity .3s,-webkit-transform .4s;transition:opacity .3s,-webkit-transform .4s;transition:opacity .3s,transform .4s;transition:opacity .3s,transform .4s,-webkit-transform .4s;overflow:hidden;padding:15px 15px 15px 20px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.custom-theme .el-message.is-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.custom-theme .el-message p{margin:0}.custom-theme .el-message--info .el-message__content{color:#0a76a4}.custom-theme .el-message--success{background-color:#ecf4f0;border-color:#d9e9e1}.custom-theme .el-message--success .el-message__content{color:#409167}.custom-theme .el-message--warning{background-color:#f5f6e6;border-color:#ebedce}.custom-theme .el-message--warning .el-message__content{color:#9da408}.custom-theme .el-message--error{background-color:#f7ece7;border-color:#f0dacf}.custom-theme .el-message--error .el-message__content{color:#b3450e}.custom-theme .el-message__icon{margin-right:10px}.custom-theme .el-message__content{padding:0;font-size:14px;line-height:1}.custom-theme .el-message__content:focus{outline-width:0}.custom-theme .el-message__closeBtn{position:absolute;top:50%;right:15px;-webkit-transform:translateY(-50%);transform:translateY(-50%);cursor:pointer;color:#b4bccc;font-size:16px}.custom-theme .el-message__closeBtn:focus{outline-width:0}.custom-theme .el-message__closeBtn:hover{color:#878d99}.custom-theme .el-message .el-icon-success{color:#409167}.custom-theme .el-message .el-icon-error{color:#b3450e}.custom-theme .el-message .el-icon-info{color:#0a76a4}.custom-theme .el-message .el-icon-warning{color:#9da408}.custom-theme .el-message-fade-enter,.custom-theme .el-message-fade-leave-active{opacity:0;-webkit-transform:translate(-50%,-100%);transform:translate(-50%,-100%)}.custom-theme .el-badge{position:relative;vertical-align:middle;display:inline-block}.custom-theme .el-badge__content{background-color:#b3450e;border-radius:10px;color:#fff;display:inline-block;font-size:12px;height:18px;line-height:18px;padding:0 6px;text-align:center;white-space:nowrap;border:1px solid #fff}.custom-theme .el-badge__content.is-fixed{position:absolute;top:0;right:10px;-webkit-transform:translateY(-50%) translateX(100%);transform:translateY(-50%) translateX(100%)}.custom-theme .el-badge__content.is-fixed.is-dot{right:5px}.custom-theme .el-badge__content.is-dot{height:8px;width:8px;padding:0;right:0;border-radius:50%}.custom-theme .el-card{border-radius:4px;border:1px solid #e6ebf5;background-color:#fff;overflow:hidden;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);color:#2d2f33}.custom-theme .el-card__header{padding:18px 20px;border-bottom:1px solid #e6ebf5;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-card__body{padding:20px}.custom-theme .el-rate{height:20px;line-height:1}.custom-theme .el-rate:active,.custom-theme .el-rate:focus{outline-width:0}.custom-theme .el-rate__item{display:inline-block;position:relative;font-size:0;vertical-align:middle}.custom-theme .el-rate__icon{position:relative;display:inline-block;font-size:18px;margin-right:6px;color:#b4bccc;-webkit-transition:.3s;transition:.3s}.custom-theme .el-rate__icon.hover{-webkit-transform:scale(1.15);transform:scale(1.15)}.custom-theme .el-rate__icon .path2{position:absolute;left:0;top:0}.custom-theme .el-rate__decimal{position:absolute;top:0;left:0;display:inline-block;overflow:hidden}.custom-theme .el-rate__text{font-size:14px;vertical-align:middle}.custom-theme .el-steps{display:-webkit-box;display:-ms-flexbox;display:flex}.custom-theme .el-steps--simple{padding:13px 8%;border-radius:4px;background:#f5f7fa}.custom-theme .el-steps--horizontal{white-space:nowrap}.custom-theme .el-steps--vertical{height:100%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-flow:column;flex-flow:column}.custom-theme .el-step{position:relative;-ms-flex-negative:1;flex-shrink:1}.custom-theme .el-step:last-of-type .el-step__line{display:none}.custom-theme .el-step:last-of-type.is-flex{-ms-flex-preferred-size:auto!important;flex-basis:auto!important;-ms-flex-negative:0;flex-shrink:0;-webkit-box-flex:0;-ms-flex-positive:0;flex-grow:0}.custom-theme .el-step:last-of-type .el-step__description,.custom-theme .el-step:last-of-type .el-step__main{padding-right:0}.custom-theme .el-step__head{position:relative;width:100%}.custom-theme .el-step__head.is-process{color:#2d2f33;border-color:#2d2f33}.custom-theme .el-step__head.is-wait{color:#b4bccc;border-color:#b4bccc}.custom-theme .el-step__head.is-success{color:#409167;border-color:#409167}.custom-theme .el-step__head.is-error{color:#b3450e;border-color:#b3450e}.custom-theme .el-step__head.is-finish{color:#262729;border-color:#262729}.custom-theme .el-step__icon{position:relative;z-index:1;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:24px;height:24px;font-size:14px;-webkit-box-sizing:border-box;box-sizing:border-box;background:#fff;-webkit-transition:.15s ease-out;transition:.15s ease-out}.custom-theme .el-step__icon.is-text{border-radius:50%;border:2px solid;border-color:inherit}.custom-theme .el-step__icon.is-icon{width:40px}.custom-theme .el-step__icon-inner{display:inline-block;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-align:center;font-weight:700;line-height:1;color:inherit}.custom-theme .el-step__icon-inner[class*=el-icon]:not(.is-status){font-size:25px;font-weight:400}.custom-theme .el-step__icon-inner.is-status{-webkit-transform:translateY(1px);transform:translateY(1px)}.custom-theme .el-step__line{position:absolute;border-color:inherit;background-color:#b4bccc}.custom-theme .el-step__line-inner{display:block;border-width:1px;border-style:solid;border-color:inherit;-webkit-transition:.15s ease-out;transition:.15s ease-out;-webkit-box-sizing:border-box;box-sizing:border-box;width:0;height:0}.custom-theme .el-step__main{white-space:normal;text-align:left}.custom-theme .el-step__title{font-size:16px;line-height:38px}.custom-theme .el-step__title.is-process{font-weight:700;color:#2d2f33}.custom-theme .el-step__title.is-wait{color:#b4bccc}.custom-theme .el-step__title.is-success{color:#409167}.custom-theme .el-step__title.is-error{color:#b3450e}.custom-theme .el-step__title.is-finish{color:#262729}.custom-theme .el-step__description{padding-right:10%;margin-top:-5px;font-size:12px;line-height:20px;font-weight:400}.custom-theme .el-step__description.is-process{color:#2d2f33}.custom-theme .el-step__description.is-wait{color:#b4bccc}.custom-theme .el-step__description.is-success{color:#409167}.custom-theme .el-step__description.is-error{color:#b3450e}.custom-theme .el-step__description.is-finish{color:#262729}.custom-theme .el-step.is-horizontal{display:inline-block}.custom-theme .el-step.is-horizontal .el-step__line{height:2px;top:11px;left:0;right:0}.custom-theme .el-step.is-vertical{display:-webkit-box;display:-ms-flexbox;display:flex}.custom-theme .el-step.is-vertical .el-step__head{-webkit-box-flex:0;-ms-flex-positive:0;flex-grow:0;width:24px}.custom-theme .el-step.is-vertical .el-step__main{padding-left:10px;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.custom-theme .el-step.is-vertical .el-step__title{line-height:24px;padding-bottom:8px}.custom-theme .el-step.is-vertical .el-step__line{width:2px;top:0;bottom:0;left:11px}.custom-theme .el-step.is-vertical .el-step__icon.is-icon{width:24px}.custom-theme .el-step.is-center .el-step__head{text-align:center}.custom-theme .el-step.is-center .el-step__main{text-align:center}.custom-theme .el-step.is-center .el-step__description{padding-left:20%;padding-right:20%}.custom-theme .el-step.is-center .el-step__line{left:50%;right:-50%}.custom-theme .el-step.is-simple{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.custom-theme .el-step.is-simple .el-step__head{width:auto;font-size:0;padding-right:10px}.custom-theme .el-step.is-simple .el-step__icon{background:0 0;width:16px;height:16px;font-size:12px}.custom-theme .el-step.is-simple .el-step__icon-inner[class*=el-icon]:not(.is-status){font-size:18px}.custom-theme .el-step.is-simple .el-step__icon-inner.is-status{-webkit-transform:scale(.8) translateY(1px);transform:scale(.8) translateY(1px)}.custom-theme .el-step.is-simple .el-step__main{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.custom-theme .el-step.is-simple .el-step__title{font-size:16px;line-height:20px}.custom-theme .el-step.is-simple:not(:last-of-type) .el-step__title{max-width:50%;word-break:break-all}.custom-theme .el-step.is-simple .el-step__arrow{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.custom-theme .el-step.is-simple .el-step__arrow::after,.custom-theme .el-step.is-simple .el-step__arrow::before{content:'';display:inline-block;position:absolute;height:15px;width:1px;background:#b4bccc}.custom-theme .el-step.is-simple .el-step__arrow::before{-webkit-transform:rotate(-45deg) translateY(-4px);transform:rotate(-45deg) translateY(-4px);-webkit-transform-origin:0 0;transform-origin:0 0}.custom-theme .el-step.is-simple .el-step__arrow::after{-webkit-transform:rotate(45deg) translateY(4px);transform:rotate(45deg) translateY(4px);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}.custom-theme .el-step.is-simple:last-of-type .el-step__arrow{display:none}.custom-theme .el-carousel{overflow-x:hidden;position:relative}.custom-theme .el-carousel__container{position:relative;height:300px}.custom-theme .el-carousel__arrow{border:none;outline:0;padding:0;margin:0;height:36px;width:36px;cursor:pointer;-webkit-transition:.3s;transition:.3s;border-radius:50%;background-color:rgba(31,45,61,.11);color:#fff;position:absolute;top:50%;z-index:10;-webkit-transform:translateY(-50%);transform:translateY(-50%);text-align:center;font-size:12px}.custom-theme .el-carousel__arrow--left{left:16px}.custom-theme .el-carousel__arrow--right{right:16px}.custom-theme .el-carousel__arrow:hover{background-color:rgba(31,45,61,.23)}.custom-theme .el-carousel__arrow i{cursor:pointer}.custom-theme .el-carousel__indicators{position:absolute;list-style:none;bottom:0;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);margin:0;padding:0;z-index:2}.custom-theme .el-carousel__indicators--outside{bottom:26px;text-align:center;position:static;-webkit-transform:none;transform:none}.custom-theme .el-carousel__indicators--outside .el-carousel__indicator:hover button{opacity:.64}.custom-theme .el-carousel__indicators--outside button{background-color:#b4bccc;opacity:.24}.custom-theme .el-carousel__indicators--labels{left:0;right:0;-webkit-transform:none;transform:none;text-align:center}.custom-theme .el-carousel__indicators--labels .el-carousel__button{height:auto;width:auto;padding:2px 18px;font-size:12px}.custom-theme .el-carousel__indicators--labels .el-carousel__indicator{padding:6px 4px}.custom-theme .el-carousel__indicator{display:inline-block;background-color:transparent;padding:12px 4px;cursor:pointer}.custom-theme .el-carousel__indicator:hover button{opacity:.72}.custom-theme .el-carousel__indicator.is-active button{opacity:1}.custom-theme .el-carousel__button{display:block;opacity:.48;width:30px;height:2px;background-color:#fff;border:none;outline:0;padding:0;margin:0;cursor:pointer;-webkit-transition:.3s;transition:.3s}.custom-theme .carousel-arrow-left-enter,.custom-theme .carousel-arrow-left-leave-active{-webkit-transform:translateY(-50%) translateX(-10px);transform:translateY(-50%) translateX(-10px);opacity:0}.custom-theme .carousel-arrow-right-enter,.custom-theme .carousel-arrow-right-leave-active{-webkit-transform:translateY(-50%) translateX(10px);transform:translateY(-50%) translateX(10px);opacity:0}.custom-theme .el-scrollbar{overflow:hidden;position:relative}.custom-theme .el-scrollbar:active>.el-scrollbar__bar,.custom-theme .el-scrollbar:focus>.el-scrollbar__bar,.custom-theme .el-scrollbar:hover>.el-scrollbar__bar{opacity:1;-webkit-transition:opacity 340ms ease-out;transition:opacity 340ms ease-out}.custom-theme .el-scrollbar__wrap{overflow:scroll;height:100%}.custom-theme .el-scrollbar__wrap--hidden-default::-webkit-scrollbar{width:0;height:0}.custom-theme .el-scrollbar__thumb{position:relative;display:block;width:0;height:0;cursor:pointer;border-radius:inherit;background-color:rgba(135,141,153,.3);-webkit-transition:.3s background-color;transition:.3s background-color}.custom-theme .el-scrollbar__thumb:hover{background-color:rgba(135,141,153,.5)}.custom-theme .el-scrollbar__bar{position:absolute;right:2px;bottom:2px;z-index:1;border-radius:4px;opacity:0;-webkit-transition:opacity 120ms ease-out;transition:opacity 120ms ease-out}.custom-theme .el-scrollbar__bar.is-vertical{width:6px;top:2px}.custom-theme .el-scrollbar__bar.is-vertical>div{width:100%}.custom-theme .el-scrollbar__bar.is-horizontal{height:6px;left:2px}.custom-theme .el-scrollbar__bar.is-horizontal>div{height:100%}.custom-theme .el-carousel__item{position:absolute;top:0;left:0;width:100%;height:100%;display:inline-block;overflow:hidden;z-index:0}.custom-theme .el-carousel__item.is-active{z-index:2}.custom-theme .el-carousel__item.is-animating{-webkit-transition:-webkit-transform .4s ease-in-out;transition:-webkit-transform .4s ease-in-out;transition:transform .4s ease-in-out;transition:transform .4s ease-in-out,-webkit-transform .4s ease-in-out}.custom-theme .el-carousel__item--card{width:50%;-webkit-transition:-webkit-transform .4s ease-in-out;transition:-webkit-transform .4s ease-in-out;transition:transform .4s ease-in-out;transition:transform .4s ease-in-out,-webkit-transform .4s ease-in-out}.custom-theme .el-carousel__item--card.is-in-stage{cursor:pointer;z-index:1}.custom-theme .el-carousel__item--card.is-in-stage.is-hover .el-carousel__mask,.custom-theme .el-carousel__item--card.is-in-stage:hover .el-carousel__mask{opacity:.12}.custom-theme .el-carousel__item--card.is-active{z-index:2}.custom-theme .el-carousel__mask{position:absolute;width:100%;height:100%;top:0;left:0;background-color:#fff;opacity:.24;-webkit-transition:.2s;transition:.2s}.custom-theme .el-collapse{border-top:1px solid #e6ebf5;border-bottom:1px solid #e6ebf5}.custom-theme .el-collapse-item__header{height:48px;line-height:48px;background-color:#fff;color:#2d2f33;cursor:pointer;border-bottom:1px solid #e6ebf5;font-size:13px;font-weight:500;-webkit-transition:border-bottom-color .3s;transition:border-bottom-color .3s}.custom-theme .el-collapse-item__header:active,.custom-theme .el-collapse-item__header:focus:not(.focusing){outline-width:0}.custom-theme .el-collapse-item__arrow{margin-right:8px;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;float:right;line-height:48px;font-weight:300}.custom-theme .el-collapse-item__wrap{will-change:height;background-color:#fff;overflow:hidden;-webkit-box-sizing:border-box;box-sizing:border-box;border-bottom:1px solid #e6ebf5}.custom-theme .el-collapse-item__content{padding-bottom:25px;font-size:13px;color:#2d2f33;line-height:1.769230769230769}.custom-theme .el-collapse-item.is-active .el-collapse-item__header{border-bottom-color:transparent}.custom-theme .el-collapse-item.is-active .el-collapse-item__header .el-collapse-item__arrow{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.custom-theme .el-collapse-item:last-child{margin-bottom:-1px}.custom-theme .el-input{position:relative;font-size:14px;display:inline-block;width:100%}.custom-theme .el-input::-webkit-scrollbar{z-index:11;width:6px}.custom-theme .el-input::-webkit-scrollbar:horizontal{height:6px}.custom-theme .el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.custom-theme .el-input::-webkit-scrollbar-corner{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.custom-theme .el-input__inner{-webkit-appearance:none;background-color:#fff;background-image:none;border-radius:4px;border:1px solid #d8dce5;-webkit-box-sizing:border-box;box-sizing:border-box;color:#5a5e66;display:inline-block;font-size:inherit;height:40px;line-height:1;outline:0;padding:0 15px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.custom-theme .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input__inner:hover{border-color:#b4bccc}.custom-theme .el-input__inner:focus{outline:0;border-color:#262729}.custom-theme .el-input__suffix{position:absolute;height:100%;right:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s;pointer-events:none}.custom-theme .el-input__suffix-inner{pointer-events:all}.custom-theme .el-input__prefix{position:absolute;height:100%;left:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s}.custom-theme .el-input__icon{height:100%;width:25px;text-align:center;-webkit-transition:all .3s;transition:all .3s;line-height:40px}.custom-theme .el-input__icon:after{content:'';height:100%;width:0;display:inline-block;vertical-align:middle}.custom-theme .el-input__validateIcon{pointer-events:none}.custom-theme .el-input.is-active .el-input__inner{outline:0;border-color:#262729}.custom-theme .el-input.is-disabled .el-input__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-input.is-disabled .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__icon{cursor:not-allowed}.custom-theme .el-input--suffix .el-input__inner{padding-right:30px}.custom-theme .el-input--prefix .el-input__inner{padding-left:30px}.custom-theme .el-input--medium{font-size:14px}.custom-theme .el-input--medium .el-input__inner{height:36px}.custom-theme .el-input--medium .el-input__icon{line-height:36px}.custom-theme .el-input--small{font-size:13px}.custom-theme .el-input--small .el-input__inner{height:32px}.custom-theme .el-input--small .el-input__icon{line-height:32px}.custom-theme .el-input--mini{font-size:12px}.custom-theme .el-input--mini .el-input__inner{height:28px}.custom-theme .el-input--mini .el-input__icon{line-height:28px}.custom-theme .el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate}.custom-theme .el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.custom-theme .el-input-group__append,.custom-theme .el-input-group__prepend{background-color:#f5f7fa;color:#0a76a4;vertical-align:middle;display:table-cell;position:relative;border:1px solid #d8dce5;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.custom-theme .el-input-group__append:focus,.custom-theme .el-input-group__prepend:focus{outline:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-select,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-select{display:inline-block;margin:-20px}.custom-theme .el-input-group__append button.el-button,.custom-theme .el-input-group__append div.el-select .el-input__inner,.custom-theme .el-input-group__append div.el-select:hover .el-input__inner,.custom-theme .el-input-group__prepend button.el-button,.custom-theme .el-input-group__prepend div.el-select .el-input__inner,.custom-theme .el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-input,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-input{font-size:inherit}.custom-theme .el-input-group__prepend{border-right:0;border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-input-group__append{border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--prepend .el-input__inner{border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--append .el-input__inner{border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-textarea{display:inline-block;width:100%;vertical-align:bottom}.custom-theme .el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;font-size:14px;color:#5a5e66;background-color:#fff;background-image:none;border:1px solid #d8dce5;border-radius:4px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.custom-theme .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:hover{border-color:#b4bccc}.custom-theme .el-textarea__inner:focus{outline:0;border-color:#262729}.custom-theme .el-textarea.is-disabled .el-textarea__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-popper .popper__arrow,.custom-theme .el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.custom-theme .el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03));filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.custom-theme .el-popper .popper__arrow::after{content:" ";border-width:6px}.custom-theme .el-popper[x-placement^=top]{margin-bottom:12px}.custom-theme .el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#e6ebf5;border-bottom-width:0}.custom-theme .el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#fff;border-bottom-width:0}.custom-theme .el-popper[x-placement^=bottom]{margin-top:12px}.custom-theme .el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#e6ebf5}.custom-theme .el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#fff}.custom-theme .el-popper[x-placement^=right]{margin-left:12px}.custom-theme .el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#e6ebf5;border-left-width:0}.custom-theme .el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#fff;border-left-width:0}.custom-theme .el-popper[x-placement^=left]{margin-right:12px}.custom-theme .el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#e6ebf5}.custom-theme .el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#fff}.custom-theme .el-cascader{display:inline-block;position:relative;font-size:14px;line-height:40px}.custom-theme .el-cascader .el-input,.custom-theme .el-cascader .el-input__inner{cursor:pointer}.custom-theme .el-cascader .el-input__icon{-webkit-transition:none;transition:none}.custom-theme .el-cascader .el-icon-arrow-down{-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;font-size:14px}.custom-theme .el-cascader .el-icon-arrow-down.is-reverse{-webkit-transform:rotateZ(180deg);transform:rotateZ(180deg)}.custom-theme .el-cascader .el-icon-circle-close{z-index:2;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1)}.custom-theme .el-cascader .el-icon-circle-close:hover{color:#878d99}.custom-theme .el-cascader__clearIcon{z-index:2;position:relative}.custom-theme .el-cascader__label{position:absolute;left:0;top:0;height:100%;padding:0 25px 0 15px;color:#5a5e66;width:100%;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;-webkit-box-sizing:border-box;box-sizing:border-box;cursor:pointer;text-align:left;font-size:inherit}.custom-theme .el-cascader__label span{color:#000}.custom-theme .el-cascader--medium{font-size:14px;line-height:36px}.custom-theme .el-cascader--small{font-size:13px;line-height:32px}.custom-theme .el-cascader--mini{font-size:12px;line-height:28px}.custom-theme .el-cascader.is-disabled .el-cascader__label{z-index:2;color:#b4bccc}.custom-theme .el-cascader-menus{white-space:nowrap;background:#fff;position:absolute;margin:5px 0;z-index:2;border:solid 1px #dfe4ed;border-radius:2px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.custom-theme .el-cascader-menus .popper__arrow{-webkit-transform:translateX(-400%);transform:translateX(-400%)}.custom-theme .el-cascader-menu{display:inline-block;vertical-align:top;height:204px;overflow:auto;border-right:solid 1px #dfe4ed;background-color:#fff;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:6px 0;min-width:160px}.custom-theme .el-cascader-menu:last-child{border-right:0}.custom-theme .el-cascader-menu__item{font-size:14px;padding:8px 20px;position:relative;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#5a5e66;height:34px;line-height:1.5;-webkit-box-sizing:border-box;box-sizing:border-box;cursor:pointer}.custom-theme .el-cascader-menu__item--extensible:after{font-family:element-icons;content:"\e604";font-size:14px;color:#bfcbd9;position:absolute;right:15px}.custom-theme .el-cascader-menu__item.is-disabled{color:#b4bccc;background-color:#fff;cursor:not-allowed}.custom-theme .el-cascader-menu__item.is-disabled:hover{background-color:#fff}.custom-theme .el-cascader-menu__item.is-active{color:#262729}.custom-theme .el-cascader-menu__item:hover{background-color:#f5f7fa}.custom-theme .el-cascader-menu__item.selected{color:#fff;background-color:#f5f7fa}.custom-theme .el-cascader-menu__item__keyword{font-weight:700}.custom-theme .el-cascader-menu--flexible{height:auto;max-height:180px;overflow:auto}.custom-theme .el-cascader-menu--flexible .el-cascader-menu__item{overflow:visible}.custom-theme .el-color-hue-slider{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:280px;height:12px;background-color:red;padding:0 2px}.custom-theme .el-color-hue-slider__bar{position:relative;background:-webkit-gradient(linear,left top,right top,from(red),color-stop(17%,#ff0),color-stop(33%,#0f0),color-stop(50%,#0ff),color-stop(67%,#00f),color-stop(83%,#f0f),to(red));background:linear-gradient(to right,red 0,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,red 100%);height:100%}.custom-theme .el-color-hue-slider__thumb{position:absolute;cursor:pointer;-webkit-box-sizing:border-box;box-sizing:border-box;left:0;top:0;width:4px;height:100%;border-radius:1px;background:#fff;border:1px solid #f0f0f0;-webkit-box-shadow:0 0 2px rgba(0,0,0,.6);box-shadow:0 0 2px rgba(0,0,0,.6);z-index:1}.custom-theme .el-color-hue-slider.is-vertical{width:12px;height:180px;padding:2px 0}.custom-theme .el-color-hue-slider.is-vertical .el-color-hue-slider__bar{background:-webkit-gradient(linear,left top,left bottom,from(red),color-stop(17%,#ff0),color-stop(33%,#0f0),color-stop(50%,#0ff),color-stop(67%,#00f),color-stop(83%,#f0f),to(red));background:linear-gradient(to bottom,red 0,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,red 100%)}.custom-theme .el-color-hue-slider.is-vertical .el-color-hue-slider__thumb{left:0;top:0;width:100%;height:4px}.custom-theme .el-color-svpanel{position:relative;width:280px;height:180px}.custom-theme .el-color-svpanel__black,.custom-theme .el-color-svpanel__white{position:absolute;top:0;left:0;right:0;bottom:0}.custom-theme .el-color-svpanel__white{background:-webkit-gradient(linear,left top,right top,from(#fff),to(rgba(255,255,255,0)));background:linear-gradient(to right,#fff,rgba(255,255,255,0))}.custom-theme .el-color-svpanel__black{background:-webkit-gradient(linear,left bottom,left top,from(#000),to(transparent));background:linear-gradient(to top,#000,transparent)}.custom-theme .el-color-svpanel__cursor{position:absolute}.custom-theme .el-color-svpanel__cursor>div{cursor:head;width:4px;height:4px;-webkit-box-shadow:0 0 0 1.5px #fff,inset 0 0 1px 1px rgba(0,0,0,.3),0 0 1px 2px rgba(0,0,0,.4);box-shadow:0 0 0 1.5px #fff,inset 0 0 1px 1px rgba(0,0,0,.3),0 0 1px 2px rgba(0,0,0,.4);border-radius:50%;-webkit-transform:translate(-2px,-2px);transform:translate(-2px,-2px)}.custom-theme .el-color-alpha-slider{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:280px;height:12px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.custom-theme .el-color-alpha-slider__bar{position:relative;background:-webkit-gradient(linear,left top,right top,from(rgba(255,255,255,0)),to(white));background:linear-gradient(to right,rgba(255,255,255,0) 0,#fff 100%);height:100%}.custom-theme .el-color-alpha-slider__thumb{position:absolute;cursor:pointer;-webkit-box-sizing:border-box;box-sizing:border-box;left:0;top:0;width:4px;height:100%;border-radius:1px;background:#fff;border:1px solid #f0f0f0;-webkit-box-shadow:0 0 2px rgba(0,0,0,.6);box-shadow:0 0 2px rgba(0,0,0,.6);z-index:1}.custom-theme .el-color-alpha-slider.is-vertical{width:20px;height:180px}.custom-theme .el-color-alpha-slider.is-vertical .el-color-alpha-slider__bar{background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0)),to(white));background:linear-gradient(to bottom,rgba(255,255,255,0) 0,#fff 100%)}.custom-theme .el-color-alpha-slider.is-vertical .el-color-alpha-slider__thumb{left:0;top:0;width:100%;height:4px}.custom-theme .el-color-dropdown{width:300px}.custom-theme .el-color-dropdown__main-wrapper{margin-bottom:6px}.custom-theme .el-color-dropdown__main-wrapper::after{content:"";display:table;clear:both}.custom-theme .el-color-dropdown__btns{margin-top:6px;text-align:right}.custom-theme .el-color-dropdown__value{float:left;line-height:26px;font-size:12px;color:#000;width:160px}.custom-theme .el-color-dropdown__btn{border:1px solid #dcdcdc;color:#333;line-height:24px;border-radius:2px;padding:0 20px;cursor:pointer;background-color:transparent;outline:0;font-size:12px}.custom-theme .el-color-dropdown__btn[disabled]{color:#ccc;cursor:not-allowed}.custom-theme .el-color-dropdown__btn:hover{color:#262729;border-color:#262729}.custom-theme .el-color-dropdown__link-btn{cursor:pointer;color:#262729;text-decoration:none;padding:15px;font-size:12px}.custom-theme .el-color-dropdown__link-btn:hover{color:tint(#262729,20%)}.custom-theme .el-color-picker{display:inline-block;position:relative;line-height:normal;height:40px}.custom-theme .el-color-picker.is-disabled .el-color-picker__trigger{cursor:not-allowed}.custom-theme .el-color-picker--medium{height:36px}.custom-theme .el-color-picker--medium .el-color-picker__trigger{height:36px;width:36px}.custom-theme .el-color-picker--medium .el-color-picker__mask{height:34px;width:34px}.custom-theme .el-color-picker--small{height:32px}.custom-theme .el-color-picker--small .el-color-picker__trigger{height:32px;width:32px}.custom-theme .el-color-picker--small .el-color-picker__mask{height:30px;width:30px}.custom-theme .el-color-picker--small .el-color-picker__empty,.custom-theme .el-color-picker--small .el-color-picker__icon{-webkit-transform:translate3d(-50%,-50%,0) scale(.8);transform:translate3d(-50%,-50%,0) scale(.8)}.custom-theme .el-color-picker--mini{height:28px}.custom-theme .el-color-picker--mini .el-color-picker__trigger{height:28px;width:28px}.custom-theme .el-color-picker--mini .el-color-picker__mask{height:26px;width:26px}.custom-theme .el-color-picker--mini .el-color-picker__empty,.custom-theme .el-color-picker--mini .el-color-picker__icon{-webkit-transform:translate3d(-50%,-50%,0) scale(.8);transform:translate3d(-50%,-50%,0) scale(.8)}.custom-theme .el-color-picker__mask{height:38px;width:38px;border-radius:4px;position:absolute;top:1px;left:1px;z-index:1;cursor:not-allowed;background-color:rgba(255,255,255,.7)}.custom-theme .el-color-picker__trigger{display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;height:40px;width:40px;padding:4px;border:1px solid #e6e6e6;border-radius:4px;font-size:0;position:relative;cursor:pointer}.custom-theme .el-color-picker__color{position:relative;display:block;-webkit-box-sizing:border-box;box-sizing:border-box;border:1px solid #999;border-radius:2px;width:100%;height:100%;text-align:center}.custom-theme .el-color-picker__color.is-alpha{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.custom-theme .el-color-picker__color-inner{position:absolute;left:0;top:0;right:0;bottom:0}.custom-theme .el-color-picker__empty{font-size:12px;color:#999;position:absolute;top:50%;left:50%;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0)}.custom-theme .el-color-picker__icon{display:inline-block;position:absolute;width:100%;top:50%;left:50%;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0);color:#fff;text-align:center;font-size:12px}.custom-theme .el-color-picker__panel{position:absolute;z-index:10;padding:6px;background-color:#fff;border:1px solid #e6ebf5;border-radius:4px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.custom-theme .el-input{position:relative;font-size:14px;display:inline-block;width:100%}.custom-theme .el-input::-webkit-scrollbar{z-index:11;width:6px}.custom-theme .el-input::-webkit-scrollbar:horizontal{height:6px}.custom-theme .el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.custom-theme .el-input::-webkit-scrollbar-corner{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track{background:#fff}.custom-theme .el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.custom-theme .el-input__inner{-webkit-appearance:none;background-color:#fff;background-image:none;border-radius:4px;border:1px solid #d8dce5;-webkit-box-sizing:border-box;box-sizing:border-box;color:#5a5e66;display:inline-block;font-size:inherit;height:40px;line-height:1;outline:0;padding:0 15px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.custom-theme .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input__inner:hover{border-color:#b4bccc}.custom-theme .el-input__inner:focus{outline:0;border-color:#262729}.custom-theme .el-input__suffix{position:absolute;height:100%;right:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s;pointer-events:none}.custom-theme .el-input__suffix-inner{pointer-events:all}.custom-theme .el-input__prefix{position:absolute;height:100%;left:5px;top:0;text-align:center;color:#b4bccc;-webkit-transition:all .3s;transition:all .3s}.custom-theme .el-input__icon{height:100%;width:25px;text-align:center;-webkit-transition:all .3s;transition:all .3s;line-height:40px}.custom-theme .el-input__icon:after{content:'';height:100%;width:0;display:inline-block;vertical-align:middle}.custom-theme .el-input__validateIcon{pointer-events:none}.custom-theme .el-input.is-active .el-input__inner{outline:0;border-color:#262729}.custom-theme .el-input.is-disabled .el-input__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-input.is-disabled .el-input__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__inner::placeholder{color:#b4bccc}.custom-theme .el-input.is-disabled .el-input__icon{cursor:not-allowed}.custom-theme .el-input--suffix .el-input__inner{padding-right:30px}.custom-theme .el-input--prefix .el-input__inner{padding-left:30px}.custom-theme .el-input--medium{font-size:14px}.custom-theme .el-input--medium .el-input__inner{height:36px}.custom-theme .el-input--medium .el-input__icon{line-height:36px}.custom-theme .el-input--small{font-size:13px}.custom-theme .el-input--small .el-input__inner{height:32px}.custom-theme .el-input--small .el-input__icon{line-height:32px}.custom-theme .el-input--mini{font-size:12px}.custom-theme .el-input--mini .el-input__inner{height:28px}.custom-theme .el-input--mini .el-input__icon{line-height:28px}.custom-theme .el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate}.custom-theme .el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.custom-theme .el-input-group__append,.custom-theme .el-input-group__prepend{background-color:#f5f7fa;color:#0a76a4;vertical-align:middle;display:table-cell;position:relative;border:1px solid #d8dce5;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.custom-theme .el-input-group__append:focus,.custom-theme .el-input-group__prepend:focus{outline:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-select,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-select{display:inline-block;margin:-20px}.custom-theme .el-input-group__append button.el-button,.custom-theme .el-input-group__append div.el-select .el-input__inner,.custom-theme .el-input-group__append div.el-select:hover .el-input__inner,.custom-theme .el-input-group__prepend button.el-button,.custom-theme .el-input-group__prepend div.el-select .el-input__inner,.custom-theme .el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.custom-theme .el-input-group__append .el-button,.custom-theme .el-input-group__append .el-input,.custom-theme .el-input-group__prepend .el-button,.custom-theme .el-input-group__prepend .el-input{font-size:inherit}.custom-theme .el-input-group__prepend{border-right:0;border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-input-group__append{border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--prepend .el-input__inner{border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-input-group--append .el-input__inner{border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-textarea{display:inline-block;width:100%;vertical-align:bottom}.custom-theme .el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;font-size:14px;color:#5a5e66;background-color:#fff;background-image:none;border:1px solid #d8dce5;border-radius:4px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.custom-theme .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-textarea__inner:hover{border-color:#b4bccc}.custom-theme .el-textarea__inner:focus{outline:0;border-color:#262729}.custom-theme .el-textarea.is-disabled .el-textarea__inner{background-color:#f5f7fa;border-color:#dfe4ed;color:#b4bccc;cursor:not-allowed}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-webkit-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::-ms-input-placeholder{color:#b4bccc}.custom-theme .el-textarea.is-disabled .el-textarea__inner::placeholder{color:#b4bccc}.custom-theme .el-button{display:inline-block;line-height:1;white-space:nowrap;cursor:pointer;background:#fff;border:1px solid #d8dce5;border-color:#d8dce5;color:#5a5e66;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;-webkit-transition:.1s;transition:.1s;font-weight:500;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;padding:12px 20px;font-size:14px;border-radius:4px}.custom-theme .el-button+.el-button{margin-left:10px}.custom-theme .el-button.is-round{padding:12px 20px}.custom-theme .el-button:focus,.custom-theme .el-button:hover{color:#262729;border-color:#bebebf;background-color:#e9e9ea}.custom-theme .el-button:active{color:#222325;border-color:#222325;outline:0}.custom-theme .el-button::-moz-focus-inner{border:0}.custom-theme .el-button [class*=el-icon-]+span{margin-left:5px}.custom-theme .el-button.is-plain:focus,.custom-theme .el-button.is-plain:hover{background:#fff;border-color:#262729;color:#262729}.custom-theme .el-button.is-plain:active{background:#fff;border-color:#222325;color:#222325;outline:0}.custom-theme .el-button.is-active{color:#222325;border-color:#222325}.custom-theme .el-button.is-disabled,.custom-theme .el-button.is-disabled:focus,.custom-theme .el-button.is-disabled:hover{color:#b4bccc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#e6ebf5}.custom-theme .el-button.is-disabled.el-button--text{background-color:transparent}.custom-theme .el-button.is-disabled.is-plain,.custom-theme .el-button.is-disabled.is-plain:focus,.custom-theme .el-button.is-disabled.is-plain:hover{background-color:#fff;border-color:#e6ebf5;color:#b4bccc}.custom-theme .el-button.is-loading{position:relative;pointer-events:none}.custom-theme .el-button.is-loading:before{pointer-events:none;content:'';position:absolute;left:-1px;top:-1px;right:-1px;bottom:-1px;border-radius:inherit;background-color:rgba(255,255,255,.35)}.custom-theme .el-button.is-round{border-radius:20px;padding:12px 23px}.custom-theme .el-button--primary{color:#fff;background-color:#262729;border-color:#262729}.custom-theme .el-button--primary:focus,.custom-theme .el-button--primary:hover{background:#515254;border-color:#515254;color:#fff}.custom-theme .el-button--primary:active{background:#222325;border-color:#222325;color:#fff;outline:0}.custom-theme .el-button--primary.is-active{background:#222325;border-color:#222325;color:#fff}.custom-theme .el-button--primary.is-disabled,.custom-theme .el-button--primary.is-disabled:active,.custom-theme .el-button--primary.is-disabled:focus,.custom-theme .el-button--primary.is-disabled:hover{color:#fff;background-color:#939394;border-color:#939394}.custom-theme .el-button--primary.is-plain{color:#262729;background:#e9e9ea;border-color:#a8a9a9}.custom-theme .el-button--primary.is-plain:focus,.custom-theme .el-button--primary.is-plain:hover{background:#262729;border-color:#262729;color:#fff}.custom-theme .el-button--primary.is-plain:active{background:#222325;border-color:#222325;color:#fff;outline:0}.custom-theme .el-button--primary.is-plain.is-disabled,.custom-theme .el-button--primary.is-plain.is-disabled:active,.custom-theme .el-button--primary.is-plain.is-disabled:focus,.custom-theme .el-button--primary.is-plain.is-disabled:hover{color:#7d7d7f;background-color:#e9e9ea;border-color:#d4d4d4}.custom-theme .el-button--success{color:#fff;background-color:#409167;border-color:#409167}.custom-theme .el-button--success:focus,.custom-theme .el-button--success:hover{background:#66a785;border-color:#66a785;color:#fff}.custom-theme .el-button--success:active{background:#3a835d;border-color:#3a835d;color:#fff;outline:0}.custom-theme .el-button--success.is-active{background:#3a835d;border-color:#3a835d;color:#fff}.custom-theme .el-button--success.is-disabled,.custom-theme .el-button--success.is-disabled:active,.custom-theme .el-button--success.is-disabled:focus,.custom-theme .el-button--success.is-disabled:hover{color:#fff;background-color:#a0c8b3;border-color:#a0c8b3}.custom-theme .el-button--success.is-plain{color:#409167;background:#ecf4f0;border-color:#b3d3c2}.custom-theme .el-button--success.is-plain:focus,.custom-theme .el-button--success.is-plain:hover{background:#409167;border-color:#409167;color:#fff}.custom-theme .el-button--success.is-plain:active{background:#3a835d;border-color:#3a835d;color:#fff;outline:0}.custom-theme .el-button--success.is-plain.is-disabled,.custom-theme .el-button--success.is-plain.is-disabled:active,.custom-theme .el-button--success.is-plain.is-disabled:focus,.custom-theme .el-button--success.is-plain.is-disabled:hover{color:#8cbda4;background-color:#ecf4f0;border-color:#d9e9e1}.custom-theme .el-button--warning{color:#fff;background-color:#9da408;border-color:#9da408}.custom-theme .el-button--warning:focus,.custom-theme .el-button--warning:hover{background:#b1b639;border-color:#b1b639;color:#fff}.custom-theme .el-button--warning:active{background:#8d9407;border-color:#8d9407;color:#fff;outline:0}.custom-theme .el-button--warning.is-active{background:#8d9407;border-color:#8d9407;color:#fff}.custom-theme .el-button--warning.is-disabled,.custom-theme .el-button--warning.is-disabled:active,.custom-theme .el-button--warning.is-disabled:focus,.custom-theme .el-button--warning.is-disabled:hover{color:#fff;background-color:#ced284;border-color:#ced284}.custom-theme .el-button--warning.is-plain{color:#9da408;background:#f5f6e6;border-color:#d8db9c}.custom-theme .el-button--warning.is-plain:focus,.custom-theme .el-button--warning.is-plain:hover{background:#9da408;border-color:#9da408;color:#fff}.custom-theme .el-button--warning.is-plain:active{background:#8d9407;border-color:#8d9407;color:#fff;outline:0}.custom-theme .el-button--warning.is-plain.is-disabled,.custom-theme .el-button--warning.is-plain.is-disabled:active,.custom-theme .el-button--warning.is-plain.is-disabled:focus,.custom-theme .el-button--warning.is-plain.is-disabled:hover{color:#c4c86b;background-color:#f5f6e6;border-color:#ebedce}.custom-theme .el-button--danger{color:#fff;background-color:#b3450e;border-color:#b3450e}.custom-theme .el-button--danger:focus,.custom-theme .el-button--danger:hover{background:#c26a3e;border-color:#c26a3e;color:#fff}.custom-theme .el-button--danger:active{background:#a13e0d;border-color:#a13e0d;color:#fff;outline:0}.custom-theme .el-button--danger.is-active{background:#a13e0d;border-color:#a13e0d;color:#fff}.custom-theme .el-button--danger.is-disabled,.custom-theme .el-button--danger.is-disabled:active,.custom-theme .el-button--danger.is-disabled:focus,.custom-theme .el-button--danger.is-disabled:hover{color:#fff;background-color:#d9a287;border-color:#d9a287}.custom-theme .el-button--danger.is-plain{color:#b3450e;background:#f7ece7;border-color:#e1b59f}.custom-theme .el-button--danger.is-plain:focus,.custom-theme .el-button--danger.is-plain:hover{background:#b3450e;border-color:#b3450e;color:#fff}.custom-theme .el-button--danger.is-plain:active{background:#a13e0d;border-color:#a13e0d;color:#fff;outline:0}.custom-theme .el-button--danger.is-plain.is-disabled,.custom-theme .el-button--danger.is-plain.is-disabled:active,.custom-theme .el-button--danger.is-plain.is-disabled:focus,.custom-theme .el-button--danger.is-plain.is-disabled:hover{color:#d18f6e;background-color:#f7ece7;border-color:#f0dacf}.custom-theme .el-button--info{color:#fff;background-color:#0a76a4;border-color:#0a76a4}.custom-theme .el-button--info:focus,.custom-theme .el-button--info:hover{background:#3b91b6;border-color:#3b91b6;color:#fff}.custom-theme .el-button--info:active{background:#096a94;border-color:#096a94;color:#fff;outline:0}.custom-theme .el-button--info.is-active{background:#096a94;border-color:#096a94;color:#fff}.custom-theme .el-button--info.is-disabled,.custom-theme .el-button--info.is-disabled:active,.custom-theme .el-button--info.is-disabled:focus,.custom-theme .el-button--info.is-disabled:hover{color:#fff;background-color:#85bbd2;border-color:#85bbd2}.custom-theme .el-button--info.is-plain{color:#0a76a4;background:#e7f1f6;border-color:#9dc8db}.custom-theme .el-button--info.is-plain:focus,.custom-theme .el-button--info.is-plain:hover{background:#0a76a4;border-color:#0a76a4;color:#fff}.custom-theme .el-button--info.is-plain:active{background:#096a94;border-color:#096a94;color:#fff;outline:0}.custom-theme .el-button--info.is-plain.is-disabled,.custom-theme .el-button--info.is-plain.is-disabled:active,.custom-theme .el-button--info.is-plain.is-disabled:focus,.custom-theme .el-button--info.is-plain.is-disabled:hover{color:#6cadc8;background-color:#e7f1f6;border-color:#cee4ed}.custom-theme .el-button--medium{padding:10px 20px;font-size:14px;border-radius:4px}.custom-theme .el-button--medium.is-round{padding:10px 20px}.custom-theme .el-button--small{padding:9px 15px;font-size:12px;border-radius:3px}.custom-theme .el-button--small.is-round{padding:9px 15px}.custom-theme .el-button--mini{padding:7px 15px;font-size:12px;border-radius:3px}.custom-theme .el-button--mini.is-round{padding:7px 15px}.custom-theme .el-button--text{border:none;color:#262729;background:0 0;padding-left:0;padding-right:0}.custom-theme .el-button--text:focus,.custom-theme .el-button--text:hover{color:#515254;border-color:transparent;background-color:transparent}.custom-theme .el-button--text:active{color:#222325;border-color:transparent;background-color:transparent}.custom-theme .el-button-group{display:inline-block;vertical-align:middle}.custom-theme .el-button-group::after,.custom-theme .el-button-group::before{display:table;content:""}.custom-theme .el-button-group::after{clear:both}.custom-theme .el-button-group .el-button{float:left;position:relative}.custom-theme .el-button-group .el-button+.el-button{margin-left:0}.custom-theme .el-button-group .el-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.custom-theme .el-button-group .el-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.custom-theme .el-button-group .el-button:not(:first-child):not(:last-child){border-radius:0}.custom-theme .el-button-group .el-button:not(:last-child){margin-right:-1px}.custom-theme .el-button-group .el-button:active,.custom-theme .el-button-group .el-button:focus,.custom-theme .el-button-group .el-button:hover{z-index:1}.custom-theme .el-button-group .el-button.is-active{z-index:1}.custom-theme .el-button-group .el-button--primary:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--primary:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--primary:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--success:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--success:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--success:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--warning:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--warning:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--warning:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--danger:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--danger:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--danger:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--info:first-child{border-right-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--info:last-child{border-left-color:rgba(255,255,255,.5)}.custom-theme .el-button-group .el-button--info:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.custom-theme .el-checkbox{color:#5a5e66;font-weight:500;font-size:14px;position:relative;cursor:pointer;display:inline-block;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.custom-theme .el-checkbox.is-bordered{padding:9px 20px 9px 10px;border-radius:4px;border:1px solid #d8dce5}.custom-theme .el-checkbox.is-bordered.is-checked{border-color:#262729}.custom-theme .el-checkbox.is-bordered.is-disabled{border-color:#e6ebf5;cursor:not-allowed}.custom-theme .el-checkbox.is-bordered+.el-checkbox.is-bordered{margin-left:10px}.custom-theme .el-checkbox.is-bordered.el-checkbox--medium{padding:7px 20px 7px 10px;border-radius:4px}.custom-theme .el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__label{line-height:17px;font-size:14px}.custom-theme .el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__inner{height:14px;width:14px}.custom-theme .el-checkbox.is-bordered.el-checkbox--small{padding:3px 15px 7px 10px;border-radius:3px}.custom-theme .el-checkbox.is-bordered.el-checkbox--small .el-checkbox__label{line-height:15px;font-size:12px}.custom-theme .el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner{height:12px;width:12px}.custom-theme .el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner::after{height:6px;width:2px}.custom-theme .el-checkbox.is-bordered.el-checkbox--mini{padding:1px 15px 5px 10px;border-radius:3px}.custom-theme .el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__label{line-height:12px;font-size:12px}.custom-theme .el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner{height:12px;width:12px}.custom-theme .el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner::after{height:6px;width:2px}.custom-theme .el-checkbox__input{white-space:nowrap;cursor:pointer;outline:0;display:inline-block;line-height:1;position:relative;vertical-align:middle}.custom-theme .el-checkbox__input.is-disabled .el-checkbox__inner{background-color:#edf2fc;border-color:#d8dce5;cursor:not-allowed}.custom-theme .el-checkbox__input.is-disabled .el-checkbox__inner::after{cursor:not-allowed;border-color:#b4bccc}.custom-theme .el-checkbox__input.is-disabled .el-checkbox__inner+.el-checkbox__label{cursor:not-allowed}.custom-theme .el-checkbox__input.is-disabled.is-checked .el-checkbox__inner{background-color:#edf2fc;border-color:#d8dce5}.custom-theme .el-checkbox__input.is-disabled.is-checked .el-checkbox__inner::after{border-color:#b4bccc}.custom-theme .el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner{background-color:#edf2fc;border-color:#d8dce5}.custom-theme .el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner::before{background-color:#b4bccc;border-color:#b4bccc}.custom-theme .el-checkbox__input.is-disabled+span.el-checkbox__label{color:#b4bccc;cursor:not-allowed}.custom-theme .el-checkbox__input.is-checked .el-checkbox__inner{background-color:#262729;border-color:#262729}.custom-theme .el-checkbox__input.is-checked .el-checkbox__inner::after{-webkit-transform:rotate(45deg) scaleY(1);transform:rotate(45deg) scaleY(1)}.custom-theme .el-checkbox__input.is-checked+.el-checkbox__label{color:#262729}.custom-theme .el-checkbox__input.is-focus .el-checkbox__inner{border-color:#262729}.custom-theme .el-checkbox__input.is-indeterminate .el-checkbox__inner{background-color:#262729;border-color:#262729}.custom-theme .el-checkbox__input.is-indeterminate .el-checkbox__inner::before{content:'';position:absolute;display:block;background-color:#fff;height:2px;-webkit-transform:scale(.5);transform:scale(.5);left:0;right:0;top:5px}.custom-theme .el-checkbox__input.is-indeterminate .el-checkbox__inner::after{display:none}.custom-theme .el-checkbox__inner{display:inline-block;position:relative;border:1px solid #d8dce5;border-radius:2px;-webkit-box-sizing:border-box;box-sizing:border-box;width:14px;height:14px;background-color:#fff;z-index:1;-webkit-transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46);transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46)}.custom-theme .el-checkbox__inner:hover{border-color:#262729}.custom-theme .el-checkbox__inner::after{-webkit-box-sizing:content-box;box-sizing:content-box;content:"";border:1px solid #fff;border-left:0;border-top:0;height:7px;left:4px;position:absolute;top:1px;-webkit-transform:rotate(45deg) scaleY(0);transform:rotate(45deg) scaleY(0);width:3px;-webkit-transition:-webkit-transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms;transition:-webkit-transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms;transition:transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms;transition:transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms,-webkit-transform .15s cubic-bezier(.71,-.46,.88,.6) 50ms;-webkit-transform-origin:center;transform-origin:center}.custom-theme .el-checkbox__original{opacity:0;outline:0;position:absolute;margin:0;width:0;height:0;left:-999px}.custom-theme .el-checkbox__label{display:inline-block;padding-left:10px;line-height:19px;font-size:14px}.custom-theme .el-checkbox+.el-checkbox{margin-left:30px}.custom-theme .el-checkbox-button{position:relative;display:inline-block}.custom-theme .el-checkbox-button__inner{display:inline-block;line-height:1;font-weight:500;white-space:nowrap;vertical-align:middle;cursor:pointer;background:#fff;border:1px solid #d8dce5;border-left:0;color:#5a5e66;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;position:relative;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;padding:12px 20px;font-size:14px;border-radius:0}.custom-theme .el-checkbox-button__inner.is-round{padding:12px 20px}.custom-theme .el-checkbox-button__inner:hover{color:#262729}.custom-theme .el-checkbox-button__inner [class*=el-icon-]{line-height:.9}.custom-theme .el-checkbox-button__inner [class*=el-icon-]+span{margin-left:5px}.custom-theme .el-checkbox-button__original{opacity:0;outline:0;position:absolute;margin:0;left:-999px}.custom-theme .el-checkbox-button.is-checked .el-checkbox-button__inner{color:#fff;background-color:#262729;border-color:#262729;-webkit-box-shadow:-1px 0 0 0 #7d7d7f;box-shadow:-1px 0 0 0 #7d7d7f}.custom-theme .el-checkbox-button.is-disabled .el-checkbox-button__inner{color:#b4bccc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#e6ebf5;-webkit-box-shadow:none;box-shadow:none}.custom-theme .el-checkbox-button:first-child .el-checkbox-button__inner{border-left:1px solid #d8dce5;border-radius:4px 0 0 4px;-webkit-box-shadow:none!important;box-shadow:none!important}.custom-theme .el-checkbox-button.is-focus .el-checkbox-button__inner{border-color:#262729}.custom-theme .el-checkbox-button:last-child .el-checkbox-button__inner{border-radius:0 4px 4px 0}.custom-theme .el-checkbox-button--medium .el-checkbox-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.custom-theme .el-checkbox-button--medium .el-checkbox-button__inner.is-round{padding:10px 20px}.custom-theme .el-checkbox-button--small .el-checkbox-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.custom-theme .el-checkbox-button--small .el-checkbox-button__inner.is-round{padding:9px 15px}.custom-theme .el-checkbox-button--mini .el-checkbox-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.custom-theme .el-checkbox-button--mini .el-checkbox-button__inner.is-round{padding:7px 15px}.custom-theme .el-checkbox-group{font-size:0}.custom-theme .el-transfer{font-size:14px}.custom-theme .el-transfer__buttons{display:inline-block;vertical-align:middle;padding:0 30px}.custom-theme .el-transfer__button{display:block;margin:0 auto;padding:10px;border-radius:50%;color:#fff;background-color:#262729;font-size:0}.custom-theme .el-transfer__button.is-with-texts{border-radius:4px}.custom-theme .el-transfer__button.is-disabled{border:1px solid #d8dce5;background-color:#f5f7fa;color:#b4bccc}.custom-theme .el-transfer__button.is-disabled:hover{border:1px solid #d8dce5;background-color:#f5f7fa;color:#b4bccc}.custom-theme .el-transfer__button:first-child{margin-bottom:10px}.custom-theme .el-transfer__button:nth-child(2){margin:0}.custom-theme .el-transfer__button i,.custom-theme .el-transfer__button span{font-size:14px}.custom-theme .el-transfer__button [class*=el-icon-]+span{margin-left:0}.custom-theme .el-transfer-panel{border:1px solid #e6ebf5;border-radius:4px;overflow:hidden;background:#fff;display:inline-block;vertical-align:middle;width:200px;-webkit-box-sizing:border-box;box-sizing:border-box;position:relative}.custom-theme .el-transfer-panel__body{height:246px}.custom-theme .el-transfer-panel__body.is-with-footer{padding-bottom:40px}.custom-theme .el-transfer-panel__list{margin:0;padding:6px 0;list-style:none;height:246px;overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-transfer-panel__list.is-filterable{height:194px;padding-top:0}.custom-theme .el-transfer-panel__item{height:30px;line-height:30px;padding-left:15px;display:block}.custom-theme .el-transfer-panel__item+.el-transfer-panel__item{margin-left:0}.custom-theme .el-transfer-panel__item.el-checkbox{color:#5a5e66}.custom-theme .el-transfer-panel__item:hover{color:#262729}.custom-theme .el-transfer-panel__item.el-checkbox .el-checkbox__label{width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;-webkit-box-sizing:border-box;box-sizing:border-box;padding-left:24px;line-height:30px}.custom-theme .el-transfer-panel__item .el-checkbox__input{position:absolute;top:8px}.custom-theme .el-transfer-panel__filter{text-align:center;margin:15px;-webkit-box-sizing:border-box;box-sizing:border-box;display:block;width:auto}.custom-theme .el-transfer-panel__filter .el-input__inner{height:32px;width:100%;font-size:12px;display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:16px;padding-right:10px;padding-left:30px}.custom-theme .el-transfer-panel__filter .el-input__icon{margin-left:5px}.custom-theme .el-transfer-panel__filter .el-icon-circle-close{cursor:pointer}.custom-theme .el-transfer-panel .el-transfer-panel__header{height:40px;line-height:40px;background:#f5f7fa;margin:0;padding-left:15px;border-bottom:1px solid #e6ebf5;-webkit-box-sizing:border-box;box-sizing:border-box;color:#000}.custom-theme .el-transfer-panel .el-transfer-panel__header .el-checkbox{display:block;line-height:40px}.custom-theme .el-transfer-panel .el-transfer-panel__header .el-checkbox .el-checkbox__label{font-size:16px;color:#2d2f33;font-weight:400}.custom-theme .el-transfer-panel .el-transfer-panel__header .el-checkbox .el-checkbox__label span{position:absolute;right:15px;color:#878d99;font-size:12px;font-weight:400}.custom-theme .el-transfer-panel .el-transfer-panel__footer{height:40px;background:#fff;margin:0;padding:0;border-top:1px solid #e6ebf5;position:absolute;bottom:0;left:0;width:100%;z-index:1}.custom-theme .el-transfer-panel .el-transfer-panel__footer::after{display:inline-block;content:"";height:100%;vertical-align:middle}.custom-theme .el-transfer-panel .el-transfer-panel__footer .el-checkbox{padding-left:20px;color:#5a5e66}.custom-theme .el-transfer-panel .el-transfer-panel__empty{margin:0;height:30px;line-height:30px;padding:6px 15px 0;color:#878d99}.custom-theme .el-transfer-panel .el-checkbox__label{padding-left:8px}.custom-theme .el-transfer-panel .el-checkbox__inner{height:14px;width:14px;border-radius:3px}.custom-theme .el-transfer-panel .el-checkbox__inner::after{height:6px;width:3px;left:4px}.custom-theme .el-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-container.is-vertical{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.custom-theme .el-header{padding:0 20px;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-aside{overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box}.custom-theme .el-main{-webkit-box-flex:1;-ms-flex:1;flex:1;overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box;padding:20px}.custom-theme .el-footer{padding:0 20px;-webkit-box-sizing:border-box;box-sizing:border-box}
\ No newline at end of file
diff --git a/frontend/src/components/BackToTop/index.vue b/frontend/src/components/BackToTop/index.vue
new file mode 100644
index 0000000..36522f4
--- /dev/null
+++ b/frontend/src/components/BackToTop/index.vue
@@ -0,0 +1,111 @@
+<template>
+  <transition :name="transitionName">
+    <div v-show="visible" :style="customStyle" class="back-to-ceiling" @click="backToTop">
+      <svg width="16" height="16" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg" class="Icon Icon--backToTopArrow" aria-hidden="true" style="height:16px;width:16px"><path d="M12.036 15.59a1 1 0 0 1-.997.995H5.032a.996.996 0 0 1-.997-.996V8.584H1.03c-1.1 0-1.36-.633-.578-1.416L7.33.29a1.003 1.003 0 0 1 1.412 0l6.878 6.88c.782.78.523 1.415-.58 1.415h-3.004v7.004z" /></svg>
+    </div>
+  </transition>
+</template>
+
+<script>
+export default {
+  name: 'BackToTop',
+  props: {
+    visibilityHeight: {
+      type: Number,
+      default: 400
+    },
+    backPosition: {
+      type: Number,
+      default: 0
+    },
+    customStyle: {
+      type: Object,
+      default: function() {
+        return {
+          right: '50px',
+          bottom: '50px',
+          width: '40px',
+          height: '40px',
+          'border-radius': '4px',
+          'line-height': '45px',
+          background: '#e7eaf1'
+        }
+      }
+    },
+    transitionName: {
+      type: String,
+      default: 'fade'
+    }
+  },
+  data() {
+    return {
+      visible: false,
+      interval: null,
+      isMoving: false
+    }
+  },
+  mounted() {
+    window.addEventListener('scroll', this.handleScroll)
+  },
+  beforeDestroy() {
+    window.removeEventListener('scroll', this.handleScroll)
+    if (this.interval) {
+      clearInterval(this.interval)
+    }
+  },
+  methods: {
+    handleScroll() {
+      this.visible = window.pageYOffset > this.visibilityHeight
+    },
+    backToTop() {
+      if (this.isMoving) return
+      const start = window.pageYOffset
+      let i = 0
+      this.isMoving = true
+      this.interval = setInterval(() => {
+        const next = Math.floor(this.easeInOutQuad(10 * i, start, -start, 500))
+        if (next <= this.backPosition) {
+          window.scrollTo(0, this.backPosition)
+          clearInterval(this.interval)
+          this.isMoving = false
+        } else {
+          window.scrollTo(0, next)
+        }
+        i++
+      }, 16.7)
+    },
+    easeInOutQuad(t, b, c, d) {
+      if ((t /= d / 2) < 1) return c / 2 * t * t + b
+      return -c / 2 * (--t * (t - 2) - 1) + b
+    }
+  }
+}
+</script>
+
+<style scoped>
+.back-to-ceiling {
+  position: fixed;
+  display: inline-block;
+  text-align: center;
+  cursor: pointer;
+}
+
+.back-to-ceiling:hover {
+  background: #d5dbe7;
+}
+
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity .5s;
+}
+
+.fade-enter,
+.fade-leave-to {
+  opacity: 0
+}
+
+.back-to-ceiling .Icon {
+  fill: #9aaabf;
+  background: none;
+}
+</style>
diff --git a/frontend/src/components/Breadcrumb/index.vue b/frontend/src/components/Breadcrumb/index.vue
new file mode 100644
index 0000000..e224ff7
--- /dev/null
+++ b/frontend/src/components/Breadcrumb/index.vue
@@ -0,0 +1,82 @@
+<template>
+  <el-breadcrumb class="app-breadcrumb" separator="/">
+    <transition-group name="breadcrumb">
+      <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
+        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
+        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
+      </el-breadcrumb-item>
+    </transition-group>
+  </el-breadcrumb>
+</template>
+
+<script>
+import pathToRegexp from 'path-to-regexp'
+
+export default {
+  data() {
+    return {
+      levelList: null
+    }
+  },
+  watch: {
+    $route(route) {
+      // if you go to the redirect page, do not update the breadcrumbs
+      if (route.path.startsWith('/redirect/')) {
+        return
+      }
+      this.getBreadcrumb()
+    }
+  },
+  created() {
+    this.getBreadcrumb()
+  },
+  methods: {
+    getBreadcrumb() {
+      // only show routes with meta.title
+      let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
+      const first = matched[0]
+
+      if (!this.isDashboard(first)) {
+        matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
+      }
+
+      this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
+    },
+    isDashboard(route) {
+      const name = route && route.name
+      if (!name) {
+        return false
+      }
+      return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
+    },
+    pathCompile(path) {
+      // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
+      const { params } = this.$route
+      var toPath = pathToRegexp.compile(path)
+      return toPath(params)
+    },
+    handleLink(item) {
+      const { redirect, path } = item
+      if (redirect) {
+        this.$router.push(redirect)
+        return
+      }
+      this.$router.push(this.pathCompile(path))
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-breadcrumb.el-breadcrumb {
+  display: inline-block;
+  font-size: 14px;
+  line-height: 50px;
+  margin-left: 8px;
+
+  .no-redirect {
+    color: #97a8be;
+    cursor: text;
+  }
+}
+</style>
diff --git a/frontend/src/components/Charts/Keyboard.vue b/frontend/src/components/Charts/Keyboard.vue
new file mode 100644
index 0000000..0b258f3
--- /dev/null
+++ b/frontend/src/components/Charts/Keyboard.vue
@@ -0,0 +1,155 @@
+<template>
+  <div :id="id" :class="className" :style="{height:height,width:width}" />
+</template>
+
+<script>
+import echarts from 'echarts'
+import resize from './mixins/resize'
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    id: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '200px'
+    },
+    height: {
+      type: String,
+      default: '200px'
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  mounted() {
+    this.initChart()
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(document.getElementById(this.id))
+
+      const xAxisData = []
+      const data = []
+      const data2 = []
+      for (let i = 0; i < 50; i++) {
+        xAxisData.push(i)
+        data.push((Math.sin(i / 5) * (i / 5 - 10) + i / 6) * 5)
+        data2.push((Math.sin(i / 5) * (i / 5 + 10) + i / 6) * 3)
+      }
+      this.chart.setOption({
+        backgroundColor: '#08263a',
+        grid: {
+          left: '5%',
+          right: '5%'
+        },
+        xAxis: [{
+          show: false,
+          data: xAxisData
+        }, {
+          show: false,
+          data: xAxisData
+        }],
+        visualMap: {
+          show: false,
+          min: 0,
+          max: 50,
+          dimension: 0,
+          inRange: {
+            color: ['#4a657a', '#308e92', '#b1cfa5', '#f5d69f', '#f5898b', '#ef5055']
+          }
+        },
+        yAxis: {
+          axisLine: {
+            show: false
+          },
+          axisLabel: {
+            textStyle: {
+              color: '#4a657a'
+            }
+          },
+          splitLine: {
+            show: true,
+            lineStyle: {
+              color: '#08263f'
+            }
+          },
+          axisTick: {
+            show: false
+          }
+        },
+        series: [{
+          name: 'back',
+          type: 'bar',
+          data: data2,
+          z: 1,
+          itemStyle: {
+            normal: {
+              opacity: 0.4,
+              barBorderRadius: 5,
+              shadowBlur: 3,
+              shadowColor: '#111'
+            }
+          }
+        }, {
+          name: 'Simulate Shadow',
+          type: 'line',
+          data,
+          z: 2,
+          showSymbol: false,
+          animationDelay: 0,
+          animationEasing: 'linear',
+          animationDuration: 1200,
+          lineStyle: {
+            normal: {
+              color: 'transparent'
+            }
+          },
+          areaStyle: {
+            normal: {
+              color: '#08263a',
+              shadowBlur: 50,
+              shadowColor: '#000'
+            }
+          }
+        }, {
+          name: 'front',
+          type: 'bar',
+          data,
+          xAxisIndex: 1,
+          z: 3,
+          itemStyle: {
+            normal: {
+              barBorderRadius: 5
+            }
+          }
+        }],
+        animationEasing: 'elasticOut',
+        animationEasingUpdate: 'elasticOut',
+        animationDelay(idx) {
+          return idx * 20
+        },
+        animationDelayUpdate(idx) {
+          return idx * 20
+        }
+      })
+    }
+  }
+}
+</script>
diff --git a/frontend/src/components/Charts/LineMarker.vue b/frontend/src/components/Charts/LineMarker.vue
new file mode 100644
index 0000000..3dd7436
--- /dev/null
+++ b/frontend/src/components/Charts/LineMarker.vue
@@ -0,0 +1,227 @@
+<template>
+  <div :id="id" :class="className" :style="{height:height,width:width}" />
+</template>
+
+<script>
+import echarts from 'echarts'
+import resize from './mixins/resize'
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    id: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '200px'
+    },
+    height: {
+      type: String,
+      default: '200px'
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  mounted() {
+    this.initChart()
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(document.getElementById(this.id))
+
+      this.chart.setOption({
+        backgroundColor: '#394056',
+        title: {
+          top: 20,
+          text: 'Requests',
+          textStyle: {
+            fontWeight: 'normal',
+            fontSize: 16,
+            color: '#F1F1F3'
+          },
+          left: '1%'
+        },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            lineStyle: {
+              color: '#57617B'
+            }
+          }
+        },
+        legend: {
+          top: 20,
+          icon: 'rect',
+          itemWidth: 14,
+          itemHeight: 5,
+          itemGap: 13,
+          data: ['CMCC', 'CTCC', 'CUCC'],
+          right: '4%',
+          textStyle: {
+            fontSize: 12,
+            color: '#F1F1F3'
+          }
+        },
+        grid: {
+          top: 100,
+          left: '2%',
+          right: '2%',
+          bottom: '2%',
+          containLabel: true
+        },
+        xAxis: [{
+          type: 'category',
+          boundaryGap: false,
+          axisLine: {
+            lineStyle: {
+              color: '#57617B'
+            }
+          },
+          data: ['13:00', '13:05', '13:10', '13:15', '13:20', '13:25', '13:30', '13:35', '13:40', '13:45', '13:50', '13:55']
+        }],
+        yAxis: [{
+          type: 'value',
+          name: '(%)',
+          axisTick: {
+            show: false
+          },
+          axisLine: {
+            lineStyle: {
+              color: '#57617B'
+            }
+          },
+          axisLabel: {
+            margin: 10,
+            textStyle: {
+              fontSize: 14
+            }
+          },
+          splitLine: {
+            lineStyle: {
+              color: '#57617B'
+            }
+          }
+        }],
+        series: [{
+          name: 'CMCC',
+          type: 'line',
+          smooth: true,
+          symbol: 'circle',
+          symbolSize: 5,
+          showSymbol: false,
+          lineStyle: {
+            normal: {
+              width: 1
+            }
+          },
+          areaStyle: {
+            normal: {
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
+                offset: 0,
+                color: 'rgba(137, 189, 27, 0.3)'
+              }, {
+                offset: 0.8,
+                color: 'rgba(137, 189, 27, 0)'
+              }], false),
+              shadowColor: 'rgba(0, 0, 0, 0.1)',
+              shadowBlur: 10
+            }
+          },
+          itemStyle: {
+            normal: {
+              color: 'rgb(137,189,27)',
+              borderColor: 'rgba(137,189,2,0.27)',
+              borderWidth: 12
+
+            }
+          },
+          data: [220, 182, 191, 134, 150, 120, 110, 125, 145, 122, 165, 122]
+        }, {
+          name: 'CTCC',
+          type: 'line',
+          smooth: true,
+          symbol: 'circle',
+          symbolSize: 5,
+          showSymbol: false,
+          lineStyle: {
+            normal: {
+              width: 1
+            }
+          },
+          areaStyle: {
+            normal: {
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
+                offset: 0,
+                color: 'rgba(0, 136, 212, 0.3)'
+              }, {
+                offset: 0.8,
+                color: 'rgba(0, 136, 212, 0)'
+              }], false),
+              shadowColor: 'rgba(0, 0, 0, 0.1)',
+              shadowBlur: 10
+            }
+          },
+          itemStyle: {
+            normal: {
+              color: 'rgb(0,136,212)',
+              borderColor: 'rgba(0,136,212,0.2)',
+              borderWidth: 12
+
+            }
+          },
+          data: [120, 110, 125, 145, 122, 165, 122, 220, 182, 191, 134, 150]
+        }, {
+          name: 'CUCC',
+          type: 'line',
+          smooth: true,
+          symbol: 'circle',
+          symbolSize: 5,
+          showSymbol: false,
+          lineStyle: {
+            normal: {
+              width: 1
+            }
+          },
+          areaStyle: {
+            normal: {
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
+                offset: 0,
+                color: 'rgba(219, 50, 51, 0.3)'
+              }, {
+                offset: 0.8,
+                color: 'rgba(219, 50, 51, 0)'
+              }], false),
+              shadowColor: 'rgba(0, 0, 0, 0.1)',
+              shadowBlur: 10
+            }
+          },
+          itemStyle: {
+            normal: {
+              color: 'rgb(219,50,51)',
+              borderColor: 'rgba(219,50,51,0.2)',
+              borderWidth: 12
+            }
+          },
+          data: [220, 182, 125, 145, 122, 191, 134, 150, 120, 110, 165, 122]
+        }]
+      })
+    }
+  }
+}
+</script>
diff --git a/frontend/src/components/Charts/MixChart.vue b/frontend/src/components/Charts/MixChart.vue
new file mode 100644
index 0000000..c416542
--- /dev/null
+++ b/frontend/src/components/Charts/MixChart.vue
@@ -0,0 +1,271 @@
+<template>
+  <div :id="id" :class="className" :style="{height:height,width:width}" />
+</template>
+
+<script>
+import echarts from 'echarts'
+import resize from './mixins/resize'
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    id: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '200px'
+    },
+    height: {
+      type: String,
+      default: '200px'
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  mounted() {
+    this.initChart()
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(document.getElementById(this.id))
+      const xData = (function() {
+        const data = []
+        for (let i = 1; i < 13; i++) {
+          data.push(i + 'month')
+        }
+        return data
+      }())
+      this.chart.setOption({
+        backgroundColor: '#344b58',
+        title: {
+          text: 'statistics',
+          x: '20',
+          top: '20',
+          textStyle: {
+            color: '#fff',
+            fontSize: '22'
+          },
+          subtextStyle: {
+            color: '#90979c',
+            fontSize: '16'
+          }
+        },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            textStyle: {
+              color: '#fff'
+            }
+          }
+        },
+        grid: {
+          left: '5%',
+          right: '5%',
+          borderWidth: 0,
+          top: 150,
+          bottom: 95,
+          textStyle: {
+            color: '#fff'
+          }
+        },
+        legend: {
+          x: '5%',
+          top: '10%',
+          textStyle: {
+            color: '#90979c'
+          },
+          data: ['female', 'male', 'average']
+        },
+        calculable: true,
+        xAxis: [{
+          type: 'category',
+          axisLine: {
+            lineStyle: {
+              color: '#90979c'
+            }
+          },
+          splitLine: {
+            show: false
+          },
+          axisTick: {
+            show: false
+          },
+          splitArea: {
+            show: false
+          },
+          axisLabel: {
+            interval: 0
+
+          },
+          data: xData
+        }],
+        yAxis: [{
+          type: 'value',
+          splitLine: {
+            show: false
+          },
+          axisLine: {
+            lineStyle: {
+              color: '#90979c'
+            }
+          },
+          axisTick: {
+            show: false
+          },
+          axisLabel: {
+            interval: 0
+          },
+          splitArea: {
+            show: false
+          }
+        }],
+        dataZoom: [{
+          show: true,
+          height: 30,
+          xAxisIndex: [
+            0
+          ],
+          bottom: 30,
+          start: 10,
+          end: 80,
+          handleIcon: 'path://M306.1,413c0,2.2-1.8,4-4,4h-59.8c-2.2,0-4-1.8-4-4V200.8c0-2.2,1.8-4,4-4h59.8c2.2,0,4,1.8,4,4V413z',
+          handleSize: '110%',
+          handleStyle: {
+            color: '#d3dee5'
+
+          },
+          textStyle: {
+            color: '#fff' },
+          borderColor: '#90979c'
+
+        }, {
+          type: 'inside',
+          show: true,
+          height: 15,
+          start: 1,
+          end: 35
+        }],
+        series: [{
+          name: 'female',
+          type: 'bar',
+          stack: 'total',
+          barMaxWidth: 35,
+          barGap: '10%',
+          itemStyle: {
+            normal: {
+              color: 'rgba(255,144,128,1)',
+              label: {
+                show: true,
+                textStyle: {
+                  color: '#fff'
+                },
+                position: 'insideTop',
+                formatter(p) {
+                  return p.value > 0 ? p.value : ''
+                }
+              }
+            }
+          },
+          data: [
+            709,
+            1917,
+            2455,
+            2610,
+            1719,
+            1433,
+            1544,
+            3285,
+            5208,
+            3372,
+            2484,
+            4078
+          ]
+        },
+
+        {
+          name: 'male',
+          type: 'bar',
+          stack: 'total',
+          itemStyle: {
+            normal: {
+              color: 'rgba(0,191,183,1)',
+              barBorderRadius: 0,
+              label: {
+                show: true,
+                position: 'top',
+                formatter(p) {
+                  return p.value > 0 ? p.value : ''
+                }
+              }
+            }
+          },
+          data: [
+            327,
+            1776,
+            507,
+            1200,
+            800,
+            482,
+            204,
+            1390,
+            1001,
+            951,
+            381,
+            220
+          ]
+        }, {
+          name: 'average',
+          type: 'line',
+          stack: 'total',
+          symbolSize: 10,
+          symbol: 'circle',
+          itemStyle: {
+            normal: {
+              color: 'rgba(252,230,48,1)',
+              barBorderRadius: 0,
+              label: {
+                show: true,
+                position: 'top',
+                formatter(p) {
+                  return p.value > 0 ? p.value : ''
+                }
+              }
+            }
+          },
+          data: [
+            1036,
+            3693,
+            2962,
+            3810,
+            2519,
+            1915,
+            1748,
+            4675,
+            6209,
+            4323,
+            2865,
+            4298
+          ]
+        }
+        ]
+      })
+    }
+  }
+}
+</script>
diff --git a/frontend/src/components/Charts/mixins/resize.js b/frontend/src/components/Charts/mixins/resize.js
new file mode 100644
index 0000000..b1e76e9
--- /dev/null
+++ b/frontend/src/components/Charts/mixins/resize.js
@@ -0,0 +1,56 @@
+import { debounce } from '@/utils'
+
+export default {
+  data() {
+    return {
+      $_sidebarElm: null,
+      $_resizeHandler: null
+    }
+  },
+  mounted() {
+    this.initListener()
+  },
+  activated() {
+    if (!this.$_resizeHandler) {
+      // avoid duplication init
+      this.initListener()
+    }
+
+    // when keep-alive chart activated, auto resize
+    this.resize()
+  },
+  beforeDestroy() {
+    this.destroyListener()
+  },
+  deactivated() {
+    this.destroyListener()
+  },
+  methods: {
+    // use $_ for mixins properties
+    // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
+    $_sidebarResizeHandler(e) {
+      if (e.propertyName === 'width') {
+        this.$_resizeHandler()
+      }
+    },
+    initListener() {
+      this.$_resizeHandler = debounce(() => {
+        this.resize()
+      }, 100)
+      window.addEventListener('resize', this.$_resizeHandler)
+
+      this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
+      this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
+    },
+    destroyListener() {
+      window.removeEventListener('resize', this.$_resizeHandler)
+      this.$_resizeHandler = null
+
+      this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
+    },
+    resize() {
+      const { chart } = this
+      chart && chart.resize()
+    }
+  }
+}
diff --git a/frontend/src/components/DndList/index.vue b/frontend/src/components/DndList/index.vue
new file mode 100644
index 0000000..23ca006
--- /dev/null
+++ b/frontend/src/components/DndList/index.vue
@@ -0,0 +1,166 @@
+<template>
+  <div class="dndList">
+    <div :style="{width:width1}" class="dndList-list">
+      <h3>{{ list1Title }}</h3>
+      <draggable :set-data="setData" :list="list1" group="article" class="dragArea">
+        <div v-for="element in list1" :key="element.id" class="list-complete-item">
+          <div class="list-complete-item-handle">
+            {{ element.id }}[{{ element.author }}] {{ element.title }}
+          </div>
+          <div style="position:absolute;right:0px;">
+            <span style="float: right ;margin-top: -20px;margin-right:5px;" @click="deleteEle(element)">
+              <i style="color:#ff4949" class="el-icon-delete" />
+            </span>
+          </div>
+        </div>
+      </draggable>
+    </div>
+    <div :style="{width:width2}" class="dndList-list">
+      <h3>{{ list2Title }}</h3>
+      <draggable :list="list2" group="article" class="dragArea">
+        <div v-for="element in list2" :key="element.id" class="list-complete-item">
+          <div class="list-complete-item-handle2" @click="pushEle(element)">
+            {{ element.id }} [{{ element.author }}] {{ element.title }}
+          </div>
+        </div>
+      </draggable>
+    </div>
+  </div>
+</template>
+
+<script>
+import draggable from 'vuedraggable'
+
+export default {
+  name: 'DndList',
+  components: { draggable },
+  props: {
+    list1: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    list2: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    list1Title: {
+      type: String,
+      default: 'list1'
+    },
+    list2Title: {
+      type: String,
+      default: 'list2'
+    },
+    width1: {
+      type: String,
+      default: '48%'
+    },
+    width2: {
+      type: String,
+      default: '48%'
+    }
+  },
+  methods: {
+    isNotInList1(v) {
+      return this.list1.every(k => v.id !== k.id)
+    },
+    isNotInList2(v) {
+      return this.list2.every(k => v.id !== k.id)
+    },
+    deleteEle(ele) {
+      for (const item of this.list1) {
+        if (item.id === ele.id) {
+          const index = this.list1.indexOf(item)
+          this.list1.splice(index, 1)
+          break
+        }
+      }
+      if (this.isNotInList2(ele)) {
+        this.list2.unshift(ele)
+      }
+    },
+    pushEle(ele) {
+      for (const item of this.list2) {
+        if (item.id === ele.id) {
+          const index = this.list2.indexOf(item)
+          this.list2.splice(index, 1)
+          break
+        }
+      }
+      if (this.isNotInList1(ele)) {
+        this.list1.push(ele)
+      }
+    },
+    setData(dataTransfer) {
+      // to avoid Firefox bug
+      // Detail see : https://github.com/RubaXa/Sortable/issues/1012
+      dataTransfer.setData('Text', '')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.dndList {
+  background: #fff;
+  padding-bottom: 40px;
+  &:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+  .dndList-list {
+    float: left;
+    padding-bottom: 30px;
+    &:first-of-type {
+      margin-right: 2%;
+    }
+    .dragArea {
+      margin-top: 15px;
+      min-height: 50px;
+      padding-bottom: 30px;
+    }
+  }
+}
+
+.list-complete-item {
+  cursor: pointer;
+  position: relative;
+  font-size: 14px;
+  padding: 5px 12px;
+  margin-top: 4px;
+  border: 1px solid #bfcbd9;
+  transition: all 1s;
+}
+
+.list-complete-item-handle {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  margin-right: 50px;
+}
+
+.list-complete-item-handle2 {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  margin-right: 20px;
+}
+
+.list-complete-item.sortable-chosen {
+  background: #4AB7BD;
+}
+
+.list-complete-item.sortable-ghost {
+  background: #30B08F;
+}
+
+.list-complete-enter,
+.list-complete-leave-active {
+  opacity: 0;
+}
+</style>
diff --git a/frontend/src/components/DragSelect/index.vue b/frontend/src/components/DragSelect/index.vue
new file mode 100644
index 0000000..47f4247
--- /dev/null
+++ b/frontend/src/components/DragSelect/index.vue
@@ -0,0 +1,65 @@
+<template>
+  <el-select ref="dragSelect" v-model="selectVal" v-bind="$attrs" class="drag-select" multiple v-on="$listeners">
+    <slot />
+  </el-select>
+</template>
+
+<script>
+import Sortable from 'sortablejs'
+
+export default {
+  name: 'DragSelect',
+  props: {
+    value: {
+      type: Array,
+      required: true
+    }
+  },
+  computed: {
+    selectVal: {
+      get() {
+        return [...this.value]
+      },
+      set(val) {
+        this.$emit('input', [...val])
+      }
+    }
+  },
+  mounted() {
+    this.setSort()
+  },
+  methods: {
+    setSort() {
+      const el = this.$refs.dragSelect.$el.querySelectorAll('.el-select__tags > span')[0]
+      this.sortable = Sortable.create(el, {
+        ghostClass: 'sortable-ghost', // Class name for the drop placeholder,
+        setData: function(dataTransfer) {
+          dataTransfer.setData('Text', '')
+          // to avoid Firefox bug
+          // Detail see : https://github.com/RubaXa/Sortable/issues/1012
+        },
+        onEnd: evt => {
+          const targetRow = this.value.splice(evt.oldIndex, 1)[0]
+          this.value.splice(evt.newIndex, 0, targetRow)
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.drag-select {
+  ::v-deep {
+    .sortable-ghost {
+      opacity: .8;
+      color: #fff !important;
+      background: #42b983 !important;
+    }
+
+    .el-tag {
+      cursor: pointer;
+    }
+  }
+}
+</style>
diff --git a/frontend/src/components/Dropzone/index.vue b/frontend/src/components/Dropzone/index.vue
new file mode 100644
index 0000000..bad9eb9
--- /dev/null
+++ b/frontend/src/components/Dropzone/index.vue
@@ -0,0 +1,297 @@
+<template>
+  <div :id="id" :ref="id" :action="url" class="dropzone">
+    <input type="file" name="file">
+  </div>
+</template>
+
+<script>
+import Dropzone from 'dropzone'
+import 'dropzone/dist/dropzone.css'
+// import { getToken } from 'api/qiniu';
+
+Dropzone.autoDiscover = false
+
+export default {
+  props: {
+    id: {
+      type: String,
+      required: true
+    },
+    url: {
+      type: String,
+      required: true
+    },
+    clickable: {
+      type: Boolean,
+      default: true
+    },
+    defaultMsg: {
+      type: String,
+      default: '上传图片'
+    },
+    acceptedFiles: {
+      type: String,
+      default: ''
+    },
+    thumbnailHeight: {
+      type: Number,
+      default: 200
+    },
+    thumbnailWidth: {
+      type: Number,
+      default: 200
+    },
+    showRemoveLink: {
+      type: Boolean,
+      default: true
+    },
+    maxFilesize: {
+      type: Number,
+      default: 2
+    },
+    maxFiles: {
+      type: Number,
+      default: 3
+    },
+    autoProcessQueue: {
+      type: Boolean,
+      default: true
+    },
+    useCustomDropzoneOptions: {
+      type: Boolean,
+      default: false
+    },
+    defaultImg: {
+      default: '',
+      type: [String, Array]
+    },
+    couldPaste: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      dropzone: '',
+      initOnce: true
+    }
+  },
+  watch: {
+    defaultImg(val) {
+      if (val.length === 0) {
+        this.initOnce = false
+        return
+      }
+      if (!this.initOnce) return
+      this.initImages(val)
+      this.initOnce = false
+    }
+  },
+  mounted() {
+    const element = document.getElementById(this.id)
+    const vm = this
+    this.dropzone = new Dropzone(element, {
+      clickable: this.clickable,
+      thumbnailWidth: this.thumbnailWidth,
+      thumbnailHeight: this.thumbnailHeight,
+      maxFiles: this.maxFiles,
+      maxFilesize: this.maxFilesize,
+      dictRemoveFile: 'Remove',
+      addRemoveLinks: this.showRemoveLink,
+      acceptedFiles: this.acceptedFiles,
+      autoProcessQueue: this.autoProcessQueue,
+      dictDefaultMessage: '<i style="margin-top: 3em;display: inline-block" class="material-icons">' + this.defaultMsg + '</i><br>Drop files here to upload',
+      dictMaxFilesExceeded: '只能一个图',
+      previewTemplate: '<div class="dz-preview dz-file-preview">  <div class="dz-image" style="width:' + this.thumbnailWidth + 'px;height:' + this.thumbnailHeight + 'px" ><img style="width:' + this.thumbnailWidth + 'px;height:' + this.thumbnailHeight + 'px" data-dz-thumbnail /></div>  <div class="dz-details"><div class="dz-size"><span data-dz-size></span></div> <div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>  <div class="dz-error-message"><span data-dz-errormessage></span></div>  <div class="dz-success-mark"> <i class="material-icons">done</i> </div>  <div class="dz-error-mark"><i class="material-icons">error</i></div></div>',
+      init() {
+        const val = vm.defaultImg
+        if (!val) return
+        if (Array.isArray(val)) {
+          if (val.length === 0) return
+          val.map((v, i) => {
+            const mockFile = { name: 'name' + i, size: 12345, url: v }
+            this.options.addedfile.call(this, mockFile)
+            this.options.thumbnail.call(this, mockFile, v)
+            mockFile.previewElement.classList.add('dz-success')
+            mockFile.previewElement.classList.add('dz-complete')
+            vm.initOnce = false
+            return true
+          })
+        } else {
+          const mockFile = { name: 'name', size: 12345, url: val }
+          this.options.addedfile.call(this, mockFile)
+          this.options.thumbnail.call(this, mockFile, val)
+          mockFile.previewElement.classList.add('dz-success')
+          mockFile.previewElement.classList.add('dz-complete')
+          vm.initOnce = false
+        }
+      },
+      accept: (file, done) => {
+        /* 七牛*/
+        // const token = this.$store.getters.token;
+        // getToken(token).then(response => {
+        //   file.token = response.data.qiniu_token;
+        //   file.key = response.data.qiniu_key;
+        //   file.url = response.data.qiniu_url;
+        //   done();
+        // })
+        done()
+      },
+      sending: (file, xhr, formData) => {
+        // formData.append('token', file.token);
+        // formData.append('key', file.key);
+        vm.initOnce = false
+      }
+    })
+
+    if (this.couldPaste) {
+      document.addEventListener('paste', this.pasteImg)
+    }
+
+    this.dropzone.on('success', file => {
+      vm.$emit('dropzone-success', file, vm.dropzone.element)
+    })
+    this.dropzone.on('addedfile', file => {
+      vm.$emit('dropzone-fileAdded', file)
+    })
+    this.dropzone.on('removedfile', file => {
+      vm.$emit('dropzone-removedFile', file)
+    })
+    this.dropzone.on('error', (file, error, xhr) => {
+      vm.$emit('dropzone-error', file, error, xhr)
+    })
+    this.dropzone.on('successmultiple', (file, error, xhr) => {
+      vm.$emit('dropzone-successmultiple', file, error, xhr)
+    })
+  },
+  destroyed() {
+    document.removeEventListener('paste', this.pasteImg)
+    this.dropzone.destroy()
+  },
+  methods: {
+    removeAllFiles() {
+      this.dropzone.removeAllFiles(true)
+    },
+    processQueue() {
+      this.dropzone.processQueue()
+    },
+    pasteImg(event) {
+      const items = (event.clipboardData || event.originalEvent.clipboardData).items
+      if (items[0].kind === 'file') {
+        this.dropzone.addFile(items[0].getAsFile())
+      }
+    },
+    initImages(val) {
+      if (!val) return
+      if (Array.isArray(val)) {
+        val.map((v, i) => {
+          const mockFile = { name: 'name' + i, size: 12345, url: v }
+          this.dropzone.options.addedfile.call(this.dropzone, mockFile)
+          this.dropzone.options.thumbnail.call(this.dropzone, mockFile, v)
+          mockFile.previewElement.classList.add('dz-success')
+          mockFile.previewElement.classList.add('dz-complete')
+          return true
+        })
+      } else {
+        const mockFile = { name: 'name', size: 12345, url: val }
+        this.dropzone.options.addedfile.call(this.dropzone, mockFile)
+        this.dropzone.options.thumbnail.call(this.dropzone, mockFile, val)
+        mockFile.previewElement.classList.add('dz-success')
+        mockFile.previewElement.classList.add('dz-complete')
+      }
+    }
+
+  }
+}
+</script>
+
+<style scoped>
+    .dropzone {
+        border: 2px solid #E5E5E5;
+        font-family: 'Roboto', sans-serif;
+        color: #777;
+        transition: background-color .2s linear;
+        padding: 5px;
+    }
+
+    .dropzone:hover {
+        background-color: #F6F6F6;
+    }
+
+    i {
+        color: #CCC;
+    }
+
+    .dropzone .dz-image img {
+        width: 100%;
+        height: 100%;
+    }
+
+    .dropzone input[name='file'] {
+        display: none;
+    }
+
+    .dropzone .dz-preview .dz-image {
+        border-radius: 0px;
+    }
+
+    .dropzone .dz-preview:hover .dz-image img {
+        transform: none;
+        filter: none;
+        width: 100%;
+        height: 100%;
+    }
+
+    .dropzone .dz-preview .dz-details {
+        bottom: 0px;
+        top: 0px;
+        color: white;
+        background-color: rgba(33, 150, 243, 0.8);
+        transition: opacity .2s linear;
+        text-align: left;
+    }
+
+    .dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span {
+        background-color: transparent;
+    }
+
+    .dropzone .dz-preview .dz-details .dz-filename:not(:hover) span {
+        border: none;
+    }
+
+    .dropzone .dz-preview .dz-details .dz-filename:hover span {
+        background-color: transparent;
+        border: none;
+    }
+
+    .dropzone .dz-preview .dz-remove {
+        position: absolute;
+        z-index: 30;
+        color: white;
+        margin-left: 15px;
+        padding: 10px;
+        top: inherit;
+        bottom: 15px;
+        border: 2px white solid;
+        text-decoration: none;
+        text-transform: uppercase;
+        font-size: 0.8rem;
+        font-weight: 800;
+        letter-spacing: 1.1px;
+        opacity: 0;
+    }
+
+    .dropzone .dz-preview:hover .dz-remove {
+        opacity: 1;
+    }
+
+    .dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark {
+        margin-left: -40px;
+        margin-top: -50px;
+    }
+
+    .dropzone .dz-preview .dz-success-mark i, .dropzone .dz-preview .dz-error-mark i {
+        color: white;
+        font-size: 5rem;
+    }
+</style>
diff --git a/frontend/src/components/ErrorLog/index.vue b/frontend/src/components/ErrorLog/index.vue
new file mode 100644
index 0000000..6119c03
--- /dev/null
+++ b/frontend/src/components/ErrorLog/index.vue
@@ -0,0 +1,78 @@
+<template>
+  <div v-if="errorLogs.length>0">
+    <el-badge :is-dot="true" style="line-height: 25px;margin-top: -5px;" @click.native="dialogTableVisible=true">
+      <el-button style="padding: 8px 10px;" size="small" type="danger">
+        <svg-icon icon-class="bug" />
+      </el-button>
+    </el-badge>
+
+    <el-dialog :visible.sync="dialogTableVisible" width="80%" append-to-body>
+      <div slot="title">
+        <span style="padding-right: 10px;">Error Log</span>
+        <el-button size="mini" type="primary" icon="el-icon-delete" @click="clearAll">Clear All</el-button>
+      </div>
+      <el-table :data="errorLogs" border>
+        <el-table-column label="Message">
+          <template slot-scope="{row}">
+            <div>
+              <span class="message-title">Msg:</span>
+              <el-tag type="danger">
+                {{ row.err.message }}
+              </el-tag>
+            </div>
+            <br>
+            <div>
+              <span class="message-title" style="padding-right: 10px;">Info: </span>
+              <el-tag type="warning">
+                {{ row.vm.$vnode.tag }} error in {{ row.info }}
+              </el-tag>
+            </div>
+            <br>
+            <div>
+              <span class="message-title" style="padding-right: 16px;">Url: </span>
+              <el-tag type="success">
+                {{ row.url }}
+              </el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="Stack">
+          <template slot-scope="scope">
+            {{ scope.row.err.stack }}
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ErrorLog',
+  data() {
+    return {
+      dialogTableVisible: false
+    }
+  },
+  computed: {
+    errorLogs() {
+      return this.$store.getters.errorLogs
+    }
+  },
+  methods: {
+    clearAll() {
+      this.dialogTableVisible = false
+      this.$store.dispatch('errorLog/clearErrorLog')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.message-title {
+  font-size: 16px;
+  color: #333;
+  font-weight: bold;
+  padding-right: 8px;
+}
+</style>
diff --git a/frontend/src/components/GithubCorner/index.vue b/frontend/src/components/GithubCorner/index.vue
new file mode 100644
index 0000000..970faaf
--- /dev/null
+++ b/frontend/src/components/GithubCorner/index.vue
@@ -0,0 +1,54 @@
+<template>
+  <a href="https://github.com/PanJiaChen/vue-element-admin" target="_blank" class="github-corner" aria-label="View source on Github">
+    <svg
+      width="80"
+      height="80"
+      viewBox="0 0 250 250"
+      style="fill:#40c9c6; color:#fff;"
+      aria-hidden="true"
+    >
+      <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z" />
+      <path
+        d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
+        fill="currentColor"
+        style="transform-origin: 130px 106px;"
+        class="octo-arm"
+      />
+      <path
+        d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
+        fill="currentColor"
+        class="octo-body"
+      />
+    </svg>
+  </a>
+</template>
+
+<style scoped>
+.github-corner:hover .octo-arm {
+  animation: octocat-wave 560ms ease-in-out
+}
+
+@keyframes octocat-wave {
+  0%,
+  100% {
+    transform: rotate(0)
+  }
+  20%,
+  60% {
+    transform: rotate(-25deg)
+  }
+  40%,
+  80% {
+    transform: rotate(10deg)
+  }
+}
+
+@media (max-width:500px) {
+  .github-corner:hover .octo-arm {
+    animation: none
+  }
+  .github-corner .octo-arm {
+    animation: octocat-wave 560ms ease-in-out
+  }
+}
+</style>
diff --git a/frontend/src/components/Hamburger/index.vue b/frontend/src/components/Hamburger/index.vue
new file mode 100644
index 0000000..368b002
--- /dev/null
+++ b/frontend/src/components/Hamburger/index.vue
@@ -0,0 +1,44 @@
+<template>
+  <div style="padding: 0 15px;" @click="toggleClick">
+    <svg
+      :class="{'is-active':isActive}"
+      class="hamburger"
+      viewBox="0 0 1024 1024"
+      xmlns="http://www.w3.org/2000/svg"
+      width="64"
+      height="64"
+    >
+      <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
+    </svg>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Hamburger',
+  props: {
+    isActive: {
+      type: Boolean,
+      default: false
+    }
+  },
+  methods: {
+    toggleClick() {
+      this.$emit('toggleClick')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.hamburger {
+  display: inline-block;
+  vertical-align: middle;
+  width: 20px;
+  height: 20px;
+}
+
+.hamburger.is-active {
+  transform: rotate(180deg);
+}
+</style>
diff --git a/frontend/src/components/HeaderSearch/index.vue b/frontend/src/components/HeaderSearch/index.vue
new file mode 100644
index 0000000..6026ebb
--- /dev/null
+++ b/frontend/src/components/HeaderSearch/index.vue
@@ -0,0 +1,180 @@
+<template>
+  <div :class="{'show':show}" class="header-search">
+    <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
+    <el-select
+      ref="headerSearchSelect"
+      v-model="search"
+      :remote-method="querySearch"
+      filterable
+      default-first-option
+      remote
+      placeholder="Search"
+      class="header-search-select"
+      @change="change"
+    >
+      <el-option v-for="item in options" :key="item.path" :value="item" :label="item.title.join(' > ')" />
+    </el-select>
+  </div>
+</template>
+
+<script>
+// fuse is a lightweight fuzzy-search module
+// make search results more in line with expectations
+import Fuse from 'fuse.js'
+import path from 'path'
+
+export default {
+  name: 'HeaderSearch',
+  data() {
+    return {
+      search: '',
+      options: [],
+      searchPool: [],
+      show: false,
+      fuse: undefined
+    }
+  },
+  computed: {
+    routes() {
+      return this.$store.getters.permission_routes
+    }
+  },
+  watch: {
+    routes() {
+      this.searchPool = this.generateRoutes(this.routes)
+    },
+    searchPool(list) {
+      this.initFuse(list)
+    },
+    show(value) {
+      if (value) {
+        document.body.addEventListener('click', this.close)
+      } else {
+        document.body.removeEventListener('click', this.close)
+      }
+    }
+  },
+  mounted() {
+    this.searchPool = this.generateRoutes(this.routes)
+  },
+  methods: {
+    click() {
+      this.show = !this.show
+      if (this.show) {
+        this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
+      }
+    },
+    close() {
+      this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
+      this.options = []
+      this.show = false
+    },
+    change(val) {
+      this.$router.push(val.path)
+      this.search = ''
+      this.options = []
+      this.$nextTick(() => {
+        this.show = false
+      })
+    },
+    initFuse(list) {
+      this.fuse = new Fuse(list, {
+        shouldSort: true,
+        threshold: 0.4,
+        location: 0,
+        distance: 100,
+        maxPatternLength: 32,
+        minMatchCharLength: 1,
+        keys: [{
+          name: 'title',
+          weight: 0.7
+        }, {
+          name: 'path',
+          weight: 0.3
+        }]
+      })
+    },
+    // Filter out the routes that can be displayed in the sidebar
+    // And generate the internationalized title
+    generateRoutes(routes, basePath = '/', prefixTitle = []) {
+      let res = []
+
+      for (const router of routes) {
+        // skip hidden router
+        if (router.hidden) { continue }
+
+        const data = {
+          path: path.resolve(basePath, router.path),
+          title: [...prefixTitle]
+        }
+
+        if (router.meta && router.meta.title) {
+          data.title = [...data.title, router.meta.title]
+
+          if (router.redirect !== 'noRedirect') {
+            // only push the routes with title
+            // special case: need to exclude parent router without redirect
+            res.push(data)
+          }
+        }
+
+        // recursive child routes
+        if (router.children) {
+          const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
+          if (tempRoutes.length >= 1) {
+            res = [...res, ...tempRoutes]
+          }
+        }
+      }
+      return res
+    },
+    querySearch(query) {
+      if (query !== '') {
+        this.options = this.fuse.search(query)
+      } else {
+        this.options = []
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.header-search {
+  font-size: 0 !important;
+
+  .search-icon {
+    cursor: pointer;
+    font-size: 18px;
+    vertical-align: middle;
+  }
+
+  .header-search-select {
+    font-size: 18px;
+    transition: width 0.2s;
+    width: 0;
+    overflow: hidden;
+    background: transparent;
+    border-radius: 0;
+    display: inline-block;
+    vertical-align: middle;
+
+    ::v-deep .el-input__inner {
+      border-radius: 0;
+      border: 0;
+      padding-left: 0;
+      padding-right: 0;
+      box-shadow: none !important;
+      border-bottom: 1px solid #d9d9d9;
+      vertical-align: middle;
+    }
+  }
+
+  &.show {
+    .header-search-select {
+      width: 210px;
+      margin-left: 10px;
+    }
+  }
+}
+</style>
diff --git a/frontend/src/components/ImageCropper/index.vue b/frontend/src/components/ImageCropper/index.vue
new file mode 100644
index 0000000..65a4262
--- /dev/null
+++ b/frontend/src/components/ImageCropper/index.vue
@@ -0,0 +1,1779 @@
+<template>
+  <div v-show="value" class="vue-image-crop-upload">
+    <div class="vicp-wrap">
+      <div class="vicp-close" @click="off">
+        <i class="vicp-icon4" />
+      </div>
+
+      <div v-show="step == 1" class="vicp-step1">
+        <div
+          class="vicp-drop-area"
+          @dragleave="preventDefault"
+          @dragover="preventDefault"
+          @dragenter="preventDefault"
+          @click="handleClick"
+          @drop="handleChange"
+        >
+          <i v-show="loading != 1" class="vicp-icon1">
+            <i class="vicp-icon1-arrow" />
+            <i class="vicp-icon1-body" />
+            <i class="vicp-icon1-bottom" />
+          </i>
+          <span v-show="loading !== 1" class="vicp-hint">{{ lang.hint }}</span>
+          <span v-show="!isSupported" class="vicp-no-supported-hint">{{ lang.noSupported }}</span>
+          <input v-show="false" v-if="step == 1" ref="fileinput" type="file" @change="handleChange">
+        </div>
+        <div v-show="hasError" class="vicp-error">
+          <i class="vicp-icon2" />
+          {{ errorMsg }}
+        </div>
+        <div class="vicp-operate">
+          <a @click="off" @mousedown="ripple">{{ lang.btn.off }}</a>
+        </div>
+      </div>
+
+      <div v-if="step == 2" class="vicp-step2">
+        <div class="vicp-crop">
+          <div v-show="true" class="vicp-crop-left">
+            <div class="vicp-img-container">
+              <img
+                ref="img"
+                :src="sourceImgUrl"
+                :style="sourceImgStyle"
+                class="vicp-img"
+                draggable="false"
+                @drag="preventDefault"
+                @dragstart="preventDefault"
+                @dragend="preventDefault"
+                @dragleave="preventDefault"
+                @dragover="preventDefault"
+                @dragenter="preventDefault"
+                @drop="preventDefault"
+                @touchstart="imgStartMove"
+                @touchmove="imgMove"
+                @touchend="createImg"
+                @touchcancel="createImg"
+                @mousedown="imgStartMove"
+                @mousemove="imgMove"
+                @mouseup="createImg"
+                @mouseout="createImg"
+              >
+              <div :style="sourceImgShadeStyle" class="vicp-img-shade vicp-img-shade-1" />
+              <div :style="sourceImgShadeStyle" class="vicp-img-shade vicp-img-shade-2" />
+            </div>
+
+            <div class="vicp-range">
+              <input
+                :value="scale.range"
+                type="range"
+                step="1"
+                min="0"
+                max="100"
+                @input="zoomChange"
+              >
+              <i
+                class="vicp-icon5"
+                @mousedown="startZoomSub"
+                @mouseout="endZoomSub"
+                @mouseup="endZoomSub"
+              />
+              <i
+                class="vicp-icon6"
+                @mousedown="startZoomAdd"
+                @mouseout="endZoomAdd"
+                @mouseup="endZoomAdd"
+              />
+            </div>
+
+            <div v-if="!noRotate" class="vicp-rotate">
+              <i @mousedown="startRotateLeft" @mouseout="endRotate" @mouseup="endRotate">↺</i>
+              <i @mousedown="startRotateRight" @mouseout="endRotate" @mouseup="endRotate">↻</i>
+            </div>
+          </div>
+          <div v-show="true" class="vicp-crop-right">
+            <div class="vicp-preview">
+              <div v-if="!noSquare" class="vicp-preview-item">
+                <img :src="createImgUrl" :style="previewStyle">
+                <span>{{ lang.preview }}</span>
+              </div>
+              <div v-if="!noCircle" class="vicp-preview-item vicp-preview-item-circle">
+                <img :src="createImgUrl" :style="previewStyle">
+                <span>{{ lang.preview }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="vicp-operate">
+          <a @click="setStep(1)" @mousedown="ripple">{{ lang.btn.back }}</a>
+          <a class="vicp-operate-btn" @click="prepareUpload" @mousedown="ripple">{{ lang.btn.save }}</a>
+        </div>
+      </div>
+
+      <div v-if="step == 3" class="vicp-step3">
+        <div class="vicp-upload">
+          <span v-show="loading === 1" class="vicp-loading">{{ lang.loading }}</span>
+          <div class="vicp-progress-wrap">
+            <span v-show="loading === 1" :style="progressStyle" class="vicp-progress" />
+          </div>
+          <div v-show="hasError" class="vicp-error">
+            <i class="vicp-icon2" />
+            {{ errorMsg }}
+          </div>
+          <div v-show="loading === 2" class="vicp-success">
+            <i class="vicp-icon3" />
+            {{ lang.success }}
+          </div>
+        </div>
+        <div class="vicp-operate">
+          <a @click="setStep(2)" @mousedown="ripple">{{ lang.btn.back }}</a>
+          <a @click="off" @mousedown="ripple">{{ lang.btn.close }}</a>
+        </div>
+      </div>
+      <canvas v-show="false" ref="canvas" :width="width" :height="height" />
+    </div>
+  </div>
+</template>
+
+<script>
+'use strict'
+import request from '@/utils/request'
+import language from './utils/language.js'
+import mimes from './utils/mimes.js'
+import data2blob from './utils/data2blob.js'
+import effectRipple from './utils/effectRipple.js'
+export default {
+  props: {
+    // 域,上传文件name,触发事件会带上(如果一个页面多个图片上传控件,可以做区分
+    field: {
+      type: String,
+      default: 'avatar'
+    },
+    // 原名key,类似于id,触发事件会带上(如果一个页面多个图片上传控件,可以做区分
+    ki: {
+      type: Number,
+      default: 0
+    },
+    // 显示该控件与否
+    value: {
+      type: Boolean,
+      default: true
+    },
+    // 上传地址
+    url: {
+      type: String,
+      default: ''
+    },
+    // 其他要上传文件附带的数据,对象格式
+    params: {
+      type: Object,
+      default: null
+    },
+    // Add custom headers
+    headers: {
+      type: Object,
+      default: null
+    },
+    // 剪裁图片的宽
+    width: {
+      type: Number,
+      default: 200
+    },
+    // 剪裁图片的高
+    height: {
+      type: Number,
+      default: 200
+    },
+    // 不显示旋转功能
+    noRotate: {
+      type: Boolean,
+      default: true
+    },
+    // 不预览圆形图片
+    noCircle: {
+      type: Boolean,
+      default: false
+    },
+    // 不预览方形图片
+    noSquare: {
+      type: Boolean,
+      default: false
+    },
+    // 单文件大小限制
+    maxSize: {
+      type: Number,
+      default: 10240
+    },
+    // 语言类型
+    langType: {
+      type: String,
+      default: 'zh'
+    },
+    // 语言包
+    langExt: {
+      type: Object,
+      default: null
+    },
+    // 图片上传格式
+    imgFormat: {
+      type: String,
+      default: 'png'
+    },
+    // 是否支持跨域
+    withCredentials: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    const { imgFormat, langType, langExt, width, height } = this
+    let isSupported = true
+    const allowImgFormat = ['jpg', 'png']
+    const tempImgFormat =
+      allowImgFormat.indexOf(imgFormat) === -1 ? 'jpg' : imgFormat
+    const lang = language[langType] ? language[langType] : language['en']
+    const mime = mimes[tempImgFormat]
+    // 规范图片格式
+    this.imgFormat = tempImgFormat
+    if (langExt) {
+      Object.assign(lang, langExt)
+    }
+    if (typeof FormData !== 'function') {
+      isSupported = false
+    }
+    return {
+      // 图片的mime
+      mime,
+      // 语言包
+      lang,
+      // 浏览器是否支持该控件
+      isSupported,
+      // 浏览器是否支持触屏事件
+      // eslint-disable-next-line no-prototype-builtins
+      isSupportTouch: document.hasOwnProperty('ontouchstart'),
+      // 步骤
+      step: 1, // 1选择文件 2剪裁 3上传
+      // 上传状态及进度
+      loading: 0, // 0未开始 1正在 2成功 3错误
+      progress: 0,
+      // 是否有错误及错误信息
+      hasError: false,
+      errorMsg: '',
+      // 需求图宽高比
+      ratio: width / height,
+      // 原图地址、生成图片地址
+      sourceImg: null,
+      sourceImgUrl: '',
+      createImgUrl: '',
+      // 原图片拖动事件初始值
+      sourceImgMouseDown: {
+        on: false,
+        mX: 0, // 鼠标按下的坐标
+        mY: 0,
+        x: 0, // scale原图坐标
+        y: 0
+      },
+      // 生成图片预览的容器大小
+      previewContainer: {
+        width: 100,
+        height: 100
+      },
+      // 原图容器宽高
+      sourceImgContainer: {
+        // sic
+        width: 240,
+        height: 184 // 如果生成图比例与此一致会出现bug,先改成特殊的格式吧,哈哈哈
+      },
+      // 原图展示属性
+      scale: {
+        zoomAddOn: false, // 按钮缩放事件开启
+        zoomSubOn: false, // 按钮缩放事件开启
+        range: 1, // 最大100
+        rotateLeft: false, // 按钮向左旋转事件开启
+        rotateRight: false, // 按钮向右旋转事件开启
+        degree: 0, // 旋转度数
+        x: 0,
+        y: 0,
+        width: 0,
+        height: 0,
+        maxWidth: 0,
+        maxHeight: 0,
+        minWidth: 0, // 最宽
+        minHeight: 0,
+        naturalWidth: 0, // 原宽
+        naturalHeight: 0
+      }
+    }
+  },
+  computed: {
+    // 进度条样式
+    progressStyle() {
+      const { progress } = this
+      return {
+        width: progress + '%'
+      }
+    },
+    // 原图样式
+    sourceImgStyle() {
+      const { scale, sourceImgMasking } = this
+      const top = scale.y + sourceImgMasking.y + 'px'
+      const left = scale.x + sourceImgMasking.x + 'px'
+      return {
+        top,
+        left,
+        width: scale.width + 'px',
+        height: scale.height + 'px',
+        transform: 'rotate(' + scale.degree + 'deg)', // 旋转时 左侧原始图旋转样式
+        '-ms-transform': 'rotate(' + scale.degree + 'deg)', // 兼容IE9
+        '-moz-transform': 'rotate(' + scale.degree + 'deg)', // 兼容FireFox
+        '-webkit-transform': 'rotate(' + scale.degree + 'deg)', // 兼容Safari 和 chrome
+        '-o-transform': 'rotate(' + scale.degree + 'deg)' // 兼容 Opera
+      }
+    },
+    // 原图蒙版属性
+    sourceImgMasking() {
+      const { width, height, ratio, sourceImgContainer } = this
+      const sic = sourceImgContainer
+      const sicRatio = sic.width / sic.height // 原图容器宽高比
+      let x = 0
+      let y = 0
+      let w = sic.width
+      let h = sic.height
+      let scale = 1
+      if (ratio < sicRatio) {
+        scale = sic.height / height
+        w = sic.height * ratio
+        x = (sic.width - w) / 2
+      }
+      if (ratio > sicRatio) {
+        scale = sic.width / width
+        h = sic.width / ratio
+        y = (sic.height - h) / 2
+      }
+      return {
+        scale, // 蒙版相对需求宽高的缩放
+        x,
+        y,
+        width: w,
+        height: h
+      }
+    },
+    // 原图遮罩样式
+    sourceImgShadeStyle() {
+      const { sourceImgMasking, sourceImgContainer } = this
+      const sic = sourceImgContainer
+      const sim = sourceImgMasking
+      const w =
+        sim.width === sic.width ? sim.width : (sic.width - sim.width) / 2
+      const h =
+        sim.height === sic.height ? sim.height : (sic.height - sim.height) / 2
+      return {
+        width: w + 'px',
+        height: h + 'px'
+      }
+    },
+    previewStyle() {
+      const { ratio, previewContainer } = this
+      const pc = previewContainer
+      let w = pc.width
+      let h = pc.height
+      const pcRatio = w / h
+      if (ratio < pcRatio) {
+        w = pc.height * ratio
+      }
+      if (ratio > pcRatio) {
+        h = pc.width / ratio
+      }
+      return {
+        width: w + 'px',
+        height: h + 'px'
+      }
+    }
+  },
+  watch: {
+    value(newValue) {
+      if (newValue && this.loading !== 1) {
+        this.reset()
+      }
+    }
+  },
+  created() {
+    // 绑定按键esc隐藏此插件事件
+    document.addEventListener('keyup', this.closeHandler)
+  },
+  destroyed() {
+    document.removeEventListener('keyup', this.closeHandler)
+  },
+  methods: {
+    // 点击波纹效果
+    ripple(e) {
+      effectRipple(e)
+    },
+    // 关闭控件
+    off() {
+      setTimeout(() => {
+        this.$emit('input', false)
+        this.$emit('close')
+        if (this.step === 3 && this.loading === 2) {
+          this.setStep(1)
+        }
+      }, 200)
+    },
+    // 设置步骤
+    setStep(no) {
+      // 延时是为了显示动画效果呢,哈哈哈
+      setTimeout(() => {
+        this.step = no
+      }, 200)
+    },
+    /* 图片选择区域函数绑定
+     ---------------------------------------------------------------*/
+    preventDefault(e) {
+      e.preventDefault()
+      return false
+    },
+    handleClick(e) {
+      if (this.loading !== 1) {
+        if (e.target !== this.$refs.fileinput) {
+          e.preventDefault()
+          if (document.activeElement !== this.$refs) {
+            this.$refs.fileinput.click()
+          }
+        }
+      }
+    },
+    handleChange(e) {
+      e.preventDefault()
+      if (this.loading !== 1) {
+        const files = e.target.files || e.dataTransfer.files
+        this.reset()
+        if (this.checkFile(files[0])) {
+          this.setSourceImg(files[0])
+        }
+      }
+    },
+    /* ---------------------------------------------------------------*/
+    // 检测选择的文件是否合适
+    checkFile(file) {
+      const { lang, maxSize } = this
+      // 仅限图片
+      if (file.type.indexOf('image') === -1) {
+        this.hasError = true
+        this.errorMsg = lang.error.onlyImg
+        return false
+      }
+      // 超出大小
+      if (file.size / 1024 > maxSize) {
+        this.hasError = true
+        this.errorMsg = lang.error.outOfSize + maxSize + 'kb'
+        return false
+      }
+      return true
+    },
+    // 重置控件
+    reset() {
+      this.loading = 0
+      this.hasError = false
+      this.errorMsg = ''
+      this.progress = 0
+    },
+    // 设置图片源
+    setSourceImg(file) {
+      const fr = new FileReader()
+      fr.onload = e => {
+        this.sourceImgUrl = fr.result
+        this.startCrop()
+      }
+      fr.readAsDataURL(file)
+    },
+    // 剪裁前准备工作
+    startCrop() {
+      const {
+        width,
+        height,
+        ratio,
+        scale,
+        sourceImgUrl,
+        sourceImgMasking,
+        lang
+      } = this
+      const sim = sourceImgMasking
+      const img = new Image()
+      img.src = sourceImgUrl
+      img.onload = () => {
+        const nWidth = img.naturalWidth
+        const nHeight = img.naturalHeight
+        const nRatio = nWidth / nHeight
+        let w = sim.width
+        let h = sim.height
+        let x = 0
+        let y = 0
+        // 图片像素不达标
+        if (nWidth < width || nHeight < height) {
+          this.hasError = true
+          this.errorMsg = lang.error.lowestPx + width + '*' + height
+          return false
+        }
+        if (ratio > nRatio) {
+          h = w / nRatio
+          y = (sim.height - h) / 2
+        }
+        if (ratio < nRatio) {
+          w = h * nRatio
+          x = (sim.width - w) / 2
+        }
+        scale.range = 0
+        scale.x = x
+        scale.y = y
+        scale.width = w
+        scale.height = h
+        scale.degree = 0
+        scale.minWidth = w
+        scale.minHeight = h
+        scale.maxWidth = nWidth * sim.scale
+        scale.maxHeight = nHeight * sim.scale
+        scale.naturalWidth = nWidth
+        scale.naturalHeight = nHeight
+        this.sourceImg = img
+        this.createImg()
+        this.setStep(2)
+      }
+    },
+    // 鼠标按下图片准备移动
+    imgStartMove(e) {
+      e.preventDefault()
+      // 支持触摸事件,则鼠标事件无效
+      if (this.isSupportTouch && !e.targetTouches) {
+        return false
+      }
+      const et = e.targetTouches ? e.targetTouches[0] : e
+      const { sourceImgMouseDown, scale } = this
+      const simd = sourceImgMouseDown
+      simd.mX = et.screenX
+      simd.mY = et.screenY
+      simd.x = scale.x
+      simd.y = scale.y
+      simd.on = true
+    },
+    // 鼠标按下状态下移动,图片移动
+    imgMove(e) {
+      e.preventDefault()
+      // 支持触摸事件,则鼠标事件无效
+      if (this.isSupportTouch && !e.targetTouches) {
+        return false
+      }
+      const et = e.targetTouches ? e.targetTouches[0] : e
+      const {
+        sourceImgMouseDown: { on, mX, mY, x, y },
+        scale,
+        sourceImgMasking
+      } = this
+      const sim = sourceImgMasking
+      const nX = et.screenX
+      const nY = et.screenY
+      const dX = nX - mX
+      const dY = nY - mY
+      let rX = x + dX
+      let rY = y + dY
+      if (!on) return
+      if (rX > 0) {
+        rX = 0
+      }
+      if (rY > 0) {
+        rY = 0
+      }
+      if (rX < sim.width - scale.width) {
+        rX = sim.width - scale.width
+      }
+      if (rY < sim.height - scale.height) {
+        rY = sim.height - scale.height
+      }
+      scale.x = rX
+      scale.y = rY
+    },
+    // 按钮按下开始向右旋转
+    startRotateRight(e) {
+      const { scale } = this
+      scale.rotateRight = true
+      const rotate = () => {
+        if (scale.rotateRight) {
+          const degree = ++scale.degree
+          this.createImg(degree)
+          setTimeout(function() {
+            rotate()
+          }, 60)
+        }
+      }
+      rotate()
+    },
+    // 按钮按下开始向左旋转
+    startRotateLeft(e) {
+      const { scale } = this
+      scale.rotateLeft = true
+      const rotate = () => {
+        if (scale.rotateLeft) {
+          const degree = --scale.degree
+          this.createImg(degree)
+          setTimeout(function() {
+            rotate()
+          }, 60)
+        }
+      }
+      rotate()
+    },
+    // 停止旋转
+    endRotate() {
+      const { scale } = this
+      scale.rotateLeft = false
+      scale.rotateRight = false
+    },
+    // 按钮按下开始放大
+    startZoomAdd(e) {
+      const { scale } = this
+      scale.zoomAddOn = true
+      const zoom = () => {
+        if (scale.zoomAddOn) {
+          const range = scale.range >= 100 ? 100 : ++scale.range
+          this.zoomImg(range)
+          setTimeout(function() {
+            zoom()
+          }, 60)
+        }
+      }
+      zoom()
+    },
+    // 按钮松开或移开取消放大
+    endZoomAdd(e) {
+      this.scale.zoomAddOn = false
+    },
+    // 按钮按下开始缩小
+    startZoomSub(e) {
+      const { scale } = this
+      scale.zoomSubOn = true
+      const zoom = () => {
+        if (scale.zoomSubOn) {
+          const range = scale.range <= 0 ? 0 : --scale.range
+          this.zoomImg(range)
+          setTimeout(function() {
+            zoom()
+          }, 60)
+        }
+      }
+      zoom()
+    },
+    // 按钮松开或移开取消缩小
+    endZoomSub(e) {
+      const { scale } = this
+      scale.zoomSubOn = false
+    },
+    zoomChange(e) {
+      this.zoomImg(e.target.value)
+    },
+    // 缩放原图
+    zoomImg(newRange) {
+      const { sourceImgMasking, scale } = this
+      const {
+        maxWidth,
+        maxHeight,
+        minWidth,
+        minHeight,
+        width,
+        height,
+        x,
+        y
+      } = scale
+      const sim = sourceImgMasking
+      // 蒙版宽高
+      const sWidth = sim.width
+      const sHeight = sim.height
+      // 新宽高
+      const nWidth = minWidth + ((maxWidth - minWidth) * newRange) / 100
+      const nHeight = minHeight + ((maxHeight - minHeight) * newRange) / 100
+      // 新坐标(根据蒙版中心点缩放)
+      let nX = sWidth / 2 - (nWidth / width) * (sWidth / 2 - x)
+      let nY = sHeight / 2 - (nHeight / height) * (sHeight / 2 - y)
+      // 判断新坐标是否超过蒙版限制
+      if (nX > 0) {
+        nX = 0
+      }
+      if (nY > 0) {
+        nY = 0
+      }
+      if (nX < sWidth - nWidth) {
+        nX = sWidth - nWidth
+      }
+      if (nY < sHeight - nHeight) {
+        nY = sHeight - nHeight
+      }
+      // 赋值处理
+      scale.x = nX
+      scale.y = nY
+      scale.width = nWidth
+      scale.height = nHeight
+      scale.range = newRange
+      setTimeout(() => {
+        if (scale.range === newRange) {
+          this.createImg()
+        }
+      }, 300)
+    },
+    // 生成需求图片
+    createImg(e) {
+      const {
+        mime,
+        sourceImg,
+        scale: { x, y, width, height, degree },
+        sourceImgMasking: { scale }
+      } = this
+      const canvas = this.$refs.canvas
+      const ctx = canvas.getContext('2d')
+      if (e) {
+        // 取消鼠标按下移动状态
+        this.sourceImgMouseDown.on = false
+      }
+      canvas.width = this.width
+      canvas.height = this.height
+      ctx.clearRect(0, 0, this.width, this.height)
+      // 将透明区域设置为白色底边
+      ctx.fillStyle = '#fff'
+      ctx.fillRect(0, 0, this.width, this.height)
+      ctx.translate(this.width * 0.5, this.height * 0.5)
+      ctx.rotate((Math.PI * degree) / 180)
+      ctx.translate(-this.width * 0.5, -this.height * 0.5)
+      ctx.drawImage(
+        sourceImg,
+        x / scale,
+        y / scale,
+        width / scale,
+        height / scale
+      )
+      this.createImgUrl = canvas.toDataURL(mime)
+    },
+    prepareUpload() {
+      const { url, createImgUrl, field, ki } = this
+      this.$emit('crop-success', createImgUrl, field, ki)
+      if (typeof url === 'string' && url) {
+        this.upload()
+      } else {
+        this.off()
+      }
+    },
+    // 上传图片
+    upload() {
+      const {
+        lang,
+        imgFormat,
+        mime,
+        url,
+        params,
+        field,
+        ki,
+        createImgUrl
+      } = this
+      const fmData = new FormData()
+      fmData.append(
+        field,
+        data2blob(createImgUrl, mime),
+        field + '.' + imgFormat
+      )
+      // 添加其他参数
+      if (typeof params === 'object' && params) {
+        Object.keys(params).forEach(k => {
+          fmData.append(k, params[k])
+        })
+      }
+      // 监听进度回调
+      // const uploadProgress = (event) => {
+      //   if (event.lengthComputable) {
+      //     this.progress = 100 * Math.round(event.loaded) / event.total
+      //   }
+      // }
+      // 上传文件
+      this.reset()
+      this.loading = 1
+      this.setStep(3)
+      request({
+        url,
+        method: 'post',
+        data: fmData
+      })
+        .then(resData => {
+          this.loading = 2
+          this.$emit('crop-upload-success', resData.data)
+        })
+        .catch(err => {
+          if (this.value) {
+            this.loading = 3
+            this.hasError = true
+            this.errorMsg = lang.fail
+            this.$emit('crop-upload-fail', err, field, ki)
+          }
+        })
+    },
+    closeHandler(e) {
+      if (this.value && (e.key === 'Escape' || e.keyCode === 27)) {
+        this.off()
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+@charset "UTF-8";
+@-webkit-keyframes vicp_progress {
+  0% {
+    background-position-y: 0;
+  }
+  100% {
+    background-position-y: 40px;
+  }
+}
+@keyframes vicp_progress {
+  0% {
+    background-position-y: 0;
+  }
+  100% {
+    background-position-y: 40px;
+  }
+}
+@-webkit-keyframes vicp {
+  0% {
+    opacity: 0;
+    -webkit-transform: scale(0) translatey(-60px);
+    transform: scale(0) translatey(-60px);
+  }
+  100% {
+    opacity: 1;
+    -webkit-transform: scale(1) translatey(0);
+    transform: scale(1) translatey(0);
+  }
+}
+@keyframes vicp {
+  0% {
+    opacity: 0;
+    -webkit-transform: scale(0) translatey(-60px);
+    transform: scale(0) translatey(-60px);
+  }
+  100% {
+    opacity: 1;
+    -webkit-transform: scale(1) translatey(0);
+    transform: scale(1) translatey(0);
+  }
+}
+.vue-image-crop-upload {
+  position: fixed;
+  display: block;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  z-index: 10000;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.65);
+  -webkit-tap-highlight-color: transparent;
+  -moz-tap-highlight-color: transparent;
+}
+.vue-image-crop-upload .vicp-wrap {
+  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  position: fixed;
+  display: block;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  z-index: 10000;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  margin: auto;
+  width: 600px;
+  height: 330px;
+  padding: 25px;
+  background-color: #fff;
+  border-radius: 2px;
+  -webkit-animation: vicp 0.12s ease-in;
+  animation: vicp 0.12s ease-in;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-close {
+  position: absolute;
+  right: -30px;
+  top: -30px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4 {
+  position: relative;
+  display: block;
+  width: 30px;
+  height: 30px;
+  cursor: pointer;
+  -webkit-transition: -webkit-transform 0.18s;
+  transition: -webkit-transform 0.18s;
+  transition: transform 0.18s;
+  transition: transform 0.18s, -webkit-transform 0.18s;
+  -webkit-transform: rotate(0);
+  -ms-transform: rotate(0);
+  transform: rotate(0);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4::after,
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4::before {
+  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  content: "";
+  position: absolute;
+  top: 12px;
+  left: 4px;
+  width: 20px;
+  height: 3px;
+  -webkit-transform: rotate(45deg);
+  -ms-transform: rotate(45deg);
+  transform: rotate(45deg);
+  background-color: #fff;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4::after {
+  -webkit-transform: rotate(-45deg);
+  -ms-transform: rotate(-45deg);
+  transform: rotate(-45deg);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4:hover {
+  -webkit-transform: rotate(90deg);
+  -ms-transform: rotate(90deg);
+  transform: rotate(90deg);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area {
+  position: relative;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  padding: 35px;
+  height: 170px;
+  background-color: rgba(0, 0, 0, 0.03);
+  text-align: center;
+  border: 1px dashed rgba(0, 0, 0, 0.08);
+  overflow: hidden;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-icon1 {
+  display: block;
+  margin: 0 auto 6px;
+  width: 42px;
+  height: 42px;
+  overflow: hidden;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step1
+  .vicp-drop-area
+  .vicp-icon1
+  .vicp-icon1-arrow {
+  display: block;
+  margin: 0 auto;
+  width: 0;
+  height: 0;
+  border-bottom: 14.7px solid rgba(0, 0, 0, 0.3);
+  border-left: 14.7px solid transparent;
+  border-right: 14.7px solid transparent;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step1
+  .vicp-drop-area
+  .vicp-icon1
+  .vicp-icon1-body {
+  display: block;
+  width: 12.6px;
+  height: 14.7px;
+  margin: 0 auto;
+  background-color: rgba(0, 0, 0, 0.3);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step1
+  .vicp-drop-area
+  .vicp-icon1
+  .vicp-icon1-bottom {
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  display: block;
+  height: 12.6px;
+  border: 6px solid rgba(0, 0, 0, 0.3);
+  border-top: none;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-hint {
+  display: block;
+  padding: 15px;
+  font-size: 14px;
+  color: #666;
+  line-height: 30px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step1
+  .vicp-drop-area
+  .vicp-no-supported-hint {
+  display: block;
+  position: absolute;
+  top: 0;
+  left: 0;
+  padding: 30px;
+  width: 100%;
+  height: 60px;
+  line-height: 30px;
+  background-color: #eee;
+  text-align: center;
+  color: #666;
+  font-size: 14px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area:hover {
+  cursor: pointer;
+  border-color: rgba(0, 0, 0, 0.1);
+  background-color: rgba(0, 0, 0, 0.05);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop {
+  overflow: hidden;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left {
+  float: left;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-img-container {
+  position: relative;
+  display: block;
+  width: 240px;
+  height: 180px;
+  background-color: #e5e5e0;
+  overflow: hidden;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-img-container
+  .vicp-img {
+  position: absolute;
+  display: block;
+  cursor: move;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-img-container
+  .vicp-img-shade {
+  -webkit-box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+  box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+  position: absolute;
+  background-color: rgba(241, 242, 243, 0.8);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-img-container
+  .vicp-img-shade.vicp-img-shade-1 {
+  top: 0;
+  left: 0;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-img-container
+  .vicp-img-shade.vicp-img-shade-2 {
+  bottom: 0;
+  right: 0;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-rotate {
+  position: relative;
+  width: 240px;
+  height: 18px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-rotate
+  i {
+  display: block;
+  width: 18px;
+  height: 18px;
+  border-radius: 100%;
+  line-height: 18px;
+  text-align: center;
+  font-size: 12px;
+  font-weight: bold;
+  background-color: rgba(0, 0, 0, 0.08);
+  color: #fff;
+  overflow: hidden;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-rotate
+  i:hover {
+  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  cursor: pointer;
+  background-color: rgba(0, 0, 0, 0.14);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-rotate
+  i:first-child {
+  float: left;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-rotate
+  i:last-child {
+  float: right;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range {
+  position: relative;
+  margin: 30px 0 10px 0;
+  width: 240px;
+  height: 18px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon5,
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon6 {
+  position: absolute;
+  top: 0;
+  width: 18px;
+  height: 18px;
+  border-radius: 100%;
+  background-color: rgba(0, 0, 0, 0.08);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon5:hover,
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon6:hover {
+  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  cursor: pointer;
+  background-color: rgba(0, 0, 0, 0.14);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon5 {
+  left: 0;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon5::before {
+  position: absolute;
+  content: "";
+  display: block;
+  left: 3px;
+  top: 8px;
+  width: 12px;
+  height: 2px;
+  background-color: #fff;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon6 {
+  right: 0;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon6::before {
+  position: absolute;
+  content: "";
+  display: block;
+  left: 3px;
+  top: 8px;
+  width: 12px;
+  height: 2px;
+  background-color: #fff;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon6::after {
+  position: absolute;
+  content: "";
+  display: block;
+  top: 3px;
+  left: 8px;
+  width: 2px;
+  height: 12px;
+  background-color: #fff;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"] {
+  display: block;
+  padding-top: 5px;
+  margin: 0 auto;
+  width: 180px;
+  height: 8px;
+  vertical-align: top;
+  background: transparent;
+  -webkit-appearance: none;
+  -moz-appearance: none;
+  appearance: none;
+  cursor: pointer;
+  /* 滑块
+               ---------------------------------------------------------------*/
+  /* 轨道
+               ---------------------------------------------------------------*/
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:focus {
+  outline: none;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-webkit-slider-thumb {
+  -webkit-box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+  box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+  -webkit-appearance: none;
+  appearance: none;
+  margin-top: -3px;
+  width: 12px;
+  height: 12px;
+  background-color: #61c091;
+  border-radius: 100%;
+  border: none;
+  -webkit-transition: 0.2s;
+  transition: 0.2s;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-moz-range-thumb {
+  box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+  -moz-appearance: none;
+  appearance: none;
+  width: 12px;
+  height: 12px;
+  background-color: #61c091;
+  border-radius: 100%;
+  border: none;
+  -webkit-transition: 0.2s;
+  transition: 0.2s;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-ms-thumb {
+  box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+  appearance: none;
+  width: 12px;
+  height: 12px;
+  background-color: #61c091;
+  border: none;
+  border-radius: 100%;
+  -webkit-transition: 0.2s;
+  transition: 0.2s;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:active::-moz-range-thumb {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  width: 14px;
+  height: 14px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:active::-ms-thumb {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  width: 14px;
+  height: 14px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:active::-webkit-slider-thumb {
+  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  margin-top: -4px;
+  width: 14px;
+  height: 14px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-webkit-slider-runnable-track {
+  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  width: 100%;
+  height: 6px;
+  cursor: pointer;
+  border-radius: 2px;
+  border: none;
+  background-color: rgba(68, 170, 119, 0.3);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-moz-range-track {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  width: 100%;
+  height: 6px;
+  cursor: pointer;
+  border-radius: 2px;
+  border: none;
+  background-color: rgba(68, 170, 119, 0.3);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-ms-track {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  width: 100%;
+  cursor: pointer;
+  background: transparent;
+  border-color: transparent;
+  color: transparent;
+  height: 6px;
+  border-radius: 2px;
+  border: none;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-ms-fill-lower {
+  background-color: rgba(68, 170, 119, 0.3);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-ms-fill-upper {
+  background-color: rgba(68, 170, 119, 0.15);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:focus::-webkit-slider-runnable-track {
+  background-color: rgba(68, 170, 119, 0.5);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:focus::-moz-range-track {
+  background-color: rgba(68, 170, 119, 0.5);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:focus::-ms-fill-lower {
+  background-color: rgba(68, 170, 119, 0.45);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:focus::-ms-fill-upper {
+  background-color: rgba(68, 170, 119, 0.25);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right {
+  float: right;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-right
+  .vicp-preview {
+  height: 150px;
+  overflow: hidden;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-right
+  .vicp-preview
+  .vicp-preview-item {
+  position: relative;
+  padding: 5px;
+  width: 100px;
+  height: 100px;
+  float: left;
+  margin-right: 16px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-right
+  .vicp-preview
+  .vicp-preview-item
+  span {
+  position: absolute;
+  bottom: -30px;
+  width: 100%;
+  font-size: 14px;
+  color: #bbb;
+  display: block;
+  text-align: center;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-right
+  .vicp-preview
+  .vicp-preview-item
+  img {
+  position: absolute;
+  display: block;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  margin: auto;
+  padding: 3px;
+  background-color: #fff;
+  border: 1px solid rgba(0, 0, 0, 0.15);
+  overflow: hidden;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-right
+  .vicp-preview
+  .vicp-preview-item.vicp-preview-item-circle {
+  margin-right: 0;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-right
+  .vicp-preview
+  .vicp-preview-item.vicp-preview-item-circle
+  img {
+  border-radius: 100%;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload {
+  position: relative;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  padding: 35px;
+  height: 170px;
+  background-color: rgba(0, 0, 0, 0.03);
+  text-align: center;
+  border: 1px dashed #ddd;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-loading {
+  display: block;
+  padding: 15px;
+  font-size: 16px;
+  color: #999;
+  line-height: 30px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-progress-wrap {
+  margin-top: 12px;
+  background-color: rgba(0, 0, 0, 0.08);
+  border-radius: 3px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step3
+  .vicp-upload
+  .vicp-progress-wrap
+  .vicp-progress {
+  position: relative;
+  display: block;
+  height: 5px;
+  border-radius: 3px;
+  background-color: #4a7;
+  -webkit-box-shadow: 0 2px 6px 0 rgba(68, 170, 119, 0.3);
+  box-shadow: 0 2px 6px 0 rgba(68, 170, 119, 0.3);
+  -webkit-transition: width 0.15s linear;
+  transition: width 0.15s linear;
+  background-image: -webkit-linear-gradient(
+    135deg,
+    rgba(255, 255, 255, 0.2) 25%,
+    transparent 25%,
+    transparent 50%,
+    rgba(255, 255, 255, 0.2) 50%,
+    rgba(255, 255, 255, 0.2) 75%,
+    transparent 75%,
+    transparent
+  );
+  background-image: linear-gradient(
+    -45deg,
+    rgba(255, 255, 255, 0.2) 25%,
+    transparent 25%,
+    transparent 50%,
+    rgba(255, 255, 255, 0.2) 50%,
+    rgba(255, 255, 255, 0.2) 75%,
+    transparent 75%,
+    transparent
+  );
+  background-size: 40px 40px;
+  -webkit-animation: vicp_progress 0.5s linear infinite;
+  animation: vicp_progress 0.5s linear infinite;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step3
+  .vicp-upload
+  .vicp-progress-wrap
+  .vicp-progress::after {
+  content: "";
+  position: absolute;
+  display: block;
+  top: -3px;
+  right: -3px;
+  width: 9px;
+  height: 9px;
+  border: 1px solid rgba(245, 246, 247, 0.7);
+  -webkit-box-shadow: 0 1px 4px 0 rgba(68, 170, 119, 0.7);
+  box-shadow: 0 1px 4px 0 rgba(68, 170, 119, 0.7);
+  border-radius: 100%;
+  background-color: #4a7;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-error,
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-success {
+  height: 100px;
+  line-height: 100px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-operate {
+  position: absolute;
+  right: 20px;
+  bottom: 20px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-operate a {
+  position: relative;
+  float: left;
+  display: block;
+  margin-left: 10px;
+  width: 100px;
+  height: 36px;
+  line-height: 36px;
+  text-align: center;
+  cursor: pointer;
+  font-size: 14px;
+  color: #4a7;
+  border-radius: 2px;
+  overflow: hidden;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-operate a:hover {
+  background-color: rgba(0, 0, 0, 0.03);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-error,
+.vue-image-crop-upload .vicp-wrap .vicp-success {
+  display: block;
+  font-size: 14px;
+  line-height: 24px;
+  height: 24px;
+  color: #d10;
+  text-align: center;
+  vertical-align: top;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-success {
+  color: #4a7;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-icon3 {
+  position: relative;
+  display: inline-block;
+  width: 20px;
+  height: 20px;
+  top: 4px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-icon3::after {
+  position: absolute;
+  top: 3px;
+  left: 6px;
+  width: 6px;
+  height: 10px;
+  border-width: 0 2px 2px 0;
+  border-color: #4a7;
+  border-style: solid;
+  -webkit-transform: rotate(45deg);
+  -ms-transform: rotate(45deg);
+  transform: rotate(45deg);
+  content: "";
+}
+.vue-image-crop-upload .vicp-wrap .vicp-icon2 {
+  position: relative;
+  display: inline-block;
+  width: 20px;
+  height: 20px;
+  top: 4px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-icon2::after,
+.vue-image-crop-upload .vicp-wrap .vicp-icon2::before {
+  content: "";
+  position: absolute;
+  top: 9px;
+  left: 4px;
+  width: 13px;
+  height: 2px;
+  background-color: #d10;
+  -webkit-transform: rotate(45deg);
+  -ms-transform: rotate(45deg);
+  transform: rotate(45deg);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-icon2::after {
+  -webkit-transform: rotate(-45deg);
+  -ms-transform: rotate(-45deg);
+  transform: rotate(-45deg);
+}
+.e-ripple {
+  position: absolute;
+  border-radius: 100%;
+  background-color: rgba(0, 0, 0, 0.15);
+  background-clip: padding-box;
+  pointer-events: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+  -webkit-transform: scale(0);
+  -ms-transform: scale(0);
+  transform: scale(0);
+  opacity: 1;
+}
+.e-ripple.z-active {
+  opacity: 0;
+  -webkit-transform: scale(2);
+  -ms-transform: scale(2);
+  transform: scale(2);
+  -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
+  transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
+  transition: opacity 1.2s ease-out, transform 0.6s ease-out;
+  transition: opacity 1.2s ease-out, transform 0.6s ease-out,
+    -webkit-transform 0.6s ease-out;
+}
+</style>
diff --git a/frontend/src/components/ImageCropper/utils/data2blob.js b/frontend/src/components/ImageCropper/utils/data2blob.js
new file mode 100644
index 0000000..9c47f8a
--- /dev/null
+++ b/frontend/src/components/ImageCropper/utils/data2blob.js
@@ -0,0 +1,19 @@
+/**
+ * database64文件格式转换为2进制
+ *
+ * @param  {[String]} data dataURL 的格式为 “data:image/png;base64,****”,逗号之前都是一些说明性的文字,我们只需要逗号之后的就行了
+ * @param  {[String]} mime [description]
+ * @return {[blob]}      [description]
+ */
+export default function(data, mime) {
+  data = data.split(',')[1]
+  data = window.atob(data)
+  var ia = new Uint8Array(data.length)
+  for (var i = 0; i < data.length; i++) {
+    ia[i] = data.charCodeAt(i)
+  }
+  // canvas.toDataURL 返回的默认格式就是 image/png
+  return new Blob([ia], {
+    type: mime
+  })
+}
diff --git a/frontend/src/components/ImageCropper/utils/effectRipple.js b/frontend/src/components/ImageCropper/utils/effectRipple.js
new file mode 100644
index 0000000..46a0164
--- /dev/null
+++ b/frontend/src/components/ImageCropper/utils/effectRipple.js
@@ -0,0 +1,39 @@
+/**
+ * 点击波纹效果
+ *
+ * @param  {[event]} e        [description]
+ * @param  {[Object]} arg_opts [description]
+ * @return {[bollean]}          [description]
+ */
+export default function(e, arg_opts) {
+  var opts = Object.assign({
+    ele: e.target, // 波纹作用元素
+    type: 'hit', // hit点击位置扩散center中心点扩展
+    bgc: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
+  }, arg_opts)
+  var target = opts.ele
+  if (target) {
+    var rect = target.getBoundingClientRect()
+    var ripple = target.querySelector('.e-ripple')
+    if (!ripple) {
+      ripple = document.createElement('span')
+      ripple.className = 'e-ripple'
+      ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
+      target.appendChild(ripple)
+    } else {
+      ripple.className = 'e-ripple'
+    }
+    switch (opts.type) {
+      case 'center':
+        ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px'
+        ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px'
+        break
+      default:
+        ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.body.scrollTop) + 'px'
+        ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.body.scrollLeft) + 'px'
+    }
+    ripple.style.backgroundColor = opts.bgc
+    ripple.className = 'e-ripple z-active'
+    return false
+  }
+}
diff --git a/frontend/src/components/ImageCropper/utils/language.js b/frontend/src/components/ImageCropper/utils/language.js
new file mode 100644
index 0000000..727872d
--- /dev/null
+++ b/frontend/src/components/ImageCropper/utils/language.js
@@ -0,0 +1,232 @@
+export default {
+  zh: {
+    hint: '点击,或拖动图片至此处',
+    loading: '正在上传……',
+    noSupported: '浏览器不支持该功能,请使用IE10以上或其他现在浏览器!',
+    success: '上传成功',
+    fail: '图片上传失败',
+    preview: '头像预览',
+    btn: {
+      off: '取消',
+      close: '关闭',
+      back: '上一步',
+      save: '保存'
+    },
+    error: {
+      onlyImg: '仅限图片格式',
+      outOfSize: '单文件大小不能超过 ',
+      lowestPx: '图片最低像素为(宽*高):'
+    }
+  },
+  'zh-tw': {
+    hint: '點擊,或拖動圖片至此處',
+    loading: '正在上傳……',
+    noSupported: '瀏覽器不支持該功能,請使用IE10以上或其他現代瀏覽器!',
+    success: '上傳成功',
+    fail: '圖片上傳失敗',
+    preview: '頭像預覽',
+    btn: {
+      off: '取消',
+      close: '關閉',
+      back: '上一步',
+      save: '保存'
+    },
+    error: {
+      onlyImg: '僅限圖片格式',
+      outOfSize: '單文件大小不能超過 ',
+      lowestPx: '圖片最低像素為(寬*高):'
+    }
+  },
+  en: {
+    hint: 'Click or drag the file here to upload',
+    loading: 'Uploading…',
+    noSupported: 'Browser is not supported, please use IE10+ or other browsers',
+    success: 'Upload success',
+    fail: 'Upload failed',
+    preview: 'Preview',
+    btn: {
+      off: 'Cancel',
+      close: 'Close',
+      back: 'Back',
+      save: 'Save'
+    },
+    error: {
+      onlyImg: 'Image only',
+      outOfSize: 'Image exceeds size limit: ',
+      lowestPx: 'Image\'s size is too low. Expected at least: '
+    }
+  },
+  ro: {
+    hint: 'Atinge sau trage fișierul aici',
+    loading: 'Se încarcă',
+    noSupported: 'Browser-ul tău nu suportă acest feature. Te rugăm încearcă cu alt browser.',
+    success: 'S-a încărcat cu succes',
+    fail: 'A apărut o problemă la încărcare',
+    preview: 'Previzualizează',
+
+    btn: {
+      off: 'Anulează',
+      close: 'Închide',
+      back: 'Înapoi',
+      save: 'Salvează'
+    },
+
+    error: {
+      onlyImg: 'Doar imagini',
+      outOfSize: 'Imaginea depășește limita de: ',
+      loewstPx: 'Imaginea este prea mică; Minim: '
+    }
+  },
+  ru: {
+    hint: 'Нажмите, или перетащите файл в это окно',
+    loading: 'Загружаю……',
+    noSupported: 'Ваш браузер не поддерживается, пожалуйста, используйте IE10 + или другие браузеры',
+    success: 'Загрузка выполнена успешно',
+    fail: 'Ошибка загрузки',
+    preview: 'Предпросмотр',
+    btn: {
+      off: 'Отменить',
+      close: 'Закрыть',
+      back: 'Назад',
+      save: 'Сохранить'
+    },
+    error: {
+      onlyImg: 'Только изображения',
+      outOfSize: 'Изображение превышает предельный размер: ',
+      lowestPx: 'Минимальный размер изображения: '
+    }
+  },
+  'pt-br': {
+    hint: 'Clique ou arraste o arquivo aqui para carregar',
+    loading: 'Carregando…',
+    noSupported: 'Browser não suportado, use o IE10+ ou outro browser',
+    success: 'Sucesso ao carregar imagem',
+    fail: 'Falha ao carregar imagem',
+    preview: 'Pré-visualizar',
+    btn: {
+      off: 'Cancelar',
+      close: 'Fechar',
+      back: 'Voltar',
+      save: 'Salvar'
+    },
+    error: {
+      onlyImg: 'Apenas imagens',
+      outOfSize: 'A imagem excede o limite de tamanho: ',
+      lowestPx: 'O tamanho da imagem é muito pequeno. Tamanho mínimo: '
+    }
+  },
+  fr: {
+    hint: 'Cliquez ou glissez le fichier ici.',
+    loading: 'Téléchargement…',
+    noSupported: 'Votre navigateur n\'est pas supporté. Utilisez IE10 + ou un autre navigateur s\'il vous plaît.',
+    success: 'Téléchargement réussit',
+    fail: 'Téléchargement echoué',
+    preview: 'Aperçu',
+    btn: {
+      off: 'Annuler',
+      close: 'Fermer',
+      back: 'Retour',
+      save: 'Enregistrer'
+    },
+    error: {
+      onlyImg: 'Image uniquement',
+      outOfSize: 'L\'image sélectionnée dépasse la taille maximum: ',
+      lowestPx: 'L\'image sélectionnée est trop petite. Dimensions attendues: '
+    }
+  },
+  nl: {
+    hint: 'Klik hier of sleep een afbeelding in dit vlak',
+    loading: 'Uploaden…',
+    noSupported: 'Je browser wordt helaas niet ondersteund. Gebruik IE10+ of een andere browser.',
+    success: 'Upload succesvol',
+    fail: 'Upload mislukt',
+    preview: 'Voorbeeld',
+    btn: {
+      off: 'Annuleren',
+      close: 'Sluiten',
+      back: 'Terug',
+      save: 'Opslaan'
+    },
+    error: {
+      onlyImg: 'Alleen afbeeldingen',
+      outOfSize: 'De afbeelding is groter dan: ',
+      lowestPx: 'De afbeelding is te klein! Minimale afmetingen: '
+    }
+  },
+  tr: {
+    hint: 'Tıkla veya yüklemek istediğini buraya sürükle',
+    loading: 'Yükleniyor…',
+    noSupported: 'Tarayıcı desteklenmiyor, lütfen IE10+ veya farklı tarayıcı kullanın',
+    success: 'Yükleme başarılı',
+    fail: 'Yüklemede hata oluştu',
+    preview: 'Önizle',
+    btn: {
+      off: 'İptal',
+      close: 'Kapat',
+      back: 'Geri',
+      save: 'Kaydet'
+    },
+    error: {
+      onlyImg: 'Sadece resim',
+      outOfSize: 'Resim yükleme limitini aşıyor: ',
+      lowestPx: 'Resmin boyutu çok küçük. En az olması gereken: '
+    }
+  },
+  'es-MX': {
+    hint: 'Selecciona o arrastra una imagen',
+    loading: 'Subiendo...',
+    noSupported: 'Tu navegador no es soportado, porfavor usa IE10+ u otros navegadores mas recientes',
+    success: 'Subido exitosamente',
+    fail: 'Sucedió un error',
+    preview: 'Vista previa',
+    btn: {
+      off: 'Cancelar',
+      close: 'Cerrar',
+      back: 'Atras',
+      save: 'Guardar'
+    },
+    error: {
+      onlyImg: 'Unicamente imagenes',
+      outOfSize: 'La imagen excede el tamaño maximo:',
+      lowestPx: 'La imagen es demasiado pequeño. Se espera por lo menos:'
+    }
+  },
+  de: {
+    hint: 'Klick hier oder zieh eine Datei hier rein zum Hochladen',
+    loading: 'Hochladen…',
+    noSupported: 'Browser wird nicht unterstützt, bitte verwende IE10+ oder andere Browser',
+    success: 'Upload erfolgreich',
+    fail: 'Upload fehlgeschlagen',
+    preview: 'Vorschau',
+    btn: {
+      off: 'Abbrechen',
+      close: 'Schließen',
+      back: 'Zurück',
+      save: 'Speichern'
+    },
+    error: {
+      onlyImg: 'Nur Bilder',
+      outOfSize: 'Das Bild ist zu groß: ',
+      lowestPx: 'Das Bild ist zu klein. Mindestens: '
+    }
+  },
+  ja: {
+    hint: 'クリック・ドラッグしてファイルをアップロード',
+    loading: 'アップロード中...',
+    noSupported: 'このブラウザは対応されていません。IE10+かその他の主要ブラウザをお使いください。',
+    success: 'アップロード成功',
+    fail: 'アップロード失敗',
+    preview: 'プレビュー',
+    btn: {
+      off: 'キャンセル',
+      close: '閉じる',
+      back: '戻る',
+      save: '保存'
+    },
+    error: {
+      onlyImg: '画像のみ',
+      outOfSize: '画像サイズが上限を超えています。上限: ',
+      lowestPx: '画像が小さすぎます。最小サイズ: '
+    }
+  }
+}
diff --git a/frontend/src/components/ImageCropper/utils/mimes.js b/frontend/src/components/ImageCropper/utils/mimes.js
new file mode 100644
index 0000000..e20c085
--- /dev/null
+++ b/frontend/src/components/ImageCropper/utils/mimes.js
@@ -0,0 +1,7 @@
+export default {
+  'jpg': 'image/jpeg',
+  'png': 'image/png',
+  'gif': 'image/gif',
+  'svg': 'image/svg+xml',
+  'psd': 'image/photoshop'
+}
diff --git a/frontend/src/components/JsonEditor/index.vue b/frontend/src/components/JsonEditor/index.vue
new file mode 100644
index 0000000..c05b090
--- /dev/null
+++ b/frontend/src/components/JsonEditor/index.vue
@@ -0,0 +1,77 @@
+<template>
+  <div class="json-editor">
+    <textarea ref="textarea" />
+  </div>
+</template>
+
+<script>
+import CodeMirror from 'codemirror'
+import 'codemirror/addon/lint/lint.css'
+import 'codemirror/lib/codemirror.css'
+import 'codemirror/theme/rubyblue.css'
+require('script-loader!jsonlint')
+import 'codemirror/mode/javascript/javascript'
+import 'codemirror/addon/lint/lint'
+import 'codemirror/addon/lint/json-lint'
+
+export default {
+  name: 'JsonEditor',
+  /* eslint-disable vue/require-prop-types */
+  props: ['value'],
+  data() {
+    return {
+      jsonEditor: false
+    }
+  },
+  watch: {
+    value(value) {
+      const editorValue = this.jsonEditor.getValue()
+      if (value !== editorValue) {
+        this.jsonEditor.setValue(JSON.stringify(this.value, null, 2))
+      }
+    }
+  },
+  mounted() {
+    this.jsonEditor = CodeMirror.fromTextArea(this.$refs.textarea, {
+      lineNumbers: true,
+      mode: 'application/json',
+      gutters: ['CodeMirror-lint-markers'],
+      theme: 'rubyblue',
+      lint: true
+    })
+
+    this.jsonEditor.setValue(JSON.stringify(this.value, null, 2))
+    this.jsonEditor.on('change', cm => {
+      this.$emit('changed', cm.getValue())
+      this.$emit('input', cm.getValue())
+    })
+  },
+  methods: {
+    getValue() {
+      return this.jsonEditor.getValue()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.json-editor {
+  height: 100%;
+  position: relative;
+
+  ::v-deep {
+    .CodeMirror {
+      height: auto;
+      min-height: 300px;
+    }
+
+    .CodeMirror-scroll {
+      min-height: 300px;
+    }
+
+    .cm-s-rubyblue span.cm-string {
+      color: #F08047;
+    }
+  }
+}
+</style>
diff --git a/frontend/src/components/Kanban/index.vue b/frontend/src/components/Kanban/index.vue
new file mode 100644
index 0000000..82f7dd7
--- /dev/null
+++ b/frontend/src/components/Kanban/index.vue
@@ -0,0 +1,99 @@
+<template>
+  <div class="board-column">
+    <div class="board-column-header">
+      {{ headerText }}
+    </div>
+    <draggable
+      :list="list"
+      v-bind="$attrs"
+      class="board-column-content"
+      :set-data="setData"
+    >
+      <div v-for="element in list" :key="element.id" class="board-item">
+        {{ element.name }} {{ element.id }}
+      </div>
+    </draggable>
+  </div>
+</template>
+
+<script>
+import draggable from 'vuedraggable'
+
+export default {
+  name: 'DragKanbanDemo',
+  components: {
+    draggable
+  },
+  props: {
+    headerText: {
+      type: String,
+      default: 'Header'
+    },
+    options: {
+      type: Object,
+      default() {
+        return {}
+      }
+    },
+    list: {
+      type: Array,
+      default() {
+        return []
+      }
+    }
+  },
+  methods: {
+    setData(dataTransfer) {
+      // to avoid Firefox bug
+      // Detail see : https://github.com/RubaXa/Sortable/issues/1012
+      dataTransfer.setData('Text', '')
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.board-column {
+  min-width: 300px;
+  min-height: 100px;
+  height: auto;
+  overflow: hidden;
+  background: #f0f0f0;
+  border-radius: 3px;
+
+  .board-column-header {
+    height: 50px;
+    line-height: 50px;
+    overflow: hidden;
+    padding: 0 20px;
+    text-align: center;
+    background: #333;
+    color: #fff;
+    border-radius: 3px 3px 0 0;
+  }
+
+  .board-column-content {
+    height: auto;
+    overflow: hidden;
+    border: 10px solid transparent;
+    min-height: 60px;
+    display: flex;
+    justify-content: flex-start;
+    flex-direction: column;
+    align-items: center;
+
+    .board-item {
+      cursor: pointer;
+      width: 100%;
+      height: 64px;
+      margin: 5px 0;
+      background-color: #fff;
+      text-align: left;
+      line-height: 54px;
+      padding: 5px 10px;
+      box-sizing: border-box;
+      box-shadow: 0px 1px 3px 0 rgba(0, 0, 0, 0.2);
+    }
+  }
+}
+</style>
+
diff --git a/frontend/src/components/MDinput/index.vue b/frontend/src/components/MDinput/index.vue
new file mode 100644
index 0000000..c59ea34
--- /dev/null
+++ b/frontend/src/components/MDinput/index.vue
@@ -0,0 +1,360 @@
+<template>
+  <div :class="computedClasses" class="material-input__component">
+    <div :class="{iconClass:icon}">
+      <i v-if="icon" :class="['el-icon-' + icon]" class="el-input__icon material-input__icon" />
+      <input
+        v-if="type === 'email'"
+        v-model="currentValue"
+        :name="name"
+        :placeholder="fillPlaceHolder"
+        :readonly="readonly"
+        :disabled="disabled"
+        :autocomplete="autoComplete"
+        :required="required"
+        type="email"
+        class="material-input"
+        @focus="handleMdFocus"
+        @blur="handleMdBlur"
+        @input="handleModelInput"
+      >
+      <input
+        v-if="type === 'url'"
+        v-model="currentValue"
+        :name="name"
+        :placeholder="fillPlaceHolder"
+        :readonly="readonly"
+        :disabled="disabled"
+        :autocomplete="autoComplete"
+        :required="required"
+        type="url"
+        class="material-input"
+        @focus="handleMdFocus"
+        @blur="handleMdBlur"
+        @input="handleModelInput"
+      >
+      <input
+        v-if="type === 'number'"
+        v-model="currentValue"
+        :name="name"
+        :placeholder="fillPlaceHolder"
+        :step="step"
+        :readonly="readonly"
+        :disabled="disabled"
+        :autocomplete="autoComplete"
+        :max="max"
+        :min="min"
+        :minlength="minlength"
+        :maxlength="maxlength"
+        :required="required"
+        type="number"
+        class="material-input"
+        @focus="handleMdFocus"
+        @blur="handleMdBlur"
+        @input="handleModelInput"
+      >
+      <input
+        v-if="type === 'password'"
+        v-model="currentValue"
+        :name="name"
+        :placeholder="fillPlaceHolder"
+        :readonly="readonly"
+        :disabled="disabled"
+        :autocomplete="autoComplete"
+        :max="max"
+        :min="min"
+        :required="required"
+        type="password"
+        class="material-input"
+        @focus="handleMdFocus"
+        @blur="handleMdBlur"
+        @input="handleModelInput"
+      >
+      <input
+        v-if="type === 'tel'"
+        v-model="currentValue"
+        :name="name"
+        :placeholder="fillPlaceHolder"
+        :readonly="readonly"
+        :disabled="disabled"
+        :autocomplete="autoComplete"
+        :required="required"
+        type="tel"
+        class="material-input"
+        @focus="handleMdFocus"
+        @blur="handleMdBlur"
+        @input="handleModelInput"
+      >
+      <input
+        v-if="type === 'text'"
+        v-model="currentValue"
+        :name="name"
+        :placeholder="fillPlaceHolder"
+        :readonly="readonly"
+        :disabled="disabled"
+        :autocomplete="autoComplete"
+        :minlength="minlength"
+        :maxlength="maxlength"
+        :required="required"
+        type="text"
+        class="material-input"
+        @focus="handleMdFocus"
+        @blur="handleMdBlur"
+        @input="handleModelInput"
+      >
+      <span class="material-input-bar" />
+      <label class="material-label">
+        <slot />
+      </label>
+    </div>
+  </div>
+</template>
+
+<script>
+// source:https://github.com/wemake-services/vue-material-input/blob/master/src/components/MaterialInput.vue
+
+export default {
+  name: 'MdInput',
+  props: {
+    /* eslint-disable */
+    icon: String,
+    name: String,
+    type: {
+      type: String,
+      default: 'text'
+    },
+    value: [String, Number],
+    placeholder: String,
+    readonly: Boolean,
+    disabled: Boolean,
+    min: String,
+    max: String,
+    step: String,
+    minlength: Number,
+    maxlength: Number,
+    required: {
+      type: Boolean,
+      default: true
+    },
+    autoComplete: {
+      type: String,
+      default: 'off'
+    },
+    validateEvent: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data() {
+    return {
+      currentValue: this.value,
+      focus: false,
+      fillPlaceHolder: null
+    }
+  },
+  computed: {
+    computedClasses() {
+      return {
+        'material--active': this.focus,
+        'material--disabled': this.disabled,
+        'material--raised': Boolean(this.focus || this.currentValue) // has value
+      }
+    }
+  },
+  watch: {
+    value(newValue) {
+      this.currentValue = newValue
+    }
+  },
+  methods: {
+    handleModelInput(event) {
+      const value = event.target.value
+      this.$emit('input', value)
+      if (this.$parent.$options.componentName === 'ElFormItem') {
+        if (this.validateEvent) {
+          this.$parent.$emit('el.form.change', [value])
+        }
+      }
+      this.$emit('change', value)
+    },
+    handleMdFocus(event) {
+      this.focus = true
+      this.$emit('focus', event)
+      if (this.placeholder && this.placeholder !== '') {
+        this.fillPlaceHolder = this.placeholder
+      }
+    },
+    handleMdBlur(event) {
+      this.focus = false
+      this.$emit('blur', event)
+      this.fillPlaceHolder = null
+      if (this.$parent.$options.componentName === 'ElFormItem') {
+        if (this.validateEvent) {
+          this.$parent.$emit('el.form.blur', [this.currentValue])
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  // Fonts:
+  $font-size-base: 16px;
+  $font-size-small: 18px;
+  $font-size-smallest: 12px;
+  $font-weight-normal: normal;
+  $font-weight-bold: bold;
+  $apixel: 1px;
+  // Utils
+  $spacer: 12px;
+  $transition: 0.2s ease all;
+  $index: 0px;
+  $index-has-icon: 30px;
+  // Theme:
+  $color-white: white;
+  $color-grey: #9E9E9E;
+  $color-grey-light: #E0E0E0;
+  $color-blue: #2196F3;
+  $color-red: #F44336;
+  $color-black: black;
+  // Base clases:
+  %base-bar-pseudo {
+    content: '';
+    height: 1px;
+    width: 0;
+    bottom: 0;
+    position: absolute;
+    transition: $transition;
+  }
+
+  // Mixins:
+  @mixin slided-top() {
+    top: - ($font-size-base + $spacer);
+    left: 0;
+    font-size: $font-size-base;
+    font-weight: $font-weight-bold;
+  }
+
+  // Component:
+  .material-input__component {
+    margin-top: 36px;
+    position: relative;
+    * {
+      box-sizing: border-box;
+    }
+    .iconClass {
+      .material-input__icon {
+        position: absolute;
+        left: 0;
+        line-height: $font-size-base;
+        color: $color-blue;
+        top: $spacer;
+        width: $index-has-icon;
+        height: $font-size-base;
+        font-size: $font-size-base;
+        font-weight: $font-weight-normal;
+        pointer-events: none;
+      }
+      .material-label {
+        left: $index-has-icon;
+      }
+      .material-input {
+        text-indent: $index-has-icon;
+      }
+    }
+    .material-input {
+      font-size: $font-size-base;
+      padding: $spacer $spacer $spacer - $apixel * 10 $spacer / 2;
+      display: block;
+      width: 100%;
+      border: none;
+      line-height: 1;
+      border-radius: 0;
+      &:focus {
+        outline: none;
+        border: none;
+        border-bottom: 1px solid transparent; // fixes the height issue
+      }
+    }
+    .material-label {
+      font-weight: $font-weight-normal;
+      position: absolute;
+      pointer-events: none;
+      left: $index;
+      top: 0;
+      transition: $transition;
+      font-size: $font-size-small;
+    }
+    .material-input-bar {
+      position: relative;
+      display: block;
+      width: 100%;
+      &:before {
+        @extend %base-bar-pseudo;
+        left: 50%;
+      }
+      &:after {
+        @extend %base-bar-pseudo;
+        right: 50%;
+      }
+    }
+    // Disabled state:
+    &.material--disabled {
+      .material-input {
+        border-bottom-style: dashed;
+      }
+    }
+    // Raised state:
+    &.material--raised {
+      .material-label {
+        @include slided-top();
+      }
+    }
+    // Active state:
+    &.material--active {
+      .material-input-bar {
+        &:before,
+        &:after {
+          width: 50%;
+        }
+      }
+    }
+  }
+
+  .material-input__component {
+    background: $color-white;
+    .material-input {
+      background: none;
+      color: $color-black;
+      text-indent: $index;
+      border-bottom: 1px solid $color-grey-light;
+    }
+    .material-label {
+      color: $color-grey;
+    }
+    .material-input-bar {
+      &:before,
+      &:after {
+        background: $color-blue;
+      }
+    }
+    // Active state:
+    &.material--active {
+      .material-label {
+        color: $color-blue;
+      }
+    }
+    // Errors:
+    &.material--has-errors {
+      &.material--active .material-label {
+        color: $color-red;
+      }
+      .material-input-bar {
+        &:before,
+        &:after {
+          background: transparent;
+        }
+      }
+    }
+  }
+</style>
diff --git a/frontend/src/components/MarkdownEditor/default-options.js b/frontend/src/components/MarkdownEditor/default-options.js
new file mode 100644
index 0000000..303aa13
--- /dev/null
+++ b/frontend/src/components/MarkdownEditor/default-options.js
@@ -0,0 +1,31 @@
+// doc: https://nhnent.github.io/tui.editor/api/latest/ToastUIEditor.html#ToastUIEditor
+export default {
+  minHeight: '200px',
+  previewStyle: 'vertical',
+  useCommandShortcut: true,
+  useDefaultHTMLSanitizer: true,
+  usageStatistics: false,
+  hideModeSwitch: false,
+  toolbarItems: [
+    'heading',
+    'bold',
+    'italic',
+    'strike',
+    'divider',
+    'hr',
+    'quote',
+    'divider',
+    'ul',
+    'ol',
+    'task',
+    'indent',
+    'outdent',
+    'divider',
+    'table',
+    'image',
+    'link',
+    'divider',
+    'code',
+    'codeblock'
+  ]
+}
diff --git a/frontend/src/components/MarkdownEditor/index.vue b/frontend/src/components/MarkdownEditor/index.vue
new file mode 100644
index 0000000..1a8a01e
--- /dev/null
+++ b/frontend/src/components/MarkdownEditor/index.vue
@@ -0,0 +1,118 @@
+<template>
+  <div :id="id" />
+</template>
+
+<script>
+// deps for editor
+import 'codemirror/lib/codemirror.css' // codemirror
+import 'tui-editor/dist/tui-editor.css' // editor ui
+import 'tui-editor/dist/tui-editor-contents.css' // editor content
+
+import Editor from 'tui-editor'
+import defaultOptions from './default-options'
+
+export default {
+  name: 'MarkdownEditor',
+  props: {
+    value: {
+      type: String,
+      default: ''
+    },
+    id: {
+      type: String,
+      required: false,
+      default() {
+        return 'markdown-editor-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
+      }
+    },
+    options: {
+      type: Object,
+      default() {
+        return defaultOptions
+      }
+    },
+    mode: {
+      type: String,
+      default: 'markdown'
+    },
+    height: {
+      type: String,
+      required: false,
+      default: '300px'
+    },
+    language: {
+      type: String,
+      required: false,
+      default: 'en_US' // https://github.com/nhnent/tui.editor/tree/master/src/js/langs
+    }
+  },
+  data() {
+    return {
+      editor: null
+    }
+  },
+  computed: {
+    editorOptions() {
+      const options = Object.assign({}, defaultOptions, this.options)
+      options.initialEditType = this.mode
+      options.height = this.height
+      options.language = this.language
+      return options
+    }
+  },
+  watch: {
+    value(newValue, preValue) {
+      if (newValue !== preValue && newValue !== this.editor.getValue()) {
+        this.editor.setValue(newValue)
+      }
+    },
+    language(val) {
+      this.destroyEditor()
+      this.initEditor()
+    },
+    height(newValue) {
+      this.editor.height(newValue)
+    },
+    mode(newValue) {
+      this.editor.changeMode(newValue)
+    }
+  },
+  mounted() {
+    this.initEditor()
+  },
+  destroyed() {
+    this.destroyEditor()
+  },
+  methods: {
+    initEditor() {
+      this.editor = new Editor({
+        el: document.getElementById(this.id),
+        ...this.editorOptions
+      })
+      if (this.value) {
+        this.editor.setValue(this.value)
+      }
+      this.editor.on('change', () => {
+        this.$emit('input', this.editor.getValue())
+      })
+    },
+    destroyEditor() {
+      if (!this.editor) return
+      this.editor.off('change')
+      this.editor.remove()
+    },
+    setValue(value) {
+      this.editor.setValue(value)
+    },
+    getValue() {
+      return this.editor.getValue()
+    },
+    setHtml(value) {
+      this.editor.setHtml(value)
+    },
+    getHtml() {
+      return this.editor.getHtml()
+    }
+  }
+}
+</script>
diff --git a/frontend/src/components/Pagination/index.vue b/frontend/src/components/Pagination/index.vue
new file mode 100644
index 0000000..c815e13
--- /dev/null
+++ b/frontend/src/components/Pagination/index.vue
@@ -0,0 +1,101 @@
+<template>
+  <div :class="{'hidden':hidden}" class="pagination-container">
+    <el-pagination
+      :background="background"
+      :current-page.sync="currentPage"
+      :page-size.sync="pageSize"
+      :layout="layout"
+      :page-sizes="pageSizes"
+      :total="total"
+      v-bind="$attrs"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+    />
+  </div>
+</template>
+
+<script>
+import { scrollTo } from '@/utils/scroll-to'
+
+export default {
+  name: 'Pagination',
+  props: {
+    total: {
+      required: true,
+      type: Number
+    },
+    page: {
+      type: Number,
+      default: 1
+    },
+    limit: {
+      type: Number,
+      default: 20
+    },
+    pageSizes: {
+      type: Array,
+      default() {
+        return [10, 20, 30, 50]
+      }
+    },
+    layout: {
+      type: String,
+      default: 'total, sizes, prev, pager, next, jumper'
+    },
+    background: {
+      type: Boolean,
+      default: true
+    },
+    autoScroll: {
+      type: Boolean,
+      default: true
+    },
+    hidden: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    currentPage: {
+      get() {
+        return this.page
+      },
+      set(val) {
+        this.$emit('update:page', val)
+      }
+    },
+    pageSize: {
+      get() {
+        return this.limit
+      },
+      set(val) {
+        this.$emit('update:limit', val)
+      }
+    }
+  },
+  methods: {
+    handleSizeChange(val) {
+      this.$emit('pagination', { page: this.currentPage, limit: val })
+      if (this.autoScroll) {
+        scrollTo(0, 800)
+      }
+    },
+    handleCurrentChange(val) {
+      this.$emit('pagination', { page: val, limit: this.pageSize })
+      if (this.autoScroll) {
+        scrollTo(0, 800)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.pagination-container {
+  background: #fff;
+  padding: 32px 16px;
+}
+.pagination-container.hidden {
+  display: none;
+}
+</style>
diff --git a/frontend/src/components/PanThumb/index.vue b/frontend/src/components/PanThumb/index.vue
new file mode 100644
index 0000000..1bcf417
--- /dev/null
+++ b/frontend/src/components/PanThumb/index.vue
@@ -0,0 +1,142 @@
+<template>
+  <div :style="{zIndex:zIndex,height:height,width:width}" class="pan-item">
+    <div class="pan-info">
+      <div class="pan-info-roles-container">
+        <slot />
+      </div>
+    </div>
+    <!-- eslint-disable-next-line -->
+    <div :style="{backgroundImage: `url(${image})`}" class="pan-thumb"></div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'PanThumb',
+  props: {
+    image: {
+      type: String,
+      required: true
+    },
+    zIndex: {
+      type: Number,
+      default: 1
+    },
+    width: {
+      type: String,
+      default: '150px'
+    },
+    height: {
+      type: String,
+      default: '150px'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.pan-item {
+  width: 200px;
+  height: 200px;
+  border-radius: 50%;
+  display: inline-block;
+  position: relative;
+  cursor: default;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+}
+
+.pan-info-roles-container {
+  padding: 20px;
+  text-align: center;
+}
+
+.pan-thumb {
+  width: 100%;
+  height: 100%;
+  background-position: center center;
+  background-size: cover;
+  border-radius: 50%;
+  overflow: hidden;
+  position: absolute;
+  transform-origin: 95% 40%;
+  transition: all 0.3s ease-in-out;
+}
+
+/* .pan-thumb:after {
+  content: '';
+  width: 8px;
+  height: 8px;
+  position: absolute;
+  border-radius: 50%;
+  top: 40%;
+  left: 95%;
+  margin: -4px 0 0 -4px;
+  background: radial-gradient(ellipse at center, rgba(14, 14, 14, 1) 0%, rgba(125, 126, 125, 1) 100%);
+  box-shadow: 0 0 1px rgba(255, 255, 255, 0.9);
+} */
+
+.pan-info {
+  position: absolute;
+  width: inherit;
+  height: inherit;
+  border-radius: 50%;
+  overflow: hidden;
+  box-shadow: inset 0 0 0 5px rgba(0, 0, 0, 0.05);
+}
+
+.pan-info h3 {
+  color: #fff;
+  text-transform: uppercase;
+  position: relative;
+  letter-spacing: 2px;
+  font-size: 18px;
+  margin: 0 60px;
+  padding: 22px 0 0 0;
+  height: 85px;
+  font-family: 'Open Sans', Arial, sans-serif;
+  text-shadow: 0 0 1px #fff, 0 1px 2px rgba(0, 0, 0, 0.3);
+}
+
+.pan-info p {
+  color: #fff;
+  padding: 10px 5px;
+  font-style: italic;
+  margin: 0 30px;
+  font-size: 12px;
+  border-top: 1px solid rgba(255, 255, 255, 0.5);
+}
+
+.pan-info p a {
+  display: block;
+  color: #333;
+  width: 80px;
+  height: 80px;
+  background: rgba(255, 255, 255, 0.3);
+  border-radius: 50%;
+  color: #fff;
+  font-style: normal;
+  font-weight: 700;
+  text-transform: uppercase;
+  font-size: 9px;
+  letter-spacing: 1px;
+  padding-top: 24px;
+  margin: 7px auto 0;
+  font-family: 'Open Sans', Arial, sans-serif;
+  opacity: 0;
+  transition: transform 0.3s ease-in-out 0.2s, opacity 0.3s ease-in-out 0.2s, background 0.2s linear 0s;
+  transform: translateX(60px) rotate(90deg);
+}
+
+.pan-info p a:hover {
+  background: rgba(255, 255, 255, 0.5);
+}
+
+.pan-item:hover .pan-thumb {
+  transform: rotate(-110deg);
+}
+
+.pan-item:hover .pan-info p a {
+  opacity: 1;
+  transform: translateX(0px) rotate(0deg);
+}
+</style>
diff --git a/frontend/src/components/RightPanel/index.vue b/frontend/src/components/RightPanel/index.vue
new file mode 100644
index 0000000..55e8c1e
--- /dev/null
+++ b/frontend/src/components/RightPanel/index.vue
@@ -0,0 +1,145 @@
+<template>
+  <div ref="rightPanel" :class="{show:show}" class="rightPanel-container">
+    <div class="rightPanel-background" />
+    <div class="rightPanel">
+      <div class="handle-button" :style="{'top':buttonTop+'px','background-color':theme}" @click="show=!show">
+        <i :class="show?'el-icon-close':'el-icon-setting'" />
+      </div>
+      <div class="rightPanel-items">
+        <slot />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { addClass, removeClass } from '@/utils'
+
+export default {
+  name: 'RightPanel',
+  props: {
+    clickNotClose: {
+      default: false,
+      type: Boolean
+    },
+    buttonTop: {
+      default: 250,
+      type: Number
+    }
+  },
+  data() {
+    return {
+      show: false
+    }
+  },
+  computed: {
+    theme() {
+      return this.$store.state.settings.theme
+    }
+  },
+  watch: {
+    show(value) {
+      if (value && !this.clickNotClose) {
+        this.addEventClick()
+      }
+      if (value) {
+        addClass(document.body, 'showRightPanel')
+      } else {
+        removeClass(document.body, 'showRightPanel')
+      }
+    }
+  },
+  mounted() {
+    this.insertToBody()
+  },
+  beforeDestroy() {
+    const elx = this.$refs.rightPanel
+    elx.remove()
+  },
+  methods: {
+    addEventClick() {
+      window.addEventListener('click', this.closeSidebar)
+    },
+    closeSidebar(evt) {
+      const parent = evt.target.closest('.rightPanel')
+      if (!parent) {
+        this.show = false
+        window.removeEventListener('click', this.closeSidebar)
+      }
+    },
+    insertToBody() {
+      const elx = this.$refs.rightPanel
+      const body = document.querySelector('body')
+      body.insertBefore(elx, body.firstChild)
+    }
+  }
+}
+</script>
+
+<style>
+.showRightPanel {
+  overflow: hidden;
+  position: relative;
+  width: calc(100% - 15px);
+}
+</style>
+
+<style lang="scss" scoped>
+.rightPanel-background {
+  position: fixed;
+  top: 0;
+  left: 0;
+  opacity: 0;
+  transition: opacity .3s cubic-bezier(.7, .3, .1, 1);
+  background: rgba(0, 0, 0, .2);
+  z-index: -1;
+}
+
+.rightPanel {
+  width: 100%;
+  max-width: 260px;
+  height: 100vh;
+  position: fixed;
+  top: 0;
+  right: 0;
+  box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, .05);
+  transition: all .25s cubic-bezier(.7, .3, .1, 1);
+  transform: translate(100%);
+  background: #fff;
+  z-index: 40000;
+}
+
+.show {
+  transition: all .3s cubic-bezier(.7, .3, .1, 1);
+
+  .rightPanel-background {
+    z-index: 20000;
+    opacity: 1;
+    width: 100%;
+    height: 100%;
+  }
+
+  .rightPanel {
+    transform: translate(0);
+  }
+}
+
+.handle-button {
+  width: 48px;
+  height: 48px;
+  position: absolute;
+  left: -48px;
+  text-align: center;
+  font-size: 24px;
+  border-radius: 6px 0 0 6px !important;
+  z-index: 0;
+  pointer-events: auto;
+  cursor: pointer;
+  color: #fff;
+  line-height: 48px;
+  i {
+    font-size: 24px;
+    line-height: 48px;
+  }
+}
+</style>
diff --git a/frontend/src/components/Screenfull/index.vue b/frontend/src/components/Screenfull/index.vue
new file mode 100644
index 0000000..260c90d
--- /dev/null
+++ b/frontend/src/components/Screenfull/index.vue
@@ -0,0 +1,60 @@
+<template>
+  <div>
+    <svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
+  </div>
+</template>
+
+<script>
+import screenfull from 'screenfull'
+
+export default {
+  name: 'Screenfull',
+  data() {
+    return {
+      isFullscreen: false
+    }
+  },
+  mounted() {
+    this.init()
+  },
+  beforeDestroy() {
+    this.destroy()
+  },
+  methods: {
+    click() {
+      if (!screenfull.enabled) {
+        this.$message({
+          message: 'you browser can not work',
+          type: 'warning'
+        })
+        return false
+      }
+      screenfull.toggle()
+    },
+    change() {
+      this.isFullscreen = screenfull.isFullscreen
+    },
+    init() {
+      if (screenfull.enabled) {
+        screenfull.on('change', this.change)
+      }
+    },
+    destroy() {
+      if (screenfull.enabled) {
+        screenfull.off('change', this.change)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.screenfull-svg {
+  display: inline-block;
+  cursor: pointer;
+  fill: #5a5e66;;
+  width: 20px;
+  height: 20px;
+  vertical-align: 10px;
+}
+</style>
diff --git a/frontend/src/components/Share/DropdownMenu.vue b/frontend/src/components/Share/DropdownMenu.vue
new file mode 100644
index 0000000..d194a51
--- /dev/null
+++ b/frontend/src/components/Share/DropdownMenu.vue
@@ -0,0 +1,103 @@
+<template>
+  <div :class="{active:isActive}" class="share-dropdown-menu">
+    <div class="share-dropdown-menu-wrapper">
+      <span class="share-dropdown-menu-title" @click.self="clickTitle">{{ title }}</span>
+      <div v-for="(item,index) of items" :key="index" class="share-dropdown-menu-item">
+        <a v-if="item.href" :href="item.href" target="_blank">{{ item.title }}</a>
+        <span v-else>{{ item.title }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    items: {
+      type: Array,
+      default: function() {
+        return []
+      }
+    },
+    title: {
+      type: String,
+      default: 'vue'
+    }
+  },
+  data() {
+    return {
+      isActive: false
+    }
+  },
+  methods: {
+    clickTitle() {
+      this.isActive = !this.isActive
+    }
+  }
+}
+</script>
+
+<style lang="scss" >
+$n: 9; //和items.length 相同
+$t: .1s;
+.share-dropdown-menu {
+  width: 250px;
+  position: relative;
+  z-index: 1;
+  height: auto!important;
+  &-title {
+    width: 100%;
+    display: block;
+    cursor: pointer;
+    background: black;
+    color: white;
+    height: 60px;
+    line-height: 60px;
+    font-size: 20px;
+    text-align: center;
+    z-index: 2;
+    transform: translate3d(0,0,0);
+  }
+  &-wrapper {
+    position: relative;
+  }
+  &-item {
+    text-align: center;
+    position: absolute;
+    width: 100%;
+    background: #e0e0e0;
+    color: #000;
+    line-height: 60px;
+    height: 60px;
+    cursor: pointer;
+    font-size: 18px;
+    overflow: hidden;
+    opacity: 1;
+    transition: transform 0.28s ease;
+    &:hover {
+      background: black;
+      color: white;
+    }
+    @for $i from 1 through $n {
+      &:nth-of-type(#{$i}) {
+        z-index: -1;
+        transition-delay: $i*$t;
+        transform: translate3d(0, -60px, 0);
+      }
+    }
+  }
+  &.active {
+    .share-dropdown-menu-wrapper {
+      z-index: 1;
+    }
+    .share-dropdown-menu-item {
+      @for $i from 1 through $n {
+        &:nth-of-type(#{$i}) {
+          transition-delay: ($n - $i)*$t;
+          transform: translate3d(0, ($i - 1)*60px, 0);
+        }
+      }
+    }
+  }
+}
+</style>
diff --git a/frontend/src/components/SizeSelect/index.vue b/frontend/src/components/SizeSelect/index.vue
new file mode 100644
index 0000000..e88065b
--- /dev/null
+++ b/frontend/src/components/SizeSelect/index.vue
@@ -0,0 +1,57 @@
+<template>
+  <el-dropdown trigger="click" @command="handleSetSize">
+    <div>
+      <svg-icon class-name="size-icon" icon-class="size" />
+    </div>
+    <el-dropdown-menu slot="dropdown">
+      <el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size===item.value" :command="item.value">
+        {{
+          item.label }}
+      </el-dropdown-item>
+    </el-dropdown-menu>
+  </el-dropdown>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      sizeOptions: [
+        { label: 'Default', value: 'default' },
+        { label: 'Medium', value: 'medium' },
+        { label: 'Small', value: 'small' },
+        { label: 'Mini', value: 'mini' }
+      ]
+    }
+  },
+  computed: {
+    size() {
+      return this.$store.getters.size
+    }
+  },
+  methods: {
+    handleSetSize(size) {
+      this.$ELEMENT.size = size
+      this.$store.dispatch('app/setSize', size)
+      this.refreshView()
+      this.$message({
+        message: 'Switch Size Success',
+        type: 'success'
+      })
+    },
+    refreshView() {
+      // In order to make the cached page re-rendered
+      this.$store.dispatch('tagsView/delAllCachedViews', this.$route)
+
+      const { fullPath } = this.$route
+
+      this.$nextTick(() => {
+        this.$router.replace({
+          path: '/redirect' + fullPath
+        })
+      })
+    }
+  }
+
+}
+</script>
diff --git a/frontend/src/components/Sticky/index.vue b/frontend/src/components/Sticky/index.vue
new file mode 100644
index 0000000..97ce0e9
--- /dev/null
+++ b/frontend/src/components/Sticky/index.vue
@@ -0,0 +1,91 @@
+<template>
+  <div :style="{height:height+'px',zIndex:zIndex}">
+    <div
+      :class="className"
+      :style="{top:(isSticky ? stickyTop +'px' : ''),zIndex:zIndex,position:position,width:width,height:height+'px'}"
+    >
+      <slot>
+        <div>sticky</div>
+      </slot>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Sticky',
+  props: {
+    stickyTop: {
+      type: Number,
+      default: 0
+    },
+    zIndex: {
+      type: Number,
+      default: 1
+    },
+    className: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      active: false,
+      position: '',
+      width: undefined,
+      height: undefined,
+      isSticky: false
+    }
+  },
+  mounted() {
+    this.height = this.$el.getBoundingClientRect().height
+    window.addEventListener('scroll', this.handleScroll)
+    window.addEventListener('resize', this.handleResize)
+  },
+  activated() {
+    this.handleScroll()
+  },
+  destroyed() {
+    window.removeEventListener('scroll', this.handleScroll)
+    window.removeEventListener('resize', this.handleResize)
+  },
+  methods: {
+    sticky() {
+      if (this.active) {
+        return
+      }
+      this.position = 'fixed'
+      this.active = true
+      this.width = this.width + 'px'
+      this.isSticky = true
+    },
+    handleReset() {
+      if (!this.active) {
+        return
+      }
+      this.reset()
+    },
+    reset() {
+      this.position = ''
+      this.width = 'auto'
+      this.active = false
+      this.isSticky = false
+    },
+    handleScroll() {
+      const width = this.$el.getBoundingClientRect().width
+      this.width = width || 'auto'
+      const offsetTop = this.$el.getBoundingClientRect().top
+      if (offsetTop < this.stickyTop) {
+        this.sticky()
+        return
+      }
+      this.handleReset()
+    },
+    handleResize() {
+      if (this.isSticky) {
+        this.width = this.$el.getBoundingClientRect().width + 'px'
+      }
+    }
+  }
+}
+</script>
diff --git a/frontend/src/components/SvgIcon/index.vue b/frontend/src/components/SvgIcon/index.vue
new file mode 100644
index 0000000..b07ded2
--- /dev/null
+++ b/frontend/src/components/SvgIcon/index.vue
@@ -0,0 +1,62 @@
+<template>
+  <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
+  <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
+    <use :xlink:href="iconName" />
+  </svg>
+</template>
+
+<script>
+// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
+import { isExternal } from '@/utils/validate'
+
+export default {
+  name: 'SvgIcon',
+  props: {
+    iconClass: {
+      type: String,
+      required: true
+    },
+    className: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    isExternal() {
+      return isExternal(this.iconClass)
+    },
+    iconName() {
+      return `#icon-${this.iconClass}`
+    },
+    svgClass() {
+      if (this.className) {
+        return 'svg-icon ' + this.className
+      } else {
+        return 'svg-icon'
+      }
+    },
+    styleExternalIcon() {
+      return {
+        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
+        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.svg-icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+}
+
+.svg-external-icon {
+  background-color: currentColor;
+  mask-size: cover!important;
+  display: inline-block;
+}
+</style>
diff --git a/frontend/src/components/TextHoverEffect/Mallki.vue b/frontend/src/components/TextHoverEffect/Mallki.vue
new file mode 100644
index 0000000..5d6d16c
--- /dev/null
+++ b/frontend/src/components/TextHoverEffect/Mallki.vue
@@ -0,0 +1,113 @@
+<template>
+  <a :class="className" class="link--mallki" href="#">
+    {{ text }}
+    <span :data-letters="text" />
+    <span :data-letters="text" />
+  </a>
+</template>
+
+<script>
+export default {
+  props: {
+    className: {
+      type: String,
+      default: ''
+    },
+    text: {
+      type: String,
+      default: 'vue-element-admin'
+    }
+  }
+}
+</script>
+
+<style>
+/* Mallki */
+
+.link--mallki {
+  font-weight: 800;
+  color: #4dd9d5;
+  font-family: 'Dosis', sans-serif;
+  -webkit-transition: color 0.5s 0.25s;
+  transition: color 0.5s 0.25s;
+  overflow: hidden;
+  position: relative;
+  display: inline-block;
+  line-height: 1;
+  outline: none;
+  text-decoration: none;
+}
+
+.link--mallki:hover {
+  -webkit-transition: none;
+  transition: none;
+  color: transparent;
+}
+
+.link--mallki::before {
+  content: '';
+  width: 100%;
+  height: 6px;
+  margin: -3px 0 0 0;
+  background: #3888fa;
+  position: absolute;
+  left: 0;
+  top: 50%;
+  -webkit-transform: translate3d(-100%, 0, 0);
+  transform: translate3d(-100%, 0, 0);
+  -webkit-transition: -webkit-transform 0.4s;
+  transition: transform 0.4s;
+  -webkit-transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1);
+  transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1);
+}
+
+.link--mallki:hover::before {
+  -webkit-transform: translate3d(100%, 0, 0);
+  transform: translate3d(100%, 0, 0);
+}
+
+.link--mallki span {
+  position: absolute;
+  height: 50%;
+  width: 100%;
+  left: 0;
+  top: 0;
+  overflow: hidden;
+}
+
+.link--mallki span::before {
+  content: attr(data-letters);
+  color: red;
+  position: absolute;
+  left: 0;
+  width: 100%;
+  color: #3888fa;
+  -webkit-transition: -webkit-transform 0.5s;
+  transition: transform 0.5s;
+}
+
+.link--mallki span:nth-child(2) {
+  top: 50%;
+}
+
+.link--mallki span:first-child::before {
+  top: 0;
+  -webkit-transform: translate3d(0, 100%, 0);
+  transform: translate3d(0, 100%, 0);
+}
+
+.link--mallki span:nth-child(2)::before {
+  bottom: 0;
+  -webkit-transform: translate3d(0, -100%, 0);
+  transform: translate3d(0, -100%, 0);
+}
+
+.link--mallki:hover span::before {
+  -webkit-transition-delay: 0.3s;
+  transition-delay: 0.3s;
+  -webkit-transform: translate3d(0, 0, 0);
+  transform: translate3d(0, 0, 0);
+  -webkit-transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1);
+  transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1);
+}
+</style>
diff --git a/frontend/src/components/ThemePicker/index.vue b/frontend/src/components/ThemePicker/index.vue
new file mode 100644
index 0000000..3879c5a
--- /dev/null
+++ b/frontend/src/components/ThemePicker/index.vue
@@ -0,0 +1,175 @@
+<template>
+  <el-color-picker
+    v-model="theme"
+    :predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
+    class="theme-picker"
+    popper-class="theme-picker-dropdown"
+  />
+</template>
+
+<script>
+const version = require('element-ui/package.json').version // element-ui version from node_modules
+const ORIGINAL_THEME = '#409EFF' // default color
+
+export default {
+  data() {
+    return {
+      chalk: '', // content of theme-chalk css
+      theme: ''
+    }
+  },
+  computed: {
+    defaultTheme() {
+      return this.$store.state.settings.theme
+    }
+  },
+  watch: {
+    defaultTheme: {
+      handler: function(val, oldVal) {
+        this.theme = val
+      },
+      immediate: true
+    },
+    async theme(val) {
+      const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
+      if (typeof val !== 'string') return
+      const themeCluster = this.getThemeCluster(val.replace('#', ''))
+      const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
+      console.log(themeCluster, originalCluster)
+
+      const $message = this.$message({
+        message: '  Compiling the theme',
+        customClass: 'theme-message',
+        type: 'success',
+        duration: 0,
+        iconClass: 'el-icon-loading'
+      })
+
+      const getHandler = (variable, id) => {
+        return () => {
+          const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
+          const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
+
+          let styleTag = document.getElementById(id)
+          if (!styleTag) {
+            styleTag = document.createElement('style')
+            styleTag.setAttribute('id', id)
+            document.head.appendChild(styleTag)
+          }
+          styleTag.innerText = newStyle
+        }
+      }
+
+      if (!this.chalk) {
+        const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
+        await this.getCSSString(url, 'chalk')
+      }
+
+      const chalkHandler = getHandler('chalk', 'chalk-style')
+
+      chalkHandler()
+
+      const styles = [].slice.call(document.querySelectorAll('style'))
+        .filter(style => {
+          const text = style.innerText
+          return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
+        })
+      styles.forEach(style => {
+        const { innerText } = style
+        if (typeof innerText !== 'string') return
+        style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
+      })
+
+      this.$emit('change', val)
+
+      $message.close()
+    }
+  },
+
+  methods: {
+    updateStyle(style, oldCluster, newCluster) {
+      let newStyle = style
+      oldCluster.forEach((color, index) => {
+        newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
+      })
+      return newStyle
+    },
+
+    getCSSString(url, variable) {
+      return new Promise(resolve => {
+        const xhr = new XMLHttpRequest()
+        xhr.onreadystatechange = () => {
+          if (xhr.readyState === 4 && xhr.status === 200) {
+            this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
+            resolve()
+          }
+        }
+        xhr.open('GET', url)
+        xhr.send()
+      })
+    },
+
+    getThemeCluster(theme) {
+      const tintColor = (color, tint) => {
+        let red = parseInt(color.slice(0, 2), 16)
+        let green = parseInt(color.slice(2, 4), 16)
+        let blue = parseInt(color.slice(4, 6), 16)
+
+        if (tint === 0) { // when primary color is in its rgb space
+          return [red, green, blue].join(',')
+        } else {
+          red += Math.round(tint * (255 - red))
+          green += Math.round(tint * (255 - green))
+          blue += Math.round(tint * (255 - blue))
+
+          red = red.toString(16)
+          green = green.toString(16)
+          blue = blue.toString(16)
+
+          return `#${red}${green}${blue}`
+        }
+      }
+
+      const shadeColor = (color, shade) => {
+        let red = parseInt(color.slice(0, 2), 16)
+        let green = parseInt(color.slice(2, 4), 16)
+        let blue = parseInt(color.slice(4, 6), 16)
+
+        red = Math.round((1 - shade) * red)
+        green = Math.round((1 - shade) * green)
+        blue = Math.round((1 - shade) * blue)
+
+        red = red.toString(16)
+        green = green.toString(16)
+        blue = blue.toString(16)
+
+        return `#${red}${green}${blue}`
+      }
+
+      const clusters = [theme]
+      for (let i = 0; i <= 9; i++) {
+        clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
+      }
+      clusters.push(shadeColor(theme, 0.1))
+      return clusters
+    }
+  }
+}
+</script>
+
+<style>
+.theme-message,
+.theme-picker-dropdown {
+  z-index: 99999 !important;
+}
+
+.theme-picker .el-color-picker__trigger {
+  height: 26px !important;
+  width: 26px !important;
+  padding: 2px;
+}
+
+.theme-picker-dropdown .el-color-dropdown__link-btn {
+  display: none;
+}
+</style>
diff --git a/frontend/src/components/Tinymce/components/EditorImage.vue b/frontend/src/components/Tinymce/components/EditorImage.vue
new file mode 100644
index 0000000..07d48e6
--- /dev/null
+++ b/frontend/src/components/Tinymce/components/EditorImage.vue
@@ -0,0 +1,111 @@
+<template>
+  <div class="upload-container">
+    <el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click=" dialogVisible=true">
+      upload
+    </el-button>
+    <el-dialog :visible.sync="dialogVisible">
+      <el-upload
+        :multiple="true"
+        :file-list="fileList"
+        :show-file-list="true"
+        :on-remove="handleRemove"
+        :on-success="handleSuccess"
+        :before-upload="beforeUpload"
+        class="editor-slide-upload"
+        action="https://httpbin.org/post"
+        list-type="picture-card"
+      >
+        <el-button size="small" type="primary">
+          Click upload
+        </el-button>
+      </el-upload>
+      <el-button @click="dialogVisible = false">
+        Cancel
+      </el-button>
+      <el-button type="primary" @click="handleSubmit">
+        Confirm
+      </el-button>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+// import { getToken } from 'api/qiniu'
+
+export default {
+  name: 'EditorSlideUpload',
+  props: {
+    color: {
+      type: String,
+      default: '#1890ff'
+    }
+  },
+  data() {
+    return {
+      dialogVisible: false,
+      listObj: {},
+      fileList: []
+    }
+  },
+  methods: {
+    checkAllSuccess() {
+      return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)
+    },
+    handleSubmit() {
+      const arr = Object.keys(this.listObj).map(v => this.listObj[v])
+      if (!this.checkAllSuccess()) {
+        this.$message('Please wait for all images to be uploaded successfully. If there is a network problem, please refresh the page and upload again!')
+        return
+      }
+      this.$emit('successCBK', arr)
+      this.listObj = {}
+      this.fileList = []
+      this.dialogVisible = false
+    },
+    handleSuccess(response, file) {
+      const uid = file.uid
+      const objKeyArr = Object.keys(this.listObj)
+      for (let i = 0, len = objKeyArr.length; i < len; i++) {
+        if (this.listObj[objKeyArr[i]].uid === uid) {
+          this.listObj[objKeyArr[i]].url = response.files.file
+          this.listObj[objKeyArr[i]].hasSuccess = true
+          return
+        }
+      }
+    },
+    handleRemove(file) {
+      const uid = file.uid
+      const objKeyArr = Object.keys(this.listObj)
+      for (let i = 0, len = objKeyArr.length; i < len; i++) {
+        if (this.listObj[objKeyArr[i]].uid === uid) {
+          delete this.listObj[objKeyArr[i]]
+          return
+        }
+      }
+    },
+    beforeUpload(file) {
+      const _self = this
+      const _URL = window.URL || window.webkitURL
+      const fileName = file.uid
+      this.listObj[fileName] = {}
+      return new Promise((resolve, reject) => {
+        const img = new Image()
+        img.src = _URL.createObjectURL(file)
+        img.onload = function() {
+          _self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }
+        }
+        resolve(true)
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.editor-slide-upload {
+  margin-bottom: 20px;
+  ::v-deep .el-upload--picture-card {
+    width: 100%;
+  }
+}
+</style>
diff --git a/frontend/src/components/Tinymce/dynamicLoadScript.js b/frontend/src/components/Tinymce/dynamicLoadScript.js
new file mode 100644
index 0000000..185f58d
--- /dev/null
+++ b/frontend/src/components/Tinymce/dynamicLoadScript.js
@@ -0,0 +1,59 @@
+let callbacks = []
+
+function loadedTinymce() {
+  // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144
+  // check is successfully downloaded script
+  return window.tinymce
+}
+
+const dynamicLoadScript = (src, callback) => {
+  const existingScript = document.getElementById(src)
+  const cb = callback || function() {}
+
+  if (!existingScript) {
+    const script = document.createElement('script')
+    script.src = src // src url for the third-party library being loaded.
+    script.id = src
+    document.body.appendChild(script)
+    callbacks.push(cb)
+    const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd
+    onEnd(script)
+  }
+
+  if (existingScript && cb) {
+    if (loadedTinymce()) {
+      cb(null, existingScript)
+    } else {
+      callbacks.push(cb)
+    }
+  }
+
+  function stdOnEnd(script) {
+    script.onload = function() {
+      // this.onload = null here is necessary
+      // because even IE9 works not like others
+      this.onerror = this.onload = null
+      for (const cb of callbacks) {
+        cb(null, script)
+      }
+      callbacks = null
+    }
+    script.onerror = function() {
+      this.onerror = this.onload = null
+      cb(new Error('Failed to load ' + src), script)
+    }
+  }
+
+  function ieOnEnd(script) {
+    script.onreadystatechange = function() {
+      if (this.readyState !== 'complete' && this.readyState !== 'loaded') return
+      this.onreadystatechange = null
+      for (const cb of callbacks) {
+        cb(null, script) // there is no way to catch loading errors in IE8
+      }
+      callbacks = null
+    }
+  }
+}
+
+export default dynamicLoadScript
diff --git a/frontend/src/components/Tinymce/index.vue b/frontend/src/components/Tinymce/index.vue
new file mode 100644
index 0000000..cb6b91c
--- /dev/null
+++ b/frontend/src/components/Tinymce/index.vue
@@ -0,0 +1,247 @@
+<template>
+  <div :class="{fullscreen:fullscreen}" class="tinymce-container" :style="{width:containerWidth}">
+    <textarea :id="tinymceId" class="tinymce-textarea" />
+    <div class="editor-custom-btn-container">
+      <editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK" />
+    </div>
+  </div>
+</template>
+
+<script>
+/**
+ * docs:
+ * https://panjiachen.github.io/vue-element-admin-site/feature/component/rich-editor.html#tinymce
+ */
+import editorImage from './components/EditorImage'
+import plugins from './plugins'
+import toolbar from './toolbar'
+import load from './dynamicLoadScript'
+
+// why use this cdn, detail see https://github.com/PanJiaChen/tinymce-all-in-one
+const tinymceCDN = 'https://cdn.jsdelivr.net/npm/tinymce-all-in-one@4.9.3/tinymce.min.js'
+
+export default {
+  name: 'Tinymce',
+  components: { editorImage },
+  props: {
+    id: {
+      type: String,
+      default: function() {
+        return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
+      }
+    },
+    value: {
+      type: String,
+      default: ''
+    },
+    toolbar: {
+      type: Array,
+      required: false,
+      default() {
+        return []
+      }
+    },
+    menubar: {
+      type: String,
+      default: 'file edit insert view format table'
+    },
+    height: {
+      type: [Number, String],
+      required: false,
+      default: 360
+    },
+    width: {
+      type: [Number, String],
+      required: false,
+      default: 'auto'
+    }
+  },
+  data() {
+    return {
+      hasChange: false,
+      hasInit: false,
+      tinymceId: this.id,
+      fullscreen: false,
+      languageTypeList: {
+        'en': 'en',
+        'zh': 'zh_CN',
+        'es': 'es_MX',
+        'ja': 'ja'
+      }
+    }
+  },
+  computed: {
+    containerWidth() {
+      const width = this.width
+      if (/^[\d]+(\.[\d]+)?$/.test(width)) { // matches `100`, `'100'`
+        return `${width}px`
+      }
+      return width
+    }
+  },
+  watch: {
+    value(val) {
+      if (!this.hasChange && this.hasInit) {
+        this.$nextTick(() =>
+          window.tinymce.get(this.tinymceId).setContent(val || ''))
+      }
+    }
+  },
+  mounted() {
+    this.init()
+  },
+  activated() {
+    if (window.tinymce) {
+      this.initTinymce()
+    }
+  },
+  deactivated() {
+    this.destroyTinymce()
+  },
+  destroyed() {
+    this.destroyTinymce()
+  },
+  methods: {
+    init() {
+      // dynamic load tinymce from cdn
+      load(tinymceCDN, (err) => {
+        if (err) {
+          this.$message.error(err.message)
+          return
+        }
+        this.initTinymce()
+      })
+    },
+    initTinymce() {
+      const _this = this
+      window.tinymce.init({
+        selector: `#${this.tinymceId}`,
+        language: this.languageTypeList['en'],
+        height: this.height,
+        body_class: 'panel-body ',
+        object_resizing: false,
+        toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
+        menubar: this.menubar,
+        plugins: plugins,
+        end_container_on_empty_block: true,
+        powerpaste_word_import: 'clean',
+        code_dialog_height: 450,
+        code_dialog_width: 1000,
+        advlist_bullet_styles: 'square',
+        advlist_number_styles: 'default',
+        imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
+        default_link_target: '_blank',
+        link_title: false,
+        nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin
+        init_instance_callback: editor => {
+          if (_this.value) {
+            editor.setContent(_this.value)
+          }
+          _this.hasInit = true
+          editor.on('NodeChange Change KeyUp SetContent', () => {
+            this.hasChange = true
+            this.$emit('input', editor.getContent())
+          })
+        },
+        setup(editor) {
+          editor.on('FullscreenStateChanged', (e) => {
+            _this.fullscreen = e.state
+          })
+        },
+        // it will try to keep these URLs intact
+        // https://www.tiny.cloud/docs-3x/reference/configuration/Configuration3x@convert_urls/
+        // https://stackoverflow.com/questions/5196205/disable-tinymce-absolute-to-relative-url-conversions
+        convert_urls: false
+        // 整合七牛上传
+        // images_dataimg_filter(img) {
+        //   setTimeout(() => {
+        //     const $image = $(img);
+        //     $image.removeAttr('width');
+        //     $image.removeAttr('height');
+        //     if ($image[0].height && $image[0].width) {
+        //       $image.attr('data-wscntype', 'image');
+        //       $image.attr('data-wscnh', $image[0].height);
+        //       $image.attr('data-wscnw', $image[0].width);
+        //       $image.addClass('wscnph');
+        //     }
+        //   }, 0);
+        //   return img
+        // },
+        // images_upload_handler(blobInfo, success, failure, progress) {
+        //   progress(0);
+        //   const token = _this.$store.getters.token;
+        //   getToken(token).then(response => {
+        //     const url = response.data.qiniu_url;
+        //     const formData = new FormData();
+        //     formData.append('token', response.data.qiniu_token);
+        //     formData.append('key', response.data.qiniu_key);
+        //     formData.append('file', blobInfo.blob(), url);
+        //     upload(formData).then(() => {
+        //       success(url);
+        //       progress(100);
+        //     })
+        //   }).catch(err => {
+        //     failure('出现未知问题,刷新页面,或者联系程序员')
+        //     console.log(err);
+        //   });
+        // },
+      })
+    },
+    destroyTinymce() {
+      const tinymce = window.tinymce.get(this.tinymceId)
+      if (this.fullscreen) {
+        tinymce.execCommand('mceFullScreen')
+      }
+
+      if (tinymce) {
+        tinymce.destroy()
+      }
+    },
+    setContent(value) {
+      window.tinymce.get(this.tinymceId).setContent(value)
+    },
+    getContent() {
+      window.tinymce.get(this.tinymceId).getContent()
+    },
+    imageSuccessCBK(arr) {
+      arr.forEach(v => window.tinymce.get(this.tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`))
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.tinymce-container {
+  position: relative;
+  line-height: normal;
+}
+
+.tinymce-container {
+  ::v-deep {
+    .mce-fullscreen {
+      z-index: 10000;
+    }
+  }
+}
+
+.tinymce-textarea {
+  visibility: hidden;
+  z-index: -1;
+}
+
+.editor-custom-btn-container {
+  position: absolute;
+  right: 4px;
+  top: 4px;
+  /*z-index: 2005;*/
+}
+
+.fullscreen .editor-custom-btn-container {
+  z-index: 10000;
+  position: fixed;
+}
+
+.editor-upload-btn {
+  display: inline-block;
+}
+</style>
diff --git a/frontend/src/components/Tinymce/plugins.js b/frontend/src/components/Tinymce/plugins.js
new file mode 100644
index 0000000..058d2ae
--- /dev/null
+++ b/frontend/src/components/Tinymce/plugins.js
@@ -0,0 +1,7 @@
+// Any plugins you want to use has to be imported
+// Detail plugins list see https://www.tinymce.com/docs/plugins/
+// Custom builds see https://www.tinymce.com/download/custom-builds/
+
+const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount']
+
+export default plugins
diff --git a/frontend/src/components/Tinymce/toolbar.js b/frontend/src/components/Tinymce/toolbar.js
new file mode 100644
index 0000000..4f8a545
--- /dev/null
+++ b/frontend/src/components/Tinymce/toolbar.js
@@ -0,0 +1,6 @@
+// Here is a list of the toolbar
+// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
+
+const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent  blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen']
+
+export default toolbar
diff --git a/frontend/src/components/Upload/SingleImage.vue b/frontend/src/components/Upload/SingleImage.vue
new file mode 100644
index 0000000..d16bbf3
--- /dev/null
+++ b/frontend/src/components/Upload/SingleImage.vue
@@ -0,0 +1,134 @@
+<template>
+  <div class="upload-container">
+    <el-upload
+      :data="dataObj"
+      :multiple="false"
+      :show-file-list="false"
+      :on-success="handleImageSuccess"
+      class="image-uploader"
+      drag
+      action="https://httpbin.org/post"
+    >
+      <i class="el-icon-upload" />
+      <div class="el-upload__text">
+        将文件拖到此处,或<em>点击上传</em>
+      </div>
+    </el-upload>
+    <div class="image-preview">
+      <div v-show="imageUrl.length>1" class="image-preview-wrapper">
+        <img :src="imageUrl+'?imageView2/1/w/200/h/200'">
+        <div class="image-preview-action">
+          <i class="el-icon-delete" @click="rmImage" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getToken } from '@/api/qiniu'
+
+export default {
+  name: 'SingleImageUpload',
+  props: {
+    value: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      tempUrl: '',
+      dataObj: { token: '', key: '' }
+    }
+  },
+  computed: {
+    imageUrl() {
+      return this.value
+    }
+  },
+  methods: {
+    rmImage() {
+      this.emitInput('')
+    },
+    emitInput(val) {
+      this.$emit('input', val)
+    },
+    handleImageSuccess() {
+      this.emitInput(this.tempUrl)
+    },
+    beforeUpload() {
+      const _self = this
+      return new Promise((resolve, reject) => {
+        getToken().then(response => {
+          const key = response.data.qiniu_key
+          const token = response.data.qiniu_token
+          _self._data.dataObj.token = token
+          _self._data.dataObj.key = key
+          this.tempUrl = response.data.qiniu_url
+          resolve(true)
+        }).catch(err => {
+          console.log(err)
+          reject(false)
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+    @import "~@/styles/mixin.scss";
+    .upload-container {
+        width: 100%;
+        position: relative;
+        @include clearfix;
+        .image-uploader {
+            width: 60%;
+            float: left;
+        }
+        .image-preview {
+            width: 200px;
+            height: 200px;
+            position: relative;
+            border: 1px dashed #d9d9d9;
+            float: left;
+            margin-left: 50px;
+            .image-preview-wrapper {
+                position: relative;
+                width: 100%;
+                height: 100%;
+                img {
+                    width: 100%;
+                    height: 100%;
+                }
+            }
+            .image-preview-action {
+                position: absolute;
+                width: 100%;
+                height: 100%;
+                left: 0;
+                top: 0;
+                cursor: default;
+                text-align: center;
+                color: #fff;
+                opacity: 0;
+                font-size: 20px;
+                background-color: rgba(0, 0, 0, .5);
+                transition: opacity .3s;
+                cursor: pointer;
+                text-align: center;
+                line-height: 200px;
+                .el-icon-delete {
+                    font-size: 36px;
+                }
+            }
+            &:hover {
+                .image-preview-action {
+                    opacity: 1;
+                }
+            }
+        }
+    }
+
+</style>
diff --git a/frontend/src/components/Upload/SingleImage2.vue b/frontend/src/components/Upload/SingleImage2.vue
new file mode 100644
index 0000000..07637a9
--- /dev/null
+++ b/frontend/src/components/Upload/SingleImage2.vue
@@ -0,0 +1,130 @@
+<template>
+  <div class="singleImageUpload2 upload-container">
+    <el-upload
+      :data="dataObj"
+      :multiple="false"
+      :show-file-list="false"
+      :on-success="handleImageSuccess"
+      class="image-uploader"
+      drag
+      action="https://httpbin.org/post"
+    >
+      <i class="el-icon-upload" />
+      <div class="el-upload__text">
+        Drag或<em>点击上传</em>
+      </div>
+    </el-upload>
+    <div v-show="imageUrl.length>0" class="image-preview">
+      <div v-show="imageUrl.length>1" class="image-preview-wrapper">
+        <img :src="imageUrl">
+        <div class="image-preview-action">
+          <i class="el-icon-delete" @click="rmImage" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getToken } from '@/api/qiniu'
+
+export default {
+  name: 'SingleImageUpload2',
+  props: {
+    value: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      tempUrl: '',
+      dataObj: { token: '', key: '' }
+    }
+  },
+  computed: {
+    imageUrl() {
+      return this.value
+    }
+  },
+  methods: {
+    rmImage() {
+      this.emitInput('')
+    },
+    emitInput(val) {
+      this.$emit('input', val)
+    },
+    handleImageSuccess() {
+      this.emitInput(this.tempUrl)
+    },
+    beforeUpload() {
+      const _self = this
+      return new Promise((resolve, reject) => {
+        getToken().then(response => {
+          const key = response.data.qiniu_key
+          const token = response.data.qiniu_token
+          _self._data.dataObj.token = token
+          _self._data.dataObj.key = key
+          this.tempUrl = response.data.qiniu_url
+          resolve(true)
+        }).catch(() => {
+          reject(false)
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.upload-container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  .image-uploader {
+    height: 100%;
+  }
+  .image-preview {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    left: 0px;
+    top: 0px;
+    border: 1px dashed #d9d9d9;
+    .image-preview-wrapper {
+      position: relative;
+      width: 100%;
+      height: 100%;
+      img {
+        width: 100%;
+        height: 100%;
+      }
+    }
+    .image-preview-action {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      left: 0;
+      top: 0;
+      cursor: default;
+      text-align: center;
+      color: #fff;
+      opacity: 0;
+      font-size: 20px;
+      background-color: rgba(0, 0, 0, .5);
+      transition: opacity .3s;
+      cursor: pointer;
+      text-align: center;
+      line-height: 200px;
+      .el-icon-delete {
+        font-size: 36px;
+      }
+    }
+    &:hover {
+      .image-preview-action {
+        opacity: 1;
+      }
+    }
+  }
+}
+</style>
diff --git a/frontend/src/components/Upload/SingleImage3.vue b/frontend/src/components/Upload/SingleImage3.vue
new file mode 100644
index 0000000..6300da4
--- /dev/null
+++ b/frontend/src/components/Upload/SingleImage3.vue
@@ -0,0 +1,157 @@
+<template>
+  <div class="upload-container">
+    <el-upload
+      :data="dataObj"
+      :multiple="false"
+      :show-file-list="false"
+      :on-success="handleImageSuccess"
+      class="image-uploader"
+      drag
+      action="https://httpbin.org/post"
+    >
+      <i class="el-icon-upload" />
+      <div class="el-upload__text">
+        将文件拖到此处,或<em>点击上传</em>
+      </div>
+    </el-upload>
+    <div class="image-preview image-app-preview">
+      <div v-show="imageUrl.length>1" class="image-preview-wrapper">
+        <img :src="imageUrl">
+        <div class="image-preview-action">
+          <i class="el-icon-delete" @click="rmImage" />
+        </div>
+      </div>
+    </div>
+    <div class="image-preview">
+      <div v-show="imageUrl.length>1" class="image-preview-wrapper">
+        <img :src="imageUrl">
+        <div class="image-preview-action">
+          <i class="el-icon-delete" @click="rmImage" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getToken } from '@/api/qiniu'
+
+export default {
+  name: 'SingleImageUpload3',
+  props: {
+    value: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      tempUrl: '',
+      dataObj: { token: '', key: '' }
+    }
+  },
+  computed: {
+    imageUrl() {
+      return this.value
+    }
+  },
+  methods: {
+    rmImage() {
+      this.emitInput('')
+    },
+    emitInput(val) {
+      this.$emit('input', val)
+    },
+    handleImageSuccess(file) {
+      this.emitInput(file.files.file)
+    },
+    beforeUpload() {
+      const _self = this
+      return new Promise((resolve, reject) => {
+        getToken().then(response => {
+          const key = response.data.qiniu_key
+          const token = response.data.qiniu_token
+          _self._data.dataObj.token = token
+          _self._data.dataObj.key = key
+          this.tempUrl = response.data.qiniu_url
+          resolve(true)
+        }).catch(err => {
+          console.log(err)
+          reject(false)
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import "~@/styles/mixin.scss";
+.upload-container {
+  width: 100%;
+  position: relative;
+  @include clearfix;
+  .image-uploader {
+    width: 35%;
+    float: left;
+  }
+  .image-preview {
+    width: 200px;
+    height: 200px;
+    position: relative;
+    border: 1px dashed #d9d9d9;
+    float: left;
+    margin-left: 50px;
+    .image-preview-wrapper {
+      position: relative;
+      width: 100%;
+      height: 100%;
+      img {
+        width: 100%;
+        height: 100%;
+      }
+    }
+    .image-preview-action {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      left: 0;
+      top: 0;
+      cursor: default;
+      text-align: center;
+      color: #fff;
+      opacity: 0;
+      font-size: 20px;
+      background-color: rgba(0, 0, 0, .5);
+      transition: opacity .3s;
+      cursor: pointer;
+      text-align: center;
+      line-height: 200px;
+      .el-icon-delete {
+        font-size: 36px;
+      }
+    }
+    &:hover {
+      .image-preview-action {
+        opacity: 1;
+      }
+    }
+  }
+  .image-app-preview {
+    width: 320px;
+    height: 180px;
+    position: relative;
+    border: 1px dashed #d9d9d9;
+    float: left;
+    margin-left: 50px;
+    .app-fake-conver {
+      height: 44px;
+      position: absolute;
+      width: 100%; // background: rgba(0, 0, 0, .1);
+      text-align: center;
+      line-height: 64px;
+      color: #fff;
+    }
+  }
+}
+</style>
diff --git a/frontend/src/components/UploadExcel/index.vue b/frontend/src/components/UploadExcel/index.vue
new file mode 100644
index 0000000..9e8ba8b
--- /dev/null
+++ b/frontend/src/components/UploadExcel/index.vue
@@ -0,0 +1,138 @@
+<template>
+  <div>
+    <input ref="excel-upload-input" class="excel-upload-input" type="file" accept=".xlsx, .xls" @change="handleClick">
+    <div class="drop" @drop="handleDrop" @dragover="handleDragover" @dragenter="handleDragover">
+      Drop excel file here or
+      <el-button :loading="loading" style="margin-left:16px;" size="mini" type="primary" @click="handleUpload">
+        Browse
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import XLSX from 'xlsx'
+
+export default {
+  props: {
+    beforeUpload: Function, // eslint-disable-line
+    onSuccess: Function// eslint-disable-line
+  },
+  data() {
+    return {
+      loading: false,
+      excelData: {
+        header: null,
+        results: null
+      }
+    }
+  },
+  methods: {
+    generateData({ header, results }) {
+      this.excelData.header = header
+      this.excelData.results = results
+      this.onSuccess && this.onSuccess(this.excelData)
+    },
+    handleDrop(e) {
+      e.stopPropagation()
+      e.preventDefault()
+      if (this.loading) return
+      const files = e.dataTransfer.files
+      if (files.length !== 1) {
+        this.$message.error('Only support uploading one file!')
+        return
+      }
+      const rawFile = files[0] // only use files[0]
+
+      if (!this.isExcel(rawFile)) {
+        this.$message.error('Only supports upload .xlsx, .xls, .csv suffix files')
+        return false
+      }
+      this.upload(rawFile)
+      e.stopPropagation()
+      e.preventDefault()
+    },
+    handleDragover(e) {
+      e.stopPropagation()
+      e.preventDefault()
+      e.dataTransfer.dropEffect = 'copy'
+    },
+    handleUpload() {
+      this.$refs['excel-upload-input'].click()
+    },
+    handleClick(e) {
+      const files = e.target.files
+      const rawFile = files[0] // only use files[0]
+      if (!rawFile) return
+      this.upload(rawFile)
+    },
+    upload(rawFile) {
+      this.$refs['excel-upload-input'].value = null // fix can't select the same excel
+
+      if (!this.beforeUpload) {
+        this.readerData(rawFile)
+        return
+      }
+      const before = this.beforeUpload(rawFile)
+      if (before) {
+        this.readerData(rawFile)
+      }
+    },
+    readerData(rawFile) {
+      this.loading = true
+      return new Promise((resolve, reject) => {
+        const reader = new FileReader()
+        reader.onload = e => {
+          const data = e.target.result
+          const workbook = XLSX.read(data, { type: 'array' })
+          const firstSheetName = workbook.SheetNames[0]
+          const worksheet = workbook.Sheets[firstSheetName]
+          const header = this.getHeaderRow(worksheet)
+          const results = XLSX.utils.sheet_to_json(worksheet)
+          this.generateData({ header, results })
+          this.loading = false
+          resolve()
+        }
+        reader.readAsArrayBuffer(rawFile)
+      })
+    },
+    getHeaderRow(sheet) {
+      const headers = []
+      const range = XLSX.utils.decode_range(sheet['!ref'])
+      let C
+      const R = range.s.r
+      /* start in the first row */
+      for (C = range.s.c; C <= range.e.c; ++C) { /* walk every column in the range */
+        const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })]
+        /* find the cell in the first row */
+        let hdr = 'UNKNOWN ' + C // <-- replace with your desired default
+        if (cell && cell.t) hdr = XLSX.utils.format_cell(cell)
+        headers.push(hdr)
+      }
+      return headers
+    },
+    isExcel(file) {
+      return /\.(xlsx|xls|csv)$/.test(file.name)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.excel-upload-input{
+  display: none;
+  z-index: -9999;
+}
+.drop{
+  border: 2px dashed #bbb;
+  width: 600px;
+  height: 160px;
+  line-height: 160px;
+  margin: 0 auto;
+  font-size: 24px;
+  border-radius: 5px;
+  text-align: center;
+  color: #bbb;
+  position: relative;
+}
+</style>
diff --git a/frontend/src/directive/clipboard/clipboard.js b/frontend/src/directive/clipboard/clipboard.js
new file mode 100644
index 0000000..514aad2
--- /dev/null
+++ b/frontend/src/directive/clipboard/clipboard.js
@@ -0,0 +1,49 @@
+// Inspired by https://github.com/Inndy/vue-clipboard2
+const Clipboard = require('clipboard')
+if (!Clipboard) {
+  throw new Error('you should npm install `clipboard` --save at first ')
+}
+
+export default {
+  bind(el, binding) {
+    if (binding.arg === 'success') {
+      el._v_clipboard_success = binding.value
+    } else if (binding.arg === 'error') {
+      el._v_clipboard_error = binding.value
+    } else {
+      const clipboard = new Clipboard(el, {
+        text() { return binding.value },
+        action() { return binding.arg === 'cut' ? 'cut' : 'copy' }
+      })
+      clipboard.on('success', e => {
+        const callback = el._v_clipboard_success
+        callback && callback(e) // eslint-disable-line
+      })
+      clipboard.on('error', e => {
+        const callback = el._v_clipboard_error
+        callback && callback(e) // eslint-disable-line
+      })
+      el._v_clipboard = clipboard
+    }
+  },
+  update(el, binding) {
+    if (binding.arg === 'success') {
+      el._v_clipboard_success = binding.value
+    } else if (binding.arg === 'error') {
+      el._v_clipboard_error = binding.value
+    } else {
+      el._v_clipboard.text = function() { return binding.value }
+      el._v_clipboard.action = function() { return binding.arg === 'cut' ? 'cut' : 'copy' }
+    }
+  },
+  unbind(el, binding) {
+    if (binding.arg === 'success') {
+      delete el._v_clipboard_success
+    } else if (binding.arg === 'error') {
+      delete el._v_clipboard_error
+    } else {
+      el._v_clipboard.destroy()
+      delete el._v_clipboard
+    }
+  }
+}
diff --git a/frontend/src/directive/clipboard/index.js b/frontend/src/directive/clipboard/index.js
new file mode 100644
index 0000000..02c9816
--- /dev/null
+++ b/frontend/src/directive/clipboard/index.js
@@ -0,0 +1,13 @@
+import Clipboard from './clipboard'
+
+const install = function(Vue) {
+  Vue.directive('Clipboard', Clipboard)
+}
+
+if (window.Vue) {
+  window.clipboard = Clipboard
+  Vue.use(install); // eslint-disable-line
+}
+
+Clipboard.install = install
+export default Clipboard
diff --git a/frontend/src/directive/el-drag-dialog/drag.js b/frontend/src/directive/el-drag-dialog/drag.js
new file mode 100644
index 0000000..299e985
--- /dev/null
+++ b/frontend/src/directive/el-drag-dialog/drag.js
@@ -0,0 +1,77 @@
+export default {
+  bind(el, binding, vnode) {
+    const dialogHeaderEl = el.querySelector('.el-dialog__header')
+    const dragDom = el.querySelector('.el-dialog')
+    dialogHeaderEl.style.cssText += ';cursor:move;'
+    dragDom.style.cssText += ';top:0px;'
+
+    // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
+    const getStyle = (function() {
+      if (window.document.currentStyle) {
+        return (dom, attr) => dom.currentStyle[attr]
+      } else {
+        return (dom, attr) => getComputedStyle(dom, false)[attr]
+      }
+    })()
+
+    dialogHeaderEl.onmousedown = (e) => {
+      // 鼠标按下,计算当前元素距离可视区的距离
+      const disX = e.clientX - dialogHeaderEl.offsetLeft
+      const disY = e.clientY - dialogHeaderEl.offsetTop
+
+      const dragDomWidth = dragDom.offsetWidth
+      const dragDomHeight = dragDom.offsetHeight
+
+      const screenWidth = document.body.clientWidth
+      const screenHeight = document.body.clientHeight
+
+      const minDragDomLeft = dragDom.offsetLeft
+      const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth
+
+      const minDragDomTop = dragDom.offsetTop
+      const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight
+
+      // 获取到的值带px 正则匹配替换
+      let styL = getStyle(dragDom, 'left')
+      let styT = getStyle(dragDom, 'top')
+
+      if (styL.includes('%')) {
+        styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100)
+        styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100)
+      } else {
+        styL = +styL.replace(/\px/g, '')
+        styT = +styT.replace(/\px/g, '')
+      }
+
+      document.onmousemove = function(e) {
+        // 通过事件委托,计算移动的距离
+        let left = e.clientX - disX
+        let top = e.clientY - disY
+
+        // 边界处理
+        if (-(left) > minDragDomLeft) {
+          left = -minDragDomLeft
+        } else if (left > maxDragDomLeft) {
+          left = maxDragDomLeft
+        }
+
+        if (-(top) > minDragDomTop) {
+          top = -minDragDomTop
+        } else if (top > maxDragDomTop) {
+          top = maxDragDomTop
+        }
+
+        // 移动当前元素
+        dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`
+
+        // emit onDrag event
+        vnode.child.$emit('dragDialog')
+      }
+
+      document.onmouseup = function(e) {
+        document.onmousemove = null
+        document.onmouseup = null
+      }
+    }
+  }
+}
diff --git a/frontend/src/directive/el-drag-dialog/index.js b/frontend/src/directive/el-drag-dialog/index.js
new file mode 100644
index 0000000..29facbf
--- /dev/null
+++ b/frontend/src/directive/el-drag-dialog/index.js
@@ -0,0 +1,13 @@
+import drag from './drag'
+
+const install = function(Vue) {
+  Vue.directive('el-drag-dialog', drag)
+}
+
+if (window.Vue) {
+  window['el-drag-dialog'] = drag
+  Vue.use(install); // eslint-disable-line
+}
+
+drag.install = install
+export default drag
diff --git a/frontend/src/directive/el-table/adaptive.js b/frontend/src/directive/el-table/adaptive.js
new file mode 100644
index 0000000..d229e9f
--- /dev/null
+++ b/frontend/src/directive/el-table/adaptive.js
@@ -0,0 +1,41 @@
+import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event'
+
+/**
+ * How to use
+ * <el-table height="100px" v-el-height-adaptive-table="{bottomOffset: 30}">...</el-table>
+ * el-table height is must be set
+ * bottomOffset: 30(default)   // The height of the table from the bottom of the page.
+ */
+
+const doResize = (el, binding, vnode) => {
+  const { componentInstance: $table } = vnode
+
+  const { value } = binding
+
+  if (!$table.height) {
+    throw new Error(`el-$table must set the height. Such as height='100px'`)
+  }
+  const bottomOffset = (value && value.bottomOffset) || 30
+
+  if (!$table) return
+
+  const height = window.innerHeight - el.getBoundingClientRect().top - bottomOffset
+  $table.layout.setHeight(height)
+  $table.doLayout()
+}
+
+export default {
+  bind(el, binding, vnode) {
+    el.resizeListener = () => {
+      doResize(el, binding, vnode)
+    }
+    // parameter 1 is must be "Element" type
+    addResizeListener(window.document.body, el.resizeListener)
+  },
+  inserted(el, binding, vnode) {
+    doResize(el, binding, vnode)
+  },
+  unbind(el) {
+    removeResizeListener(window.document.body, el.resizeListener)
+  }
+}
diff --git a/frontend/src/directive/el-table/index.js b/frontend/src/directive/el-table/index.js
new file mode 100644
index 0000000..d3d4515
--- /dev/null
+++ b/frontend/src/directive/el-table/index.js
@@ -0,0 +1,13 @@
+import adaptive from './adaptive'
+
+const install = function(Vue) {
+  Vue.directive('el-height-adaptive-table', adaptive)
+}
+
+if (window.Vue) {
+  window['el-height-adaptive-table'] = adaptive
+  Vue.use(install); // eslint-disable-line
+}
+
+adaptive.install = install
+export default adaptive
diff --git a/frontend/src/directive/permission/index.js b/frontend/src/directive/permission/index.js
new file mode 100644
index 0000000..e5dadd3
--- /dev/null
+++ b/frontend/src/directive/permission/index.js
@@ -0,0 +1,13 @@
+import permission from './permission'
+
+const install = function(Vue) {
+  Vue.directive('permission', permission)
+}
+
+if (window.Vue) {
+  window['permission'] = permission
+  Vue.use(install); // eslint-disable-line
+}
+
+permission.install = install
+export default permission
diff --git a/frontend/src/directive/permission/permission.js b/frontend/src/directive/permission/permission.js
new file mode 100644
index 0000000..49d1f88
--- /dev/null
+++ b/frontend/src/directive/permission/permission.js
@@ -0,0 +1,31 @@
+import store from '@/store'
+
+function checkPermission(el, binding) {
+  const { value } = binding
+  const roles = store.getters && store.getters.roles
+
+  if (value && value instanceof Array) {
+    if (value.length > 0) {
+      const permissionRoles = value
+
+      const hasPermission = roles.some(role => {
+        return permissionRoles.includes(role)
+      })
+
+      if (!hasPermission) {
+        el.parentNode && el.parentNode.removeChild(el)
+      }
+    }
+  } else {
+    throw new Error(`need roles! Like v-permission="['admin','editor']"`)
+  }
+}
+
+export default {
+  inserted(el, binding) {
+    checkPermission(el, binding)
+  },
+  update(el, binding) {
+    checkPermission(el, binding)
+  }
+}
diff --git a/frontend/src/directive/sticky.js b/frontend/src/directive/sticky.js
new file mode 100644
index 0000000..bc23466
--- /dev/null
+++ b/frontend/src/directive/sticky.js
@@ -0,0 +1,91 @@
+const vueSticky = {}
+let listenAction
+vueSticky.install = Vue => {
+  Vue.directive('sticky', {
+    inserted(el, binding) {
+      const params = binding.value || {}
+      const stickyTop = params.stickyTop || 0
+      const zIndex = params.zIndex || 1000
+      const elStyle = el.style
+
+      elStyle.position = '-webkit-sticky'
+      elStyle.position = 'sticky'
+      // if the browser support css sticky(Currently Safari, Firefox and Chrome Canary)
+      // if (~elStyle.position.indexOf('sticky')) {
+      //     elStyle.top = `${stickyTop}px`;
+      //     elStyle.zIndex = zIndex;
+      //     return
+      // }
+      const elHeight = el.getBoundingClientRect().height
+      const elWidth = el.getBoundingClientRect().width
+      elStyle.cssText = `top: ${stickyTop}px; z-index: ${zIndex}`
+
+      const parentElm = el.parentNode || document.documentElement
+      const placeholder = document.createElement('div')
+      placeholder.style.display = 'none'
+      placeholder.style.width = `${elWidth}px`
+      placeholder.style.height = `${elHeight}px`
+      parentElm.insertBefore(placeholder, el)
+
+      let active = false
+
+      const getScroll = (target, top) => {
+        const prop = top ? 'pageYOffset' : 'pageXOffset'
+        const method = top ? 'scrollTop' : 'scrollLeft'
+        let ret = target[prop]
+        if (typeof ret !== 'number') {
+          ret = window.document.documentElement[method]
+        }
+        return ret
+      }
+
+      const sticky = () => {
+        if (active) {
+          return
+        }
+        if (!elStyle.height) {
+          elStyle.height = `${el.offsetHeight}px`
+        }
+
+        elStyle.position = 'fixed'
+        elStyle.width = `${elWidth}px`
+        placeholder.style.display = 'inline-block'
+        active = true
+      }
+
+      const reset = () => {
+        if (!active) {
+          return
+        }
+
+        elStyle.position = ''
+        placeholder.style.display = 'none'
+        active = false
+      }
+
+      const check = () => {
+        const scrollTop = getScroll(window, true)
+        const offsetTop = el.getBoundingClientRect().top
+        if (offsetTop < stickyTop) {
+          sticky()
+        } else {
+          if (scrollTop < elHeight + stickyTop) {
+            reset()
+          }
+        }
+      }
+      listenAction = () => {
+        check()
+      }
+
+      window.addEventListener('scroll', listenAction)
+    },
+
+    unbind() {
+      window.removeEventListener('scroll', listenAction)
+    }
+  })
+}
+
+export default vueSticky
+
diff --git a/frontend/src/directive/waves/index.js b/frontend/src/directive/waves/index.js
new file mode 100644
index 0000000..65f9b30
--- /dev/null
+++ b/frontend/src/directive/waves/index.js
@@ -0,0 +1,13 @@
+import waves from './waves'
+
+const install = function(Vue) {
+  Vue.directive('waves', waves)
+}
+
+if (window.Vue) {
+  window.waves = waves
+  Vue.use(install); // eslint-disable-line
+}
+
+waves.install = install
+export default waves
diff --git a/frontend/src/directive/waves/waves.css b/frontend/src/directive/waves/waves.css
new file mode 100644
index 0000000..af7a7ef
--- /dev/null
+++ b/frontend/src/directive/waves/waves.css
@@ -0,0 +1,26 @@
+.waves-ripple {
+    position: absolute;
+    border-radius: 100%;
+    background-color: rgba(0, 0, 0, 0.15);
+    background-clip: padding-box;
+    pointer-events: none;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+    -webkit-transform: scale(0);
+    -ms-transform: scale(0);
+    transform: scale(0);
+    opacity: 1;
+}
+
+.waves-ripple.z-active {
+    opacity: 0;
+    -webkit-transform: scale(2);
+    -ms-transform: scale(2);
+    transform: scale(2);
+    -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
+    transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
+    transition: opacity 1.2s ease-out, transform 0.6s ease-out;
+    transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
+}
\ No newline at end of file
diff --git a/frontend/src/directive/waves/waves.js b/frontend/src/directive/waves/waves.js
new file mode 100644
index 0000000..ec2ff43
--- /dev/null
+++ b/frontend/src/directive/waves/waves.js
@@ -0,0 +1,72 @@
+import './waves.css'
+
+const context = '@@wavesContext'
+
+function handleClick(el, binding) {
+  function handle(e) {
+    const customOpts = Object.assign({}, binding.value)
+    const opts = Object.assign({
+      ele: el, // 波纹作用元素
+      type: 'hit', // hit 点击位置扩散 center中心点扩展
+      color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
+    },
+    customOpts
+    )
+    const target = opts.ele
+    if (target) {
+      target.style.position = 'relative'
+      target.style.overflow = 'hidden'
+      const rect = target.getBoundingClientRect()
+      let ripple = target.querySelector('.waves-ripple')
+      if (!ripple) {
+        ripple = document.createElement('span')
+        ripple.className = 'waves-ripple'
+        ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
+        target.appendChild(ripple)
+      } else {
+        ripple.className = 'waves-ripple'
+      }
+      switch (opts.type) {
+        case 'center':
+          ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px'
+          ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px'
+          break
+        default:
+          ripple.style.top =
+            (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop ||
+              document.body.scrollTop) + 'px'
+          ripple.style.left =
+            (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft ||
+              document.body.scrollLeft) + 'px'
+      }
+      ripple.style.backgroundColor = opts.color
+      ripple.className = 'waves-ripple z-active'
+      return false
+    }
+  }
+
+  if (!el[context]) {
+    el[context] = {
+      removeHandle: handle
+    }
+  } else {
+    el[context].removeHandle = handle
+  }
+
+  return handle
+}
+
+export default {
+  bind(el, binding) {
+    el.addEventListener('click', handleClick(el, binding), false)
+  },
+  update(el, binding) {
+    el.removeEventListener('click', el[context].removeHandle, false)
+    el.addEventListener('click', handleClick(el, binding), false)
+  },
+  unbind(el) {
+    el.removeEventListener('click', el[context].removeHandle, false)
+    el[context] = null
+    delete el[context]
+  }
+}
diff --git a/frontend/src/filters/index.js b/frontend/src/filters/index.js
new file mode 100644
index 0000000..9822233
--- /dev/null
+++ b/frontend/src/filters/index.js
@@ -0,0 +1,68 @@
+// import parseTime, formatTime and set to filter
+export { parseTime, formatTime } from '@/utils'
+
+/**
+ * Show plural label if time is plural number
+ * @param {number} time
+ * @param {string} label
+ * @return {string}
+ */
+function pluralize(time, label) {
+  if (time === 1) {
+    return time + label
+  }
+  return time + label + 's'
+}
+
+/**
+ * @param {number} time
+ */
+export function timeAgo(time) {
+  const between = Date.now() / 1000 - Number(time)
+  if (between < 3600) {
+    return pluralize(~~(between / 60), ' minute')
+  } else if (between < 86400) {
+    return pluralize(~~(between / 3600), ' hour')
+  } else {
+    return pluralize(~~(between / 86400), ' day')
+  }
+}
+
+/**
+ * Number formatting
+ * like 10000 => 10k
+ * @param {number} num
+ * @param {number} digits
+ */
+export function numberFormatter(num, digits) {
+  const si = [
+    { value: 1E18, symbol: 'E' },
+    { value: 1E15, symbol: 'P' },
+    { value: 1E12, symbol: 'T' },
+    { value: 1E9, symbol: 'G' },
+    { value: 1E6, symbol: 'M' },
+    { value: 1E3, symbol: 'k' }
+  ]
+  for (let i = 0; i < si.length; i++) {
+    if (num >= si[i].value) {
+      return (num / si[i].value).toFixed(digits).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, '$1') + si[i].symbol
+    }
+  }
+  return num.toString()
+}
+
+/**
+ * 10000 => "10,000"
+ * @param {number} num
+ */
+export function toThousandFilter(num) {
+  return (+num || 0).toString().replace(/^-?\d+/g, m => m.replace(/(?=(?!\b)(\d{3})+$)/g, ','))
+}
+
+/**
+ * Upper case first char
+ * @param {String} string
+ */
+export function uppercaseFirst(string) {
+  return string.charAt(0).toUpperCase() + string.slice(1)
+}
diff --git a/frontend/src/icons/index.js b/frontend/src/icons/index.js
new file mode 100644
index 0000000..2c6b309
--- /dev/null
+++ b/frontend/src/icons/index.js
@@ -0,0 +1,9 @@
+import Vue from 'vue'
+import SvgIcon from '@/components/SvgIcon'// svg component
+
+// register globally
+Vue.component('svg-icon', SvgIcon)
+
+const req = require.context('./svg', false, /\.svg$/)
+const requireAll = requireContext => requireContext.keys().map(requireContext)
+requireAll(req)
diff --git a/frontend/src/icons/svg/404.svg b/frontend/src/icons/svg/404.svg
new file mode 100644
index 0000000..6df5019
--- /dev/null
+++ b/frontend/src/icons/svg/404.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M121.718 73.272v9.953c3.957-7.584 6.199-16.05 6.199-24.995C127.917 26.079 99.273 0 63.958 0 28.644 0 0 26.079 0 58.23c0 .403.028.806.028 1.21l22.97-25.953h13.34l-19.76 27.187h6.42V53.77l13.728-19.477v49.361H22.998V73.272H2.158c5.951 20.284 23.608 36.208 45.998 41.399-1.44 3.3-5.618 11.263-12.565 12.674-8.607 1.764 23.358.428 46.163-13.178 17.519-4.611 31.938-15.849 39.77-30.513h-13.506V73.272H85.02V59.464l22.998-25.977h13.008l-19.429 27.187h6.421v-7.433l13.727-19.402v39.433h-.027zm-78.24 2.822a10.516 10.516 0 0 1-.996-4.535V44.548c0-1.613.332-3.124.996-4.535a11.66 11.66 0 0 1 2.713-3.68c1.134-1.032 2.49-1.864 4.04-2.468 1.55-.605 3.21-.908 4.982-.908h11.292c1.77 0 3.431.303 4.981.908 1.522.604 2.85 1.41 3.986 2.418l-12.26 16.303v-2.898a1.96 1.96 0 0 0-.665-1.512c-.443-.403-.996-.604-1.66-.604-.665 0-1.218.201-1.661.604a1.96 1.96 0 0 0-.664 1.512v9.071L44.364 77.606a10.556 10.556 0 0 1-.886-1.512zm35.73-4.535c0 1.613-.332 3.124-.997 4.535a11.66 11.66 0 0 1-2.712 3.68c-1.134 1.032-2.49 1.864-4.04 2.469-1.55.604-3.21.907-4.982.907H55.185c-1.77 0-3.431-.303-4.981-.907-1.55-.605-2.906-1.437-4.041-2.47a12.49 12.49 0 0 1-1.384-1.512l13.727-18.217v6.375c0 .605.222 1.109.665 1.512.442.403.996.604 1.66.604.664 0 1.218-.201 1.66-.604a1.96 1.96 0 0 0 .665-1.512V53.87L75.97 36.838c.913.932 1.66 1.99 2.214 3.175.664 1.41.996 2.922.996 4.535v27.011h.028z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/bug.svg b/frontend/src/icons/svg/bug.svg
new file mode 100644
index 0000000..05a150d
--- /dev/null
+++ b/frontend/src/icons/svg/bug.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M127.88 73.143c0 1.412-.506 2.635-1.518 3.669-1.011 1.033-2.209 1.55-3.592 1.55h-17.887c0 9.296-1.783 17.178-5.35 23.645l16.609 17.044c1.011 1.034 1.517 2.257 1.517 3.67 0 1.412-.506 2.635-1.517 3.668-.958 1.033-2.155 1.55-3.593 1.55-1.438 0-2.635-.517-3.593-1.55l-15.811-16.063a15.49 15.49 0 0 1-1.196 1.06c-.532.434-1.65 1.208-3.353 2.322a50.104 50.104 0 0 1-5.192 2.974c-1.758.87-3.94 1.658-6.546 2.364-2.607.706-5.189 1.06-7.748 1.06V47.044H58.89v73.062c-2.716 0-5.417-.367-8.106-1.102-2.688-.734-5.003-1.631-6.945-2.692a66.769 66.769 0 0 1-5.268-3.179c-1.571-1.057-2.73-1.94-3.476-2.65L33.9 109.34l-14.611 16.877c-1.066 1.14-2.344 1.711-3.833 1.711-1.277 0-2.422-.434-3.434-1.304-1.012-.978-1.557-2.187-1.635-3.627-.079-1.44.333-2.705 1.236-3.794l16.129-18.51c-3.087-6.197-4.63-13.644-4.63-22.342H5.235c-1.383 0-2.58-.517-3.592-1.55S.125 74.545.125 73.132c0-1.412.506-2.635 1.518-3.668 1.012-1.034 2.21-1.55 3.592-1.55h17.887V43.939L9.308 29.833c-1.012-1.033-1.517-2.256-1.517-3.669 0-1.412.505-2.635 1.517-3.668 1.012-1.034 2.21-1.55 3.593-1.55s2.58.516 3.593 1.55l13.813 14.106h67.396l13.814-14.106c1.012-1.034 2.21-1.55 3.592-1.55 1.384 0 2.581.516 3.593 1.55 1.012 1.033 1.518 2.256 1.518 3.668 0 1.413-.506 2.636-1.518 3.67l-13.814 14.105v23.975h17.887c1.383 0 2.58.516 3.593 1.55 1.011 1.033 1.517 2.256 1.517 3.668l-.005.01zM89.552 26.175H38.448c0-7.23 2.489-13.386 7.466-18.469C50.892 2.623 56.92.082 64 .082c7.08 0 13.108 2.541 18.086 7.624 4.977 5.083 7.466 11.24 7.466 18.469z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/chart.svg b/frontend/src/icons/svg/chart.svg
new file mode 100644
index 0000000..27728fb
--- /dev/null
+++ b/frontend/src/icons/svg/chart.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 54.857h36.571V128H0V54.857zM91.429 27.43H128V128H91.429V27.429zM45.714 0h36.572v128H45.714V0z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/clipboard.svg b/frontend/src/icons/svg/clipboard.svg
new file mode 100644
index 0000000..90923ff
--- /dev/null
+++ b/frontend/src/icons/svg/clipboard.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M54.857 118.857h64V73.143H89.143c-1.902 0-3.52-.668-4.855-2.002-1.335-1.335-2.002-2.954-2.002-4.855V36.57H54.857v82.286zM73.143 16v-4.571a2.2 2.2 0 0 0-.677-1.61 2.198 2.198 0 0 0-1.609-.676H20.571c-.621 0-1.158.225-1.609.676a2.198 2.198 0 0 0-.676 1.61V16a2.2 2.2 0 0 0 .676 1.61c.451.45.988.676 1.61.676h50.285c.622 0 1.158-.226 1.61-.677.45-.45.676-.987.676-1.609zm18.286 48h21.357L91.43 42.642V64zM128 73.143v48c0 1.902-.667 3.52-2.002 4.855-1.335 1.335-2.953 2.002-4.855 2.002H52.57c-1.901 0-3.52-.667-4.854-2.002-1.335-1.335-2.003-2.953-2.003-4.855v-11.429H6.857c-1.902 0-3.52-.667-4.855-2.002C.667 106.377 0 104.759 0 102.857v-96c0-1.902.667-3.52 2.002-4.855C3.337.667 4.955 0 6.857 0h77.714c1.902 0 3.52.667 4.855 2.002 1.335 1.335 2.003 2.953 2.003 4.855V30.29c1 .622 1.856 1.29 2.569 2.003l29.147 29.147c1.335 1.335 2.478 3.145 3.429 5.43.95 2.287 1.426 4.383 1.426 6.291v-.018z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/component.svg b/frontend/src/icons/svg/component.svg
new file mode 100644
index 0000000..207ada3
--- /dev/null
+++ b/frontend/src/icons/svg/component.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h54.857v54.857H0V0zm0 73.143h54.857V128H0V73.143zm73.143 0H128V128H73.143V73.143zm27.428-18.286C115.72 54.857 128 42.577 128 27.43 128 12.28 115.72 0 100.571 0 85.423 0 73.143 12.28 73.143 27.429c0 15.148 12.28 27.428 27.428 27.428z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/dashboard.svg b/frontend/src/icons/svg/dashboard.svg
new file mode 100644
index 0000000..5317d37
--- /dev/null
+++ b/frontend/src/icons/svg/dashboard.svg
@@ -0,0 +1 @@
+<svg width="128" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M27.429 63.638c0-2.508-.893-4.65-2.679-6.424-1.786-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.465 2.662-1.785 1.774-2.678 3.916-2.678 6.424 0 2.508.893 4.65 2.678 6.424 1.786 1.775 3.94 2.662 6.465 2.662 2.524 0 4.678-.887 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm13.714-31.801c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM71.714 65.98l7.215-27.116c.285-1.23.107-2.378-.536-3.443-.643-1.064-1.56-1.762-2.75-2.094-1.19-.33-2.333-.177-3.429.462-1.095.639-1.81 1.573-2.143 2.804l-7.214 27.116c-2.857.237-5.405 1.266-7.643 3.088-2.238 1.822-3.738 4.152-4.5 6.992-.952 3.644-.476 7.098 1.429 10.364 1.905 3.265 4.69 5.37 8.357 6.317 3.667.947 7.143.474 10.429-1.42 3.285-1.892 5.404-4.66 6.357-8.305.762-2.84.619-5.607-.429-8.305-1.047-2.697-2.762-4.85-5.143-6.46zm47.143-2.342c0-2.508-.893-4.65-2.678-6.424-1.786-1.775-3.94-2.662-6.465-2.662-2.524 0-4.678.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.786 1.775 3.94 2.662 6.464 2.662 2.524 0 4.679-.887 6.465-2.662 1.785-1.775 2.678-3.916 2.678-6.424zm-45.714-45.43c0-2.509-.893-4.65-2.679-6.425C68.68 10.01 66.524 9.122 64 9.122c-2.524 0-4.679.887-6.464 2.661-1.786 1.775-2.679 3.916-2.679 6.425 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm32 13.629c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM128 63.638c0 12.351-3.357 23.78-10.071 34.286-.905 1.372-2.19 2.058-3.858 2.058H13.93c-1.667 0-2.953-.686-3.858-2.058C3.357 87.465 0 76.037 0 63.638c0-8.613 1.69-16.847 5.071-24.703C8.452 31.08 13 24.312 18.714 18.634c5.715-5.68 12.524-10.199 20.429-13.559C47.048 1.715 55.333.035 64 .035c8.667 0 16.952 1.68 24.857 5.04 7.905 3.36 14.714 7.88 20.429 13.559 5.714 5.678 10.262 12.446 13.643 20.301 3.38 7.856 5.071 16.09 5.071 24.703z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/documentation.svg b/frontend/src/icons/svg/documentation.svg
new file mode 100644
index 0000000..7043122
--- /dev/null
+++ b/frontend/src/icons/svg/documentation.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M71.984 44.815H115.9L71.984 9.642v35.173zM16.094.05h63.875l47.906 38.37v76.74c0 3.392-1.682 6.645-4.677 9.044-2.995 2.399-7.056 3.746-11.292 3.746H16.094c-4.236 0-8.297-1.347-11.292-3.746-2.995-2.399-4.677-5.652-4.677-9.044V12.84C.125 5.742 7.23.05 16.094.05zm71.86 102.32V89.58h-71.86v12.79h71.86zm23.952-25.58V64H16.094v12.79h95.812z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/drag.svg b/frontend/src/icons/svg/drag.svg
new file mode 100644
index 0000000..4185d3c
--- /dev/null
+++ b/frontend/src/icons/svg/drag.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M73.137 29.08h-9.209 29.7L63.886.093 34.373 29.08h20.49v27.035H27.238v17.948h27.625v27.133h18.274V74.063h27.41V56.115h-27.41V29.08zm-9.245 98.827l27.518-26.711H36.59l27.302 26.71zM.042 64.982l27.196 27.029V38.167L.042 64.982zm100.505-26.815V92.01l27.41-27.029-27.41-26.815z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/edit.svg b/frontend/src/icons/svg/edit.svg
new file mode 100644
index 0000000..d26101f
--- /dev/null
+++ b/frontend/src/icons/svg/edit.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M106.133 67.2a4.797 4.797 0 0 0-4.8 4.8c0 .187.014.36.027.533h-.027V118.4H9.6V26.667h50.133c2.654 0 4.8-2.147 4.8-4.8 0-2.654-2.146-4.8-4.8-4.8H9.6a9.594 9.594 0 0 0-9.6 9.6V118.4c0 5.307 4.293 9.6 9.6 9.6h91.733c5.307 0 9.6-4.293 9.6-9.6V72.533h-.026c.013-.173.026-.346.026-.533 0-2.653-2.146-4.8-4.8-4.8z"/><path d="M125.16 13.373L114.587 2.8c-3.747-3.747-9.854-3.72-13.6.027l-52.96 52.96a4.264 4.264 0 0 0-.907 1.36L33.813 88.533c-.746 1.76-.226 3.534.907 4.68 1.133 1.147 2.92 1.667 4.693.92l31.4-13.293c.507-.213.96-.52 1.36-.907l52.96-52.96c3.747-3.746 3.774-9.853.027-13.6zM66.107 72.4l-18.32 7.76 7.76-18.32L92.72 24.667l10.56 10.56L66.107 72.4zm52.226-52.227l-8.266 8.267-10.56-10.56 8.266-8.267.027-.026 10.56 10.56-.027.026z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/education.svg b/frontend/src/icons/svg/education.svg
new file mode 100644
index 0000000..7bfb01d
--- /dev/null
+++ b/frontend/src/icons/svg/education.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M88.883 119.565c-7.284 0-19.434 2.495-21.333 8.25v.127c-4.232.13-5.222 0-7.108 0-1.895-5.76-14.045-8.256-21.333-8.256H0V0h42.523c9.179 0 17.109 5.47 21.47 13.551C68.352 5.475 76.295 0 85.478 0H128v119.57l-39.113-.005h-.004zM60.442 24.763c0-9.651-8.978-16.507-17.777-16.507H7.108V111.43H39.11c7.054-.14 18.177.082 21.333 6.12v-4.628c-.134-5.722-.004-13.522 0-13.832V27.413l.004-2.655-.004.005zm60.442-16.517h-35.55c-8.802 0-17.78 6.856-17.78 16.493v74.259c.004.32.138 8.115 0 13.813v4.627c3.155-6.022 14.279-6.26 21.333-6.114h32V8.25l-.003-.005z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/email.svg b/frontend/src/icons/svg/email.svg
new file mode 100644
index 0000000..74d25e2
--- /dev/null
+++ b/frontend/src/icons/svg/email.svg
@@ -0,0 +1 @@
+<svg width="128" height="96" xmlns="http://www.w3.org/2000/svg"><path d="M64.125 56.975L120.188.912A12.476 12.476 0 0 0 115.5 0h-103c-1.588 0-3.113.3-4.513.838l56.138 56.137z"/><path d="M64.125 68.287l-62.3-62.3A12.42 12.42 0 0 0 0 12.5v71C0 90.4 5.6 96 12.5 96h103c6.9 0 12.5-5.6 12.5-12.5v-71a12.47 12.47 0 0 0-1.737-6.35L64.125 68.287z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/example.svg b/frontend/src/icons/svg/example.svg
new file mode 100644
index 0000000..46f42b5
--- /dev/null
+++ b/frontend/src/icons/svg/example.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M96.258 57.462h31.421C124.794 27.323 100.426 2.956 70.287.07v31.422a32.856 32.856 0 0 1 25.971 25.97zm-38.796-25.97V.07C27.323 2.956 2.956 27.323.07 57.462h31.422a32.856 32.856 0 0 1 25.97-25.97zm12.825 64.766v31.421c30.46-2.885 54.507-27.253 57.713-57.712H96.579c-2.886 13.466-13.146 23.726-26.292 26.291zM31.492 70.287H.07c2.886 30.46 27.253 54.507 57.713 57.713V96.579c-13.466-2.886-23.726-13.146-26.291-26.292z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/excel.svg b/frontend/src/icons/svg/excel.svg
new file mode 100644
index 0000000..74d97b8
--- /dev/null
+++ b/frontend/src/icons/svg/excel.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M78.208 16.576v8.384h38.72v5.376h-38.72v8.704h38.72v5.376h-38.72v8.576h38.72v5.376h-38.72v8.576h38.72v5.376h-38.72v8.576h38.72v5.376h-38.72v8.512h38.72v5.376h-38.72v11.136H128v-94.72H78.208zM0 114.368L72.128 128V0L0 13.632v100.736z"/><path d="M28.672 82.56h-11.2l14.784-23.488-14.08-22.592h11.52l8.192 14.976 8.448-14.976h11.136l-14.08 22.208L58.368 82.56H46.656l-8.768-15.68z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/exit-fullscreen.svg b/frontend/src/icons/svg/exit-fullscreen.svg
new file mode 100644
index 0000000..485c128
--- /dev/null
+++ b/frontend/src/icons/svg/exit-fullscreen.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M49.217 41.329l-.136-35.24c-.06-2.715-2.302-4.345-5.022-4.405h-3.65c-2.712-.06-4.866 2.303-4.806 5.016l.152 19.164-24.151-23.79a6.698 6.698 0 0 0-9.499 0 6.76 6.76 0 0 0 0 9.526l23.93 23.713-18.345.074c-2.712-.069-5.228 1.813-5.64 5.02v3.462c.069 2.721 2.31 4.97 5.022 5.03l35.028-.207c.052.005.087.025.133.025l2.457.054a4.626 4.626 0 0 0 3.436-1.38c.88-.874 1.205-2.096 1.169-3.462l-.262-2.465c0-.048.182-.081.182-.136h.002zm52.523 51.212l18.32-.073c2.713.06 5.224-1.609 5.64-4.815v-3.462c-.068-2.722-2.317-4.97-5.021-5.04l-34.58.21c-.053 0-.086-.021-.138-.021l-2.451-.06a4.64 4.64 0 0 0-3.445 1.381c-.885.868-1.201 2.094-1.174 3.46l.27 2.46c.005.06-.177.095-.177.141l.141 34.697c.069 2.713 2.31 4.338 5.022 4.397l3.45.006c2.705.062 4.867-2.31 4.8-5.026l-.153-18.752 24.151 23.946a6.69 6.69 0 0 0 9.494 0 6.747 6.747 0 0 0 0-9.523L101.74 92.54v.001zM48.125 80.662a4.636 4.636 0 0 0-3.437-1.382l-2.457.06c-.05 0-.082.022-.137.022l-35.025-.21c-2.712.07-4.957 2.318-5.022 5.04v3.462c.409 3.206 2.925 4.874 5.633 4.814l18.554.06-24.132 23.928c-2.62 2.626-2.62 6.89 0 9.524a6.694 6.694 0 0 0 9.496 0l24.155-23.79-.155 18.866c-.06 2.722 2.094 5.093 4.801 5.025h3.65c2.72-.069 4.962-1.685 5.022-4.406l.141-34.956c0-.05-.182-.082-.182-.136l.262-2.46c.03-1.366-.286-2.592-1.166-3.46h-.001zM80.08 47.397a4.62 4.62 0 0 0 3.443 1.374l2.45-.054c.055 0 .088-.02.143-.028l35.08.21c2.712-.062 4.953-2.312 5.021-5.033l.009-3.463c-.417-3.211-2.937-5.084-5.64-5.025l-18.615-.073 23.917-23.715c2.63-2.623 2.63-6.879.008-9.513a6.691 6.691 0 0 0-9.494 0L92.251 26.016l.155-19.312c.065-2.713-2.097-5.085-4.802-5.025h-3.45c-2.713.069-4.954 1.693-5.022 4.406l-.139 35.247c0 .054.18.088.18.136l-.267 2.465c-.028 1.366.288 2.588 1.174 3.463v.001z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/eye-open.svg b/frontend/src/icons/svg/eye-open.svg
new file mode 100644
index 0000000..88dcc98
--- /dev/null
+++ b/frontend/src/icons/svg/eye-open.svg
@@ -0,0 +1 @@
+<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/eye.svg b/frontend/src/icons/svg/eye.svg
new file mode 100644
index 0000000..16ed2d8
--- /dev/null
+++ b/frontend/src/icons/svg/eye.svg
@@ -0,0 +1 @@
+<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/form.svg b/frontend/src/icons/svg/form.svg
new file mode 100644
index 0000000..dcbaa18
--- /dev/null
+++ b/frontend/src/icons/svg/form.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.068 23.784c-1.02 0-1.877-.32-2.572-.96a8.588 8.588 0 0 1-1.738-2.237 11.524 11.524 0 0 1-1.042-2.621c-.232-.895-.348-1.641-.348-2.238V0h.278c.834 0 1.622.085 2.363.256.742.17 1.645.575 2.711 1.214 1.066.64 2.363 1.535 3.892 2.686 1.53 1.15 3.453 2.664 5.77 4.54 2.502 2.045 4.494 3.771 5.977 5.178 1.483 1.406 2.618 2.6 3.406 3.58.787.98 1.274 1.812 1.46 2.494.185.682.277 1.278.277 1.79v2.046H84.068zM127.3 84.01c.278.682.464 1.535.556 2.558.093 1.023-.37 2.003-1.39 2.94-.463.427-.88.832-1.25 1.215-.372.384-.696.704-.974.96a6.69 6.69 0 0 1-.973.767l-11.816-10.741a44.331 44.331 0 0 0 1.877-1.535 31.028 31.028 0 0 1 1.737-1.406c1.112-.938 2.317-1.343 3.615-1.215 1.297.128 2.363.405 3.197.83.927.427 1.923 1.173 2.989 2.239 1.065 1.065 1.876 2.195 2.432 3.388zM78.23 95.902c2.038 0 3.752-.511 5.143-1.534l-26.969 25.83H18.037c-1.761 0-3.684-.47-5.77-1.407a24.549 24.549 0 0 1-5.838-3.709 21.373 21.373 0 0 1-4.518-5.306c-1.204-2.003-1.807-4.07-1.807-6.202V16.495c0-1.79.44-3.665 1.32-5.626A18.41 18.41 0 0 1 5.04 5.562a21.798 21.798 0 0 1 5.213-3.964C12.198.533 14.237 0 16.37 0h53.24v15.984c0 1.62.278 3.367.834 5.242a16.704 16.704 0 0 0 2.572 5.179c1.159 1.577 2.665 2.898 4.518 3.964 1.853 1.066 4.078 1.598 6.673 1.598h20.295v42.325L85.458 92.45c1.02-1.364 1.529-2.856 1.529-4.476 0-2.216-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1c-2.409 0-4.448.789-6.116 2.366-1.668 1.577-2.502 3.474-2.502 5.69 0 2.217.834 4.092 2.502 5.626 1.668 1.535 3.707 2.302 6.117 2.302h52.13zM26.1 47.951c-2.41 0-4.449.789-6.117 2.366-1.668 1.577-2.502 3.473-2.502 5.69 0 2.216.834 4.092 2.502 5.626 1.668 1.534 3.707 2.302 6.117 2.302h52.13c2.409 0 4.47-.768 6.185-2.302 1.715-1.534 2.572-3.41 2.572-5.626 0-2.217-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1zm52.407 64.063l1.807-1.663 3.476-3.196a479.75 479.75 0 0 0 4.587-4.284 500.757 500.757 0 0 1 5.004-4.667c3.985-3.666 8.48-7.758 13.485-12.276l11.677 10.741-13.485 12.404-5.004 4.603-4.587 4.22a179.46 179.46 0 0 0-3.267 3.068c-.88.853-1.367 1.322-1.46 1.407-.463.341-.973.703-1.529 1.087-.556.383-1.112.703-1.668.959-.556.256-1.413.575-2.572.959a83.5 83.5 0 0 1-3.545 1.087 72.2 72.2 0 0 1-3.475.895c-1.112.256-1.946.426-2.502.511-1.112.17-1.854.043-2.224-.383-.371-.426-.464-1.151-.278-2.174.092-.511.278-1.279.556-2.302.278-1.023.602-2.067.973-3.132l1.042-3.005c.325-.938.58-1.577.765-1.918a10.157 10.157 0 0 1 2.224-2.941z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/fullscreen.svg b/frontend/src/icons/svg/fullscreen.svg
new file mode 100644
index 0000000..0e86b6f
--- /dev/null
+++ b/frontend/src/icons/svg/fullscreen.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M38.47 52L52 38.462l-23.648-23.67L43.209 0H.035L0 43.137l14.757-14.865L38.47 52zm74.773 47.726L89.526 76 76 89.536l23.648 23.672L84.795 128h43.174L128 84.863l-14.757 14.863zM89.538 52l23.668-23.648L128 43.207V.038L84.866 0 99.73 14.76 76 38.472 89.538 52zM38.46 76L14.792 99.651 0 84.794v43.173l43.137.033-14.865-14.757L52 89.53 38.46 76z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/guide.svg b/frontend/src/icons/svg/guide.svg
new file mode 100644
index 0000000..b271001
--- /dev/null
+++ b/frontend/src/icons/svg/guide.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M1.482 70.131l36.204 16.18 69.932-65.485-61.38 70.594 46.435 18.735c1.119.425 2.397-.17 2.797-1.363v-.085L127.998.047 1.322 65.874c-1.12.597-1.519 1.959-1.04 3.151.32.511.72.937 1.2 1.107zm44.676 57.821L64.22 107.26l-18.062-7.834v28.527z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/icon.svg b/frontend/src/icons/svg/icon.svg
new file mode 100644
index 0000000..82be8ee
--- /dev/null
+++ b/frontend/src/icons/svg/icon.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.147.062a13 13 0 0 1 4.94.945c1.55.63 2.907 1.526 4.069 2.688a13.148 13.148 0 0 1 2.761 4.069c.678 1.55 1.017 3.245 1.017 5.086v102.3c0 3.681-1.187 6.733-3.56 9.155-2.373 2.422-5.352 3.633-8.937 3.633H12.992c-3.875 0-7-1.26-9.373-3.779-2.373-2.518-3.56-5.667-3.56-9.445V12.704c0-3.39 1.163-6.345 3.488-8.863C5.872 1.32 8.972.062 12.847.062h102.3zM81.434 109.047c1.744 0 3.003-.412 3.778-1.235.775-.824 1.163-1.914 1.163-3.27 0-1.26-.388-2.325-1.163-3.197-.775-.872-2.034-1.307-3.778-1.307H72.57c.097-.194.145-.485.145-.872V27.09h9.01c1.743 0 2.954-.436 3.633-1.308.678-.872 1.017-1.938 1.017-3.197 0-1.26-.34-2.325-1.017-3.197-.679-.872-1.89-1.308-3.633-1.308H46.268c-1.743 0-2.954.436-3.632 1.308-.678.872-1.018 1.938-1.018 3.197 0 1.26.34 2.325 1.018 3.197.678.872 1.889 1.308 3.632 1.308h8.138v72.075c0 .193.024.339.073.436.048.096.072.242.072.436H46.56c-1.744 0-3.003.435-3.778 1.307-.775.872-1.163 1.938-1.163 3.197 0 1.356.388 2.446 1.163 3.27.775.823 2.034 1.235 3.778 1.235h34.875z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/international.svg b/frontend/src/icons/svg/international.svg
new file mode 100644
index 0000000..e9b56ee
--- /dev/null
+++ b/frontend/src/icons/svg/international.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M83.287 103.01c-1.57-3.84-6.778-10.414-15.447-19.548-2.327-2.444-2.182-4.306-1.338-9.862v-.64c.553-3.81 1.513-6.05 14.313-8.087 6.516-1.018 8.203 1.57 10.589 5.178l.785 1.193a12.625 12.625 0 0 0 6.43 5.207c1.134.524 2.53 1.164 4.421 2.24 4.596 2.53 4.596 5.41 4.596 11.753v.727a26.91 26.91 0 0 1-5.178 17.454 59.055 59.055 0 0 1-19.025 11.026c3.49-6.546.814-14.313 0-16.553l-.146-.087zM64 5.12a58.502 58.502 0 0 1 25.484 5.818 54.313 54.313 0 0 0-12.859 10.327c-.93 1.28-1.716 2.473-2.472 3.579-2.444 3.694-3.637 5.352-5.818 5.614a25.105 25.105 0 0 1-4.219 0c-4.276-.29-10.094-.64-11.956 4.422-1.193 3.23-1.396 11.956 2.444 16.495.66 1.077.778 2.4.32 3.578a7.01 7.01 0 0 1-2.066 3.229 18.938 18.938 0 0 1-2.909-2.91 18.91 18.91 0 0 0-8.32-6.603c-1.25-.349-2.647-.64-3.985-.93-3.782-.786-8.03-1.688-9.019-3.812a14.895 14.895 0 0 1-.727-5.818 21.935 21.935 0 0 0-1.396-9.25 8.873 8.873 0 0 0-5.557-4.946A58.705 58.705 0 0 1 64 5.12zM0 64c0 35.346 28.654 64 64 64 35.346 0 64-28.654 64-64 0-35.346-28.654-64-64-64C28.654 0 0 28.654 0 64z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/language.svg b/frontend/src/icons/svg/language.svg
new file mode 100644
index 0000000..0082b57
--- /dev/null
+++ b/frontend/src/icons/svg/language.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.742 36.8c2.398 7.2 5.595 12.8 11.19 18.4 4.795-4.8 7.992-11.2 10.39-18.4h-21.58zm-52.748 40h20.78l-10.39-28-10.39 28z"/><path d="M111.916 0H16.009C7.218 0 .025 7.2.025 16v96c0 8.8 7.193 16 15.984 16h95.907c8.791 0 15.984-7.2 15.984-16V16c0-8.8-6.394-16-15.984-16zM72.754 103.2c-1.598 1.6-3.197 1.6-4.795 1.6-.8 0-2.398 0-3.197-.8-.8-.8-1.599 0-1.599-.8s-.799-1.6-1.598-3.2c-.8-1.6-.8-2.4-1.599-4l-3.196-8.8H28.797L25.6 96c-1.598 3.2-2.398 5.6-3.197 7.2-.8 1.6-2.398 1.6-4.795 1.6-1.599 0-3.197-.8-4.796-1.6-1.598-1.6-2.397-2.4-2.397-4 0-.8 0-1.6.799-3.2.8-1.6.8-2.4 1.598-4l17.583-44.8c.8-1.6.8-3.2 1.599-4.8.799-1.6 1.598-3.2 2.397-4 .8-.8 1.599-2.4 3.197-3.2 1.599-.8 3.197-.8 4.796-.8 1.598 0 3.196 0 4.795.8 1.598.8 2.398 1.6 3.197 3.2.799.8 1.598 2.4 2.397 4 .8 1.6 1.599 3.2 2.398 5.6l17.583 44c1.598 3.2 2.398 5.6 2.398 7.2-.8.8-1.599 2.4-2.398 4zM116.711 72c-8.791-3.2-15.185-7.2-20.78-12-5.594 5.6-12.787 9.6-21.579 12l-2.397-4c8.791-2.4 15.984-5.6 21.579-11.2C87.939 51.2 83.144 44 81.545 36h-7.992v-3.2h21.58c-1.6-2.4-3.198-5.6-4.796-8l2.397-.8c1.599 2.4 3.997 5.6 5.595 8.8h19.98v4h-7.992c-2.397 8-6.393 15.2-11.189 20 5.595 4.8 11.988 8.8 20.78 11.2l-3.197 4z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/link.svg b/frontend/src/icons/svg/link.svg
new file mode 100644
index 0000000..48197ba
--- /dev/null
+++ b/frontend/src/icons/svg/link.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/list.svg b/frontend/src/icons/svg/list.svg
new file mode 100644
index 0000000..20259ed
--- /dev/null
+++ b/frontend/src/icons/svg/list.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M1.585 12.087c0 6.616 3.974 11.98 8.877 11.98 4.902 0 8.877-5.364 8.877-11.98 0-6.616-3.975-11.98-8.877-11.98-4.903 0-8.877 5.364-8.877 11.98zM125.86.107H35.613c-1.268 0-2.114 1.426-2.114 2.852v18.255c0 1.712 1.057 2.853 2.114 2.853h90.247c1.268 0 2.114-1.426 2.114-2.853V2.96c0-1.711-1.057-2.852-2.114-2.852zM.106 62.86c0 6.615 3.974 11.979 8.876 11.979 4.903 0 8.877-5.364 8.877-11.98 0-6.616-3.974-11.98-8.877-11.98-4.902 0-8.876 5.364-8.876 11.98zM124.17 50.88H33.921c-1.268 0-2.114 1.425-2.114 2.851v18.256c0 1.711 1.057 2.852 2.114 2.852h90.247c1.268 0 2.114-1.426 2.114-2.852V53.73c0-1.426-.846-2.852-2.114-2.852zM.106 115.913c0 6.616 3.974 11.98 8.876 11.98 4.903 0 8.877-5.364 8.877-11.98 0-6.616-3.974-11.98-8.877-11.98-4.902 0-8.876 5.364-8.876 11.98zm124.064-11.98H33.921c-1.268 0-2.114 1.426-2.114 2.853v18.255c0 1.711 1.057 2.852 2.114 2.852h90.247c1.268 0 2.114-1.426 2.114-2.852v-18.255c0-1.427-.846-2.853-2.114-2.853z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/lock.svg b/frontend/src/icons/svg/lock.svg
new file mode 100644
index 0000000..74fee54
--- /dev/null
+++ b/frontend/src/icons/svg/lock.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M119.88 49.674h-7.987V39.52C111.893 17.738 90.45.08 63.996.08 37.543.08 16.1 17.738 16.1 39.52v10.154H8.113c-4.408 0-7.987 2.94-7.987 6.577v65.13c0 3.637 3.57 6.577 7.987 6.577H119.88c4.407 0 7.987-2.94 7.987-6.577v-65.13c-.008-3.636-3.58-6.577-7.987-6.577zm-23.953 0H32.065V39.52c0-14.524 14.301-26.295 31.931-26.295 17.63 0 31.932 11.777 31.932 26.295v10.153z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/message.svg b/frontend/src/icons/svg/message.svg
new file mode 100644
index 0000000..14ca817
--- /dev/null
+++ b/frontend/src/icons/svg/message.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 20.967v59.59c0 11.59 8.537 20.966 19.075 20.966h28.613l1 26.477L76.8 101.523h32.125c10.538 0 19.075-9.377 19.075-20.966v-59.59C128 9.377 119.463 0 108.925 0h-89.85C8.538 0 0 9.377 0 20.967zm82.325 33.1c0-5.524 4.013-9.935 9.037-9.935 5.026 0 9.038 4.41 9.038 9.934 0 5.524-4.025 9.934-9.038 9.934-5.024 0-9.037-4.41-9.037-9.934zm-27.613 0c0-5.524 4.013-9.935 9.038-9.935s9.037 4.41 9.037 9.934c0 5.524-4.025 9.934-9.037 9.934-5.025 0-9.038-4.41-9.038-9.934zm-27.1 0c0-5.524 4.013-9.935 9.038-9.935s9.038 4.41 9.038 9.934c0 5.524-4.026 9.934-9.05 9.934-5.013 0-9.025-4.41-9.025-9.934z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/money.svg b/frontend/src/icons/svg/money.svg
new file mode 100644
index 0000000..c1580de
--- /dev/null
+++ b/frontend/src/icons/svg/money.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M54.122 127.892v-28.68H7.513V87.274h46.609v-12.4H7.513v-12.86h38.003L.099 0h22.6l32.556 45.07c3.617 5.144 6.44 9.611 8.487 13.385 1.788-3.05 4.89-7.779 9.301-14.186L103.93 0h24.01L82.385 62.013h38.34v12.862h-46.41v12.4h46.41v11.937h-46.41v28.68H54.123z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/nested.svg b/frontend/src/icons/svg/nested.svg
new file mode 100644
index 0000000..06713a8
--- /dev/null
+++ b/frontend/src/icons/svg/nested.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.002 9.2c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-5.043-3.58-9.132-7.997-9.132S.002 4.157.002 9.2zM31.997.066h95.981V18.33H31.997V.066zm0 45.669c0 5.044 3.58 9.132 7.998 9.132 4.417 0 7.997-4.088 7.997-9.132 0-3.263-1.524-6.278-3.998-7.91-2.475-1.63-5.524-1.63-7.998 0-2.475 1.632-4 4.647-4 7.91zM63.992 36.6h63.986v18.265H63.992V36.6zm-31.995 82.2c0 5.043 3.58 9.132 7.998 9.132 4.417 0 7.997-4.089 7.997-9.132 0-5.044-3.58-9.133-7.997-9.133s-7.998 4.089-7.998 9.133zm31.995-9.131h63.986v18.265H63.992V109.67zm0-27.404c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-3.263-1.524-6.277-3.998-7.909-2.475-1.631-5.524-1.631-7.998 0-2.475 1.632-4 4.646-4 7.91zm31.995-9.13h31.991V91.4H95.987V73.135z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/password.svg b/frontend/src/icons/svg/password.svg
new file mode 100644
index 0000000..e291d85
--- /dev/null
+++ b/frontend/src/icons/svg/password.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/pdf.svg b/frontend/src/icons/svg/pdf.svg
new file mode 100644
index 0000000..957aa0c
--- /dev/null
+++ b/frontend/src/icons/svg/pdf.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><path d="M869.073 277.307H657.111V65.344l211.962 211.963zm-238.232 26.27V65.344l-476.498-.054v416.957h714.73v-178.67H630.841zm-335.836 360.57c-5.07-3.064-10.944-5.133-17.61-6.201-6.67-1.064-13.603-1.6-20.81-1.6h-48.821v85.641h48.822c7.206 0 14.14-.532 20.81-1.6 6.665-1.065 12.54-3.133 17.609-6.202 5.064-3.063 9.134-7.406 12.208-13.007 3.065-5.602 4.6-12.937 4.6-22.011 0-9.07-1.535-16.408-4.6-22.01-3.074-5.603-7.144-9.94-12.208-13.01zM35.82 541.805v416.904h952.358V541.805H35.821zm331.421 191.179c-3.6 11.071-9.343 20.879-17.209 29.413-7.874 8.542-18.078 15.408-30.617 20.61-12.544 5.206-27.747 7.807-45.621 7.807h-66.036v102.45h-62.831V607.517h128.867c17.874 0 33.077 2.6 45.62 7.802 12.541 5.207 22.745 12.076 30.618 20.615 7.866 8.538 13.604 18.277 17.21 29.212 3.6 10.943 5.401 22.278 5.401 34.018 0 11.477-1.8 22.752-5.402 33.819zM644.9 806.417c-5.343 17.61-13.408 32.818-24.212 45.627-10.807 12.803-24.283 22.879-40.423 30.213-16.146 7.343-35.155 11.007-57.03 11.007h-123.26V607.518h123.26c18.41 0 35.552 2.941 51.428 8.808 15.873 5.869 29.618 14.671 41.22 26.412 11.608 11.744 20.674 26.411 27.217 44.02 6.535 17.61 9.803 38.288 9.803 62.035 0 20.81-2.67 40.02-8.003 57.624zm245.362-146.07h-138.07v66.03h119.66v48.829h-119.66v118.058h-62.83V607.518h200.9v52.829h-.001zm-318.2 25.611c-6.402-8.266-14.877-14.604-25.412-19.01-10.544-4.402-23.551-6.602-39.019-6.602h-44.825v180.088h56.029c9.07 0 17.872-1.463 26.415-4.401 8.535-2.932 16.14-7.802 22.812-14.609 6.665-6.8 12.007-15.667 16.007-26.61 4.003-10.94 6.003-24.275 6.003-40.021 0-14.408-1.4-27.416-4.202-39.019-2.8-11.607-7.406-21.542-13.808-29.816zm0 0"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/people.svg b/frontend/src/icons/svg/people.svg
new file mode 100644
index 0000000..2bd54ae
--- /dev/null
+++ b/frontend/src/icons/svg/people.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M104.185 95.254c8.161 7.574 13.145 17.441 13.145 28.28 0 1.508-.098 2.998-.285 4.466h-10.784c.238-1.465.403-2.948.403-4.465 0-8.983-4.36-17.115-11.419-23.216C86 104.66 75.355 107.162 64 107.162c-11.344 0-21.98-2.495-31.22-6.83-7.064 6.099-11.444 14.218-11.444 23.203 0 1.517.165 3 .403 4.465H10.955a35.444 35.444 0 0 1-.285-4.465c0-10.838 4.974-20.713 13.127-28.291C9.294 85.42.003 70.417.003 53.58.003 23.99 28.656.001 64 .001s63.997 23.988 63.997 53.58c0 16.842-9.299 31.85-23.812 41.673zM64 36.867c-29.454 0-53.33-10.077-53.33 15.342 0 25.418 23.876 46.023 53.33 46.023 29.454 0 53.33-20.605 53.33-46.023 0-25.419-23.876-15.342-53.33-15.342zm24.888 25.644c-3.927 0-7.111-2.665-7.111-5.953 0-3.288 3.184-5.954 7.11-5.954 3.928 0 7.111 2.666 7.111 5.954s-3.183 5.953-7.11 5.953zm-3.556 16.372c0 4.11-9.55 7.442-21.332 7.442-11.781 0-21.332-3.332-21.332-7.442 0-1.06.656-2.064 1.8-2.976 3.295 2.626 10.79 4.465 19.532 4.465 8.743 0 16.237-1.84 19.531-4.465 1.145.912 1.801 1.916 1.801 2.976zm-46.22-16.372c-3.927 0-7.11-2.665-7.11-5.953 0-3.288 3.183-5.954 7.11-5.954 3.927 0 7.111 2.666 7.111 5.954s-3.184 5.953-7.11 5.953z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/peoples.svg b/frontend/src/icons/svg/peoples.svg
new file mode 100644
index 0000000..aab852e
--- /dev/null
+++ b/frontend/src/icons/svg/peoples.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M95.648 118.762c0 5.035-3.563 9.121-7.979 9.121H7.98c-4.416 0-7.979-4.086-7.979-9.121C0 100.519 15.408 83.47 31.152 76.75c-9.099-6.43-15.216-17.863-15.216-30.987v-9.128c0-20.16 14.293-36.518 31.893-36.518s31.894 16.358 31.894 36.518v9.122c0 13.137-6.123 24.556-15.216 30.993 15.738 6.726 31.141 23.769 31.141 42.012z"/><path d="M106.032 118.252h15.867c3.376 0 6.101-3.125 6.101-6.972 0-13.957-11.787-26.984-23.819-32.123 6.955-4.919 11.638-13.66 11.638-23.704v-6.985c0-15.416-10.928-27.926-24.39-27.926-1.674 0-3.306.193-4.89.561 1.936 4.713 3.018 9.974 3.018 15.526v9.121c0 13.137-3.056 23.111-11.066 30.993 14.842 4.41 27.312 23.42 27.541 41.509z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/qq.svg b/frontend/src/icons/svg/qq.svg
new file mode 100644
index 0000000..ee13d4e
--- /dev/null
+++ b/frontend/src/icons/svg/qq.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M18.448 57.545l-.244-.744-.198-.968-.132-.53v-2.181l.236-.859.24-.908.317-.953.428-1.06.561-1.103.794-1.104v-.773l.077-.724.123-.984.34-1.106.313-1.194.25-.548.289-.511.371-.569.405-.423v-2.73l.234-1.407.236-1.633.42-1.955.577-2.035.43-1.118.426-1.217.468-1.135.559-1.216.57-1.332.655-1.247.737-1.331.929-1.33.43-.762.457-.624.995-1.406 1.025-1.403 1.163-1.444 1.246-1.405 1.352-1.384 1.41-1.423 1.708-1.536 1.083-.934 1.322-1.008 1.34-.89 1.448-.855 1.392-.76 1.57-.63 1.667-.775 1.657-.532 1.653-.552 1.787-.548 1.785-.417 1.876-.347L59.128.68l1.879-.245 1.876-.252 2.002-.106h5.912l1.97.243 1.981.231 2.019.207 1.874.441 1.979.413 1.857.475 2.035.53 1.862.646 1.782.738 1.904.78 1.736.853 1.689.95 1.655 1.044 1.425.971.662.548.693.401 1.323 1.1 1.115 1.064 1.112 1.1 1.083 1.214.894 1.178 1.064 1.217.74 1.306.752 1.162.798 1.352.661 1.175 1.113 2.489.546 1.286.428 1.192.428 1.294.384 1.217.267 1.047.347 1.231.607 2.198.388 1.924.253 1.861.217 1.497.342 2.28.077.362.274.41.737 1.18.473.8.42.832.534.892.472 1.07.307 1.093.334 1.2.252 1.232.115.605.106.746v.648l-.106.643v.8l-.192.774-.35 1.5-.403.76-.299.852v.213l.142.264.4.623 1.746 2.53 1.377 1.9.66 1.267.889 1.389.774 1.52.893 1.627.894 1.828 1.006 2.069.567 1.268.518 1.239.447 1.307.44 1.175.336 1.235.342 1.16.432 2.261.343 2.31.235 2.05v2.891l-.158 1.025-.226 1.768-.308 1.59-.48 1.44-.18.588-.336.707-.28.493-.375.607-.33.383-.42.494-.375.4-.401.34-.48.207-.432.207-.355.114h-.543l-.346-.114-.66-.32-.302-.212-.317-.223-.347-.304-.35-.342-.579-.63-.684-.89-.539-.917-.538-.734-.526-.855-.741-1.517-.833-1.579-.098-.055h-.138l-.338.247-.196.415-.326.516-.567 1.533-.856 2.182-1.096 2.626-.824 1.308-.864 1.366-1.027 1.536-1.09 1.503-.557.68-.676.743-1.555 1.497.136.135.21.214.777.446 3.235 1.524 1.41.779 1.347.756 1.332.953 1.187.982.574.443.432.511.445.593.367.643.198.533.242.64.105.554.115.647-.115.433v.44l-.105.454-.242.415-.092.325-.22.394-.587.784-.543.627-.42.47-.35.348-.893.638-1.01.556-1.077.532-1.155.511-1.287.495-.693.207-.608.167-1.496.342-1.545.325-1.552.323-1.689.27-1.74.072-1.785.21h-5.539l-1.998-.114-1.86-.168-2.005-.27-1.99-.209-2.095-.286-2.03-.495-1.981-.374-1.968-.552-2.019-.707-1.98-.585-1.044-.342-.927-.323-.586-.223-.582-.12h-1.647l-1.904-.131-.962-.096-1.24-.135-.795.705-1.085.665-1.471.701-1.628.875-.99.475-1.033.376-2.281.914-1.24.305-1.3.343-1.803.344-1.13.086-1.193.1-1.246.135-1.45.053h-5.926l-3.346-.053-3.25-.321-1.644-.23-1.589-.23-1.546-.227-1.547-.305-1.442-.456-1.434-.325-1.294-.51-1.223-.474-1.142-.533-.99-.583-.984-.71-.336-.343-.44-.415-.334-.362-.3-.417-.278-.415-.215-.42-.311-.89-.109-.46-.138-.51v-.473l.138-.533v-.53l.109-.53v-1.069l.052-.564.259-.647.215-.646.39-.779.286-.3.236-.348.615-.738.49-.38.464-.266.428-.338.676-.21.543-.324.676-.341.77-.227.775-.231.897-.192.85-.11 1.008-.13 1.093-.081.284-.092h.063l.137-.115v-.13l-.2-.266-.58-.27-1.45-1.231-.975-.761-1.127-.967-1.136-1.082-1.181-1.382-1.36-1.558-.508-.843-.672-.87-.58-1.007-.522-1.1-.704-1.047-.459-1.194-.547-1.192-.546-1.33-.397-1.273-.378-1.575-.112-.057h-.115l-.059-.113h-.14l-.23.113-.114.057-.158.264-.057.321-.119.286-.206.477-.664 1.157-.345.701-.546.612-.58.736-.641.816-.677.724-.795.701-.734.658-.814.524-.89.546-.855.325-1.008.247-.99.095h-.233l-.228-.095-.18-.384-.29-.188-.38-.912-.237-.493-.255-.707-.21-.734-.113-.724-.313-1.648-.12-.972v-3.185l.12-2.379.196-1.214.23-1.252.21-1.347.374-1.254.42-1.443.431-1.407.578-1.448.545-1.38.754-1.4.699-1.52.855-1.425 1.006-1.538 1.023-1.382 1.069-1.538.891-1.071 1.142-1.227 1.202-1.237.56-.59.678-.662.985-.836 1.012-.853 1.647-1.446 1.242-.889z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/search.svg b/frontend/src/icons/svg/search.svg
new file mode 100644
index 0000000..84233dd
--- /dev/null
+++ b/frontend/src/icons/svg/search.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M124.884 109.812L94.256 79.166c-.357-.357-.757-.629-1.129-.914a50.366 50.366 0 0 0 8.186-27.59C101.327 22.689 78.656 0 50.67 0 22.685 0 0 22.688 0 50.663c0 27.989 22.685 50.663 50.656 50.663 10.186 0 19.643-3.03 27.6-8.201.286.385.557.771.9 1.114l30.628 30.632a10.633 10.633 0 0 0 7.543 3.129c2.728 0 5.457-1.043 7.543-3.115 4.171-4.157 4.171-10.915.014-15.073M50.671 85.338C31.557 85.338 16 69.78 16 50.663c0-19.102 15.557-34.661 34.67-34.661 19.115 0 34.657 15.559 34.657 34.675 0 19.102-15.557 34.661-34.656 34.661"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/shopping.svg b/frontend/src/icons/svg/shopping.svg
new file mode 100644
index 0000000..87513e7
--- /dev/null
+++ b/frontend/src/icons/svg/shopping.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M42.913 101.36c1.642 0 3.198.332 4.667.996a12.28 12.28 0 0 1 3.89 2.772c1.123 1.184 1.987 2.582 2.592 4.193.605 1.612.908 3.318.908 5.118 0 1.8-.303 3.507-.908 5.118-.605 1.611-1.469 3.01-2.593 4.194a13.3 13.3 0 0 1-3.889 2.843 10.582 10.582 0 0 1-4.667 1.066c-1.729 0-3.306-.355-4.732-1.066a13.604 13.604 0 0 1-3.825-2.843c-1.123-1.185-1.988-2.583-2.593-4.194a14.437 14.437 0 0 1-.907-5.118c0-1.8.302-3.506.907-5.118.605-1.61 1.47-3.009 2.593-4.193a12.515 12.515 0 0 1 3.825-2.772c1.426-.664 3.003-.996 4.732-.996zm53.932.285c1.643 0 3.22.331 4.733.995a11.386 11.386 0 0 1 3.889 2.772c1.08 1.185 1.945 2.583 2.593 4.194.648 1.61.972 3.317.972 5.118 0 1.8-.324 3.506-.972 5.117-.648 1.611-1.513 3.01-2.593 4.194a12.253 12.253 0 0 1-3.89 2.843 11 11 0 0 1-4.732 1.066 10.58 10.58 0 0 1-4.667-1.066 12.478 12.478 0 0 1-3.824-2.843c-1.08-1.185-1.945-2.583-2.593-4.194a13.581 13.581 0 0 1-.973-5.117c0-1.801.325-3.507.973-5.118.648-1.611 1.512-3.01 2.593-4.194a11.559 11.559 0 0 1 3.824-2.772 11.212 11.212 0 0 1 4.667-.995zm21.781-80.747c2.42 0 4.3.355 5.64 1.066 1.34.71 2.29 1.587 2.852 2.63a6.427 6.427 0 0 1 .778 3.34c-.044 1.185-.195 2.204-.454 3.057-.26.853-.8 2.606-1.62 5.26a589.268 589.268 0 0 1-2.788 8.743 1236.373 1236.373 0 0 0-3.047 9.453c-.994 3.128-1.75 5.592-2.269 7.393-1.123 3.79-2.55 6.42-4.278 7.89-1.728 1.469-3.846 2.203-6.352 2.203H39.023l1.945 12.795h65.342c4.148 0 6.223 1.943 6.223 5.828 0 1.896-.41 3.53-1.232 4.905-.821 1.374-2.442 2.061-4.862 2.061H38.505c-1.729 0-3.176-.426-4.343-1.28-1.167-.852-2.14-1.966-2.917-3.34a21.277 21.277 0 0 1-1.88-4.478 44.128 44.128 0 0 1-1.102-4.55c-.087-.568-.324-1.942-.713-4.122-.39-2.18-.865-4.904-1.426-8.174l-1.88-10.947c-.692-4.027-1.383-8.079-2.075-12.154-1.642-9.572-3.5-20.234-5.574-31.986H6.87c-1.296 0-2.377-.356-3.24-1.067a9.024 9.024 0 0 1-2.14-2.558 10.416 10.416 0 0 1-1.167-3.2C.108 8.53 0 7.488 0 6.54c0-1.896.583-3.46 1.75-4.69C2.917.615 4.494 0 6.482 0h13.095c1.728 0 3.111.284 4.148.853 1.037.569 1.858 1.28 2.463 2.132a8.548 8.548 0 0 1 1.297 2.701c.26.948.475 1.754.648 2.417.173.758.346 1.825.519 3.199.173 1.374.345 2.772.518 4.193.26 1.706.519 3.507.778 5.403h88.678z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/size.svg b/frontend/src/icons/svg/size.svg
new file mode 100644
index 0000000..ddb25b8
--- /dev/null
+++ b/frontend/src/icons/svg/size.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 54.857h54.796v18.286H36.531V128H18.265V73.143H0V54.857zm127.857-36.571H91.935V128H72.456V18.286H36.534V0h91.326l-.003 18.286z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/skill.svg b/frontend/src/icons/svg/skill.svg
new file mode 100644
index 0000000..a3b7312
--- /dev/null
+++ b/frontend/src/icons/svg/skill.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M31.652 93.206h33.401c1.44 2.418 3.077 4.663 4.93 6.692h-38.33v-6.692zm0-10.586h28.914a44.8 44.8 0 0 1-1.264-6.688h-27.65v6.688zm0-17.27H59.39c.288-2.286.714-4.532 1.34-6.687H31.65v6.687h.003zm53.913 44.84v5.85c0 2.798-2.095 5.075-4.667 5.075h-70.07c-2.576 0-4.663-2.277-4.663-5.075V31.26l23.22-20.96v22.25H17.16v6.688h18.39V6.688h45.348c2.576 0 4.667 2.277 4.667 5.066v20.009c1.987-.675 4.053-1.128 6.17-1.445v-18.56C91.738 5.28 86.874 0 80.902 0H31.15L0 28.118v87.917c0 6.48 4.859 11.759 10.832 11.759h70.07c5.974 0 10.837-5.27 10.837-11.759v-4.41c-2.117-.312-4.183-.765-6.17-1.435h-.004zM23.279 58.667h-7.96v6.688h7.96v-6.688zm-7.956 41.23h7.96v-6.691h-7.96v6.692zm7.956-23.96h-7.96v6.687h7.96v-6.688zm89.718-15.042l-4.896-4.07-12.447 17.613-11.19-9.305-3.762 5.311 16.091 13.38 16.204-22.929zM128 70.978c0-18.632-13.97-33.782-31.147-33.782-17.168 0-31.135 15.155-31.135 33.782 0 18.628 13.97 33.783 31.135 33.783 17.172 0 31.143-15.15 31.143-33.783H128zm-6.17 0c0 14.933-11.203 27.1-24.981 27.1-13.77 0-24.987-12.158-24.987-27.1 0-14.941 11.195-27.099 24.987-27.099 13.778 0 24.982 12.158 24.982 27.1z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/star.svg b/frontend/src/icons/svg/star.svg
new file mode 100644
index 0000000..6cf86e6
--- /dev/null
+++ b/frontend/src/icons/svg/star.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M70.66 4.328l14.01 29.693c1.088 2.29 3.177 3.882 5.603 4.25l31.347 4.76c6.087.926 8.528 8.756 4.117 13.247L103.05 79.395c-1.75 1.78-2.544 4.352-2.132 6.867l5.352 32.641c1.043 6.337-5.33 11.182-10.778 8.19l-28.039-15.409a7.13 7.13 0 0 0-6.91 0l-28.039 15.41c-5.448 2.99-11.821-1.854-10.777-8.19l5.352-32.642c.415-2.515-.387-5.088-2.136-6.867L2.264 56.278C-2.146 51.787.286 43.957 6.38 43.031l31.343-4.76c2.419-.368 4.51-1.96 5.595-4.25L57.334 4.328c2.728-5.77 10.605-5.77 13.325 0z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/tab.svg b/frontend/src/icons/svg/tab.svg
new file mode 100644
index 0000000..b4b48e4
--- /dev/null
+++ b/frontend/src/icons/svg/tab.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M78.921.052H49.08c-1.865 0-3.198 1.599-3.198 3.464v6.661c0 1.865 1.6 3.464 3.198 3.464h29.84c1.865 0 3.198-1.599 3.198-3.464V3.516C82.385 1.65 80.786.052 78.92.052zm45.563 0H94.642c-1.865 0-3.464 1.599-3.464 3.464v6.661c0 1.865 1.599 3.464 3.464 3.464h29.842c1.865-.266 3.464-1.599 3.464-3.464V3.516c0-1.865-1.599-3.464-3.464-3.464zm0 22.382H40.02c-1.866 0-3.464-1.599-3.464-3.464V3.516c0-1.865-1.599-3.464-3.464-3.464H3.516C1.65.052.052 1.651.052 3.516V124.75c0 1.598 1.599 3.197 3.464 3.197h120.968c1.865 0 3.464-1.599 3.464-3.464V25.898c0-1.865-1.599-3.464-3.464-3.464z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/table.svg b/frontend/src/icons/svg/table.svg
new file mode 100644
index 0000000..0e3dc9d
--- /dev/null
+++ b/frontend/src/icons/svg/table.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/theme.svg b/frontend/src/icons/svg/theme.svg
new file mode 100644
index 0000000..5982a2f
--- /dev/null
+++ b/frontend/src/icons/svg/theme.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M125.5 36.984L95.336 2.83C93.735 1.018 91.565 0 89.3 0c-2.263 0-4.433 1.018-6.033 2.83l-3.786 4.286c-1.6 1.812-3.77 2.83-6.032 2.831H54.553c-2.263 0-4.434-1.018-6.033-2.83L44.734 2.83C43.134 1.018 40.964 0 38.701 0c-2.263 0-4.434 1.018-6.034 2.83L2.5 36.984C.9 38.796 0 41.254 0 43.815c0 2.562.899 5.02 2.5 6.831L14.565 64.31c2.178 2.468 5.367 3.403 8.33 2.444 1.35-.435 2.709.592 2.709 2.18v49.407c0 5.313 3.84 9.66 8.532 9.66h59.726c4.693 0 8.532-4.347 8.532-9.66V68.934c0-1.59 1.36-2.616 2.71-2.181 2.962.96 6.15.024 8.329-2.444L125.5 50.646c1.6-1.811 2.499-4.269 2.499-6.83 0-2.563-.899-5.02-2.5-6.832z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/tree-table.svg b/frontend/src/icons/svg/tree-table.svg
new file mode 100644
index 0000000..8aafdb8
--- /dev/null
+++ b/frontend/src/icons/svg/tree-table.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M44.8 0h79.543C126.78 0 128 1.422 128 4.267v23.466c0 2.845-1.219 4.267-3.657 4.267H44.8c-2.438 0-3.657-1.422-3.657-4.267V4.267C41.143 1.422 42.362 0 44.8 0zm22.857 48h56.686c2.438 0 3.657 1.422 3.657 4.267v23.466c0 2.845-1.219 4.267-3.657 4.267H67.657C65.22 80 64 78.578 64 75.733V52.267C64 49.422 65.219 48 67.657 48zm0 48h56.686c2.438 0 3.657 1.422 3.657 4.267v23.466c0 2.845-1.219 4.267-3.657 4.267H67.657C65.22 128 64 126.578 64 123.733v-23.466C64 97.422 65.219 96 67.657 96zM50.286 68.267c2.02 0 3.657-1.91 3.657-4.267 0-2.356-1.638-4.267-3.657-4.267H17.37V32h6.4c2.02 0 3.658-1.91 3.658-4.267V4.267C27.429 1.91 25.79 0 23.77 0H3.657C1.637 0 0 1.91 0 4.267v23.466C0 30.09 1.637 32 3.657 32h6.4v80c0 2.356 1.638 4.267 3.657 4.267h36.572c2.02 0 3.657-1.91 3.657-4.267 0-2.356-1.638-4.267-3.657-4.267H17.37V68.267h32.915z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/tree.svg b/frontend/src/icons/svg/tree.svg
new file mode 100644
index 0000000..dd4b7dd
--- /dev/null
+++ b/frontend/src/icons/svg/tree.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 5.186 5.186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/user.svg b/frontend/src/icons/svg/user.svg
new file mode 100644
index 0000000..0ba0716
--- /dev/null
+++ b/frontend/src/icons/svg/user.svg
@@ -0,0 +1 @@
+<svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/wechat.svg b/frontend/src/icons/svg/wechat.svg
new file mode 100644
index 0000000..c586e55
--- /dev/null
+++ b/frontend/src/icons/svg/wechat.svg
@@ -0,0 +1 @@
+<svg width="128" height="110" xmlns="http://www.w3.org/2000/svg"><path d="M86.635 33.334c1.467 0 2.917.113 4.358.283C87.078 14.392 67.58.111 45.321.111 20.44.111.055 17.987.055 40.687c0 13.104 6.781 23.863 18.115 32.209l-4.527 14.352 15.82-8.364c5.666 1.182 10.207 2.395 15.858 2.395 1.42 0 2.829-.073 4.227-.189-.886-3.19-1.398-6.53-1.398-9.996 0-20.845 16.98-37.76 38.485-37.76zm-24.34-12.936c3.407 0 5.665 2.363 5.665 5.954 0 3.576-2.258 5.97-5.666 5.97-3.392 0-6.795-2.395-6.795-5.97 0-3.591 3.403-5.954 6.795-5.954zM30.616 32.323c-3.393 0-6.818-2.395-6.818-5.971 0-3.591 3.425-5.954 6.818-5.954 3.392 0 5.65 2.363 5.65 5.954 0 3.576-2.258 5.97-5.65 5.97z"/><path d="M127.945 70.52c0-19.075-18.108-34.623-38.448-34.623-21.537 0-38.5 15.548-38.5 34.623 0 19.108 16.963 34.622 38.5 34.622 4.508 0 9.058-1.2 13.584-2.395l12.414 7.167-3.404-11.923c9.087-7.184 15.854-16.712 15.854-27.471zm-50.928-5.97c-2.254 0-4.53-2.362-4.53-4.773 0-2.378 2.276-4.771 4.53-4.771 3.422 0 5.665 2.393 5.665 4.771 0 2.41-2.243 4.773-5.665 4.773zm24.897 0c-2.24 0-4.498-2.362-4.498-4.773 0-2.378 2.258-4.771 4.498-4.771 3.392 0 5.665 2.393 5.665 4.771 0 2.41-2.273 4.773-5.665 4.773z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svg/zip.svg b/frontend/src/icons/svg/zip.svg
new file mode 100644
index 0000000..f806fc4
--- /dev/null
+++ b/frontend/src/icons/svg/zip.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M78.527 116.793c.178.008.348.024.527.024h40.233c4.711-.005 8.53-3.677 8.534-8.21V18.895c-.004-4.532-3.823-8.204-8.534-8.209H79.054c-.179 0-.353.016-.527.024V0L0 10.082v107.406l78.527 10.342v-11.037zm0-101.362c.174-.024.348-.052.527-.052h40.233c2.018 0 3.659 1.578 3.659 3.52v89.713c-.003 1.942-1.64 3.517-3.659 3.519H79.054c-.179 0-.353-.028-.527-.052V15.431zM30.262 75.757l-18.721-.46V72.37l11.3-16.673v-.148l-10.266.164v-4.51l17.504-.44v3.264L18.696 70.76v.144l11.566.176v4.678zm9.419.231l-5.823-.144V50.671l5.823-.144v25.461zm22.255-11.632c-2.168 1.922-5.353 2.76-9.02 2.736-.702.004-1.402-.04-2.097-.131v9.303l-5.997-.148V50.743c1.852-.352 4.473-.647 8.218-.743 3.838-.096 6.608.539 8.48 1.913 1.807 1.306 3.032 3.5 3.032 6.112s-.926 4.833-2.612 6.331h-.004zM53.36 54.45c-.856-.01-1.71.083-2.541.275v7.682c.523.116 1.167.152 2.06.152 3.301-.004 5.36-1.614 5.36-4.314 0-2.425-1.772-3.843-4.875-3.791l-.004-.004zm39.847-37.066h9.564v3.795h-9.564v-3.795zm-9.568 5.68h9.564v3.8h-9.564v-3.8zm9.568 6.216h9.564v3.799h-9.564V29.28zm0 12h9.564v3.794h-9.564V41.28zm-9.568-6.096h9.564v3.795h-9.564v-3.795zm9.472 47.064c2.512 0 4.921-.96 6.697-2.67 1.776-1.708 2.773-4.026 2.772-6.442l-1.748-15.263c0-5.033-2.492-9.112-7.725-9.112-5.232 0-7.72 4.079-7.72 9.112l-1.752 15.263c-.001 2.417.996 4.735 2.773 6.444 1.777 1.71 4.187 2.669 6.7 2.668h.003zm-3.135-16.75h6.27v12.743h-6.27V65.5z"/></svg>
\ No newline at end of file
diff --git a/frontend/src/icons/svgo.yml b/frontend/src/icons/svgo.yml
new file mode 100644
index 0000000..d11906a
--- /dev/null
+++ b/frontend/src/icons/svgo.yml
@@ -0,0 +1,22 @@
+# replace default config
+
+# multipass: true
+# full: true
+
+plugins:
+
+  # - name
+  #
+  # or:
+  # - name: false
+  # - name: true
+  #
+  # or:
+  # - name:
+  #     param1: 1
+  #     param2: 2
+
+- removeAttrs:
+    attrs:
+      - 'fill'
+      - 'fill-rule'
diff --git a/frontend/src/layout/components/AppMain.vue b/frontend/src/layout/components/AppMain.vue
new file mode 100644
index 0000000..a897638
--- /dev/null
+++ b/frontend/src/layout/components/AppMain.vue
@@ -0,0 +1,57 @@
+<template>
+  <section class="app-main">
+    <transition name="fade-transform" mode="out-in">
+      <keep-alive :include="cachedViews">
+        <router-view :key="key" />
+      </keep-alive>
+    </transition>
+  </section>
+</template>
+
+<script>
+export default {
+  name: 'AppMain',
+  computed: {
+    cachedViews() {
+      return this.$store.state.tagsView.cachedViews
+    },
+    key() {
+      return this.$route.path
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-main {
+  /* 50= navbar  50  */
+  min-height: calc(100vh - 50px);
+  width: 100%;
+  position: relative;
+  overflow: hidden;
+}
+
+.fixed-header+.app-main {
+  padding-top: 50px;
+}
+
+.hasTagsView {
+  .app-main {
+    /* 84 = navbar + tags-view = 50 + 34 */
+    min-height: calc(100vh - 84px);
+  }
+
+  .fixed-header+.app-main {
+    padding-top: 84px;
+  }
+}
+</style>
+
+<style lang="scss">
+// fix css style bug in open el-dialog
+.el-popup-parent--hidden {
+  .fixed-header {
+    padding-right: 15px;
+  }
+}
+</style>
diff --git a/frontend/src/layout/components/Navbar.vue b/frontend/src/layout/components/Navbar.vue
new file mode 100644
index 0000000..37bc1e6
--- /dev/null
+++ b/frontend/src/layout/components/Navbar.vue
@@ -0,0 +1,167 @@
+<template>
+  <div class="navbar">
+    <hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
+
+    <breadcrumb id="breadcrumb-container" class="breadcrumb-container" />
+
+    <div class="right-menu">
+      <template v-if="device!=='mobile'">
+        <search id="header-search" class="right-menu-item" />
+
+        <error-log class="errLog-container right-menu-item hover-effect" />
+
+        <screenfull id="screenfull" class="right-menu-item hover-effect" />
+
+        <el-tooltip content="Global Size" effect="dark" placement="bottom">
+          <size-select id="size-select" class="right-menu-item hover-effect" />
+        </el-tooltip>
+
+      </template>
+
+      <el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
+        <div class="avatar-wrapper">
+          <img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar">
+          <i class="el-icon-caret-bottom" />
+        </div>
+        <el-dropdown-menu slot="dropdown">
+          <router-link to="/profile/index">
+            <el-dropdown-item>Profile</el-dropdown-item>
+          </router-link>
+          <router-link to="/">
+            <el-dropdown-item>Dashboard</el-dropdown-item>
+          </router-link>
+          <a target="_blank" href="https://github.com/PanJiaChen/vue-element-admin/">
+            <el-dropdown-item>Github</el-dropdown-item>
+          </a>
+          <a target="_blank" href="https://panjiachen.github.io/vue-element-admin-site/#/">
+            <el-dropdown-item>Docs</el-dropdown-item>
+          </a>
+          <el-dropdown-item divided @click.native="logout">
+            <span style="display:block;">Log Out</span>
+          </el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import Breadcrumb from '@/components/Breadcrumb'
+import Hamburger from '@/components/Hamburger'
+import ErrorLog from '@/components/ErrorLog'
+import Screenfull from '@/components/Screenfull'
+import SizeSelect from '@/components/SizeSelect'
+import Search from '@/components/HeaderSearch'
+
+export default {
+  components: {
+    Breadcrumb,
+    Hamburger,
+    ErrorLog,
+    Screenfull,
+    SizeSelect,
+    Search
+  },
+  computed: {
+    ...mapGetters([
+      'sidebar',
+      'avatar',
+      'device'
+    ])
+  },
+  methods: {
+    toggleSideBar() {
+      this.$store.dispatch('app/toggleSideBar')
+    },
+    async logout() {
+      await this.$store.dispatch('user/logout')
+      this.$router.push(`/login?redirect=${this.$route.fullPath}`)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.navbar {
+  height: 50px;
+  overflow: hidden;
+  position: relative;
+  background: #fff;
+  box-shadow: 0 1px 4px rgba(0,21,41,.08);
+
+  .hamburger-container {
+    line-height: 46px;
+    height: 100%;
+    float: left;
+    cursor: pointer;
+    transition: background .3s;
+    -webkit-tap-highlight-color:transparent;
+
+    &:hover {
+      background: rgba(0, 0, 0, .025)
+    }
+  }
+
+  .breadcrumb-container {
+    float: left;
+  }
+
+  .errLog-container {
+    display: inline-block;
+    vertical-align: top;
+  }
+
+  .right-menu {
+    float: right;
+    height: 100%;
+    line-height: 50px;
+
+    &:focus {
+      outline: none;
+    }
+
+    .right-menu-item {
+      display: inline-block;
+      padding: 0 8px;
+      height: 100%;
+      font-size: 18px;
+      color: #5a5e66;
+      vertical-align: text-bottom;
+
+      &.hover-effect {
+        cursor: pointer;
+        transition: background .3s;
+
+        &:hover {
+          background: rgba(0, 0, 0, .025)
+        }
+      }
+    }
+
+    .avatar-container {
+      margin-right: 30px;
+
+      .avatar-wrapper {
+        margin-top: 5px;
+        position: relative;
+
+        .user-avatar {
+          cursor: pointer;
+          width: 40px;
+          height: 40px;
+          border-radius: 10px;
+        }
+
+        .el-icon-caret-bottom {
+          cursor: pointer;
+          position: absolute;
+          right: -20px;
+          top: 25px;
+          font-size: 12px;
+        }
+      }
+    }
+  }
+}
+</style>
diff --git a/frontend/src/layout/components/Settings/index.vue b/frontend/src/layout/components/Settings/index.vue
new file mode 100644
index 0000000..32ef018
--- /dev/null
+++ b/frontend/src/layout/components/Settings/index.vue
@@ -0,0 +1,108 @@
+<template>
+  <div class="drawer-container">
+    <div>
+      <h3 class="drawer-title">Page style setting</h3>
+
+      <div class="drawer-item">
+        <span>Theme Color</span>
+        <theme-picker style="float: right;height: 26px;margin: -3px 8px 0 0;" @change="themeChange" />
+      </div>
+
+      <div class="drawer-item">
+        <span>Open Tags-View</span>
+        <el-switch v-model="tagsView" class="drawer-switch" />
+      </div>
+
+      <div class="drawer-item">
+        <span>Fixed Header</span>
+        <el-switch v-model="fixedHeader" class="drawer-switch" />
+      </div>
+
+      <div class="drawer-item">
+        <span>Sidebar Logo</span>
+        <el-switch v-model="sidebarLogo" class="drawer-switch" />
+      </div>
+
+    </div>
+  </div>
+</template>
+
+<script>
+import ThemePicker from '@/components/ThemePicker'
+
+export default {
+  components: { ThemePicker },
+  data() {
+    return {}
+  },
+  computed: {
+    fixedHeader: {
+      get() {
+        return this.$store.state.settings.fixedHeader
+      },
+      set(val) {
+        this.$store.dispatch('settings/changeSetting', {
+          key: 'fixedHeader',
+          value: val
+        })
+      }
+    },
+    tagsView: {
+      get() {
+        return this.$store.state.settings.tagsView
+      },
+      set(val) {
+        this.$store.dispatch('settings/changeSetting', {
+          key: 'tagsView',
+          value: val
+        })
+      }
+    },
+    sidebarLogo: {
+      get() {
+        return this.$store.state.settings.sidebarLogo
+      },
+      set(val) {
+        this.$store.dispatch('settings/changeSetting', {
+          key: 'sidebarLogo',
+          value: val
+        })
+      }
+    }
+  },
+  methods: {
+    themeChange(val) {
+      this.$store.dispatch('settings/changeSetting', {
+        key: 'theme',
+        value: val
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.drawer-container {
+  padding: 24px;
+  font-size: 14px;
+  line-height: 1.5;
+  word-wrap: break-word;
+
+  .drawer-title {
+    margin-bottom: 12px;
+    color: rgba(0, 0, 0, .85);
+    font-size: 14px;
+    line-height: 22px;
+  }
+
+  .drawer-item {
+    color: rgba(0, 0, 0, .65);
+    font-size: 14px;
+    padding: 12px 0;
+  }
+
+  .drawer-switch {
+    float: right
+  }
+}
+</style>
diff --git a/frontend/src/layout/components/Sidebar/FixiOSBug.js b/frontend/src/layout/components/Sidebar/FixiOSBug.js
new file mode 100644
index 0000000..bc14856
--- /dev/null
+++ b/frontend/src/layout/components/Sidebar/FixiOSBug.js
@@ -0,0 +1,26 @@
+export default {
+  computed: {
+    device() {
+      return this.$store.state.app.device
+    }
+  },
+  mounted() {
+    // In order to fix the click on menu on the ios device will trigger the mouseleave bug
+    // https://github.com/PanJiaChen/vue-element-admin/issues/1135
+    this.fixBugIniOS()
+  },
+  methods: {
+    fixBugIniOS() {
+      const $subMenu = this.$refs.subMenu
+      if ($subMenu) {
+        const handleMouseleave = $subMenu.handleMouseleave
+        $subMenu.handleMouseleave = (e) => {
+          if (this.device === 'mobile') {
+            return
+          }
+          handleMouseleave(e)
+        }
+      }
+    }
+  }
+}
diff --git a/frontend/src/layout/components/Sidebar/Item.vue b/frontend/src/layout/components/Sidebar/Item.vue
new file mode 100644
index 0000000..aa1f5da
--- /dev/null
+++ b/frontend/src/layout/components/Sidebar/Item.vue
@@ -0,0 +1,41 @@
+<script>
+export default {
+  name: 'MenuItem',
+  functional: true,
+  props: {
+    icon: {
+      type: String,
+      default: ''
+    },
+    title: {
+      type: String,
+      default: ''
+    }
+  },
+  render(h, context) {
+    const { icon, title } = context.props
+    const vnodes = []
+
+    if (icon) {
+      if (icon.includes('el-icon')) {
+        vnodes.push(<i class={[icon, 'sub-el-icon']} />)
+      } else {
+        vnodes.push(<svg-icon icon-class={icon}/>)
+      }
+    }
+
+    if (title) {
+      vnodes.push(<span slot='title'>{(title)}</span>)
+    }
+    return vnodes
+  }
+}
+</script>
+
+<style scoped>
+.sub-el-icon {
+  color: currentColor;
+  width: 1em;
+  height: 1em;
+}
+</style>
diff --git a/frontend/src/layout/components/Sidebar/Link.vue b/frontend/src/layout/components/Sidebar/Link.vue
new file mode 100644
index 0000000..530b3d5
--- /dev/null
+++ b/frontend/src/layout/components/Sidebar/Link.vue
@@ -0,0 +1,43 @@
+<template>
+  <component :is="type" v-bind="linkProps(to)">
+    <slot />
+  </component>
+</template>
+
+<script>
+import { isExternal } from '@/utils/validate'
+
+export default {
+  props: {
+    to: {
+      type: String,
+      required: true
+    }
+  },
+  computed: {
+    isExternal() {
+      return isExternal(this.to)
+    },
+    type() {
+      if (this.isExternal) {
+        return 'a'
+      }
+      return 'router-link'
+    }
+  },
+  methods: {
+    linkProps(to) {
+      if (this.isExternal) {
+        return {
+          href: to,
+          target: '_blank',
+          rel: 'noopener'
+        }
+      }
+      return {
+        to: to
+      }
+    }
+  }
+}
+</script>
diff --git a/frontend/src/layout/components/Sidebar/Logo.vue b/frontend/src/layout/components/Sidebar/Logo.vue
new file mode 100644
index 0000000..ac0c8d8
--- /dev/null
+++ b/frontend/src/layout/components/Sidebar/Logo.vue
@@ -0,0 +1,82 @@
+<template>
+  <div class="sidebar-logo-container" :class="{'collapse':collapse}">
+    <transition name="sidebarLogoFade">
+      <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
+        <img v-if="logo" :src="logo" class="sidebar-logo">
+        <h1 v-else class="sidebar-title">{{ title }} </h1>
+      </router-link>
+      <router-link v-else key="expand" class="sidebar-logo-link" to="/">
+        <img v-if="logo" :src="logo" class="sidebar-logo">
+        <h1 class="sidebar-title">{{ title }} </h1>
+      </router-link>
+    </transition>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'SidebarLogo',
+  props: {
+    collapse: {
+      type: Boolean,
+      required: true
+    }
+  },
+  data() {
+    return {
+      title: 'Vue Element Admin',
+      logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png'
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.sidebarLogoFade-enter-active {
+  transition: opacity 1.5s;
+}
+
+.sidebarLogoFade-enter,
+.sidebarLogoFade-leave-to {
+  opacity: 0;
+}
+
+.sidebar-logo-container {
+  position: relative;
+  width: 100%;
+  height: 50px;
+  line-height: 50px;
+  background: #2b2f3a;
+  text-align: center;
+  overflow: hidden;
+
+  & .sidebar-logo-link {
+    height: 100%;
+    width: 100%;
+
+    & .sidebar-logo {
+      width: 32px;
+      height: 32px;
+      vertical-align: middle;
+      margin-right: 12px;
+    }
+
+    & .sidebar-title {
+      display: inline-block;
+      margin: 0;
+      color: #fff;
+      font-weight: 600;
+      line-height: 50px;
+      font-size: 14px;
+      font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
+      vertical-align: middle;
+    }
+  }
+
+  &.collapse {
+    .sidebar-logo {
+      margin-right: 0px;
+    }
+  }
+}
+</style>
diff --git a/frontend/src/layout/components/Sidebar/SidebarItem.vue b/frontend/src/layout/components/Sidebar/SidebarItem.vue
new file mode 100644
index 0000000..a418c3d
--- /dev/null
+++ b/frontend/src/layout/components/Sidebar/SidebarItem.vue
@@ -0,0 +1,95 @@
+<template>
+  <div v-if="!item.hidden">
+    <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
+      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
+        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
+          <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
+        </el-menu-item>
+      </app-link>
+    </template>
+
+    <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
+      <template slot="title">
+        <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
+      </template>
+      <sidebar-item
+        v-for="child in item.children"
+        :key="child.path"
+        :is-nest="true"
+        :item="child"
+        :base-path="resolvePath(child.path)"
+        class="nest-menu"
+      />
+    </el-submenu>
+  </div>
+</template>
+
+<script>
+import path from 'path'
+import { isExternal } from '@/utils/validate'
+import Item from './Item'
+import AppLink from './Link'
+import FixiOSBug from './FixiOSBug'
+
+export default {
+  name: 'SidebarItem',
+  components: { Item, AppLink },
+  mixins: [FixiOSBug],
+  props: {
+    // route object
+    item: {
+      type: Object,
+      required: true
+    },
+    isNest: {
+      type: Boolean,
+      default: false
+    },
+    basePath: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
+    // TODO: refactor with render function
+    this.onlyOneChild = null
+    return {}
+  },
+  methods: {
+    hasOneShowingChild(children = [], parent) {
+      const showingChildren = children.filter(item => {
+        if (item.hidden) {
+          return false
+        } else {
+          // Temp set(will be used if only has one showing child)
+          this.onlyOneChild = item
+          return true
+        }
+      })
+
+      // When there is only one child router, the child router is displayed by default
+      if (showingChildren.length === 1) {
+        return true
+      }
+
+      // Show parent if there are no child router to display
+      if (showingChildren.length === 0) {
+        this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
+        return true
+      }
+
+      return false
+    },
+    resolvePath(routePath) {
+      if (isExternal(routePath)) {
+        return routePath
+      }
+      if (isExternal(this.basePath)) {
+        return this.basePath
+      }
+      return path.resolve(this.basePath, routePath)
+    }
+  }
+}
+</script>
diff --git a/frontend/src/layout/components/Sidebar/index.vue b/frontend/src/layout/components/Sidebar/index.vue
new file mode 100644
index 0000000..fb014a2
--- /dev/null
+++ b/frontend/src/layout/components/Sidebar/index.vue
@@ -0,0 +1,54 @@
+<template>
+  <div :class="{'has-logo':showLogo}">
+    <logo v-if="showLogo" :collapse="isCollapse" />
+    <el-scrollbar wrap-class="scrollbar-wrapper">
+      <el-menu
+        :default-active="activeMenu"
+        :collapse="isCollapse"
+        :background-color="variables.menuBg"
+        :text-color="variables.menuText"
+        :unique-opened="false"
+        :active-text-color="variables.menuActiveText"
+        :collapse-transition="false"
+        mode="vertical"
+      >
+        <sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
+      </el-menu>
+    </el-scrollbar>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import Logo from './Logo'
+import SidebarItem from './SidebarItem'
+import variables from '@/styles/variables.scss'
+
+export default {
+  components: { SidebarItem, Logo },
+  computed: {
+    ...mapGetters([
+      'permission_routes',
+      'sidebar'
+    ]),
+    activeMenu() {
+      const route = this.$route
+      const { meta, path } = route
+      // if set path, the sidebar will highlight the path you set
+      if (meta.activeMenu) {
+        return meta.activeMenu
+      }
+      return path
+    },
+    showLogo() {
+      return this.$store.state.settings.sidebarLogo
+    },
+    variables() {
+      return variables
+    },
+    isCollapse() {
+      return !this.sidebar.opened
+    }
+  }
+}
+</script>
diff --git a/frontend/src/layout/components/TagsView/ScrollPane.vue b/frontend/src/layout/components/TagsView/ScrollPane.vue
new file mode 100644
index 0000000..bb753a1
--- /dev/null
+++ b/frontend/src/layout/components/TagsView/ScrollPane.vue
@@ -0,0 +1,94 @@
+<template>
+  <el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
+    <slot />
+  </el-scrollbar>
+</template>
+
+<script>
+const tagAndTagSpacing = 4 // tagAndTagSpacing
+
+export default {
+  name: 'ScrollPane',
+  data() {
+    return {
+      left: 0
+    }
+  },
+  computed: {
+    scrollWrapper() {
+      return this.$refs.scrollContainer.$refs.wrap
+    }
+  },
+  mounted() {
+    this.scrollWrapper.addEventListener('scroll', this.emitScroll, true)
+  },
+  beforeDestroy() {
+    this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
+  },
+  methods: {
+    handleScroll(e) {
+      const eventDelta = e.wheelDelta || -e.deltaY * 40
+      const $scrollWrapper = this.scrollWrapper
+      $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
+    },
+    emitScroll() {
+      this.$emit('scroll')
+    },
+    moveToTarget(currentTag) {
+      const $container = this.$refs.scrollContainer.$el
+      const $containerWidth = $container.offsetWidth
+      const $scrollWrapper = this.scrollWrapper
+      const tagList = this.$parent.$refs.tag
+
+      let firstTag = null
+      let lastTag = null
+
+      // find first tag and last tag
+      if (tagList.length > 0) {
+        firstTag = tagList[0]
+        lastTag = tagList[tagList.length - 1]
+      }
+
+      if (firstTag === currentTag) {
+        $scrollWrapper.scrollLeft = 0
+      } else if (lastTag === currentTag) {
+        $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
+      } else {
+        // find preTag and nextTag
+        const currentIndex = tagList.findIndex(item => item === currentTag)
+        const prevTag = tagList[currentIndex - 1]
+        const nextTag = tagList[currentIndex + 1]
+
+        // the tag's offsetLeft after of nextTag
+        const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
+
+        // the tag's offsetLeft before of prevTag
+        const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
+
+        if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
+          $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
+        } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
+          $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.scroll-container {
+  white-space: nowrap;
+  position: relative;
+  overflow: hidden;
+  width: 100%;
+  ::v-deep {
+    .el-scrollbar__bar {
+      bottom: 0px;
+    }
+    .el-scrollbar__wrap {
+      height: 49px;
+    }
+  }
+}
+</style>
diff --git a/frontend/src/layout/components/TagsView/index.vue b/frontend/src/layout/components/TagsView/index.vue
new file mode 100644
index 0000000..d2a56e7
--- /dev/null
+++ b/frontend/src/layout/components/TagsView/index.vue
@@ -0,0 +1,292 @@
+<template>
+  <div id="tags-view-container" class="tags-view-container">
+    <scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
+      <router-link
+        v-for="tag in visitedViews"
+        ref="tag"
+        :key="tag.path"
+        :class="isActive(tag)?'active':''"
+        :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
+        tag="span"
+        class="tags-view-item"
+        @click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
+        @contextmenu.prevent.native="openMenu(tag,$event)"
+      >
+        {{ tag.title }}
+        <span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
+      </router-link>
+    </scroll-pane>
+    <ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
+      <li @click="refreshSelectedTag(selectedTag)">Refresh</li>
+      <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">Close</li>
+      <li @click="closeOthersTags">Close Others</li>
+      <li @click="closeAllTags(selectedTag)">Close All</li>
+    </ul>
+  </div>
+</template>
+
+<script>
+import ScrollPane from './ScrollPane'
+import path from 'path'
+
+export default {
+  components: { ScrollPane },
+  data() {
+    return {
+      visible: false,
+      top: 0,
+      left: 0,
+      selectedTag: {},
+      affixTags: []
+    }
+  },
+  computed: {
+    visitedViews() {
+      return this.$store.state.tagsView.visitedViews
+    },
+    routes() {
+      return this.$store.state.permission.routes
+    }
+  },
+  watch: {
+    $route() {
+      this.addTags()
+      this.moveToCurrentTag()
+    },
+    visible(value) {
+      if (value) {
+        document.body.addEventListener('click', this.closeMenu)
+      } else {
+        document.body.removeEventListener('click', this.closeMenu)
+      }
+    }
+  },
+  mounted() {
+    this.initTags()
+    this.addTags()
+  },
+  methods: {
+    isActive(route) {
+      return route.path === this.$route.path
+    },
+    isAffix(tag) {
+      return tag.meta && tag.meta.affix
+    },
+    filterAffixTags(routes, basePath = '/') {
+      let tags = []
+      routes.forEach(route => {
+        if (route.meta && route.meta.affix) {
+          const tagPath = path.resolve(basePath, route.path)
+          tags.push({
+            fullPath: tagPath,
+            path: tagPath,
+            name: route.name,
+            meta: { ...route.meta }
+          })
+        }
+        if (route.children) {
+          const tempTags = this.filterAffixTags(route.children, route.path)
+          if (tempTags.length >= 1) {
+            tags = [...tags, ...tempTags]
+          }
+        }
+      })
+      return tags
+    },
+    initTags() {
+      const affixTags = this.affixTags = this.filterAffixTags(this.routes)
+      for (const tag of affixTags) {
+        // Must have tag name
+        if (tag.name) {
+          this.$store.dispatch('tagsView/addVisitedView', tag)
+        }
+      }
+    },
+    addTags() {
+      const { name } = this.$route
+      if (name) {
+        this.$store.dispatch('tagsView/addView', this.$route)
+      }
+      return false
+    },
+    moveToCurrentTag() {
+      const tags = this.$refs.tag
+      this.$nextTick(() => {
+        for (const tag of tags) {
+          if (tag.to.path === this.$route.path) {
+            this.$refs.scrollPane.moveToTarget(tag)
+            // when query is different then update
+            if (tag.to.fullPath !== this.$route.fullPath) {
+              this.$store.dispatch('tagsView/updateVisitedView', this.$route)
+            }
+            break
+          }
+        }
+      })
+    },
+    refreshSelectedTag(view) {
+      this.$store.dispatch('tagsView/delCachedView', view).then(() => {
+        const { fullPath } = view
+        this.$nextTick(() => {
+          this.$router.replace({
+            path: '/redirect' + fullPath
+          })
+        })
+      })
+    },
+    closeSelectedTag(view) {
+      this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
+        if (this.isActive(view)) {
+          this.toLastView(visitedViews, view)
+        }
+      })
+    },
+    closeOthersTags() {
+      this.$router.push(this.selectedTag)
+      this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
+        this.moveToCurrentTag()
+      })
+    },
+    closeAllTags(view) {
+      this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
+        if (this.affixTags.some(tag => tag.path === view.path)) {
+          return
+        }
+        this.toLastView(visitedViews, view)
+      })
+    },
+    toLastView(visitedViews, view) {
+      const latestView = visitedViews.slice(-1)[0]
+      if (latestView) {
+        this.$router.push(latestView.fullPath)
+      } else {
+        // now the default is to redirect to the home page if there is no tags-view,
+        // you can adjust it according to your needs.
+        if (view.name === 'Dashboard') {
+          // to reload home page
+          this.$router.replace({ path: '/redirect' + view.fullPath })
+        } else {
+          this.$router.push('/')
+        }
+      }
+    },
+    openMenu(tag, e) {
+      const menuMinWidth = 105
+      const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
+      const offsetWidth = this.$el.offsetWidth // container width
+      const maxLeft = offsetWidth - menuMinWidth // left boundary
+      const left = e.clientX - offsetLeft + 15 // 15: margin right
+
+      if (left > maxLeft) {
+        this.left = maxLeft
+      } else {
+        this.left = left
+      }
+
+      this.top = e.clientY
+      this.visible = true
+      this.selectedTag = tag
+    },
+    closeMenu() {
+      this.visible = false
+    },
+    handleScroll() {
+      this.closeMenu()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.tags-view-container {
+  height: 34px;
+  width: 100%;
+  background: #fff;
+  border-bottom: 1px solid #d8dce5;
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
+  .tags-view-wrapper {
+    .tags-view-item {
+      display: inline-block;
+      position: relative;
+      cursor: pointer;
+      height: 26px;
+      line-height: 26px;
+      border: 1px solid #d8dce5;
+      color: #495060;
+      background: #fff;
+      padding: 0 8px;
+      font-size: 12px;
+      margin-left: 5px;
+      margin-top: 4px;
+      &:first-of-type {
+        margin-left: 15px;
+      }
+      &:last-of-type {
+        margin-right: 15px;
+      }
+      &.active {
+        background-color: #42b983;
+        color: #fff;
+        border-color: #42b983;
+        &::before {
+          content: '';
+          background: #fff;
+          display: inline-block;
+          width: 8px;
+          height: 8px;
+          border-radius: 50%;
+          position: relative;
+          margin-right: 2px;
+        }
+      }
+    }
+  }
+  .contextmenu {
+    margin: 0;
+    background: #fff;
+    z-index: 3000;
+    position: absolute;
+    list-style-type: none;
+    padding: 5px 0;
+    border-radius: 4px;
+    font-size: 12px;
+    font-weight: 400;
+    color: #333;
+    box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
+    li {
+      margin: 0;
+      padding: 7px 16px;
+      cursor: pointer;
+      &:hover {
+        background: #eee;
+      }
+    }
+  }
+}
+</style>
+
+<style lang="scss">
+//reset element css of el-icon-close
+.tags-view-wrapper {
+  .tags-view-item {
+    .el-icon-close {
+      width: 16px;
+      height: 16px;
+      vertical-align: 2px;
+      border-radius: 50%;
+      text-align: center;
+      transition: all .3s cubic-bezier(.645, .045, .355, 1);
+      transform-origin: 100% 50%;
+      &:before {
+        transform: scale(.6);
+        display: inline-block;
+        vertical-align: -3px;
+      }
+      &:hover {
+        background-color: #b4bccc;
+        color: #fff;
+      }
+    }
+  }
+}
+</style>
diff --git a/frontend/src/layout/components/index.js b/frontend/src/layout/components/index.js
new file mode 100644
index 0000000..104bd3a
--- /dev/null
+++ b/frontend/src/layout/components/index.js
@@ -0,0 +1,5 @@
+export { default as AppMain } from './AppMain'
+export { default as Navbar } from './Navbar'
+export { default as Settings } from './Settings'
+export { default as Sidebar } from './Sidebar/index.vue'
+export { default as TagsView } from './TagsView/index.vue'
diff --git a/frontend/src/layout/index.vue b/frontend/src/layout/index.vue
new file mode 100644
index 0000000..965bcd1
--- /dev/null
+++ b/frontend/src/layout/index.vue
@@ -0,0 +1,102 @@
+<template>
+  <div :class="classObj" class="app-wrapper">
+    <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
+    <sidebar class="sidebar-container" />
+    <div :class="{hasTagsView:needTagsView}" class="main-container">
+      <div :class="{'fixed-header':fixedHeader}">
+        <navbar />
+        <tags-view v-if="needTagsView" />
+      </div>
+      <app-main />
+      <right-panel v-if="showSettings">
+        <settings />
+      </right-panel>
+    </div>
+  </div>
+</template>
+
+<script>
+import RightPanel from '@/components/RightPanel'
+import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
+import ResizeMixin from './mixin/ResizeHandler'
+import { mapState } from 'vuex'
+
+export default {
+  name: 'Layout',
+  components: {
+    AppMain,
+    Navbar,
+    RightPanel,
+    Settings,
+    Sidebar,
+    TagsView
+  },
+  mixins: [ResizeMixin],
+  computed: {
+    ...mapState({
+      sidebar: state => state.app.sidebar,
+      device: state => state.app.device,
+      showSettings: state => state.settings.showSettings,
+      needTagsView: state => state.settings.tagsView,
+      fixedHeader: state => state.settings.fixedHeader
+    }),
+    classObj() {
+      return {
+        hideSidebar: !this.sidebar.opened,
+        openSidebar: this.sidebar.opened,
+        withoutAnimation: this.sidebar.withoutAnimation,
+        mobile: this.device === 'mobile'
+      }
+    }
+  },
+  methods: {
+    handleClickOutside() {
+      this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  @import "~@/styles/mixin.scss";
+  @import "~@/styles/variables.scss";
+
+  .app-wrapper {
+    @include clearfix;
+    position: relative;
+    height: 100%;
+    width: 100%;
+
+    &.mobile.openSidebar {
+      position: fixed;
+      top: 0;
+    }
+  }
+
+  .drawer-bg {
+    background: #000;
+    opacity: 0.3;
+    width: 100%;
+    top: 0;
+    height: 100%;
+    position: absolute;
+    z-index: 999;
+  }
+
+  .fixed-header {
+    position: fixed;
+    top: 0;
+    right: 0;
+    z-index: 9;
+    width: calc(100% - #{$sideBarWidth});
+    transition: width 0.28s;
+  }
+
+  .hideSidebar .fixed-header {
+    width: calc(100% - 54px)
+  }
+
+  .mobile .fixed-header {
+    width: 100%;
+  }
+</style>
diff --git a/frontend/src/layout/mixin/ResizeHandler.js b/frontend/src/layout/mixin/ResizeHandler.js
new file mode 100644
index 0000000..e8d0df8
--- /dev/null
+++ b/frontend/src/layout/mixin/ResizeHandler.js
@@ -0,0 +1,45 @@
+import store from '@/store'
+
+const { body } = document
+const WIDTH = 992 // refer to Bootstrap's responsive design
+
+export default {
+  watch: {
+    $route(route) {
+      if (this.device === 'mobile' && this.sidebar.opened) {
+        store.dispatch('app/closeSideBar', { withoutAnimation: false })
+      }
+    }
+  },
+  beforeMount() {
+    window.addEventListener('resize', this.$_resizeHandler)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.$_resizeHandler)
+  },
+  mounted() {
+    const isMobile = this.$_isMobile()
+    if (isMobile) {
+      store.dispatch('app/toggleDevice', 'mobile')
+      store.dispatch('app/closeSideBar', { withoutAnimation: true })
+    }
+  },
+  methods: {
+    // use $_ for mixins properties
+    // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
+    $_isMobile() {
+      const rect = body.getBoundingClientRect()
+      return rect.width - 1 < WIDTH
+    },
+    $_resizeHandler() {
+      if (!document.hidden) {
+        const isMobile = this.$_isMobile()
+        store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
+
+        if (isMobile) {
+          store.dispatch('app/closeSideBar', { withoutAnimation: true })
+        }
+      }
+    }
+  }
+}
diff --git a/frontend/src/main.js b/frontend/src/main.js
new file mode 100644
index 0000000..b5fa135
--- /dev/null
+++ b/frontend/src/main.js
@@ -0,0 +1,53 @@
+import Vue from 'vue'
+
+import Cookies from 'js-cookie'
+
+import 'normalize.css/normalize.css' // a modern alternative to CSS resets
+
+import Element from 'element-ui'
+import './styles/element-variables.scss'
+import enLang from 'element-ui/lib/locale/lang/en'// 如果使用中文语言包请默认支持,无需额外引入,请删除该依赖
+
+import '@/styles/index.scss' // global css
+
+import App from './App'
+import store from './store'
+import router from './router'
+
+import './icons' // icon
+import './permission' // permission control
+import './utils/error-log' // error log
+
+import * as filters from './filters' // global filters
+
+/**
+ * If you don't want to use mock-server
+ * you want to use MockJs for mock api
+ * you can execute: mockXHR()
+ *
+ * Currently MockJs will be used in the production environment,
+ * please remove it before going online ! ! !
+ */
+if (process.env.NODE_ENV === 'production') {
+  const { mockXHR } = require('../mock')
+  mockXHR()
+}
+
+Vue.use(Element, {
+  size: Cookies.get('size') || 'medium', // set element-ui default size
+  locale: enLang // 如果使用中文,无需设置,请删除
+})
+
+// register global utility filters
+Object.keys(filters).forEach(key => {
+  Vue.filter(key, filters[key])
+})
+
+Vue.config.productionTip = false
+
+new Vue({
+  el: '#app',
+  router,
+  store,
+  render: h => h(App)
+})
diff --git a/frontend/src/permission.js b/frontend/src/permission.js
new file mode 100644
index 0000000..ff5eaad
--- /dev/null
+++ b/frontend/src/permission.js
@@ -0,0 +1,74 @@
+import router from './router'
+import store from './store'
+import { Message } from 'element-ui'
+import NProgress from 'nprogress' // progress bar
+import 'nprogress/nprogress.css' // progress bar style
+import { getToken } from '@/utils/auth' // get token from cookie
+import getPageTitle from '@/utils/get-page-title'
+
+NProgress.configure({ showSpinner: false }) // NProgress Configuration
+
+const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist
+
+router.beforeEach(async(to, from, next) => {
+  // start progress bar
+  NProgress.start()
+
+  // set page title
+  document.title = getPageTitle(to.meta.title)
+
+  // determine whether the user has logged in
+  const hasToken = getToken()
+
+  if (hasToken) {
+    if (to.path === '/login') {
+      // if is logged in, redirect to the home page
+      next({ path: '/' })
+      NProgress.done() // hack: https://github.com/PanJiaChen/vue-element-admin/pull/2939
+    } else {
+      // determine whether the user has obtained his permission roles through getInfo
+      const hasRoles = store.getters.roles && store.getters.roles.length > 0
+      if (hasRoles) {
+        next()
+      } else {
+        try {
+          // get user info
+          // note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
+          const { roles } = await store.dispatch('user/getInfo')
+
+          // generate accessible routes map based on roles
+          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
+
+          // dynamically add accessible routes
+          router.addRoutes(accessRoutes)
+
+          // hack method to ensure that addRoutes is complete
+          // set the replace: true, so the navigation will not leave a history record
+          next({ ...to, replace: true })
+        } catch (error) {
+          // remove token and go to login page to re-login
+          await store.dispatch('user/resetToken')
+          Message.error(error || 'Has Error')
+          next(`/login?redirect=${to.path}`)
+          NProgress.done()
+        }
+      }
+    }
+  } else {
+    /* has no token*/
+
+    if (whiteList.indexOf(to.path) !== -1) {
+      // in the free login whitelist, go directly
+      next()
+    } else {
+      // other pages that do not have permission to access are redirected to the login page.
+      next(`/login?redirect=${to.path}`)
+      NProgress.done()
+    }
+  }
+})
+
+router.afterEach(() => {
+  // finish progress bar
+  NProgress.done()
+})
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
new file mode 100644
index 0000000..2be959d
--- /dev/null
+++ b/frontend/src/router/index.js
@@ -0,0 +1,404 @@
+import Vue from 'vue'
+import Router from 'vue-router'
+
+Vue.use(Router)
+
+/* Layout */
+import Layout from '@/layout'
+
+/* Router Modules */
+import componentsRouter from './modules/components'
+import chartsRouter from './modules/charts'
+import tableRouter from './modules/table'
+import nestedRouter from './modules/nested'
+
+/**
+ * Note: sub-menu only appear when route children.length >= 1
+ * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
+ *
+ * hidden: true                   if set true, item will not show in the sidebar(default is false)
+ * alwaysShow: true               if set true, will always show the root menu
+ *                                if not set alwaysShow, when item has more than one children route,
+ *                                it will becomes nested mode, otherwise not show the root menu
+ * redirect: noRedirect           if set noRedirect will no redirect in the breadcrumb
+ * name:'router-name'             the name is used by <keep-alive> (must set!!!)
+ * meta : {
+    roles: ['admin','editor']    control the page roles (you can set multiple roles)
+    title: 'title'               the name show in sidebar and breadcrumb (recommend set)
+    icon: 'svg-name'/'el-icon-x' the icon show in the sidebar
+    noCache: true                if set true, the page will no be cached(default is false)
+    affix: true                  if set true, the tag will affix in the tags-view
+    breadcrumb: false            if set false, the item will hidden in breadcrumb(default is true)
+    activeMenu: '/example/list'  if set path, the sidebar will highlight the path you set
+  }
+ */
+
+/**
+ * constantRoutes
+ * a base page that does not have permission requirements
+ * all roles can be accessed
+ */
+export const constantRoutes = [
+  {
+    path: '/redirect',
+    component: Layout,
+    hidden: true,
+    children: [
+      {
+        path: '/redirect/:path(.*)',
+        component: () => import('@/views/redirect/index')
+      }
+    ]
+  },
+  {
+    path: '/login',
+    component: () => import('@/views/login/index'),
+    hidden: true
+  },
+  {
+    path: '/auth-redirect',
+    component: () => import('@/views/login/auth-redirect'),
+    hidden: true
+  },
+  {
+    path: '/404',
+    component: () => import('@/views/error-page/404'),
+    hidden: true
+  },
+  {
+    path: '/401',
+    component: () => import('@/views/error-page/401'),
+    hidden: true
+  },
+  {
+    path: '/',
+    component: Layout,
+    redirect: '/dashboard',
+    children: [
+      {
+        path: 'dashboard',
+        component: () => import('@/views/dashboard/index'),
+        name: 'Dashboard',
+        meta: { title: 'Dashboard', icon: 'dashboard', affix: true }
+      }
+    ]
+  },
+  {
+    path: '/documentation',
+    component: Layout,
+    children: [
+      {
+        path: 'index',
+        component: () => import('@/views/documentation/index'),
+        name: 'Documentation',
+        meta: { title: 'Documentation', icon: 'documentation', affix: true }
+      }
+    ]
+  },
+  {
+    path: '/guide',
+    component: Layout,
+    redirect: '/guide/index',
+    children: [
+      {
+        path: 'index',
+        component: () => import('@/views/guide/index'),
+        name: 'Guide',
+        meta: { title: 'Guide', icon: 'guide', noCache: true }
+      }
+    ]
+  },
+  {
+    path: '/profile',
+    component: Layout,
+    redirect: '/profile/index',
+    hidden: true,
+    children: [
+      {
+        path: 'index',
+        component: () => import('@/views/profile/index'),
+        name: 'Profile',
+        meta: { title: 'Profile', icon: 'user', noCache: true }
+      }
+    ]
+  }
+]
+
+/**
+ * asyncRoutes
+ * the routes that need to be dynamically loaded based on user roles
+ */
+export const asyncRoutes = [
+  {
+    path: '/permission',
+    component: Layout,
+    redirect: '/permission/page',
+    alwaysShow: true, // will always show the root menu
+    name: 'Permission',
+    meta: {
+      title: 'Permission',
+      icon: 'lock',
+      roles: ['admin', 'editor'] // you can set roles in root nav
+    },
+    children: [
+      {
+        path: 'page',
+        component: () => import('@/views/permission/page'),
+        name: 'PagePermission',
+        meta: {
+          title: 'Page Permission',
+          roles: ['admin'] // or you can only set roles in sub nav
+        }
+      },
+      {
+        path: 'directive',
+        component: () => import('@/views/permission/directive'),
+        name: 'DirectivePermission',
+        meta: {
+          title: 'Directive Permission'
+          // if do not set roles, means: this page does not require permission
+        }
+      },
+      {
+        path: 'role',
+        component: () => import('@/views/permission/role'),
+        name: 'RolePermission',
+        meta: {
+          title: 'Role Permission',
+          roles: ['admin']
+        }
+      }
+    ]
+  },
+
+  {
+    path: '/icon',
+    component: Layout,
+    children: [
+      {
+        path: 'index',
+        component: () => import('@/views/icons/index'),
+        name: 'Icons',
+        meta: { title: 'Icons', icon: 'icon', noCache: true }
+      }
+    ]
+  },
+
+  /** when your routing map is too long, you can split it into small modules **/
+  componentsRouter,
+  chartsRouter,
+  nestedRouter,
+  tableRouter,
+
+  {
+    path: '/example',
+    component: Layout,
+    redirect: '/example/list',
+    name: 'Example',
+    meta: {
+      title: 'Example',
+      icon: 'el-icon-s-help'
+    },
+    children: [
+      {
+        path: 'create',
+        component: () => import('@/views/example/create'),
+        name: 'CreateArticle',
+        meta: { title: 'Create Article', icon: 'edit' }
+      },
+      {
+        path: 'edit/:id(\\d+)',
+        component: () => import('@/views/example/edit'),
+        name: 'EditArticle',
+        meta: { title: 'Edit Article', noCache: true, activeMenu: '/example/list' },
+        hidden: true
+      },
+      {
+        path: 'list',
+        component: () => import('@/views/example/list'),
+        name: 'ArticleList',
+        meta: { title: 'Article List', icon: 'list' }
+      }
+    ]
+  },
+
+  {
+    path: '/tab',
+    component: Layout,
+    children: [
+      {
+        path: 'index',
+        component: () => import('@/views/tab/index'),
+        name: 'Tab',
+        meta: { title: 'Tab', icon: 'tab' }
+      }
+    ]
+  },
+
+  {
+    path: '/error',
+    component: Layout,
+    redirect: 'noRedirect',
+    name: 'ErrorPages',
+    meta: {
+      title: 'Error Pages',
+      icon: '404'
+    },
+    children: [
+      {
+        path: '401',
+        component: () => import('@/views/error-page/401'),
+        name: 'Page401',
+        meta: { title: '401', noCache: true }
+      },
+      {
+        path: '404',
+        component: () => import('@/views/error-page/404'),
+        name: 'Page404',
+        meta: { title: '404', noCache: true }
+      }
+    ]
+  },
+
+  {
+    path: '/error-log',
+    component: Layout,
+    children: [
+      {
+        path: 'log',
+        component: () => import('@/views/error-log/index'),
+        name: 'ErrorLog',
+        meta: { title: 'Error Log', icon: 'bug' }
+      }
+    ]
+  },
+
+  {
+    path: '/excel',
+    component: Layout,
+    redirect: '/excel/export-excel',
+    name: 'Excel',
+    meta: {
+      title: 'Excel',
+      icon: 'excel'
+    },
+    children: [
+      {
+        path: 'export-excel',
+        component: () => import('@/views/excel/export-excel'),
+        name: 'ExportExcel',
+        meta: { title: 'Export Excel' }
+      },
+      {
+        path: 'export-selected-excel',
+        component: () => import('@/views/excel/select-excel'),
+        name: 'SelectExcel',
+        meta: { title: 'Export Selected' }
+      },
+      {
+        path: 'export-merge-header',
+        component: () => import('@/views/excel/merge-header'),
+        name: 'MergeHeader',
+        meta: { title: 'Merge Header' }
+      },
+      {
+        path: 'upload-excel',
+        component: () => import('@/views/excel/upload-excel'),
+        name: 'UploadExcel',
+        meta: { title: 'Upload Excel' }
+      }
+    ]
+  },
+
+  {
+    path: '/zip',
+    component: Layout,
+    redirect: '/zip/download',
+    alwaysShow: true,
+    name: 'Zip',
+    meta: { title: 'Zip', icon: 'zip' },
+    children: [
+      {
+        path: 'download',
+        component: () => import('@/views/zip/index'),
+        name: 'ExportZip',
+        meta: { title: 'Export Zip' }
+      }
+    ]
+  },
+
+  {
+    path: '/pdf',
+    component: Layout,
+    redirect: '/pdf/index',
+    children: [
+      {
+        path: 'index',
+        component: () => import('@/views/pdf/index'),
+        name: 'PDF',
+        meta: { title: 'PDF', icon: 'pdf' }
+      }
+    ]
+  },
+  {
+    path: '/pdf/download',
+    component: () => import('@/views/pdf/download'),
+    hidden: true
+  },
+
+  {
+    path: '/theme',
+    component: Layout,
+    children: [
+      {
+        path: 'index',
+        component: () => import('@/views/theme/index'),
+        name: 'Theme',
+        meta: { title: 'Theme', icon: 'theme' }
+      }
+    ]
+  },
+
+  {
+    path: '/clipboard',
+    component: Layout,
+    children: [
+      {
+        path: 'index',
+        component: () => import('@/views/clipboard/index'),
+        name: 'ClipboardDemo',
+        meta: { title: 'Clipboard', icon: 'clipboard' }
+      }
+    ]
+  },
+
+  {
+    path: 'external-link',
+    component: Layout,
+    children: [
+      {
+        path: 'https://github.com/PanJiaChen/vue-element-admin',
+        meta: { title: 'External Link', icon: 'link' }
+      }
+    ]
+  },
+
+  // 404 page must be placed at the end !!!
+  { path: '*', redirect: '/404', hidden: true }
+]
+
+const createRouter = () => new Router({
+  // mode: 'history', // require service support
+  scrollBehavior: () => ({ y: 0 }),
+  routes: constantRoutes
+})
+
+const router = createRouter()
+
+// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
+export function resetRouter() {
+  const newRouter = createRouter()
+  router.matcher = newRouter.matcher // reset router
+}
+
+export default router
diff --git a/frontend/src/router/modules/charts.js b/frontend/src/router/modules/charts.js
new file mode 100644
index 0000000..29684de
--- /dev/null
+++ b/frontend/src/router/modules/charts.js
@@ -0,0 +1,36 @@
+/** When your routing table is too long, you can split it into small modules**/
+
+import Layout from '@/layout'
+
+const chartsRouter = {
+  path: '/charts',
+  component: Layout,
+  redirect: 'noRedirect',
+  name: 'Charts',
+  meta: {
+    title: 'Charts',
+    icon: 'chart'
+  },
+  children: [
+    {
+      path: 'keyboard',
+      component: () => import('@/views/charts/keyboard'),
+      name: 'KeyboardChart',
+      meta: { title: 'Keyboard Chart', noCache: true }
+    },
+    {
+      path: 'line',
+      component: () => import('@/views/charts/line'),
+      name: 'LineChart',
+      meta: { title: 'Line Chart', noCache: true }
+    },
+    {
+      path: 'mix-chart',
+      component: () => import('@/views/charts/mix-chart'),
+      name: 'MixChart',
+      meta: { title: 'Mix Chart', noCache: true }
+    }
+  ]
+}
+
+export default chartsRouter
diff --git a/frontend/src/router/modules/components.js b/frontend/src/router/modules/components.js
new file mode 100644
index 0000000..0da96f9
--- /dev/null
+++ b/frontend/src/router/modules/components.js
@@ -0,0 +1,102 @@
+/** When your routing table is too long, you can split it into small modules **/
+
+import Layout from '@/layout'
+
+const componentsRouter = {
+  path: '/components',
+  component: Layout,
+  redirect: 'noRedirect',
+  name: 'ComponentDemo',
+  meta: {
+    title: 'Components',
+    icon: 'component'
+  },
+  children: [
+    {
+      path: 'tinymce',
+      component: () => import('@/views/components-demo/tinymce'),
+      name: 'TinymceDemo',
+      meta: { title: 'Tinymce' }
+    },
+    {
+      path: 'markdown',
+      component: () => import('@/views/components-demo/markdown'),
+      name: 'MarkdownDemo',
+      meta: { title: 'Markdown' }
+    },
+    {
+      path: 'json-editor',
+      component: () => import('@/views/components-demo/json-editor'),
+      name: 'JsonEditorDemo',
+      meta: { title: 'JSON Editor' }
+    },
+    {
+      path: 'split-pane',
+      component: () => import('@/views/components-demo/split-pane'),
+      name: 'SplitpaneDemo',
+      meta: { title: 'SplitPane' }
+    },
+    {
+      path: 'avatar-upload',
+      component: () => import('@/views/components-demo/avatar-upload'),
+      name: 'AvatarUploadDemo',
+      meta: { title: 'Upload' }
+    },
+    {
+      path: 'dropzone',
+      component: () => import('@/views/components-demo/dropzone'),
+      name: 'DropzoneDemo',
+      meta: { title: 'Dropzone' }
+    },
+    {
+      path: 'sticky',
+      component: () => import('@/views/components-demo/sticky'),
+      name: 'StickyDemo',
+      meta: { title: 'Sticky' }
+    },
+    {
+      path: 'count-to',
+      component: () => import('@/views/components-demo/count-to'),
+      name: 'CountToDemo',
+      meta: { title: 'Count To' }
+    },
+    {
+      path: 'mixin',
+      component: () => import('@/views/components-demo/mixin'),
+      name: 'ComponentMixinDemo',
+      meta: { title: 'Component Mixin' }
+    },
+    {
+      path: 'back-to-top',
+      component: () => import('@/views/components-demo/back-to-top'),
+      name: 'BackToTopDemo',
+      meta: { title: 'Back To Top' }
+    },
+    {
+      path: 'drag-dialog',
+      component: () => import('@/views/components-demo/drag-dialog'),
+      name: 'DragDialogDemo',
+      meta: { title: 'Drag Dialog' }
+    },
+    {
+      path: 'drag-select',
+      component: () => import('@/views/components-demo/drag-select'),
+      name: 'DragSelectDemo',
+      meta: { title: 'Drag Select' }
+    },
+    {
+      path: 'dnd-list',
+      component: () => import('@/views/components-demo/dnd-list'),
+      name: 'DndListDemo',
+      meta: { title: 'Dnd List' }
+    },
+    {
+      path: 'drag-kanban',
+      component: () => import('@/views/components-demo/drag-kanban'),
+      name: 'DragKanbanDemo',
+      meta: { title: 'Drag Kanban' }
+    }
+  ]
+}
+
+export default componentsRouter
diff --git a/frontend/src/router/modules/nested.js b/frontend/src/router/modules/nested.js
new file mode 100644
index 0000000..48033ed
--- /dev/null
+++ b/frontend/src/router/modules/nested.js
@@ -0,0 +1,66 @@
+/** When your routing table is too long, you can split it into small modules **/
+
+import Layout from '@/layout'
+
+const nestedRouter = {
+  path: '/nested',
+  component: Layout,
+  redirect: '/nested/menu1/menu1-1',
+  name: 'Nested',
+  meta: {
+    title: 'Nested Routes',
+    icon: 'nested'
+  },
+  children: [
+    {
+      path: 'menu1',
+      component: () => import('@/views/nested/menu1/index'), // Parent router-view
+      name: 'Menu1',
+      meta: { title: 'Menu 1' },
+      redirect: '/nested/menu1/menu1-1',
+      children: [
+        {
+          path: 'menu1-1',
+          component: () => import('@/views/nested/menu1/menu1-1'),
+          name: 'Menu1-1',
+          meta: { title: 'Menu 1-1' }
+        },
+        {
+          path: 'menu1-2',
+          component: () => import('@/views/nested/menu1/menu1-2'),
+          name: 'Menu1-2',
+          redirect: '/nested/menu1/menu1-2/menu1-2-1',
+          meta: { title: 'Menu 1-2' },
+          children: [
+            {
+              path: 'menu1-2-1',
+              component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'),
+              name: 'Menu1-2-1',
+              meta: { title: 'Menu 1-2-1' }
+            },
+            {
+              path: 'menu1-2-2',
+              component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'),
+              name: 'Menu1-2-2',
+              meta: { title: 'Menu 1-2-2' }
+            }
+          ]
+        },
+        {
+          path: 'menu1-3',
+          component: () => import('@/views/nested/menu1/menu1-3'),
+          name: 'Menu1-3',
+          meta: { title: 'Menu 1-3' }
+        }
+      ]
+    },
+    {
+      path: 'menu2',
+      name: 'Menu2',
+      component: () => import('@/views/nested/menu2/index'),
+      meta: { title: 'Menu 2' }
+    }
+  ]
+}
+
+export default nestedRouter
diff --git a/frontend/src/router/modules/table.js b/frontend/src/router/modules/table.js
new file mode 100644
index 0000000..ec28c52
--- /dev/null
+++ b/frontend/src/router/modules/table.js
@@ -0,0 +1,41 @@
+/** When your routing table is too long, you can split it into small modules **/
+
+import Layout from '@/layout'
+
+const tableRouter = {
+  path: '/table',
+  component: Layout,
+  redirect: '/table/complex-table',
+  name: 'Table',
+  meta: {
+    title: 'Table',
+    icon: 'table'
+  },
+  children: [
+    {
+      path: 'dynamic-table',
+      component: () => import('@/views/table/dynamic-table/index'),
+      name: 'DynamicTable',
+      meta: { title: 'Dynamic Table' }
+    },
+    {
+      path: 'drag-table',
+      component: () => import('@/views/table/drag-table'),
+      name: 'DragTable',
+      meta: { title: 'Drag Table' }
+    },
+    {
+      path: 'inline-edit-table',
+      component: () => import('@/views/table/inline-edit-table'),
+      name: 'InlineEditTable',
+      meta: { title: 'Inline Edit' }
+    },
+    {
+      path: 'complex-table',
+      component: () => import('@/views/table/complex-table'),
+      name: 'ComplexTable',
+      meta: { title: 'Complex Table' }
+    }
+  ]
+}
+export default tableRouter
diff --git a/frontend/src/settings.js b/frontend/src/settings.js
new file mode 100644
index 0000000..1ebc7f2
--- /dev/null
+++ b/frontend/src/settings.js
@@ -0,0 +1,35 @@
+module.exports = {
+  title: 'Vue Element Admin',
+
+  /**
+   * @type {boolean} true | false
+   * @description Whether show the settings right-panel
+   */
+  showSettings: true,
+
+  /**
+   * @type {boolean} true | false
+   * @description Whether need tagsView
+   */
+  tagsView: true,
+
+  /**
+   * @type {boolean} true | false
+   * @description Whether fix the header
+   */
+  fixedHeader: false,
+
+  /**
+   * @type {boolean} true | false
+   * @description Whether show the logo in sidebar
+   */
+  sidebarLogo: false,
+
+  /**
+   * @type {string | array} 'production' | ['production', 'development']
+   * @description Need show err logs component.
+   * The default is only used in the production env
+   * If you want to also use it in dev, you can pass ['production', 'development']
+   */
+  errorLog: 'production'
+}
diff --git a/frontend/src/store/getters.js b/frontend/src/store/getters.js
new file mode 100644
index 0000000..8fcf5a5
--- /dev/null
+++ b/frontend/src/store/getters.js
@@ -0,0 +1,15 @@
+const getters = {
+  sidebar: state => state.app.sidebar,
+  size: state => state.app.size,
+  device: state => state.app.device,
+  visitedViews: state => state.tagsView.visitedViews,
+  cachedViews: state => state.tagsView.cachedViews,
+  token: state => state.user.token,
+  avatar: state => state.user.avatar,
+  name: state => state.user.name,
+  introduction: state => state.user.introduction,
+  roles: state => state.user.roles,
+  permission_routes: state => state.permission.routes,
+  errorLogs: state => state.errorLog.logs
+}
+export default getters
diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js
new file mode 100644
index 0000000..0fd8395
--- /dev/null
+++ b/frontend/src/store/index.js
@@ -0,0 +1,25 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+import getters from './getters'
+
+Vue.use(Vuex)
+
+// https://webpack.js.org/guides/dependency-management/#requirecontext
+const modulesFiles = require.context('./modules', true, /\.js$/)
+
+// you do not need `import app from './modules/app'`
+// it will auto require all vuex module from modules file
+const modules = modulesFiles.keys().reduce((modules, modulePath) => {
+  // set './app.js' => 'app'
+  const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
+  const value = modulesFiles(modulePath)
+  modules[moduleName] = value.default
+  return modules
+}, {})
+
+const store = new Vuex.Store({
+  modules,
+  getters
+})
+
+export default store
diff --git a/frontend/src/store/modules/app.js b/frontend/src/store/modules/app.js
new file mode 100644
index 0000000..45d89bb
--- /dev/null
+++ b/frontend/src/store/modules/app.js
@@ -0,0 +1,56 @@
+import Cookies from 'js-cookie'
+
+const state = {
+  sidebar: {
+    opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
+    withoutAnimation: false
+  },
+  device: 'desktop',
+  size: Cookies.get('size') || 'medium'
+}
+
+const mutations = {
+  TOGGLE_SIDEBAR: state => {
+    state.sidebar.opened = !state.sidebar.opened
+    state.sidebar.withoutAnimation = false
+    if (state.sidebar.opened) {
+      Cookies.set('sidebarStatus', 1)
+    } else {
+      Cookies.set('sidebarStatus', 0)
+    }
+  },
+  CLOSE_SIDEBAR: (state, withoutAnimation) => {
+    Cookies.set('sidebarStatus', 0)
+    state.sidebar.opened = false
+    state.sidebar.withoutAnimation = withoutAnimation
+  },
+  TOGGLE_DEVICE: (state, device) => {
+    state.device = device
+  },
+  SET_SIZE: (state, size) => {
+    state.size = size
+    Cookies.set('size', size)
+  }
+}
+
+const actions = {
+  toggleSideBar({ commit }) {
+    commit('TOGGLE_SIDEBAR')
+  },
+  closeSideBar({ commit }, { withoutAnimation }) {
+    commit('CLOSE_SIDEBAR', withoutAnimation)
+  },
+  toggleDevice({ commit }, device) {
+    commit('TOGGLE_DEVICE', device)
+  },
+  setSize({ commit }, size) {
+    commit('SET_SIZE', size)
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}
diff --git a/frontend/src/store/modules/errorLog.js b/frontend/src/store/modules/errorLog.js
new file mode 100644
index 0000000..6b01f95
--- /dev/null
+++ b/frontend/src/store/modules/errorLog.js
@@ -0,0 +1,28 @@
+const state = {
+  logs: []
+}
+
+const mutations = {
+  ADD_ERROR_LOG: (state, log) => {
+    state.logs.push(log)
+  },
+  CLEAR_ERROR_LOG: (state) => {
+    state.logs.splice(0)
+  }
+}
+
+const actions = {
+  addErrorLog({ commit }, log) {
+    commit('ADD_ERROR_LOG', log)
+  },
+  clearErrorLog({ commit }) {
+    commit('CLEAR_ERROR_LOG')
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}
diff --git a/frontend/src/store/modules/permission.js b/frontend/src/store/modules/permission.js
new file mode 100644
index 0000000..aeb5ee5
--- /dev/null
+++ b/frontend/src/store/modules/permission.js
@@ -0,0 +1,69 @@
+import { asyncRoutes, constantRoutes } from '@/router'
+
+/**
+ * Use meta.role to determine if the current user has permission
+ * @param roles
+ * @param route
+ */
+function hasPermission(roles, route) {
+  if (route.meta && route.meta.roles) {
+    return roles.some(role => route.meta.roles.includes(role))
+  } else {
+    return true
+  }
+}
+
+/**
+ * Filter asynchronous routing tables by recursion
+ * @param routes asyncRoutes
+ * @param roles
+ */
+export function filterAsyncRoutes(routes, roles) {
+  const res = []
+
+  routes.forEach(route => {
+    const tmp = { ...route }
+    if (hasPermission(roles, tmp)) {
+      if (tmp.children) {
+        tmp.children = filterAsyncRoutes(tmp.children, roles)
+      }
+      res.push(tmp)
+    }
+  })
+
+  return res
+}
+
+const state = {
+  routes: [],
+  addRoutes: []
+}
+
+const mutations = {
+  SET_ROUTES: (state, routes) => {
+    state.addRoutes = routes
+    state.routes = constantRoutes.concat(routes)
+  }
+}
+
+const actions = {
+  generateRoutes({ commit }, roles) {
+    return new Promise(resolve => {
+      let accessedRoutes
+      if (roles.includes('admin')) {
+        accessedRoutes = asyncRoutes || []
+      } else {
+        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
+      }
+      commit('SET_ROUTES', accessedRoutes)
+      resolve(accessedRoutes)
+    })
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}
diff --git a/frontend/src/store/modules/settings.js b/frontend/src/store/modules/settings.js
new file mode 100644
index 0000000..110533f
--- /dev/null
+++ b/frontend/src/store/modules/settings.js
@@ -0,0 +1,35 @@
+import variables from '@/styles/element-variables.scss'
+import defaultSettings from '@/settings'
+
+const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings
+
+const state = {
+  theme: variables.theme,
+  showSettings: showSettings,
+  tagsView: tagsView,
+  fixedHeader: fixedHeader,
+  sidebarLogo: sidebarLogo
+}
+
+const mutations = {
+  CHANGE_SETTING: (state, { key, value }) => {
+    // eslint-disable-next-line no-prototype-builtins
+    if (state.hasOwnProperty(key)) {
+      state[key] = value
+    }
+  }
+}
+
+const actions = {
+  changeSetting({ commit }, data) {
+    commit('CHANGE_SETTING', data)
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}
+
diff --git a/frontend/src/store/modules/tagsView.js b/frontend/src/store/modules/tagsView.js
new file mode 100644
index 0000000..57e7242
--- /dev/null
+++ b/frontend/src/store/modules/tagsView.js
@@ -0,0 +1,160 @@
+const state = {
+  visitedViews: [],
+  cachedViews: []
+}
+
+const mutations = {
+  ADD_VISITED_VIEW: (state, view) => {
+    if (state.visitedViews.some(v => v.path === view.path)) return
+    state.visitedViews.push(
+      Object.assign({}, view, {
+        title: view.meta.title || 'no-name'
+      })
+    )
+  },
+  ADD_CACHED_VIEW: (state, view) => {
+    if (state.cachedViews.includes(view.name)) return
+    if (!view.meta.noCache) {
+      state.cachedViews.push(view.name)
+    }
+  },
+
+  DEL_VISITED_VIEW: (state, view) => {
+    for (const [i, v] of state.visitedViews.entries()) {
+      if (v.path === view.path) {
+        state.visitedViews.splice(i, 1)
+        break
+      }
+    }
+  },
+  DEL_CACHED_VIEW: (state, view) => {
+    const index = state.cachedViews.indexOf(view.name)
+    index > -1 && state.cachedViews.splice(index, 1)
+  },
+
+  DEL_OTHERS_VISITED_VIEWS: (state, view) => {
+    state.visitedViews = state.visitedViews.filter(v => {
+      return v.meta.affix || v.path === view.path
+    })
+  },
+  DEL_OTHERS_CACHED_VIEWS: (state, view) => {
+    const index = state.cachedViews.indexOf(view.name)
+    if (index > -1) {
+      state.cachedViews = state.cachedViews.slice(index, index + 1)
+    } else {
+      // if index = -1, there is no cached tags
+      state.cachedViews = []
+    }
+  },
+
+  DEL_ALL_VISITED_VIEWS: state => {
+    // keep affix tags
+    const affixTags = state.visitedViews.filter(tag => tag.meta.affix)
+    state.visitedViews = affixTags
+  },
+  DEL_ALL_CACHED_VIEWS: state => {
+    state.cachedViews = []
+  },
+
+  UPDATE_VISITED_VIEW: (state, view) => {
+    for (let v of state.visitedViews) {
+      if (v.path === view.path) {
+        v = Object.assign(v, view)
+        break
+      }
+    }
+  }
+}
+
+const actions = {
+  addView({ dispatch }, view) {
+    dispatch('addVisitedView', view)
+    dispatch('addCachedView', view)
+  },
+  addVisitedView({ commit }, view) {
+    commit('ADD_VISITED_VIEW', view)
+  },
+  addCachedView({ commit }, view) {
+    commit('ADD_CACHED_VIEW', view)
+  },
+
+  delView({ dispatch, state }, view) {
+    return new Promise(resolve => {
+      dispatch('delVisitedView', view)
+      dispatch('delCachedView', view)
+      resolve({
+        visitedViews: [...state.visitedViews],
+        cachedViews: [...state.cachedViews]
+      })
+    })
+  },
+  delVisitedView({ commit, state }, view) {
+    return new Promise(resolve => {
+      commit('DEL_VISITED_VIEW', view)
+      resolve([...state.visitedViews])
+    })
+  },
+  delCachedView({ commit, state }, view) {
+    return new Promise(resolve => {
+      commit('DEL_CACHED_VIEW', view)
+      resolve([...state.cachedViews])
+    })
+  },
+
+  delOthersViews({ dispatch, state }, view) {
+    return new Promise(resolve => {
+      dispatch('delOthersVisitedViews', view)
+      dispatch('delOthersCachedViews', view)
+      resolve({
+        visitedViews: [...state.visitedViews],
+        cachedViews: [...state.cachedViews]
+      })
+    })
+  },
+  delOthersVisitedViews({ commit, state }, view) {
+    return new Promise(resolve => {
+      commit('DEL_OTHERS_VISITED_VIEWS', view)
+      resolve([...state.visitedViews])
+    })
+  },
+  delOthersCachedViews({ commit, state }, view) {
+    return new Promise(resolve => {
+      commit('DEL_OTHERS_CACHED_VIEWS', view)
+      resolve([...state.cachedViews])
+    })
+  },
+
+  delAllViews({ dispatch, state }, view) {
+    return new Promise(resolve => {
+      dispatch('delAllVisitedViews', view)
+      dispatch('delAllCachedViews', view)
+      resolve({
+        visitedViews: [...state.visitedViews],
+        cachedViews: [...state.cachedViews]
+      })
+    })
+  },
+  delAllVisitedViews({ commit, state }) {
+    return new Promise(resolve => {
+      commit('DEL_ALL_VISITED_VIEWS')
+      resolve([...state.visitedViews])
+    })
+  },
+  delAllCachedViews({ commit, state }) {
+    return new Promise(resolve => {
+      commit('DEL_ALL_CACHED_VIEWS')
+      resolve([...state.cachedViews])
+    })
+  },
+
+  updateVisitedView({ commit }, view) {
+    commit('UPDATE_VISITED_VIEW', view)
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}
diff --git a/frontend/src/store/modules/user.js b/frontend/src/store/modules/user.js
new file mode 100644
index 0000000..7800941
--- /dev/null
+++ b/frontend/src/store/modules/user.js
@@ -0,0 +1,131 @@
+import { login, logout, getInfo } from '@/api/user'
+import { getToken, setToken, removeToken } from '@/utils/auth'
+import router, { resetRouter } from '@/router'
+
+const state = {
+  token: getToken(),
+  name: '',
+  avatar: '',
+  introduction: '',
+  roles: []
+}
+
+const mutations = {
+  SET_TOKEN: (state, token) => {
+    state.token = token
+  },
+  SET_INTRODUCTION: (state, introduction) => {
+    state.introduction = introduction
+  },
+  SET_NAME: (state, name) => {
+    state.name = name
+  },
+  SET_AVATAR: (state, avatar) => {
+    state.avatar = avatar
+  },
+  SET_ROLES: (state, roles) => {
+    state.roles = roles
+  }
+}
+
+const actions = {
+  // user login
+  login({ commit }, userInfo) {
+    const { username, password } = userInfo
+    return new Promise((resolve, reject) => {
+      login({ username: username.trim(), password: password }).then(response => {
+        const { data } = response
+        commit('SET_TOKEN', data.token)
+        setToken(data.token)
+        resolve()
+      }).catch(error => {
+        reject(error)
+      })
+    })
+  },
+
+  // get user info
+  getInfo({ commit, state }) {
+    return new Promise((resolve, reject) => {
+      getInfo(state.token).then(response => {
+        const { data } = response
+
+        if (!data) {
+          reject('Verification failed, please Login again.')
+        }
+
+        const { roles, name, avatar, introduction } = data
+
+        // roles must be a non-empty array
+        if (!roles || roles.length <= 0) {
+          reject('getInfo: roles must be a non-null array!')
+        }
+
+        commit('SET_ROLES', roles)
+        commit('SET_NAME', name)
+        commit('SET_AVATAR', avatar)
+        commit('SET_INTRODUCTION', introduction)
+        resolve(data)
+      }).catch(error => {
+        reject(error)
+      })
+    })
+  },
+
+  // user logout
+  logout({ commit, state, dispatch }) {
+    return new Promise((resolve, reject) => {
+      logout(state.token).then(() => {
+        commit('SET_TOKEN', '')
+        commit('SET_ROLES', [])
+        removeToken()
+        resetRouter()
+
+        // reset visited views and cached views
+        // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2485
+        dispatch('tagsView/delAllViews', null, { root: true })
+
+        resolve()
+      }).catch(error => {
+        reject(error)
+      })
+    })
+  },
+
+  // remove token
+  resetToken({ commit }) {
+    return new Promise(resolve => {
+      commit('SET_TOKEN', '')
+      commit('SET_ROLES', [])
+      removeToken()
+      resolve()
+    })
+  },
+
+  // dynamically modify permissions
+  async changeRoles({ commit, dispatch }, role) {
+    const token = role + '-token'
+
+    commit('SET_TOKEN', token)
+    setToken(token)
+
+    const { roles } = await dispatch('getInfo')
+
+    resetRouter()
+
+    // generate accessible routes map based on roles
+    const accessRoutes = await dispatch('permission/generateRoutes', roles, { root: true })
+    // dynamically add accessible routes
+    router.addRoutes(accessRoutes)
+
+    // reset visited views and cached views
+    dispatch('tagsView/delAllViews', null, { root: true })
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}
diff --git a/frontend/src/styles/btn.scss b/frontend/src/styles/btn.scss
new file mode 100644
index 0000000..e6ba1a8
--- /dev/null
+++ b/frontend/src/styles/btn.scss
@@ -0,0 +1,99 @@
+@import './variables.scss';
+
+@mixin colorBtn($color) {
+  background: $color;
+
+  &:hover {
+    color: $color;
+
+    &:before,
+    &:after {
+      background: $color;
+    }
+  }
+}
+
+.blue-btn {
+  @include colorBtn($blue)
+}
+
+.light-blue-btn {
+  @include colorBtn($light-blue)
+}
+
+.red-btn {
+  @include colorBtn($red)
+}
+
+.pink-btn {
+  @include colorBtn($pink)
+}
+
+.green-btn {
+  @include colorBtn($green)
+}
+
+.tiffany-btn {
+  @include colorBtn($tiffany)
+}
+
+.yellow-btn {
+  @include colorBtn($yellow)
+}
+
+.pan-btn {
+  font-size: 14px;
+  color: #fff;
+  padding: 14px 36px;
+  border-radius: 8px;
+  border: none;
+  outline: none;
+  transition: 600ms ease all;
+  position: relative;
+  display: inline-block;
+
+  &:hover {
+    background: #fff;
+
+    &:before,
+    &:after {
+      width: 100%;
+      transition: 600ms ease all;
+    }
+  }
+
+  &:before,
+  &:after {
+    content: '';
+    position: absolute;
+    top: 0;
+    right: 0;
+    height: 2px;
+    width: 0;
+    transition: 400ms ease all;
+  }
+
+  &::after {
+    right: inherit;
+    top: inherit;
+    left: 0;
+    bottom: 0;
+  }
+}
+
+.custom-button {
+  display: inline-block;
+  line-height: 1;
+  white-space: nowrap;
+  cursor: pointer;
+  background: #fff;
+  color: #fff;
+  -webkit-appearance: none;
+  text-align: center;
+  box-sizing: border-box;
+  outline: 0;
+  margin: 0;
+  padding: 10px 15px;
+  font-size: 14px;
+  border-radius: 4px;
+}
diff --git a/frontend/src/styles/element-ui.scss b/frontend/src/styles/element-ui.scss
new file mode 100644
index 0000000..49474de
--- /dev/null
+++ b/frontend/src/styles/element-ui.scss
@@ -0,0 +1,84 @@
+// cover some element-ui styles
+
+.el-breadcrumb__inner,
+.el-breadcrumb__inner a {
+  font-weight: 400 !important;
+}
+
+.el-upload {
+  input[type="file"] {
+    display: none !important;
+  }
+}
+
+.el-upload__input {
+  display: none;
+}
+
+.cell {
+  .el-tag {
+    margin-right: 0px;
+  }
+}
+
+.small-padding {
+  .cell {
+    padding-left: 5px;
+    padding-right: 5px;
+  }
+}
+
+.fixed-width {
+  .el-button--mini {
+    padding: 7px 10px;
+    min-width: 60px;
+  }
+}
+
+.status-col {
+  .cell {
+    padding: 0 10px;
+    text-align: center;
+
+    .el-tag {
+      margin-right: 0px;
+    }
+  }
+}
+
+// to fixed https://github.com/ElemeFE/element/issues/2461
+.el-dialog {
+  transform: none;
+  left: 0;
+  position: relative;
+  margin: 0 auto;
+}
+
+// refine element ui upload
+.upload-container {
+  .el-upload {
+    width: 100%;
+
+    .el-upload-dragger {
+      width: 100%;
+      height: 200px;
+    }
+  }
+}
+
+// dropdown
+.el-dropdown-menu {
+  a {
+    display: block
+  }
+}
+
+// fix date-picker ui bug in filter-item
+.el-range-editor.el-input__inner {
+  display: inline-flex !important;
+}
+
+// to fix el-date-picker css style
+.el-range-separator {
+  box-sizing: content-box;
+}
diff --git a/frontend/src/styles/element-variables.scss b/frontend/src/styles/element-variables.scss
new file mode 100644
index 0000000..5bdc4da
--- /dev/null
+++ b/frontend/src/styles/element-variables.scss
@@ -0,0 +1,31 @@
+/**
+* I think element-ui's default theme color is too light for long-term use.
+* So I modified the default color and you can modify it to your liking.
+**/
+
+/* theme color */
+$--color-primary: #1890ff;
+$--color-success: #13ce66;
+$--color-warning: #ffba00;
+$--color-danger: #ff4949;
+// $--color-info: #1E1E1E;
+
+$--button-font-weight: 400;
+
+// $--color-text-regular: #1f2d3d;
+
+$--border-color-light: #dfe4ed;
+$--border-color-lighter: #e6ebf5;
+
+$--table-border: 1px solid #dfe6ec;
+
+/* icon font path, required */
+$--font-path: "~element-ui/lib/theme-chalk/fonts";
+
+@import "~element-ui/packages/theme-chalk/src/index";
+
+// the :export directive is the magic sauce for webpack
+// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
+:export {
+  theme: $--color-primary;
+}
diff --git a/frontend/src/styles/index.scss b/frontend/src/styles/index.scss
new file mode 100644
index 0000000..96095ef
--- /dev/null
+++ b/frontend/src/styles/index.scss
@@ -0,0 +1,191 @@
+@import './variables.scss';
+@import './mixin.scss';
+@import './transition.scss';
+@import './element-ui.scss';
+@import './sidebar.scss';
+@import './btn.scss';
+
+body {
+  height: 100%;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-font-smoothing: antialiased;
+  text-rendering: optimizeLegibility;
+  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+}
+
+label {
+  font-weight: 700;
+}
+
+html {
+  height: 100%;
+  box-sizing: border-box;
+}
+
+#app {
+  height: 100%;
+}
+
+*,
+*:before,
+*:after {
+  box-sizing: inherit;
+}
+
+.no-padding {
+  padding: 0px !important;
+}
+
+.padding-content {
+  padding: 4px 0;
+}
+
+a:focus,
+a:active {
+  outline: none;
+}
+
+a,
+a:focus,
+a:hover {
+  cursor: pointer;
+  color: inherit;
+  text-decoration: none;
+}
+
+div:focus {
+  outline: none;
+}
+
+.fr {
+  float: right;
+}
+
+.fl {
+  float: left;
+}
+
+.pr-5 {
+  padding-right: 5px;
+}
+
+.pl-5 {
+  padding-left: 5px;
+}
+
+.block {
+  display: block;
+}
+
+.pointer {
+  cursor: pointer;
+}
+
+.inlineBlock {
+  display: block;
+}
+
+.clearfix {
+  &:after {
+    visibility: hidden;
+    display: block;
+    font-size: 0;
+    content: " ";
+    clear: both;
+    height: 0;
+  }
+}
+
+aside {
+  background: #eef1f6;
+  padding: 8px 24px;
+  margin-bottom: 20px;
+  border-radius: 2px;
+  display: block;
+  line-height: 32px;
+  font-size: 16px;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+  color: #2c3e50;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+
+  a {
+    color: #337ab7;
+    cursor: pointer;
+
+    &:hover {
+      color: rgb(32, 160, 255);
+    }
+  }
+}
+
+//main-container全局样式
+.app-container {
+  padding: 20px;
+}
+
+.components-container {
+  margin: 30px 50px;
+  position: relative;
+}
+
+.pagination-container {
+  margin-top: 30px;
+}
+
+.text-center {
+  text-align: center
+}
+
+.sub-navbar {
+  height: 50px;
+  line-height: 50px;
+  position: relative;
+  width: 100%;
+  text-align: right;
+  padding-right: 20px;
+  transition: 600ms ease position;
+  background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
+
+  .subtitle {
+    font-size: 20px;
+    color: #fff;
+  }
+
+  &.draft {
+    background: #d0d0d0;
+  }
+
+  &.deleted {
+    background: #d0d0d0;
+  }
+}
+
+.link-type,
+.link-type:focus {
+  color: #337ab7;
+  cursor: pointer;
+
+  &:hover {
+    color: rgb(32, 160, 255);
+  }
+}
+
+.filter-container {
+  padding-bottom: 10px;
+
+  .filter-item {
+    display: inline-block;
+    vertical-align: middle;
+    margin-bottom: 10px;
+  }
+}
+
+//refine vue-multiselect plugin
+.multiselect {
+  line-height: 16px;
+}
+
+.multiselect--active {
+  z-index: 1000 !important;
+}
diff --git a/frontend/src/styles/mixin.scss b/frontend/src/styles/mixin.scss
new file mode 100644
index 0000000..06fa061
--- /dev/null
+++ b/frontend/src/styles/mixin.scss
@@ -0,0 +1,66 @@
+@mixin clearfix {
+  &:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+}
+
+@mixin scrollBar {
+  &::-webkit-scrollbar-track-piece {
+    background: #d3dce6;
+  }
+
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    background: #99a9bf;
+    border-radius: 20px;
+  }
+}
+
+@mixin relative {
+  position: relative;
+  width: 100%;
+  height: 100%;
+}
+
+@mixin pct($pct) {
+  width: #{$pct};
+  position: relative;
+  margin: 0 auto;
+}
+
+@mixin triangle($width, $height, $color, $direction) {
+  $width: $width/2;
+  $color-border-style: $height solid $color;
+  $transparent-border-style: $width solid transparent;
+  height: 0;
+  width: 0;
+
+  @if $direction==up {
+    border-bottom: $color-border-style;
+    border-left: $transparent-border-style;
+    border-right: $transparent-border-style;
+  }
+
+  @else if $direction==right {
+    border-left: $color-border-style;
+    border-top: $transparent-border-style;
+    border-bottom: $transparent-border-style;
+  }
+
+  @else if $direction==down {
+    border-top: $color-border-style;
+    border-left: $transparent-border-style;
+    border-right: $transparent-border-style;
+  }
+
+  @else if $direction==left {
+    border-right: $color-border-style;
+    border-top: $transparent-border-style;
+    border-bottom: $transparent-border-style;
+  }
+}
diff --git a/frontend/src/styles/sidebar.scss b/frontend/src/styles/sidebar.scss
new file mode 100644
index 0000000..94760cc
--- /dev/null
+++ b/frontend/src/styles/sidebar.scss
@@ -0,0 +1,226 @@
+#app {
+
+  .main-container {
+    min-height: 100%;
+    transition: margin-left .28s;
+    margin-left: $sideBarWidth;
+    position: relative;
+  }
+
+  .sidebar-container {
+    transition: width 0.28s;
+    width: $sideBarWidth !important;
+    background-color: $menuBg;
+    height: 100%;
+    position: fixed;
+    font-size: 0px;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    z-index: 1001;
+    overflow: hidden;
+
+    // reset element-ui css
+    .horizontal-collapse-transition {
+      transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
+    }
+
+    .scrollbar-wrapper {
+      overflow-x: hidden !important;
+    }
+
+    .el-scrollbar__bar.is-vertical {
+      right: 0px;
+    }
+
+    .el-scrollbar {
+      height: 100%;
+    }
+
+    &.has-logo {
+      .el-scrollbar {
+        height: calc(100% - 50px);
+      }
+    }
+
+    .is-horizontal {
+      display: none;
+    }
+
+    a {
+      display: inline-block;
+      width: 100%;
+      overflow: hidden;
+    }
+
+    .svg-icon {
+      margin-right: 16px;
+    }
+
+    .sub-el-icon {
+      margin-right: 12px;
+      margin-left: -2px;
+    }
+
+    .el-menu {
+      border: none;
+      height: 100%;
+      width: 100% !important;
+    }
+
+    // menu hover
+    .submenu-title-noDropdown,
+    .el-submenu__title {
+      &:hover {
+        background-color: $menuHover !important;
+      }
+    }
+
+    .is-active>.el-submenu__title {
+      color: $subMenuActiveText !important;
+    }
+
+    & .nest-menu .el-submenu>.el-submenu__title,
+    & .el-submenu .el-menu-item {
+      min-width: $sideBarWidth !important;
+      background-color: $subMenuBg !important;
+
+      &:hover {
+        background-color: $subMenuHover !important;
+      }
+    }
+  }
+
+  .hideSidebar {
+    .sidebar-container {
+      width: 54px !important;
+    }
+
+    .main-container {
+      margin-left: 54px;
+    }
+
+    .submenu-title-noDropdown {
+      padding: 0 !important;
+      position: relative;
+
+      .el-tooltip {
+        padding: 0 !important;
+
+        .svg-icon {
+          margin-left: 20px;
+        }
+
+        .sub-el-icon {
+          margin-left: 19px;
+        }
+      }
+    }
+
+    .el-submenu {
+      overflow: hidden;
+
+      &>.el-submenu__title {
+        padding: 0 !important;
+
+        .svg-icon {
+          margin-left: 20px;
+        }
+
+        .sub-el-icon {
+          margin-left: 19px;
+        }
+
+        .el-submenu__icon-arrow {
+          display: none;
+        }
+      }
+    }
+
+    .el-menu--collapse {
+      .el-submenu {
+        &>.el-submenu__title {
+          &>span {
+            height: 0;
+            width: 0;
+            overflow: hidden;
+            visibility: hidden;
+            display: inline-block;
+          }
+        }
+      }
+    }
+  }
+
+  .el-menu--collapse .el-menu .el-submenu {
+    min-width: $sideBarWidth !important;
+  }
+
+  // mobile responsive
+  .mobile {
+    .main-container {
+      margin-left: 0px;
+    }
+
+    .sidebar-container {
+      transition: transform .28s;
+      width: $sideBarWidth !important;
+    }
+
+    &.hideSidebar {
+      .sidebar-container {
+        pointer-events: none;
+        transition-duration: 0.3s;
+        transform: translate3d(-$sideBarWidth, 0, 0);
+      }
+    }
+  }
+
+  .withoutAnimation {
+
+    .main-container,
+    .sidebar-container {
+      transition: none;
+    }
+  }
+}
+
+// when menu collapsed
+.el-menu--vertical {
+  &>.el-menu {
+    .svg-icon {
+      margin-right: 16px;
+    }
+    .sub-el-icon {
+      margin-right: 12px;
+      margin-left: -2px;
+    }
+  }
+
+  .nest-menu .el-submenu>.el-submenu__title,
+  .el-menu-item {
+    &:hover {
+      // you can use $subMenuHover
+      background-color: $menuHover !important;
+    }
+  }
+
+  // the scroll bar appears when the subMenu is too long
+  >.el-menu--popup {
+    max-height: 100vh;
+    overflow-y: auto;
+
+    &::-webkit-scrollbar-track-piece {
+      background: #d3dce6;
+    }
+
+    &::-webkit-scrollbar {
+      width: 6px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: #99a9bf;
+      border-radius: 20px;
+    }
+  }
+}
diff --git a/frontend/src/styles/transition.scss b/frontend/src/styles/transition.scss
new file mode 100644
index 0000000..4cb27cc
--- /dev/null
+++ b/frontend/src/styles/transition.scss
@@ -0,0 +1,48 @@
+// global transition css
+
+/* fade */
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity 0.28s;
+}
+
+.fade-enter,
+.fade-leave-active {
+  opacity: 0;
+}
+
+/* fade-transform */
+.fade-transform-leave-active,
+.fade-transform-enter-active {
+  transition: all .5s;
+}
+
+.fade-transform-enter {
+  opacity: 0;
+  transform: translateX(-30px);
+}
+
+.fade-transform-leave-to {
+  opacity: 0;
+  transform: translateX(30px);
+}
+
+/* breadcrumb transition */
+.breadcrumb-enter-active,
+.breadcrumb-leave-active {
+  transition: all .5s;
+}
+
+.breadcrumb-enter,
+.breadcrumb-leave-active {
+  opacity: 0;
+  transform: translateX(20px);
+}
+
+.breadcrumb-move {
+  transition: all .5s;
+}
+
+.breadcrumb-leave-active {
+  position: absolute;
+}
diff --git a/frontend/src/styles/variables.scss b/frontend/src/styles/variables.scss
new file mode 100644
index 0000000..a19c27c
--- /dev/null
+++ b/frontend/src/styles/variables.scss
@@ -0,0 +1,35 @@
+// base color
+$blue:#324157;
+$light-blue:#3A71A8;
+$red:#C03639;
+$pink: #E65D6E;
+$green: #30B08F;
+$tiffany: #4AB7BD;
+$yellow:#FEC171;
+$panGreen: #30B08F;
+
+// sidebar
+$menuText:#bfcbd9;
+$menuActiveText:#409EFF;
+$subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951
+
+$menuBg:#304156;
+$menuHover:#263445;
+
+$subMenuBg:#1f2d3d;
+$subMenuHover:#001528;
+
+$sideBarWidth: 210px;
+
+// the :export directive is the magic sauce for webpack
+// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
+:export {
+  menuText: $menuText;
+  menuActiveText: $menuActiveText;
+  subMenuActiveText: $subMenuActiveText;
+  menuBg: $menuBg;
+  menuHover: $menuHover;
+  subMenuBg: $subMenuBg;
+  subMenuHover: $subMenuHover;
+  sideBarWidth: $sideBarWidth;
+}
diff --git a/frontend/src/utils/auth.js b/frontend/src/utils/auth.js
new file mode 100644
index 0000000..08a43d6
--- /dev/null
+++ b/frontend/src/utils/auth.js
@@ -0,0 +1,15 @@
+import Cookies from 'js-cookie'
+
+const TokenKey = 'Admin-Token'
+
+export function getToken() {
+  return Cookies.get(TokenKey)
+}
+
+export function setToken(token) {
+  return Cookies.set(TokenKey, token)
+}
+
+export function removeToken() {
+  return Cookies.remove(TokenKey)
+}
diff --git a/frontend/src/utils/clipboard.js b/frontend/src/utils/clipboard.js
new file mode 100644
index 0000000..cf5b07a
--- /dev/null
+++ b/frontend/src/utils/clipboard.js
@@ -0,0 +1,32 @@
+import Vue from 'vue'
+import Clipboard from 'clipboard'
+
+function clipboardSuccess() {
+  Vue.prototype.$message({
+    message: 'Copy successfully',
+    type: 'success',
+    duration: 1500
+  })
+}
+
+function clipboardError() {
+  Vue.prototype.$message({
+    message: 'Copy failed',
+    type: 'error'
+  })
+}
+
+export default function handleClipboard(text, event) {
+  const clipboard = new Clipboard(event.target, {
+    text: () => text
+  })
+  clipboard.on('success', () => {
+    clipboardSuccess()
+    clipboard.destroy()
+  })
+  clipboard.on('error', () => {
+    clipboardError()
+    clipboard.destroy()
+  })
+  clipboard.onClick(event)
+}
diff --git a/frontend/src/utils/error-log.js b/frontend/src/utils/error-log.js
new file mode 100644
index 0000000..a7f5b55
--- /dev/null
+++ b/frontend/src/utils/error-log.js
@@ -0,0 +1,35 @@
+import Vue from 'vue'
+import store from '@/store'
+import { isString, isArray } from '@/utils/validate'
+import settings from '@/settings'
+
+// you can set in settings.js
+// errorLog:'production' | ['production', 'development']
+const { errorLog: needErrorLog } = settings
+
+function checkNeed() {
+  const env = process.env.NODE_ENV
+  if (isString(needErrorLog)) {
+    return env === needErrorLog
+  }
+  if (isArray(needErrorLog)) {
+    return needErrorLog.includes(env)
+  }
+  return false
+}
+
+if (checkNeed()) {
+  Vue.config.errorHandler = function(err, vm, info, a) {
+  // Don't ask me why I use Vue.nextTick, it just a hack.
+  // detail see https://forum.vuejs.org/t/dispatch-in-vue-config-errorhandler-has-some-problem/23500
+    Vue.nextTick(() => {
+      store.dispatch('errorLog/addErrorLog', {
+        err,
+        vm,
+        info,
+        url: window.location.href
+      })
+      console.error(err, info)
+    })
+  }
+}
diff --git a/frontend/src/utils/get-page-title.js b/frontend/src/utils/get-page-title.js
new file mode 100644
index 0000000..cab7fd9
--- /dev/null
+++ b/frontend/src/utils/get-page-title.js
@@ -0,0 +1,10 @@
+import defaultSettings from '@/settings'
+
+const title = defaultSettings.title || 'Vue Element Admin'
+
+export default function getPageTitle(pageTitle) {
+  if (pageTitle) {
+    return `${pageTitle} - ${title}`
+  }
+  return `${title}`
+}
diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js
new file mode 100644
index 0000000..3225d3c
--- /dev/null
+++ b/frontend/src/utils/index.js
@@ -0,0 +1,357 @@
+/**
+ * Created by PanJiaChen on 16/11/18.
+ */
+
+/**
+ * Parse the time to string
+ * @param {(Object|string|number)} time
+ * @param {string} cFormat
+ * @returns {string | null}
+ */
+export function parseTime(time, cFormat) {
+  if (arguments.length === 0 || !time) {
+    return null
+  }
+  const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
+  let date
+  if (typeof time === 'object') {
+    date = time
+  } else {
+    if ((typeof time === 'string')) {
+      if ((/^[0-9]+$/.test(time))) {
+        // support "1548221490638"
+        time = parseInt(time)
+      } else {
+        // support safari
+        // https://stackoverflow.com/questions/4310953/invalid-date-in-safari
+        time = time.replace(new RegExp(/-/gm), '/')
+      }
+    }
+
+    if ((typeof time === 'number') && (time.toString().length === 10)) {
+      time = time * 1000
+    }
+    date = new Date(time)
+  }
+  const formatObj = {
+    y: date.getFullYear(),
+    m: date.getMonth() + 1,
+    d: date.getDate(),
+    h: date.getHours(),
+    i: date.getMinutes(),
+    s: date.getSeconds(),
+    a: date.getDay()
+  }
+  const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
+    const value = formatObj[key]
+    // Note: getDay() returns 0 on Sunday
+    if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
+    return value.toString().padStart(2, '0')
+  })
+  return time_str
+}
+
+/**
+ * @param {number} time
+ * @param {string} option
+ * @returns {string}
+ */
+export function formatTime(time, option) {
+  if (('' + time).length === 10) {
+    time = parseInt(time) * 1000
+  } else {
+    time = +time
+  }
+  const d = new Date(time)
+  const now = Date.now()
+
+  const diff = (now - d) / 1000
+
+  if (diff < 30) {
+    return '刚刚'
+  } else if (diff < 3600) {
+    // less 1 hour
+    return Math.ceil(diff / 60) + '分钟前'
+  } else if (diff < 3600 * 24) {
+    return Math.ceil(diff / 3600) + '小时前'
+  } else if (diff < 3600 * 24 * 2) {
+    return '1天前'
+  }
+  if (option) {
+    return parseTime(time, option)
+  } else {
+    return (
+      d.getMonth() +
+      1 +
+      '月' +
+      d.getDate() +
+      '日' +
+      d.getHours() +
+      '时' +
+      d.getMinutes() +
+      '分'
+    )
+  }
+}
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export function getQueryObject(url) {
+  url = url == null ? window.location.href : url
+  const search = url.substring(url.lastIndexOf('?') + 1)
+  const obj = {}
+  const reg = /([^?&=]+)=([^?&=]*)/g
+  search.replace(reg, (rs, $1, $2) => {
+    const name = decodeURIComponent($1)
+    let val = decodeURIComponent($2)
+    val = String(val)
+    obj[name] = val
+    return rs
+  })
+  return obj
+}
+
+/**
+ * @param {string} input value
+ * @returns {number} output value
+ */
+export function byteLength(str) {
+  // returns the byte length of an utf8 string
+  let s = str.length
+  for (var i = str.length - 1; i >= 0; i--) {
+    const code = str.charCodeAt(i)
+    if (code > 0x7f && code <= 0x7ff) s++
+    else if (code > 0x7ff && code <= 0xffff) s += 2
+    if (code >= 0xDC00 && code <= 0xDFFF) i--
+  }
+  return s
+}
+
+/**
+ * @param {Array} actual
+ * @returns {Array}
+ */
+export function cleanArray(actual) {
+  const newArray = []
+  for (let i = 0; i < actual.length; i++) {
+    if (actual[i]) {
+      newArray.push(actual[i])
+    }
+  }
+  return newArray
+}
+
+/**
+ * @param {Object} json
+ * @returns {Array}
+ */
+export function param(json) {
+  if (!json) return ''
+  return cleanArray(
+    Object.keys(json).map(key => {
+      if (json[key] === undefined) return ''
+      return encodeURIComponent(key) + '=' + encodeURIComponent(json[key])
+    })
+  ).join('&')
+}
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export function param2Obj(url) {
+  const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
+  if (!search) {
+    return {}
+  }
+  const obj = {}
+  const searchArr = search.split('&')
+  searchArr.forEach(v => {
+    const index = v.indexOf('=')
+    if (index !== -1) {
+      const name = v.substring(0, index)
+      const val = v.substring(index + 1, v.length)
+      obj[name] = val
+    }
+  })
+  return obj
+}
+
+/**
+ * @param {string} val
+ * @returns {string}
+ */
+export function html2Text(val) {
+  const div = document.createElement('div')
+  div.innerHTML = val
+  return div.textContent || div.innerText
+}
+
+/**
+ * Merges two objects, giving the last one precedence
+ * @param {Object} target
+ * @param {(Object|Array)} source
+ * @returns {Object}
+ */
+export function objectMerge(target, source) {
+  if (typeof target !== 'object') {
+    target = {}
+  }
+  if (Array.isArray(source)) {
+    return source.slice()
+  }
+  Object.keys(source).forEach(property => {
+    const sourceProperty = source[property]
+    if (typeof sourceProperty === 'object') {
+      target[property] = objectMerge(target[property], sourceProperty)
+    } else {
+      target[property] = sourceProperty
+    }
+  })
+  return target
+}
+
+/**
+ * @param {HTMLElement} element
+ * @param {string} className
+ */
+export function toggleClass(element, className) {
+  if (!element || !className) {
+    return
+  }
+  let classString = element.className
+  const nameIndex = classString.indexOf(className)
+  if (nameIndex === -1) {
+    classString += '' + className
+  } else {
+    classString =
+      classString.substr(0, nameIndex) +
+      classString.substr(nameIndex + className.length)
+  }
+  element.className = classString
+}
+
+/**
+ * @param {string} type
+ * @returns {Date}
+ */
+export function getTime(type) {
+  if (type === 'start') {
+    return new Date().getTime() - 3600 * 1000 * 24 * 90
+  } else {
+    return new Date(new Date().toDateString())
+  }
+}
+
+/**
+ * @param {Function} func
+ * @param {number} wait
+ * @param {boolean} immediate
+ * @return {*}
+ */
+export function debounce(func, wait, immediate) {
+  let timeout, args, context, timestamp, result
+
+  const later = function() {
+    // 据上一次触发时间间隔
+    const last = +new Date() - timestamp
+
+    // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
+    if (last < wait && last > 0) {
+      timeout = setTimeout(later, wait - last)
+    } else {
+      timeout = null
+      // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
+      if (!immediate) {
+        result = func.apply(context, args)
+        if (!timeout) context = args = null
+      }
+    }
+  }
+
+  return function(...args) {
+    context = this
+    timestamp = +new Date()
+    const callNow = immediate && !timeout
+    // 如果延时不存在,重新设定延时
+    if (!timeout) timeout = setTimeout(later, wait)
+    if (callNow) {
+      result = func.apply(context, args)
+      context = args = null
+    }
+
+    return result
+  }
+}
+
+/**
+ * This is just a simple version of deep copy
+ * Has a lot of edge cases bug
+ * If you want to use a perfect deep copy, use lodash's _.cloneDeep
+ * @param {Object} source
+ * @returns {Object}
+ */
+export function deepClone(source) {
+  if (!source && typeof source !== 'object') {
+    throw new Error('error arguments', 'deepClone')
+  }
+  const targetObj = source.constructor === Array ? [] : {}
+  Object.keys(source).forEach(keys => {
+    if (source[keys] && typeof source[keys] === 'object') {
+      targetObj[keys] = deepClone(source[keys])
+    } else {
+      targetObj[keys] = source[keys]
+    }
+  })
+  return targetObj
+}
+
+/**
+ * @param {Array} arr
+ * @returns {Array}
+ */
+export function uniqueArr(arr) {
+  return Array.from(new Set(arr))
+}
+
+/**
+ * @returns {string}
+ */
+export function createUniqueString() {
+  const timestamp = +new Date() + ''
+  const randomNum = parseInt((1 + Math.random()) * 65536) + ''
+  return (+(randomNum + timestamp)).toString(32)
+}
+
+/**
+ * Check if an element has a class
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ * @returns {boolean}
+ */
+export function hasClass(ele, cls) {
+  return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'))
+}
+
+/**
+ * Add class to element
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ */
+export function addClass(ele, cls) {
+  if (!hasClass(ele, cls)) ele.className += ' ' + cls
+}
+
+/**
+ * Remove class from element
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ */
+export function removeClass(ele, cls) {
+  if (hasClass(ele, cls)) {
+    const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)')
+    ele.className = ele.className.replace(reg, ' ')
+  }
+}
diff --git a/frontend/src/utils/open-window.js b/frontend/src/utils/open-window.js
new file mode 100644
index 0000000..1a655d7
--- /dev/null
+++ b/frontend/src/utils/open-window.js
@@ -0,0 +1,25 @@
+/**
+ *Created by PanJiaChen on 16/11/29.
+ * @param {Sting} url
+ * @param {Sting} title
+ * @param {Number} w
+ * @param {Number} h
+ */
+export default function openWindow(url, title, w, h) {
+  // Fixes dual-screen position                            Most browsers       Firefox
+  const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left
+  const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top
+
+  const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width
+  const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height
+
+  const left = ((width / 2) - (w / 2)) + dualScreenLeft
+  const top = ((height / 2) - (h / 2)) + dualScreenTop
+  const newWindow = window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=yes, copyhistory=no, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left)
+
+  // Puts focus on the newWindow
+  if (window.focus) {
+    newWindow.focus()
+  }
+}
+
diff --git a/frontend/src/utils/permission.js b/frontend/src/utils/permission.js
new file mode 100644
index 0000000..8e2bbad
--- /dev/null
+++ b/frontend/src/utils/permission.js
@@ -0,0 +1,21 @@
+import store from '@/store'
+
+/**
+ * @param {Array} value
+ * @returns {Boolean}
+ * @example see @/views/permission/directive.vue
+ */
+export default function checkPermission(value) {
+  if (value && value instanceof Array && value.length > 0) {
+    const roles = store.getters && store.getters.roles
+    const permissionRoles = value
+
+    const hasPermission = roles.some(role => {
+      return permissionRoles.includes(role)
+    })
+    return hasPermission
+  } else {
+    console.error(`need roles! Like v-permission="['admin','editor']"`)
+    return false
+  }
+}
diff --git a/frontend/src/utils/request.js b/frontend/src/utils/request.js
new file mode 100644
index 0000000..2fb95ac
--- /dev/null
+++ b/frontend/src/utils/request.js
@@ -0,0 +1,85 @@
+import axios from 'axios'
+import { MessageBox, Message } from 'element-ui'
+import store from '@/store'
+import { getToken } from '@/utils/auth'
+
+// create an axios instance
+const service = axios.create({
+  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
+  // withCredentials: true, // send cookies when cross-domain requests
+  timeout: 5000 // request timeout
+})
+
+// request interceptor
+service.interceptors.request.use(
+  config => {
+    // do something before request is sent
+
+    if (store.getters.token) {
+      // let each request carry token
+      // ['X-Token'] is a custom headers key
+      // please modify it according to the actual situation
+      config.headers['X-Token'] = getToken()
+    }
+    return config
+  },
+  error => {
+    // do something with request error
+    console.log(error) // for debug
+    return Promise.reject(error)
+  }
+)
+
+// response interceptor
+service.interceptors.response.use(
+  /**
+   * If you want to get http information such as headers or status
+   * Please return  response => response
+  */
+
+  /**
+   * Determine the request status by custom code
+   * Here is just an example
+   * You can also judge the status by HTTP Status Code
+   */
+  response => {
+    const res = response.data
+
+    // if the custom code is not 20000, it is judged as an error.
+    if (res.code !== 20000) {
+      Message({
+        message: res.message || 'Error',
+        type: 'error',
+        duration: 5 * 1000
+      })
+
+      // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
+      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
+        // to re-login
+        MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
+          confirmButtonText: 'Re-Login',
+          cancelButtonText: 'Cancel',
+          type: 'warning'
+        }).then(() => {
+          store.dispatch('user/resetToken').then(() => {
+            location.reload()
+          })
+        })
+      }
+      return Promise.reject(new Error(res.message || 'Error'))
+    } else {
+      return res
+    }
+  },
+  error => {
+    console.log('err' + error) // for debug
+    Message({
+      message: error.message,
+      type: 'error',
+      duration: 5 * 1000
+    })
+    return Promise.reject(error)
+  }
+)
+
+export default service
diff --git a/frontend/src/utils/scroll-to.js b/frontend/src/utils/scroll-to.js
new file mode 100644
index 0000000..c5d8e04
--- /dev/null
+++ b/frontend/src/utils/scroll-to.js
@@ -0,0 +1,58 @@
+Math.easeInOutQuad = function(t, b, c, d) {
+  t /= d / 2
+  if (t < 1) {
+    return c / 2 * t * t + b
+  }
+  t--
+  return -c / 2 * (t * (t - 2) - 1) + b
+}
+
+// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
+var requestAnimFrame = (function() {
+  return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
+})()
+
+/**
+ * Because it's so fucking difficult to detect the scrolling element, just move them all
+ * @param {number} amount
+ */
+function move(amount) {
+  document.documentElement.scrollTop = amount
+  document.body.parentNode.scrollTop = amount
+  document.body.scrollTop = amount
+}
+
+function position() {
+  return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
+}
+
+/**
+ * @param {number} to
+ * @param {number} duration
+ * @param {Function} callback
+ */
+export function scrollTo(to, duration, callback) {
+  const start = position()
+  const change = to - start
+  const increment = 20
+  let currentTime = 0
+  duration = (typeof (duration) === 'undefined') ? 500 : duration
+  var animateScroll = function() {
+    // increment the time
+    currentTime += increment
+    // find the value with the quadratic in-out easing function
+    var val = Math.easeInOutQuad(currentTime, start, change, duration)
+    // move the document.body
+    move(val)
+    // do the animation unless its over
+    if (currentTime < duration) {
+      requestAnimFrame(animateScroll)
+    } else {
+      if (callback && typeof (callback) === 'function') {
+        // the animation is done so lets callback
+        callback()
+      }
+    }
+  }
+  animateScroll()
+}
diff --git a/frontend/src/utils/validate.js b/frontend/src/utils/validate.js
new file mode 100644
index 0000000..6b3ac41
--- /dev/null
+++ b/frontend/src/utils/validate.js
@@ -0,0 +1,87 @@
+/**
+ * Created by PanJiaChen on 16/11/18.
+ */
+
+/**
+ * @param {string} path
+ * @returns {Boolean}
+ */
+export function isExternal(path) {
+  return /^(https?:|mailto:|tel:)/.test(path)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUsername(str) {
+  const valid_map = ['admin', 'editor']
+  return valid_map.indexOf(str.trim()) >= 0
+}
+
+/**
+ * @param {string} url
+ * @returns {Boolean}
+ */
+export function validURL(url) {
+  const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
+  return reg.test(url)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validLowerCase(str) {
+  const reg = /^[a-z]+$/
+  return reg.test(str)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUpperCase(str) {
+  const reg = /^[A-Z]+$/
+  return reg.test(str)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validAlphabets(str) {
+  const reg = /^[A-Za-z]+$/
+  return reg.test(str)
+}
+
+/**
+ * @param {string} email
+ * @returns {Boolean}
+ */
+export function validEmail(email) {
+  const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+  return reg.test(email)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function isString(str) {
+  if (typeof str === 'string' || str instanceof String) {
+    return true
+  }
+  return false
+}
+
+/**
+ * @param {Array} arg
+ * @returns {Boolean}
+ */
+export function isArray(arg) {
+  if (typeof Array.isArray === 'undefined') {
+    return Object.prototype.toString.call(arg) === '[object Array]'
+  }
+  return Array.isArray(arg)
+}
diff --git a/frontend/src/vendor/Export2Excel.js b/frontend/src/vendor/Export2Excel.js
new file mode 100644
index 0000000..d8a2af3
--- /dev/null
+++ b/frontend/src/vendor/Export2Excel.js
@@ -0,0 +1,220 @@
+/* eslint-disable */
+import { saveAs } from 'file-saver'
+import XLSX from 'xlsx'
+
+function generateArray(table) {
+  var out = [];
+  var rows = table.querySelectorAll('tr');
+  var ranges = [];
+  for (var R = 0; R < rows.length; ++R) {
+    var outRow = [];
+    var row = rows[R];
+    var columns = row.querySelectorAll('td');
+    for (var C = 0; C < columns.length; ++C) {
+      var cell = columns[C];
+      var colspan = cell.getAttribute('colspan');
+      var rowspan = cell.getAttribute('rowspan');
+      var cellValue = cell.innerText;
+      if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue;
+
+      //Skip ranges
+      ranges.forEach(function (range) {
+        if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) {
+          for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null);
+        }
+      });
+
+      //Handle Row Span
+      if (rowspan || colspan) {
+        rowspan = rowspan || 1;
+        colspan = colspan || 1;
+        ranges.push({
+          s: {
+            r: R,
+            c: outRow.length
+          },
+          e: {
+            r: R + rowspan - 1,
+            c: outRow.length + colspan - 1
+          }
+        });
+      };
+
+      //Handle Value
+      outRow.push(cellValue !== "" ? cellValue : null);
+
+      //Handle Colspan
+      if (colspan)
+        for (var k = 0; k < colspan - 1; ++k) outRow.push(null);
+    }
+    out.push(outRow);
+  }
+  return [out, ranges];
+};
+
+function datenum(v, date1904) {
+  if (date1904) v += 1462;
+  var epoch = Date.parse(v);
+  return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000);
+}
+
+function sheet_from_array_of_arrays(data, opts) {
+  var ws = {};
+  var range = {
+    s: {
+      c: 10000000,
+      r: 10000000
+    },
+    e: {
+      c: 0,
+      r: 0
+    }
+  };
+  for (var R = 0; R != data.length; ++R) {
+    for (var C = 0; C != data[R].length; ++C) {
+      if (range.s.r > R) range.s.r = R;
+      if (range.s.c > C) range.s.c = C;
+      if (range.e.r < R) range.e.r = R;
+      if (range.e.c < C) range.e.c = C;
+      var cell = {
+        v: data[R][C]
+      };
+      if (cell.v == null) continue;
+      var cell_ref = XLSX.utils.encode_cell({
+        c: C,
+        r: R
+      });
+
+      if (typeof cell.v === 'number') cell.t = 'n';
+      else if (typeof cell.v === 'boolean') cell.t = 'b';
+      else if (cell.v instanceof Date) {
+        cell.t = 'n';
+        cell.z = XLSX.SSF._table[14];
+        cell.v = datenum(cell.v);
+      } else cell.t = 's';
+
+      ws[cell_ref] = cell;
+    }
+  }
+  if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);
+  return ws;
+}
+
+function Workbook() {
+  if (!(this instanceof Workbook)) return new Workbook();
+  this.SheetNames = [];
+  this.Sheets = {};
+}
+
+function s2ab(s) {
+  var buf = new ArrayBuffer(s.length);
+  var view = new Uint8Array(buf);
+  for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
+  return buf;
+}
+
+export function export_table_to_excel(id) {
+  var theTable = document.getElementById(id);
+  var oo = generateArray(theTable);
+  var ranges = oo[1];
+
+  /* original data */
+  var data = oo[0];
+  var ws_name = "SheetJS";
+
+  var wb = new Workbook(),
+    ws = sheet_from_array_of_arrays(data);
+
+  /* add ranges to worksheet */
+  // ws['!cols'] = ['apple', 'banan'];
+  ws['!merges'] = ranges;
+
+  /* add worksheet to workbook */
+  wb.SheetNames.push(ws_name);
+  wb.Sheets[ws_name] = ws;
+
+  var wbout = XLSX.write(wb, {
+    bookType: 'xlsx',
+    bookSST: false,
+    type: 'binary'
+  });
+
+  saveAs(new Blob([s2ab(wbout)], {
+    type: "application/octet-stream"
+  }), "test.xlsx")
+}
+
+export function export_json_to_excel({
+  multiHeader = [],
+  header,
+  data,
+  filename,
+  merges = [],
+  autoWidth = true,
+  bookType = 'xlsx'
+} = {}) {
+  /* original data */
+  filename = filename || 'excel-list'
+  data = [...data]
+  data.unshift(header);
+
+  for (let i = multiHeader.length - 1; i > -1; i--) {
+    data.unshift(multiHeader[i])
+  }
+
+  var ws_name = "SheetJS";
+  var wb = new Workbook(),
+    ws = sheet_from_array_of_arrays(data);
+
+  if (merges.length > 0) {
+    if (!ws['!merges']) ws['!merges'] = [];
+    merges.forEach(item => {
+      ws['!merges'].push(XLSX.utils.decode_range(item))
+    })
+  }
+
+  if (autoWidth) {
+    /*设置worksheet每列的最大宽度*/
+    const colWidth = data.map(row => row.map(val => {
+      /*先判断是否为null/undefined*/
+      if (val == null) {
+        return {
+          'wch': 10
+        };
+      }
+      /*再判断是否为中文*/
+      else if (val.toString().charCodeAt(0) > 255) {
+        return {
+          'wch': val.toString().length * 2
+        };
+      } else {
+        return {
+          'wch': val.toString().length
+        };
+      }
+    }))
+    /*以第一行为初始值*/
+    let result = colWidth[0];
+    for (let i = 1; i < colWidth.length; i++) {
+      for (let j = 0; j < colWidth[i].length; j++) {
+        if (result[j]['wch'] < colWidth[i][j]['wch']) {
+          result[j]['wch'] = colWidth[i][j]['wch'];
+        }
+      }
+    }
+    ws['!cols'] = result;
+  }
+
+  /* add worksheet to workbook */
+  wb.SheetNames.push(ws_name);
+  wb.Sheets[ws_name] = ws;
+
+  var wbout = XLSX.write(wb, {
+    bookType: bookType,
+    bookSST: false,
+    type: 'binary'
+  });
+  saveAs(new Blob([s2ab(wbout)], {
+    type: "application/octet-stream"
+  }), `${filename}.${bookType}`);
+}
diff --git a/frontend/src/vendor/Export2Zip.js b/frontend/src/vendor/Export2Zip.js
new file mode 100644
index 0000000..db70707
--- /dev/null
+++ b/frontend/src/vendor/Export2Zip.js
@@ -0,0 +1,24 @@
+/* eslint-disable */
+import { saveAs } from 'file-saver'
+import JSZip from 'jszip'
+
+export function export_txt_to_zip(th, jsonData, txtName, zipName) {
+  const zip = new JSZip()
+  const txt_name = txtName || 'file'
+  const zip_name = zipName || 'file'
+  const data = jsonData
+  let txtData = `${th}\r\n`
+  data.forEach((row) => {
+    let tempStr = ''
+    tempStr = row.toString()
+    txtData += `${tempStr}\r\n`
+  })
+  zip.file(`${txt_name}.txt`, txtData)
+  zip.generateAsync({
+    type: "blob"
+  }).then((blob) => {
+    saveAs(blob, `${zip_name}.zip`)
+  }, (err) => {
+    alert('导出失败')
+  })
+}
diff --git a/frontend/src/views/charts/keyboard.vue b/frontend/src/views/charts/keyboard.vue
new file mode 100644
index 0000000..917f8ee
--- /dev/null
+++ b/frontend/src/views/charts/keyboard.vue
@@ -0,0 +1,23 @@
+<template>
+  <div class="chart-container">
+    <chart height="100%" width="100%" />
+  </div>
+</template>
+
+<script>
+import Chart from '@/components/Charts/Keyboard'
+
+export default {
+  name: 'KeyboardChart',
+  components: { Chart }
+}
+</script>
+
+<style scoped>
+.chart-container{
+  position: relative;
+  width: 100%;
+  height: calc(100vh - 84px);
+}
+</style>
+
diff --git a/frontend/src/views/charts/line.vue b/frontend/src/views/charts/line.vue
new file mode 100644
index 0000000..fea1497
--- /dev/null
+++ b/frontend/src/views/charts/line.vue
@@ -0,0 +1,23 @@
+<template>
+  <div class="chart-container">
+    <chart height="100%" width="100%" />
+  </div>
+</template>
+
+<script>
+import Chart from '@/components/Charts/LineMarker'
+
+export default {
+  name: 'LineChart',
+  components: { Chart }
+}
+</script>
+
+<style scoped>
+.chart-container{
+  position: relative;
+  width: 100%;
+  height: calc(100vh - 84px);
+}
+</style>
+
diff --git a/frontend/src/views/charts/mix-chart.vue b/frontend/src/views/charts/mix-chart.vue
new file mode 100644
index 0000000..c57db75
--- /dev/null
+++ b/frontend/src/views/charts/mix-chart.vue
@@ -0,0 +1,23 @@
+<template>
+  <div class="chart-container">
+    <chart height="100%" width="100%" />
+  </div>
+</template>
+
+<script>
+import Chart from '@/components/Charts/MixChart'
+
+export default {
+  name: 'MixChart',
+  components: { Chart }
+}
+</script>
+
+<style scoped>
+.chart-container{
+  position: relative;
+  width: 100%;
+  height: calc(100vh - 84px);
+}
+</style>
+
diff --git a/frontend/src/views/clipboard/index.vue b/frontend/src/views/clipboard/index.vue
new file mode 100644
index 0000000..4a6bdd1
--- /dev/null
+++ b/frontend/src/views/clipboard/index.vue
@@ -0,0 +1,49 @@
+<template>
+  <div class="app-container">
+    <el-tabs v-model="activeName">
+      <el-tab-pane label="use clipboard  directly" name="directly">
+        <el-input v-model="inputData" placeholder="Please input" style="width:400px;max-width:100%;" />
+        <el-button type="primary" icon="el-icon-document" @click="handleCopy(inputData,$event)">
+          copy
+        </el-button>
+      </el-tab-pane>
+      <el-tab-pane label="use clipboard by v-directive" name="v-directive">
+        <el-input v-model="inputData" placeholder="Please input" style="width:400px;max-width:100%;" />
+        <el-button v-clipboard:copy="inputData" v-clipboard:success="clipboardSuccess" type="primary" icon="el-icon-document">
+          copy
+        </el-button>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script>
+import clip from '@/utils/clipboard' // use clipboard directly
+import clipboard from '@/directive/clipboard/index.js' // use clipboard by v-directive
+
+export default {
+  name: 'ClipboardDemo',
+  directives: {
+    clipboard
+  },
+  data() {
+    return {
+      activeName: 'directly',
+      inputData: 'https://github.com/PanJiaChen/vue-element-admin'
+    }
+  },
+  methods: {
+    handleCopy(text, event) {
+      clip(text, event)
+    },
+    clipboardSuccess() {
+      this.$message({
+        message: 'Copy successfully',
+        type: 'success',
+        duration: 1500
+      })
+    }
+  }
+}
+</script>
+
diff --git a/frontend/src/views/components-demo/avatar-upload.vue b/frontend/src/views/components-demo/avatar-upload.vue
new file mode 100644
index 0000000..5cb20a5
--- /dev/null
+++ b/frontend/src/views/components-demo/avatar-upload.vue
@@ -0,0 +1,61 @@
+<template>
+  <div class="components-container">
+    <aside>This is based on
+      <a class="link-type" href="//github.com/dai-siki/vue-image-crop-upload"> vue-image-crop-upload</a>.
+      Since I was using only the vue@1 version, and it is not compatible with mockjs at the moment, I modified it myself, and if you are going to use it, it is better to use official version.
+    </aside>
+
+    <pan-thumb :image="image" />
+
+    <el-button type="primary" icon="el-icon-upload" style="position: absolute;bottom: 15px;margin-left: 40px;" @click="imagecropperShow=true">
+      Change Avatar
+    </el-button>
+
+    <image-cropper
+      v-show="imagecropperShow"
+      :key="imagecropperKey"
+      :width="300"
+      :height="300"
+      url="https://httpbin.org/post"
+      lang-type="en"
+      @close="close"
+      @crop-upload-success="cropSuccess"
+    />
+  </div>
+</template>
+
+<script>
+import ImageCropper from '@/components/ImageCropper'
+import PanThumb from '@/components/PanThumb'
+
+export default {
+  name: 'AvatarUploadDemo',
+  components: { ImageCropper, PanThumb },
+  data() {
+    return {
+      imagecropperShow: false,
+      imagecropperKey: 0,
+      image: 'https://wpimg.wallstcn.com/577965b9-bb9e-4e02-9f0c-095b41417191'
+    }
+  },
+  methods: {
+    cropSuccess(resData) {
+      this.imagecropperShow = false
+      this.imagecropperKey = this.imagecropperKey + 1
+      this.image = resData.files.avatar
+    },
+    close() {
+      this.imagecropperShow = false
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .avatar{
+    width: 200px;
+    height: 200px;
+    border-radius: 50%;
+  }
+</style>
+
diff --git a/frontend/src/views/components-demo/back-to-top.vue b/frontend/src/views/components-demo/back-to-top.vue
new file mode 100644
index 0000000..df5370b
--- /dev/null
+++ b/frontend/src/views/components-demo/back-to-top.vue
@@ -0,0 +1,154 @@
+<template>
+  <div class="components-container">
+    <aside>
+      When the page is scrolled to the specified position, the Back to Top button appears in the lower right corner
+    </aside>
+    <aside>
+      You can customize the style of the button, show / hide, height of appearance, height of the return. If you need a text prompt, you can use element-ui el-tooltip elements externally
+    </aside>
+    <div class="placeholder-container">
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+    </div>
+    <!-- you can add element-ui's tooltip -->
+    <el-tooltip placement="top" content="tooltip">
+      <back-to-top :custom-style="myBackToTopStyle" :visibility-height="300" :back-position="50" transition-name="fade" />
+    </el-tooltip>
+  </div>
+</template>
+
+<script>
+import BackToTop from '@/components/BackToTop'
+
+export default {
+  name: 'BackToTopDemo',
+  components: { BackToTop },
+  data() {
+    return {
+      // customizable button style, show/hide critical point, return position
+      myBackToTopStyle: {
+        right: '50px',
+        bottom: '50px',
+        width: '40px',
+        height: '40px',
+        'border-radius': '4px',
+        'line-height': '45px', // 请保持与高度一致以垂直居中 Please keep consistent with height to center vertically
+        background: '#e7eaf1'// 按钮的背景颜色 The background color of the button
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.placeholder-container div {
+  margin: 10px;
+}
+</style>
diff --git a/frontend/src/views/components-demo/count-to.vue b/frontend/src/views/components-demo/count-to.vue
new file mode 100644
index 0000000..70681df
--- /dev/null
+++ b/frontend/src/views/components-demo/count-to.vue
@@ -0,0 +1,218 @@
+<template>
+  <div class="components-container">
+    <aside>
+      <a href="https://github.com/PanJiaChen/vue-countTo" target="_blank">countTo-component</a>
+    </aside>
+    <count-to
+      ref="example"
+      :start-val="_startVal"
+      :end-val="_endVal"
+      :duration="_duration"
+      :decimals="_decimals"
+      :separator="_separator"
+      :prefix="_prefix"
+      :suffix="_suffix"
+      :autoplay="false"
+      class="example"
+    />
+    <div style="margin-left: 25%;margin-top: 40px;">
+      <label class="label" for="startValInput">startVal:
+        <input v-model.number="setStartVal" type="number" name="startValInput">
+      </label>
+      <label class="label" for="endValInput">endVal:
+        <input v-model.number="setEndVal" type="number" name="endVaInput">
+      </label>
+      <label class="label" for="durationInput">duration:
+        <input v-model.number="setDuration" type="number" name="durationInput">
+      </label>
+      <div class="startBtn example-btn" @click="start">
+        Start
+      </div>
+      <div class="pause-resume-btn example-btn" @click="pauseResume">
+        pause/resume
+      </div>
+      <br>
+      <label class="label" for="decimalsInput">decimals:
+        <input v-model.number="setDecimals" type="number" name="decimalsInput">
+      </label>
+      <label class="label" for="separatorInput">separator:
+        <input v-model="setSeparator" name="separatorInput">
+      </label>
+      <label class="label" for="prefixInput">prefix:
+        <input v-model="setPrefix" name="prefixInput">
+      </label>
+      <label class="label" for="suffixInput">suffix:
+        <input v-model="setSuffix" name="suffixInput">
+      </label>
+    </div>
+    <aside>&lt;count-to :start-val=&#x27;{{ _startVal }}&#x27; :end-val=&#x27;{{ _endVal }}&#x27; :duration=&#x27;{{ _duration }}&#x27;
+      :decimals=&#x27;{{ _decimals }}&#x27; :separator=&#x27;{{ _separator }}&#x27; :prefix=&#x27;{{ _prefix }}&#x27; :suffix=&#x27;{{ _suffix }}&#x27;
+      :autoplay=false&gt;</aside>
+  </div>
+</template>
+
+<script>
+import countTo from 'vue-count-to'
+
+export default {
+  name: 'CountToDemo',
+  components: { countTo },
+  data() {
+    return {
+      setStartVal: 0,
+      setEndVal: 2017,
+      setDuration: 4000,
+      setDecimals: 0,
+      setSeparator: ',',
+      setSuffix: ' rmb',
+      setPrefix: '¥ '
+    }
+  },
+  computed: {
+    _startVal() {
+      if (this.setStartVal) {
+        return this.setStartVal
+      } else {
+        return 0
+      }
+    },
+    _endVal() {
+      if (this.setEndVal) {
+        return this.setEndVal
+      } else {
+        return 0
+      }
+    },
+    _duration() {
+      if (this.setDuration) {
+        return this.setDuration
+      } else {
+        return 100
+      }
+    },
+    _decimals() {
+      if (this.setDecimals) {
+        if (this.setDecimals < 0 || this.setDecimals > 20) {
+          alert('digits argument must be between 0 and 20')
+          return 0
+        }
+        return this.setDecimals
+      } else {
+        return 0
+      }
+    },
+    _separator() {
+      return this.setSeparator
+    },
+    _suffix() {
+      return this.setSuffix
+    },
+    _prefix() {
+      return this.setPrefix
+    }
+  },
+  methods: {
+    start() {
+      this.$refs.example.start()
+    },
+    pauseResume() {
+      this.$refs.example.pauseResume()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.example-btn {
+  display: inline-block;
+  margin-bottom: 0;
+  font-weight: 500;
+  text-align: center;
+  -ms-touch-action: manipulation;
+  touch-action: manipulation;
+  cursor: pointer;
+  background-image: none;
+  border: 1px solid transparent;
+  white-space: nowrap;
+  line-height: 1.5;
+  padding: 4px 15px;
+  font-size: 12px;
+  border-radius: 4px;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+  -webkit-transition: all .3s cubic-bezier(.645, .045, .355, 1);
+  transition: all .3s cubic-bezier(.645, .045, .355, 1);
+  position: relative;
+  color: rgba(0, 0, 0, .65);
+  background-color: #fff;
+  border-color: #d9d9d9;
+}
+
+.example-btn:hover {
+  color: #4AB7BD;
+  background-color: #fff;
+  border-color: #4AB7BD;
+}
+.example {
+  font-size: 50px;
+  color: #F6416C;
+  display: block;
+  margin: 10px 0;
+  text-align: center;
+  font-size: 80px;
+  font-weight: 500;
+}
+
+.label {
+  color: #2f4f4f;
+  font-size: 16px;
+  display: inline-block;
+  margin: 15px 30px 15px 0;
+}
+
+input {
+  position: relative;
+  display: inline-block;
+  padding: 4px 7px;
+  width: 70px;
+  height: 28px;
+  cursor: text;
+  font-size: 12px;
+  line-height: 1.5;
+  color: rgba(0, 0, 0, .65);
+  background-color: #fff;
+  background-image: none;
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+  -webkit-transition: all .3s;
+  transition: all .3s;
+}
+
+.startBtn {
+  margin-left: 20px;
+  font-size: 20px;
+  color: #30B08F;
+  background-color: #fff;
+}
+
+.startBtn:hover {
+  background-color: #30B08F;
+  color: #fff;
+  border-color: #30B08F;
+}
+
+.pause-resume-btn {
+  font-size: 20px;
+  color: #E65D6E;
+  background-color: #fff;
+}
+
+.pause-resume-btn:hover {
+  background-color: #E65D6E;
+  color: #fff;
+  border-color: #E65D6E;
+}
+</style>
+
diff --git a/frontend/src/views/components-demo/dnd-list.vue b/frontend/src/views/components-demo/dnd-list.vue
new file mode 100644
index 0000000..e299fa6
--- /dev/null
+++ b/frontend/src/views/components-demo/dnd-list.vue
@@ -0,0 +1,39 @@
+<template>
+  <div class="components-container">
+    <aside>drag-list base on
+      <a href="https://github.com/SortableJS/Vue.Draggable" target="_blank">Vue.Draggable</a>
+    </aside>
+    <div class="editor-container">
+      <dnd-list :list1="list1" :list2="list2" list1-title="List" list2-title="Article pool" />
+    </div>
+  </div>
+</template>
+
+<script>
+import DndList from '@/components/DndList'
+import { fetchList } from '@/api/article'
+
+export default {
+  name: 'DndListDemo',
+  components: { DndList },
+  data() {
+    return {
+      list1: [],
+      list2: []
+    }
+  },
+  created() {
+    this.getData()
+  },
+  methods: {
+    getData() {
+      this.listLoading = true
+      fetchList().then(response => {
+        this.list1 = response.data.items.splice(0, 5)
+        this.list2 = response.data.items
+      })
+    }
+  }
+}
+</script>
+
diff --git a/frontend/src/views/components-demo/drag-dialog.vue b/frontend/src/views/components-demo/drag-dialog.vue
new file mode 100644
index 0000000..c815b28
--- /dev/null
+++ b/frontend/src/views/components-demo/drag-dialog.vue
@@ -0,0 +1,61 @@
+<template>
+  <div class="components-container">
+    <el-button type="primary" @click="dialogTableVisible = true">
+      open a Drag Dialog
+    </el-button>
+    <el-dialog v-el-drag-dialog :visible.sync="dialogTableVisible" title="Shipping address" @dragDialog="handleDrag">
+      <el-select ref="select" v-model="value" placeholder="请选择">
+        <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
+      </el-select>
+      <el-table :data="gridData">
+        <el-table-column property="date" label="Date" width="150" />
+        <el-table-column property="name" label="Name" width="200" />
+        <el-table-column property="address" label="Address" />
+      </el-table>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import elDragDialog from '@/directive/el-drag-dialog' // base on element-ui
+
+export default {
+  name: 'DragDialogDemo',
+  directives: { elDragDialog },
+  data() {
+    return {
+      dialogTableVisible: false,
+      options: [
+        { value: '选项1', label: '黄金糕' },
+        { value: '选项2', label: '双皮奶' },
+        { value: '选项3', label: '蚵仔煎' },
+        { value: '选项4', label: '龙须面' }
+      ],
+      value: '',
+      gridData: [{
+        date: '2016-05-02',
+        name: 'John Smith',
+        address: 'No.1518,  Jinshajiang Road, Putuo District'
+      }, {
+        date: '2016-05-04',
+        name: 'John Smith',
+        address: 'No.1518,  Jinshajiang Road, Putuo District'
+      }, {
+        date: '2016-05-01',
+        name: 'John Smith',
+        address: 'No.1518,  Jinshajiang Road, Putuo District'
+      }, {
+        date: '2016-05-03',
+        name: 'John Smith',
+        address: 'No.1518,  Jinshajiang Road, Putuo District'
+      }]
+    }
+  },
+  methods: {
+    // v-el-drag-dialog onDrag callback function
+    handleDrag() {
+      this.$refs.select.blur()
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/components-demo/drag-kanban.vue b/frontend/src/views/components-demo/drag-kanban.vue
new file mode 100644
index 0000000..943be45
--- /dev/null
+++ b/frontend/src/views/components-demo/drag-kanban.vue
@@ -0,0 +1,66 @@
+<template>
+  <div class="components-container board">
+    <Kanban :key="1" :list="list1" :group="group" class="kanban todo" header-text="Todo" />
+    <Kanban :key="2" :list="list2" :group="group" class="kanban working" header-text="Working" />
+    <Kanban :key="3" :list="list3" :group="group" class="kanban done" header-text="Done" />
+  </div>
+</template>
+<script>
+import Kanban from '@/components/Kanban'
+
+export default {
+  name: 'DragKanbanDemo',
+  components: {
+    Kanban
+  },
+  data() {
+    return {
+      group: 'mission',
+      list1: [
+        { name: 'Mission', id: 1 },
+        { name: 'Mission', id: 2 },
+        { name: 'Mission', id: 3 },
+        { name: 'Mission', id: 4 }
+      ],
+      list2: [
+        { name: 'Mission', id: 5 },
+        { name: 'Mission', id: 6 },
+        { name: 'Mission', id: 7 }
+      ],
+      list3: [
+        { name: 'Mission', id: 8 },
+        { name: 'Mission', id: 9 },
+        { name: 'Mission', id: 10 }
+      ]
+    }
+  }
+}
+</script>
+<style lang="scss">
+.board {
+  width: 1000px;
+  margin-left: 20px;
+  display: flex;
+  justify-content: space-around;
+  flex-direction: row;
+  align-items: flex-start;
+}
+.kanban {
+  &.todo {
+    .board-column-header {
+      background: #4A9FF9;
+    }
+  }
+  &.working {
+    .board-column-header {
+      background: #f9944a;
+    }
+  }
+  &.done {
+    .board-column-header {
+      background: #2ac06d;
+    }
+  }
+}
+</style>
+
diff --git a/frontend/src/views/components-demo/drag-select.vue b/frontend/src/views/components-demo/drag-select.vue
new file mode 100644
index 0000000..905ecb9
--- /dev/null
+++ b/frontend/src/views/components-demo/drag-select.vue
@@ -0,0 +1,43 @@
+<template>
+  <div class="components-container">
+    <el-drag-select v-model="value" style="width:500px;" multiple placeholder="请选择">
+      <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
+    </el-drag-select>
+
+    <div style="margin-top:30px;">
+      <el-tag v-for="item of value" :key="item" style="margin-right:15px;">
+        {{ item }}
+      </el-tag>
+    </div>
+  </div>
+</template>
+
+<script>
+import ElDragSelect from '@/components/DragSelect' // base on element-ui
+
+export default {
+  name: 'DragSelectDemo',
+  components: { ElDragSelect },
+  data() {
+    return {
+      value: ['Apple', 'Banana', 'Orange'],
+      options: [{
+        value: 'Apple',
+        label: 'Apple'
+      }, {
+        value: 'Banana',
+        label: 'Banana'
+      }, {
+        value: 'Orange',
+        label: 'Orange'
+      }, {
+        value: 'Pear',
+        label: 'Pear'
+      }, {
+        value: 'Strawberry',
+        label: 'Strawberry'
+      }]
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/components-demo/dropzone.vue b/frontend/src/views/components-demo/dropzone.vue
new file mode 100644
index 0000000..a8c1040
--- /dev/null
+++ b/frontend/src/views/components-demo/dropzone.vue
@@ -0,0 +1,31 @@
+<template>
+  <div class="components-container">
+    <aside>
+      Based on <a class="link-type" href="https://github.com/rowanwins/vue-dropzone"> dropzone </a>.
+      Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/Dropzone.
+    </aside>
+    <div class="editor-container">
+      <dropzone id="myVueDropzone" url="https://httpbin.org/post" @dropzone-removedFile="dropzoneR" @dropzone-success="dropzoneS" />
+    </div>
+  </div>
+</template>
+
+<script>
+import Dropzone from '@/components/Dropzone'
+
+export default {
+  name: 'DropzoneDemo',
+  components: { Dropzone },
+  methods: {
+    dropzoneS(file) {
+      console.log(file)
+      this.$message({ message: 'Upload success', type: 'success' })
+    },
+    dropzoneR(file) {
+      console.log(file)
+      this.$message({ message: 'Delete success', type: 'success' })
+    }
+  }
+}
+</script>
+
diff --git a/frontend/src/views/components-demo/json-editor.vue b/frontend/src/views/components-demo/json-editor.vue
new file mode 100644
index 0000000..85bf383
--- /dev/null
+++ b/frontend/src/views/components-demo/json-editor.vue
@@ -0,0 +1,36 @@
+<template>
+  <div class="components-container">
+    <aside>Json-Editor is base on <a href="https://github.com/codemirror/CodeMirror" target="_blank">CodeMirrorr</a>. Lint
+      base on <a
+        href="https://github.com/codemirror/CodeMirror/blob/master/addon/lint/json-lint.js"
+        target="_blank"
+      >json-lint</a>.</aside>
+    <div class="editor-container">
+      <json-editor ref="jsonEditor" v-model="value" />
+    </div>
+  </div>
+</template>
+
+<script>
+import JsonEditor from '@/components/JsonEditor'
+
+const jsonData = '[{"items":[{"market_type":"forexdata","symbol":"XAUUSD"},{"market_type":"forexdata","symbol":"UKOIL"},{"market_type":"forexdata","symbol":"CORN"}],"name":""},{"items":[{"market_type":"forexdata","symbol":"XAUUSD"},{"market_type":"forexdata","symbol":"XAGUSD"},{"market_type":"forexdata","symbol":"AUTD"},{"market_type":"forexdata","symbol":"AGTD"}],"name":"贵金属"},{"items":[{"market_type":"forexdata","symbol":"CORN"},{"market_type":"forexdata","symbol":"WHEAT"},{"market_type":"forexdata","symbol":"SOYBEAN"},{"market_type":"forexdata","symbol":"SUGAR"}],"name":"农产品"},{"items":[{"market_type":"forexdata","symbol":"UKOIL"},{"market_type":"forexdata","symbol":"USOIL"},{"market_type":"forexdata","symbol":"NGAS"}],"name":"能源化工"}]'
+
+export default {
+  name: 'JsonEditorDemo',
+  components: { JsonEditor },
+  data() {
+    return {
+      value: JSON.parse(jsonData)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.editor-container{
+  position: relative;
+  height: 100%;
+}
+</style>
+
diff --git a/frontend/src/views/components-demo/markdown.vue b/frontend/src/views/components-demo/markdown.vue
new file mode 100644
index 0000000..25cf3e3
--- /dev/null
+++ b/frontend/src/views/components-demo/markdown.vue
@@ -0,0 +1,101 @@
+<template>
+  <div class="components-container">
+    <aside>Markdown is based on
+      <a href="https://github.com/nhnent/tui.editor" target="_blank">tui.editor</a> ,simply wrapped with Vue.
+      <a
+        target="_blank"
+        href="https://panjiachen.github.io/vue-element-admin-site/feature/component/markdown-editor.html"
+      >
+        Documentation </a>
+    </aside>
+
+    <div class="editor-container">
+      <el-tag class="tag-title">
+        Basic:
+      </el-tag>
+      <markdown-editor v-model="content1" height="300px" />
+    </div>
+
+    <div class="editor-container">
+      <el-tag class="tag-title">
+        Markdown Mode:
+      </el-tag>
+      <markdown-editor ref="markdownEditor" v-model="content2" :options="{hideModeSwitch:true,previewStyle:'tab'}" height="200px" />
+    </div>
+
+    <div class="editor-container">
+      <el-tag class="tag-title">
+        Customize Toolbar:
+      </el-tag>
+      <markdown-editor v-model="content3" :options="{ toolbarItems: ['heading','bold','italic']}" />
+    </div>
+
+    <div class="editor-container">
+      <el-tag class="tag-title">
+        I18n:
+      </el-tag>
+      <el-alert
+        :closable="false"
+        title="You can change the language of the admin system to see the effect"
+        type="success"
+      />
+      <markdown-editor ref="markdownEditor" v-model="content4" :language="language" height="300px" />
+    </div>
+
+    <el-button style="margin-top:80px;" type="primary" icon="el-icon-document" @click="getHtml">
+      Get HTML
+    </el-button>
+    <div v-html="html" />
+  </div>
+</template>
+
+<script>
+import MarkdownEditor from '@/components/MarkdownEditor'
+
+const content = `
+**This is test**
+
+* vue
+* element
+* webpack
+
+`
+export default {
+  name: 'MarkdownDemo',
+  components: { MarkdownEditor },
+  data() {
+    return {
+      content1: content,
+      content2: content,
+      content3: content,
+      content4: content,
+      html: '',
+      languageTypeList: {
+        'en': 'en_US',
+        'zh': 'zh_CN',
+        'es': 'es_ES'
+      }
+    }
+  },
+  computed: {
+    language() {
+      return this.languageTypeList['en']
+    }
+  },
+  methods: {
+    getHtml() {
+      this.html = this.$refs.markdownEditor.getHtml()
+      console.log(this.html)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.editor-container{
+  margin-bottom: 30px;
+}
+.tag-title{
+  margin-bottom: 5px;
+}
+</style>
diff --git a/frontend/src/views/components-demo/mixin.vue b/frontend/src/views/components-demo/mixin.vue
new file mode 100644
index 0000000..425cf4f
--- /dev/null
+++ b/frontend/src/views/components-demo/mixin.vue
@@ -0,0 +1,169 @@
+<template>
+  <div class="mixin-components-container">
+    <el-row>
+      <el-card class="box-card">
+        <div slot="header" class="clearfix">
+          <span>Buttons</span>
+        </div>
+        <div style="margin-bottom:50px;">
+          <el-col :span="4" class="text-center">
+            <router-link class="pan-btn blue-btn" to="/documentation/index">
+              Documentation
+            </router-link>
+          </el-col>
+          <el-col :span="4" class="text-center">
+            <router-link class="pan-btn light-blue-btn" to="/icon/index">
+              Icons
+            </router-link>
+          </el-col>
+          <el-col :span="4" class="text-center">
+            <router-link class="pan-btn pink-btn" to="/excel/export-excel">
+              Excel
+            </router-link>
+          </el-col>
+          <el-col :span="4" class="text-center">
+            <router-link class="pan-btn green-btn" to="/table/complex-table">
+              Table
+            </router-link>
+          </el-col>
+          <el-col :span="4" class="text-center">
+            <router-link class="pan-btn tiffany-btn" to="/example/create">
+              Form
+            </router-link>
+          </el-col>
+          <el-col :span="4" class="text-center">
+            <router-link class="pan-btn yellow-btn" to="/theme/index">
+              Theme
+            </router-link>
+          </el-col>
+        </div>
+      </el-card>
+    </el-row>
+
+    <el-row :gutter="20" style="margin-top:50px;">
+      <el-col :span="6">
+        <el-card class="box-card">
+          <div slot="header" class="clearfix">
+            <span>Material Design 的input</span>
+          </div>
+          <div style="height:100px;">
+            <el-form :model="demo" :rules="demoRules">
+              <el-form-item prop="title">
+                <md-input v-model="demo.title" icon="el-icon-search" name="title" placeholder="输入标题">
+                  标题
+                </md-input>
+              </el-form-item>
+            </el-form>
+          </div>
+        </el-card>
+      </el-col>
+
+      <el-col :span="6">
+        <el-card class="box-card">
+          <div slot="header" class="clearfix">
+            <span>图片hover效果</span>
+          </div>
+          <div class="component-item">
+            <pan-thumb width="100px" height="100px" image="https://wpimg.wallstcn.com/577965b9-bb9e-4e02-9f0c-095b41417191">
+              vue-element-admin
+            </pan-thumb>
+          </div>
+        </el-card>
+      </el-col>
+
+      <el-col :span="6">
+        <el-card class="box-card">
+          <div slot="header" class="clearfix">
+            <span>水波纹 waves v-directive</span>
+          </div>
+          <div class="component-item">
+            <el-button v-waves type="primary">
+              水波纹效果
+            </el-button>
+          </div>
+        </el-card>
+      </el-col>
+
+      <el-col :span="6">
+        <el-card class="box-card">
+          <div slot="header" class="clearfix">
+            <span>hover text</span>
+          </div>
+          <div class="component-item">
+            <mallki class-name="mallki-text" text="vue-element-admin" />
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="20" style="margin-top:50px;">
+      <el-col :span="8">
+        <el-card class="box-card">
+          <div slot="header" class="clearfix">
+            <span>Share</span>
+          </div>
+          <div class="component-item" style="height:420px;">
+            <dropdown-menu :items="articleList" style="margin:0 auto;" title="系列文章" />
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import PanThumb from '@/components/PanThumb'
+import MdInput from '@/components/MDinput'
+import Mallki from '@/components/TextHoverEffect/Mallki'
+import DropdownMenu from '@/components/Share/DropdownMenu'
+import waves from '@/directive/waves/index.js' // 水波纹指令
+
+export default {
+  name: 'ComponentMixinDemo',
+  components: {
+    PanThumb,
+    MdInput,
+    Mallki,
+    DropdownMenu
+  },
+  directives: {
+    waves
+  },
+  data() {
+    const validate = (rule, value, callback) => {
+      if (value.length !== 6) {
+        callback(new Error('请输入六个字符'))
+      } else {
+        callback()
+      }
+    }
+    return {
+      demo: {
+        title: ''
+      },
+      demoRules: {
+        title: [{ required: true, trigger: 'change', validator: validate }]
+      },
+      articleList: [
+        { title: '基础篇', href: 'https://juejin.im/post/59097cd7a22b9d0065fb61d2' },
+        { title: '登录权限篇', href: 'https://juejin.im/post/591aa14f570c35006961acac' },
+        { title: '实战篇', href: 'https://juejin.im/post/593121aa0ce4630057f70d35' },
+        { title: 'vue-admin-template 篇', href: 'https://juejin.im/post/595b4d776fb9a06bbe7dba56' },
+        { title: 'v4.0 篇', href: 'https://juejin.im/post/5c92ff94f265da6128275a85' },
+        { title: '优雅的使用 icon', href: 'https://juejin.im/post/59bb864b5188257e7a427c09' }
+      ]
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mixin-components-container {
+  background-color: #f0f2f5;
+  padding: 30px;
+  min-height: calc(100vh - 84px);
+}
+.component-item{
+  min-height: 100px;
+}
+</style>
diff --git a/frontend/src/views/components-demo/split-pane.vue b/frontend/src/views/components-demo/split-pane.vue
new file mode 100644
index 0000000..7dba353
--- /dev/null
+++ b/frontend/src/views/components-demo/split-pane.vue
@@ -0,0 +1,67 @@
+<template>
+  <div class="components-container">
+    <aside><strong>SplitPane</strong> If you've used
+      <a href="https://codepen.io/" target="_blank"> codepen</a>,
+      <a href="https://jsfiddle.net/" target="_blank"> jsfiddle </a>will not be unfamiliar.
+      <a href="https://github.com/PanJiaChen/vue-split-pane" target="_blank"> Github repository</a>
+    </aside>
+    <split-pane split="vertical" @resize="resize">
+      <template slot="paneL">
+        <div class="left-container" />
+      </template>
+      <template slot="paneR">
+        <split-pane split="horizontal">
+          <template slot="paneL">
+            <div class="top-container" />
+          </template>
+          <template slot="paneR">
+            <div class="bottom-container" />
+          </template>
+        </split-pane>
+      </template>
+    </split-pane>
+  </div>
+</template>
+
+<script>
+import splitPane from 'vue-splitpane'
+
+export default {
+  name: 'SplitpaneDemo',
+  components: { splitPane },
+  methods: {
+    resize() {
+      console.log('resize')
+    }
+  }
+}
+</script>
+
+<style  scoped>
+  .components-container {
+    position: relative;
+    height: 100vh;
+  }
+
+  .left-container {
+    background-color: #F38181;
+    height: 100%;
+  }
+
+  .right-container {
+    background-color: #FCE38A;
+    height: 200px;
+  }
+
+  .top-container {
+    background-color: #FCE38A;
+    width: 100%;
+    height: 100%;
+  }
+
+  .bottom-container {
+    width: 100%;
+    background-color: #95E1D3;
+    height: 100%;
+  }
+</style>
diff --git a/frontend/src/views/components-demo/sticky.vue b/frontend/src/views/components-demo/sticky.vue
new file mode 100644
index 0000000..f01d088
--- /dev/null
+++ b/frontend/src/views/components-demo/sticky.vue
@@ -0,0 +1,135 @@
+<template>
+  <div>
+    <sticky :z-index="10" class-name="sub-navbar">
+      <el-dropdown trigger="click">
+        <el-button plain>
+          Platform<i class="el-icon-caret-bottom el-icon--right" />
+        </el-button>
+        <el-dropdown-menu slot="dropdown" class="no-border">
+          <el-checkbox-group v-model="platforms" style="padding: 5px 15px;">
+            <el-checkbox v-for="item in platformsOptions" :key="item.key" :label="item.key">
+              {{ item.name }}
+            </el-checkbox>
+          </el-checkbox-group>
+        </el-dropdown-menu>
+      </el-dropdown>
+
+      <el-dropdown trigger="click">
+        <el-button plain>
+          Link<i class="el-icon-caret-bottom el-icon--right" />
+        </el-button>
+        <el-dropdown-menu slot="dropdown" class="no-padding no-border" style="width:300px">
+          <el-input v-model="url" placeholder="Please enter the content">
+            <template slot="prepend">
+              Url
+            </template>
+          </el-input>
+        </el-dropdown-menu>
+      </el-dropdown>
+
+      <div class="time-container">
+        <el-date-picker v-model="time" type="datetime" format="yyyy-MM-dd HH:mm:ss" placeholder="Release time" />
+      </div>
+
+      <el-button style="margin-left: 10px;" type="success">
+        publish
+      </el-button>
+    </sticky>
+
+    <div class="components-container">
+      <aside>
+        Sticky header, When the page is scrolled to the preset position will be sticky on the top.
+      </aside>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <sticky :sticky-top="200">
+        <el-button type="primary"> placeholder</el-button>
+      </sticky>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+      <div>placeholder</div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Sticky from '@/components/Sticky'
+
+export default {
+  name: 'StickyDemo',
+  components: { Sticky },
+  data() {
+    return {
+      time: '',
+      url: '',
+      platforms: ['a-platform'],
+      platformsOptions: [
+        { key: 'a-platform', name: 'platformA' },
+        { key: 'b-platform', name: 'platformB' },
+        { key: 'c-platform', name: 'platformC' }
+      ],
+      pickerOptions: {
+        disabledDate(time) {
+          return time.getTime() > Date.now()
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.components-container div {
+  margin: 10px;
+}
+
+.time-container {
+  display: inline-block;
+}
+</style>
diff --git a/frontend/src/views/components-demo/tinymce.vue b/frontend/src/views/components-demo/tinymce.vue
new file mode 100644
index 0000000..f03389a
--- /dev/null
+++ b/frontend/src/views/components-demo/tinymce.vue
@@ -0,0 +1,36 @@
+<template>
+  <div class="components-container">
+    <aside>
+      Rich text is a core feature of the management backend, but at the same time it is a place with lots of pits. In the process of selecting rich texts, I also took a lot of detours. The common rich texts on the market have been basically used, and I finally chose Tinymce. See the more detailed rich text comparison and introduction.
+      <a target="_blank" class="link-type" href="https://panjiachen.github.io/vue-element-admin-site/feature/component/rich-editor.html">Documentation</a>
+    </aside>
+    <div>
+      <tinymce v-model="content" :height="300" />
+    </div>
+    <div class="editor-content" v-html="content" />
+  </div>
+</template>
+
+<script>
+import Tinymce from '@/components/Tinymce'
+
+export default {
+  name: 'TinymceDemo',
+  components: { Tinymce },
+  data() {
+    return {
+      content:
+      `<h1 style="text-align: center;">Welcome to the TinyMCE demo!</h1><p style="text-align: center; font-size: 15px;"><img title="TinyMCE Logo" src="//www.tinymce.com/images/glyph-tinymce@2x.png" alt="TinyMCE Logo" width="110" height="97" /><ul>
+        <li>Our <a href="//www.tinymce.com/docs/">documentation</a> is a great resource for learning how to configure TinyMCE.</li><li>Have a specific question? Visit the <a href="https://community.tinymce.com/forum/">Community Forum</a>.</li><li>We also offer enterprise grade support as part of <a href="https://tinymce.com/pricing">TinyMCE premium subscriptions</a>.</li>
+      </ul>`
+    }
+  }
+}
+</script>
+
+<style scoped>
+.editor-content{
+  margin-top: 20px;
+}
+</style>
+
diff --git a/frontend/src/views/dashboard/admin/components/BarChart.vue b/frontend/src/views/dashboard/admin/components/BarChart.vue
new file mode 100644
index 0000000..be0af34
--- /dev/null
+++ b/frontend/src/views/dashboard/admin/components/BarChart.vue
@@ -0,0 +1,102 @@
+<template>
+  <div :class="className" :style="{height:height,width:width}" />
+</template>
+
+<script>
+import echarts from 'echarts'
+require('echarts/theme/macarons') // echarts theme
+import resize from './mixins/resize'
+
+const animationDuration = 6000
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '100%'
+    },
+    height: {
+      type: String,
+      default: '300px'
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.initChart()
+    })
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(this.$el, 'macarons')
+
+      this.chart.setOption({
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { // 坐标轴指示器,坐标轴触发有效
+            type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
+          }
+        },
+        grid: {
+          top: 10,
+          left: '2%',
+          right: '2%',
+          bottom: '3%',
+          containLabel: true
+        },
+        xAxis: [{
+          type: 'category',
+          data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+          axisTick: {
+            alignWithLabel: true
+          }
+        }],
+        yAxis: [{
+          type: 'value',
+          axisTick: {
+            show: false
+          }
+        }],
+        series: [{
+          name: 'pageA',
+          type: 'bar',
+          stack: 'vistors',
+          barWidth: '60%',
+          data: [79, 52, 200, 334, 390, 330, 220],
+          animationDuration
+        }, {
+          name: 'pageB',
+          type: 'bar',
+          stack: 'vistors',
+          barWidth: '60%',
+          data: [80, 52, 200, 334, 390, 330, 220],
+          animationDuration
+        }, {
+          name: 'pageC',
+          type: 'bar',
+          stack: 'vistors',
+          barWidth: '60%',
+          data: [30, 52, 200, 334, 390, 330, 220],
+          animationDuration
+        }]
+      })
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/dashboard/admin/components/BoxCard.vue b/frontend/src/views/dashboard/admin/components/BoxCard.vue
new file mode 100644
index 0000000..1ec1d37
--- /dev/null
+++ b/frontend/src/views/dashboard/admin/components/BoxCard.vue
@@ -0,0 +1,118 @@
+<template>
+  <el-card class="box-card-component" style="margin-left:8px;">
+    <div slot="header" class="box-card-header">
+      <img src="https://wpimg.wallstcn.com/e7d23d71-cf19-4b90-a1cc-f56af8c0903d.png">
+    </div>
+    <div style="position:relative;">
+      <pan-thumb :image="avatar" class="panThumb" />
+      <mallki class-name="mallki-text" text="vue-element-admin" />
+      <div style="padding-top:35px;" class="progress-item">
+        <span>Vue</span>
+        <el-progress :percentage="70" />
+      </div>
+      <div class="progress-item">
+        <span>JavaScript</span>
+        <el-progress :percentage="18" />
+      </div>
+      <div class="progress-item">
+        <span>CSS</span>
+        <el-progress :percentage="12" />
+      </div>
+      <div class="progress-item">
+        <span>ESLint</span>
+        <el-progress :percentage="100" status="success" />
+      </div>
+    </div>
+  </el-card>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import PanThumb from '@/components/PanThumb'
+import Mallki from '@/components/TextHoverEffect/Mallki'
+
+export default {
+  components: { PanThumb, Mallki },
+
+  filters: {
+    statusFilter(status) {
+      const statusMap = {
+        success: 'success',
+        pending: 'danger'
+      }
+      return statusMap[status]
+    }
+  },
+  data() {
+    return {
+      statisticsData: {
+        article_count: 1024,
+        pageviews_count: 1024
+      }
+    }
+  },
+  computed: {
+    ...mapGetters([
+      'name',
+      'avatar',
+      'roles'
+    ])
+  }
+}
+</script>
+
+<style lang="scss" >
+.box-card-component{
+  .el-card__header {
+    padding: 0px!important;
+  }
+}
+</style>
+<style lang="scss" scoped>
+.box-card-component {
+  .box-card-header {
+    position: relative;
+    height: 220px;
+    img {
+      width: 100%;
+      height: 100%;
+      transition: all 0.2s linear;
+      &:hover {
+        transform: scale(1.1, 1.1);
+        filter: contrast(130%);
+      }
+    }
+  }
+  .mallki-text {
+    position: absolute;
+    top: 0px;
+    right: 0px;
+    font-size: 20px;
+    font-weight: bold;
+  }
+  .panThumb {
+    z-index: 100;
+    height: 70px!important;
+    width: 70px!important;
+    position: absolute!important;
+    top: -45px;
+    left: 0px;
+    border: 5px solid #ffffff;
+    background-color: #fff;
+    margin: auto;
+    box-shadow: none!important;
+    ::v-deep .pan-info {
+      box-shadow: none!important;
+    }
+  }
+  .progress-item {
+    margin-bottom: 10px;
+    font-size: 14px;
+  }
+  @media only screen and (max-width: 1510px){
+    .mallki-text{
+      display: none;
+    }
+  }
+}
+</style>
diff --git a/frontend/src/views/dashboard/admin/components/LineChart.vue b/frontend/src/views/dashboard/admin/components/LineChart.vue
new file mode 100644
index 0000000..e654168
--- /dev/null
+++ b/frontend/src/views/dashboard/admin/components/LineChart.vue
@@ -0,0 +1,135 @@
+<template>
+  <div :class="className" :style="{height:height,width:width}" />
+</template>
+
+<script>
+import echarts from 'echarts'
+require('echarts/theme/macarons') // echarts theme
+import resize from './mixins/resize'
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '100%'
+    },
+    height: {
+      type: String,
+      default: '350px'
+    },
+    autoResize: {
+      type: Boolean,
+      default: true
+    },
+    chartData: {
+      type: Object,
+      required: true
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  watch: {
+    chartData: {
+      deep: true,
+      handler(val) {
+        this.setOptions(val)
+      }
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.initChart()
+    })
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(this.$el, 'macarons')
+      this.setOptions(this.chartData)
+    },
+    setOptions({ expectedData, actualData } = {}) {
+      this.chart.setOption({
+        xAxis: {
+          data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+          boundaryGap: false,
+          axisTick: {
+            show: false
+          }
+        },
+        grid: {
+          left: 10,
+          right: 10,
+          bottom: 20,
+          top: 30,
+          containLabel: true
+        },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            type: 'cross'
+          },
+          padding: [5, 10]
+        },
+        yAxis: {
+          axisTick: {
+            show: false
+          }
+        },
+        legend: {
+          data: ['expected', 'actual']
+        },
+        series: [{
+          name: 'expected', itemStyle: {
+            normal: {
+              color: '#FF005A',
+              lineStyle: {
+                color: '#FF005A',
+                width: 2
+              }
+            }
+          },
+          smooth: true,
+          type: 'line',
+          data: expectedData,
+          animationDuration: 2800,
+          animationEasing: 'cubicInOut'
+        },
+        {
+          name: 'actual',
+          smooth: true,
+          type: 'line',
+          itemStyle: {
+            normal: {
+              color: '#3888fa',
+              lineStyle: {
+                color: '#3888fa',
+                width: 2
+              },
+              areaStyle: {
+                color: '#f3f8ff'
+              }
+            }
+          },
+          data: actualData,
+          animationDuration: 2800,
+          animationEasing: 'quadraticOut'
+        }]
+      })
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/dashboard/admin/components/PanelGroup.vue b/frontend/src/views/dashboard/admin/components/PanelGroup.vue
new file mode 100644
index 0000000..589236e
--- /dev/null
+++ b/frontend/src/views/dashboard/admin/components/PanelGroup.vue
@@ -0,0 +1,181 @@
+<template>
+  <el-row :gutter="40" class="panel-group">
+    <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
+      <div class="card-panel" @click="handleSetLineChartData('newVisitis')">
+        <div class="card-panel-icon-wrapper icon-people">
+          <svg-icon icon-class="peoples" class-name="card-panel-icon" />
+        </div>
+        <div class="card-panel-description">
+          <div class="card-panel-text">
+            New Visits
+          </div>
+          <count-to :start-val="0" :end-val="102400" :duration="2600" class="card-panel-num" />
+        </div>
+      </div>
+    </el-col>
+    <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
+      <div class="card-panel" @click="handleSetLineChartData('messages')">
+        <div class="card-panel-icon-wrapper icon-message">
+          <svg-icon icon-class="message" class-name="card-panel-icon" />
+        </div>
+        <div class="card-panel-description">
+          <div class="card-panel-text">
+            Messages
+          </div>
+          <count-to :start-val="0" :end-val="81212" :duration="3000" class="card-panel-num" />
+        </div>
+      </div>
+    </el-col>
+    <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
+      <div class="card-panel" @click="handleSetLineChartData('purchases')">
+        <div class="card-panel-icon-wrapper icon-money">
+          <svg-icon icon-class="money" class-name="card-panel-icon" />
+        </div>
+        <div class="card-panel-description">
+          <div class="card-panel-text">
+            Purchases
+          </div>
+          <count-to :start-val="0" :end-val="9280" :duration="3200" class="card-panel-num" />
+        </div>
+      </div>
+    </el-col>
+    <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
+      <div class="card-panel" @click="handleSetLineChartData('shoppings')">
+        <div class="card-panel-icon-wrapper icon-shopping">
+          <svg-icon icon-class="shopping" class-name="card-panel-icon" />
+        </div>
+        <div class="card-panel-description">
+          <div class="card-panel-text">
+            Shoppings
+          </div>
+          <count-to :start-val="0" :end-val="13600" :duration="3600" class="card-panel-num" />
+        </div>
+      </div>
+    </el-col>
+  </el-row>
+</template>
+
+<script>
+import CountTo from 'vue-count-to'
+
+export default {
+  components: {
+    CountTo
+  },
+  methods: {
+    handleSetLineChartData(type) {
+      this.$emit('handleSetLineChartData', type)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.panel-group {
+  margin-top: 18px;
+
+  .card-panel-col {
+    margin-bottom: 32px;
+  }
+
+  .card-panel {
+    height: 108px;
+    cursor: pointer;
+    font-size: 12px;
+    position: relative;
+    overflow: hidden;
+    color: #666;
+    background: #fff;
+    box-shadow: 4px 4px 40px rgba(0, 0, 0, .05);
+    border-color: rgba(0, 0, 0, .05);
+
+    &:hover {
+      .card-panel-icon-wrapper {
+        color: #fff;
+      }
+
+      .icon-people {
+        background: #40c9c6;
+      }
+
+      .icon-message {
+        background: #36a3f7;
+      }
+
+      .icon-money {
+        background: #f4516c;
+      }
+
+      .icon-shopping {
+        background: #34bfa3
+      }
+    }
+
+    .icon-people {
+      color: #40c9c6;
+    }
+
+    .icon-message {
+      color: #36a3f7;
+    }
+
+    .icon-money {
+      color: #f4516c;
+    }
+
+    .icon-shopping {
+      color: #34bfa3
+    }
+
+    .card-panel-icon-wrapper {
+      float: left;
+      margin: 14px 0 0 14px;
+      padding: 16px;
+      transition: all 0.38s ease-out;
+      border-radius: 6px;
+    }
+
+    .card-panel-icon {
+      float: left;
+      font-size: 48px;
+    }
+
+    .card-panel-description {
+      float: right;
+      font-weight: bold;
+      margin: 26px;
+      margin-left: 0px;
+
+      .card-panel-text {
+        line-height: 18px;
+        color: rgba(0, 0, 0, 0.45);
+        font-size: 16px;
+        margin-bottom: 12px;
+      }
+
+      .card-panel-num {
+        font-size: 20px;
+      }
+    }
+  }
+}
+
+@media (max-width:550px) {
+  .card-panel-description {
+    display: none;
+  }
+
+  .card-panel-icon-wrapper {
+    float: none !important;
+    width: 100%;
+    height: 100%;
+    margin: 0 !important;
+
+    .svg-icon {
+      display: block;
+      margin: 14px auto !important;
+      float: none !important;
+    }
+  }
+}
+</style>
diff --git a/frontend/src/views/dashboard/admin/components/PieChart.vue b/frontend/src/views/dashboard/admin/components/PieChart.vue
new file mode 100644
index 0000000..4d2ef32
--- /dev/null
+++ b/frontend/src/views/dashboard/admin/components/PieChart.vue
@@ -0,0 +1,79 @@
+<template>
+  <div :class="className" :style="{height:height,width:width}" />
+</template>
+
+<script>
+import echarts from 'echarts'
+require('echarts/theme/macarons') // echarts theme
+import resize from './mixins/resize'
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '100%'
+    },
+    height: {
+      type: String,
+      default: '300px'
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.initChart()
+    })
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(this.$el, 'macarons')
+
+      this.chart.setOption({
+        tooltip: {
+          trigger: 'item',
+          formatter: '{a} <br/>{b} : {c} ({d}%)'
+        },
+        legend: {
+          left: 'center',
+          bottom: '10',
+          data: ['Industries', 'Technology', 'Forex', 'Gold', 'Forecasts']
+        },
+        series: [
+          {
+            name: 'WEEKLY WRITE ARTICLES',
+            type: 'pie',
+            roseType: 'radius',
+            radius: [15, 95],
+            center: ['50%', '38%'],
+            data: [
+              { value: 320, name: 'Industries' },
+              { value: 240, name: 'Technology' },
+              { value: 149, name: 'Forex' },
+              { value: 100, name: 'Gold' },
+              { value: 59, name: 'Forecasts' }
+            ],
+            animationEasing: 'cubicInOut',
+            animationDuration: 2600
+          }
+        ]
+      })
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/dashboard/admin/components/RaddarChart.vue b/frontend/src/views/dashboard/admin/components/RaddarChart.vue
new file mode 100644
index 0000000..52c8f9f
--- /dev/null
+++ b/frontend/src/views/dashboard/admin/components/RaddarChart.vue
@@ -0,0 +1,116 @@
+<template>
+  <div :class="className" :style="{height:height,width:width}" />
+</template>
+
+<script>
+import echarts from 'echarts'
+require('echarts/theme/macarons') // echarts theme
+import resize from './mixins/resize'
+
+const animationDuration = 3000
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '100%'
+    },
+    height: {
+      type: String,
+      default: '300px'
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.initChart()
+    })
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(this.$el, 'macarons')
+
+      this.chart.setOption({
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { // 坐标轴指示器,坐标轴触发有效
+            type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
+          }
+        },
+        radar: {
+          radius: '66%',
+          center: ['50%', '42%'],
+          splitNumber: 8,
+          splitArea: {
+            areaStyle: {
+              color: 'rgba(127,95,132,.3)',
+              opacity: 1,
+              shadowBlur: 45,
+              shadowColor: 'rgba(0,0,0,.5)',
+              shadowOffsetX: 0,
+              shadowOffsetY: 15
+            }
+          },
+          indicator: [
+            { name: 'Sales', max: 10000 },
+            { name: 'Administration', max: 20000 },
+            { name: 'Information Technology', max: 20000 },
+            { name: 'Customer Support', max: 20000 },
+            { name: 'Development', max: 20000 },
+            { name: 'Marketing', max: 20000 }
+          ]
+        },
+        legend: {
+          left: 'center',
+          bottom: '10',
+          data: ['Allocated Budget', 'Expected Spending', 'Actual Spending']
+        },
+        series: [{
+          type: 'radar',
+          symbolSize: 0,
+          areaStyle: {
+            normal: {
+              shadowBlur: 13,
+              shadowColor: 'rgba(0,0,0,.2)',
+              shadowOffsetX: 0,
+              shadowOffsetY: 10,
+              opacity: 1
+            }
+          },
+          data: [
+            {
+              value: [5000, 7000, 12000, 11000, 15000, 14000],
+              name: 'Allocated Budget'
+            },
+            {
+              value: [4000, 9000, 15000, 15000, 13000, 11000],
+              name: 'Expected Spending'
+            },
+            {
+              value: [5500, 11000, 12000, 15000, 12000, 12000],
+              name: 'Actual Spending'
+            }
+          ],
+          animationDuration: animationDuration
+        }]
+      })
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/dashboard/admin/components/TodoList/Todo.vue b/frontend/src/views/dashboard/admin/components/TodoList/Todo.vue
new file mode 100644
index 0000000..c4b3cae
--- /dev/null
+++ b/frontend/src/views/dashboard/admin/components/TodoList/Todo.vue
@@ -0,0 +1,81 @@
+<template>
+  <li :class="{ completed: todo.done, editing: editing }" class="todo">
+    <div class="view">
+      <input
+        :checked="todo.done"
+        class="toggle"
+        type="checkbox"
+        @change="toggleTodo( todo)"
+      >
+      <label @dblclick="editing = true" v-text="todo.text" />
+      <button class="destroy" @click="deleteTodo( todo )" />
+    </div>
+    <input
+      v-show="editing"
+      v-focus="editing"
+      :value="todo.text"
+      class="edit"
+      @keyup.enter="doneEdit"
+      @keyup.esc="cancelEdit"
+      @blur="doneEdit"
+    >
+  </li>
+</template>
+
+<script>
+export default {
+  name: 'Todo',
+  directives: {
+    focus(el, { value }, { context }) {
+      if (value) {
+        context.$nextTick(() => {
+          el.focus()
+        })
+      }
+    }
+  },
+  props: {
+    todo: {
+      type: Object,
+      default: function() {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      editing: false
+    }
+  },
+  methods: {
+    deleteTodo(todo) {
+      this.$emit('deleteTodo', todo)
+    },
+    editTodo({ todo, value }) {
+      this.$emit('editTodo', { todo, value })
+    },
+    toggleTodo(todo) {
+      this.$emit('toggleTodo', todo)
+    },
+    doneEdit(e) {
+      const value = e.target.value.trim()
+      const { todo } = this
+      if (!value) {
+        this.deleteTodo({
+          todo
+        })
+      } else if (this.editing) {
+        this.editTodo({
+          todo,
+          value
+        })
+        this.editing = false
+      }
+    },
+    cancelEdit(e) {
+      e.target.value = this.todo.text
+      this.editing = false
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/dashboard/admin/components/TodoList/index.scss b/frontend/src/views/dashboard/admin/components/TodoList/index.scss
new file mode 100644
index 0000000..74ce0d5
--- /dev/null
+++ b/frontend/src/views/dashboard/admin/components/TodoList/index.scss
@@ -0,0 +1,320 @@
+.todoapp {
+  font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
+  line-height: 1.4em;
+  color: #4d4d4d;
+  min-width: 230px;
+  max-width: 550px;
+  margin: 0 auto ;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  font-weight: 300;
+  background: #fff;
+  z-index: 1;
+  position: relative;
+  button {
+    margin: 0;
+    padding: 0;
+    border: 0;
+    background: none;
+    font-size: 100%;
+    vertical-align: baseline;
+    font-family: inherit;
+    font-weight: inherit;
+    color: inherit;
+    -webkit-appearance: none;
+    appearance: none;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+  }
+  :focus {
+    outline: 0;
+  }
+  .hidden {
+    display: none;
+  }
+  .todoapp {
+    background: #fff;
+    margin: 130px 0 40px 0;
+    position: relative;
+    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
+  }
+  .todoapp input::-webkit-input-placeholder {
+    font-style: italic;
+    font-weight: 300;
+    color: #e6e6e6;
+  }
+  .todoapp input::-moz-placeholder {
+    font-style: italic;
+    font-weight: 300;
+    color: #e6e6e6;
+  }
+  .todoapp input::input-placeholder {
+    font-style: italic;
+    font-weight: 300;
+    color: #e6e6e6;
+  }
+  .todoapp h1 {
+    position: absolute;
+    top: -155px;
+    width: 100%;
+    font-size: 100px;
+    font-weight: 100;
+    text-align: center;
+    color: rgba(175, 47, 47, 0.15);
+    -webkit-text-rendering: optimizeLegibility;
+    -moz-text-rendering: optimizeLegibility;
+    text-rendering: optimizeLegibility;
+  }
+  .new-todo,
+  .edit {
+    position: relative;
+    margin: 0;
+    width: 100%;
+    font-size: 18px;
+    font-family: inherit;
+    font-weight: inherit;
+    line-height: 1.4em;
+    border: 0;
+    color: inherit;
+    padding: 6px;
+    border: 1px solid #999;
+    box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
+    box-sizing: border-box;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+  }
+  .new-todo {
+    padding: 10px 16px 16px 60px;
+    border: none;
+    background: rgba(0, 0, 0, 0.003);
+    box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
+  }
+  .main {
+    position: relative;
+    z-index: 2;
+    border-top: 1px solid #e6e6e6;
+  }
+  .toggle-all {
+    text-align: center;
+    border: none;
+    /* Mobile Safari */
+    opacity: 0;
+    position: absolute;
+  }
+  .toggle-all+label {
+    width: 60px;
+    height: 34px;
+    font-size: 0;
+    position: absolute;
+    top: -52px;
+    left: -13px;
+    -webkit-transform: rotate(90deg);
+    transform: rotate(90deg);
+  }
+  .toggle-all+label:before {
+    content: '❯';
+    font-size: 22px;
+    color: #e6e6e6;
+    padding: 10px 27px 10px 27px;
+  }
+  .toggle-all:checked+label:before {
+    color: #737373;
+  }
+  .todo-list {
+    margin: 0;
+    padding: 0;
+    list-style: none;
+  }
+  .todo-list li {
+    position: relative;
+    font-size: 24px;
+    border-bottom: 1px solid #ededed;
+  }
+  .todo-list li:last-child {
+    border-bottom: none;
+  }
+  .todo-list li.editing {
+    border-bottom: none;
+    padding: 0;
+  }
+  .todo-list li.editing .edit {
+    display: block;
+    width: 506px;
+    padding: 12px 16px;
+    margin: 0 0 0 43px;
+  }
+  .todo-list li.editing .view {
+    display: none;
+  }
+  .todo-list li .toggle {
+    text-align: center;
+    width: 40px;
+    /* auto, since non-WebKit browsers doesn't support input styling */
+    height: auto;
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    margin: auto 0;
+    border: none;
+    /* Mobile Safari */
+    -webkit-appearance: none;
+    appearance: none;
+  }
+  .todo-list li .toggle {
+    opacity: 0;
+  }
+  .todo-list li .toggle+label {
+    /*
+    Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
+    IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
+  */
+    background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
+    background-repeat: no-repeat;
+    background-position: center left;
+    background-size: 36px;
+  }
+  .todo-list li .toggle:checked+label {
+    background-size: 36px;
+    background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
+  }
+  .todo-list li label {
+    word-break: break-all;
+    padding: 15px 15px 15px 50px;
+    display: block;
+    line-height: 1.0;
+        font-size: 14px;
+    transition: color 0.4s;
+  }
+  .todo-list li.completed label {
+    color: #d9d9d9;
+    text-decoration: line-through;
+  }
+  .todo-list li .destroy {
+    display: none;
+    position: absolute;
+    top: 0;
+    right: 10px;
+    bottom: 0;
+    width: 40px;
+    height: 40px;
+    margin: auto 0;
+    font-size: 30px;
+    color: #cc9a9a;
+    transition: color 0.2s ease-out;
+    cursor: pointer;
+  }
+  .todo-list li .destroy:hover {
+    color: #af5b5e;
+  }
+  .todo-list li .destroy:after {
+    content: '×';
+  }
+  .todo-list li:hover .destroy {
+    display: block;
+  }
+  .todo-list li .edit {
+    display: none;
+  }
+  .todo-list li.editing:last-child {
+    margin-bottom: -1px;
+  }
+  .footer {
+    color: #777;
+    position: relative;
+    padding: 10px 15px;
+    height: 40px;
+    text-align: center;
+    border-top: 1px solid #e6e6e6;
+  }
+  .footer:before {
+    content: '';
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    height: 40px;
+    overflow: hidden;
+    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2);
+  }
+  .todo-count {
+    float: left;
+    text-align: left;
+  }
+  .todo-count strong {
+    font-weight: 300;
+  }
+  .filters {
+    margin: 0;
+    padding: 0;
+    position: relative;
+    z-index: 1;
+    list-style: none;
+  }
+  .filters li {
+    display: inline;
+  }
+  .filters li a {
+    color: inherit;
+    font-size: 12px;
+    padding: 3px 7px;
+    text-decoration: none;
+    border: 1px solid transparent;
+    border-radius: 3px;
+  }
+  .filters li a:hover {
+    border-color: rgba(175, 47, 47, 0.1);
+  }
+  .filters li a.selected {
+    border-color: rgba(175, 47, 47, 0.2);
+  }
+  .clear-completed,
+  html .clear-completed:active {
+    float: right;
+    position: relative;
+    line-height: 20px;
+    text-decoration: none;
+    cursor: pointer;
+  }
+  .clear-completed:hover {
+    text-decoration: underline;
+  }
+  .info {
+    margin: 65px auto 0;
+    color: #bfbfbf;
+    font-size: 10px;
+    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+    text-align: center;
+  }
+  .info p {
+    line-height: 1;
+  }
+  .info a {
+    color: inherit;
+    text-decoration: none;
+    font-weight: 400;
+  }
+  .info a:hover {
+    text-decoration: underline;
+  }
+  /*
+  Hack to remove background from Mobile Safari.
+  Can't use it globally since it destroys checkboxes in Firefox
+*/
+  @media screen and (-webkit-min-device-pixel-ratio:0) {
+    .toggle-all,
+    .todo-list li .toggle {
+      background: none;
+    }
+    .todo-list li .toggle {
+      height: 40px;
+    }
+  }
+  @media (max-width: 430px) {
+    .footer {
+      height: 50px;
+    }
+    .filters {
+      bottom: 10px;
+    }
+  }
+}
diff --git a/frontend/src/views/dashboard/admin/components/TodoList/index.vue b/frontend/src/views/dashboard/admin/components/TodoList/index.vue
new file mode 100644
index 0000000..8000d41
--- /dev/null
+++ b/frontend/src/views/dashboard/admin/components/TodoList/index.vue
@@ -0,0 +1,127 @@
+<template>
+  <section class="todoapp">
+    <!-- header -->
+    <header class="header">
+      <input class="new-todo" autocomplete="off" placeholder="Todo List" @keyup.enter="addTodo">
+    </header>
+    <!-- main section -->
+    <section v-show="todos.length" class="main">
+      <input id="toggle-all" :checked="allChecked" class="toggle-all" type="checkbox" @change="toggleAll({ done: !allChecked })">
+      <label for="toggle-all" />
+      <ul class="todo-list">
+        <todo
+          v-for="(todo, index) in filteredTodos"
+          :key="index"
+          :todo="todo"
+          @toggleTodo="toggleTodo"
+          @editTodo="editTodo"
+          @deleteTodo="deleteTodo"
+        />
+      </ul>
+    </section>
+    <!-- footer -->
+    <footer v-show="todos.length" class="footer">
+      <span class="todo-count">
+        <strong>{{ remaining }}</strong>
+        {{ remaining | pluralize('item') }} left
+      </span>
+      <ul class="filters">
+        <li v-for="(val, key) in filters" :key="key">
+          <a :class="{ selected: visibility === key }" @click.prevent="visibility = key">{{ key | capitalize }}</a>
+        </li>
+      </ul>
+      <!-- <button class="clear-completed" v-show="todos.length > remaining" @click="clearCompleted">
+        Clear completed
+      </button> -->
+    </footer>
+  </section>
+</template>
+
+<script>
+import Todo from './Todo.vue'
+
+const STORAGE_KEY = 'todos'
+const filters = {
+  all: todos => todos,
+  active: todos => todos.filter(todo => !todo.done),
+  completed: todos => todos.filter(todo => todo.done)
+}
+const defalutList = [
+  { text: 'star this repository', done: false },
+  { text: 'fork this repository', done: false },
+  { text: 'follow author', done: false },
+  { text: 'vue-element-admin', done: true },
+  { text: 'vue', done: true },
+  { text: 'element-ui', done: true },
+  { text: 'axios', done: true },
+  { text: 'webpack', done: true }
+]
+export default {
+  components: { Todo },
+  filters: {
+    pluralize: (n, w) => n === 1 ? w : w + 's',
+    capitalize: s => s.charAt(0).toUpperCase() + s.slice(1)
+  },
+  data() {
+    return {
+      visibility: 'all',
+      filters,
+      // todos: JSON.parse(window.localStorage.getItem(STORAGE_KEY)) || defalutList
+      todos: defalutList
+    }
+  },
+  computed: {
+    allChecked() {
+      return this.todos.every(todo => todo.done)
+    },
+    filteredTodos() {
+      return filters[this.visibility](this.todos)
+    },
+    remaining() {
+      return this.todos.filter(todo => !todo.done).length
+    }
+  },
+  methods: {
+    setLocalStorage() {
+      window.localStorage.setItem(STORAGE_KEY, JSON.stringify(this.todos))
+    },
+    addTodo(e) {
+      const text = e.target.value
+      if (text.trim()) {
+        this.todos.push({
+          text,
+          done: false
+        })
+        this.setLocalStorage()
+      }
+      e.target.value = ''
+    },
+    toggleTodo(val) {
+      val.done = !val.done
+      this.setLocalStorage()
+    },
+    deleteTodo(todo) {
+      this.todos.splice(this.todos.indexOf(todo), 1)
+      this.setLocalStorage()
+    },
+    editTodo({ todo, value }) {
+      todo.text = value
+      this.setLocalStorage()
+    },
+    clearCompleted() {
+      this.todos = this.todos.filter(todo => !todo.done)
+      this.setLocalStorage()
+    },
+    toggleAll({ done }) {
+      this.todos.forEach(todo => {
+        todo.done = done
+        this.setLocalStorage()
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+  @import './index.scss';
+</style>
diff --git a/frontend/src/views/dashboard/admin/components/TransactionTable.vue b/frontend/src/views/dashboard/admin/components/TransactionTable.vue
new file mode 100644
index 0000000..d07b0ed
--- /dev/null
+++ b/frontend/src/views/dashboard/admin/components/TransactionTable.vue
@@ -0,0 +1,55 @@
+<template>
+  <el-table :data="list" style="width: 100%;padding-top: 15px;">
+    <el-table-column label="Order_No" min-width="200">
+      <template slot-scope="scope">
+        {{ scope.row.order_no | orderNoFilter }}
+      </template>
+    </el-table-column>
+    <el-table-column label="Price" width="195" align="center">
+      <template slot-scope="scope">
+        ¥{{ scope.row.price | toThousandFilter }}
+      </template>
+    </el-table-column>
+    <el-table-column label="Status" width="100" align="center">
+      <template slot-scope="{row}">
+        <el-tag :type="row.status | statusFilter">
+          {{ row.status }}
+        </el-tag>
+      </template>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script>
+import { transactionList } from '@/api/remote-search'
+
+export default {
+  filters: {
+    statusFilter(status) {
+      const statusMap = {
+        success: 'success',
+        pending: 'danger'
+      }
+      return statusMap[status]
+    },
+    orderNoFilter(str) {
+      return str.substring(0, 30)
+    }
+  },
+  data() {
+    return {
+      list: null
+    }
+  },
+  created() {
+    this.fetchData()
+  },
+  methods: {
+    fetchData() {
+      transactionList().then(response => {
+        this.list = response.data.items.slice(0, 8)
+      })
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/dashboard/admin/components/mixins/resize.js b/frontend/src/views/dashboard/admin/components/mixins/resize.js
new file mode 100644
index 0000000..234953b
--- /dev/null
+++ b/frontend/src/views/dashboard/admin/components/mixins/resize.js
@@ -0,0 +1,55 @@
+import { debounce } from '@/utils'
+
+export default {
+  data() {
+    return {
+      $_sidebarElm: null,
+      $_resizeHandler: null
+    }
+  },
+  mounted() {
+    this.$_resizeHandler = debounce(() => {
+      if (this.chart) {
+        this.chart.resize()
+      }
+    }, 100)
+    this.$_initResizeEvent()
+    this.$_initSidebarResizeEvent()
+  },
+  beforeDestroy() {
+    this.$_destroyResizeEvent()
+    this.$_destroySidebarResizeEvent()
+  },
+  // to fixed bug when cached by keep-alive
+  // https://github.com/PanJiaChen/vue-element-admin/issues/2116
+  activated() {
+    this.$_initResizeEvent()
+    this.$_initSidebarResizeEvent()
+  },
+  deactivated() {
+    this.$_destroyResizeEvent()
+    this.$_destroySidebarResizeEvent()
+  },
+  methods: {
+    // use $_ for mixins properties
+    // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
+    $_initResizeEvent() {
+      window.addEventListener('resize', this.$_resizeHandler)
+    },
+    $_destroyResizeEvent() {
+      window.removeEventListener('resize', this.$_resizeHandler)
+    },
+    $_sidebarResizeHandler(e) {
+      if (e.propertyName === 'width') {
+        this.$_resizeHandler()
+      }
+    },
+    $_initSidebarResizeEvent() {
+      this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
+      this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
+    },
+    $_destroySidebarResizeEvent() {
+      this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
+    }
+  }
+}
diff --git a/frontend/src/views/dashboard/admin/index.vue b/frontend/src/views/dashboard/admin/index.vue
new file mode 100644
index 0000000..8cb557b
--- /dev/null
+++ b/frontend/src/views/dashboard/admin/index.vue
@@ -0,0 +1,124 @@
+<template>
+  <div class="dashboard-editor-container">
+    <github-corner class="github-corner" />
+
+    <panel-group @handleSetLineChartData="handleSetLineChartData" />
+
+    <el-row style="background:#fff;padding:16px 16px 0;margin-bottom:32px;">
+      <line-chart :chart-data="lineChartData" />
+    </el-row>
+
+    <el-row :gutter="32">
+      <el-col :xs="24" :sm="24" :lg="8">
+        <div class="chart-wrapper">
+          <raddar-chart />
+        </div>
+      </el-col>
+      <el-col :xs="24" :sm="24" :lg="8">
+        <div class="chart-wrapper">
+          <pie-chart />
+        </div>
+      </el-col>
+      <el-col :xs="24" :sm="24" :lg="8">
+        <div class="chart-wrapper">
+          <bar-chart />
+        </div>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="8">
+      <el-col :xs="{span: 24}" :sm="{span: 24}" :md="{span: 24}" :lg="{span: 12}" :xl="{span: 12}" style="padding-right:8px;margin-bottom:30px;">
+        <transaction-table />
+      </el-col>
+      <el-col :xs="{span: 24}" :sm="{span: 12}" :md="{span: 12}" :lg="{span: 6}" :xl="{span: 6}" style="margin-bottom:30px;">
+        <todo-list />
+      </el-col>
+      <el-col :xs="{span: 24}" :sm="{span: 12}" :md="{span: 12}" :lg="{span: 6}" :xl="{span: 6}" style="margin-bottom:30px;">
+        <box-card />
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import GithubCorner from '@/components/GithubCorner'
+import PanelGroup from './components/PanelGroup'
+import LineChart from './components/LineChart'
+import RaddarChart from './components/RaddarChart'
+import PieChart from './components/PieChart'
+import BarChart from './components/BarChart'
+import TransactionTable from './components/TransactionTable'
+import TodoList from './components/TodoList'
+import BoxCard from './components/BoxCard'
+
+const lineChartData = {
+  newVisitis: {
+    expectedData: [100, 120, 161, 134, 105, 160, 165],
+    actualData: [120, 82, 91, 154, 162, 140, 145]
+  },
+  messages: {
+    expectedData: [200, 192, 120, 144, 160, 130, 140],
+    actualData: [180, 160, 151, 106, 145, 150, 130]
+  },
+  purchases: {
+    expectedData: [80, 100, 121, 104, 105, 90, 100],
+    actualData: [120, 90, 100, 138, 142, 130, 130]
+  },
+  shoppings: {
+    expectedData: [130, 140, 141, 142, 145, 150, 160],
+    actualData: [120, 82, 91, 154, 162, 140, 130]
+  }
+}
+
+export default {
+  name: 'DashboardAdmin',
+  components: {
+    GithubCorner,
+    PanelGroup,
+    LineChart,
+    RaddarChart,
+    PieChart,
+    BarChart,
+    TransactionTable,
+    TodoList,
+    BoxCard
+  },
+  data() {
+    return {
+      lineChartData: lineChartData.newVisitis
+    }
+  },
+  methods: {
+    handleSetLineChartData(type) {
+      this.lineChartData = lineChartData[type]
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.dashboard-editor-container {
+  padding: 32px;
+  background-color: rgb(240, 242, 245);
+  position: relative;
+
+  .github-corner {
+    position: absolute;
+    top: 0px;
+    border: 0;
+    right: 0;
+  }
+
+  .chart-wrapper {
+    background: #fff;
+    padding: 16px 16px 0;
+    margin-bottom: 32px;
+  }
+}
+
+@media (max-width:1024px) {
+  .chart-wrapper {
+    padding: 8px;
+  }
+}
+</style>
diff --git a/frontend/src/views/dashboard/editor/index.vue b/frontend/src/views/dashboard/editor/index.vue
new file mode 100644
index 0000000..9723bcc
--- /dev/null
+++ b/frontend/src/views/dashboard/editor/index.vue
@@ -0,0 +1,74 @@
+<template>
+  <div class="dashboard-editor-container">
+    <div class=" clearfix">
+      <pan-thumb :image="avatar" style="float: left">
+        Your roles:
+        <span v-for="item in roles" :key="item" class="pan-info-roles">{{ item }}</span>
+      </pan-thumb>
+      <github-corner style="position: absolute; top: 0px; border: 0; right: 0;" />
+      <div class="info-container">
+        <span class="display_name">{{ name }}</span>
+        <span style="font-size:20px;padding-top:20px;display:inline-block;">Editor's Dashboard</span>
+      </div>
+    </div>
+    <div>
+      <img :src="emptyGif" class="emptyGif">
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import PanThumb from '@/components/PanThumb'
+import GithubCorner from '@/components/GithubCorner'
+
+export default {
+  name: 'DashboardEditor',
+  components: { PanThumb, GithubCorner },
+  data() {
+    return {
+      emptyGif: 'https://wpimg.wallstcn.com/0e03b7da-db9e-4819-ba10-9016ddfdaed3'
+    }
+  },
+  computed: {
+    ...mapGetters([
+      'name',
+      'avatar',
+      'roles'
+    ])
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  .emptyGif {
+    display: block;
+    width: 45%;
+    margin: 0 auto;
+  }
+
+  .dashboard-editor-container {
+    background-color: #e3e3e3;
+    min-height: 100vh;
+    padding: 50px 60px 0px;
+    .pan-info-roles {
+      font-size: 12px;
+      font-weight: 700;
+      color: #333;
+      display: block;
+    }
+    .info-container {
+      position: relative;
+      margin-left: 190px;
+      height: 150px;
+      line-height: 200px;
+      .display_name {
+        font-size: 48px;
+        line-height: 48px;
+        color: #212121;
+        position: absolute;
+        top: 25px;
+      }
+    }
+  }
+</style>
diff --git a/frontend/src/views/dashboard/index.vue b/frontend/src/views/dashboard/index.vue
new file mode 100644
index 0000000..1720ea8
--- /dev/null
+++ b/frontend/src/views/dashboard/index.vue
@@ -0,0 +1,31 @@
+<template>
+  <div class="dashboard-container">
+    <component :is="currentRole" />
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import adminDashboard from './admin'
+import editorDashboard from './editor'
+
+export default {
+  name: 'Dashboard',
+  components: { adminDashboard, editorDashboard },
+  data() {
+    return {
+      currentRole: 'adminDashboard'
+    }
+  },
+  computed: {
+    ...mapGetters([
+      'roles'
+    ])
+  },
+  created() {
+    if (!this.roles.includes('admin')) {
+      this.currentRole = 'editorDashboard'
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/documentation/index.vue b/frontend/src/views/documentation/index.vue
new file mode 100644
index 0000000..d3f7746
--- /dev/null
+++ b/frontend/src/views/documentation/index.vue
@@ -0,0 +1,56 @@
+<template>
+  <div class="app-container documentation-container">
+    <a class="document-btn" target="_blank" href="https://panjiachen.github.io/vue-element-admin-site/">Documentation</a>
+    <a class="document-btn" target="_blank" href="https://github.com/PanJiaChen/vue-element-admin/">Github Repository</a>
+    <a class="document-btn" target="_blank" href="https://panjiachen.gitee.io/vue-element-admin-site/zh/">国内文档</a>
+    <dropdown-menu class="document-btn" :items="articleList" title="系列文章" />
+    <a class="document-btn" target="_blank" href="https://panjiachen.github.io/vue-element-admin-site/zh/job/">内推招聘</a>
+  </div>
+</template>
+
+<script>
+import DropdownMenu from '@/components/Share/DropdownMenu'
+
+export default {
+  name: 'Documentation',
+  components: { DropdownMenu },
+  data() {
+    return {
+      articleList: [
+        { title: '基础篇', href: 'https://juejin.im/post/59097cd7a22b9d0065fb61d2' },
+        { title: '登录权限篇', href: 'https://juejin.im/post/591aa14f570c35006961acac' },
+        { title: '实战篇', href: 'https://juejin.im/post/593121aa0ce4630057f70d35' },
+        { title: 'vue-admin-template 篇', href: 'https://juejin.im/post/595b4d776fb9a06bbe7dba56' },
+        { title: 'v4.0 篇', href: 'https://juejin.im/post/5c92ff94f265da6128275a85' },
+        { title: '自行封装 component', href: 'https://segmentfault.com/a/1190000009090836' },
+        { title: '优雅的使用 icon', href: 'https://juejin.im/post/59bb864b5188257e7a427c09' },
+        { title: 'webpack4(上)', href: 'https://juejin.im/post/59bb864b5188257e7a427c09' },
+        { title: 'webpack4(下)', href: 'https://juejin.im/post/5b5d6d6f6fb9a04fea58aabc' }
+      ]
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.documentation-container {
+  margin: 50px;
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-evenly;
+
+  .document-btn {
+    flex-shrink: 0;
+    display: block;
+    cursor: pointer;
+    background: black;
+    color: white;
+    height: 60px;
+    width: 200px;
+    margin-bottom: 16px;
+    line-height: 60px;
+    font-size: 20px;
+    text-align: center;
+  }
+}
+</style>
diff --git a/frontend/src/views/error-log/components/ErrorTestA.vue b/frontend/src/views/error-log/components/ErrorTestA.vue
new file mode 100644
index 0000000..52654e0
--- /dev/null
+++ b/frontend/src/views/error-log/components/ErrorTestA.vue
@@ -0,0 +1,13 @@
+<template>
+  <div>
+    <!--error code-->
+    {{ a.a }}
+    <!--error code-->
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ErrorTestA'
+}
+</script>
diff --git a/frontend/src/views/error-log/components/ErrorTestB.vue b/frontend/src/views/error-log/components/ErrorTestB.vue
new file mode 100644
index 0000000..d796bee
--- /dev/null
+++ b/frontend/src/views/error-log/components/ErrorTestB.vue
@@ -0,0 +1,11 @@
+<template>
+  <div />
+</template>
+
+<script>
+export default {
+  created() {
+    this.b = b // eslint-disable-line
+  }
+}
+</script>
diff --git a/frontend/src/views/error-log/index.vue b/frontend/src/views/error-log/index.vue
new file mode 100644
index 0000000..e999c85
--- /dev/null
+++ b/frontend/src/views/error-log/index.vue
@@ -0,0 +1,32 @@
+<template>
+  <div class="errPage-container">
+    <ErrorA />
+    <ErrorB />
+    <h3>Please click the bug icon in the upper right corner</h3>
+    <aside>
+      Now the management system are basically the form of the spa, it enhances the user experience, but it also increases the possibility of page problems, a small negligence may lead to the entire page deadlock. Fortunately Vue provides a way to catch handling exceptions, where you can handle errors or report exceptions.
+      <a target="_blank" class="link-type" href="https://panjiachen.github.io/vue-element-admin-site/guide/advanced/error.html">
+        Document introduction
+      </a>
+    </aside>
+    <a href="#">
+      <img src="https://wpimg.wallstcn.com/360e4842-4db5-42d0-b078-f9a84a825546.gif">
+    </a>
+  </div>
+</template>
+
+<script>
+import ErrorA from './components/ErrorTestA'
+import ErrorB from './components/ErrorTestB'
+
+export default {
+  name: 'ErrorLog',
+  components: { ErrorA, ErrorB }
+}
+</script>
+
+<style scoped>
+  .errPage-container {
+    padding: 30px;
+  }
+</style>
diff --git a/frontend/src/views/error-page/401.vue b/frontend/src/views/error-page/401.vue
new file mode 100644
index 0000000..a52ed23
--- /dev/null
+++ b/frontend/src/views/error-page/401.vue
@@ -0,0 +1,99 @@
+<template>
+  <div class="errPage-container">
+    <el-button icon="el-icon-arrow-left" class="pan-back-btn" @click="back">
+      返回
+    </el-button>
+    <el-row>
+      <el-col :span="12">
+        <h1 class="text-jumbo text-ginormous">
+          Oops!
+        </h1>
+        gif来源<a href="https://zh.airbnb.com/" target="_blank">airbnb</a> 页面
+        <h2>你没有权限去该页面</h2>
+        <h6>如有不满请联系你领导</h6>
+        <ul class="list-unstyled">
+          <li>或者你可以去:</li>
+          <li class="link-type">
+            <router-link to="/dashboard">
+              回首页
+            </router-link>
+          </li>
+          <li class="link-type">
+            <a href="https://www.taobao.com/">随便看看</a>
+          </li>
+          <li><a href="#" @click.prevent="dialogVisible=true">点我看图</a></li>
+        </ul>
+      </el-col>
+      <el-col :span="12">
+        <img :src="errGif" width="313" height="428" alt="Girl has dropped her ice cream.">
+      </el-col>
+    </el-row>
+    <el-dialog :visible.sync="dialogVisible" title="随便看">
+      <img :src="ewizardClap" class="pan-img">
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import errGif from '@/assets/401_images/401.gif'
+
+export default {
+  name: 'Page401',
+  data() {
+    return {
+      errGif: errGif + '?' + +new Date(),
+      ewizardClap: 'https://wpimg.wallstcn.com/007ef517-bafd-4066-aae4-6883632d9646',
+      dialogVisible: false
+    }
+  },
+  methods: {
+    back() {
+      if (this.$route.query.noGoBack) {
+        this.$router.push({ path: '/dashboard' })
+      } else {
+        this.$router.go(-1)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  .errPage-container {
+    width: 800px;
+    max-width: 100%;
+    margin: 100px auto;
+    .pan-back-btn {
+      background: #008489;
+      color: #fff;
+      border: none!important;
+    }
+    .pan-gif {
+      margin: 0 auto;
+      display: block;
+    }
+    .pan-img {
+      display: block;
+      margin: 0 auto;
+      width: 100%;
+    }
+    .text-jumbo {
+      font-size: 60px;
+      font-weight: 700;
+      color: #484848;
+    }
+    .list-unstyled {
+      font-size: 14px;
+      li {
+        padding-bottom: 5px;
+      }
+      a {
+        color: #008489;
+        text-decoration: none;
+        &:hover {
+          text-decoration: underline;
+        }
+      }
+    }
+  }
+</style>
diff --git a/frontend/src/views/error-page/404.vue b/frontend/src/views/error-page/404.vue
new file mode 100644
index 0000000..1791f55
--- /dev/null
+++ b/frontend/src/views/error-page/404.vue
@@ -0,0 +1,228 @@
+<template>
+  <div class="wscn-http404-container">
+    <div class="wscn-http404">
+      <div class="pic-404">
+        <img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
+        <img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
+        <img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
+        <img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
+      </div>
+      <div class="bullshit">
+        <div class="bullshit__oops">OOPS!</div>
+        <div class="bullshit__info">All rights reserved
+          <a style="color:#20a0ff" href="https://wallstreetcn.com" target="_blank">wallstreetcn</a>
+        </div>
+        <div class="bullshit__headline">{{ message }}</div>
+        <div class="bullshit__info">Please check that the URL you entered is correct, or click the button below to return to the homepage.</div>
+        <a href="" class="bullshit__return-home">Back to home</a>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: 'Page404',
+  computed: {
+    message() {
+      return 'The webmaster said that you can not enter this page...'
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.wscn-http404-container{
+  transform: translate(-50%,-50%);
+  position: absolute;
+  top: 40%;
+  left: 50%;
+}
+.wscn-http404 {
+  position: relative;
+  width: 1200px;
+  padding: 0 50px;
+  overflow: hidden;
+  .pic-404 {
+    position: relative;
+    float: left;
+    width: 600px;
+    overflow: hidden;
+    &__parent {
+      width: 100%;
+    }
+    &__child {
+      position: absolute;
+      &.left {
+        width: 80px;
+        top: 17px;
+        left: 220px;
+        opacity: 0;
+        animation-name: cloudLeft;
+        animation-duration: 2s;
+        animation-timing-function: linear;
+        animation-fill-mode: forwards;
+        animation-delay: 1s;
+      }
+      &.mid {
+        width: 46px;
+        top: 10px;
+        left: 420px;
+        opacity: 0;
+        animation-name: cloudMid;
+        animation-duration: 2s;
+        animation-timing-function: linear;
+        animation-fill-mode: forwards;
+        animation-delay: 1.2s;
+      }
+      &.right {
+        width: 62px;
+        top: 100px;
+        left: 500px;
+        opacity: 0;
+        animation-name: cloudRight;
+        animation-duration: 2s;
+        animation-timing-function: linear;
+        animation-fill-mode: forwards;
+        animation-delay: 1s;
+      }
+      @keyframes cloudLeft {
+        0% {
+          top: 17px;
+          left: 220px;
+          opacity: 0;
+        }
+        20% {
+          top: 33px;
+          left: 188px;
+          opacity: 1;
+        }
+        80% {
+          top: 81px;
+          left: 92px;
+          opacity: 1;
+        }
+        100% {
+          top: 97px;
+          left: 60px;
+          opacity: 0;
+        }
+      }
+      @keyframes cloudMid {
+        0% {
+          top: 10px;
+          left: 420px;
+          opacity: 0;
+        }
+        20% {
+          top: 40px;
+          left: 360px;
+          opacity: 1;
+        }
+        70% {
+          top: 130px;
+          left: 180px;
+          opacity: 1;
+        }
+        100% {
+          top: 160px;
+          left: 120px;
+          opacity: 0;
+        }
+      }
+      @keyframes cloudRight {
+        0% {
+          top: 100px;
+          left: 500px;
+          opacity: 0;
+        }
+        20% {
+          top: 120px;
+          left: 460px;
+          opacity: 1;
+        }
+        80% {
+          top: 180px;
+          left: 340px;
+          opacity: 1;
+        }
+        100% {
+          top: 200px;
+          left: 300px;
+          opacity: 0;
+        }
+      }
+    }
+  }
+  .bullshit {
+    position: relative;
+    float: left;
+    width: 300px;
+    padding: 30px 0;
+    overflow: hidden;
+    &__oops {
+      font-size: 32px;
+      font-weight: bold;
+      line-height: 40px;
+      color: #1482f0;
+      opacity: 0;
+      margin-bottom: 20px;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-fill-mode: forwards;
+    }
+    &__headline {
+      font-size: 20px;
+      line-height: 24px;
+      color: #222;
+      font-weight: bold;
+      opacity: 0;
+      margin-bottom: 10px;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-delay: 0.1s;
+      animation-fill-mode: forwards;
+    }
+    &__info {
+      font-size: 13px;
+      line-height: 21px;
+      color: grey;
+      opacity: 0;
+      margin-bottom: 30px;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-delay: 0.2s;
+      animation-fill-mode: forwards;
+    }
+    &__return-home {
+      display: block;
+      float: left;
+      width: 110px;
+      height: 36px;
+      background: #1482f0;
+      border-radius: 100px;
+      text-align: center;
+      color: #ffffff;
+      opacity: 0;
+      font-size: 14px;
+      line-height: 36px;
+      cursor: pointer;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-delay: 0.3s;
+      animation-fill-mode: forwards;
+    }
+    @keyframes slideUp {
+      0% {
+        transform: translateY(60px);
+        opacity: 0;
+      }
+      100% {
+        transform: translateY(0);
+        opacity: 1;
+      }
+    }
+  }
+}
+</style>
diff --git a/frontend/src/views/example/components/ArticleDetail.vue b/frontend/src/views/example/components/ArticleDetail.vue
new file mode 100644
index 0000000..157497b
--- /dev/null
+++ b/frontend/src/views/example/components/ArticleDetail.vue
@@ -0,0 +1,289 @@
+<template>
+  <div class="createPost-container">
+    <el-form ref="postForm" :model="postForm" :rules="rules" class="form-container">
+
+      <sticky :z-index="10" :class-name="'sub-navbar '+postForm.status">
+        <CommentDropdown v-model="postForm.comment_disabled" />
+        <PlatformDropdown v-model="postForm.platforms" />
+        <SourceUrlDropdown v-model="postForm.source_uri" />
+        <el-button v-loading="loading" style="margin-left: 10px;" type="success" @click="submitForm">
+          Publish
+        </el-button>
+        <el-button v-loading="loading" type="warning" @click="draftForm">
+          Draft
+        </el-button>
+      </sticky>
+
+      <div class="createPost-main-container">
+        <el-row>
+          <Warning />
+
+          <el-col :span="24">
+            <el-form-item style="margin-bottom: 40px;" prop="title">
+              <MDinput v-model="postForm.title" :maxlength="100" name="name" required>
+                Title
+              </MDinput>
+            </el-form-item>
+
+            <div class="postInfo-container">
+              <el-row>
+                <el-col :span="8">
+                  <el-form-item label-width="60px" label="Author:" class="postInfo-container-item">
+                    <el-select v-model="postForm.author" :remote-method="getRemoteUserList" filterable default-first-option remote placeholder="Search user">
+                      <el-option v-for="(item,index) in userListOptions" :key="item+index" :label="item" :value="item" />
+                    </el-select>
+                  </el-form-item>
+                </el-col>
+
+                <el-col :span="10">
+                  <el-form-item label-width="120px" label="Publish Time:" class="postInfo-container-item">
+                    <el-date-picker v-model="displayTime" type="datetime" format="yyyy-MM-dd HH:mm:ss" placeholder="Select date and time" />
+                  </el-form-item>
+                </el-col>
+
+                <el-col :span="6">
+                  <el-form-item label-width="90px" label="Importance:" class="postInfo-container-item">
+                    <el-rate
+                      v-model="postForm.importance"
+                      :max="3"
+                      :colors="['#99A9BF', '#F7BA2A', '#FF9900']"
+                      :low-threshold="1"
+                      :high-threshold="3"
+                      style="display:inline-block"
+                    />
+                  </el-form-item>
+                </el-col>
+              </el-row>
+            </div>
+          </el-col>
+        </el-row>
+
+        <el-form-item style="margin-bottom: 40px;" label-width="70px" label="Summary:">
+          <el-input v-model="postForm.content_short" :rows="1" type="textarea" class="article-textarea" autosize placeholder="Please enter the content" />
+          <span v-show="contentShortLength" class="word-counter">{{ contentShortLength }}words</span>
+        </el-form-item>
+
+        <el-form-item prop="content" style="margin-bottom: 30px;">
+          <Tinymce ref="editor" v-model="postForm.content" :height="400" />
+        </el-form-item>
+
+        <el-form-item prop="image_uri" style="margin-bottom: 30px;">
+          <Upload v-model="postForm.image_uri" />
+        </el-form-item>
+      </div>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import Tinymce from '@/components/Tinymce'
+import Upload from '@/components/Upload/SingleImage3'
+import MDinput from '@/components/MDinput'
+import Sticky from '@/components/Sticky' // 粘性header组件
+import { validURL } from '@/utils/validate'
+import { fetchArticle } from '@/api/article'
+import { searchUser } from '@/api/remote-search'
+import Warning from './Warning'
+import { CommentDropdown, PlatformDropdown, SourceUrlDropdown } from './Dropdown'
+
+const defaultForm = {
+  status: 'draft',
+  title: '', // 文章题目
+  content: '', // 文章内容
+  content_short: '', // 文章摘要
+  source_uri: '', // 文章外链
+  image_uri: '', // 文章图片
+  display_time: undefined, // 前台展示时间
+  id: undefined,
+  platforms: ['a-platform'],
+  comment_disabled: false,
+  importance: 0
+}
+
+export default {
+  name: 'ArticleDetail',
+  components: { Tinymce, MDinput, Upload, Sticky, Warning, CommentDropdown, PlatformDropdown, SourceUrlDropdown },
+  props: {
+    isEdit: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    const validateRequire = (rule, value, callback) => {
+      if (value === '') {
+        this.$message({
+          message: rule.field + '为必传项',
+          type: 'error'
+        })
+        callback(new Error(rule.field + '为必传项'))
+      } else {
+        callback()
+      }
+    }
+    const validateSourceUri = (rule, value, callback) => {
+      if (value) {
+        if (validURL(value)) {
+          callback()
+        } else {
+          this.$message({
+            message: '外链url填写不正确',
+            type: 'error'
+          })
+          callback(new Error('外链url填写不正确'))
+        }
+      } else {
+        callback()
+      }
+    }
+    return {
+      postForm: Object.assign({}, defaultForm),
+      loading: false,
+      userListOptions: [],
+      rules: {
+        image_uri: [{ validator: validateRequire }],
+        title: [{ validator: validateRequire }],
+        content: [{ validator: validateRequire }],
+        source_uri: [{ validator: validateSourceUri, trigger: 'blur' }]
+      },
+      tempRoute: {}
+    }
+  },
+  computed: {
+    contentShortLength() {
+      return this.postForm.content_short.length
+    },
+    displayTime: {
+      // set and get is useful when the data
+      // returned by the back end api is different from the front end
+      // back end return => "2013-06-25 06:59:25"
+      // front end need timestamp => 1372114765000
+      get() {
+        return (+new Date(this.postForm.display_time))
+      },
+      set(val) {
+        this.postForm.display_time = new Date(val)
+      }
+    }
+  },
+  created() {
+    if (this.isEdit) {
+      const id = this.$route.params && this.$route.params.id
+      this.fetchData(id)
+    }
+
+    // Why need to make a copy of this.$route here?
+    // Because if you enter this page and quickly switch tag, may be in the execution of the setTagsViewTitle function, this.$route is no longer pointing to the current page
+    // https://github.com/PanJiaChen/vue-element-admin/issues/1221
+    this.tempRoute = Object.assign({}, this.$route)
+  },
+  methods: {
+    fetchData(id) {
+      fetchArticle(id).then(response => {
+        this.postForm = response.data
+
+        // just for test
+        this.postForm.title += `   Article Id:${this.postForm.id}`
+        this.postForm.content_short += `   Article Id:${this.postForm.id}`
+
+        // set tagsview title
+        this.setTagsViewTitle()
+
+        // set page title
+        this.setPageTitle()
+      }).catch(err => {
+        console.log(err)
+      })
+    },
+    setTagsViewTitle() {
+      const title = 'Edit Article'
+      const route = Object.assign({}, this.tempRoute, { title: `${title}-${this.postForm.id}` })
+      this.$store.dispatch('tagsView/updateVisitedView', route)
+    },
+    setPageTitle() {
+      const title = 'Edit Article'
+      document.title = `${title} - ${this.postForm.id}`
+    },
+    submitForm() {
+      console.log(this.postForm)
+      this.$refs.postForm.validate(valid => {
+        if (valid) {
+          this.loading = true
+          this.$notify({
+            title: '成功',
+            message: '发布文章成功',
+            type: 'success',
+            duration: 2000
+          })
+          this.postForm.status = 'published'
+          this.loading = false
+        } else {
+          console.log('error submit!!')
+          return false
+        }
+      })
+    },
+    draftForm() {
+      if (this.postForm.content.length === 0 || this.postForm.title.length === 0) {
+        this.$message({
+          message: '请填写必要的标题和内容',
+          type: 'warning'
+        })
+        return
+      }
+      this.$message({
+        message: '保存成功',
+        type: 'success',
+        showClose: true,
+        duration: 1000
+      })
+      this.postForm.status = 'draft'
+    },
+    getRemoteUserList(query) {
+      searchUser(query).then(response => {
+        if (!response.data.items) return
+        this.userListOptions = response.data.items.map(v => v.name)
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import "~@/styles/mixin.scss";
+
+.createPost-container {
+  position: relative;
+
+  .createPost-main-container {
+    padding: 40px 45px 20px 50px;
+
+    .postInfo-container {
+      position: relative;
+      @include clearfix;
+      margin-bottom: 10px;
+
+      .postInfo-container-item {
+        float: left;
+      }
+    }
+  }
+
+  .word-counter {
+    width: 40px;
+    position: absolute;
+    right: 10px;
+    top: 0px;
+  }
+}
+
+.article-textarea ::v-deep {
+  textarea {
+    padding-right: 40px;
+    resize: none;
+    border: none;
+    border-radius: 0px;
+    border-bottom: 1px solid #bfcbd9;
+  }
+}
+</style>
diff --git a/frontend/src/views/example/components/Dropdown/Comment.vue b/frontend/src/views/example/components/Dropdown/Comment.vue
new file mode 100644
index 0000000..d34b2b9
--- /dev/null
+++ b/frontend/src/views/example/components/Dropdown/Comment.vue
@@ -0,0 +1,41 @@
+<template>
+  <el-dropdown :show-timeout="100" trigger="click">
+    <el-button plain>
+      {{ !comment_disabled?'Comment: opened':'Comment: closed' }}
+      <i class="el-icon-caret-bottom el-icon--right" />
+    </el-button>
+    <el-dropdown-menu slot="dropdown" class="no-padding">
+      <el-dropdown-item>
+        <el-radio-group v-model="comment_disabled" style="padding: 10px;">
+          <el-radio :label="true">
+            Close comment
+          </el-radio>
+          <el-radio :label="false">
+            Open comment
+          </el-radio>
+        </el-radio-group>
+      </el-dropdown-item>
+    </el-dropdown-menu>
+  </el-dropdown>
+</template>
+
+<script>
+export default {
+  props: {
+    value: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    comment_disabled: {
+      get() {
+        return this.value
+      },
+      set(val) {
+        this.$emit('input', val)
+      }
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/example/components/Dropdown/Platform.vue b/frontend/src/views/example/components/Dropdown/Platform.vue
new file mode 100644
index 0000000..0a52726
--- /dev/null
+++ b/frontend/src/views/example/components/Dropdown/Platform.vue
@@ -0,0 +1,46 @@
+<template>
+  <el-dropdown :hide-on-click="false" :show-timeout="100" trigger="click">
+    <el-button plain>
+      Platfroms({{ platforms.length }})
+      <i class="el-icon-caret-bottom el-icon--right" />
+    </el-button>
+    <el-dropdown-menu slot="dropdown" class="no-border">
+      <el-checkbox-group v-model="platforms" style="padding: 5px 15px;">
+        <el-checkbox v-for="item in platformsOptions" :key="item.key" :label="item.key">
+          {{ item.name }}
+        </el-checkbox>
+      </el-checkbox-group>
+    </el-dropdown-menu>
+  </el-dropdown>
+</template>
+
+<script>
+export default {
+  props: {
+    value: {
+      required: true,
+      default: () => [],
+      type: Array
+    }
+  },
+  data() {
+    return {
+      platformsOptions: [
+        { key: 'a-platform', name: 'a-platform' },
+        { key: 'b-platform', name: 'b-platform' },
+        { key: 'c-platform', name: 'c-platform' }
+      ]
+    }
+  },
+  computed: {
+    platforms: {
+      get() {
+        return this.value
+      },
+      set(val) {
+        this.$emit('input', val)
+      }
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/example/components/Dropdown/SourceUrl.vue b/frontend/src/views/example/components/Dropdown/SourceUrl.vue
new file mode 100644
index 0000000..8f47485
--- /dev/null
+++ b/frontend/src/views/example/components/Dropdown/SourceUrl.vue
@@ -0,0 +1,38 @@
+<template>
+  <el-dropdown :show-timeout="100" trigger="click">
+    <el-button plain>
+      Link
+      <i class="el-icon-caret-bottom el-icon--right" />
+    </el-button>
+    <el-dropdown-menu slot="dropdown" class="no-padding no-border" style="width:400px">
+      <el-form-item label-width="0px" style="margin-bottom: 0px" prop="source_uri">
+        <el-input v-model="source_uri" placeholder="Please enter the content">
+          <template slot="prepend">
+            URL
+          </template>
+        </el-input>
+      </el-form-item>
+    </el-dropdown-menu>
+  </el-dropdown>
+</template>
+
+<script>
+export default {
+  props: {
+    value: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    source_uri: {
+      get() {
+        return this.value
+      },
+      set(val) {
+        this.$emit('input', val)
+      }
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/example/components/Dropdown/index.js b/frontend/src/views/example/components/Dropdown/index.js
new file mode 100644
index 0000000..bc0c171
--- /dev/null
+++ b/frontend/src/views/example/components/Dropdown/index.js
@@ -0,0 +1,3 @@
+export { default as CommentDropdown } from './Comment'
+export { default as PlatformDropdown } from './Platform'
+export { default as SourceUrlDropdown } from './SourceUrl'
diff --git a/frontend/src/views/example/components/Warning.vue b/frontend/src/views/example/components/Warning.vue
new file mode 100644
index 0000000..8d2a7e5
--- /dev/null
+++ b/frontend/src/views/example/components/Warning.vue
@@ -0,0 +1,13 @@
+<template>
+  <aside>
+    Creating and editing pages cannot be cached by keep-alive because keep-alive include does not currently support
+    caching based on routes, so it is currently cached based on component name. If you want to achieve a similar caching
+    effect, you can use a browser caching scheme such as localStorage. Or do not use keep-alive include to cache all
+    pages directly. See details
+    <a
+      href="https://panjiachen.github.io/vue-element-admin-site/guide/essentials/tags-view.html"
+      target="_blank"
+    >Document</a>
+  </aside>
+</template>
+
diff --git a/frontend/src/views/example/create.vue b/frontend/src/views/example/create.vue
new file mode 100644
index 0000000..f28ce28
--- /dev/null
+++ b/frontend/src/views/example/create.vue
@@ -0,0 +1,13 @@
+<template>
+  <article-detail :is-edit="false" />
+</template>
+
+<script>
+import ArticleDetail from './components/ArticleDetail'
+
+export default {
+  name: 'CreateArticle',
+  components: { ArticleDetail }
+}
+</script>
+
diff --git a/frontend/src/views/example/edit.vue b/frontend/src/views/example/edit.vue
new file mode 100644
index 0000000..87b6126
--- /dev/null
+++ b/frontend/src/views/example/edit.vue
@@ -0,0 +1,13 @@
+<template>
+  <article-detail :is-edit="true" />
+</template>
+
+<script>
+import ArticleDetail from './components/ArticleDetail'
+
+export default {
+  name: 'EditForm',
+  components: { ArticleDetail }
+}
+</script>
+
diff --git a/frontend/src/views/example/list.vue b/frontend/src/views/example/list.vue
new file mode 100644
index 0000000..7cdc4ac
--- /dev/null
+++ b/frontend/src/views/example/list.vue
@@ -0,0 +1,112 @@
+<template>
+  <div class="app-container">
+    <el-table v-loading="listLoading" :data="list" border fit highlight-current-row style="width: 100%">
+      <el-table-column align="center" label="ID" width="80">
+        <template slot-scope="scope">
+          <span>{{ scope.row.id }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column width="180px" align="center" label="Date">
+        <template slot-scope="scope">
+          <span>{{ scope.row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column width="120px" align="center" label="Author">
+        <template slot-scope="scope">
+          <span>{{ scope.row.author }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column width="100px" label="Importance">
+        <template slot-scope="scope">
+          <svg-icon v-for="n in +scope.row.importance" :key="n" icon-class="star" class="meta-item__icon" />
+        </template>
+      </el-table-column>
+
+      <el-table-column class-name="status-col" label="Status" width="110">
+        <template slot-scope="{row}">
+          <el-tag :type="row.status | statusFilter">
+            {{ row.status }}
+          </el-tag>
+        </template>
+      </el-table-column>
+
+      <el-table-column min-width="300px" label="Title">
+        <template slot-scope="{row}">
+          <router-link :to="'/example/edit/'+row.id" class="link-type">
+            <span>{{ row.title }}</span>
+          </router-link>
+        </template>
+      </el-table-column>
+
+      <el-table-column align="center" label="Actions" width="120">
+        <template slot-scope="scope">
+          <router-link :to="'/example/edit/'+scope.row.id">
+            <el-button type="primary" size="small" icon="el-icon-edit">
+              Edit
+            </el-button>
+          </router-link>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="listQuery.page" :limit.sync="listQuery.limit" @pagination="getList" />
+  </div>
+</template>
+
+<script>
+import { fetchList } from '@/api/article'
+import Pagination from '@/components/Pagination' // Secondary package based on el-pagination
+
+export default {
+  name: 'ArticleList',
+  components: { Pagination },
+  filters: {
+    statusFilter(status) {
+      const statusMap = {
+        published: 'success',
+        draft: 'info',
+        deleted: 'danger'
+      }
+      return statusMap[status]
+    }
+  },
+  data() {
+    return {
+      list: null,
+      total: 0,
+      listLoading: true,
+      listQuery: {
+        page: 1,
+        limit: 20
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.listLoading = true
+      fetchList(this.listQuery).then(response => {
+        this.list = response.data.items
+        this.total = response.data.total
+        this.listLoading = false
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.edit-input {
+  padding-right: 100px;
+}
+.cancel-btn {
+  position: absolute;
+  right: 15px;
+  top: 10px;
+}
+</style>
diff --git a/frontend/src/views/excel/components/AutoWidthOption.vue b/frontend/src/views/excel/components/AutoWidthOption.vue
new file mode 100644
index 0000000..9050e65
--- /dev/null
+++ b/frontend/src/views/excel/components/AutoWidthOption.vue
@@ -0,0 +1,34 @@
+<template>
+  <div style="display:inline-block;">
+    <label class="radio-label">Cell Auto-Width: </label>
+    <el-radio-group v-model="autoWidth">
+      <el-radio :label="true" border>
+        True
+      </el-radio>
+      <el-radio :label="false" border>
+        False
+      </el-radio>
+    </el-radio-group>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    value: {
+      type: Boolean,
+      default: true
+    }
+  },
+  computed: {
+    autoWidth: {
+      get() {
+        return this.value
+      },
+      set(val) {
+        this.$emit('input', val)
+      }
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/excel/components/BookTypeOption.vue b/frontend/src/views/excel/components/BookTypeOption.vue
new file mode 100644
index 0000000..af9fed3
--- /dev/null
+++ b/frontend/src/views/excel/components/BookTypeOption.vue
@@ -0,0 +1,39 @@
+<template>
+  <div style="display:inline-block;">
+    <label class="radio-label">Book Type: </label>
+    <el-select v-model="bookType" style="width:120px;">
+      <el-option
+        v-for="item in options"
+        :key="item"
+        :label="item"
+        :value="item"
+      />
+    </el-select>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    value: {
+      type: String,
+      default: 'xlsx'
+    }
+  },
+  data() {
+    return {
+      options: ['xlsx', 'csv', 'txt']
+    }
+  },
+  computed: {
+    bookType: {
+      get() {
+        return this.value
+      },
+      set(val) {
+        this.$emit('input', val)
+      }
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/excel/components/FilenameOption.vue b/frontend/src/views/excel/components/FilenameOption.vue
new file mode 100644
index 0000000..7509223
--- /dev/null
+++ b/frontend/src/views/excel/components/FilenameOption.vue
@@ -0,0 +1,27 @@
+<template>
+  <div style="display:inline-block;">
+    <label class="radio-label" style="padding-left:0;">Filename: </label>
+    <el-input v-model="filename" placeholder="Please enter the file name (default excel-list)" style="width:345px;" prefix-icon="el-icon-document" />
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    value: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    filename: {
+      get() {
+        return this.value
+      },
+      set(val) {
+        this.$emit('input', val)
+      }
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/excel/export-excel.vue b/frontend/src/views/excel/export-excel.vue
new file mode 100644
index 0000000..b4d7500
--- /dev/null
+++ b/frontend/src/views/excel/export-excel.vue
@@ -0,0 +1,116 @@
+<template>
+  <div class="app-container">
+
+    <div>
+      <FilenameOption v-model="filename" />
+      <AutoWidthOption v-model="autoWidth" />
+      <BookTypeOption v-model="bookType" />
+      <el-button :loading="downloadLoading" style="margin:0 0 20px 20px;" type="primary" icon="el-icon-document" @click="handleDownload">
+        Export Excel
+      </el-button>
+      <a href="https://panjiachen.github.io/vue-element-admin-site/feature/component/excel.html" target="_blank" style="margin-left:15px;">
+        <el-tag type="info">Documentation</el-tag>
+      </a>
+    </div>
+
+    <el-table v-loading="listLoading" :data="list" element-loading-text="Loading..." border fit highlight-current-row>
+      <el-table-column align="center" label="Id" width="95">
+        <template slot-scope="scope">
+          {{ scope.$index }}
+        </template>
+      </el-table-column>
+      <el-table-column label="Title">
+        <template slot-scope="scope">
+          {{ scope.row.title }}
+        </template>
+      </el-table-column>
+      <el-table-column label="Author" width="110" align="center">
+        <template slot-scope="scope">
+          <el-tag>{{ scope.row.author }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="Readings" width="115" align="center">
+        <template slot-scope="scope">
+          {{ scope.row.pageviews }}
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="Date" width="220">
+        <template slot-scope="scope">
+          <i class="el-icon-time" />
+          <span>{{ scope.row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+import { fetchList } from '@/api/article'
+import { parseTime } from '@/utils'
+// options components
+import FilenameOption from './components/FilenameOption'
+import AutoWidthOption from './components/AutoWidthOption'
+import BookTypeOption from './components/BookTypeOption'
+
+export default {
+  name: 'ExportExcel',
+  components: { FilenameOption, AutoWidthOption, BookTypeOption },
+  data() {
+    return {
+      list: null,
+      listLoading: true,
+      downloadLoading: false,
+      filename: '',
+      autoWidth: true,
+      bookType: 'xlsx'
+    }
+  },
+  created() {
+    this.fetchData()
+  },
+  methods: {
+    fetchData() {
+      this.listLoading = true
+      fetchList().then(response => {
+        this.list = response.data.items
+        this.listLoading = false
+      })
+    },
+    handleDownload() {
+      this.downloadLoading = true
+      import('@/vendor/Export2Excel').then(excel => {
+        const tHeader = ['Id', 'Title', 'Author', 'Readings', 'Date']
+        const filterVal = ['id', 'title', 'author', 'pageviews', 'display_time']
+        const list = this.list
+        const data = this.formatJson(filterVal, list)
+        excel.export_json_to_excel({
+          header: tHeader,
+          data,
+          filename: this.filename,
+          autoWidth: this.autoWidth,
+          bookType: this.bookType
+        })
+        this.downloadLoading = false
+      })
+    },
+    formatJson(filterVal, jsonData) {
+      return jsonData.map(v => filterVal.map(j => {
+        if (j === 'timestamp') {
+          return parseTime(v[j])
+        } else {
+          return v[j]
+        }
+      }))
+    }
+  }
+}
+</script>
+
+<style>
+.radio-label {
+  font-size: 14px;
+  color: #606266;
+  line-height: 40px;
+  padding: 0 12px 0 30px;
+}
+</style>
diff --git a/frontend/src/views/excel/merge-header.vue b/frontend/src/views/excel/merge-header.vue
new file mode 100644
index 0000000..4be0227
--- /dev/null
+++ b/frontend/src/views/excel/merge-header.vue
@@ -0,0 +1,101 @@
+<template>
+  <div class="app-container">
+
+    <el-button :loading="downloadLoading" style="margin-bottom:20px" type="primary" icon="el-icon-document" @click="handleDownload">Export</el-button>
+
+    <el-table
+      ref="multipleTable"
+      v-loading="listLoading"
+      :data="list"
+      element-loading-text="Loading"
+      border
+      fit
+      highlight-current-row
+    >
+      <el-table-column align="center" label="Id" width="95">
+        <template slot-scope="scope">
+          {{ scope.$index }}
+        </template>
+      </el-table-column>
+      <el-table-column label="Main Information" align="center">
+        <el-table-column label="Title">
+          <template slot-scope="scope">
+            {{ scope.row.title }}
+          </template>
+        </el-table-column>
+        <el-table-column label="Author" width="110" align="center">
+          <template slot-scope="scope">
+            <el-tag>{{ scope.row.author }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="Readings" width="115" align="center">
+          <template slot-scope="scope">
+            {{ scope.row.pageviews }}
+          </template>
+        </el-table-column>
+      </el-table-column>
+      <el-table-column align="center" label="Date" width="220">
+        <template slot-scope="scope">
+          <i class="el-icon-time" />
+          <span>{{ scope.row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+  </div>
+</template>
+
+<script>
+import { fetchList } from '@/api/article'
+import { parseTime } from '@/utils'
+
+export default {
+  name: 'MergeHeader',
+  data() {
+    return {
+      list: null,
+      listLoading: true,
+      downloadLoading: false
+    }
+  },
+  created() {
+    this.fetchData()
+  },
+  methods: {
+    fetchData() {
+      this.listLoading = true
+      fetchList(this.listQuery).then(response => {
+        this.list = response.data.items
+        this.listLoading = false
+      })
+    },
+    handleDownload() {
+      this.downloadLoading = true
+      import('@/vendor/Export2Excel').then(excel => {
+        const multiHeader = [['Id', 'Main Information', '', '', 'Date']]
+        const header = ['', 'Title', 'Author', 'Readings', '']
+        const filterVal = ['id', 'title', 'author', 'pageviews', 'display_time']
+        const list = this.list
+        const data = this.formatJson(filterVal, list)
+        const merges = ['A1:A2', 'B1:D1', 'E1:E2']
+        excel.export_json_to_excel({
+          multiHeader,
+          header,
+          merges,
+          data
+        })
+        this.downloadLoading = false
+      })
+    },
+    formatJson(filterVal, jsonData) {
+      return jsonData.map(v => filterVal.map(j => {
+        if (j === 'timestamp') {
+          return parseTime(v[j])
+        } else {
+          return v[j]
+        }
+      }))
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/excel/select-excel.vue b/frontend/src/views/excel/select-excel.vue
new file mode 100644
index 0000000..05b85fb
--- /dev/null
+++ b/frontend/src/views/excel/select-excel.vue
@@ -0,0 +1,107 @@
+<template>
+  <div class="app-container">
+    <el-input v-model="filename" placeholder="Please enter the file name (default excel-list)" style="width:350px;" prefix-icon="el-icon-document" />
+    <el-button :loading="downloadLoading" style="margin-bottom:20px" type="primary" icon="el-icon-document" @click="handleDownload">
+      Export Selected Items
+    </el-button>
+    <a href="https://panjiachen.github.io/vue-element-admin-site/feature/component/excel.html" target="_blank" style="margin-left:15px;">
+      <el-tag type="info">Documentation</el-tag>
+    </a>
+    <el-table
+      ref="multipleTable"
+      v-loading="listLoading"
+      :data="list"
+      element-loading-text="拼命加载中"
+      border
+      fit
+      highlight-current-row
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" align="center" />
+      <el-table-column align="center" label="Id" width="95">
+        <template slot-scope="scope">
+          {{ scope.$index }}
+        </template>
+      </el-table-column>
+      <el-table-column label="Title">
+        <template slot-scope="scope">
+          {{ scope.row.title }}
+        </template>
+      </el-table-column>
+      <el-table-column label="Author" width="110" align="center">
+        <template slot-scope="scope">
+          <el-tag>{{ scope.row.author }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="Readings" width="115" align="center">
+        <template slot-scope="scope">
+          {{ scope.row.pageviews }}
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="PDate" width="220">
+        <template slot-scope="scope">
+          <i class="el-icon-time" />
+          <span>{{ scope.row.display_time }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+import { fetchList } from '@/api/article'
+
+export default {
+  name: 'SelectExcel',
+  data() {
+    return {
+      list: null,
+      listLoading: true,
+      multipleSelection: [],
+      downloadLoading: false,
+      filename: ''
+    }
+  },
+  created() {
+    this.fetchData()
+  },
+  methods: {
+    fetchData() {
+      this.listLoading = true
+      fetchList(this.listQuery).then(response => {
+        this.list = response.data.items
+        this.listLoading = false
+      })
+    },
+    handleSelectionChange(val) {
+      this.multipleSelection = val
+    },
+    handleDownload() {
+      if (this.multipleSelection.length) {
+        this.downloadLoading = true
+        import('@/vendor/Export2Excel').then(excel => {
+          const tHeader = ['Id', 'Title', 'Author', 'Readings', 'Date']
+          const filterVal = ['id', 'title', 'author', 'pageviews', 'display_time']
+          const list = this.multipleSelection
+          const data = this.formatJson(filterVal, list)
+          excel.export_json_to_excel({
+            header: tHeader,
+            data,
+            filename: this.filename
+          })
+          this.$refs.multipleTable.clearSelection()
+          this.downloadLoading = false
+        })
+      } else {
+        this.$message({
+          message: 'Please select at least one item',
+          type: 'warning'
+        })
+      }
+    },
+    formatJson(filterVal, jsonData) {
+      return jsonData.map(v => filterVal.map(j => v[j]))
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/excel/upload-excel.vue b/frontend/src/views/excel/upload-excel.vue
new file mode 100644
index 0000000..1772b7f
--- /dev/null
+++ b/frontend/src/views/excel/upload-excel.vue
@@ -0,0 +1,42 @@
+<template>
+  <div class="app-container">
+    <upload-excel-component :on-success="handleSuccess" :before-upload="beforeUpload" />
+    <el-table :data="tableData" border highlight-current-row style="width: 100%;margin-top:20px;">
+      <el-table-column v-for="item of tableHeader" :key="item" :prop="item" :label="item" />
+    </el-table>
+  </div>
+</template>
+
+<script>
+import UploadExcelComponent from '@/components/UploadExcel/index.vue'
+
+export default {
+  name: 'UploadExcel',
+  components: { UploadExcelComponent },
+  data() {
+    return {
+      tableData: [],
+      tableHeader: []
+    }
+  },
+  methods: {
+    beforeUpload(file) {
+      const isLt1M = file.size / 1024 / 1024 < 1
+
+      if (isLt1M) {
+        return true
+      }
+
+      this.$message({
+        message: 'Please do not upload files larger than 1m in size.',
+        type: 'warning'
+      })
+      return false
+    },
+    handleSuccess({ results, header }) {
+      this.tableData = results
+      this.tableHeader = header
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/guide/index.vue b/frontend/src/views/guide/index.vue
new file mode 100644
index 0000000..4897c8f
--- /dev/null
+++ b/frontend/src/views/guide/index.vue
@@ -0,0 +1,36 @@
+<template>
+  <div class="app-container">
+    <aside>
+      The guide page is useful for some people who entered the project for the first time. You can briefly introduce the
+      features of the project. Demo is based on
+      <a href="https://github.com/kamranahmedse/driver.js" target="_blank">driver.js.</a>
+    </aside>
+    <el-button icon="el-icon-question" type="primary" @click.prevent.stop="guide">
+      Show Guide
+    </el-button>
+  </div>
+</template>
+
+<script>
+import Driver from 'driver.js' // import driver.js
+import 'driver.js/dist/driver.min.css' // import driver.js css
+import steps from './steps'
+
+export default {
+  name: 'Guide',
+  data() {
+    return {
+      driver: null
+    }
+  },
+  mounted() {
+    this.driver = new Driver()
+  },
+  methods: {
+    guide() {
+      this.driver.defineSteps(steps)
+      this.driver.start()
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/guide/steps.js b/frontend/src/views/guide/steps.js
new file mode 100644
index 0000000..283c672
--- /dev/null
+++ b/frontend/src/views/guide/steps.js
@@ -0,0 +1,53 @@
+const steps = [
+  {
+    element: '#hamburger-container',
+    popover: {
+      title: 'Hamburger',
+      description: 'Open && Close sidebar',
+      position: 'bottom'
+    }
+  },
+  {
+    element: '#breadcrumb-container',
+    popover: {
+      title: 'Breadcrumb',
+      description: 'Indicate the current page location',
+      position: 'bottom'
+    }
+  },
+  {
+    element: '#header-search',
+    popover: {
+      title: 'Page Search',
+      description: 'Page search, quick navigation',
+      position: 'left'
+    }
+  },
+  {
+    element: '#screenfull',
+    popover: {
+      title: 'Screenfull',
+      description: 'Set the page into fullscreen',
+      position: 'left'
+    }
+  },
+  {
+    element: '#size-select',
+    popover: {
+      title: 'Switch Size',
+      description: 'Switch the system size',
+      position: 'left'
+    }
+  },
+  {
+    element: '#tags-view-container',
+    popover: {
+      title: 'Tags view',
+      description: 'The history of the page you visited',
+      position: 'bottom'
+    },
+    padding: 0
+  }
+]
+
+export default steps
diff --git a/frontend/src/views/icons/element-icons.js b/frontend/src/views/icons/element-icons.js
new file mode 100644
index 0000000..9ea4d63
--- /dev/null
+++ b/frontend/src/views/icons/element-icons.js
@@ -0,0 +1,3 @@
+const elementIcons = ['platform-eleme', 'eleme', 'delete-solid', 'delete', 's-tools', 'setting', 'user-solid', 'user', 'phone', 'phone-outline', 'more', 'more-outline', 'star-on', 'star-off', 's-goods', 'goods', 'warning', 'warning-outline', 'question', 'info', 'remove', 'circle-plus', 'success', 'error', 'zoom-in', 'zoom-out', 'remove-outline', 'circle-plus-outline', 'circle-check', 'circle-close', 's-help', 'help', 'minus', 'plus', 'check', 'close', 'picture', 'picture-outline', 'picture-outline-round', 'upload', 'upload2', 'download', 'camera-solid', 'camera', 'video-camera-solid', 'video-camera', 'message-solid', 'bell', 's-cooperation', 's-order', 's-platform', 's-fold', 's-unfold', 's-operation', 's-promotion', 's-home', 's-release', 's-ticket', 's-management', 's-open', 's-shop', 's-marketing', 's-flag', 's-comment', 's-finance', 's-claim', 's-custom', 's-opportunity', 's-data', 's-check', 's-grid', 'menu', 'share', 'd-caret', 'caret-left', 'caret-right', 'caret-bottom', 'caret-top', 'bottom-left', 'bottom-right', 'back', 'right', 'bottom', 'top', 'top-left', 'top-right', 'arrow-left', 'arrow-right', 'arrow-down', 'arrow-up', 'd-arrow-left', 'd-arrow-right', 'video-pause', 'video-play', 'refresh', 'refresh-right', 'refresh-left', 'finished', 'sort', 'sort-up', 'sort-down', 'rank', 'loading', 'view', 'c-scale-to-original', 'date', 'edit', 'edit-outline', 'folder', 'folder-opened', 'folder-add', 'folder-remove', 'folder-delete', 'folder-checked', 'tickets', 'document-remove', 'document-delete', 'document-copy', 'document-checked', 'document', 'document-add', 'printer', 'paperclip', 'takeaway-box', 'search', 'monitor', 'attract', 'mobile', 'scissors', 'umbrella', 'headset', 'brush', 'mouse', 'coordinate', 'magic-stick', 'reading', 'data-line', 'data-board', 'pie-chart', 'data-analysis', 'collection-tag', 'film', 'suitcase', 'suitcase-1', 'receiving', 'collection', 'files', 'notebook-1', 'notebook-2', 'toilet-paper', 'office-building', 'school', 'table-lamp', 'house', 'no-smoking', 'smoking', 'shopping-cart-full', 'shopping-cart-1', 'shopping-cart-2', 'shopping-bag-1', 'shopping-bag-2', 'sold-out', 'sell', 'present', 'box', 'bank-card', 'money', 'coin', 'wallet', 'discount', 'price-tag', 'news', 'guide', 'male', 'female', 'thumb', 'cpu', 'link', 'connection', 'open', 'turn-off', 'set-up', 'chat-round', 'chat-line-round', 'chat-square', 'chat-dot-round', 'chat-dot-square', 'chat-line-square', 'message', 'postcard', 'position', 'turn-off-microphone', 'microphone', 'close-notification', 'bangzhu', 'time', 'odometer', 'crop', 'aim', 'switch-button', 'full-screen', 'copy-document', 'mic', 'stopwatch', 'medal-1', 'medal', 'trophy', 'trophy-1', 'first-aid-kit', 'discover', 'place', 'location', 'location-outline', 'location-information', 'add-location', 'delete-location', 'map-location', 'alarm-clock', 'timer', 'watch-1', 'watch', 'lock', 'unlock', 'key', 'service', 'mobile-phone', 'bicycle', 'truck', 'ship', 'basketball', 'football', 'soccer', 'baseball', 'wind-power', 'light-rain', 'lightning', 'heavy-rain', 'sunrise', 'sunrise-1', 'sunset', 'sunny', 'cloudy', 'partly-cloudy', 'cloudy-and-sunny', 'moon', 'moon-night', 'dish', 'dish-1', 'food', 'chicken', 'fork-spoon', 'knife-fork', 'burger', 'tableware', 'sugar', 'dessert', 'ice-cream', 'hot-water', 'water-cup', 'coffee-cup', 'cold-drink', 'goblet', 'goblet-full', 'goblet-square', 'goblet-square-full', 'refrigerator', 'grape', 'watermelon', 'cherry', 'apple', 'pear', 'orange', 'coffee', 'ice-tea', 'ice-drink', 'milk-tea', 'potato-strips', 'lollipop', 'ice-cream-square', 'ice-cream-round']
+
+export default elementIcons
diff --git a/frontend/src/views/icons/index.vue b/frontend/src/views/icons/index.vue
new file mode 100644
index 0000000..4c483db
--- /dev/null
+++ b/frontend/src/views/icons/index.vue
@@ -0,0 +1,101 @@
+<template>
+  <div class="icons-container">
+    <aside>
+      <a href="https://panjiachen.github.io/vue-element-admin-site/guide/advanced/icon.html" target="_blank">Add and use
+      </a>
+    </aside>
+    <el-tabs type="border-card">
+      <el-tab-pane label="Icons">
+        <div class="grid">
+          <div v-for="item of svgIcons" :key="item" @click="handleClipboard(generateIconCode(item),$event)">
+            <el-tooltip placement="top">
+              <div slot="content">
+                {{ generateIconCode(item) }}
+              </div>
+              <div class="icon-item">
+                <svg-icon :icon-class="item" class-name="disabled" />
+                <span>{{ item }}</span>
+              </div>
+            </el-tooltip>
+          </div>
+        </div>
+      </el-tab-pane>
+      <el-tab-pane label="Element-UI Icons">
+        <div class="grid">
+          <div v-for="item of elementIcons" :key="item" @click="handleClipboard(generateElementIconCode(item),$event)">
+            <el-tooltip placement="top">
+              <div slot="content">
+                {{ generateElementIconCode(item) }}
+              </div>
+              <div class="icon-item">
+                <i :class="'el-icon-' + item" />
+                <span>{{ item }}</span>
+              </div>
+            </el-tooltip>
+          </div>
+        </div>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script>
+import clipboard from '@/utils/clipboard'
+import svgIcons from './svg-icons'
+import elementIcons from './element-icons'
+
+export default {
+  name: 'Icons',
+  data() {
+    return {
+      svgIcons,
+      elementIcons
+    }
+  },
+  methods: {
+    generateIconCode(symbol) {
+      return `<svg-icon icon-class="${symbol}" />`
+    },
+    generateElementIconCode(symbol) {
+      return `<i class="el-icon-${symbol}" />`
+    },
+    handleClipboard(text, event) {
+      clipboard(text, event)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.icons-container {
+  margin: 10px 20px 0;
+  overflow: hidden;
+
+  .grid {
+    position: relative;
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+  }
+
+  .icon-item {
+    margin: 20px;
+    height: 85px;
+    text-align: center;
+    width: 100px;
+    float: left;
+    font-size: 30px;
+    color: #24292e;
+    cursor: pointer;
+  }
+
+  span {
+    display: block;
+    font-size: 16px;
+    margin-top: 10px;
+  }
+
+  .disabled {
+    pointer-events: none;
+  }
+}
+</style>
diff --git a/frontend/src/views/icons/svg-icons.js b/frontend/src/views/icons/svg-icons.js
new file mode 100644
index 0000000..1e3c66d
--- /dev/null
+++ b/frontend/src/views/icons/svg-icons.js
@@ -0,0 +1,10 @@
+const req = require.context('../../icons/svg', false, /\.svg$/)
+const requireAll = requireContext => requireContext.keys()
+
+const re = /\.\/(.*)\.svg/
+
+const svgIcons = requireAll(req).map(i => {
+  return i.match(re)[1]
+})
+
+export default svgIcons
diff --git a/frontend/src/views/login/auth-redirect.vue b/frontend/src/views/login/auth-redirect.vue
new file mode 100644
index 0000000..7df8934
--- /dev/null
+++ b/frontend/src/views/login/auth-redirect.vue
@@ -0,0 +1,15 @@
+<script>
+export default {
+  name: 'AuthRedirect',
+  created() {
+    const hash = window.location.search.slice(1)
+    if (window.localStorage) {
+      window.localStorage.setItem('x-admin-oauth-code', hash)
+      window.close()
+    }
+  },
+  render: function(h) {
+    return h() // avoid warning message
+  }
+}
+</script>
diff --git a/frontend/src/views/login/components/SocialSignin.vue b/frontend/src/views/login/components/SocialSignin.vue
new file mode 100644
index 0000000..e9bf4f2
--- /dev/null
+++ b/frontend/src/views/login/components/SocialSignin.vue
@@ -0,0 +1,72 @@
+<template>
+  <div class="social-signup-container">
+    <div class="sign-btn" @click="wechatHandleClick('wechat')">
+      <span class="wx-svg-container"><svg-icon icon-class="wechat" class="icon" /></span>
+      WeChat
+    </div>
+    <div class="sign-btn" @click="tencentHandleClick('tencent')">
+      <span class="qq-svg-container"><svg-icon icon-class="qq" class="icon" /></span>
+      QQ
+    </div>
+  </div>
+</template>
+
+<script>
+// import openWindow from '@/utils/open-window'
+
+export default {
+  name: 'SocialSignin',
+  methods: {
+    wechatHandleClick(thirdpart) {
+      alert('ok')
+      // this.$store.commit('SET_AUTH_TYPE', thirdpart)
+      // const appid = 'xxxxx'
+      // const redirect_uri = encodeURIComponent('xxx/redirect?redirect=' + window.location.origin + '/auth-redirect')
+      // const url = 'https://open.weixin.qq.com/connect/qrconnect?appid=' + appid + '&redirect_uri=' + redirect_uri + '&response_type=code&scope=snsapi_login#wechat_redirect'
+      // openWindow(url, thirdpart, 540, 540)
+    },
+    tencentHandleClick(thirdpart) {
+      alert('ok')
+      // this.$store.commit('SET_AUTH_TYPE', thirdpart)
+      // const client_id = 'xxxxx'
+      // const redirect_uri = encodeURIComponent('xxx/redirect?redirect=' + window.location.origin + '/auth-redirect')
+      // const url = 'https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=' + client_id + '&redirect_uri=' + redirect_uri
+      // openWindow(url, thirdpart, 540, 540)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  .social-signup-container {
+    margin: 20px 0;
+    .sign-btn {
+      display: inline-block;
+      cursor: pointer;
+    }
+    .icon {
+      color: #fff;
+      font-size: 24px;
+      margin-top: 8px;
+    }
+    .wx-svg-container,
+    .qq-svg-container {
+      display: inline-block;
+      width: 40px;
+      height: 40px;
+      line-height: 40px;
+      text-align: center;
+      padding-top: 1px;
+      border-radius: 4px;
+      margin-bottom: 20px;
+      margin-right: 5px;
+    }
+    .wx-svg-container {
+      background-color: #24da70;
+    }
+    .qq-svg-container {
+      background-color: #6BA2D6;
+      margin-left: 50px;
+    }
+  }
+</style>
diff --git a/frontend/src/views/login/index.vue b/frontend/src/views/login/index.vue
new file mode 100644
index 0000000..2590640
--- /dev/null
+++ b/frontend/src/views/login/index.vue
@@ -0,0 +1,324 @@
+<template>
+  <div class="login-container">
+    <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" autocomplete="on" label-position="left">
+
+      <div class="title-container">
+        <h3 class="title">Login Form</h3>
+      </div>
+
+      <el-form-item prop="username">
+        <span class="svg-container">
+          <svg-icon icon-class="user" />
+        </span>
+        <el-input
+          ref="username"
+          v-model="loginForm.username"
+          placeholder="Username"
+          name="username"
+          type="text"
+          tabindex="1"
+          autocomplete="on"
+        />
+      </el-form-item>
+
+      <el-tooltip v-model="capsTooltip" content="Caps lock is On" placement="right" manual>
+        <el-form-item prop="password">
+          <span class="svg-container">
+            <svg-icon icon-class="password" />
+          </span>
+          <el-input
+            :key="passwordType"
+            ref="password"
+            v-model="loginForm.password"
+            :type="passwordType"
+            placeholder="Password"
+            name="password"
+            tabindex="2"
+            autocomplete="on"
+            @keyup.native="checkCapslock"
+            @blur="capsTooltip = false"
+            @keyup.enter.native="handleLogin"
+          />
+          <span class="show-pwd" @click="showPwd">
+            <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
+          </span>
+        </el-form-item>
+      </el-tooltip>
+
+      <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">Login</el-button>
+
+      <div style="position:relative">
+        <div class="tips">
+          <span>Username : admin</span>
+          <span>Password : any</span>
+        </div>
+        <div class="tips">
+          <span style="margin-right:18px;">Username : editor</span>
+          <span>Password : any</span>
+        </div>
+
+        <el-button class="thirdparty-button" type="primary" @click="showDialog=true">
+          Or connect with
+        </el-button>
+      </div>
+    </el-form>
+
+    <el-dialog title="Or connect with" :visible.sync="showDialog">
+      Can not be simulated on local, so please combine you own business simulation! ! !
+      <br>
+      <br>
+      <br>
+      <social-sign />
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { validUsername } from '@/utils/validate'
+import SocialSign from './components/SocialSignin'
+
+export default {
+  name: 'Login',
+  components: { SocialSign },
+  data() {
+    const validateUsername = (rule, value, callback) => {
+      if (!validUsername(value)) {
+        callback(new Error('Please enter the correct user name'))
+      } else {
+        callback()
+      }
+    }
+    const validatePassword = (rule, value, callback) => {
+      if (value.length < 6) {
+        callback(new Error('The password can not be less than 6 digits'))
+      } else {
+        callback()
+      }
+    }
+    return {
+      loginForm: {
+        username: 'admin',
+        password: '111111'
+      },
+      loginRules: {
+        username: [{ required: true, trigger: 'blur', validator: validateUsername }],
+        password: [{ required: true, trigger: 'blur', validator: validatePassword }]
+      },
+      passwordType: 'password',
+      capsTooltip: false,
+      loading: false,
+      showDialog: false,
+      redirect: undefined,
+      otherQuery: {}
+    }
+  },
+  watch: {
+    $route: {
+      handler: function(route) {
+        const query = route.query
+        if (query) {
+          this.redirect = query.redirect
+          this.otherQuery = this.getOtherQuery(query)
+        }
+      },
+      immediate: true
+    }
+  },
+  created() {
+    // window.addEventListener('storage', this.afterQRScan)
+  },
+  mounted() {
+    if (this.loginForm.username === '') {
+      this.$refs.username.focus()
+    } else if (this.loginForm.password === '') {
+      this.$refs.password.focus()
+    }
+  },
+  destroyed() {
+    // window.removeEventListener('storage', this.afterQRScan)
+  },
+  methods: {
+    checkCapslock(e) {
+      const { key } = e
+      this.capsTooltip = key && key.length === 1 && (key >= 'A' && key <= 'Z')
+    },
+    showPwd() {
+      if (this.passwordType === 'password') {
+        this.passwordType = ''
+      } else {
+        this.passwordType = 'password'
+      }
+      this.$nextTick(() => {
+        this.$refs.password.focus()
+      })
+    },
+    handleLogin() {
+      this.$refs.loginForm.validate(valid => {
+        if (valid) {
+          this.loading = true
+          this.$store.dispatch('user/login', this.loginForm)
+            .then(() => {
+              this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
+              this.loading = false
+            })
+            .catch(() => {
+              this.loading = false
+            })
+        } else {
+          console.log('error submit!!')
+          return false
+        }
+      })
+    },
+    getOtherQuery(query) {
+      return Object.keys(query).reduce((acc, cur) => {
+        if (cur !== 'redirect') {
+          acc[cur] = query[cur]
+        }
+        return acc
+      }, {})
+    }
+    // afterQRScan() {
+    //   if (e.key === 'x-admin-oauth-code') {
+    //     const code = getQueryObject(e.newValue)
+    //     const codeMap = {
+    //       wechat: 'code',
+    //       tencent: 'code'
+    //     }
+    //     const type = codeMap[this.auth_type]
+    //     const codeName = code[type]
+    //     if (codeName) {
+    //       this.$store.dispatch('LoginByThirdparty', codeName).then(() => {
+    //         this.$router.push({ path: this.redirect || '/' })
+    //       })
+    //     } else {
+    //       alert('第三方登录失败')
+    //     }
+    //   }
+    // }
+  }
+}
+</script>
+
+<style lang="scss">
+/* 修复input 背景不协调 和光标变色 */
+/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */
+
+$bg:#283443;
+$light_gray:#fff;
+$cursor: #fff;
+
+@supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
+  .login-container .el-input input {
+    color: $cursor;
+  }
+}
+
+/* reset element-ui css */
+.login-container {
+  .el-input {
+    display: inline-block;
+    height: 47px;
+    width: 85%;
+
+    input {
+      background: transparent;
+      border: 0px;
+      -webkit-appearance: none;
+      border-radius: 0px;
+      padding: 12px 5px 12px 15px;
+      color: $light_gray;
+      height: 47px;
+      caret-color: $cursor;
+
+      &:-webkit-autofill {
+        box-shadow: 0 0 0px 1000px $bg inset !important;
+        -webkit-text-fill-color: $cursor !important;
+      }
+    }
+  }
+
+  .el-form-item {
+    border: 1px solid rgba(255, 255, 255, 0.1);
+    background: rgba(0, 0, 0, 0.1);
+    border-radius: 5px;
+    color: #454545;
+  }
+}
+</style>
+
+<style lang="scss" scoped>
+$bg:#2d3a4b;
+$dark_gray:#889aa4;
+$light_gray:#eee;
+
+.login-container {
+  min-height: 100%;
+  width: 100%;
+  background-color: $bg;
+  overflow: hidden;
+
+  .login-form {
+    position: relative;
+    width: 520px;
+    max-width: 100%;
+    padding: 160px 35px 0;
+    margin: 0 auto;
+    overflow: hidden;
+  }
+
+  .tips {
+    font-size: 14px;
+    color: #fff;
+    margin-bottom: 10px;
+
+    span {
+      &:first-of-type {
+        margin-right: 16px;
+      }
+    }
+  }
+
+  .svg-container {
+    padding: 6px 5px 6px 15px;
+    color: $dark_gray;
+    vertical-align: middle;
+    width: 30px;
+    display: inline-block;
+  }
+
+  .title-container {
+    position: relative;
+
+    .title {
+      font-size: 26px;
+      color: $light_gray;
+      margin: 0px auto 40px auto;
+      text-align: center;
+      font-weight: bold;
+    }
+  }
+
+  .show-pwd {
+    position: absolute;
+    right: 10px;
+    top: 7px;
+    font-size: 16px;
+    color: $dark_gray;
+    cursor: pointer;
+    user-select: none;
+  }
+
+  .thirdparty-button {
+    position: absolute;
+    right: 0;
+    bottom: 6px;
+  }
+
+  @media only screen and (max-width: 470px) {
+    .thirdparty-button {
+      display: none;
+    }
+  }
+}
+</style>
diff --git a/frontend/src/views/nested/menu1/index.vue b/frontend/src/views/nested/menu1/index.vue
new file mode 100644
index 0000000..30cb670
--- /dev/null
+++ b/frontend/src/views/nested/menu1/index.vue
@@ -0,0 +1,7 @@
+<template>
+  <div style="padding:30px;">
+    <el-alert :closable="false" title="menu 1">
+      <router-view />
+    </el-alert>
+  </div>
+</template>
diff --git a/frontend/src/views/nested/menu1/menu1-1/index.vue b/frontend/src/views/nested/menu1/menu1-1/index.vue
new file mode 100644
index 0000000..27e173a
--- /dev/null
+++ b/frontend/src/views/nested/menu1/menu1-1/index.vue
@@ -0,0 +1,7 @@
+<template>
+  <div style="padding:30px;">
+    <el-alert :closable="false" title="menu 1-1" type="success">
+      <router-view />
+    </el-alert>
+  </div>
+</template>
diff --git a/frontend/src/views/nested/menu1/menu1-2/index.vue b/frontend/src/views/nested/menu1/menu1-2/index.vue
new file mode 100644
index 0000000..0c86276
--- /dev/null
+++ b/frontend/src/views/nested/menu1/menu1-2/index.vue
@@ -0,0 +1,7 @@
+<template>
+  <div style="padding:30px;">
+    <el-alert :closable="false" title="menu 1-2" type="success">
+      <router-view />
+    </el-alert>
+  </div>
+</template>
diff --git a/frontend/src/views/nested/menu1/menu1-2/menu1-2-1/index.vue b/frontend/src/views/nested/menu1/menu1-2/menu1-2-1/index.vue
new file mode 100644
index 0000000..f87d88f
--- /dev/null
+++ b/frontend/src/views/nested/menu1/menu1-2/menu1-2-1/index.vue
@@ -0,0 +1,5 @@
+<template functional>
+  <div style="padding:30px;">
+    <el-alert :closable="false" title="menu 1-2-1" type="warning" />
+  </div>
+</template>
diff --git a/frontend/src/views/nested/menu1/menu1-2/menu1-2-2/index.vue b/frontend/src/views/nested/menu1/menu1-2/menu1-2-2/index.vue
new file mode 100644
index 0000000..d88789f
--- /dev/null
+++ b/frontend/src/views/nested/menu1/menu1-2/menu1-2-2/index.vue
@@ -0,0 +1,5 @@
+<template functional>
+  <div style="padding:30px;">
+    <el-alert :closable="false" title="menu 1-2-2" type="warning" />
+  </div>
+</template>
diff --git a/frontend/src/views/nested/menu1/menu1-3/index.vue b/frontend/src/views/nested/menu1/menu1-3/index.vue
new file mode 100644
index 0000000..f7cd073
--- /dev/null
+++ b/frontend/src/views/nested/menu1/menu1-3/index.vue
@@ -0,0 +1,5 @@
+<template functional>
+  <div style="padding:30px;">
+    <el-alert :closable="false" title="menu 1-3" type="success" />
+  </div>
+</template>
diff --git a/frontend/src/views/nested/menu2/index.vue b/frontend/src/views/nested/menu2/index.vue
new file mode 100644
index 0000000..19dd48f
--- /dev/null
+++ b/frontend/src/views/nested/menu2/index.vue
@@ -0,0 +1,5 @@
+<template>
+  <div style="padding:30px;">
+    <el-alert :closable="false" title="menu 2" />
+  </div>
+</template>
diff --git a/frontend/src/views/pdf/content.js b/frontend/src/views/pdf/content.js
new file mode 100644
index 0000000..e62b1a2
--- /dev/null
+++ b/frontend/src/views/pdf/content.js
@@ -0,0 +1,58 @@
+const title = 'Plans for the Next Iteration of Vue.js'
+
+const content = `<p>Last week at<a href="https://vuejs.london/summary" rel="nofollow">Vue.js London</a>I gave a brief sneak peek of what’s coming in the next major version of Vue. This post provides an in-depth overview of the plan.</p>
+<p><img class=" wscnph" src="https://wpimg.wallstcn.com/b8a1d7be-0b73-41b8-be8c-7c01c93cab66.png" data-wscntype="image" data-wscnh="742" data-wscnw="692" /></p>
+<h3>Why a new majorversion?</h3>
+<p>Vue 2.0 was released<a href="https://medium.com/the-vue-point/vue-2-0-is-here-ef1f26acf4b8" rel="nofollow">exactly two years ago</a>(how time flies!). During this period, the core has remained backwards compatible with five minor releases. We’ve accumulated a number of ideas that would bring improvements, but they were held off because they would result in breaking changes. At the same time, the JavaScript ecosystem and the language itself has been evolving rapidly. There are greatly improved tools that could enhance our workflow, and many new language features that could unlock simpler, more complete, and more efficient solutions to the problems Vue is trying to solve. What’s more exciting is that we are seeing ES2015 support becoming a baseline for all major evergreen browsers. Vue 3.0 aims to leverage these new language features to make Vue core smaller, faster, and more powerful.</p>
+<p>Vue 3.0 is currently in prototyping phase, and we have already implemented a runtime close to feature-parity with 2.x.<strong>Many of the items listed below are either already implemented, or confirmed to be feasible. Ones that are not yet implemented or still in exploration phase are marked with a *.</strong></p>
+<h3>The Details</h3>
+<h4>High-Level APIChanges</h4>
+<blockquote>TL;DR: Everything except render function API and scoped-slots syntax will either remain the same or can be made 2.x compatible via a compatibility build.</blockquote>
+<p>Since it’s a new major, there is going to be some breaking changes. However, we take backwards compatibility seriously, so we want to start communicating these changes as soon as possible. Here’s the currently planned public API changes:</p>
+<ul><li>Template syntax will remain 99% the same. There may be small tweaks in scoped slots syntax, but other than that we have no plans to change anything else for templates.</li><li>3.0 will support class-based components natively, with the aim to provide an API that is pleasant to use in native ES2015 without requiring any transpilation or stage-x features. Most current options will have a reasonable mapping in the class-based API. Stage-x features such as class fields and decorators can still be used optionally to enhance the authoring experience. In addition, the API is designed with TypeScript type inference in mind. The 3.x codebase will itself be written in TypeScript, and providing improved TypeScript support. (That said, usage of TypeScript in an application is still entirely optional.)</li><li>The 2.x object-based component format will still be supported by internally transforming the object to a corresponding class.</li><li>Mixins will still be supported.*</li><li>Top level APIs will likely receive an overhaul to avoid globally mutating the Vue runtime when installing plugins. Instead, plugins will be applied and scoped to a component tree. This will make it easier to test components that rely on specific plugins, and also make it possible to mount multiple Vue applications on the same page with different plugins, but using the same Vue runtime.*</li><li>Functional components can finally be plain functions —however, async components will now need to be explicitly created via a helper function.</li><li>The part that will receive the most changes is the Virtual DOM format used in render functions. We are currently collecting feedback from major library authors and will be sharing more details as we are more confident of the changes, but as long as you don’t heavily rely on hand-written (non-JSX) render functions in your app, upgrading should be a reasonably straightforward process.</li></ul>
+<h4>Source Code Architecture</h4>
+<blockquote>TL;DR: better decoupled internal modules, TypeScript, and a codebase that is easier to contribute to.</blockquote>
+<p>We are re-writing 3.0 from the ground up for a cleaner and more maintainable architecture, in particular trying to make it easier to contribute to. We are breaking some internal functionalities into individual packages in order to isolate the scope of complexity. For example, the observer module will become its own package, with its own public API and tests. Note this does not affect framework-level API— you will not have to manually import individual bits from multiple packages in order to use Vue. Instead, the final Vue package is assembled using these internal packages.</p>
+<p>The codebase is also now written in TypeScript. Although this will make proficiency in TypeScript a pre-requisite for contributing to the new codebase, we believe the type information and IDE support will actually make it easier for a new contributor to make meaningful contributions.</p>
+<p>Decoupling the observer and scheduler into separate packages also allows us to easily experiment with alternative implementations of these parts. For example, we can implement an IE11 compatible observer implementation with the same API, or an alternative scheduler that leverages<code>requestIdleCallback</code>to yield to the browser during long updates.*</p>
+<p><img class=" wscnph" src="https://wpimg.wallstcn.com/4d0b5fb2-d7f9-48fd-8f1b-03362b534dd9.png" data-wscntype="image" data-wscnh="716" data-wscnw="460" /></p>
+<h4>Observation Mechanism</h4>
+<blockquote>TL;DR: more complete, precise, efficient and debuggable reactivity tracking &amp; API for creating observables.</blockquote>
+<p>3.0 will ship with a Proxy-based observer implementation that provides reactivity tracking with full language coverage. This eliminates a number of limitations of Vue 2’s current implementation based on<code>Object.defineProperty</code>:</p>
+<p>The new observer also features the following:</p>
+<p>Easily understand why a component is re-rendering</p>
+<p><img class=" wscnph" src="https://wpimg.wallstcn.com/a0c9d811-1ef9-4628-8976-f7c1aaa66da0.png" data-wscntype="image" data-wscnh="540" data-wscnw="789" /></p>
+<h4>Other Runtime Improvements</h4>
+<blockquote>TL;DR: smaller, faster, tree-shakable features, fragments &amp; portals, custom renderer API.</blockquote>
+<h4>Compiler Improvements*</h4>
+<blockquote>TL;DR: tree-shaking friendly output, more AOT optimizations, parser with better error info and source map support.</blockquote>
+<h4>IE11 Support*</h4>
+<blockquote>TL;DR: it will be supported, but in a separate build with the same reactivity limitations of Vue 2.x.</blockquote>
+<p>The new codebase currently targets evergreen browsers only and assumes baseline native ES2015 support. But alas, we know a lot of our users still need to support IE11 for the foreseeable future. Most of the ES2015 features used can be transpiled / polyfilled for IE11, with the exception for Proxies. Our plan is to implement an alternative observer with the same API, but using the good old ES5<code>Object.defineProperty</code>API. A separate build of Vue 3.x will be distributed using this observer implementation. However, this build will be subject to the same change detection caveats of Vue 2.x and thus not fully compatible with the “modern” build of 3.x. We are aware that this imposes some inconvenience for library authors as they will need to be aware of compatibility for two different builds, but we will make sure to provide clear guidelines on this when we reach that stage.</p>
+<h3>How Do We GetThere</h3>
+<p>First of all, although we are announcing it today, we do not have a definitive timeline yet. What we do know at the moment is the steps we will be taking to get there:</p>
+<h4>1. Internal Feedback for the Runtime Prototype</h4>
+<p>This is the phase we are in right now. Currently, we already have a working runtime prototype that includes the new observer, Virtual DOM and component implementation. We have invited a group of authors of influential community projects to provide feedback for the internal changes, and would like to make sure they are comfortable with the changes before moving forward. We want to ensure that important libraries in the ecosystem will be ready at the same time when we release 3.0, so that users relying on those projects can upgrade easily.</p>
+<h4>2. Public Feedback viaRFCs</h4>
+<p>Once we gain a certain level of confidence in the new design, for each breaking change we will be opening a dedicated RFC issue which includes:</p>
+<p>We will anticipate public feedback from the wider community to help us consolidate these ideas.</p>
+<h4>3. Introduce Compatible Features in 2.x &amp;2.x-next</h4>
+<p>We are not forgetting about 2.x! In fact, we plan to use 2.x to progressively accustom users to the new changes. We will be gradually introducing confirmed API changes into 2.x via opt-in adaptors, and 2.x-next will allow users to try out the new Proxy-based observer.</p>
+<p>The last minor release in 2.x will become LTS and continue to receive bug and security fixes for 18 months when 3.0 is released.</p>
+<h4>4. AlphaPhase</h4>
+<p>Next, we will finish up the compiler and server-side rendering parts of 3.0 and start making alpha releases. These will mostly be for stability testing purposes in small greenfield apps.</p>
+<h4>5. BetaPhase</h4>
+<p>During beta phase, our main goal is updating support libraries and tools like Vue Router, Vuex, Vue CLI, Vue DevTools and make sure they work smoothly with the new core. We will also be working with major library authors from the community to help them get ready for 3.0.</p>
+<h4>6. RCPhase</h4>
+<p>Once we consider the API and codebase stable, we will enter RC phase with API freeze. During this phase we will also work on a “compat build”: a build of 3.0 that includes compatibility layers for 2.x API. This build will also ship with a flag you can turn on to emit deprecation warnings for 2.x API usage in your app. The compat build can be used as a guide to upgrade your app to 3.0.</p>
+<h4>7. IE11build</h4>
+<p>The last task before the final release will be the IE11 compatibility build as mentioned above.</p>
+<h4>8. FinalRelease</h4>
+<p>In all honesty, we don’t know when this will happen yet, but likely in 2019. Again, we care more about shipping something that is solid and stable rather than hitting specific dates. There is a lot of work to be done, but we are excited for what’s coming next!</p>`
+
+const data = {
+  title,
+  content
+}
+
+export default data
diff --git a/frontend/src/views/pdf/download.vue b/frontend/src/views/pdf/download.vue
new file mode 100644
index 0000000..a348c69
--- /dev/null
+++ b/frontend/src/views/pdf/download.vue
@@ -0,0 +1,201 @@
+<template>
+  <div v-loading.fullscreen.lock="fullscreenLoading" class="main-article" element-loading-text="Efforts to generate PDF">
+    <div class="article__heading">
+      <div class="article__heading__title">
+        {{ article.title }}
+      </div>
+    </div>
+    <div style="color: #ccc;">
+      This article is from Evan You on <a target="_blank" href="https://medium.com/the-vue-point/plans-for-the-next-iteration-of-vue-js-777ffea6fabf">medium</a>
+    </div>
+    <div ref="content" class="node-article-content" v-html="article.content" />
+  </div>
+</template>
+
+<script>
+
+export default {
+  data() {
+    return {
+      article: '',
+      fullscreenLoading: true
+    }
+  },
+  mounted() {
+    this.fetchData()
+  },
+  methods: {
+    fetchData() {
+      import('./content.js').then(data => {
+        const { title } = data.default
+        document.title = title
+        this.article = data.default
+        setTimeout(() => {
+          this.fullscreenLoading = false
+          this.$nextTick(() => {
+            window.print()
+          })
+        }, 3000)
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+@mixin clearfix {
+  &:before {
+    display: table;
+    content: '';
+    clear: both;
+  }
+
+  &:after {
+    display: table;
+    content: '';
+    clear: both;
+  }
+}
+
+.main-article {
+  padding: 20px;
+  margin: 0 auto;
+  display: block;
+  width: 740px;
+  background: #fff;
+}
+
+.article__heading {
+  position: relative;
+  padding: 0 0 20px;
+  overflow: hidden;
+}
+
+.article__heading__title {
+  display: block;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  line-clamp: 2;
+  word-wrap: break-word;
+  overflow-wrap: break-word;
+  font-size: 32px;
+  line-height: 48px;
+  font-weight: 600;
+  color: #333;
+  overflow: hidden;
+}
+
+.node-article-content {
+  margin: 20px 0 0;
+  @include clearfix;
+  font-size: 16px;
+  color: #333;
+  letter-spacing: 0.5px;
+  line-height: 28px;
+  margin-bottom: 30px;
+  font-family: medium-content-serif-font, Georgia, Cambria, "Times New Roman", Times, serif;
+
+  &> :last-child {
+    margin-bottom: 0;
+  }
+
+  b,
+  strong {
+    font-weight: inherit;
+    font-weight: bolder;
+  }
+
+  img {
+    max-width: 100%;
+    display: block;
+    margin: 0 auto;
+  }
+
+  p {
+    font-weight: 400;
+    font-style: normal;
+    font-size: 21px;
+    line-height: 1.58;
+    letter-spacing: -.003em;
+
+  }
+
+  ul {
+    margin-bottom: 30px;
+  }
+
+  li {
+    --x-height-multiplier: 0.375;
+    --baseline-multiplier: 0.17;
+
+    letter-spacing: .01rem;
+    font-weight: 400;
+    font-style: normal;
+    font-size: 21px;
+    line-height: 1.58;
+    letter-spacing: -.003em;
+    margin-left: 30px;
+    margin-bottom: 14px;
+  }
+
+  a {
+    text-decoration: none;
+    background-repeat: repeat-x;
+    background-image: linear-gradient(to right, rgba(0, 0, 0, .84) 100%, rgba(0, 0, 0, 0) 0);
+    background-size: 1px 1px;
+    background-position: 0 calc(1em + 1px);
+    padding: 0 6px;
+  }
+
+  code {
+    background: rgba(0, 0, 0, .05);
+    padding: 3px 4px;
+    margin: 0 2px;
+    font-size: 16px;
+    display: inline-block;
+  }
+
+  img {
+    border: 0;
+  }
+
+  /* 解决 IE6-7 图片缩放锯齿问题 */
+  img {
+    -ms-interpolation-mode: bicubic;
+  }
+
+  blockquote {
+    --x-height-multiplier: 0.375;
+    --baseline-multiplier: 0.17;
+    font-family: medium-content-serif-font, Georgia, Cambria, "Times New Roman", Times, serif;
+    letter-spacing: .01rem;
+    font-weight: 400;
+    font-style: italic;
+    font-size: 21px;
+    line-height: 1.58;
+    letter-spacing: -.003em;
+    border-left: 3px solid rgba(0, 0, 0, .84);
+    padding-left: 20px;
+    margin-left: -23px;
+    padding-bottom: 2px;
+  }
+
+  a {
+    text-decoration: none;
+  }
+
+  h2,
+  h3,
+  h4 {
+    font-size: 34px;
+    line-height: 1.15;
+    letter-spacing: -.015em;
+    margin: 53px 0 0;
+  }
+
+  h4 {
+    font-size: 26px;
+  }
+}
+</style>
diff --git a/frontend/src/views/pdf/index.vue b/frontend/src/views/pdf/index.vue
new file mode 100644
index 0000000..86278b3
--- /dev/null
+++ b/frontend/src/views/pdf/index.vue
@@ -0,0 +1,13 @@
+<template>
+  <div class="app-container">
+    <aside style="margin-top:15px;">
+      Here we use window.print() to implement the feature of downloading PDF.
+    </aside>
+    <router-link target="_blank" to="/pdf/download">
+      <el-button type="primary">
+        Click to download PDF
+      </el-button>
+    </router-link>
+  </div>
+</template>
+
diff --git a/frontend/src/views/permission/components/SwitchRoles.vue b/frontend/src/views/permission/components/SwitchRoles.vue
new file mode 100644
index 0000000..9fabbe1
--- /dev/null
+++ b/frontend/src/views/permission/components/SwitchRoles.vue
@@ -0,0 +1,32 @@
+<template>
+  <div>
+    <div style="margin-bottom:15px;">
+      Your roles: {{ roles }}
+    </div>
+    Switch roles:
+    <el-radio-group v-model="switchRoles">
+      <el-radio-button label="editor" />
+      <el-radio-button label="admin" />
+    </el-radio-group>
+  </div>
+</template>
+
+<script>
+export default {
+  computed: {
+    roles() {
+      return this.$store.getters.roles
+    },
+    switchRoles: {
+      get() {
+        return this.roles[0]
+      },
+      set(val) {
+        this.$store.dispatch('user/changeRoles', val).then(() => {
+          this.$emit('change')
+        })
+      }
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/permission/directive.vue b/frontend/src/views/permission/directive.vue
new file mode 100644
index 0000000..4a49792
--- /dev/null
+++ b/frontend/src/views/permission/directive.vue
@@ -0,0 +1,111 @@
+<template>
+  <div class="app-container">
+    <switch-roles @change="handleRolesChange" />
+    <div :key="key" style="margin-top:30px;">
+      <div>
+        <span v-permission="['admin']" class="permission-alert">
+          Only
+          <el-tag class="permission-tag" size="small">admin</el-tag> can see this
+        </span>
+        <el-tag v-permission="['admin']" class="permission-sourceCode" type="info">
+          v-permission="['admin']"
+        </el-tag>
+      </div>
+
+      <div>
+        <span v-permission="['editor']" class="permission-alert">
+          Only
+          <el-tag class="permission-tag" size="small">editor</el-tag> can see this
+        </span>
+        <el-tag v-permission="['editor']" class="permission-sourceCode" type="info">
+          v-permission="['editor']"
+        </el-tag>
+      </div>
+
+      <div>
+        <span v-permission="['admin','editor']" class="permission-alert">
+          Both
+          <el-tag class="permission-tag" size="small">admin</el-tag> and
+          <el-tag class="permission-tag" size="small">editor</el-tag> can see this
+        </span>
+        <el-tag v-permission="['admin','editor']" class="permission-sourceCode" type="info">
+          v-permission="['admin','editor']"
+        </el-tag>
+      </div>
+    </div>
+
+    <div :key="'checkPermission'+key" style="margin-top:60px;">
+      <aside>
+        In some cases, using v-permission will have no effect. For example: Element-UI's Tab component or el-table-column and other scenes that dynamically render dom. You can only do this with v-if.
+        <br> e.g.
+      </aside>
+
+      <el-tabs type="border-card" style="width:550px;">
+        <el-tab-pane v-if="checkPermission(['admin'])" label="Admin">
+          Admin can see this
+          <el-tag class="permission-sourceCode" type="info">
+            v-if="checkPermission(['admin'])"
+          </el-tag>
+        </el-tab-pane>
+
+        <el-tab-pane v-if="checkPermission(['editor'])" label="Editor">
+          Editor can see this
+          <el-tag class="permission-sourceCode" type="info">
+            v-if="checkPermission(['editor'])"
+          </el-tag>
+        </el-tab-pane>
+
+        <el-tab-pane v-if="checkPermission(['admin','editor'])" label="Admin-OR-Editor">
+          Both admin or editor can see this
+          <el-tag class="permission-sourceCode" type="info">
+            v-if="checkPermission(['admin','editor'])"
+          </el-tag>
+        </el-tab-pane>
+      </el-tabs>
+    </div>
+  </div>
+</template>
+
+<script>
+import permission from '@/directive/permission/index.js' // 权限判断指令
+import checkPermission from '@/utils/permission' // 权限判断函数
+import SwitchRoles from './components/SwitchRoles'
+
+export default {
+  name: 'DirectivePermission',
+  components: { SwitchRoles },
+  directives: { permission },
+  data() {
+    return {
+      key: 1 // 为了能每次切换权限的时候重新初始化指令
+    }
+  },
+  methods: {
+    checkPermission,
+    handleRolesChange() {
+      this.key++
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  ::v-deep .permission-alert {
+    width: 320px;
+    margin-top: 15px;
+    background-color: #f0f9eb;
+    color: #67c23a;
+    padding: 8px 16px;
+    border-radius: 4px;
+    display: inline-block;
+  }
+  ::v-deep .permission-sourceCode {
+    margin-left: 15px;
+  }
+  ::v-deep .permission-tag {
+    background-color: #ecf5ff;
+  }
+}
+</style>
+
diff --git a/frontend/src/views/permission/page.vue b/frontend/src/views/permission/page.vue
new file mode 100644
index 0000000..5291a78
--- /dev/null
+++ b/frontend/src/views/permission/page.vue
@@ -0,0 +1,19 @@
+<template>
+  <div class="app-container">
+    <switch-roles @change="handleRolesChange" />
+  </div>
+</template>
+
+<script>
+import SwitchRoles from './components/SwitchRoles'
+
+export default {
+  name: 'PagePermission',
+  components: { SwitchRoles },
+  methods: {
+    handleRolesChange() {
+      this.$router.push({ path: '/permission/index?' + +new Date() })
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/permission/role.vue b/frontend/src/views/permission/role.vue
new file mode 100644
index 0000000..cd7ecd4
--- /dev/null
+++ b/frontend/src/views/permission/role.vue
@@ -0,0 +1,270 @@
+<template>
+  <div class="app-container">
+    <el-button type="primary" @click="handleAddRole">New Role</el-button>
+
+    <el-table :data="rolesList" style="width: 100%;margin-top:30px;" border>
+      <el-table-column align="center" label="Role Key" width="220">
+        <template slot-scope="scope">
+          {{ scope.row.key }}
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="Role Name" width="220">
+        <template slot-scope="scope">
+          {{ scope.row.name }}
+        </template>
+      </el-table-column>
+      <el-table-column align="header-center" label="Description">
+        <template slot-scope="scope">
+          {{ scope.row.description }}
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="Operations">
+        <template slot-scope="scope">
+          <el-button type="primary" size="small" @click="handleEdit(scope)">Edit</el-button>
+          <el-button type="danger" size="small" @click="handleDelete(scope)">Delete</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-dialog :visible.sync="dialogVisible" :title="dialogType==='edit'?'Edit Role':'New Role'">
+      <el-form :model="role" label-width="80px" label-position="left">
+        <el-form-item label="Name">
+          <el-input v-model="role.name" placeholder="Role Name" />
+        </el-form-item>
+        <el-form-item label="Desc">
+          <el-input
+            v-model="role.description"
+            :autosize="{ minRows: 2, maxRows: 4}"
+            type="textarea"
+            placeholder="Role Description"
+          />
+        </el-form-item>
+        <el-form-item label="Menus">
+          <el-tree
+            ref="tree"
+            :check-strictly="checkStrictly"
+            :data="routesData"
+            :props="defaultProps"
+            show-checkbox
+            node-key="path"
+            class="permission-tree"
+          />
+        </el-form-item>
+      </el-form>
+      <div style="text-align:right;">
+        <el-button type="danger" @click="dialogVisible=false">Cancel</el-button>
+        <el-button type="primary" @click="confirmRole">Confirm</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import path from 'path'
+import { deepClone } from '@/utils'
+import { getRoutes, getRoles, addRole, deleteRole, updateRole } from '@/api/role'
+
+const defaultRole = {
+  key: '',
+  name: '',
+  description: '',
+  routes: []
+}
+
+export default {
+  data() {
+    return {
+      role: Object.assign({}, defaultRole),
+      routes: [],
+      rolesList: [],
+      dialogVisible: false,
+      dialogType: 'new',
+      checkStrictly: false,
+      defaultProps: {
+        children: 'children',
+        label: 'title'
+      }
+    }
+  },
+  computed: {
+    routesData() {
+      return this.routes
+    }
+  },
+  created() {
+    // Mock: get all routes and roles list from server
+    this.getRoutes()
+    this.getRoles()
+  },
+  methods: {
+    async getRoutes() {
+      const res = await getRoutes()
+      this.serviceRoutes = res.data
+      this.routes = this.generateRoutes(res.data)
+    },
+    async getRoles() {
+      const res = await getRoles()
+      this.rolesList = res.data
+    },
+
+    // Reshape the routes structure so that it looks the same as the sidebar
+    generateRoutes(routes, basePath = '/') {
+      const res = []
+
+      for (let route of routes) {
+        // skip some route
+        if (route.hidden) { continue }
+
+        const onlyOneShowingChild = this.onlyOneShowingChild(route.children, route)
+
+        if (route.children && onlyOneShowingChild && !route.alwaysShow) {
+          route = onlyOneShowingChild
+        }
+
+        const data = {
+          path: path.resolve(basePath, route.path),
+          title: route.meta && route.meta.title
+
+        }
+
+        // recursive child routes
+        if (route.children) {
+          data.children = this.generateRoutes(route.children, data.path)
+        }
+        res.push(data)
+      }
+      return res
+    },
+    generateArr(routes) {
+      let data = []
+      routes.forEach(route => {
+        data.push(route)
+        if (route.children) {
+          const temp = this.generateArr(route.children)
+          if (temp.length > 0) {
+            data = [...data, ...temp]
+          }
+        }
+      })
+      return data
+    },
+    handleAddRole() {
+      this.role = Object.assign({}, defaultRole)
+      if (this.$refs.tree) {
+        this.$refs.tree.setCheckedNodes([])
+      }
+      this.dialogType = 'new'
+      this.dialogVisible = true
+    },
+    handleEdit(scope) {
+      this.dialogType = 'edit'
+      this.dialogVisible = true
+      this.checkStrictly = true
+      this.role = deepClone(scope.row)
+      this.$nextTick(() => {
+        const routes = this.generateRoutes(this.role.routes)
+        this.$refs.tree.setCheckedNodes(this.generateArr(routes))
+        // set checked state of a node not affects its father and child nodes
+        this.checkStrictly = false
+      })
+    },
+    handleDelete({ $index, row }) {
+      this.$confirm('Confirm to remove the role?', 'Warning', {
+        confirmButtonText: 'Confirm',
+        cancelButtonText: 'Cancel',
+        type: 'warning'
+      })
+        .then(async() => {
+          await deleteRole(row.key)
+          this.rolesList.splice($index, 1)
+          this.$message({
+            type: 'success',
+            message: 'Delete succed!'
+          })
+        })
+        .catch(err => { console.error(err) })
+    },
+    generateTree(routes, basePath = '/', checkedKeys) {
+      const res = []
+
+      for (const route of routes) {
+        const routePath = path.resolve(basePath, route.path)
+
+        // recursive child routes
+        if (route.children) {
+          route.children = this.generateTree(route.children, routePath, checkedKeys)
+        }
+
+        if (checkedKeys.includes(routePath) || (route.children && route.children.length >= 1)) {
+          res.push(route)
+        }
+      }
+      return res
+    },
+    async confirmRole() {
+      const isEdit = this.dialogType === 'edit'
+
+      const checkedKeys = this.$refs.tree.getCheckedKeys()
+      this.role.routes = this.generateTree(deepClone(this.serviceRoutes), '/', checkedKeys)
+
+      if (isEdit) {
+        await updateRole(this.role.key, this.role)
+        for (let index = 0; index < this.rolesList.length; index++) {
+          if (this.rolesList[index].key === this.role.key) {
+            this.rolesList.splice(index, 1, Object.assign({}, this.role))
+            break
+          }
+        }
+      } else {
+        const { data } = await addRole(this.role)
+        this.role.key = data.key
+        this.rolesList.push(this.role)
+      }
+
+      const { description, key, name } = this.role
+      this.dialogVisible = false
+      this.$notify({
+        title: 'Success',
+        dangerouslyUseHTMLString: true,
+        message: `
+            <div>Role Key: ${key}</div>
+            <div>Role Name: ${name}</div>
+            <div>Description: ${description}</div>
+          `,
+        type: 'success'
+      })
+    },
+    // reference: src/view/layout/components/Sidebar/SidebarItem.vue
+    onlyOneShowingChild(children = [], parent) {
+      let onlyOneChild = null
+      const showingChildren = children.filter(item => !item.hidden)
+
+      // When there is only one child route, the child route is displayed by default
+      if (showingChildren.length === 1) {
+        onlyOneChild = showingChildren[0]
+        onlyOneChild.path = path.resolve(parent.path, onlyOneChild.path)
+        return onlyOneChild
+      }
+
+      // Show parent if there are no child route to display
+      if (showingChildren.length === 0) {
+        onlyOneChild = { ... parent, path: '', noShowingChildren: true }
+        return onlyOneChild
+      }
+
+      return false
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  .roles-table {
+    margin-top: 30px;
+  }
+  .permission-tree {
+    margin-bottom: 30px;
+  }
+}
+</style>
diff --git a/frontend/src/views/profile/components/Account.vue b/frontend/src/views/profile/components/Account.vue
new file mode 100644
index 0000000..9f2e386
--- /dev/null
+++ b/frontend/src/views/profile/components/Account.vue
@@ -0,0 +1,38 @@
+<template>
+  <el-form>
+    <el-form-item label="Name">
+      <el-input v-model.trim="user.name" />
+    </el-form-item>
+    <el-form-item label="Email">
+      <el-input v-model.trim="user.email" />
+    </el-form-item>
+    <el-form-item>
+      <el-button type="primary" @click="submit">Update</el-button>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script>
+export default {
+  props: {
+    user: {
+      type: Object,
+      default: () => {
+        return {
+          name: '',
+          email: ''
+        }
+      }
+    }
+  },
+  methods: {
+    submit() {
+      this.$message({
+        message: 'User information has been updated successfully',
+        type: 'success',
+        duration: 5 * 1000
+      })
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/profile/components/Activity.vue b/frontend/src/views/profile/components/Activity.vue
new file mode 100644
index 0000000..dd5db3a
--- /dev/null
+++ b/frontend/src/views/profile/components/Activity.vue
@@ -0,0 +1,185 @@
+<template>
+  <div class="user-activity">
+    <div class="post">
+      <div class="user-block">
+        <img class="img-circle" :src="'https://wpimg.wallstcn.com/57ed425a-c71e-4201-9428-68760c0537c4.jpg'+avatarPrefix">
+        <span class="username text-muted">Iron Man</span>
+        <span class="description">Shared publicly - 7:30 PM today</span>
+      </div>
+      <p>
+        Lorem ipsum represents a long-held tradition for designers,
+        typographers and the like. Some people hate it and argue for
+        its demise, but others ignore the hate as they create awesome
+        tools to help create filler text for everyone from bacon lovers
+        to Charlie Sheen fans.
+      </p>
+      <ul class="list-inline">
+        <li>
+          <span class="link-black text-sm">
+            <i class="el-icon-share" />
+            Share
+          </span>
+        </li>
+        <li>
+          <span class="link-black text-sm">
+            <svg-icon icon-class="like" />
+            Like
+          </span>
+        </li>
+      </ul>
+    </div>
+    <div class="post">
+      <div class="user-block">
+        <img class="img-circle" :src="'https://wpimg.wallstcn.com/9e2a5d0a-bd5b-457f-ac8e-86554616c87b.jpg'+avatarPrefix">
+        <span class="username text-muted">Captain American</span>
+        <span class="description">Sent you a message - yesterday</span>
+      </div>
+      <p>
+        Lorem ipsum represents a long-held tradition for designers,
+        typographers and the like. Some people hate it and argue for
+        its demise, but others ignore the hate as they create awesome
+        tools to help create filler text for everyone from bacon lovers
+        to Charlie Sheen fans.
+      </p>
+      <ul class="list-inline">
+        <li>
+          <span class="link-black text-sm">
+            <i class="el-icon-share" />
+            Share
+          </span>
+        </li>
+        <li>
+          <span class="link-black text-sm">
+            <svg-icon icon-class="like" />
+            Like
+          </span>
+        </li>
+      </ul>
+    </div>
+    <div class="post">
+      <div class="user-block">
+        <img class="img-circle" :src="'https://wpimg.wallstcn.com/fb57f689-e1ab-443c-af12-8d4066e202e2.jpg'+avatarPrefix">
+        <span class="username">Spider Man</span>
+        <span class="description">Posted 4 photos - 2 days ago</span>
+      </div>
+      <div class="user-images">
+        <el-carousel :interval="6000" type="card" height="220px">
+          <el-carousel-item v-for="item in carouselImages" :key="item">
+            <img :src="item+carouselPrefix" class="image">
+          </el-carousel-item>
+        </el-carousel>
+      </div>
+      <ul class="list-inline">
+        <li><span class="link-black text-sm"><i class="el-icon-share" /> Share</span></li>
+        <li>
+          <span class="link-black text-sm">
+            <svg-icon icon-class="like" /> Like</span>
+        </li>
+      </ul>
+    </div>
+  </div>
+</template>
+
+<script>
+const avatarPrefix = '?imageView2/1/w/80/h/80'
+const carouselPrefix = '?imageView2/2/h/440'
+
+export default {
+  data() {
+    return {
+      carouselImages: [
+        'https://wpimg.wallstcn.com/9679ffb0-9e0b-4451-9916-e21992218054.jpg',
+        'https://wpimg.wallstcn.com/bcce3734-0837-4b9f-9261-351ef384f75a.jpg',
+        'https://wpimg.wallstcn.com/d1d7b033-d75e-4cd6-ae39-fcd5f1c0a7c5.jpg',
+        'https://wpimg.wallstcn.com/50530061-851b-4ca5-9dc5-2fead928a939.jpg'
+      ],
+      avatarPrefix,
+      carouselPrefix
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.user-activity {
+  .user-block {
+
+    .username,
+    .description {
+      display: block;
+      margin-left: 50px;
+      padding: 2px 0;
+    }
+
+    .username{
+      font-size: 16px;
+      color: #000;
+    }
+
+    :after {
+      clear: both;
+    }
+
+    .img-circle {
+      border-radius: 50%;
+      width: 40px;
+      height: 40px;
+      float: left;
+    }
+
+    span {
+      font-weight: 500;
+      font-size: 12px;
+    }
+  }
+
+  .post {
+    font-size: 14px;
+    border-bottom: 1px solid #d2d6de;
+    margin-bottom: 15px;
+    padding-bottom: 15px;
+    color: #666;
+
+    .image {
+      width: 100%;
+      height: 100%;
+
+    }
+
+    .user-images {
+      padding-top: 20px;
+    }
+  }
+
+  .list-inline {
+    padding-left: 0;
+    margin-left: -5px;
+    list-style: none;
+
+    li {
+      display: inline-block;
+      padding-right: 5px;
+      padding-left: 5px;
+      font-size: 13px;
+    }
+
+    .link-black {
+
+      &:hover,
+      &:focus {
+        color: #999;
+      }
+    }
+  }
+
+}
+
+.box-center {
+  margin: 0 auto;
+  display: table;
+}
+
+.text-muted {
+  color: #777;
+}
+</style>
diff --git a/frontend/src/views/profile/components/Timeline.vue b/frontend/src/views/profile/components/Timeline.vue
new file mode 100644
index 0000000..ba90b3d
--- /dev/null
+++ b/frontend/src/views/profile/components/Timeline.vue
@@ -0,0 +1,43 @@
+<template>
+  <div class="block">
+    <el-timeline>
+      <el-timeline-item v-for="(item,index) of timeline" :key="index" :timestamp="item.timestamp" placement="top">
+        <el-card>
+          <h4>{{ item.title }}</h4>
+          <p>{{ item.content }}</p>
+        </el-card>
+      </el-timeline-item>
+    </el-timeline>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      timeline: [
+        {
+          timestamp: '2019/4/20',
+          title: 'Update Github template',
+          content: 'PanJiaChen committed 2019/4/20 20:46'
+        },
+        {
+          timestamp: '2019/4/21',
+          title: 'Update Github template',
+          content: 'PanJiaChen committed 2019/4/21 20:46'
+        },
+        {
+          timestamp: '2019/4/22',
+          title: 'Build Template',
+          content: 'PanJiaChen committed 2019/4/22 20:46'
+        },
+        {
+          timestamp: '2019/4/23',
+          title: 'Release New Version',
+          content: 'PanJiaChen committed 2019/4/23 20:46'
+        }
+      ]
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/profile/components/UserCard.vue b/frontend/src/views/profile/components/UserCard.vue
new file mode 100644
index 0000000..2476f04
--- /dev/null
+++ b/frontend/src/views/profile/components/UserCard.vue
@@ -0,0 +1,134 @@
+<template>
+  <el-card style="margin-bottom:20px;">
+    <div slot="header" class="clearfix">
+      <span>About me</span>
+    </div>
+
+    <div class="user-profile">
+      <div class="box-center">
+        <pan-thumb :image="user.avatar" :height="'100px'" :width="'100px'" :hoverable="false">
+          <div>Hello</div>
+          {{ user.role }}
+        </pan-thumb>
+      </div>
+      <div class="box-center">
+        <div class="user-name text-center">{{ user.name }}</div>
+        <div class="user-role text-center text-muted">{{ user.role | uppercaseFirst }}</div>
+      </div>
+    </div>
+
+    <div class="user-bio">
+      <div class="user-education user-bio-section">
+        <div class="user-bio-section-header"><svg-icon icon-class="education" /><span>Education</span></div>
+        <div class="user-bio-section-body">
+          <div class="text-muted">
+            JS in Computer Science from the University of Technology
+          </div>
+        </div>
+      </div>
+
+      <div class="user-skills user-bio-section">
+        <div class="user-bio-section-header"><svg-icon icon-class="skill" /><span>Skills</span></div>
+        <div class="user-bio-section-body">
+          <div class="progress-item">
+            <span>Vue</span>
+            <el-progress :percentage="70" />
+          </div>
+          <div class="progress-item">
+            <span>JavaScript</span>
+            <el-progress :percentage="18" />
+          </div>
+          <div class="progress-item">
+            <span>Css</span>
+            <el-progress :percentage="12" />
+          </div>
+          <div class="progress-item">
+            <span>ESLint</span>
+            <el-progress :percentage="100" status="success" />
+          </div>
+        </div>
+      </div>
+    </div>
+  </el-card>
+</template>
+
+<script>
+import PanThumb from '@/components/PanThumb'
+
+export default {
+  components: { PanThumb },
+  props: {
+    user: {
+      type: Object,
+      default: () => {
+        return {
+          name: '',
+          email: '',
+          avatar: '',
+          role: ''
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.box-center {
+  margin: 0 auto;
+  display: table;
+}
+
+.text-muted {
+  color: #777;
+}
+
+.user-profile {
+  .user-name {
+    font-weight: bold;
+  }
+
+  .box-center {
+    padding-top: 10px;
+  }
+
+  .user-role {
+    padding-top: 10px;
+    font-weight: 400;
+    font-size: 14px;
+  }
+
+  .box-social {
+    padding-top: 30px;
+
+    .el-table {
+      border-top: 1px solid #dfe6ec;
+    }
+  }
+
+  .user-follow {
+    padding-top: 20px;
+  }
+}
+
+.user-bio {
+  margin-top: 20px;
+  color: #606266;
+
+  span {
+    padding-left: 4px;
+  }
+
+  .user-bio-section {
+    font-size: 14px;
+    padding: 15px 0;
+
+    .user-bio-section-header {
+      border-bottom: 1px solid #dfe6ec;
+      padding-bottom: 10px;
+      margin-bottom: 10px;
+      font-weight: bold;
+    }
+  }
+}
+</style>
diff --git a/frontend/src/views/profile/index.vue b/frontend/src/views/profile/index.vue
new file mode 100644
index 0000000..87e4f94
--- /dev/null
+++ b/frontend/src/views/profile/index.vue
@@ -0,0 +1,68 @@
+<template>
+  <div class="app-container">
+    <div v-if="user">
+      <el-row :gutter="20">
+
+        <el-col :span="6" :xs="24">
+          <user-card :user="user" />
+        </el-col>
+
+        <el-col :span="18" :xs="24">
+          <el-card>
+            <el-tabs v-model="activeTab">
+              <el-tab-pane label="Activity" name="activity">
+                <activity />
+              </el-tab-pane>
+              <el-tab-pane label="Timeline" name="timeline">
+                <timeline />
+              </el-tab-pane>
+              <el-tab-pane label="Account" name="account">
+                <account :user="user" />
+              </el-tab-pane>
+            </el-tabs>
+          </el-card>
+        </el-col>
+
+      </el-row>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import UserCard from './components/UserCard'
+import Activity from './components/Activity'
+import Timeline from './components/Timeline'
+import Account from './components/Account'
+
+export default {
+  name: 'Profile',
+  components: { UserCard, Activity, Timeline, Account },
+  data() {
+    return {
+      user: {},
+      activeTab: 'activity'
+    }
+  },
+  computed: {
+    ...mapGetters([
+      'name',
+      'avatar',
+      'roles'
+    ])
+  },
+  created() {
+    this.getUser()
+  },
+  methods: {
+    getUser() {
+      this.user = {
+        name: this.name,
+        role: this.roles.join(' | '),
+        email: 'admin@test.com',
+        avatar: this.avatar
+      }
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/qiniu/upload.vue b/frontend/src/views/qiniu/upload.vue
new file mode 100644
index 0000000..9dc9aed
--- /dev/null
+++ b/frontend/src/views/qiniu/upload.vue
@@ -0,0 +1,41 @@
+<template>
+  <el-upload :data="dataObj" :multiple="true" :before-upload="beforeUpload" action="https://upload.qbox.me" drag>
+    <i class="el-icon-upload" />
+    <div class="el-upload__text">
+      将文件拖到此处,或<em>点击上传</em>
+    </div>
+  </el-upload>
+</template>
+
+<script>
+import { getToken } from '@/api/qiniu'
+// 获取七牛token 后端通过Access Key,Secret Key,bucket等生成token
+// 七牛官方sdk https://developer.qiniu.com/sdk#official-sdk
+
+export default {
+  data() {
+    return {
+      dataObj: { token: '', key: '' },
+      image_uri: [],
+      fileList: []
+    }
+  },
+  methods: {
+    beforeUpload() {
+      const _self = this
+      return new Promise((resolve, reject) => {
+        getToken().then(response => {
+          const key = response.data.qiniu_key
+          const token = response.data.qiniu_token
+          _self._data.dataObj.token = token
+          _self._data.dataObj.key = key
+          resolve(true)
+        }).catch(err => {
+          console.log(err)
+          reject(false)
+        })
+      })
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/redirect/index.vue b/frontend/src/views/redirect/index.vue
new file mode 100644
index 0000000..db4c1d6
--- /dev/null
+++ b/frontend/src/views/redirect/index.vue
@@ -0,0 +1,12 @@
+<script>
+export default {
+  created() {
+    const { params, query } = this.$route
+    const { path } = params
+    this.$router.replace({ path: '/' + path, query })
+  },
+  render: function(h) {
+    return h() // avoid warning message
+  }
+}
+</script>
diff --git a/frontend/src/views/tab/components/TabPane.vue b/frontend/src/views/tab/components/TabPane.vue
new file mode 100644
index 0000000..3fb1439
--- /dev/null
+++ b/frontend/src/views/tab/components/TabPane.vue
@@ -0,0 +1,103 @@
+<template>
+  <el-table :data="list" border fit highlight-current-row style="width: 100%">
+    <el-table-column
+      v-loading="loading"
+      align="center"
+      label="ID"
+      width="65"
+      element-loading-text="请给我点时间!"
+    >
+      <template slot-scope="scope">
+        <span>{{ scope.row.id }}</span>
+      </template>
+    </el-table-column>
+
+    <el-table-column width="180px" align="center" label="Date">
+      <template slot-scope="scope">
+        <span>{{ scope.row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}</span>
+      </template>
+    </el-table-column>
+
+    <el-table-column min-width="300px" label="Title">
+      <template slot-scope="{row}">
+        <span>{{ row.title }}</span>
+        <el-tag>{{ row.type }}</el-tag>
+      </template>
+    </el-table-column>
+
+    <el-table-column width="110px" align="center" label="Author">
+      <template slot-scope="scope">
+        <span>{{ scope.row.author }}</span>
+      </template>
+    </el-table-column>
+
+    <el-table-column width="120px" label="Importance">
+      <template slot-scope="scope">
+        <svg-icon v-for="n in +scope.row.importance" :key="n" icon-class="star" />
+      </template>
+    </el-table-column>
+
+    <el-table-column align="center" label="Readings" width="95">
+      <template slot-scope="scope">
+        <span>{{ scope.row.pageviews }}</span>
+      </template>
+    </el-table-column>
+
+    <el-table-column class-name="status-col" label="Status" width="110">
+      <template slot-scope="{row}">
+        <el-tag :type="row.status | statusFilter">
+          {{ row.status }}
+        </el-tag>
+      </template>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script>
+import { fetchList } from '@/api/article'
+
+export default {
+  filters: {
+    statusFilter(status) {
+      const statusMap = {
+        published: 'success',
+        draft: 'info',
+        deleted: 'danger'
+      }
+      return statusMap[status]
+    }
+  },
+  props: {
+    type: {
+      type: String,
+      default: 'CN'
+    }
+  },
+  data() {
+    return {
+      list: null,
+      listQuery: {
+        page: 1,
+        limit: 5,
+        type: this.type,
+        sort: '+id'
+      },
+      loading: false
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      this.$emit('create') // for test
+      fetchList(this.listQuery).then(response => {
+        this.list = response.data.items
+        this.loading = false
+      })
+    }
+  }
+}
+</script>
+
diff --git a/frontend/src/views/tab/index.vue b/frontend/src/views/tab/index.vue
new file mode 100644
index 0000000..2a35fa5
--- /dev/null
+++ b/frontend/src/views/tab/index.vue
@@ -0,0 +1,57 @@
+<template>
+  <div class="tab-container">
+    <el-tag>mounted times :{{ createdTimes }}</el-tag>
+    <el-alert :closable="false" style="width:200px;display:inline-block;vertical-align: middle;margin-left:30px;" title="Tab with keep-alive" type="success" />
+    <el-tabs v-model="activeName" style="margin-top:15px;" type="border-card">
+      <el-tab-pane v-for="item in tabMapOptions" :key="item.key" :label="item.label" :name="item.key">
+        <keep-alive>
+          <tab-pane v-if="activeName==item.key" :type="item.key" @create="showCreatedTimes" />
+        </keep-alive>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script>
+import TabPane from './components/TabPane'
+
+export default {
+  name: 'Tab',
+  components: { TabPane },
+  data() {
+    return {
+      tabMapOptions: [
+        { label: 'China', key: 'CN' },
+        { label: 'USA', key: 'US' },
+        { label: 'Japan', key: 'JP' },
+        { label: 'Eurozone', key: 'EU' }
+      ],
+      activeName: 'CN',
+      createdTimes: 0
+    }
+  },
+  watch: {
+    activeName(val) {
+      this.$router.push(`${this.$route.path}?tab=${val}`)
+    }
+  },
+  created() {
+    // init the default selected tab
+    const tab = this.$route.query.tab
+    if (tab) {
+      this.activeName = tab
+    }
+  },
+  methods: {
+    showCreatedTimes() {
+      this.createdTimes = this.createdTimes + 1
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .tab-container {
+    margin: 30px;
+  }
+</style>
diff --git a/frontend/src/views/table/complex-table.vue b/frontend/src/views/table/complex-table.vue
new file mode 100644
index 0000000..295c5fc
--- /dev/null
+++ b/frontend/src/views/table/complex-table.vue
@@ -0,0 +1,379 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container">
+      <el-input v-model="listQuery.title" placeholder="Title" style="width: 200px;" class="filter-item" @keyup.enter.native="handleFilter" />
+      <el-select v-model="listQuery.importance" placeholder="Imp" clearable style="width: 90px" class="filter-item">
+        <el-option v-for="item in importanceOptions" :key="item" :label="item" :value="item" />
+      </el-select>
+      <el-select v-model="listQuery.type" placeholder="Type" clearable class="filter-item" style="width: 130px">
+        <el-option v-for="item in calendarTypeOptions" :key="item.key" :label="item.display_name+'('+item.key+')'" :value="item.key" />
+      </el-select>
+      <el-select v-model="listQuery.sort" style="width: 140px" class="filter-item" @change="handleFilter">
+        <el-option v-for="item in sortOptions" :key="item.key" :label="item.label" :value="item.key" />
+      </el-select>
+      <el-button v-waves class="filter-item" type="primary" icon="el-icon-search" @click="handleFilter">
+        Search
+      </el-button>
+      <el-button class="filter-item" style="margin-left: 10px;" type="primary" icon="el-icon-edit" @click="handleCreate">
+        Add
+      </el-button>
+      <el-button v-waves :loading="downloadLoading" class="filter-item" type="primary" icon="el-icon-download" @click="handleDownload">
+        Export
+      </el-button>
+      <el-checkbox v-model="showReviewer" class="filter-item" style="margin-left:15px;" @change="tableKey=tableKey+1">
+        reviewer
+      </el-checkbox>
+    </div>
+
+    <el-table
+      :key="tableKey"
+      v-loading="listLoading"
+      :data="list"
+      border
+      fit
+      highlight-current-row
+      style="width: 100%;"
+      @sort-change="sortChange"
+    >
+      <el-table-column label="ID" prop="id" sortable="custom" align="center" width="80" :class-name="getSortClass('id')">
+        <template slot-scope="{row}">
+          <span>{{ row.id }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="Date" width="150px" align="center">
+        <template slot-scope="{row}">
+          <span>{{ row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="Title" min-width="150px">
+        <template slot-scope="{row}">
+          <span class="link-type" @click="handleUpdate(row)">{{ row.title }}</span>
+          <el-tag>{{ row.type | typeFilter }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="Author" width="110px" align="center">
+        <template slot-scope="{row}">
+          <span>{{ row.author }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column v-if="showReviewer" label="Reviewer" width="110px" align="center">
+        <template slot-scope="{row}">
+          <span style="color:red;">{{ row.reviewer }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="Imp" width="80px">
+        <template slot-scope="{row}">
+          <svg-icon v-for="n in + row.importance" :key="n" icon-class="star" class="meta-item__icon" />
+        </template>
+      </el-table-column>
+      <el-table-column label="Readings" align="center" width="95">
+        <template slot-scope="{row}">
+          <span v-if="row.pageviews" class="link-type" @click="handleFetchPv(row.pageviews)">{{ row.pageviews }}</span>
+          <span v-else>0</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="Status" class-name="status-col" width="100">
+        <template slot-scope="{row}">
+          <el-tag :type="row.status | statusFilter">
+            {{ row.status }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="Actions" align="center" width="230" class-name="small-padding fixed-width">
+        <template slot-scope="{row,$index}">
+          <el-button type="primary" size="mini" @click="handleUpdate(row)">
+            Edit
+          </el-button>
+          <el-button v-if="row.status!='published'" size="mini" type="success" @click="handleModifyStatus(row,'published')">
+            Publish
+          </el-button>
+          <el-button v-if="row.status!='draft'" size="mini" @click="handleModifyStatus(row,'draft')">
+            Draft
+          </el-button>
+          <el-button v-if="row.status!='deleted'" size="mini" type="danger" @click="handleDelete(row,$index)">
+            Delete
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="listQuery.page" :limit.sync="listQuery.limit" @pagination="getList" />
+
+    <el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible">
+      <el-form ref="dataForm" :rules="rules" :model="temp" label-position="left" label-width="70px" style="width: 400px; margin-left:50px;">
+        <el-form-item label="Type" prop="type">
+          <el-select v-model="temp.type" class="filter-item" placeholder="Please select">
+            <el-option v-for="item in calendarTypeOptions" :key="item.key" :label="item.display_name" :value="item.key" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="Date" prop="timestamp">
+          <el-date-picker v-model="temp.timestamp" type="datetime" placeholder="Please pick a date" />
+        </el-form-item>
+        <el-form-item label="Title" prop="title">
+          <el-input v-model="temp.title" />
+        </el-form-item>
+        <el-form-item label="Status">
+          <el-select v-model="temp.status" class="filter-item" placeholder="Please select">
+            <el-option v-for="item in statusOptions" :key="item" :label="item" :value="item" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="Imp">
+          <el-rate v-model="temp.importance" :colors="['#99A9BF', '#F7BA2A', '#FF9900']" :max="3" style="margin-top:8px;" />
+        </el-form-item>
+        <el-form-item label="Remark">
+          <el-input v-model="temp.remark" :autosize="{ minRows: 2, maxRows: 4}" type="textarea" placeholder="Please input" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogFormVisible = false">
+          Cancel
+        </el-button>
+        <el-button type="primary" @click="dialogStatus==='create'?createData():updateData()">
+          Confirm
+        </el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog :visible.sync="dialogPvVisible" title="Reading statistics">
+      <el-table :data="pvData" border fit highlight-current-row style="width: 100%">
+        <el-table-column prop="key" label="Channel" />
+        <el-table-column prop="pv" label="Pv" />
+      </el-table>
+      <span slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="dialogPvVisible = false">Confirm</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { fetchList, fetchPv, createArticle, updateArticle } from '@/api/article'
+import waves from '@/directive/waves' // waves directive
+import { parseTime } from '@/utils'
+import Pagination from '@/components/Pagination' // secondary package based on el-pagination
+
+const calendarTypeOptions = [
+  { key: 'CN', display_name: 'China' },
+  { key: 'US', display_name: 'USA' },
+  { key: 'JP', display_name: 'Japan' },
+  { key: 'EU', display_name: 'Eurozone' }
+]
+
+// arr to obj, such as { CN : "China", US : "USA" }
+const calendarTypeKeyValue = calendarTypeOptions.reduce((acc, cur) => {
+  acc[cur.key] = cur.display_name
+  return acc
+}, {})
+
+export default {
+  name: 'ComplexTable',
+  components: { Pagination },
+  directives: { waves },
+  filters: {
+    statusFilter(status) {
+      const statusMap = {
+        published: 'success',
+        draft: 'info',
+        deleted: 'danger'
+      }
+      return statusMap[status]
+    },
+    typeFilter(type) {
+      return calendarTypeKeyValue[type]
+    }
+  },
+  data() {
+    return {
+      tableKey: 0,
+      list: null,
+      total: 0,
+      listLoading: true,
+      listQuery: {
+        page: 1,
+        limit: 20,
+        importance: undefined,
+        title: undefined,
+        type: undefined,
+        sort: '+id'
+      },
+      importanceOptions: [1, 2, 3],
+      calendarTypeOptions,
+      sortOptions: [{ label: 'ID Ascending', key: '+id' }, { label: 'ID Descending', key: '-id' }],
+      statusOptions: ['published', 'draft', 'deleted'],
+      showReviewer: false,
+      temp: {
+        id: undefined,
+        importance: 1,
+        remark: '',
+        timestamp: new Date(),
+        title: '',
+        type: '',
+        status: 'published'
+      },
+      dialogFormVisible: false,
+      dialogStatus: '',
+      textMap: {
+        update: 'Edit',
+        create: 'Create'
+      },
+      dialogPvVisible: false,
+      pvData: [],
+      rules: {
+        type: [{ required: true, message: 'type is required', trigger: 'change' }],
+        timestamp: [{ type: 'date', required: true, message: 'timestamp is required', trigger: 'change' }],
+        title: [{ required: true, message: 'title is required', trigger: 'blur' }]
+      },
+      downloadLoading: false
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.listLoading = true
+      fetchList(this.listQuery).then(response => {
+        this.list = response.data.items
+        this.total = response.data.total
+
+        // Just to simulate the time of the request
+        setTimeout(() => {
+          this.listLoading = false
+        }, 1.5 * 1000)
+      })
+    },
+    handleFilter() {
+      this.listQuery.page = 1
+      this.getList()
+    },
+    handleModifyStatus(row, status) {
+      this.$message({
+        message: '操作Success',
+        type: 'success'
+      })
+      row.status = status
+    },
+    sortChange(data) {
+      const { prop, order } = data
+      if (prop === 'id') {
+        this.sortByID(order)
+      }
+    },
+    sortByID(order) {
+      if (order === 'ascending') {
+        this.listQuery.sort = '+id'
+      } else {
+        this.listQuery.sort = '-id'
+      }
+      this.handleFilter()
+    },
+    resetTemp() {
+      this.temp = {
+        id: undefined,
+        importance: 1,
+        remark: '',
+        timestamp: new Date(),
+        title: '',
+        status: 'published',
+        type: ''
+      }
+    },
+    handleCreate() {
+      this.resetTemp()
+      this.dialogStatus = 'create'
+      this.dialogFormVisible = true
+      this.$nextTick(() => {
+        this.$refs['dataForm'].clearValidate()
+      })
+    },
+    createData() {
+      this.$refs['dataForm'].validate((valid) => {
+        if (valid) {
+          this.temp.id = parseInt(Math.random() * 100) + 1024 // mock a id
+          this.temp.author = 'vue-element-admin'
+          createArticle(this.temp).then(() => {
+            this.list.unshift(this.temp)
+            this.dialogFormVisible = false
+            this.$notify({
+              title: 'Success',
+              message: 'Created Successfully',
+              type: 'success',
+              duration: 2000
+            })
+          })
+        }
+      })
+    },
+    handleUpdate(row) {
+      this.temp = Object.assign({}, row) // copy obj
+      this.temp.timestamp = new Date(this.temp.timestamp)
+      this.dialogStatus = 'update'
+      this.dialogFormVisible = true
+      this.$nextTick(() => {
+        this.$refs['dataForm'].clearValidate()
+      })
+    },
+    updateData() {
+      this.$refs['dataForm'].validate((valid) => {
+        if (valid) {
+          const tempData = Object.assign({}, this.temp)
+          tempData.timestamp = +new Date(tempData.timestamp) // change Thu Nov 30 2017 16:41:05 GMT+0800 (CST) to 1512031311464
+          updateArticle(tempData).then(() => {
+            const index = this.list.findIndex(v => v.id === this.temp.id)
+            this.list.splice(index, 1, this.temp)
+            this.dialogFormVisible = false
+            this.$notify({
+              title: 'Success',
+              message: 'Update Successfully',
+              type: 'success',
+              duration: 2000
+            })
+          })
+        }
+      })
+    },
+    handleDelete(row, index) {
+      this.$notify({
+        title: 'Success',
+        message: 'Delete Successfully',
+        type: 'success',
+        duration: 2000
+      })
+      this.list.splice(index, 1)
+    },
+    handleFetchPv(pv) {
+      fetchPv(pv).then(response => {
+        this.pvData = response.data.pvData
+        this.dialogPvVisible = true
+      })
+    },
+    handleDownload() {
+      this.downloadLoading = true
+      import('@/vendor/Export2Excel').then(excel => {
+        const tHeader = ['timestamp', 'title', 'type', 'importance', 'status']
+        const filterVal = ['timestamp', 'title', 'type', 'importance', 'status']
+        const data = this.formatJson(filterVal)
+        excel.export_json_to_excel({
+          header: tHeader,
+          data,
+          filename: 'table-list'
+        })
+        this.downloadLoading = false
+      })
+    },
+    formatJson(filterVal) {
+      return this.list.map(v => filterVal.map(j => {
+        if (j === 'timestamp') {
+          return parseTime(v[j])
+        } else {
+          return v[j]
+        }
+      }))
+    },
+    getSortClass: function(key) {
+      const sort = this.listQuery.sort
+      return sort === `+${key}` ? 'ascending' : 'descending'
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/table/drag-table.vue b/frontend/src/views/table/drag-table.vue
new file mode 100644
index 0000000..3b5c890
--- /dev/null
+++ b/frontend/src/views/table/drag-table.vue
@@ -0,0 +1,153 @@
+<template>
+  <div class="app-container">
+    <!-- Note that row-key is necessary to get a correct row order. -->
+    <el-table ref="dragTable" v-loading="listLoading" :data="list" row-key="id" border fit highlight-current-row style="width: 100%">
+      <el-table-column align="center" label="ID" width="65">
+        <template slot-scope="{row}">
+          <span>{{ row.id }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column width="180px" align="center" label="Date">
+        <template slot-scope="{row}">
+          <span>{{ row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column min-width="300px" label="Title">
+        <template slot-scope="{row}">
+          <span>{{ row.title }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column width="110px" align="center" label="Author">
+        <template slot-scope="{row}">
+          <span>{{ row.author }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column width="100px" label="Importance">
+        <template slot-scope="{row}">
+          <svg-icon v-for="n in + row.importance" :key="n" icon-class="star" class="icon-star" />
+        </template>
+      </el-table-column>
+
+      <el-table-column align="center" label="Readings" width="95">
+        <template slot-scope="{row}">
+          <span>{{ row.pageviews }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column class-name="status-col" label="Status" width="110">
+        <template slot-scope="{row}">
+          <el-tag :type="row.status | statusFilter">
+            {{ row.status }}
+          </el-tag>
+        </template>
+      </el-table-column>
+
+      <el-table-column align="center" label="Drag" width="80">
+        <template slot-scope="{}">
+          <svg-icon class="drag-handler" icon-class="drag" />
+        </template>
+      </el-table-column>
+    </el-table>
+    <div class="show-d">
+      <el-tag>The default order :</el-tag> {{ oldList }}
+    </div>
+    <div class="show-d">
+      <el-tag>The after dragging order :</el-tag> {{ newList }}
+    </div>
+  </div>
+</template>
+
+<script>
+import { fetchList } from '@/api/article'
+import Sortable from 'sortablejs'
+
+export default {
+  name: 'DragTable',
+  filters: {
+    statusFilter(status) {
+      const statusMap = {
+        published: 'success',
+        draft: 'info',
+        deleted: 'danger'
+      }
+      return statusMap[status]
+    }
+  },
+  data() {
+    return {
+      list: null,
+      total: null,
+      listLoading: true,
+      listQuery: {
+        page: 1,
+        limit: 10
+      },
+      sortable: null,
+      oldList: [],
+      newList: []
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    async getList() {
+      this.listLoading = true
+      const { data } = await fetchList(this.listQuery)
+      this.list = data.items
+      this.total = data.total
+      this.listLoading = false
+      this.oldList = this.list.map(v => v.id)
+      this.newList = this.oldList.slice()
+      this.$nextTick(() => {
+        this.setSort()
+      })
+    },
+    setSort() {
+      const el = this.$refs.dragTable.$el.querySelectorAll('.el-table__body-wrapper > table > tbody')[0]
+      this.sortable = Sortable.create(el, {
+        ghostClass: 'sortable-ghost', // Class name for the drop placeholder,
+        setData: function(dataTransfer) {
+          // to avoid Firefox bug
+          // Detail see : https://github.com/RubaXa/Sortable/issues/1012
+          dataTransfer.setData('Text', '')
+        },
+        onEnd: evt => {
+          const targetRow = this.list.splice(evt.oldIndex, 1)[0]
+          this.list.splice(evt.newIndex, 0, targetRow)
+
+          // for show the changes, you can delete in you code
+          const tempIndex = this.newList.splice(evt.oldIndex, 1)[0]
+          this.newList.splice(evt.newIndex, 0, tempIndex)
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style>
+.sortable-ghost{
+  opacity: .8;
+  color: #fff!important;
+  background: #42b983!important;
+}
+</style>
+
+<style scoped>
+.icon-star{
+  margin-right:2px;
+}
+.drag-handler{
+  width: 20px;
+  height: 20px;
+  cursor: pointer;
+}
+.show-d{
+  margin-top: 15px;
+}
+</style>
diff --git a/frontend/src/views/table/dynamic-table/components/FixedThead.vue b/frontend/src/views/table/dynamic-table/components/FixedThead.vue
new file mode 100644
index 0000000..c3deb92
--- /dev/null
+++ b/frontend/src/views/table/dynamic-table/components/FixedThead.vue
@@ -0,0 +1,62 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container">
+      <el-checkbox-group v-model="checkboxVal">
+        <el-checkbox label="apple">
+          apple
+        </el-checkbox>
+        <el-checkbox label="banana">
+          banana
+        </el-checkbox>
+        <el-checkbox label="orange">
+          orange
+        </el-checkbox>
+      </el-checkbox-group>
+    </div>
+
+    <el-table :key="key" :data="tableData" border fit highlight-current-row style="width: 100%">
+      <el-table-column prop="name" label="fruitName" width="180" />
+      <el-table-column v-for="fruit in formThead" :key="fruit" :label="fruit">
+        <template slot-scope="scope">
+          {{ scope.row[fruit] }}
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+const defaultFormThead = ['apple', 'banana']
+
+export default {
+  data() {
+    return {
+      tableData: [
+        {
+          name: 'fruit-1',
+          apple: 'apple-10',
+          banana: 'banana-10',
+          orange: 'orange-10'
+        },
+        {
+          name: 'fruit-2',
+          apple: 'apple-20',
+          banana: 'banana-20',
+          orange: 'orange-20'
+        }
+      ],
+      key: 1, // table key
+      formTheadOptions: ['apple', 'banana', 'orange'],
+      checkboxVal: defaultFormThead, // checkboxVal
+      formThead: defaultFormThead // 默认表头 Default header
+    }
+  },
+  watch: {
+    checkboxVal(valArr) {
+      this.formThead = this.formTheadOptions.filter(i => valArr.indexOf(i) >= 0)
+      this.key = this.key + 1// 为了保证table 每次都会重渲 In order to ensure the table will be re-rendered each time
+    }
+  }
+}
+</script>
+
diff --git a/frontend/src/views/table/dynamic-table/components/UnfixedThead.vue b/frontend/src/views/table/dynamic-table/components/UnfixedThead.vue
new file mode 100644
index 0000000..831b070
--- /dev/null
+++ b/frontend/src/views/table/dynamic-table/components/UnfixedThead.vue
@@ -0,0 +1,50 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container">
+      <el-checkbox-group v-model="formThead">
+        <el-checkbox label="apple">
+          apple
+        </el-checkbox>
+        <el-checkbox label="banana">
+          banana
+        </el-checkbox>
+        <el-checkbox label="orange">
+          orange
+        </el-checkbox>
+      </el-checkbox-group>
+    </div>
+
+    <el-table :data="tableData" border fit highlight-current-row style="width: 100%">
+      <el-table-column prop="name" label="fruitName" width="180" />
+      <el-table-column v-for="fruit in formThead" :key="fruit" :label="fruit">
+        <template slot-scope="scope">
+          {{ scope.row[fruit] }}
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      tableData: [
+        {
+          name: 'fruit-1',
+          apple: 'apple-10',
+          banana: 'banana-10',
+          orange: 'orange-10'
+        },
+        {
+          name: 'fruit-2',
+          apple: 'apple-20',
+          banana: 'banana-20',
+          orange: 'orange-20'
+        }
+      ],
+      formThead: ['apple', 'banana']
+    }
+  }
+}
+</script>
diff --git a/frontend/src/views/table/dynamic-table/index.vue b/frontend/src/views/table/dynamic-table/index.vue
new file mode 100644
index 0000000..5a4dd36
--- /dev/null
+++ b/frontend/src/views/table/dynamic-table/index.vue
@@ -0,0 +1,24 @@
+<template>
+  <div class="app-container">
+    <div style="margin:0 0 5px 20px">
+      Fixed header, sorted by header order,
+    </div>
+    <fixed-thead />
+
+    <div style="margin:30px 0 5px 20px">
+      Not fixed header, sorted by click order
+    </div>
+    <unfixed-thead />
+  </div>
+</template>
+
+<script>
+import FixedThead from './components/FixedThead'
+import UnfixedThead from './components/UnfixedThead'
+
+export default {
+  name: 'DynamicTable',
+  components: { FixedThead, UnfixedThead }
+}
+</script>
+
diff --git a/frontend/src/views/table/inline-edit-table.vue b/frontend/src/views/table/inline-edit-table.vue
new file mode 100644
index 0000000..31e0065
--- /dev/null
+++ b/frontend/src/views/table/inline-edit-table.vue
@@ -0,0 +1,149 @@
+<template>
+  <div class="app-container">
+    <el-table v-loading="listLoading" :data="list" border fit highlight-current-row style="width: 100%">
+      <el-table-column align="center" label="ID" width="80">
+        <template slot-scope="{row}">
+          <span>{{ row.id }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column width="180px" align="center" label="Date">
+        <template slot-scope="{row}">
+          <span>{{ row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column width="120px" align="center" label="Author">
+        <template slot-scope="{row}">
+          <span>{{ row.author }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column width="100px" label="Importance">
+        <template slot-scope="{row}">
+          <svg-icon v-for="n in + row.importance" :key="n" icon-class="star" class="meta-item__icon" />
+        </template>
+      </el-table-column>
+
+      <el-table-column class-name="status-col" label="Status" width="110">
+        <template slot-scope="{row}">
+          <el-tag :type="row.status | statusFilter">
+            {{ row.status }}
+          </el-tag>
+        </template>
+      </el-table-column>
+
+      <el-table-column min-width="300px" label="Title">
+        <template slot-scope="{row}">
+          <template v-if="row.edit">
+            <el-input v-model="row.title" class="edit-input" size="small" />
+            <el-button
+              class="cancel-btn"
+              size="small"
+              icon="el-icon-refresh"
+              type="warning"
+              @click="cancelEdit(row)"
+            >
+              cancel
+            </el-button>
+          </template>
+          <span v-else>{{ row.title }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column align="center" label="Actions" width="120">
+        <template slot-scope="{row}">
+          <el-button
+            v-if="row.edit"
+            type="success"
+            size="small"
+            icon="el-icon-circle-check-outline"
+            @click="confirmEdit(row)"
+          >
+            Ok
+          </el-button>
+          <el-button
+            v-else
+            type="primary"
+            size="small"
+            icon="el-icon-edit"
+            @click="row.edit=!row.edit"
+          >
+            Edit
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+import { fetchList } from '@/api/article'
+
+export default {
+  name: 'InlineEditTable',
+  filters: {
+    statusFilter(status) {
+      const statusMap = {
+        published: 'success',
+        draft: 'info',
+        deleted: 'danger'
+      }
+      return statusMap[status]
+    }
+  },
+  data() {
+    return {
+      list: null,
+      listLoading: true,
+      listQuery: {
+        page: 1,
+        limit: 10
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    async getList() {
+      this.listLoading = true
+      const { data } = await fetchList(this.listQuery)
+      const items = data.items
+      this.list = items.map(v => {
+        this.$set(v, 'edit', false) // https://vuejs.org/v2/guide/reactivity.html
+        v.originalTitle = v.title //  will be used when user click the cancel botton
+        return v
+      })
+      this.listLoading = false
+    },
+    cancelEdit(row) {
+      row.title = row.originalTitle
+      row.edit = false
+      this.$message({
+        message: 'The title has been restored to the original value',
+        type: 'warning'
+      })
+    },
+    confirmEdit(row) {
+      row.edit = false
+      row.originalTitle = row.title
+      this.$message({
+        message: 'The title has been edited',
+        type: 'success'
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.edit-input {
+  padding-right: 100px;
+}
+.cancel-btn {
+  position: absolute;
+  right: 15px;
+  top: 10px;
+}
+</style>
diff --git a/frontend/src/views/theme/index.vue b/frontend/src/views/theme/index.vue
new file mode 100644
index 0000000..0af7711
--- /dev/null
+++ b/frontend/src/views/theme/index.vue
@@ -0,0 +1,120 @@
+<template>
+  <div class="app-container">
+    <el-card class="box-card">
+      <div slot="header">
+        <a class="link-type link-title" target="_blank" href="https://panjiachen.github.io/vue-element-admin-site/guide/advanced/theme.html">
+          Theme documentation
+        </a>
+      </div>
+      <div class="box-item">
+        <span class="field-label">Change Theme : </span>
+        <el-switch v-model="theme" />
+        <aside style="margin-top:15px;">
+          Tips: It is different from the theme-pick on the navbar is two different skinning methods, each with different application scenarios. Refer to the documentation for details.
+        </aside>
+      </div>
+    </el-card>
+
+    <div class="block">
+      <el-button type="primary">
+        Primary
+      </el-button>
+      <el-button type="success">
+        Success
+      </el-button>
+      <el-button type="info">
+        Info
+      </el-button>
+      <el-button type="warning">
+        Warning
+      </el-button>
+      <el-button type="danger">
+        Danger
+      </el-button>
+    </div>
+
+    <div class="block">
+      <el-button type="primary" icon="el-icon-edit" />
+      <el-button type="primary" icon="el-icon-share" />
+      <el-button type="primary" icon="el-icon-delete" />
+      <el-button type="primary" icon="el-icon-search">
+        Search
+      </el-button>
+      <el-button type="primary">
+        Upload
+        <i class="el-icon-upload el-icon-right" />
+      </el-button>
+    </div>
+
+    <div class="block">
+      <el-tag v-for="tag in tags" :key="tag.type" :type="tag.type" class="tag-item">
+        {{ tag.name }}
+      </el-tag>
+    </div>
+
+    <div class="block">
+      <el-radio-group v-model="radio">
+        <el-radio :label="3">
+          Option A
+        </el-radio>
+        <el-radio :label="6">
+          Option B
+        </el-radio>
+        <el-radio :label="9">
+          Option C
+        </el-radio>
+      </el-radio-group>
+    </div>
+
+    <div class="block">
+      <el-slider v-model="slideValue" />
+    </div>
+  </div>
+</template>
+
+<script>
+import { toggleClass } from '@/utils'
+import '@/assets/custom-theme/index.css' // the theme changed version element-ui css
+
+export default {
+  name: 'Theme',
+  data() {
+    return {
+      theme: false,
+      tags: [
+        { name: 'Tag One', type: '' },
+        { name: 'Tag Two', type: 'info' },
+        { name: 'Tag Three', type: 'success' },
+        { name: 'Tag Four', type: 'warning' },
+        { name: 'Tag Five', type: 'danger' }
+      ],
+      slideValue: 50,
+      radio: 3
+    }
+  },
+  watch: {
+    theme() {
+      toggleClass(document.body, 'custom-theme')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.field-label{
+  vertical-align: middle;
+}
+.box-card {
+  width: 400px;
+  max-width: 100%;
+  margin: 20px auto;
+}
+
+.block {
+  padding: 30px 24px;
+}
+
+.tag-item {
+  margin-right: 15px;
+}
+</style>
diff --git a/frontend/src/views/zip/index.vue b/frontend/src/views/zip/index.vue
new file mode 100644
index 0000000..412608f
--- /dev/null
+++ b/frontend/src/views/zip/index.vue
@@ -0,0 +1,77 @@
+<template>
+  <div class="app-container">
+    <el-input v-model="filename" placeholder="Please enter the file name (default file)" style="width:300px;" prefix-icon="el-icon-document" />
+    <el-button :loading="downloadLoading" style="margin-bottom:20px;" type="primary" icon="el-icon-document" @click="handleDownload">
+      Export Zip
+    </el-button>
+    <el-table v-loading="listLoading" :data="list" element-loading-text="拼命加载中" border fit highlight-current-row>
+      <el-table-column align="center" label="ID" width="95">
+        <template slot-scope="scope">
+          {{ scope.$index }}
+        </template>
+      </el-table-column>
+      <el-table-column label="Title">
+        <template slot-scope="scope">
+          {{ scope.row.title }}
+        </template>
+      </el-table-column>
+      <el-table-column label="Author" width="95" align="center">
+        <template slot-scope="scope">
+          <el-tag>{{ scope.row.author }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="Readings" width="115" align="center">
+        <template slot-scope="scope">
+          {{ scope.row.pageviews }}
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="Date" width="220">
+        <template slot-scope="scope">
+          <i class="el-icon-time" />
+          <span>{{ scope.row.display_time }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+import { fetchList } from '@/api/article'
+
+export default {
+  name: 'ExportZip',
+  data() {
+    return {
+      list: null,
+      listLoading: true,
+      downloadLoading: false,
+      filename: ''
+    }
+  },
+  created() {
+    this.fetchData()
+  },
+  methods: {
+    async fetchData() {
+      this.listLoading = true
+      const { data } = await fetchList()
+      this.list = data.items
+      this.listLoading = false
+    },
+    handleDownload() {
+      this.downloadLoading = true
+      import('@/vendor/Export2Zip').then(zip => {
+        const tHeader = ['Id', 'Title', 'Author', 'Readings', 'Date']
+        const filterVal = ['id', 'title', 'author', 'pageviews', 'display_time']
+        const list = this.list
+        const data = this.formatJson(filterVal, list)
+        zip.export_txt_to_zip(tHeader, data, this.filename, this.filename)
+        this.downloadLoading = false
+      })
+    },
+    formatJson(filterVal, jsonData) {
+      return jsonData.map(v => filterVal.map(j => v[j]))
+    }
+  }
+}
+</script>
diff --git a/frontend/vue.config.js b/frontend/vue.config.js
new file mode 100644
index 0000000..33a6348
--- /dev/null
+++ b/frontend/vue.config.js
@@ -0,0 +1,124 @@
+'use strict'
+const path = require('path')
+const defaultSettings = require('./src/settings.js')
+
+function resolve(dir) {
+  return path.join(__dirname, dir)
+}
+
+const name = defaultSettings.title || 'vue Element Admin' // page title
+
+// If your port is set to 80,
+// use administrator privileges to execute the command line.
+// For example, Mac: sudo npm run
+// You can change the port by the following method:
+// port = 9527 npm run dev OR npm run dev --port = 9527
+const port = process.env.port || process.env.npm_config_port || 9527 // dev port
+
+// All configuration item explanations can be find in https://cli.vuejs.org/config/
+module.exports = {
+  /**
+   * You will need to set publicPath if you plan to deploy your site under a sub path,
+   * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
+   * then publicPath should be set to "/bar/".
+   * In most cases please use '/' !!!
+   * Detail: https://cli.vuejs.org/config/#publicpath
+   */
+  publicPath: '/',
+  outputDir: 'dist',
+  assetsDir: 'static',
+  lintOnSave: process.env.NODE_ENV === 'development',
+  productionSourceMap: false,
+  devServer: {
+    port: port,
+    open: true,
+    overlay: {
+      warnings: false,
+      errors: true
+    },
+    before: require('./mock/mock-server.js')
+  },
+  configureWebpack: {
+    // provide the app's title in webpack's name field, so that
+    // it can be accessed in index.html to inject the correct title.
+    name: name,
+    resolve: {
+      alias: {
+        '@': resolve('src')
+      }
+    }
+  },
+  chainWebpack(config) {
+    // it can improve the speed of the first screen, it is recommended to turn on preload
+    // it can improve the speed of the first screen, it is recommended to turn on preload
+    config.plugin('preload').tap(() => [
+      {
+        rel: 'preload',
+        // to ignore runtime.js
+        // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171
+        fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],
+        include: 'initial'
+      }
+    ])
+
+    // when there are many pages, it will cause too many meaningless requests
+    config.plugins.delete('prefetch')
+
+    // set svg-sprite-loader
+    config.module
+      .rule('svg')
+      .exclude.add(resolve('src/icons'))
+      .end()
+    config.module
+      .rule('icons')
+      .test(/\.svg$/)
+      .include.add(resolve('src/icons'))
+      .end()
+      .use('svg-sprite-loader')
+      .loader('svg-sprite-loader')
+      .options({
+        symbolId: 'icon-[name]'
+      })
+      .end()
+
+    config
+      .when(process.env.NODE_ENV !== 'development',
+        config => {
+          config
+            .plugin('ScriptExtHtmlWebpackPlugin')
+            .after('html')
+            .use('script-ext-html-webpack-plugin', [{
+            // `runtime` must same as runtimeChunk name. default is `runtime`
+              inline: /runtime\..*\.js$/
+            }])
+            .end()
+          config
+            .optimization.splitChunks({
+              chunks: 'all',
+              cacheGroups: {
+                libs: {
+                  name: 'chunk-libs',
+                  test: /[\\/]node_modules[\\/]/,
+                  priority: 10,
+                  chunks: 'initial' // only package third parties that are initially dependent
+                },
+                elementUI: {
+                  name: 'chunk-elementUI', // split elementUI into a single package
+                  priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
+                  test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
+                },
+                commons: {
+                  name: 'chunk-commons',
+                  test: resolve('src/components'), // can customize your rules
+                  minChunks: 3, //  minimum common number
+                  priority: 5,
+                  reuseExistingChunk: true
+                }
+              }
+            })
+          // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk
+          config.optimization.runtimeChunk('single')
+        }
+      )
+  }
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..28861d2
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..115e6ac
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..206544b
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,3 @@
+rootProject.name = 'dali_portal'
+
+include 'backend'
\ No newline at end of file