ソースを参照

update:页面UI优化,新增账单页,发票内容页

lisiyan 10 ヶ月 前
コミット
1ca6371812

+ 2 - 1
.env.development

@@ -1,3 +1,4 @@
 VUE_APP_API_DOMAIN="http://test-pdf-pro.kdan.cn:3034"
-VUE_APP_PDFSDK_DOMAIN="http://test-pdf-pro.kdan.cn:3026"
+VUE_APP_PDFSdk_DOMAIN="http://test-pdf-pro.kdan.cn:3026"
+VUE_APP_SaaS_DOMAIN="http://localhost"
 VUE_APP_MODE_ENV="development"

+ 2 - 1
.env.preparing

@@ -1,3 +1,4 @@
 VUE_APP_API_DOMAIN="http://test-pdf-pro.kdan.cn:3034"
-VUE_APP_PDFSDK_DOMAIN="http://test-pdf-pro.kdan.cn:3026"
+VUE_APP_PDFSdk_DOMAIN="http://test-pdf-pro.kdan.cn:3026"
+VUE_APP_SaaS_DOMAIN="http://test-pdf-pro.kdan.cn:3036"
 VUE_APP_MODE_ENV="preparing"

+ 2 - 1
.env.production

@@ -1,3 +1,4 @@
 VUE_APP_API_DOMAIN="https://api.compdf.com"
-VUE_APP_PDFSDK_DOMAIN="https://www.compdf.com"
+VUE_APP_PDFSdk_DOMAIN="https://www.compdf.com"
+VUE_APP_SaaS_DOMAIN="https://api-dashboard.compdf.com/"
 VUE_APP_MODE_ENV="production"

+ 51 - 44
package-lock.json

@@ -1012,6 +1012,49 @@
         "webpack-merge": "^5.7.3",
         "webpack-virtual-modules": "^0.4.2",
         "whatwg-fetch": "^3.6.2"
+      },
+      "dependencies": {
+        "@vue/vue-loader-v15": {
+          "version": "npm:vue-loader@15.11.1",
+          "resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-15.11.1.tgz",
+          "integrity": "sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q==",
+          "dev": true,
+          "requires": {
+            "@vue/component-compiler-utils": "^3.1.0",
+            "hash-sum": "^1.0.2",
+            "loader-utils": "^1.1.0",
+            "vue-hot-reload-api": "^2.3.0",
+            "vue-style-loader": "^4.1.0"
+          },
+          "dependencies": {
+            "hash-sum": {
+              "version": "1.0.2",
+              "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-1.0.2.tgz",
+              "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==",
+              "dev": true
+            }
+          }
+        },
+        "json5": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz",
+          "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+          "dev": true,
+          "requires": {
+            "minimist": "^1.2.0"
+          }
+        },
+        "loader-utils": {
+          "version": "1.4.2",
+          "resolved": "https://registry.npmmirror.com/loader-utils/-/loader-utils-1.4.2.tgz",
+          "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
+          "dev": true,
+          "requires": {
+            "big.js": "^5.2.2",
+            "emojis-list": "^3.0.0",
+            "json5": "^1.0.1"
+          }
+        }
       }
     },
     "@vue/cli-shared-utils": {
@@ -1126,47 +1169,6 @@
         "vue-eslint-parser": "^8.0.0"
       }
     },
-    "@vue/vue-loader-v15": {
-      "version": "npm:vue-loader@15.10.1",
-      "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.1.tgz",
-      "integrity": "sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA==",
-      "dev": true,
-      "requires": {
-        "@vue/component-compiler-utils": "^3.1.0",
-        "hash-sum": "^1.0.2",
-        "loader-utils": "^1.1.0",
-        "vue-hot-reload-api": "^2.3.0",
-        "vue-style-loader": "^4.1.0"
-      },
-      "dependencies": {
-        "hash-sum": {
-          "version": "1.0.2",
-          "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz",
-          "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==",
-          "dev": true
-        },
-        "json5": {
-          "version": "1.0.2",
-          "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
-          "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
-          "dev": true,
-          "requires": {
-            "minimist": "^1.2.0"
-          }
-        },
-        "loader-utils": {
-          "version": "1.4.2",
-          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
-          "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
-          "dev": true,
-          "requires": {
-            "big.js": "^5.2.2",
-            "emojis-list": "^3.0.0",
-            "json5": "^1.0.1"
-          }
-        }
-      }
-    },
     "@vue/web-component-wrapper": {
       "version": "1.3.0",
       "resolved": "https://registry.npmmirror.com/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz",
@@ -4573,9 +4575,9 @@
       }
     },
     "js-cookie": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz",
-      "integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw=="
+      "version": "3.0.5",
+      "resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz",
+      "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="
     },
     "js-message": {
       "version": "1.0.7",
@@ -7269,6 +7271,11 @@
       "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==",
       "dev": true
     },
+    "vue-i18n": {
+      "version": "8.28.2",
+      "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-8.28.2.tgz",
+      "integrity": "sha512-C5GZjs1tYlAqjwymaaCPDjCyGo10ajUphiwA922jKt9n7KPpqR7oM1PCwYzhB/E7+nT3wfdG3oRre5raIT1rKA=="
+    },
     "vue-loader": {
       "version": "17.0.1",
       "resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-17.0.1.tgz",

+ 2 - 1
package.json

@@ -15,9 +15,10 @@
     "dayjs": "^1.11.6",
     "echarts": "^5.4.0",
     "element-ui": "^2.15.12",
-    "js-cookie": "^3.0.1",
+    "js-cookie": "^3.0.5",
     "pinia": "^2.0.24",
     "vue": "^2.7.14",
+    "vue-i18n": "^8.28.2",
     "vue-router": "^3.5.1"
   },
   "devDependencies": {

+ 1 - 0
public/index.html

@@ -5,6 +5,7 @@
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no,maximum-scale=1,minimum-scale=1">
     <link rel="icon" href="/favicon.ico">
+    <link rel="stylesheet" hid="stylesheet" href="https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700,900">
     <title>ComPDFKit API for Developers</title>
   </head>
   <body>

src/assets/images/apikeys/delete.svg → src/assets/images/apiKey/delete.svg


src/assets/images/apikeys/edit.svg → src/assets/images/apiKey/edit.svg


+ 10 - 11
src/assets/scss/common.scss

@@ -1,6 +1,5 @@
 html {
-  // font-family: "Helvetica", Arial, "PingFang-SC-Regular", "ArialMT", "微软雅黑", "Microsoft YaHei", "Helvetica Neue", Helvetica, sans-serif;
-  font-family: 'Helvetica';
+  font-family: "Poppins", Arial, "PingFang-SC-Regular", "ArialMT", "微软雅黑", "Microsoft YaHei", "Helvetica Neue", Helvetica, sans-serif;
   font-style: normal;
   font-size: 16px;
   line-height: 24px;
@@ -14,7 +13,7 @@ body {
   background: #F4F8FF;
   text-align: center;
   font-weight: 400;
-  color: #000;
+  color: #232748;
 }
 body, h1, h2, h3, h4, h6, ul, p, input {
   margin: 0;
@@ -22,7 +21,7 @@ body, h1, h2, h3, h4, h6, ul, p, input {
 }
 h1, h2, h3, h4 {
   text-align: left;
-  font-weight: 700;
+  font-weight: 600;
 }
 h1 {
   font-size: 32px;
@@ -31,7 +30,7 @@ h1 {
 }
 h2 {
   font-size: 24px;
-  line-height: 32px;
+  line-height: 36px;
   color: #18191B;
 }
 li {
@@ -85,7 +84,6 @@ input {
   margin-right: auto;
 }
 .font-bold {
-  font-family: Helvetica-Bold, Helvetica;
   font-weight: bold;
 }
 .container {
@@ -107,19 +105,20 @@ input {
   left: 0
 }
 select {
-  padding-left: 8px;
-  padding-right: 20px;
   width: 180px;
   height: 28px;
-  border: 1px solid #D9D9D9;
-  border-radius: 4px;
   appearance: none;
+  padding-left: 8px;
+  border-radius: 4px;
+  padding-right: 20px;
   -moz-appearance: none;  
+  font-family: "Poppins";
   -webkit-appearance: none;
+  border: 1px solid #E1E3E8;
   background: url('@/assets/images/common/select_icon.svg') no-repeat calc(100% - 8px) center transparent;
   font-size: 14px;
   line-height: 20px;
-  color: #43474D;
+  color: #52555F;
   text-overflow: ellipsis;
   &:focus-visible {
     outline: 1px solid #1460F3;

+ 37 - 14
src/assets/scss/element-variables.scss

@@ -8,11 +8,11 @@ $--font-path: '~element-ui/lib/theme-chalk/fonts';
 .el-form {
   .el-form-item {
     .el-form-item__label {
-      font-family: 'Helvetica';
-       font-style: normal;
+      font-family: 'Poppins';
+      font-style: normal;
       font-size: 14px;
       line-height: 16px;
-      font-weight: 700;
+      font-weight: 600;
       padding-bottom: 10px;
       color: #43474D;
       .rule-tip {
@@ -89,6 +89,13 @@ $--font-path: '~element-ui/lib/theme-chalk/fonts';
   font-size: 14px;
   line-height: 20px;
 }
+@media screen and (max-width: 767px) {
+  .el-form-item__error {
+    color: #FF5050;
+    font-size: 12px;
+    line-height: 16px;
+  }
+}
 .el-form-item__error--inline {
   margin-left: 0;
 }
@@ -150,7 +157,7 @@ $--font-path: '~element-ui/lib/theme-chalk/fonts';
 .el-message-box {
   border-radius: 16px;
 }
-.security-msgbox {
+.msgBox {
   &.el-message-box--center {
     padding-bottom: 0;
     .el-message-box__btns {
@@ -214,6 +221,9 @@ $--font-path: '~element-ui/lib/theme-chalk/fonts';
 .el-table {
   th.el-table__cell {
     padding: 16px 0;
+    font-size: 14px;
+    font-weight: 600;
+    line-height: 20px;
     > .cell {
       padding: 0;
       color: #43474D;
@@ -276,6 +286,9 @@ $--font-path: '~element-ui/lib/theme-chalk/fonts';
   box-shadow: 0px 4px 16px rgba(129, 149, 200, 0.18);
   text-align: left;
   line-height: 16px;
+  span {
+    font-weight: 600;
+  }
 }
 // pagination分页器 - Plan页
 .el-pagination {
@@ -364,12 +377,14 @@ $--font-path: '~element-ui/lib/theme-chalk/fonts';
     font-size: 14px;
   }
   .el-submenu .el-menu-item {
-    height: 32px;
-    padding: 0;
-    padding-left: 38px !important;
-    margin-bottom: 8px;
-    line-height: 32px;
+    height: auto;
+    padding: 4px 0;
+    font-size: 16px;
     font-size: 16px;
+    line-height: 24px;
+    padding-left: 40px;
+    margin-bottom: 8px;
+    white-space: normal;
   }
   .el-menu-item:hover, .el-menu-item:focus, .el-menu-item.is-active {
     background-color: #e7effe;
@@ -399,17 +414,22 @@ $--font-path: '~element-ui/lib/theme-chalk/fonts';
   }
   .el-menu--horizontal > .el-submenu .el-submenu__title {
     height: 40px;
-    line-height: 40px;
     padding: 0 16px;
+    font-weight: 600;
+    line-height: 40px;
+    font-family: 'Poppins';
+    span {
+      color: #6A6F77;
+    }
   }
   .el-menu--horizontal > .el-submenu.is-active .el-submenu__title {
-    font-weight: 700;
     border-bottom: none;
     & > span {
-      display: inline-block;
       height: 100%;
-      border-bottom: 2px solid #1460F3;
+      color: #232748;
       border-radius: 1px;
+      display: inline-block;
+      border-bottom: 2px solid #1460F3;
     }
   }
   .el-menu--popup {
@@ -474,7 +494,7 @@ $--font-path: '~element-ui/lib/theme-chalk/fonts';
   .el-message-box {
     width: calc(100% - 60px);
   }
-  .security-msgbox {
+  .security-msgBox {
     &.el-message-box--center .el-message-box__content {
       padding-bottom: 0;
       .el-message-box__message h2 {
@@ -486,6 +506,9 @@ $--font-path: '~element-ui/lib/theme-chalk/fonts';
       }
     }
   }
+  .el-select {
+    width: 100%;
+  }
   // select下拉展开框
   .el-select-dropdown.el-popper {
     width: calc(100% - 20px);

+ 50 - 45
src/components/calendar/calendar.vue

@@ -10,7 +10,7 @@
 
       <div class="title">
         <div class="btn btn-left" @click="last()"><img src="@/assets/images/dashboard/calendar_arrow.svg" alt="calendar_arrow"></div>
-        <div class="text">{{ formatMonth(month) + ' ' + year }}</div>
+        <div class="text">{{ dataTime }}</div>
         <div class="btn btn-right" @click="next()"><img src="@/assets/images/dashboard/calendar_arrow.svg" alt="calendar_arrow"></div>
       </div>
 
@@ -18,13 +18,13 @@
         <div
           class="days"
           v-for="(item, index) in [
-            'Sun',
-            'Mon',
-            'Tues',
-            'Wed',
-            'Thur',
-            'Fri',
-            'Sat',
+            $t('calendar.week.Sun'),
+            $t('calendar.week.Mon'),
+            $t('calendar.week.Tues'),
+            $t('calendar.week.Wed'),
+            $t('calendar.week.Thur'),
+            $t('calendar.week.Fri'),
+            $t('calendar.week.Sat'),
           ]"
           :key="index"
         >
@@ -63,7 +63,7 @@
       </div>
 
       <div class="bottom-btn">
-        <button class="sure-btn" :class="{'disabled': startTime === '' || endTime === ''}" @click="firm()">Custom date range</button>
+        <button class="sure-btn" :class="{'disabled': startTime === '' || endTime === ''}" @click="firm()">{{ $t('dashboard.customDate') }}</button>
       </div>
     </div>
   </div>
@@ -72,7 +72,7 @@
 import { Vue, Component, Prop } from 'vue-property-decorator'
 import dayjs from 'dayjs'
 
-interface Icalendar {
+interface isCalendar {
   count: string,
   value: number|string
 }
@@ -82,8 +82,8 @@ export default class Calender extends Vue {
 
   // data
   nowTime = '' // 当前日期的时间戳
-  clickitem = '0' // 点击的时间戳
-  clickcount = 0 // 点击次数
+  clickItem = '0' // 点击的时间戳
+  clickCount = 0 // 点击次数
   startTime = '' // 开始时间 数字   默认选中当天日期
   endTime = '' // 结束时间 数字
   year = new Date().getFullYear() // 日历上的年份
@@ -93,7 +93,7 @@ export default class Calender extends Vue {
   nowMonth: number|string = new Date().getMonth() + 1
   nowDay: number|string = new Date().getDate()
   firstLoginTime = ''
-  calendarList: Icalendar[] = []
+  calendarList: isCalendar[] = []
 
   created () {
     this.Draw(this.nowYear, this.nowMonth)
@@ -108,6 +108,11 @@ export default class Calender extends Vue {
     this.nowTime = this.nowYear + '-' + timeMonth + '-' + timeDay
   }
 
+  private get dataTime (): string {
+    const time = this.$i18n.locale === 'en' ? this.formatMonth(this.month) + ' ' + this.year : this.year + '年' + this.formatMonth(this.month)
+    return time
+  }
+
   // methods
   Draw (year: number, month: any) {
     // 日期列表
@@ -144,7 +149,6 @@ export default class Calender extends Vue {
       })
     }
     this.calendarList = calendar
-    // console.log(calendar)
   }
 
   last () {
@@ -166,17 +170,17 @@ export default class Calender extends Vue {
   }
 
   click (item: string) {
-    this.clickcount++
-    this.clickitem = item
+    this.clickCount++
+    this.clickItem = item
     // 开始日期
-    if (this.clickcount % 2 === 1) {
-      this.startTime = this.clickitem
+    if (this.clickCount % 2 === 1) {
+      this.startTime = this.clickItem
       this.endTime = ''
     } else {
-      this.endTime = this.clickitem
+      this.endTime = this.clickItem
       if (this.startTime > this.endTime) {
         this.endTime = this.startTime
-        this.startTime = this.clickitem
+        this.startTime = this.clickItem
       }
     }
   }
@@ -191,38 +195,38 @@ export default class Calender extends Vue {
   formatterDate (data: string) {
     if (!data) return ''
     const dt = new Date(data)
-    let month = ''
-    month = this.formatMonth(dt.getMonth() + 1)
-    return month + ' ' + dt.getDate() + ', ' + dt.getFullYear()
+    const month = this.formatMonth(dt.getMonth() + 1)
+    const date = this.$i18n.locale === 'en' ? month + ' ' + dt.getDate() + ', ' + dt.getFullYear() : dt.getFullYear() + '年' + month + dt.getDate()
+    return date
   }
 
   // 格式化月份
   formatMonth (month: number) {
     switch (month) {
       case 1:
-        return 'Jan'
+        return `${this.$t('calendar.month.Jan')}`
       case 2:
-        return 'Feb'
+        return `${this.$t('calendar.month.Feb')}`
       case 3:
-        return 'Mar'
+        return `${this.$t('calendar.month.Mar')}`
       case 4:
-        return 'Apr'
+        return `${this.$t('calendar.month.Apr')}`
       case 5:
-        return 'May'
+        return `${this.$t('calendar.month.May')}`
       case 6:
-        return 'Jun'
+        return `${this.$t('calendar.month.Jun')}`
       case 7:
-        return 'Jul'
+        return `${this.$t('calendar.month.Jul')}`
       case 8:
-        return 'Aug'
+        return `${this.$t('calendar.month.Aug')}`
       case 9:
-        return 'Sept'
+        return `${this.$t('calendar.month.Sept')}`
       case 10:
-        return 'Oct'
+        return `${this.$t('calendar.month.Oct')}`
       case 11:
-        return 'Nov'
+        return `${this.$t('calendar.month.Nov')}`
       case 12:
-        return 'Dec'
+        return `${this.$t('calendar.month.Dec')}`
       default:
         return ''
     }
@@ -312,20 +316,21 @@ export default class Calender extends Vue {
   }
 
   &.disabled {
+    color: #BABABA;
     pointer-events: none;
-    color: #AAAEB2;
+    background-color: #E8E8E8;
   }
 }
 
 .title {
   width: calc(100% - 46px);
-  height: 62px;
+  height: 24px;
   display: flex;
   flex-wrap: nowrap;
   text-align: center;
   color: #18191B;
   font-weight: bold;
-  line-height: 62px;
+  line-height: 24px;
   font-size: 20px;
   font-weight: 700;
   margin: 0 23px;
@@ -347,15 +352,16 @@ export default class Calender extends Vue {
 }
 
 .head {
-  display: flex;
-  flex-wrap: nowrap;
-  text-align: center;
   height: 40px;
-  line-height: 40px;
+  display: flex;
+  margin: 0 26px;
   font-size: 14px;
-  font-weight: 700;
   color: #43474D;
-  margin: 0 26px;
+  font-weight: 700;
+  margin-top: 38px;
+  flex-wrap: nowrap;
+  line-height: 40px;
+  text-align: center;
   justify-content: space-between;
   .days {
     width: 28px;
@@ -369,7 +375,6 @@ export default class Calender extends Vue {
   .sure-btn {
     padding: 10px 17.5px;
     margin-right: 24px;
-    height: 36px;
     border-radius: 6px;
     background: #1665FF;
     color: #fff;

+ 163 - 46
src/components/common/Aside.vue

@@ -1,49 +1,63 @@
 <template>
   <div class="aside">
-    <div class="max">
-      <div class="user-info">
-        <div class="head-photo"><p>{{ user && user.name.slice(0, 1) }}</p></div>
-        <div class="info">
-          <p>{{ user && user.name }}</p>
-          <p>{{ user && user.email }}</p>
+    <div class="desktop">
+      <div class="max">
+        <div class="user-info">
+          <div class="head-photo"><p>{{ name.slice(0, 1) }}</p></div>
+          <div class="info">
+            <p>{{ name }}</p>
+            <p>{{ email }}</p>
+          </div>
+        </div>
+        <el-menu :default-active="path" active-text-color="#1460F3" text-color="#666" router>
+          <el-submenu index="1">
+            <template slot="title">
+              <Project />
+              <span>{{ $t('aside.project') }}</span>
+            </template>
+            <el-menu-item index="/dashboard">{{ $t('aside.dashboard') }}</el-menu-item>
+            <el-menu-item :index="'/api/keys'||'/api/keys/new-project'||'api/keys/edit-project'">{{ $t('aside.apiKeys') }}</el-menu-item>
+            <el-menu-item index="/user/webhooks">{{ $t('aside.webhook') }}</el-menu-item>
+          </el-submenu>
+          <el-submenu index="2">
+            <template slot="title">
+              <Settings />
+              <span>{{ $t('aside.setting') }}</span>
+            </template>
+            <el-menu-item index="/user/password">{{ $t('aside.security') }}</el-menu-item>
+          </el-submenu>
+          <el-submenu index="3">
+            <template slot="title">
+              <Billing />
+              <span>{{ $t('aside.billing') }}</span>
+            </template>
+            <el-menu-item index="/billing/plan">{{ $t('aside.plan') }}</el-menu-item>
+            <el-menu-item index="/billing/information">{{ $t('aside.information') }}</el-menu-item>
+            <el-menu-item index="/billing/invoices">{{ $t('aside.invoices') }}</el-menu-item>
+          </el-submenu>
+          <el-submenu index="4">
+            <template slot="title">
+              <Support />
+              <span>{{ $t('aside.support') }}</span>
+            </template>
+            <el-menu-item index="">
+              <a :href="apiDomain + '/api/docs/introduction'" class="downloadDoc">
+                {{ $t('aside.document') }}
+              </a>
+            </el-menu-item>
+          </el-submenu>
+        </el-menu>
+      </div>
+      <div @click.stop="changeLangEvent" class="switch">
+        <div class="choose" :class="languageActive && 'chooseActive'">
+          <p  @click.stop="changeLangEvent('en')">English</p>
+          <p @click.stop="changeLangEvent('zh-cn')">Chinese</p>
+        </div>
+        <div :class="languageActive && 'active'" class="sideBox">
+          <Language />
+          {{ $i18n.locale === 'en' ? 'English' : 'Chinese' }}
         </div>
       </div>
-      <el-menu :default-active="path" active-text-color="#1460F3" text-color="#666" router>
-        <el-submenu index="1">
-          <template slot="title">
-            <Project />
-            <span>Projects</span>
-          </template>
-          <el-menu-item index="/dashboard">Dashboard</el-menu-item>
-          <el-menu-item :index="'/api/keys'||'/api/keys/new-project'||'api/keys/edit-project'">API Keys</el-menu-item>
-          <el-menu-item index="/user/webhooks">Webhooks</el-menu-item>
-        </el-submenu>
-        <el-submenu index="2">
-          <template slot="title">
-            <Settings />
-            <span>Account</span>
-          </template>
-          <el-menu-item index="/user/password">Security</el-menu-item>
-        </el-submenu>
-        <el-submenu index="3">
-          <template slot="title">
-            <Billing />
-            <span>Billing</span>
-          </template>
-          <el-menu-item index="/billing/plan">Plan</el-menu-item>
-        </el-submenu>
-        <el-submenu index="4">
-          <template slot="title">
-            <Support />
-            <span>Support</span>
-          </template>
-          <el-menu-item index="">
-            <a :href="apiDomain + '/api/docs/introduction'" class="downloadDoc">
-              Documentation
-            </a>
-          </el-menu-item>
-        </el-submenu>
-      </el-menu>
     </div>
     <!-- 移动端 -->
     <el-menu class="min" :default-active="path" mode="horizontal" menu-trigger="click" unique-opened
@@ -70,6 +84,15 @@
           </a>
         </el-menu-item>
       </el-submenu>
+      <el-submenu index="5">
+        <template slot="title"><span>{{ $i18n.locale === 'en' ? 'English' : 'Chinese' }}</span></template>
+        <el-menu-item  @click="changeLangEvent('en')">
+          English
+        </el-menu-item>
+        <el-menu-item @click="changeLangEvent('zh-cn')">
+          Chinese
+        </el-menu-item>
+      </el-submenu>
     </el-menu>
   </div>
 </template>
@@ -77,24 +100,43 @@
 <script lang="ts">
 import { Component, Vue } from 'vue-property-decorator'
 import { loginStore } from '@/store/loginStore'
+import i18n from '@/i18n'
 import Project from '@/components/icon/menu_project.vue'
 import Settings from '@/components/icon/menu_setting.vue'
 import Billing from '@/components/icon/menu_billing.vue'
 import Support from '@/components/icon/menu_support.vue'
+import Language from '@/components/icon/changeLanguage.vue'
 
 @Component({
   components: {
     Project,
     Settings,
     Billing,
-    Support
+    Support,
+    Language
   }
 })
 export default class Aside extends Vue {
   // data
   apiDomain = process.env.VUE_APP_API_DOMAIN
+  languageActive = false
+
+  created () {
+    window.addEventListener('click', this.handleGlobalClick)
+  }
+
+  beforeDestroy () {
+    window.removeEventListener('click', this.handleGlobalClick)
+  }
+
+  get name () {
+    return loginStore().user.name
+  }
+
+  get email () {
+    return loginStore().user.email
+  }
 
-  user = loginStore().user
   get path () {
     if (this.$route.path === '/api/keys/new-project') {
       return '/api/keys'
@@ -108,6 +150,21 @@ export default class Aside extends Vue {
       return this.$route.path
     }
   }
+
+  handleGlobalClick () {
+    this.languageActive = false
+  }
+
+  changeLangEvent (val: string) {
+    this.languageActive = !this.languageActive
+    if (val === 'en') {
+      i18n.locale = 'en'
+      localStorage.setItem('locale', 'en')
+    } else if (val === 'zh-cn') {
+      i18n.locale = 'zh-cn'
+      localStorage.setItem('locale', 'zh-cn')
+    }
+  }
 }
 </script>
 
@@ -119,13 +176,73 @@ export default class Aside extends Vue {
   display: none;
 }
 .aside {
+  .desktop {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    .switch {
+      left: 70px;
+      bottom: 36px;
+      display: flex;
+      cursor: pointer;
+      position: sticky;
+      align-items: center;
+      margin-bottom: 36px;
+      justify-content: center;
+      .sideBox {
+        display: flex;
+        max-width: 120px;
+        border-radius: 6px;
+        padding: 8px 15px;
+        align-items: center;
+        justify-content: center;
+      }
+      .choose {
+        top: -101px;
+        display: none;
+        font-size: 16px;
+        color: #52555F;
+        overflow: hidden;
+        line-height: 24px;
+        position: absolute;
+        border-radius: 6px;
+        background: white;
+        box-shadow: 0px 3.975px 34.784px 0px rgba(129, 149, 200, 0.18);
+        p {
+          padding: 12px;
+          text-align: left;
+          min-width: 120px;
+          &:hover {
+            background-color: #EBF1FE;
+          }
+        }
+      }
+      .chooseActive {
+        display: block;
+      }
+      .active {
+        background-color: #EBF1FE;
+      }
+      svg {
+        min-width: 20px;
+        min-height: 20px;
+        margin-right: 8px;
+      }
+    }
+  }
   min-width: 260px;
   max-width: 260px;
   min-height: 100%;
   padding: 40px 20px 0;
   background-color: #fff;
-  ::v-deep .el-submenu.is-active .el-submenu__title {
-    color: #1460F3 !important;
+  ::v-deep .el-submenu {
+    .el-submenu__title {
+      font-weight: 600;
+    }
+    &.is-active .el-submenu__title {
+    color: #396FFA !important;
+  }
   }
   .user-info {
     display: flex;

+ 1 - 1
src/components/common/Header.vue

@@ -141,7 +141,7 @@ export default Vue.extend({
   }
   .link {
     padding: 28px 0;
-    font-weight: bold;
+    font-weight: 600;
     font-size: 16px;
     line-height: 24px;
     color: #18191B;

ファイルの差分が大きいため隠しています
+ 5 - 0
src/components/icon/changeLanguage.vue


ファイルの差分が大きいため隠しています
+ 5 - 0
src/components/icon/refresh.vue


+ 29 - 15
src/components/progress/Progress.vue

@@ -1,13 +1,17 @@
 <template>
   <div class="progress block">
     <div class="progress-content">
-      <div class="text">
-        <span><span :class="{'full': usedAmount > totalAmount*0.9}">{{ usedAmount }}</span> / {{ totalAmount }} <br class="min"/>processed files this month.</span>
-        <router-link to="/billing/plan" v-if="$route.path === '/dashboard'">More details</router-link>
+      <div class="text" v-if="$i18n.locale === 'en'">
+        <span><span :class="{'full': usedAmount > totalAmount*0.9}">{{ usedAmount }}</span> / {{ totalAmount }} <br class="min"/>{{ $t('dashboard.file') }}</span>
+        <router-link to="/billing/plan" v-if="$route.path === '/dashboard'">{{ $t('dashboard.details') }}</router-link>
+      </div>
+      <div class="text" v-else>
+        <span>{{ $t('dashboard.file') }} <br class="min"/><span :class="{'full': usedAmount > totalAmount*0.9}">{{ usedAmount }}</span> / {{ totalAmount }}</span>
+        <router-link to="/billing/plan" v-if="$route.path === '/dashboard'">{{ $t('dashboard.details') }}</router-link>
       </div>
       <div @click="refresh" class="refresh">
-        <img src="@/assets/images/common/refresh.svg" alt="refresh" :class="{'rotate360': rotate}" @animationend="rotateReset">
-        <a>Refresh</a>
+        <Refresh :class="{'rotate360': rotate}" @animationend="rotateReset" />
+        <p>{{ $t('dashboard.refresh') }}</p>
       </div>
     </div>
     <div class="progress-line">
@@ -19,15 +23,19 @@
 <script lang="ts">
 import { Vue, Component, Prop } from 'vue-property-decorator'
 import { getPackageBalance } from '@/request/api'
+import Refresh from '@/components/icon/refresh.vue'
 
-@Component
+@Component({
+  components: {
+    Refresh
+  }
+})
 export default class Progress extends Vue {
   @Prop() init!: {
     type: boolean,
     default: true
   }
 
-  // inject: ['reload'], // 注入App里的reload方法
   usedAmount = 0
   totalAmount = 0
   rotate = false
@@ -41,7 +49,6 @@ export default class Progress extends Vue {
 
   refresh () {
     this.rotate = true
-    // this.reload()
     this.getFileAmount()
   }
 
@@ -74,12 +81,17 @@ export default class Progress extends Vue {
     justify-content: space-between;
     align-items: center;
     margin-bottom: 16px;
-    .text > span > span {
-      font-size: 20px;
-      font-weight: 700;
-      color: #1460F3;
-      &.full {
-        color: #FF5050;
+    .text {
+      & > span {
+        color: #18191B;
+        & > span {
+        font-size: 20px;
+        font-weight: 700;
+        color: #1460F3;
+        &.full {
+          color: #FF5050;
+        }
+      }
       }
     }
     .text > a {
@@ -92,7 +104,9 @@ export default class Progress extends Vue {
       display: flex;
       align-items: center;
       cursor: pointer;
-      a {
+      p {
+        cursor: pointer;
+        color: #396FFA;
         margin-left: 6px;
       }
     }

+ 19 - 0
src/i18n.ts

@@ -0,0 +1,19 @@
+import Vue from 'vue'
+import VueI18n from 'vue-i18n'
+import zh from '@/locales/zh'
+import en from '@/locales/en'
+
+Vue.use(VueI18n)
+const locale = localStorage.getItem('locale')
+const defaultLocale = navigator.language.toLocaleLowerCase() === 'zh-cn' ? 'zh-cn' : 'en'
+const language = locale || defaultLocale
+const i18n = new VueI18n({
+  locale: language,
+  fallbackLocale: 'en',
+  messages: {
+    en,
+    'zh-cn': zh
+  }
+})
+
+export default i18n

+ 277 - 0
src/locales/en.ts

@@ -0,0 +1,277 @@
+// en.ts
+export default {
+  errorSubmit: 'Error submit.',
+  failedConnect: 'Failed to connect',
+  copiedFail: 'Copied Failed.',
+  aside: {
+    project: 'Projects',
+    dashboard: 'Dashboard',
+    apiKeys: 'API Keys',
+    webhook: 'Webhooks',
+    setting: 'Account Setting',
+    security: 'Security',
+    billing: 'Billing',
+    plan: 'Plan',
+    information: 'Billing Information ',
+    invoices: 'Invoices Center',
+    support: 'Support',
+    document: 'Documentation '
+  },
+  dashboard: {
+    title: 'Dashboard',
+    file: 'processed files this month.',
+    details: 'More details',
+    refresh: 'Refresh',
+    view: 'View by',
+    day: 'Last 24H',
+    week: 'Last Week',
+    month: 'Last Month',
+    custom: 'Custom',
+    customDate: 'Custom date range',
+    successReq: 'Successful Requests',
+    successReqTip: 'The sum of the files that have been successfully processed to completion.',
+    errorReq: 'Error Requests',
+    errorReqTip: 'The sum of the files that failed to be processed.',
+    errorRatio: 'Error Ratio',
+    errorRatioTip: 'The percentage of the files that failed to process.',
+    average: 'Average Process Time',
+    averageTip: ['Average Process Time is the average processing time of the original file from the beginning to the end.', 'Attention Please: ', 'Average Process Time may be affected by the size of the file you are uploading and the network environment.'],
+    updated: 'Stats are updated every hour',
+    project: {
+      'All Projects': 'All Projects',
+      'Default Project': 'Default Project'
+    },
+    allTools: 'All Tools',
+    tools: {
+      'Office to PDF': 'Office to PDF',
+      'PNG to PDF': 'PNG to PDF',
+      'TXT to PDF': 'TXT to PDF',
+      'PDF to Image': 'PDF to Image',
+      'PDF to Office': 'PDF to Office',
+      'PDF to TXT': 'PDF to TXT',
+      'PDF to HTML': 'PDF to HTML',
+      'PDF to CSV': 'PDF to CSV',
+      'PDF to RTF': 'PDF to RTF',
+      'HTML to PDF': 'HTML to PDF',
+      'RTF to PDF': 'RTF to PDF',
+      'CSV to PDF': 'CSV to PDF',
+      'PDF to Split': 'PDF to Split',
+      'PDF to Merge': 'PDF to Merge',
+      'PDF Delete': 'PDF Delete',
+      'PDF Extract': 'PDF Extract',
+      'PDF Rotation': 'PDF Rotation',
+      'PDF Compress': 'PDF Compress',
+      'PDF Insert': 'PDF Insert',
+      'PDF AddWatermark': 'PDF AddWatermark',
+      'PDF DelWatermark': 'PDF DelWatermark',
+      'PDF CoverCompare': 'PDF CoverCompare',
+      'PDF ContentCompare': 'PDF ContentCompare'
+    }
+  },
+  calendar: {
+    week: {
+      Sun: 'Sun',
+      Mon: 'Mon',
+      Tues: 'Tues',
+      Wed: 'Wed',
+      Thur: 'Thur',
+      Fri: 'Fri',
+      Sat: 'Sat'
+    },
+    month: {
+      Jan: 'Jan',
+      Feb: 'Feb',
+      Mar: 'Mar',
+      Apr: 'Apr',
+      May: 'May',
+      Jun: 'Jun',
+      Jul: 'Jul',
+      Aug: 'Aug',
+      Sept: 'Sept',
+      Oct: 'Oct',
+      Nov: 'Nov',
+      Dec: 'Dec'
+    }
+  },
+  apiKey: {
+    title: 'API Keys',
+    projectsKeys: 'Projects and Keys',
+    projectName: 'Project Name',
+    publicKey: 'Public Key',
+    secretKey: 'Secret Key',
+    creationDate: 'Creation Date',
+    newProject: 'Create New Project',
+    createSuccess: 'Create Successful!',
+    createFail: 'Create Failed, please try again later.',
+    enterName: 'Please enter project name',
+    cancel: 'Cancel',
+    save: 'Save',
+    copied: 'Copied',
+    actions: 'Actions',
+    edit: 'Edit',
+    editProject: 'Edit Project',
+    editSuccess: 'Edit Successful!',
+    editFail: 'Edit Failed, please try again later.',
+    delete: 'Delete',
+    deleteProject: 'Are you sure you want to delete this project?',
+    deleteSuccess: 'Delete Successful!',
+    deleteFail: 'Delete Failed, please try again later.',
+    noData: 'No Data Available',
+    exists: 'A project with the same name already exists'
+  },
+  webhooks: {
+    title: 'Webhooks',
+    tips: 'Webhooks are reverse APIs that automatically send task or file information to the client you specify when our server updates it in the Webhooks paradigm. You can set up Webhooks to avoid periodic calls to APIs.',
+    url: 'URL',
+    events: 'Events',
+    status: 'Status',
+    secretToken: 'Secret Token',
+    tokenTip: 'There is a unique Secret Token to let you know the webhook is from us.',
+    lastResponse: 'Last Response',
+    responseTip: 'The latest UTC time we sent you a webhook.',
+    actions: 'Actions',
+    noData: 'No Data Available',
+    addWebhook: 'Add New Webhook',
+    editWebhook: 'Edit Webhook',
+    copied: 'Copied',
+    webhookURL: 'Webhook URL',
+    urlTip: 'Provide Webhook URL to receive requests',
+    urlErr: 'Webhook URL cannot be blank',
+    urlValid: 'Please enter a valid URL.',
+    eventTip: "When the events which you selected occur, you'll will receive the notification on your App.",
+    eventErr: 'Please select at least one event',
+    'task.start': 'task.start',
+    'task.finish': 'task.finish',
+    'task.overdue': 'task.overdue',
+    'file.start': 'file.start',
+    'file.success': 'file.success',
+    'file.failed': 'file.failed',
+    project: 'Project',
+    allProjects: 'All Projects',
+    'Default Project': 'Default Project',
+    cancel: 'Cancel',
+    save: 'Save',
+    editInvalid: 'Edit Invalid!',
+    editSuccess: 'Edited Successfully!',
+    editFail: 'Edit Failed.',
+    createInvalid: 'Create Invalid!',
+    createSuccess: 'Create Successfully!',
+    createFail: 'Create Failed.',
+    delete: 'Delete',
+    sureDelete: 'Are you sure you want to delete this information?',
+    deleted: 'Deleted!',
+    deleteFail: 'Deleted Failed.'
+  },
+  security: {
+    title: 'Security',
+    changeName: 'Change Username',
+    name: 'Username',
+    nameBlank: 'Username cannot be blank',
+    nameErr: 'Please enter your new username',
+    nameMax: 'Ensure your username is under 255 characters in length.',
+    password: 'Change Password',
+    passwordBlank: 'Password cannot be blank',
+    oldPassword: 'Old Password',
+    oldPasswordBlank: 'Old password cannot be blank',
+    oldPasswordIncorrect: 'Incorrect old password',
+    oldErr: 'Please enter your old password',
+    newPassword: 'New Password',
+    newErr: 'Please enter your new password',
+    confirm: 'Confirm Password',
+    confirmErr: 'Please confirm password',
+    passwordRule: 'The passwords must be from 6 to 24 characters long',
+    save: 'Save the Change',
+    changedFail: 'Changed Failed!',
+    changedSuccess: 'Changed Success!',
+    passwordSame: 'New password cannot be the same as your old password.',
+    doNotMatch: "Password confirmation doesn't match Password",
+    towPasswordMatch: 'The Password and Confirm Password do not match.',
+    cannotMatch: 'Cannot Match Old Password.'
+  },
+  plan: {
+    title: 'Plan',
+    plan: 'Pricing Plan:',
+    free: 'Free',
+    monthly: 'Monthly',
+    annually: 'Annually',
+    package: 'Package',
+    month: 'Month',
+    year: 'Year',
+    upTo: 'Up to:',
+    documents: 'Documents',
+    validity: 'Validity Period:',
+    filesBalance: 'Processing Files Balance',
+    date: 'Date',
+    description: 'Description',
+    balanceChange: 'Balance Change',
+    remainingFiles: 'Remaining Files',
+    noData: 'No Data Available',
+    'Free Files': 'Free Files',
+    'Monthly Subscription Files': 'Monthly Subscription Files',
+    'Annually Subscription Files': 'Annually Subscription Files',
+    'Package Files': 'Package Files',
+    'Files Expired': 'Files Expired',
+    previous: 'Previous',
+    next: 'Next'
+  },
+  information: {
+    title: 'Billing Information',
+    Billing: 'Billing Address',
+    firstName: 'First Name',
+    firstNameTip: 'Please enter your first name',
+    firstNameErr: 'Ensure your first name is under 255 characters in length.',
+    lastName: 'Last Name',
+    lastNameTip: 'Please enter your last name',
+    lastNameErr: 'Ensure your last name is under 255 characters in length.',
+    company: 'Company Name',
+    companyTip: 'Please enter your company name',
+    companyErr: 'Ensure your company name is under 255 characters in length.',
+    address: 'Company Address',
+    addressTip: 'Please enter your company address',
+    addressErr: 'Ensure your company address is under 255 characters in length.',
+    country: 'Country',
+    countryTip: 'Scroll down to select country',
+    province: 'State/Province',
+    provinceTip: 'Please enter your state/province',
+    provinceErr: 'Ensure your state/province is under 255 characters in length.',
+    zip: 'Postal Code',
+    zipTip: 'Please enter your postal code',
+    update: 'Update',
+    mail: 'Billing Mail Address',
+    email: '* E-Mail Address',
+    emailTip: 'Please enter your email address',
+    emailFormatErr: 'Email format is incorrect. Please enter a valid email address.',
+    emailContact: ['This billing email address is only used for receiving invoices. If you need a paper invoice, please ', 'contact us'],
+    save: 'Save',
+    changedFail: 'Changed Failed!',
+    changedSuccess: 'Changed Success!'
+  },
+  invoices: {
+    title: 'Invoices Center',
+    information: 'Invoices Information',
+    date: 'Date',
+    order: 'Order ID',
+    amount: 'Total Amount',
+    number: 'Invoice Number',
+    operation: 'Operation',
+    download: 'Download',
+    send: 'Send Email',
+    apply: 'Apply for an Invoice',
+    incomplete: 'Incomplete Billing Information',
+    complete: 'Please complete your billing information before applying for an invoice.',
+    back: 'Back',
+    fillInformation: 'Fill out Billing Information',
+    emailFill: 'Email Address Not Filled',
+    completeEmail: 'Please fill out the email address before sending to email.',
+    fillEmail: 'Fill out Email Address ',
+    confirmation: 'Email Address Confirmation',
+    format: 'Invoices will be sent in PDF format to:',
+    changeEmail: 'Change Email',
+    confirmEmail: 'Confirm Send',
+    emailSuccess: 'Email sent successfully.',
+    emailFail: 'Failed to send email.',
+    success: 'Invoice issued successfully.',
+    fail: 'Failed to issue invoice.',
+    noData: 'No Data Available'
+  }
+}

+ 277 - 0
src/locales/zh.ts

@@ -0,0 +1,277 @@
+// zh.ts
+export default {
+  errorSubmit: '错误提交',
+  failedConnect: '网络连接失败',
+  copiedFail: '复制失败',
+  aside: {
+    project: '项目',
+    dashboard: '看板',
+    apiKeys: 'API 密钥',
+    webhook: '异步回调接收地址(Webhook)',
+    setting: '账户设置',
+    security: '安全',
+    billing: '账单',
+    plan: '套餐',
+    information: '账单信息',
+    invoices: '发票中心',
+    support: '支持',
+    document: '开发文档 '
+  },
+  dashboard: {
+    title: '看板',
+    file: '本月已处理文档数:',
+    details: '更多详情',
+    refresh: '刷新',
+    view: '查看方式',
+    day: '过去24小时',
+    week: '过去一周',
+    month: '过去一个月',
+    custom: '自定义',
+    customDate: '自定义时间范围',
+    successReq: '成功请求数',
+    successReqTip: '已成功处理完成的文档总数。',
+    errorReq: '错误请求数',
+    errorReqTip: '发生错误请求,文档处理失败的总数。',
+    errorRatio: '错误率',
+    errorRatioTip: '发生错误请求,文档处理失败的百分比。',
+    average: '平均处理时间',
+    averageTip: ['平均处理时间是指原始文档从开始到结束的平均处理时间。', '请注意:', '平均处理时间可能会受到您上传的文档大小和网络环境的影响。'],
+    updated: '统计数据每小时更新一次',
+    project: {
+      'All Projects': '所有项目',
+      'Default Project': '默认项目'
+    },
+    allTools: '所有工具',
+    tools: {
+      'Office to PDF': 'Office to PDF',
+      'PNG to PDF': 'PNG 转 PDF',
+      'TXT to PDF': 'TXT 转 PDF',
+      'PDF to Image': 'PDF 转 Image',
+      'PDF to Office': 'PDF 转 Office',
+      'PDF to TXT': 'PDF 转 TXT',
+      'PDF to HTML': 'PDF 转 HTML',
+      'PDF to CSV': 'PDF 转 CSV',
+      'PDF to RTF': 'PDF 转 RTF',
+      'HTML to PDF': 'HTML 转 PDF',
+      'RTF to PDF': 'RTF 转 PDF',
+      'CSV to PDF': 'CSV 转 PDF',
+      'PDF to Split': '文档拆分',
+      'PDF to Merge': '文档合并',
+      'PDF Delete': '删除页面',
+      'PDF Extract': '提取页面',
+      'PDF Rotation': '旋转页面',
+      'PDF Compress': '压缩页面',
+      'PDF Insert': '插入页面',
+      'PDF AddWatermark': '添加水印',
+      'PDF DelWatermark': '删除水印',
+      'PDF CoverCompare': '覆盖对比',
+      'PDF ContentCompare': '内容对比'
+    }
+  },
+  calendar: {
+    week: {
+      Sun: '周日',
+      Mon: '周一',
+      Tues: '周二',
+      Wed: '周三',
+      Thur: '周四',
+      Fri: '周五',
+      Sat: '周六'
+    },
+    month: {
+      Jan: '1月',
+      Feb: '2月',
+      Mar: '3月',
+      Apr: '4月',
+      May: '5月',
+      Jun: '6月',
+      Jul: '7月',
+      Aug: '8月',
+      Sept: '9月',
+      Oct: '10月',
+      Nov: '11月',
+      Dec: '12月'
+    }
+  },
+  apiKey: {
+    title: 'API 密钥',
+    projectsKeys: '项目和密钥',
+    projectName: '项目名称',
+    publicKey: '公钥',
+    secretKey: '密钥',
+    creationDate: '创建日期',
+    newProject: '创建新项目',
+    createSuccess: '创建成功!',
+    createFail: '创建失败,请稍后再试。',
+    enterName: '请输入项目名称',
+    cancel: '取消',
+    save: '保存',
+    copied: '已复制',
+    actions: '操作',
+    edit: '编辑',
+    editProject: '编辑项目',
+    editSuccess: '编辑成功!',
+    editFail: '编辑失败,请稍后再试。',
+    delete: '删除',
+    deleteProject: '您确定要删除此项目吗?',
+    deleteSuccess: '删除成功!',
+    deleteFail: '删除失败,请稍后再试。',
+    noData: '无可用数据',
+    exists: '已经存在同名项目。'
+  },
+  webhooks: {
+    title: '异步回调接收地址(Webhook)',
+    tips: '异步回调接收地址(Webhook) 是一种反向 API。在 Webhooks 范式中,当我们的服务器更新任务或文档信息时,会自动将该信息发送到您指定的客户端。您可以设置 Webhooks 来避免定期调用 API。',
+    url: 'URL链接',
+    events: '事件',
+    status: '状态',
+    secretToken: '密钥令牌',
+    tokenTip: '有一个独特的密钥令牌,用于让您知道该 Webhook 来自我们。',
+    lastResponse: '最后一次响应',
+    responseTip: '我们发送给您的最新UTC时间的Webhook。',
+    actions: '操作',
+    noData: '无可用数据',
+    addWebhook: '添加新的Webhook',
+    editWebhook: '编辑 Webhook',
+    copied: '已复制',
+    webhookURL: 'Webhook URL 链接',
+    urlTip: '提供用于接收请求的 Webhook URL',
+    urlErr: 'Webhook URL 不能为空',
+    urlValid: '请输入有效的URL。',
+    eventTip: '当您选择的事件发生时,您将在您的应用程序上收到通知。',
+    eventErr: '请选择至少一个事件',
+    'task.start': '任务开始',
+    'task.finish': '任务完成',
+    'task.overdue': '任务逾期',
+    'file.start': '文档开始处理',
+    'file.success': '文档处理成功',
+    'file.failed': '文档处理失败',
+    project: '项目',
+    allProjects: '所有项目',
+    'Default Project': '默认项目',
+    cancel: '取消',
+    save: '保存',
+    editInvalid: '编辑无效',
+    editSuccess: '编辑成功',
+    editFail: '编辑失败',
+    createInvalid: '创建无效',
+    createSuccess: '创建成功',
+    createFail: '创建失败',
+    delete: '删除',
+    sureDelete: '您确定要删除此信息吗?',
+    deleted: '已删除',
+    deleteFail: '删除失败'
+  },
+  security: {
+    title: '安全',
+    changeName: '更改用户名',
+    name: '用户名',
+    nameBlank: '用户名不能为空',
+    nameErr: '请输入您的新用户名',
+    nameMax: '请确保您的用户名长度不超过255个字符。',
+    password: '更改密码',
+    passwordBlank: '密码不能为空',
+    oldPassword: '旧密码',
+    oldPasswordBlank: '旧密码不能为空',
+    oldPasswordIncorrect: '旧密码不正确',
+    oldErr: '请输入您的旧密码',
+    newPassword: '新密码',
+    newErr: '请输入您的新密码',
+    confirm: '确认密码',
+    confirmErr: '请确认密码',
+    passwordRule: '密码长度必须在 6 至 24 个字符之间',
+    save: '保存更改',
+    changedFail: '更改失败!',
+    changedSuccess: '更改成功!',
+    passwordSame: '新密码不能与旧密码相同。',
+    doNotMatch: '确认密码与密码不匹配',
+    towPasswordMatch: '密码和确认密码不匹配。',
+    cannotMatch: '无法匹配旧密码。'
+  },
+  plan: {
+    title: '套餐',
+    plan: '定价计划:',
+    free: '免费',
+    monthly: '月订购',
+    annually: '年订购',
+    package: '套餐包',
+    month: '每月',
+    year: '每年',
+    upTo: '文档数:',
+    documents: '个文档',
+    validity: '生效期限:',
+    filesBalance: '文档数量余额',
+    date: '日期',
+    description: '描述',
+    balanceChange: '余额变动',
+    remainingFiles: '剩余文档数量',
+    noData: '无可用数据',
+    'Free Files': '免费文档数',
+    'Monthly Subscription Files': '按月订购文档',
+    'Annually Subscription Files': '按年订购文档',
+    'Package Files': '套餐包文档数',
+    'Files Expired': '文档数已过期',
+    previous: '上一页',
+    next: '下一页'
+  },
+  information: {
+    title: '账单信息',
+    Billing: '账单地址',
+    firstName: '姓',
+    firstNameTip: '请输入您的姓',
+    firstNameErr: '请确保输入内容长度不超过255个字符。',
+    lastName: '名',
+    lastNameTip: '请输入您的名字',
+    lastNameErr: '请确保输入内容长度不超过255个字符。',
+    company: '公司名',
+    companyTip: '请输入您的公司名',
+    companyErr: '请确保输入内容长度不超过255个字符。',
+    address: '公司地址',
+    addressTip: '请输入您的公司地址',
+    addressErr: '请确保输入内容长度不超过255个字符。',
+    country: '国家',
+    countryTip: '下拉选择国家',
+    province: '州/省',
+    provinceTip: '请输入您所在的州/省',
+    provinceErr: '请确保输入内容长度不超过255个字符。',
+    zip: '邮政编码',
+    zipTip: '请输入邮政编码',
+    update: '更新',
+    mail: '账单发送邮箱地址',
+    email: '邮箱地址',
+    emailTip: '请输入您的邮箱地址',
+    emailFormatErr: '邮箱格式错误,请输入有效的邮箱地址。',
+    emailContact: ['此邮箱地址仅供接收发票所使用。如果您需要纸质发票,请 ', '联系我们'],
+    save: '保存',
+    changedFail: '更改失败!',
+    changedSuccess: '更改成功!'
+  },
+  invoices: {
+    title: '发票中心',
+    information: '发票信息',
+    date: '日期',
+    order: '订单号',
+    amount: '总金额',
+    number: '发票号',
+    operation: '操作',
+    download: '下载 PDF',
+    send: '发送到邮箱',
+    apply: '申请开票',
+    incomplete: '您的账单信息不完整',
+    complete: '请完善账单信息后申请开票',
+    back: '返回',
+    fillInformation: '填写账单信息',
+    emailFill: '未填写邮箱地址',
+    completeEmail: '填写邮箱地址后方可发送成功',
+    fillEmail: '填写邮箱地址',
+    confirmation: '请确认收件邮箱地址',
+    format: '发票信息将以 PDF 格式发送至:',
+    changeEmail: '修改邮箱',
+    confirmEmail: '确认发送',
+    emailSuccess: '邮件发送成功',
+    emailFail: '邮件发送失败',
+    success: '开票成功',
+    fail: '开票失败',
+    noData: '无可用数据'
+  }
+}

+ 2 - 0
src/main.ts

@@ -7,6 +7,7 @@ import 'element-ui/lib/theme-chalk/index.css'
 import '@/assets/scss/element-variables.scss'
 import '@/assets/scss/common.scss'
 import * as echarts from 'echarts'
+import i18n from '@/i18n'
 import { message } from '@/utils/resetMessage'
 
 Vue.config.productionTip = false
@@ -15,6 +16,7 @@ Vue.prototype.$message = message
 Vue.prototype.$echarts = echarts
 
 new Vue({
+  i18n,
   pinia,
   router,
   render: h => h(App)

+ 15 - 0
src/request/api.ts

@@ -1,8 +1,10 @@
 import { get, post, put, del, getWithConfig } from './http'
 
 let baseUrl = 'https://wms.compdf.com'
+let backgroundUrl = 'https://api-backend.compdf.com'
 if (process.env.VUE_APP_MODE_ENV !== 'production') {
   baseUrl = 'http://test-compdf.kdan.cn:3060'
+  backgroundUrl = 'http://101.132.103.13:8088'
 }
 // 登录
 export const apiLogin = (data: object, config: object) => post(baseUrl + '/api/user/login', data, config)
@@ -46,6 +48,7 @@ export const getExport = (params: object, config: object) => getWithConfig('/use
 
 // security - 修改密码
 export const apiChangePassword = (data: object, config: object) => post(baseUrl + '/api/user/change-password', data, config)
+export const apiChangeUserName = (data: object, config: object) => post(backgroundUrl + '/user-api/v1/user/updateName', data, config)
 
 // API Keys - 获取列表
 export const getProjectList = (params: object) => get('/user-api/v1/project/getProjectList', params)
@@ -79,3 +82,15 @@ export const getBalanceRecordList = (params: object) => get('/user-api/v1/balanc
 export const exportBalanceRecordList = (params: object, config: object) => getWithConfig('/user-api/v1/balance/exportBalanceRecordList', params, config)
 // plan - 套餐详情
 export const getPricingPlanList = () => get('/user-api/v1/balance/getBill', {})
+// 获取账单信息
+export const getBillingInformation = () => get('/user-api/v1/user/billing/info', {})
+// 获取账单信息
+export const getInvoices = () => get('/user-api/v1/user/billing/getOrderBillVos', {})
+// 更新账单信息
+export const apiChangeInformation = (data: object, config: object) => post(backgroundUrl + '/user-api/v1/user/billing/update', data, config)
+// 更新账单邮箱
+export const apiChangeEmail = (data: object, config: object) => post(backgroundUrl + '/user-api/v1/user/billing/update/email', data, config)
+// 申请开发票
+export const apiApplyInvoice = (data: object, config: object) => post(backgroundUrl + '/user-api/v1/user/billing/applyInvoice', data, config)
+// 发送邮件
+export const apiSendEmail = (data: object, config: object) => post(backgroundUrl + '/user-api/v1/user/billing/billSendEmail', data, config)

+ 1 - 1
src/request/http.ts

@@ -55,7 +55,7 @@ axios.interceptors.response.use(
         // 跳转登录页面,并将要浏览的页面fullPath传过去,登录成功后跳转需要访问的页面
         setTimeout(() => {
           const url = window.location.href
-          window.location.href = process.env.VUE_APP_PDFSDK_DOMAIN + '/login?from=api&redirect=' + url
+          window.location.href = process.env.VUE_APP_PDFSdk_DOMAIN + '/login?from=api&redirect=' + url
         }, 1000)
       }
       return Promise.resolve(response)

+ 28 - 8
src/router/index.ts

@@ -12,6 +12,8 @@ import webhooks from '@/views/projects/user/webhooks.vue'
 import addWebhook from '@/views/projects/user/add.vue'
 import editWebhook from '@/views/projects/user/edit.vue'
 import plan from '@/views/billing/plan.vue'
+import information from '@/views/billing/information.vue'
+import invoices from '@/views/billing/invoices.vue'
 
 Vue.use(VueRouter)
 
@@ -54,7 +56,7 @@ const routes: Array<RouteConfig> = [
         name: 'addApiKeys',
         component: addApiKeys,
         meta: {
-          showfather: false,
+          showFather: false,
           title: 'API Keys | ComPDFKit API for Developers'
         }
       },
@@ -63,13 +65,13 @@ const routes: Array<RouteConfig> = [
         name: 'editApiKeys',
         component: editApiKeys,
         meta: {
-          showfather: false,
+          showFather: false,
           title: 'API Keys | ComPDFKit API for Developers'
         }
       }
     ],
     meta: {
-      showfather: true,
+      showFather: true,
       title: 'API Keys | ComPDFKit API for Developers'
     }
   },
@@ -83,7 +85,7 @@ const routes: Array<RouteConfig> = [
         name: 'addWebhook',
         component: addWebhook,
         meta: {
-          showfather: false,
+          showFather: false,
           title: 'Webhooks | ComPDFKit API for Developers'
         }
       },
@@ -92,13 +94,13 @@ const routes: Array<RouteConfig> = [
         name: 'editWebhook',
         component: editWebhook,
         meta: {
-          showfather: false,
+          showFather: false,
           title: 'Webhooks | ComPDFKit API for Developers'
         }
       }
     ],
     meta: {
-      showfather: true,
+      showFather: true,
       title: 'Webhooks | ComPDFKit API for Developers'
     }
   },
@@ -109,6 +111,24 @@ const routes: Array<RouteConfig> = [
     meta: {
       title: 'Plan | ComPDFKit API for Developers'
     }
+  },
+  {
+    path: '/billing/information',
+    name: 'information',
+    component: information,
+    meta: {
+      showFather: false,
+      title: 'Billing Information | ComPDFKit API for Developers'
+    }
+  },
+  {
+    path: '/billing/invoices',
+    name: 'invoices',
+    component: invoices,
+    meta: {
+      showFather: false,
+      title: 'Invoices Center | ComPDFKit API for Developers'
+    }
   }
 ]
 
@@ -119,13 +139,13 @@ const router = new VueRouter({
 })
 
 // 是否登录,未登录跳转登录页
-router.beforeEach(async (to, from, next) => {
+router.beforeEach((to, from, next) => {
   const user = loginStore().user
   if (user) {
     next()
   } else {
     const url = window.location.href
-    window.location.href = process.env.VUE_APP_PDFSDK_DOMAIN + '/login?from=api&redirect=' + url
+    window.location.href = process.env.VUE_APP_PDFSdk_DOMAIN + '/login?from=api&redirect=' + url
   }
 })
 

+ 250 - 0
src/utils/country.js

@@ -0,0 +1,250 @@
+export function country () {
+  var country =
+    [
+      { name: 'Afghanistan', code: 'AF' },
+      { name: 'Åland Islands', code: 'AX' },
+      { name: 'Albania', code: 'AL' },
+      { name: 'Algeria', code: 'DZ' },
+      { name: 'American Samoa', code: 'AS' },
+      { name: 'Andorra', code: 'AD' },
+      { name: 'Angola', code: 'AO' },
+      { name: 'Anguilla', code: 'AI' },
+      { name: 'Antarctica', code: 'AQ' },
+      { name: 'Antigua and Barbuda', code: 'AG' },
+      { name: 'Argentina', code: 'AR' },
+      { name: 'Armenia', code: 'AM' },
+      { name: 'Aruba', code: 'AW' },
+      { name: 'Australia', code: 'AU' },
+      { name: 'Austria', code: 'AT' },
+      { name: 'Azerbaijan', code: 'AZ' },
+      { name: 'Bahamas', code: 'BS' },
+      { name: 'Bahrain', code: 'BH' },
+      { name: 'Bangladesh', code: 'BD' },
+      { name: 'Barbados', code: 'BB' },
+      { name: 'Belarus', code: 'BY' },
+      { name: 'Belgium', code: 'BE' },
+      { name: 'Belize', code: 'BZ' },
+      { name: 'Benin', code: 'BJ' },
+      { name: 'Bermuda', code: 'BM ' },
+      { name: 'Bhutan', code: 'BT' },
+      { name: 'Bolivia', code: 'BO' },
+      { name: 'Bosnia and Herzegovina', code: 'BA' },
+      { name: 'Botswana', code: 'BW' },
+      { name: 'Bouvet Island', code: 'BV' },
+      { name: 'Brazil', code: 'BR' },
+      { name: 'British Indian Ocean Territory', code: 'IO' },
+      { name: 'Brunei Darussalam', code: 'BN' },
+      { name: 'Bulgaria', code: 'BG' },
+      { name: 'Burkina Faso', code: 'BF' },
+      { name: 'Burundi', code: 'BI' },
+      { name: 'Cambodia', code: 'KH' },
+      { name: 'Cameroon', code: 'CM' },
+      { name: 'Canada', code: 'CA' },
+      { name: 'Cape Verde', code: 'CV' },
+      { name: 'Cayman Islands', code: 'KY' },
+      { name: 'Central African Republic', code: 'CF' },
+      { name: 'Chad', code: 'TD' },
+      { name: 'Chile', code: 'CL' },
+      { name: 'China', code: 'CN' },
+      { name: 'Christmas Island', code: 'CX' },
+      { name: 'Cocos (Keeling) Islands', code: 'CC' },
+      { name: 'Colombia', code: 'CO' },
+      { name: 'Comoros', code: 'KM' },
+      { name: 'Congo', code: 'CG' },
+      { name: 'Congo, The Democratic Republic of the', code: 'CD' },
+      { name: 'Cook Islands', code: 'CK' },
+      { name: 'Costa Rica', code: 'CR' },
+      { name: "Cote D'Ivoire", code: 'CI' },
+      { name: 'Croatia', code: 'HR' },
+      { name: 'Cuba', code: 'CU' },
+      { name: 'Cyprus', code: 'CY' },
+      { name: 'Czech Republic', code: 'CZ' },
+      { name: 'Denmark', code: 'DK' },
+      { name: 'Djibouti', code: 'DJ' },
+      { name: 'Dominica', code: 'DM' },
+      { name: 'Dominican Republic', code: 'DO' },
+      { name: 'Ecuador', code: 'EC' },
+      { name: 'Egypt', code: 'EG' },
+      { name: 'El Salvador', code: 'SV' },
+      { name: 'Equatorial Guinea', code: 'GQ' },
+      { name: 'Eritrea', code: 'ER' },
+      { name: 'Estonia', code: 'EE' },
+      { name: 'Ethiopia', code: 'ET' },
+      { name: 'Falkland Islands (Malvinas)', code: 'FK' },
+      { name: 'Faroe Islands', code: 'FO' },
+      { name: 'Fiji', code: 'FJ' },
+      { name: 'Finland', code: 'FI' },
+      { name: 'France', code: 'FR' },
+      { name: 'French Guiana', code: 'GF' },
+      { name: 'French Polynesia', code: 'PF' },
+      { name: 'French Southern Territories', code: 'TF' },
+      { name: 'Gabon', code: 'GA' },
+      { name: 'Gambia', code: 'GM' },
+      { name: 'Georgia', code: 'GE' },
+      { name: 'Germany', code: 'DE' },
+      { name: 'Ghana', code: 'GH' },
+      { name: 'Gibraltar', code: 'GI' },
+      { name: 'Greece', code: 'GR' },
+      { name: 'Greenland', code: 'GL' },
+      { name: 'Grenada', code: 'GD' },
+      { name: 'Guadeloupe', code: 'GP' },
+      { name: 'Guam', code: 'GU' },
+      { name: 'Guatemala', code: 'GT' },
+      { name: 'Guernsey', code: 'GG' },
+      { name: 'Guinea', code: 'GN' },
+      { name: 'Guinea-Bissau', code: 'GW' },
+      { name: 'Guyana', code: 'GY' },
+      { name: 'Haiti', code: 'HT' },
+      { name: 'Heard Island and Mcdonald Islands', code: 'HM' },
+      { name: 'Holy See (Vatican City State)', code: 'VA' },
+      { name: 'Honduras', code: 'HN' },
+      { name: 'Hong Kong, Province of China', code: 'HK' },
+      { name: 'Hungary', code: 'HU' },
+      { name: 'Iceland', code: 'IS' },
+      { name: 'India', code: 'IN' },
+      { name: 'Indonesia', code: 'ID' },
+      { name: 'Iran, Islamic Republic Of', code: 'IR' },
+      { name: 'Iraq', code: 'IQ' },
+      { name: 'Ireland', code: 'IE' },
+      { name: 'Isle of Man', code: 'IM' },
+      { name: 'Israel', code: 'IL' },
+      { name: 'Italy', code: 'IT' },
+      { name: 'Jamaica', code: 'JM' },
+      { name: 'Japan', code: 'JP' },
+      { name: 'Jersey', code: 'JE' },
+      { name: 'Jordan', code: 'JO' },
+      { name: 'Kazakhstan', code: 'KZ' },
+      { name: 'Kenya', code: 'KE' },
+      { name: 'Kiribati', code: 'KI' },
+      { name: "Korea, Democratic People'S Republic of", code: 'KP' },
+      { name: 'Korea, Republic of', code: 'KR' },
+      { name: 'Kuwait', code: 'KW' },
+      { name: 'Kyrgyzstan', code: 'KG' },
+      { name: "Lao People'S Democratic Republic", code: 'LA' },
+      { name: 'Latvia', code: 'LV' },
+      { name: 'Lebanon', code: 'LB' },
+      { name: 'Lesotho', code: 'LS' },
+      { name: 'Liberia', code: 'LR' },
+      { name: 'Libyan Arab Jamahiriya', code: 'LY' },
+      { name: 'Liechtenstein', code: 'LI' },
+      { name: 'Lithuania', code: 'LT' },
+      { name: 'Luxembourg', code: 'LU' },
+      { name: 'Macao, Province of China', code: 'MO' },
+      { name: 'Macedonia, The Former Yugoslav Republic of', code: 'MK' },
+      { name: 'Madagascar', code: 'MG' },
+      { name: 'Malawi', code: 'MW' },
+      { name: 'Malaysia', code: 'MY' },
+      { name: 'Maldives', code: 'MV' },
+      { name: 'Mali', code: 'ML' },
+      { name: 'Malta', code: 'MT' },
+      { name: 'Marshall Islands', code: 'MH' },
+      { name: 'Martinique', code: 'MQ' },
+      { name: 'Mauritania', code: 'MR' },
+      { name: 'Mauritius', code: 'MU' },
+      { name: 'Mayotte', code: 'YT' },
+      { name: 'Mexico', code: 'MX' },
+      { name: 'Micronesia, Federated States of', code: 'FM' },
+      { name: 'Moldova, Republic of', code: 'MD' },
+      { name: 'Monaco', code: 'MC' },
+      { name: 'Mongolia', code: 'MN' },
+      { name: 'Montserrat', code: 'MS' },
+      { name: 'Morocco', code: 'MA' },
+      { name: 'Mozambique', code: 'MZ' },
+      { name: 'Myanmar', code: 'MM' },
+      { name: 'Namibia', code: 'NA' },
+      { name: 'Nauru', code: 'NR' },
+      { name: 'Nepal', code: 'NP' },
+      { name: 'Netherlands', code: 'NL' },
+      { name: 'Netherlands Antilles', code: 'AN' },
+      { name: 'New Caledonia', code: 'NC' },
+      { name: 'New Zealand', code: 'NZ' },
+      { name: 'Nicaragua', code: 'NI' },
+      { name: 'Niger', code: 'NE' },
+      { name: 'Nigeria', code: 'NG' },
+      { name: 'Niue', code: 'NU' },
+      { name: 'Norfolk Island', code: 'NF' },
+      { name: 'Northern Mariana Islands', code: 'MP' },
+      { name: 'Norway', code: 'NO' },
+      { name: 'Oman', code: 'OM' },
+      { name: 'Pakistan', code: 'PK' },
+      { name: 'Palau', code: 'PW' },
+      { name: 'Palestinian Territory, Occupied', code: 'PS' },
+      { name: 'Panama', code: 'PA' },
+      { name: 'Papua New Guinea', code: 'PG' },
+      { name: 'Paraguay', code: 'PY' },
+      { name: 'Peru', code: 'PE' },
+      { name: 'Philippines', code: 'PH' },
+      { name: 'Pitcairn', code: 'PN' },
+      { name: 'Poland', code: 'PL' },
+      { name: 'Portugal', code: 'PT' },
+      { name: 'Puerto Rico', code: 'PR' },
+      { name: 'Qatar', code: 'QA' },
+      { name: 'Reunion', code: 'RE' },
+      { name: 'Romania', code: 'RO' },
+      { name: 'Russian Federation', code: 'RU' },
+      { name: 'RWANDA', code: 'RW' },
+      { name: 'Saint Helena', code: 'SH' },
+      { name: 'Saint Kitts and Nevis', code: 'KN' },
+      { name: 'Saint Lucia', code: 'LC' },
+      { name: 'Saint Pierre and Miquelon', code: 'PM' },
+      { name: 'Saint Vincent and the Grenadines', code: 'VC' },
+      { name: 'Samoa', code: 'WS' },
+      { name: 'San Marino', code: 'SM' },
+      { name: 'Sao Tome and Principe', code: 'ST' },
+      { name: 'Saudi Arabia', code: 'SA' },
+      { name: 'Senegal', code: 'SN' },
+      { name: 'Serbia', code: 'RS' },
+      { name: 'Montenegro', code: 'ME' },
+      { name: 'Seychelles', code: 'SC' },
+      { name: 'Sierra Leone', code: 'SL' },
+      { name: 'Singapore', code: 'SG' },
+      { name: 'Slovakia', code: 'SK' },
+      { name: 'Slovenia', code: 'SI' },
+      { name: 'Solomon Islands', code: 'SB' },
+      { name: 'Somalia', code: 'SO' },
+      { name: 'South Africa', code: 'ZA' },
+      { name: 'South Georgia and the South Sandwich Islands', code: 'GS' },
+      { name: 'Spain', code: 'ES' },
+      { name: 'Sri Lanka', code: 'LK' },
+      { name: 'Sudan', code: 'SD' },
+      { name: 'Suriname', code: 'SR' },
+      { name: 'Svalbard and Jan Mayen', code: 'SJ' },
+      { name: 'Swaziland', code: 'SZ' },
+      { name: 'Sweden', code: 'SE' },
+      { name: 'Switzerland', code: 'CH' },
+      { name: 'Syrian Arab Republic', code: 'SY' },
+      { name: 'Taiwan, Province of China', code: 'TW' },
+      { name: 'Tajikistan', code: 'TJ' },
+      { name: 'Tanzania, United Republic of', code: 'TZ' },
+      { name: 'Thailand', code: 'TH' },
+      { name: 'Timor-Leste', code: 'TL' },
+      { name: 'Togo', code: 'TG' },
+      { name: 'Tokelau', code: 'TK' },
+      { name: 'Tonga', code: 'TO' },
+      { name: 'Trinidad and Tobago', code: 'TT' },
+      { name: 'Tunisia', code: 'TN' },
+      { name: 'Turkey', code: 'TR' },
+      { name: 'Turkmenistan', code: 'TM' },
+      { name: 'Turks and Caicos Islands', code: 'TC' },
+      { name: 'Tuvalu', code: 'TV' },
+      { name: 'Uganda', code: 'UG' },
+      { name: 'Ukraine', code: 'UA' },
+      { name: 'United Arab Emirates', code: 'AE' },
+      { name: 'United Kingdom', code: 'GB' },
+      { name: 'United States', code: 'US' },
+      { name: 'United States Minor Outlying Islands', code: 'UM' },
+      { name: 'Uruguay', code: 'UY' },
+      { name: 'Uzbekistan', code: 'UZ' },
+      { name: 'Vanuatu', code: 'VU' },
+      { name: 'Venezuela', code: 'VE' },
+      { name: 'Viet Nam', code: 'VN' },
+      { name: 'Virgin Islands, British', code: 'VG' },
+      { name: 'Virgin Islands, U.S.', code: 'VI' },
+      { name: 'Wallis and Futuna', code: 'WF' },
+      { name: 'Western Sahara', code: 'EH' },
+      { name: 'Yemen', code: 'YE' },
+      { name: 'Zambia', code: 'ZM' },
+      { name: 'Zimbabwe', code: 'ZW' }
+    ]
+  return country
+}

+ 112 - 51
src/views/accountSettings/security.vue

@@ -1,49 +1,61 @@
 <template>
   <div class="security container">
-    <h1>Security</h1>
+    <h1>{{ $t('security.title') }}</h1>
     <div class="password-form block">
-      <h2>Change Password</h2>
+      <h2>{{ $t('security.changeName') }}</h2>
+      <el-form :model="userRuleForm" :rules="userRules" ref="userRuleForm" label-position="top" label-width="100px" inline-message>
+        <el-form-item prop="userName" :label="$t('security.name')">
+          <el-input v-model.trim="userRuleForm.userName" auto-complete="off" :placeholder="$t('security.nameErr')"
+          @paste.native.capture.prevent="handlePaste"></el-input>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :disabled="submitBtnState('userRuleForm')" @click="submitUserName('userRuleForm')">{{ $t('security.save') }}</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div class="password-form block">
+      <h2>{{ $t('security.password') }}</h2>
       <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-position="top" label-width="100px" inline-message>
-        <el-form-item label="Old Password" prop="oldPassword">
+        <el-form-item :label="$t('security.oldPassword')" prop="oldPassword">
           <el-input
             :type="passwordType ? '' : 'password'"
             v-model.trim="ruleForm.oldPassword"
             auto-complete="off"
-            placeholder="Please enter your old password"
+            :placeholder="$t('security.oldErr')"
             @paste.native.capture.prevent="handlePaste"
             :class="{'err-border': showErrTipOld}">
           </el-input>
           <span v-show="ruleForm.oldPassword" class="show-password" :class="passwordType ? 'eye-open' : 'eye-close'" @click="passwordType = !passwordType"></span>
-          <span v-show="showErrTipOld" class="err-tip">Incorrect old password</span>
+          <span v-show="showErrTipOld" class="err-tip">{{ $t('security.oldPasswordIncorrect') }}</span>
         </el-form-item>
         <el-form-item prop="newPassword">
           <template slot="label">
-            <span>New Password</span>
-            <span class="rule-tip">The passwords must be from 6 to 24 characters long</span>
+            <span>{{ $t('security.newPassword') }}</span>
+            <span class="rule-tip">{{ $t('security.passwordRule') }}</span>
           </template>
-          <el-input :type="passwordType2 ? '' : 'password'" v-model.trim="ruleForm.newPassword" auto-complete="off" placeholder="Please enter your new password"
+          <el-input :type="passwordType2 ? '' : 'password'" v-model.trim="ruleForm.newPassword" auto-complete="off" :placeholder="$t('security.newPassword')"
           @paste.native.capture.prevent="handlePaste"></el-input>
           <span v-show="ruleForm.newPassword" class="show-password" :class="passwordType2 ? 'eye-open' : 'eye-close'" @click="passwordType2 = !passwordType2"></span>
         </el-form-item>
-        <el-form-item prop="comfirmPassword">
+        <el-form-item prop="confirmPassword">
           <template slot="label">
-            <span>Confirm Password</span>
-            <span class="rule-tip">The passwords must be from 6 to 24 characters long</span>
+            <span>{{ $t('security.confirm') }}</span>
+            <span class="rule-tip">{{ $t('security.passwordRule') }}</span>
           </template>
           <el-input
             :type="passwordType3 ? '' : 'password'"
-            v-model.trim="ruleForm.comfirmPassword"
+            v-model.trim="ruleForm.confirmPassword"
             auto-complete="off"
-            placeholder="Please confirm password"
+            :placeholder="$t('security.confirmErr')"
             @keyup.enter.native="submitPassword('ruleForm')"
             @paste.native.capture.prevent="handlePaste"
             :class="{'err-border': showErrTipNotMatch}">
           </el-input>
-          <span v-show="ruleForm.comfirmPassword" class="show-password" :class="passwordType3 ? 'eye-open' : 'eye-close'" @click="passwordType3 = !passwordType3"></span>
-          <span v-show="showErrTipNotMatch" class="err-tip">Password confirmation doesn't match Password</span>
+          <span v-show="ruleForm.confirmPassword" class="show-password" :class="passwordType3 ? 'eye-open' : 'eye-close'" @click="passwordType3 = !passwordType3"></span>
+          <span v-show="showErrTipNotMatch" class="err-tip">{{ $t('security.doNotMatch') }}</span>
         </el-form-item>
         <el-form-item>
-          <el-button type="primary" :disabled="submitBtnState('ruleForm')" @click="submitPassword('ruleForm')">Save the Changes</el-button>
+          <el-button type="primary" :disabled="submitBtnState('ruleForm')" @click="submitPassword('ruleForm')">{{ $t('security.save') }}</el-button>
         </el-form-item>
       </el-form>
     </div>
@@ -52,38 +64,52 @@
 
 <script lang="ts">
 import { Vue, Component, Watch } from 'vue-property-decorator'
-import { apiChangePassword } from '@/request/api'
-import crypto from '@/crypto/crypto'
+import { loginStore } from '@/store/loginStore'
+import { apiChangePassword, apiChangeUserName } from '@/request/api'
 import { Form } from 'element-ui'
 
-interface IruleForm{
+interface isRuleForm{
   oldPassword:string,
   newPassword:string,
-  comfirmPassword:string
+  confirmPassword:string
+}
+interface isUserNameRuleForm{
+  userName:string
 }
 @Component
 export default class Security extends Vue {
-  ruleForm:IruleForm = {
+  ruleForm:isRuleForm = {
     oldPassword: '',
     newPassword: '',
-    comfirmPassword: ''
+    confirmPassword: ''
+  }
+
+  userRuleForm:isUserNameRuleForm = {
+    userName: ''
   }
 
   rules:any = {
     oldPassword: [
-      { required: true, message: 'Old password cannot be blank', trigger: 'change' }
+      { required: true, message: this.$t('security.oldPasswordBlank'), trigger: 'change' }
     ],
     newPassword: [
-      { required: true, message: 'Password cannot be blank', trigger: 'change' },
-      { min: 6, max: 24, message: 'The passwords must be from 6 to 24 characters long', trigger: 'change' }
+      { required: true, message: this.$t('security.passwordBlank'), trigger: 'change' },
+      { min: 6, max: 24, message: this.$t('security.passwordRule'), trigger: 'change' }
     ],
-    comfirmPassword: [
-      { required: true, message: 'Password cannot be blank', trigger: 'change' },
-      { min: 6, max: 24, message: 'The passwords must be from 6 to 24 characters long', trigger: 'change' }
+    confirmPassword: [
+      { required: true, message: this.$t('security.passwordBlank'), trigger: 'change' },
+      { min: 6, max: 24, message: this.$t('security.passwordRule'), trigger: 'change' }
+    ]
+  }
+
+  userRules:any = {
+    userName: [
+      { required: true, message: this.$t('security.nameBlank'), trigger: 'change' },
+      { max: 255, message: this.$t('security.nameMax'), trigger: 'change' }
     ]
   }
 
-  pdfsdkDomain = process.env.VUE_APP_PDFSDK_DOMAIN
+  pdfSDKDomain = process.env.VUE_APP_PDFSdk_DOMAIN
 
   passwordType = false
   passwordType2 = false
@@ -99,8 +125,8 @@ export default class Security extends Vue {
     return this.ruleForm.newPassword
   }
 
-  get ruleFormComfirm () {
-    return this.ruleForm.comfirmPassword
+  get ruleFormConfirm () {
+    return this.ruleForm.confirmPassword
   }
 
   // 监视属性
@@ -114,7 +140,7 @@ export default class Security extends Vue {
     this.showErrTipNotMatch = false
   }
 
-  @Watch('ruleFormComfirm')
+  @Watch('ruleFormConfirm')
   noValue3change (newVal:string, oldVal:string) {
     this.showErrTipNotMatch = false
   }
@@ -123,13 +149,15 @@ export default class Security extends Vue {
   // 提交
   submitPassword (formName: string) {
     (this.$refs[formName] as Form).validate((valid: boolean) => {
+      const passwordSame: any = this.$t('security.passwordSame')
+      const fail: any = this.$t('security.changedFail')
       if (valid) {
         this.validPassword()
         if (!this.showErrTipOld && !this.showErrTipNotMatch) {
           apiChangePassword({
             old_password: this.ruleForm.oldPassword,
             new_password: this.ruleForm.newPassword,
-            password_confirm: this.ruleForm.comfirmPassword
+            password_confirm: this.ruleForm.confirmPassword
           }, {}).then((res: any) => {
             if (res.code === 200) {
               this.changedSuccessBox()
@@ -137,21 +165,46 @@ export default class Security extends Vue {
               if (res.message === 'Incorrect Old Password.' || res.message === 'Cannot Match Old Password.') {
                 this.showErrTipOld = true
               } else if (res.message === 'The Password and Confirm Password do not match.') {
-                this.$message.error('New password cannot be the same as your old password.')
+                this.$message.error(passwordSame)
               }
-              this.$message.error('Changed Failed!')
+              this.$message.error(fail)
             } else {
-              this.$message.error('Changed Failed!')
+              this.$message.error(fail)
             }
           }).catch((error) => {
             console.log(error)
-            this.$message.error('Changed Failed!')
+            this.$message.error(fail)
           })
         }
       }
     })
   }
 
+  submitUserName (formName: string) {
+    (this.$refs[formName] as Form).validate((valid: boolean) => {
+      const fail: any = this.$t('security.changedFail')
+      if (valid) {
+        const formData = new FormData()
+        formData.append('fullName', this.userRuleForm.userName)
+        apiChangeUserName(formData, {}).then((res: any) => {
+          if (res.code === '200') {
+            loginStore().setUser({
+              email: loginStore().user.email,
+              name: this.userRuleForm.userName,
+              access_token: loginStore().user.access_token
+            })
+            this.userChangeSuccessBox()
+          } else {
+            this.$message.error(fail)
+          }
+        }).catch((error) => {
+          console.log(error)
+          this.$message.error(fail)
+        })
+      }
+    })
+  }
+
   // 阻止粘贴
   handlePaste () {
     return false
@@ -175,28 +228,30 @@ export default class Security extends Vue {
 
   // 修改成功弹框
   changedSuccessBox () {
+    const success: any = this.$t('security.changedSuccess')
     const imgUrl = require('../../../static/images/common/success@2x.png')
-    this.$alert('<img src="' + imgUrl + '" style="width: 48px; height: 48px" /><h2 style="text-align: center; margin-top: 24px; color: #000">Changed Success!</h2>', {
+    this.$alert('<img src="' + imgUrl + '" style="width: 48px; height: 48px" /><h2 style="text-align: center; margin-top: 24px; color: #232748">' + success + '</h2>', {
       dangerouslyUseHTMLString: true,
       center: true,
       confirmButtonText: 'OK',
-      customClass: 'security-msgbox'
-    }).then(() => {
-      const isRemenber = localStorage.getItem('isRemenber')
-      if (!isRemenber) {
-        localStorage.removeItem('isRemenber')
-      }
-      const url = window.location.href
-      window.location.href = this.pdfsdkDomain + '/login?from=api&redirect=' + url
-    }).catch(() => {
-      const url = window.location.href
-      window.location.href = this.pdfsdkDomain + '/login?from=api&redirect=' + url
+      customClass: 'msgBox'
+    })
+  }
+
+  userChangeSuccessBox () {
+    const success: any = this.$t('security.changedSuccess')
+    const imgUrl = require('../../../static/images/common/success@2x.png')
+    this.$alert('<img src="' + imgUrl + '" style="width: 48px; height: 48px" /><h2 style="text-align: center; margin-top: 24px; color: #232748">' + success + '</h2>', {
+      dangerouslyUseHTMLString: true,
+      center: true,
+      confirmButtonText: 'OK',
+      customClass: 'msgBox'
     })
   }
 
   // 校验密码是否与确认密码相同
   validPassword () {
-    if (this.ruleForm.newPassword !== this.ruleForm.comfirmPassword) {
+    if (this.ruleForm.newPassword !== this.ruleForm.confirmPassword) {
       this.showErrTipNotMatch = true
       this.$message.error('Changed Failed!')
     }
@@ -206,6 +261,12 @@ export default class Security extends Vue {
 
 <style lang="scss" scoped>
 .security {
+  & .block:nth-child(2) {
+    margin-bottom: 20px;
+  }
+  & .block:nth-child(3) {
+    margin-bottom: 0;
+  }
   .password-form {
     text-align: left;
     h2 {
@@ -236,7 +297,7 @@ export default class Security extends Vue {
     width: 200px;
   }
   .el-form-item {
-    margin-bottom: 20px;
+    margin-bottom: 40px;
     &:nth-child(3) {
       margin-bottom: 40px;
     }

+ 330 - 0
src/views/billing/information.vue

@@ -0,0 +1,330 @@
+<template>
+  <div class="information container">
+    <h1>{{ $t('information.title') }}</h1>
+    <div class="password-form block">
+      <h2>{{ $t('information.Billing') }}</h2>
+      <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-position="top" label-width="100px" inline-message>
+        <el-form-item prop="lastName" :label="$t('information.lastName')">
+          <el-input @input="submitBtnState('ruleForm')" v-model.trim="ruleForm.lastName" auto-complete="off" :placeholder="$t('information.lastNameTip')"
+          ></el-input>
+        </el-form-item>
+        <el-form-item prop="firstName" :label="$t('information.firstName')">
+          <el-input @input="submitBtnState('ruleForm')" v-model.trim="ruleForm.firstName" auto-complete="off" :placeholder="$t('information.firstNameTip')"
+          ></el-input>
+        </el-form-item>
+        <el-form-item prop="company" :label="$t('information.company')">
+          <el-input @input="submitBtnState('ruleForm')" v-model.trim="ruleForm.company" auto-complete="off" :placeholder="$t('information.companyTip')"
+          ></el-input>
+        </el-form-item>
+        <el-form-item :label="$t('information.country')">
+          <el-select v-model="ruleForm.country" :placeholder="$t('information.countryTip')" class="w-full">
+            <el-option v-for="item in countryList" :key="item.value" :label="item.name" :value="item.name"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item prop="province" :label="$t('information.province')">
+          <el-input @input="submitBtnState('ruleForm')" v-model.trim="ruleForm.province" auto-complete="off" :placeholder="$t('information.provinceTip')"
+          ></el-input>
+        </el-form-item>
+        <el-form-item prop="address" :label="$t('information.address')">
+          <el-input @input="submitBtnState('ruleForm')" v-model.trim="ruleForm.address" auto-complete="off" :placeholder="$t('information.addressTip')"
+          ></el-input>
+        </el-form-item>
+        <el-form-item :label="$t('information.zip')">
+          <el-input @input="submitBtnState('ruleForm')" v-model.trim="ruleForm.zip" auto-complete="off" :placeholder="$t('information.zipTip')"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+      <el-button type="primary" :disabled="disableInfo" @click="submitInformation('ruleForm')">{{ $t('information.update') }}</el-button>
+    </div>
+    <div class="password-form block">
+      <h2>{{ $t('information.mail') }}</h2>
+      <el-form :model="emailRuleForm" :rules="emailRules" ref="emailRuleForm" label-position="top" label-width="100px" inline-message>
+        <el-form-item prop="email" :label="$t('information.email')">
+          <el-input @input="submitBtnState('emailRuleForm')" v-model.trim="emailRuleForm.email" auto-complete="off" :placeholder="$t('information.emailTip')"
+          ></el-input>
+          <div class="tips">{{ $t('information.emailContact')[0] }} <a href="mailto:support@compdf.com">{{ $t('information.emailContact')[1] }}</a></div>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :disabled="disableEmail" @click="submitEmail('emailRuleForm')">{{ $t('information.save') }}</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { Vue, Component } from 'vue-property-decorator'
+import { country } from '@/utils/country'
+import { apiChangeInformation, apiChangeEmail, getBillingInformation } from '@/request/api'
+import { Form } from 'element-ui'
+
+interface isRuleForm{
+  firstName:string,
+  lastName:string,
+  company:string,
+  country:string,
+  province:string,
+  address:string,
+  zip:string
+}
+interface isEmailRuleForm{
+  email:string
+}
+@Component
+export default class Security extends Vue {
+  ruleForm:isRuleForm = {
+    firstName: '',
+    lastName: '',
+    company: '',
+    country: '',
+    province: '',
+    address: '',
+    zip: ''
+  }
+
+  emailRuleForm:isEmailRuleForm = {
+    email: ''
+  }
+
+  rules:any = {
+    lastName: [
+      { max: 255, message: this.$t('information.lastNameErr'), trigger: 'change' }
+    ],
+    firstName: [
+      { max: 255, message: this.$t('information.firstNameErr'), trigger: 'change' }
+    ],
+    company: [
+      { max: 255, message: this.$t('information.companyErr'), trigger: 'change' }
+    ],
+    province: [
+      { max: 255, message: this.$t('information.provinceErr'), trigger: 'change' }
+    ],
+    address: [
+      { max: 255, message: this.$t('information.addressErr'), trigger: 'change' }
+    ]
+  }
+
+  disableInfo = true
+  disableEmail = true
+  countryList = country()
+
+  emailRules:any = {
+    email: [
+      {
+        required: true,
+        message: this.$t('information.emailTip'),
+        trigger: ['']
+      },
+      {
+        type: 'email',
+        message: this.$t('information.emailFormatErr'),
+        trigger: ['']
+      }
+    ]
+  }
+
+  created () {
+    this.getBillingInfo()
+  }
+
+  pdfSDKDomain = process.env.VUE_APP_PDFSdk_DOMAIN
+
+  // 方法
+
+  async getBillingInfo () {
+    const data: any = await getBillingInformation();
+    (Object.keys(this.ruleForm) as Array<keyof isRuleForm>).forEach((key: keyof isRuleForm) => {
+      this.ruleForm[key] = data.data[key]
+    })
+    this.emailRuleForm.email = data.data.email
+  }
+
+  // 提交
+  submitInformation (formName: string) {
+    this.disableInfo = true;
+    (this.$refs[formName] as Form).validate((valid: boolean) => {
+      const fail: any = this.$t('information.changedFail')
+      if (valid) {
+        apiChangeInformation({
+          firstName: this.ruleForm.firstName,
+          lastName: this.ruleForm.lastName,
+          company: this.ruleForm.company,
+          country: this.ruleForm.country,
+          province: this.ruleForm.province,
+          address: this.ruleForm.address,
+          zip: this.ruleForm.zip
+        }, {}).then((res: any) => {
+          if (res.code === '200') {
+            this.changedSuccessBox()
+          } else {
+            this.$message.error(fail)
+          }
+        }).catch((error) => {
+          console.log(error)
+          this.$message.error(fail)
+        })
+      }
+    })
+  }
+
+  submitEmail (formName: string) {
+    this.disableEmail = true;
+    (this.$refs[formName] as Form).validate((valid: boolean) => {
+      const fail: any = this.$t('information.changedFail')
+      if (valid) {
+        const formData = new FormData()
+        formData.append('email', this.emailRuleForm.email)
+        apiChangeEmail(formData, {}).then((res: any) => {
+          console.log(res)
+          if (res.code === '200') {
+            this.changedSuccessBox()
+          } else {
+            this.$message.error(fail)
+          }
+        }).catch((error) => {
+          console.log(error)
+          this.$message.error(fail)
+        })
+      }
+    })
+  }
+
+  // 阻止粘贴
+  handlePaste () {
+    return false
+  }
+
+  // 提交按钮是否禁用
+  submitBtnState (formName: string) {
+    (this.$refs[formName] as Form).validate((valid: boolean) => {
+      if (valid) {
+        if (formName === 'ruleForm') {
+          this.disableInfo = false
+        } else {
+          this.disableEmail = false
+        }
+      }
+    })
+  }
+
+  // 修改成功弹框
+  changedSuccessBox () {
+    const success: any = this.$t('information.changedSuccess')
+    const imgUrl = require('../../../static/images/common/success@2x.png')
+    this.$alert('<img src="' + imgUrl + '" style="width: 48px; height: 48px" /><h2 style="text-align: center; margin-top: 24px; color: #232748">' + success + '</h2>', {
+      dangerouslyUseHTMLString: true,
+      center: true,
+      confirmButtonText: 'OK',
+      customClass: 'msgBox'
+    }).then(() => {
+      this.getBillingInfo()
+    }).catch((err) => {
+      console.log(err)
+    })
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.information {
+  & .block:nth-child(2) {
+    margin-bottom: 20px;
+    ::v-deep .el-form {
+      display: flex;
+      flex-wrap: wrap;
+      .el-form-item {
+        width: calc(50% - 15px);
+        margin-right: 30px;
+        @media screen and (max-width: 429px) {
+          width: 100%;
+          margin-right: 0px;
+        }
+      }
+      & .el-form-item:nth-child(2n) {
+        margin-right: 0;
+      }
+    }
+  }
+  & .block:nth-child(3) {
+    margin-bottom: 0;
+  }
+  .password-form {
+    text-align: left;
+    h2 {
+      margin-bottom: 30px;
+    }
+    .show-password {
+      display: inline-block;
+      position: absolute;
+      right: 18px;
+      top: 18px;
+      width: 14px;
+      height: 14px;
+      cursor: pointer;
+      &.eye-close {
+        background: url('@/assets/images/common/eye_close.svg');
+      }
+      &.eye-open {
+        background: url('@/assets/images/common/eye_open.svg');
+      }
+    }
+  }
+}
+.err-tip {
+  position: unset;
+}
+.el-button--primary {
+  width: 200px;
+  height: 48px;
+  color: #FFF;
+  font-size: 16px;
+  font-weight: 700;
+  line-height: 18px;
+  border-radius: 6px;
+}
+.el-button--primary:nth-child(3) {
+  margin-top: 40px;
+}
+::v-deep .el-form {
+  .el-form-item {
+    margin-bottom: 20px;
+    .tips {
+      font-size: 12px;
+      margin-top: 4px;
+      color: #52555F;
+      line-height: 16px;
+      a {
+        color: #396FFA;
+      }
+    }
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+  .el-form-item__content {
+    line-height: 1;
+    .el-select {
+      width: 100%;
+    }
+  }
+}
+</style>
+<style lang="scss">
+.el-select-dropdown {
+  margin-top: 0 !important;
+  .popper__arrow {
+    display: none !important;
+  }
+  .el-select-dropdown__item.selected {
+    color: #396FFA;
+  }
+}
+@media screen and (min-width: 930px) {
+  .el-select-dropdown {
+    margin-top: 10px !important;
+    .el-select-dropdown__item {
+      text-align: left;
+    }
+  }
+}
+</style>

+ 533 - 0
src/views/billing/invoices.vue

@@ -0,0 +1,533 @@
+<template>
+  <div class="plan container" v-loading="loading">
+    <h1>{{ $t('invoices.Billing') }}</h1>
+    <div class="block">
+      <div class="top">
+        <h2>{{ $t('invoices.information') }}</h2>
+      </div>
+      <div class="plan-table">
+        <el-table :data="tableData" style="width: 100%" :default-sort = "{prop: 'date', order: 'descending'}">
+          <el-table-column prop="payTime" :label="$t('invoices.date')" min-width="120"></el-table-column>
+          <el-table-column prop="orderNo" :label="$t('invoices.order')" min-width="240"></el-table-column>
+          <el-table-column prop="price" :label="$t('invoices.amount') " min-width="120"></el-table-column>
+          <el-table-column prop="billNo" :label="$t('invoices.number')" min-width="120">
+            <template slot-scope="scope">{{scope.row.billNo ? scope.row.billNo : '-' }}</template>
+          </el-table-column>
+          <el-table-column prop="remainingFiles" :label="$t('invoices.operation') " min-width="120">
+            <template slot-scope="scope">
+              <div class="opera">
+                <template v-if="scope.row.billNo">
+                  <div @click="downloadInvoice(scope.row.billUrl)">{{ $t('invoices.download') }}</div>
+                  <div class="send">|</div>
+                  <div @click="sendEmail(scope.row.id)">{{ $t('invoices.send') }}</div>
+                </template>
+                <template v-else>
+                  <div @click="apply(scope.row)">{{ $t('invoices.apply') }}</div>
+                </template>
+              </div>
+            </template>
+          </el-table-column>
+          <p slot="empty">{{ $t('invoices.noData') }}</p>
+        </el-table>
+      </div>
+      <div class="pagination">
+        <el-pagination background layout="prev, pager, next" :page-size="pageSize" :total="totalNum" @current-change="handleCurrentChange" class="max-page-btn"></el-pagination>
+        <div class="mobile-page-btn">
+          <el-button type="primary" plain icon="el-icon-arrow-left" @click="handleCurrentChangeMobile(currentPage - 1)" :class="{'disabledBtn': disabledPrevBtn}">{{ $t('plan.previous') }}</el-button>
+          <el-button type="primary" plain @click="handleCurrentChangeMobile(currentPage + 1)" :class="{'disabledBtn': disabledNextBtn}">{{ $t('information.save') }}<i class="el-icon-arrow-right el-icon--right"></i></el-button>
+        </div>
+      </div>
+      <iframe :src="link" frameborder="0"></iframe>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import dayjs from 'dayjs'
+import { Component, Vue } from 'vue-property-decorator'
+import { getBillingInformation, getInvoices, apiApplyInvoice, apiSendEmail } from '@/request/api'
+
+@Component
+export default class Plan extends Vue {
+  tableData:any = []
+  PricingPlanList:any = []
+  pageSize = 8
+  totalNum = 0
+  currentPage = 1
+  // 当前排序,默认倒序
+  order = 1
+  disabledPrevBtn = false
+  disabledNextBtn = false
+  totalPageNum = 1
+  loading = false
+  link = ''
+
+  created () {
+    this.getInvoicesList()
+  }
+
+  // 获取流水列表数据
+  getInvoicesList () {
+    const failedConnect: any = this.$t('failedConnect')
+    getInvoices().then((res:any) => {
+      if (res.code === '200') {
+        const array = this.paginateArray(res.data, this.pageSize)[this.currentPage - 1]
+        this.tableData = []
+        this.totalNum = res.data.length
+        this.totalPageNum = Math.ceil(res.data.length / 8)
+        for (let i = 0; i < array.length; i++) {
+          this.tableData.push(array[i])
+          if (this.currentPage <= 1) {
+            this.disabledPrevBtn = true
+          }
+          if (this.currentPage >= this.totalPageNum) {
+            this.disabledNextBtn = true
+          }
+        }
+      } else {
+        this.$message.error(failedConnect)
+      }
+    }).catch(err => {
+      console.log(err)
+      this.$message.error(failedConnect)
+    })
+  }
+
+  // 分页
+  paginateArray (array:any, pageSize:number) {
+    const paginatedArray = []
+    const totalPages = Math.ceil(array.length / pageSize)
+    for (let i = 0; i < totalPages; i++) {
+      const start = i * pageSize
+      const end = start + pageSize
+      const page = array.slice(start, end)
+      paginatedArray.push(page)
+    }
+    return paginatedArray
+  }
+
+  // 日期自定格式
+  formatterDate (row:any, column:any) {
+    const val = row[column.property]
+    if (!val) return ''
+    return dayjs(val).format('MMM D,YYYY')
+  }
+
+  // 申请开票
+  async apply (val:any) {
+    const incomplete: any = this.$t('invoices.incomplete')
+    const complete: any = this.$t('invoices.complete')
+    const back: any = this.$t('invoices.back')
+    const fillInformation: any = this.$t('invoices.fillInformation')
+    const imgUrl = require('../../../static/images/common/fail@2x.png')
+    const data: any = await getBillingInformation()
+    let valid = true
+    Object.keys(data.data).forEach((key: any) => {
+      if (!data.data[key]) {
+        valid = false
+        this.$confirm('<img src="' + imgUrl + '" style="width: 48px; height: 48px" /><h2 style="text-align: center; margin: 16px auto; color: #232748">' + incomplete + '</h2><div style="color: #52555F; font-size: 16px; line-height: 24px; text-align: center;">' + complete + '</div>', {
+          dangerouslyUseHTMLString: true,
+          center: true,
+          showClose: false,
+          confirmButtonText: fillInformation,
+          cancelButtonText: back,
+          customClass: 'infoBox msgBox'
+        }).then(() => {
+          this.$router.push('/billing/information')
+        }).catch((err) => {
+          console.log(err)
+        })
+      }
+    })
+    // 用户信息完整申请开票
+    if (valid) {
+      this.loading = true
+      const emailFail: any = this.$t('invoices.fail')
+      const emailSuccess: any = this.$t('invoices.success')
+      const data: any = await apiApplyInvoice({ orderId: val.id }, {})
+      if (data.code === '200') {
+        this.loading = false
+        this.$message({
+          message: emailSuccess,
+          type: 'success'
+        })
+        this.getInvoicesList()
+      } else {
+        this.loading = false
+        this.$message.error(emailFail)
+      }
+    }
+  }
+
+  // 下载发票
+  downloadInvoice (val: string) {
+    const xhr = new XMLHttpRequest()
+    xhr.open('get', val, true)
+    xhr.setRequestHeader('Content-Type', 'application/pdf')
+    xhr.responseType = 'blob'
+    xhr.onload = function () {
+      if (this.status === 200) {
+        const blob = new Blob([this.response], { type: 'application/pdf' })
+        const objectUrl = URL.createObjectURL(blob)
+        const ele = document.createElement('a')
+        ele.href = objectUrl
+        ele.download = 'fileName'
+        ele.click()
+      }
+    }
+    xhr.send()
+  }
+
+  // 发送邮件
+  async sendEmail (val:any) {
+    const data: any = await getBillingInformation()
+    if (data.data.email) {
+      const format: any = this.$t('invoices.format')
+      const emailFail: any = this.$t('invoices.emailFail')
+      const changeEmail: any = this.$t('invoices.changeEmail')
+      const emailSuccess: any = this.$t('invoices.emailSuccess')
+      const confirmation: any = this.$t('invoices.confirmation')
+      const confirmEmail: any = this.$t('invoices.confirmEmail')
+      this.$confirm('<h2 style="text-align: left; color: #232748">' + confirmation + '</h2><div style="color: #52555F; font-size: 16px; line-height: 24px; text-align: left; margin: 20px 0 12px">' + format + '</div><div style="font-weight: 600; font-size: 14px; line-height: 20px; color: #232748; text-align: left">' + data.data.email + '</div>', {
+        dangerouslyUseHTMLString: true,
+        center: true,
+        showClose: false,
+        distinguishCancelAndClose: true,
+        cancelButtonText: changeEmail,
+        confirmButtonText: confirmEmail,
+        customClass: 'emailBox msgBox'
+      }).then(async () => {
+        const cn = this.$i18n.locale === 'zh-cn'
+        this.loading = true
+        const res: any = await apiSendEmail({ email: data.data.email, orderId: val, isCN: cn }, {})
+        this.loading = false
+        if (res.code === '200') {
+          this.$message({
+            message: emailSuccess,
+            type: 'success'
+          })
+        } else {
+          this.$message.error(emailFail)
+        }
+        console.log(res)
+      }).catch((err) => {
+        if (err === 'cancel') {
+          this.$router.push('/billing/information')
+        }
+      })
+    } else {
+      const back: any = this.$t('invoices.back')
+      const emailFill: any = this.$t('invoices.emailFill')
+      const fillEmail: any = this.$t('invoices.fillEmail')
+      const completeEmail: any = this.$t('invoices.completeEmail')
+      const imgUrl = require('../../../static/images/common/fail@2x.png')
+      this.$confirm('<img src="' + imgUrl + '" style="width: 48px; height: 48px" /><h2 style="text-align: center; margin: 16px auto; color: #232748">' + emailFill + '</h2><div style="color: #52555F; font-size: 16px; line-height: 24px; text-align: center;">' + completeEmail + '</div>', {
+        dangerouslyUseHTMLString: true,
+        center: true,
+        cancelButtonText: back,
+        confirmButtonText: fillEmail,
+        customClass: 'emailErrBox msgBox'
+      }).then(() => {
+        this.$router.push('/billing/information')
+      }).catch((err) => {
+        console.log(err)
+      })
+    }
+  }
+
+  // 切换当前页
+  handleCurrentChange (page:number):void {
+    this.currentPage = page
+    this.getInvoicesList()
+  }
+
+  // 切换当前页(移动端)
+  handleCurrentChangeMobile (page:number) {
+    if (page < 1) {
+      this.disabledPrevBtn = true
+      return undefined
+    }
+    if (page > this.totalPageNum) {
+      this.disabledNextBtn = true
+    } else {
+      this.disabledPrevBtn = false
+      this.disabledNextBtn = false
+      this.currentPage = page
+      this.getInvoicesList()
+    }
+    if (page === 1) {
+      this.disabledPrevBtn = true
+      this.currentPage = page
+      this.getInvoicesList()
+    }
+    if (page === this.totalPageNum) {
+      this.disabledNextBtn = true
+      this.currentPage = page
+      this.getInvoicesList()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.plan {
+  .plan-table {
+    margin-top: 38px;
+  }
+  .top {
+    display: flex;
+    align-items: flex-start;
+    justify-content: space-between;
+    img {
+      width: 28px;
+      height: 28px;
+    }
+    div {
+      cursor: pointer;
+    }
+  }
+  :deep .el-table--enable-row-hover .el-table__body tr:hover > td.el-table__cell {
+    background-color: #fff;
+  }
+  .pagination {
+    margin-top: 40px;
+    text-align: right;
+    .mobile-page-btn {
+      display: none;
+      ::v-deep .el-button {
+        width: 114px;
+        height: 40px;
+        border-radius: 6px;
+        font-weight: 700;
+        &:first-child {
+          padding-left: 11px;
+        }
+        &:last-child {
+          text-align: right;
+          padding-left: 13px;
+          margin-left: 12px;
+        }
+      }
+      ::v-deep .el-button--primary.is-plain {
+        background: #fff;
+        border-color: #1460F3;
+        &.disabledBtn {
+          opacity: 0.5;
+        }
+        &:not(.disabledBtn):hover {
+          background: rgba(20, 96, 243, 0.1);
+        }
+      }
+      ::v-deep [class^=el-icon-] {
+        font-weight: 700;
+      }
+      ::v-deep .el-button--primary.is-plain:hover, .el-button--primary.is-plain:focus {
+        color: #1460F3;
+      }
+    }
+  }
+  .comb{
+    margin-top: 24px;
+    margin-bottom: 20px;
+    padding: 20px 40px 24px;
+    background: #FFFFFF;
+    box-shadow: 0px 4px 35px rgba(129, 149, 200, 0.18);
+    border-radius: 16px;
+    text-align: left;
+    div + div{
+      margin-top: 8px;
+    }
+    span{
+      font-weight: 600;
+    }
+  }
+}
+::v-deep .el-table {
+  .el-table__header {
+    @media screen and (max-width: 429px) {
+      width: 1020px !important;
+    }
+    .is-leaf {
+      background-color: #EBF1FE;
+      border-bottom: none;
+    }
+  }
+  .el-table__body {
+    @media screen and (max-width: 429px) {
+      width: 1020px !important;
+    }
+  }
+  .el-table__cell {
+    div {
+      text-align: center;
+    }
+    .opera {
+      display: flex;
+      cursor: pointer;
+      color: #396FFA;
+      justify-content: center;
+      .send {
+        margin: 0 8px;
+        color: #E1E3E8;
+      }
+    }
+  }
+}
+@media screen and (max-width: 767px) {
+  .plan .pagination {
+    text-align: center;
+    .mobile-page-btn {
+      display: inline-block;
+    }
+    .max-page-btn {
+      display: none;
+    }
+  }
+}
+@media screen and (max-width: 320px) {
+  .plan .pagination {
+    .mobile-page-btn {
+      ::v-deep .el-button {
+        width: calc(50% - 6px);
+      }
+    }
+  }
+}
+</style>
+<style lang="scss">
+// 未录入收件邮箱
+.el-message-box.infoBox.el-message-box--center {
+  width: 474px;
+  @media screen and (max-width: 429px) {
+    width: calc(100% - 60px);
+  }
+  .el-message-box__btns {
+    display: flex;
+    justify-content: space-between;
+    @media screen and (max-width: 429px) {
+      flex-direction: column;
+    }
+    & button:nth-child(1) {
+      width: 151px;
+      color: #396FFA;
+      border-color: #396FFA;
+      @media screen and (max-width: 429px) {
+        width: 100%;
+      }
+      &:hover {
+        color: white;
+        background-color: #396FFA;
+      }
+    }
+    & button:nth-child(2) {
+      width: calc(100% - 167px);
+      @media screen and (max-width: 429px) {
+        width: 100%;
+        margin-left: 0;
+        margin-top: 12px;
+      }
+    }
+    button {
+      font-family: 'Poppins';
+      width: 50%;
+    }
+  }
+}
+// 邮件发送确认
+.el-message-box.emailBox.el-message-box--center {
+  width: 454px;
+  @media screen and (max-width: 429px) {
+    width: calc(100% - 60px);
+  }
+  @media screen and (max-width: 429px){
+    .el-message-box {
+        width: calc(100% - 60px);
+    }
+  }
+  .el-message-box__header {
+    display: none;
+  }
+  .el-message-box__content {
+    padding: 40px 32px 0;
+  }
+  .el-message-box__btns {
+    display: flex;
+    padding: 32px;
+    padding-bottom: 40px;
+    justify-content: flex-end;
+    @media screen and (max-width: 429px) {
+      flex-direction: column;
+      justify-content: inherit;
+    }
+    & button:nth-child(1) {
+      color: #396FFA;
+      border-color: #396FFA;
+      @media screen and (max-width: 429px) {
+        width: 100%;
+      }
+      &:hover {
+        color: white;
+        background-color: #396FFA;
+      }
+    }
+    & button:nth-child(2) {
+      @media screen and (max-width: 429px) {
+        width: 100%;
+        margin-left: 0;
+        margin-top: 12px;
+      }
+    }
+    button {
+      width: auto;
+      padding: 10px;
+      font-family: 'Poppins';
+    }
+  }
+}
+// 未录入收件邮箱
+.el-message-box.emailErrBox.el-message-box--center {
+  width: 464px;
+  @media screen and (max-width: 429px) {
+    width: calc(100% - 60px);
+  }
+  .el-message-box__header {
+    display: none;
+  }
+  .el-message-box__content {
+    padding: 40px 32px 0;
+  }
+  .el-message-box__btns {
+    display: flex;
+    padding: 32px;
+    padding-bottom: 40px;
+    justify-content: space-between;
+    @media screen and (max-width: 429px) {
+      flex-direction: column;
+      justify-content: inherit;
+    }
+    & button:nth-child(1) {
+      color: #396FFA;
+      border-color: #396FFA;
+      @media screen and (max-width: 429px) {
+        width: 100%;
+      }
+      &:hover {
+        color: white;
+        background-color: #396FFA;
+      }
+    }
+    & button:nth-child(2) {
+      @media screen and (max-width: 429px) {
+        width: 100%;
+        margin-left: 0;
+        margin-top: 12px;
+      }
+    }
+    button {
+      width: calc(50% - 8px);
+      padding: 10px;
+      font-family: 'Poppins';
+    }
+  }
+}
+</style>

+ 31 - 20
src/views/billing/plan.vue

@@ -1,39 +1,43 @@
 <template>
   <div class="plan container">
-    <h1>Plan</h1>
+    <h1>{{ $t('plan.title') }}</h1>
     <Progress :init="true" />
     <div v-for="(item, index) in PricingPlanList" :key="index" class="comb">
-      <div><span>Pricing Plan: </span>{{ item.assetTypeShow }}</div>
-      <div v-if="item.assetTypeShow === 'Monthly' || item.assetTypeShow === 'Free'"><span>Up to: </span>{{ item.totalFiles }} Documents / Month</div>
-      <div v-else-if="item.assetTypeShow === 'Annually'"><span>Up to: </span>{{ item.totalFiles }} Documents / Year</div>
-      <div v-else><span>Up to: </span>{{ item.totalFiles }}</div>
-      <div v-if="item.assetType !== 2"><span>Validity Period: </span>{{ item.startDate }} ~ {{item.endDate}}</div>
+      <div><span>{{ $t('plan.plan') }} </span>{{ $t('plan')[item.assetTypeShow.toLowerCase()] }}</div>
+      <div v-if="item.assetTypeShow === 'Monthly' || item.assetTypeShow === 'Free'"><span>{{ $t('plan.upTo') }} </span>{{ item.totalFiles }} {{ $t('plan.documents') }} / {{ $t('plan.month') }}</div>
+      <div v-else-if="item.assetTypeShow === 'Annually'"><span>{{ $t('plan.upTo') }} </span>{{ item.totalFiles }} {{ $t('plan.documents') }} / {{ $t('plan.year') }}</div>
+      <div v-else><span>{{ $t('plan.upTo') }} </span>{{ item.totalFiles }}</div>
+      <div v-if="item.assetType !== 2"><span>{{ $t('plan.validity') }} </span>{{ item.startDate }} ~ {{item.endDate}}</div>
     </div>
     <div class="block">
       <div class="top">
-        <h2>Processing Files Balance</h2>
-        <a @click="exportData">
+        <h2>{{ $t('plan.filesBalance') }}</h2>
+        <div @click="exportData">
           <picture>
             <source type="image/png" media="(min-width: 430px)" srcset="../../../static/images/dashboard/download@2x.png">
             <source type="image/png" media="(max-width: 429px)" srcset="../../../static/images/dashboard/download.png">
             <img src="../../../static/images/dashboard/download@2x.png" alt="download">
           </picture>
-        </a>
+        </div>
       </div>
       <div class="plan-table">
         <el-table :data="tableData" style="width: 100%" :default-sort = "{prop: 'date', order: 'descending'}" @sort-change="changeOrder">
-          <el-table-column prop="date" label="Date" sortable :sort-orders="['ascending', 'descending']" sort-by='id' :formatter="formatterDate" min-width="120"></el-table-column>
-          <el-table-column prop="description" label="Description" min-width="240"></el-table-column>
-          <el-table-column prop="balanceChange" label="Balance Change" min-width="120"></el-table-column>
-          <el-table-column prop="remainingFiles" label="Remaining Files" min-width="120"></el-table-column>
-          <p slot="empty">No Data Available</p>
+          <el-table-column prop="date" :label="$t('plan.date')" sortable :sort-orders="['ascending', 'descending']" sort-by='id' :formatter="formatterDate" min-width="120"></el-table-column>
+          <el-table-column :label="$t('plan.description')" min-width="240">
+            <template slot-scope="scope">
+              {{ $t('plan')[scope.row.description] }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="balanceChange" :label="$t('plan.balanceChange')" min-width="120"></el-table-column>
+          <el-table-column prop="remainingFiles" :label="$t('plan.remainingFiles')" min-width="120"></el-table-column>
+          <p slot="empty">{{ $t('plan.noData') }}</p>
         </el-table>
       </div>
       <div class="pagination">
         <el-pagination background layout="prev, pager, next" :page-size="pageSize" :total="totalNum" @current-change="handleCurrentChange" class="max-page-btn"></el-pagination>
         <div class="mobile-page-btn">
-          <el-button type="primary" plain icon="el-icon-arrow-left" @click="handleCurrentChangeMobile(currentPage - 1)" :class="{'disabledBtn': disabledPrevBtn}">Previous</el-button>
-          <el-button type="primary" plain @click="handleCurrentChangeMobile(currentPage + 1)" :class="{'disabledBtn': disabledNextBtn}">Next<i class="el-icon-arrow-right el-icon--right"></i></el-button>
+          <el-button type="primary" plain icon="el-icon-arrow-left" @click="handleCurrentChangeMobile(currentPage - 1)" :class="{'disabledBtn': disabledPrevBtn}">{{ $t('plan.previous') }}</el-button>
+          <el-button type="primary" plain @click="handleCurrentChangeMobile(currentPage + 1)" :class="{'disabledBtn': disabledNextBtn}">{{ $t('plan.next') }}<i class="el-icon-arrow-right el-icon--right"></i></el-button>
         </div>
       </div>
     </div>
@@ -70,6 +74,7 @@ export default class Plan extends Vue {
 
   // 获取plan列表数据
   getPlanList (page:number) {
+    const failedConnect: any = this.$t('failedConnect')
     getBalanceRecordList({
       page: page,
       size: this.pageSize,
@@ -89,11 +94,11 @@ export default class Plan extends Vue {
           }
         }
       } else {
-        this.$message.error('Failed to connect.')
+        this.$message.error(failedConnect)
       }
     }).catch(err => {
       console.log(err)
-      this.$message.error('Failed to connect.')
+      this.$message.error(failedConnect)
     })
   }
 
@@ -101,7 +106,8 @@ export default class Plan extends Vue {
   formatterDate (row:any, column:any) {
     const val = row[column.property]
     if (!val) return ''
-    return dayjs(val).format('MMM D,YYYY')
+    const day = this.$i18n.locale === 'en' ? dayjs(val).format('MMM D,YYYY') : dayjs(val).format('YYYY年MM月DD日')
+    return day
   }
 
   // 切换当前页
@@ -138,9 +144,11 @@ export default class Plan extends Vue {
 
   // 账户消费充值记录导出
   exportData () {
+    const cn = this.$i18n.locale === 'zh-cn'
     exportBalanceRecordList({
       isDecs: this.order,
-      timeZone: (0 - new Date().getTimezoneOffset() / 60) === 8 ? '8' : '0'
+      timeZone: (0 - new Date().getTimezoneOffset() / 60) === 8 ? '8' : '0',
+      isCN: cn
     }, {
       responseType: 'blob'
     }).then((res:any) => {
@@ -206,6 +214,9 @@ export default class Plan extends Vue {
       width: 28px;
       height: 28px;
     }
+    div {
+      cursor: pointer;
+    }
   }
   :deep .el-table--enable-row-hover .el-table__body tr:hover > td.el-table__cell {
     background-color: #fff;

+ 13 - 13
src/views/login.vue

@@ -29,7 +29,7 @@
           <span v-show="showErrTip" class="err-tip">Incorrect email or password.</span>
         </el-form-item>
         <el-form-item class="remember-content">
-          <el-checkbox-group v-model="ruleForm.isRemenber">
+          <el-checkbox-group v-model="ruleForm.isRemember">
             <el-checkbox label="Remember me"></el-checkbox>
           </el-checkbox-group>
         </el-form-item>
@@ -43,23 +43,22 @@
 
 <script lang="ts">
 import { Vue, Component, Watch } from 'vue-property-decorator'
-import Cookie from 'js-cookie'
 import { loginStore } from '@/store/loginStore'
 import { apiLogin, getUserInfo } from '@/request/api'
 import crypto from '@/crypto/crypto'
 
-interface IruleForm {
+interface isRuleForm {
   password: string,
   email: string,
-  isRemenber: boolean
+  isRemember: boolean
 }
 
 @Component
 export default class Login extends Vue {
-  ruleForm:IruleForm = {
+  ruleForm:isRuleForm = {
     password: '',
     email: '',
-    isRemenber: false
+    isRemember: false
   }
 
   showErrTipEmailBlank = false
@@ -68,12 +67,12 @@ export default class Login extends Vue {
   passwordType = false
 
   mounted () {
-    const isRemenber = localStorage.getItem('isRemenber')
-    if (isRemenber) {
+    const isRemember = localStorage.getItem('isRemember')
+    if (isRemember) {
       const password = localStorage.getItem('password')
       this.ruleForm.email = localStorage.getItem('username') || ''
       this.ruleForm.password = (password && crypto.get(password)) || ''
-      this.ruleForm.isRemenber = true
+      this.ruleForm.isRemember = true
     }
   }
 
@@ -100,6 +99,7 @@ export default class Login extends Vue {
 
   // 登录
   submitForm () {
+    const failedConnect: any = this.$t('failedConnect')
     if (this.ruleForm.email === '' || this.ruleForm.password === '') {
       if (this.ruleForm.email === '') {
         this.showErrTipEmailBlank = true
@@ -118,10 +118,10 @@ export default class Login extends Vue {
         const SECRET_PWD = crypto.set(this.ruleForm.password) // 加密
         localStorage.setItem('password', SECRET_PWD)
         localStorage.setItem('username', this.ruleForm.email)
-        if (this.ruleForm.isRemenber) {
-          localStorage.setItem('isRemenber', 'true')
+        if (this.ruleForm.isRemember) {
+          localStorage.setItem('isRemember', 'true')
         } else {
-          localStorage.removeItem('isRemenber')
+          localStorage.removeItem('isRemember')
         }
         getUserInfo({}).then((res: any) => {
           if (res.code === 200) {
@@ -141,7 +141,7 @@ export default class Login extends Vue {
       }
     }).catch(err => {
       console.log(err)
-      this.$message.error('Failed to connect.')
+      this.$message.error(failedConnect)
     })
   }
 

+ 11 - 9
src/views/projects/api/add.vue

@@ -1,14 +1,14 @@
 <template>
   <div class="apiKeys-page create">
-    <h2>Create New Project</h2>
+    <h2>{{ $t('apiKey.newProject') }}</h2>
     <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-position="top" label-width="100px">
-      <el-form-item label="Project Name" prop="projectName">
-        <el-input v-model.trim="ruleForm.projectName" auto-complete="off" placeholder="Please enter project name"></el-input>
-        <span v-show="showErrTip" class="err-tip">A project with the same name already exists.</span>
+      <el-form-item :label="$t('apiKey.projectName')" prop="projectName">
+        <el-input v-model.trim="ruleForm.projectName" auto-complete="off" :placeholder="$t('apiKey.enterName')"></el-input>
+        <span v-show="showErrTip" class="err-tip">{{ $t('apiKey.exists') }}</span>
       </el-form-item>
       <el-form-item class="btn-group">
-        <el-button @click="closePage">Cancel</el-button>
-        <el-button type="primary" :disabled="!ruleForm.projectName" @click="save">Save</el-button>
+        <el-button @click="closePage">{{ $t('apiKey.cancel') }}</el-button>
+        <el-button type="primary" :disabled="!ruleForm.projectName" @click="save">{{ $t('apiKey.save') }}</el-button>
       </el-form-item>
     </el-form>
   </div>
@@ -53,21 +53,23 @@ export default class apiAdd extends Vue {
 
   // 添加新项目
   createNewProject () {
+    const success: any = this.$t('apiKey.createSuccess')
+    const fail: any = this.$t('apiKey.createFail')
     apiAddNewProject({
       projectName: this.ruleForm.projectName
     }, {}).then((res: any) => {
       if (res.code === '200') {
         this.$message({
-          message: 'Create successfully!',
+          message: success,
           type: 'success'
         })
         this.closePage();
         (this.$parent as any).getKeysList()
       } else if (res.code === '-1') {
         this.showErrTip = true
-        this.$message.error('Create Failed.')
+        this.$message.error(fail)
       } else {
-        this.$message.error('Create Failed.')
+        this.$message.error(fail)
       }
     }).catch(err => {
       this.$message.error(err)

+ 39 - 31
src/views/projects/api/apiKeys.vue

@@ -1,35 +1,35 @@
 <template>
   <div class="api-keys container">
-    <h1>API Keys</h1>
+    <h1>{{ $t('apiKey.title') }}</h1>
     <div class="block">
       <router-view></router-view>
-      <div class="top" v-show="showfather">
-        <h2>Projects and Keys</h2>
+      <div class="top" v-show="showFather">
+        <h2>{{ $t('apiKey.projectsKeys') }}</h2>
         <router-link :to="{name: 'addApiKeys'}">
           <el-button type="primary">
             <img src="@/assets/images/common/add.svg">
-            <span class="max">Create New Project</span>
+            <span class="max">{{ $t('apiKey.newProject') }}</span>
           </el-button>
         </router-link>
       </div>
-      <div class="keys-table" v-show="showfather">
+      <div class="keys-table" v-show="showFather">
         <el-table :data="tableData" style="width: 100%">
-          <el-table-column prop="projectName" label="Project Name" min-width="130">
+          <el-table-column prop="projectName" :label="$t('apiKey.projectName')" min-width="130">
             <template slot-scope="scope">
-              <div class="key-projectName" :title="scope.row.projectName">{{scope.row.projectName}}</div>
+              <div class="key-projectName" :title="scope.row.projectName">{{ scope.row.projectName }}</div>
             </template>
           </el-table-column>
-          <el-table-column label="Public Key" min-width="200">
+          <el-table-column :label="$t('apiKey.publicKey')" min-width="200">
             <template slot-scope="scope">
               <el-tooltip
                 popper-class="copy-tip"
-                content="Copied"
+                :content="$t('apiKey.copied')"
                 :value="showTooltip === '#public' + scope.$index"
                 :visible-arrow="false"
                 :manual="true"
                 placement="top"
               >
-                <template slot="content"><img src="@/assets/images/common/check_right.svg" class="min" />Copied</template>
+                <template slot="content"><img src="@/assets/images/common/check_right.svg" class="min" />{{ $t('apiKey.copied') }}</template>
                 <div
                   class="key-input public-key-input"
                   :class="{'copy-active': isCopyActive === '#public' + scope.$index}"
@@ -40,17 +40,17 @@
               </el-tooltip>
             </template>
           </el-table-column>
-          <el-table-column prop="secretKey" label="Secret Key" min-width="200">
+          <el-table-column prop="secretKey" :label="$t('apiKey.secretKey')" min-width="200">
             <template slot-scope="scope">
               <el-tooltip
                 popper-class="copy-tip"
-                content="Copied"
+                :content="$t('apiKey.copied')"
                 :value="showTooltip === '#secret' + scope.$index"
                 :visible-arrow="false"
                 :manual="true"
                 placement="top"
               >
-                <template slot="content"><img src="@/assets/images/common/check_right.svg" class="min" />Copied</template>
+                <template slot="content"><img src="@/assets/images/common/check_right.svg" class="min" />{{ $t('apiKey.copied') }}</template>
                 <div
                   class="key-input secret-key-input"
                   :class="{'copy-active': isCopyActive === '#secret' + scope.$index}"
@@ -65,14 +65,14 @@
               </el-tooltip>
             </template>
           </el-table-column>
-          <el-table-column prop="createDate" label="Creation Date" :formatter="formatterDate" min-width="140"></el-table-column>
-          <el-table-column label="Actions" min-width="80">
+          <el-table-column prop="createDate" :label="$t('apiKey.creationDate')" :formatter="formatterDate" min-width="140"></el-table-column>
+          <el-table-column :label="$t('apiKey.actions')" min-width="80">
             <template slot-scope="scope">
-              <router-link :to="{name: 'editApiKeys', params: {'id': scope.row.id}, query: {'projectName': scope.row.projectName}}"><img src="@/assets/images/apikeys/edit.svg" alt="edit" class="edit-icon" /></router-link>
-              <img src="@/assets/images/apikeys/delete.svg" alt="delete" @click="handleDelete(scope.$index, scope.row)" class="delete-icon" />
+              <router-link :to="{name: 'editApiKeys', params: {'id': scope.row.id}, query: {'projectName': scope.row.projectName}}"><img src="@/assets/images/apiKey/edit.svg" alt="edit" class="edit-icon" /></router-link>
+              <img src="@/assets/images/apiKey/delete.svg" alt="delete" @click="handleDelete(scope.$index, scope.row)" class="delete-icon" />
             </template>
           </el-table-column>
-          <p slot="empty">No Data Available</p>
+          <p slot="empty">{{ $t('apiKey.noData') }}</p>
         </el-table>
       </div>
     </div>
@@ -96,7 +96,7 @@ interface tableDataValue{
 }
 
 @Component
-export default class apikeys extends Vue {
+export default class apiKeys extends Vue {
   tableData: Array<tableDataValue> = []
   showEye = -1
   pageType = true
@@ -105,8 +105,8 @@ export default class apikeys extends Vue {
   copyTimer: any = null
   isCopyActive = ''
 
-  get showfather () {
-    return (this as any).$route.meta.showfather
+  get showFather () {
+    return (this as any).$route.meta.showFather
   }
 
   created () {
@@ -115,6 +115,7 @@ export default class apikeys extends Vue {
 
   // 获取列表
   getKeysList () {
+    const failedConnect: any = this.$t('failedConnect')
     getProjectList({
       timeZone: (0 - new Date().getTimezoneOffset() / 60) === 8 ? '8' : '0'
     }).then((res: any) => {
@@ -124,34 +125,39 @@ export default class apikeys extends Vue {
           this.tableData.push(res.data[i])
         }
       } else {
-        this.$message.error('Failed to connect.')
+        this.$message.error(failedConnect)
       }
     }).catch(err => {
       console.log(err)
-      this.$message.error('Failed to connect.')
+      this.$message.error(failedConnect)
     })
   }
 
   // 删除
   handleDelete (index: any, row: any) {
-    this.$confirm('Are you sure you want to delete this project?', {
-      confirmButtonText: 'Delete',
-      cancelButtonText: 'Cancel',
+    const text: any = this.$t('apiKey.deleteProject')
+    const deleteText: any = this.$t('apiKey.delete')
+    const cancelText: any = this.$t('apiKey.cancel')
+    const deleteSuccess: any = this.$t('apiKey.deleteSuccess')
+    const deleteFail: any = this.$t('apiKey.deleteFail')
+    this.$confirm(text, {
+      confirmButtonText: deleteText,
+      cancelButtonText: cancelText,
       customClass: 'message-box-delete'
     }).then(() => {
       apiDeleteProject(row.id).then((res: any) => {
         if (res.code === '200') {
           this.$message({
             type: 'success',
-            message: 'Deleted!'
+            message: deleteSuccess
           })
           this.tableData.splice(index, 1)
         } else {
-          this.$message.error('Deleted Failed.')
+          this.$message.error(deleteFail)
         }
       }).catch(err => {
         console.log(err)
-        this.$message.error('Deleted Failed.')
+        this.$message.error(deleteFail)
       })
     })
   }
@@ -163,6 +169,7 @@ export default class apikeys extends Vue {
 
   // 点击复制input中的内容
   copy (id: string) {
+    const copiedFail: any = this.$t('webhooks.copiedFail')
     clearTimeout(this.copyTimer)
     var clipboard = new Clipboard(id)
     clipboard.on('success', () => {
@@ -175,7 +182,7 @@ export default class apikeys extends Vue {
       clipboard.destroy()
     })
     clipboard.on('error', () => {
-      this.$message.error('Copied Failed.')
+      this.$message.error(copiedFail)
       clipboard.destroy()
     })
   }
@@ -184,7 +191,8 @@ export default class apikeys extends Vue {
   formatterDate (row: any, column: any) {
     const val = row[column.property]
     if (!val) return ''
-    return dayjs(val).format('MMM D,YYYY HH:mm')
+    const day = this.$i18n.locale === 'en' ? dayjs(val).format('MMM D,YYYY HH:mm') : dayjs(val).format('YYYY年MM月DD日 HH:mm')
+    return day
   }
 
   // 用*隐藏值

+ 13 - 11
src/views/projects/api/edit.vue

@@ -1,14 +1,14 @@
 <template>
   <div class="apiKeys-page edit">
-    <h2>Edit Project</h2>
+    <h2>{{ $t('apiKey.editProject') }}</h2>
     <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-position="top" label-width="100px">
-      <el-form-item label="Project Name" prop="projectName">
-        <el-input v-model="ruleForm.projectName" auto-complete="off" placeholder="Please enter project name"></el-input>
-        <span v-show="showErrTip" class="err-tip">A project with the same name already exists.</span>
+      <el-form-item :label="$t('apiKey.projectName')" prop="projectName">
+        <el-input v-model="ruleForm.projectName" auto-complete="off" :placeholder="$t('apiKey.enterName')"></el-input>
+        <span v-show="showErrTip" class="err-tip">{{ $t('apiKey.exists') }}</span>
       </el-form-item>
       <el-form-item class="btn-group">
-        <el-button @click="closePage">Cancel</el-button>
-        <el-button type="primary" :disabled="!ruleForm.projectName" @click="save">Save</el-button>
+        <el-button @click="closePage">{{ $t('apiKey.cancel') }}</el-button>
+        <el-button type="primary" :disabled="!ruleForm.projectName" @click="save">{{ $t('apiKey.save') }}</el-button>
       </el-form-item>
     </el-form>
   </div>
@@ -18,14 +18,14 @@
 import { Vue, Component, Watch } from 'vue-property-decorator'
 import { apiEditProject } from '@/request/api'
 
-interface IruleForm{
+interface isRuleForm{
   projectName: string|Array<string|null>
 }
 
 @Component
 export default class apiEdit extends Vue {
   id = ''
-  ruleForm: IruleForm = {
+  ruleForm: isRuleForm = {
     projectName: ''
   }
 
@@ -63,22 +63,24 @@ export default class apiEdit extends Vue {
 
   // 编辑项目
   editProject () {
+    const success: any = this.$t('apiKey.editSuccess')
+    const fail: any = this.$t('apiKey.editFail')
     apiEditProject({
       id: this.id,
       projectName: this.ruleForm.projectName
     }, {}).then((res: any) => {
       if (res.code === '200') {
         this.$message({
-          message: 'Edited Successfully!',
+          message: success,
           type: 'success'
         });
         (this.$parent as any).getKeysList()
         this.closePage()
       } else if (res.code === '-1') {
         this.showErrTip = true
-        this.$message.error('Edit Failed.')
+        this.$message.error(fail)
       } else {
-        this.$message.error('Edit Failed.')
+        this.$message.error(fail)
       }
     }).catch(err => {
       this.$message.error(err)

+ 86 - 98
src/views/projects/dashboard.vue

@@ -1,67 +1,37 @@
 <template>
-  <div class="dashboard container">
-    <h1>Dashboard</h1>
+  <div class="dashboard container" @click="showCalender = false, timeSelected = oldTimeSelectVal">
+    <h1>{{ $t('dashboard.title') }}</h1>
     <Progress ref="progress" class="process" :init="false" />
     <div class="board block">
       <div class="top-select">
         <ul class="select-time">
-          <li class="time-box" :class="{'time-selected': timeSelected === 1}" @click="selectTime(1)">Last 24H</li>
-          <li class="time-box" :class="{'time-selected': timeSelected === 7}" @click="selectTime(7)">Last Week</li>
-          <li class="time-box" :class="{'time-selected': timeSelected === 30}" @click="selectTime(30)">Last Month</li>
+          <li class="time-box" :class="{'time-selected': timeSelected === 1}" @click="selectTime(1)">{{ $t('dashboard.day') }}</li>
+          <li class="time-box" :class="{'time-selected': timeSelected === 7}" @click="selectTime(7)">{{ $t('dashboard.week') }}</li>
+          <li class="time-box" :class="{'time-selected': timeSelected === 30}" @click="selectTime(30)">{{ $t('dashboard.month') }}</li>
           <li class="time-box custom" :class="{'time-selected': timeSelected === 4}">
-            <!-- <el-date-picker
-              v-model="datePickerValue"
-              format="MM dd,yyyy"
-              value-format="yyyy-MM-dd"
-              type="daterange"
-              :editable="false"
-              :clearable="false"
-              :unlink-panels="true"
-              :range-separator="timeSelected !== 4 || datePickerValue.length === 0 ? 'Custom' : '-'"
-              :class="{'picked' : datePickerValue.length !== 0}"
-              ref="date-picker">
-            </el-date-picker> -->
-            <span @click="selectTime(4)">{{( datePickerValue.length === 2 ? fillerDate(datePickerValue[0]).replace(/,/g, ", ") + ' - ' + fillerDate(datePickerValue[1]).replace(/,/g, ", ") : 'Custom' )}}</span>
+            <span @click.stop="selectTime(4)">{{( datePickerValue.length === 2 ? fillerDate(datePickerValue[0]).replace(/,/g, ", ") + ' - ' + fillerDate(datePickerValue[1]).replace(/,/g, ", ") : $t('dashboard.custom') )}}</span>
             <Calender v-show="showCalender" @checkedDate="checkedDate" :userFirstLogin="userFirstLogin"></Calender>
           </li>
         </ul>
         <div class="select-type">
           <span>
-            <span class="max">View by</span>
-            <!-- <el-select v-model="projectValue" size="mini" placeholder="All Projects">
-              <el-option label="All Projects" value=""></el-option>
-              <el-option
-                v-for="item in projects"
-                :key="item.projectId"
-                :label="item.projectName"
-                :value="item.projectId">
-              </el-option>
-            </el-select>
-            <el-select v-model="toolValue" size="mini" placeholder="All Tools" style="margin: 0 16px;">
-              <el-option label="All Tools" value=""></el-option>
-              <el-option
-                v-for="item in tools"
-                :key="item.id"
-                :label="item.toolName"
-                :value="item.id">
-              </el-option>
-            </el-select> -->
-            <select v-model="projectValue" placeholder="All Projects" name="projects">
-              <option value="">All Projects</option>
-              <option v-for="item in projects" :key="item.projectId" :value="item.projectId">{{ item.projectName }}</option>
+            <span class="max">{{ $t('dashboard.view') }}</span>
+            <select v-model="projectValue" :placeholder="$t('dashboard.project')['All Projects']" name="projects">
+              <option value="">{{ $t('dashboard.project')['All Projects'] }}</option>
+              <option v-for="item in projects" :key="item.projectId" :value="item.projectId">{{ $t('dashboard.project')[item.projectName] }}</option>
             </select>
-            <select v-model="toolValue" placeholder="All Tools" name="tools">
-              <option value="">All Tools</option>
-              <option v-for="item in tools" :key="item.id" :value="item.id">{{ item.toolName }}</option>
+            <select v-model="toolValue" :placeholder="$t('dashboard.project.allTools')" name="tools">
+              <option value="">{{ $t('dashboard.allTools') }}</option>
+              <option v-for="item in tools" :key="item.id" :value="item.id">{{ $t('dashboard.tools')[item.toolName] }}</option>
             </select>
           </span>
-          <a @click="exportData">
+          <div @click="exportData">
             <picture>
               <source type="image/png" media="(min-width: 430px)" srcset="../../../static/images/dashboard/download@2x.png">
               <source type="image/png" media="(max-width: 429px)" srcset="../../../static/images/dashboard/download.png">
               <img src="../../../static/images/dashboard/download@2x.png" alt="download">
             </picture>
-          </a>
+          </div>
         </div>
       </div>
       <div class="detail-data">
@@ -70,8 +40,8 @@
           <img v-else src="../../../static/images/dashboard/data_blue@2x.png" alt="data_blue">
           <div>
             <div>
-              <span>Successful Requests</span>
-              <el-tooltip class="item" effect="dark" content="The sum of the files that have been successfully processed to completion." placement="bottom">
+              <span>{{ $t('dashboard.successReq') }}</span>
+              <el-tooltip class="item" effect="dark" :content="$t('dashboard.successReqTip')" placement="bottom">
                 <img src="@/assets/images/common/info_white.svg" alt="info">
               </el-tooltip>
             </div>
@@ -83,8 +53,8 @@
           <img v-else src="../../../static/images/dashboard/data_red@2x.png" alt="data_red">
           <div>
             <div>
-              <span>Error Requests</span>
-              <el-tooltip class="item" effect="dark" content="The sum of the files that failed to be processed." placement="bottom">
+              <span>{{ $t('dashboard.errorReq') }}</span>
+              <el-tooltip class="item" effect="dark" :content="$t('dashboard.errorReqTip')" placement="bottom">
                 <img src="@/assets/images/common/info_white.svg" alt="info">
               </el-tooltip>
             </div>
@@ -96,8 +66,8 @@
           <img v-else src="../../../static/images/dashboard/data_yellow@2x.png" alt="data_yellow">
           <div>
             <div>
-              <span>Error Ratio</span>
-              <el-tooltip class="item" effect="dark" content="The percentage of the files that failed to process." placement="bottom">
+              <span>{{ $t('dashboard.errorRatio') }}</span>
+              <el-tooltip class="item" effect="dark" :content="$t('dashboard.errorRatioTip')" placement="bottom">
                 <img src="@/assets/images/common/info_white.svg" alt="info">
               </el-tooltip>
             </div>
@@ -109,10 +79,10 @@
           <img v-else src="../../../static/images/dashboard/data_purple@2x.png" alt="data_purple">
           <div>
             <div>
-              <span>Average Process Time</span>
+              <span>{{ $t('dashboard.average') }}</span>
               <el-tooltip class="item" effect="dark" placement="bottom">
-                <div slot="content">Average Process Time is the average processing time of the original file from the beginning to the end. <br/><br/>
-                  <b>Attention Please:</b> Average Process Time may be affected by the size of the file you are uploading and the network environment.</div>
+                <div slot="content">{{ $t('dashboard.averageTip')[0] }}<br/><br/>
+                  <span>{{ $t('dashboard.averageTip')[1] }}</span>{{ $t('dashboard.averageTip')[2] }}</div>
                 <img src="@/assets/images/common/info_white.svg" alt="info">
               </el-tooltip>
             </div>
@@ -121,7 +91,7 @@
         </div>
       </div>
       <div class="chart" ref="chart"></div>
-      <p class="stats-text">Stats are updated every hour</p>
+      <p class="stats-text">{{ $t('dashboard.updated') }}</p>
     </div>
     <div class="cover" v-show="showCalender" @click.self="selectTime(4)"></div>
   </div>
@@ -130,6 +100,7 @@
 <script lang="ts">
 import { Vue, Component, Watch } from 'vue-property-decorator'
 import dayjs from 'dayjs'
+import 'dayjs/locale/zh-cn'
 import Progress from '@/components/progress/Progress.vue'
 import Calender from '@/components/calendar/calendar.vue'
 import {
@@ -141,15 +112,15 @@ import {
   getUserFirstLogin
 } from '@/request/api'
 
-interface Iprojects{
+interface isProjects{
   projectId: number,
   projectName: string
 }
-interface Itools{
+interface isTools{
   id: number,
   toolName: string
 }
-interface IanalysisData{
+interface isAnalysisData{
   fileTotal?: number,
   successfulRequest?: number,
   errorRequest?: number,
@@ -164,15 +135,15 @@ interface IanalysisData{
   }
 })
 export default class dashBoard extends Vue {
-  projects: Array<Iprojects> = []
+  projects: Array<isProjects> = []
   projectValue = ''
-  tools: Array<Itools> = []
+  tools: Array<isTools> = []
   toolValue = ''
   timeSelected = 1
   oldTimeSelectVal = 1
   dataSelected = 1
   datePickerValue: Array<string> = []
-  analysisData:IanalysisData = {}
+  analysisData:isAnalysisData = {}
   xData: Array<string> = []
   yData: Array<string> = []
   yDataUnit = ''
@@ -185,6 +156,9 @@ export default class dashBoard extends Vue {
     const el = this.$refs.progress as any
     el && el.getFileAmount()
     this.getFirstLoginTime()
+    this.$watch('$i18n.locale', (newVal: string, oldVal: string) => {
+      this.selectData(this.dataSelected, true)
+    })
   }
 
   @Watch('datePickerValue')
@@ -208,7 +182,8 @@ export default class dashBoard extends Vue {
   }
 
   fillerDate (val: string) {
-    return dayjs(val).format('MMM D,YYYY ddd')
+    const day = this.$i18n.locale === 'en' ? dayjs(val).format('MMM D,YYYY ddd') : dayjs(val).locale('zh-cn').format('YYYY年MM月DD日 ddd')
+    return day
   }
 
   // 选择时间段
@@ -236,8 +211,8 @@ export default class dashBoard extends Vue {
   }
 
   // 选择数据类型
-  selectData (index: number) {
-    if (this.dataSelected === index) return
+  selectData (index: number, change: boolean) {
+    if (this.dataSelected === index && !change) return
     this.dataSelected = index
     this.getChartsData(index)
     this.getFourAnalysisData()
@@ -259,9 +234,9 @@ export default class dashBoard extends Vue {
     const self = this
     myChart.setOption({
       title: {
-        text: 'Successful Requests',
+        text: this.$t('dashboard.successReq'),
         textStyle: {
-          fontFamily: 'Helvetica',
+          fontFamily: 'Poppins',
           fontStyle: 'normal',
           color: '#18191B',
           fontWeight: '700',
@@ -338,10 +313,10 @@ export default class dashBoard extends Vue {
       },
       tooltip: {
         trigger: 'axis',
-        formatter: '<span style="color: #518CFF; font-weight: 700; font-size: 18px; line-height: 24px;">{c}</span><br />{b}',
+        formatter: '<span style="color: #518CFF; font-weight: 700; font-size: 18px; line-height: 24px;">{c}</span><br /><span style="color: #6A6F77; font-size: 14px; line-height: 20px;">{b}</span>',
         textStyle: {
           color: '#6A6F77',
-          fontFamily: 'Helvetica',
+          fontFamily: 'Poppins',
           fontStyle: 'normal',
           fontWeight: 400,
           fontSize: 12,
@@ -376,7 +351,7 @@ export default class dashBoard extends Vue {
           borderWidth: 4
         },
         textStyle: {
-          fontFamily: 'Helvetica',
+          fontFamily: 'Poppins',
           fontStyle: 'normal',
           color: '#43474D',
           fontSize: 12,
@@ -390,7 +365,7 @@ export default class dashBoard extends Vue {
     if (this.dataSelected === 2) {
       myChart.setOption({
         title: {
-          text: 'Error Requests'
+          text: this.$t('dashboard.errorReq')
         },
         xAxis: {
           axisPointer: {
@@ -423,7 +398,7 @@ export default class dashBoard extends Vue {
     } else if (this.dataSelected === 3) {
       myChart.setOption({
         title: {
-          text: 'Error Ratio'
+          text: this.$t('dashboard.errorRatio')
         },
         xAxis: {
           axisPointer: {
@@ -456,7 +431,7 @@ export default class dashBoard extends Vue {
     } else if (this.dataSelected === 4) {
       myChart.setOption({
         title: {
-          text: 'Average Process Time'
+          text: this.$t('dashboard.average')
         },
         xAxis: {
           axisPointer: {
@@ -494,6 +469,7 @@ export default class dashBoard extends Vue {
 
   // 获取当前用户下所有project
   getProjectsList () {
+    const failedConnect: any = this.$t('failedConnect')
     getProjects({}).then((res: any) => {
       if (res.code === '200') {
         this.projects = []
@@ -505,12 +481,13 @@ export default class dashBoard extends Vue {
       }
     }).catch(err => {
       console.log(err)
-      this.$message.error('Failed to connect.')
+      this.$message.error(failedConnect)
     })
   }
 
   // 获取工具列表
   getToolsList () {
+    const failedConnect: any = this.$t('failedConnect')
     getTools({}).then((res: any) => {
       if (res.code === '200') {
         this.tools = []
@@ -522,12 +499,13 @@ export default class dashBoard extends Vue {
       }
     }).catch(err => {
       console.log(err)
-      this.$message.error('Failed to connect.')
+      this.$message.error(failedConnect)
     })
   }
 
   // 获取4个点统计信息
   getFourAnalysisData () {
+    const failedConnect: any = this.$t('failedConnect')
     getAnalysisData({
       days: this.timeSelected === 4 ? '' : this.timeSelected,
       startDateTime: this.datePickerValue[0],
@@ -544,12 +522,13 @@ export default class dashBoard extends Vue {
       }
     }).catch(err => {
       console.log(err)
-      this.$message.error('Failed to connect.')
+      this.$message.error(failedConnect)
     })
   }
 
   // 获取图表数据
   getChartsData (index: number) {
+    const failedConnect: any = this.$t('failedConnect')
     getDetailedData(index, {
       days: this.timeSelected === 4 ? '' : this.timeSelected,
       startDateTime: this.datePickerValue[0],
@@ -577,12 +556,13 @@ export default class dashBoard extends Vue {
       }
     }).catch(err => {
       console.log(err)
-      this.$message.error('Failed to connect.')
+      this.$message.error(failedConnect)
     })
   }
 
   // 导出报表
   exportData () {
+    const cn = this.$i18n.locale === 'zh-cn'
     getExport({
       days: this.timeSelected === 4 ? '' : this.timeSelected,
       startDateTime: this.datePickerValue[0],
@@ -590,7 +570,8 @@ export default class dashBoard extends Vue {
       queryType: this.timeSelected === 4 ? 1 : 0,
       projectId: this.projectValue,
       toolId: this.toolValue,
-      timeZone: (0 - new Date().getTimezoneOffset() / 60) === 8 ? '8' : '0'
+      timeZone: (0 - new Date().getTimezoneOffset() / 60) === 8 ? '8' : '0',
+      isCN: cn
     }, {
       responseType: 'blob'
     }).then((res: any) => {
@@ -598,7 +579,10 @@ export default class dashBoard extends Vue {
       const downloadElement = document.createElement('a') // 创建a标签
       const href = window.URL.createObjectURL(blob) // 创建下载的链接
       downloadElement.href = href
-      downloadElement.download = 'chart.xlsx' // 下载后文件名
+      const year = dayjs().format('YYYY') + '年'
+      const month = dayjs().format('MM') + '月'
+      const day = dayjs().format('DD') + '日'
+      downloadElement.download = 'API_Requests_Report' + year + month + day + '.xlsx' // 下载后文件名
       document.body.appendChild(downloadElement)
       downloadElement.click() // 点击下载
       document.body.removeChild(downloadElement) // 下载完成移除元素
@@ -620,40 +604,40 @@ export default class dashBoard extends Vue {
     let minute: number|string = dt.getMinutes()
     switch (month) {
       case 1:
-        month = 'Jan'
+        month = `${this.$t('calendar.month.Jan')}`
         break
       case 2:
-        month = 'Feb'
+        month = `${this.$t('calendar.month.Feb')}`
         break
       case 3:
-        month = 'Mar'
+        month = `${this.$t('calendar.month.Mar')}`
         break
       case 4:
-        month = 'Apr'
+        month = `${this.$t('calendar.month.Apr')}`
         break
       case 5:
-        month = 'May'
+        month = `${this.$t('calendar.month.May')}`
         break
       case 6:
-        month = 'Jun'
+        month = `${this.$t('calendar.month.Jun')}`
         break
       case 7:
-        month = 'Jul'
+        month = `${this.$t('calendar.month.Jul')}`
         break
       case 8:
-        month = 'Aug'
+        month = `${this.$t('calendar.month.Aug')}`
         break
       case 9:
-        month = 'Sept'
+        month = `${this.$t('calendar.month.Sept')}`
         break
       case 10:
-        month = 'Oct'
+        month = `${this.$t('calendar.month.Oct')}`
         break
       case 11:
-        month = 'Nov'
+        month = `${this.$t('calendar.month.Nov')}`
         break
       case 12:
-        month = 'Dec'
+        month = `${this.$t('calendar.month.Dec')}`
         break
     }
     hour = hour < 10 ? '0' + hour : hour
@@ -698,7 +682,7 @@ export default class dashBoard extends Vue {
           padding: 6px 12px;
           font-size: 14px;
           line-height: 16px;
-          color: #43474D;
+          color: #52555F;
           margin-right: 4px;
           cursor: pointer;
           &:last-child {
@@ -726,7 +710,7 @@ export default class dashBoard extends Vue {
           font-size: 14px;
           line-height: 16px;
           font-weight: 700;
-          color: #43474D;
+          color: #52555F;
           margin-right: 16px;
           ::v-deep .el-input__inner {
             font-weight: 400;
@@ -743,6 +727,10 @@ export default class dashBoard extends Vue {
         select[name='projects'] {
           margin-right: 16px;
         }
+        div {
+          cursor: pointer;
+          display: inline-block;
+        }
       }
     }
     .detail-data {
@@ -779,7 +767,7 @@ export default class dashBoard extends Vue {
             span {
               font-size: 14px;
               line-height: 20px;
-              color: #6A6F77;
+              color: #52555F;
             }
             img {
               margin-left: 4px;
@@ -787,8 +775,8 @@ export default class dashBoard extends Vue {
           }
           p {
             font-size: 20px;
-            line-height: 24px;
-            font-weight: 700;
+            line-height: 28px;
+            font-weight: 600;
           }
         }
         &.data-selected {
@@ -809,10 +797,10 @@ export default class dashBoard extends Vue {
       }
     }
     .stats-text {
-      text-align: left;
       font-size: 14px;
-      line-height: 1;
-      color: #43474D;
+      text-align: left;
+      color: #52555F;
+      line-height: 20px;
       margin-top: -20px;
     }
   }

+ 32 - 30
src/views/projects/user/add.vue

@@ -1,38 +1,35 @@
 <template>
   <div class="webhooks-page add">
-    <h2>Add New Webhook</h2>
+    <h2>{{ $t('webhooks.addWebhook') }}</h2>
     <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-position="top" label-width="100px">
-      <el-form-item label="Webhook URL" prop="url">
+      <el-form-item :label="$t('webhooks.webhookURL')" prop="url">
         <template slot="label">
-          <span>Webhook URL</span>
-          <span class="rule-tip">Provide Webhook URL to receive requests</span>
+          <span>{{ $t('webhooks.webhookURL') }}</span>
+          <span class="rule-tip">{{ $t('webhooks.urlTip') }}</span>
         </template>
         <el-input v-model="ruleForm.url" auto-complete="off" placeholder="http://" @blur="validUrl" :class="{'err-border': showErrTipUrl}"></el-input>
-        <span v-show="showErrTipUrl" class="err-tip">Please enter a valid URL.</span>
+        <span v-show="showErrTipUrl" class="err-tip">{{ $t('webhooks.urlValid') }}</span>
       </el-form-item>
       <el-form-item prop="events" class="label-with-info checkbox">
         <template slot="label">
-          <span>Events</span>
-          <el-tooltip class="item" effect="dark" content="When the events which you selected occur, you'll will recieve the notification on your App." placement="bottom">
+          <span>{{ $t('webhooks.events') }}</span>
+          <el-tooltip class="item" effect="dark" :content="$t('webhooks.eventTip')" placement="bottom">
             <img src="@/assets/images/common/info_white.svg" alt="info">
           </el-tooltip>
         </template>
         <el-checkbox-group v-model="ruleForm.events">
-          <el-checkbox v-for="item in eventsList" :key="item.id" :label="item.id">{{ item.eventName }}</el-checkbox>
+          <el-checkbox v-for="item in eventsList" :key="item.id" :label="item.id">{{ $t('webhooks')[item.eventName] }}</el-checkbox>
         </el-checkbox-group>
       </el-form-item>
-      <el-form-item label="Project" class="select-project">
-        <!-- <el-select v-model="ruleForm.projectId" placeholder="All Projects">
-          <el-option v-for="item in projects" :key="item.projectId" :label="item.projectName" :value="item.projectId"></el-option>
-        </el-select> -->
-        <select v-model="ruleForm.projectId" placeholder="All Projects" name="projects">
-          <option value="">All Projects</option>
-          <option v-for="item in projects" :key="item.projectId" :value="item.projectId">{{ item.projectName }}</option>
+      <el-form-item :label="$t('webhooks.project')" class="select-project">
+        <select v-model="ruleForm.projectId" :placeholder="$t('webhooks.allProjects')" name="projects">
+          <option value="">{{ $t('webhooks.allProjects') }}</option>
+          <option v-for="item in projects" :key="item.projectId" :value="item.projectId">{{ $t('webhooks')[item.projectName] }}</option>
         </select>
       </el-form-item>
       <el-form-item class="btn-group">
-        <el-button @click="closePage">Cancel</el-button>
-        <el-button type="primary" :disabled="submitBtnState('ruleForm')" @click="submitForm('ruleForm')">Save</el-button>
+        <el-button @click="closePage">{{ $t('webhooks.cancel') }}</el-button>
+        <el-button type="primary" :disabled="submitBtnState('ruleForm')" @click="submitForm('ruleForm')">{{ $t('webhooks.save') }}</el-button>
       </el-form-item>
     </el-form>
   </div>
@@ -43,12 +40,12 @@ import { Vue, Component, Watch } from 'vue-property-decorator'
 import { apiAddNewWebhook, getEventList, getProjects } from '@/request/api'
 import { Form } from 'element-ui'
 
-interface IeventsList{
+interface isEventsList{
   id: number,
   triggerEvent: number,
   eventName: string
 }
-interface Iprojects{
+interface isProjects{
   projectId: number,
   projectName: string
 }
@@ -64,15 +61,15 @@ export default class userAdd extends Vue {
 
   rules = {
     url: [
-      { required: true, message: 'Webhook URL cannot be blank', trigger: 'change' }
+      { required: true, message: this.$t('webhooks.urlErr'), trigger: 'change' }
     ],
     events: [
-      { type: 'array', required: true, message: 'Please select at least one event', trigger: 'change' }
+      { type: 'array', required: true, message: this.$t('webhooks.eventErr'), trigger: 'change' }
     ]
   }
 
-  eventsList: Array<IeventsList> = []
-  projects: Array<Iprojects> = []
+  eventsList: Array<isEventsList> = []
+  projects: Array<isProjects> = []
   showErrTipUrl = false
 
   created () {
@@ -92,6 +89,7 @@ export default class userAdd extends Vue {
   // 保存
   save () {
     this.validUrl()
+    const msg: any = this.$t('webhooks.createInvalid')
     if (this.showErrTipUrl) {
       this.$message.error('Create Invalid!')
     } else {
@@ -118,6 +116,7 @@ export default class userAdd extends Vue {
 
   // 获取系统事件(添加和编辑时调用)
   getEvents () {
+    const failedConnect: any = this.$t('failedConnect')
     getEventList({}).then((res: any) => {
       if (res.code === '200') {
         this.eventsList = []
@@ -125,16 +124,18 @@ export default class userAdd extends Vue {
           this.eventsList.push(res.data[i])
         }
       } else {
-        this.$message.error('Failed to connect.')
+        this.$message.error(failedConnect)
       }
     }).catch(err => {
       console.log(err)
-      this.$message.error('Failed to connect.')
+      this.$message.error(failedConnect)
     })
   }
 
   // 添加新项目
   createNewWebhook () {
+    const success: any = this.$t('webhooks.createSuccess')
+    const fail: any = this.$t('webhooks.createFail')
     apiAddNewWebhook({
       projectId: this.ruleForm.projectId,
       url: this.ruleForm.url,
@@ -142,17 +143,17 @@ export default class userAdd extends Vue {
     }, {}).then((res: any) => {
       if (res.code === '200') {
         this.$message({
-          message: 'Create successfully!',
+          message: success,
           type: 'success'
         })
         this.closePage();
         (this.$parent as any).getList()
       } else {
-        this.$message.error('Create Failed.')
+        this.$message.error(fail)
       }
     }).catch(err => {
       console.log(err)
-      this.$message.error('Create Failed.')
+      this.$message.error(fail)
     })
   }
 
@@ -174,6 +175,7 @@ export default class userAdd extends Vue {
 
   // 获取当前用户下所有project
   getProjectsList () {
+    const failedConnect: any = this.$t('failedConnect')
     getProjects({}).then((res: any) => {
       if (res.code === '200') {
         this.projects = []
@@ -181,11 +183,11 @@ export default class userAdd extends Vue {
           this.projects.push(res.data[i])
         }
       } else {
-        this.$message.error('Failed to connect.')
+        this.$message.error(failedConnect)
       }
     }).catch(err => {
       console.log(err)
-      this.$message.error('Failed to connect.')
+      this.$message.error(failedConnect)
     })
   }
 

+ 44 - 40
src/views/projects/user/edit.vue

@@ -1,39 +1,36 @@
 <template>
   <div class="webhooks-page edit">
-    <h2>Edit Webhook</h2>
+    <h2>{{ $t('webhooks.editWebhook') }}</h2>
     <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-position="top" label-width="100px">
-      <el-form-item label="Webhook URL" prop="url">
+      <el-form-item :label="$t('webhooks.webhookURL')" prop="url">
         <template slot="label">
-          <span>Webhook URL</span>
-          <span class="rule-tip">Provide Webhook URL to receive requests</span>
+          <span>{{ $t('webhooks.webhookURL') }}</span>
+          <span class="rule-tip">{{ $t('webhooks.urlTip') }}</span>
         </template>
-        <el-input v-model="ruleForm.url" auto-complete="off" placeholder="Provide Webhook URL to receive requests"
+        <el-input v-model="ruleForm.url" auto-complete="off" :placeholder="$t('webhooks.urlTip')"
         :class="{'err-border': showErrTipUrl}"></el-input>
-        <span v-show="showErrTipUrl" class="err-tip">Please enter a valid URL.</span>
+        <span v-show="showErrTipUrl" class="err-tip">{{ $t('webhooks.urlValid') }}</span>
       </el-form-item>
       <el-form-item prop="events" class="label-with-info checkbox">
         <template slot="label">
-          <span>Events</span>
-          <el-tooltip class="item" effect="dark" content="When the events which you selected occur, you'll will recieve the notification on your App." placement="bottom">
+          <span>{{ $t('webhooks.events') }}</span>
+          <el-tooltip class="item" effect="dark" :content="$t('webhooks.eventTip')" placement="bottom">
             <img src="@/assets/images/common/info_white.svg" alt="info">
           </el-tooltip>
         </template>
         <el-checkbox-group v-model="ruleForm.events">
-          <el-checkbox v-for="item in eventsList" :key="item.id" :label="item.id">{{ item.eventName }}</el-checkbox>
+          <el-checkbox v-for="item in eventsList" :key="item.id" :label="item.id">{{ $t('webhooks')[item.eventName] }}</el-checkbox>
         </el-checkbox-group>
       </el-form-item>
-      <el-form-item label="Project" class="select-project">
-        <!-- <el-select v-model="ruleForm.projectId" placeholder="All Projects">
-          <el-option v-for="item in projects" :key="item.projectId" :label="item.projectName" :value="item.projectId"></el-option>
-        </el-select> -->
-        <select v-model="ruleForm.projectId" placeholder="All Projects" name="projects">
-          <option value="">All Projects</option>
-          <option v-for="item in projects" :key="item.projectId" :value="item.projectId">{{ item.projectName }}</option>
+      <el-form-item :label="$t('webhooks.project')" class="select-project">
+        <select v-model="ruleForm.projectId" :placeholder="$t('webhooks.allProjects')" name="projects">
+          <option value="">{{ $t('webhooks.allProjects') }}</option>
+          <option v-for="item in projects" :key="item.projectId" :value="item.projectId">{{ $t('webhooks')[item.projectName] }}</option>
         </select>
       </el-form-item>
       <el-form-item class="btn-group">
-        <el-button @click="closePage">Cancel</el-button>
-        <el-button type="primary" :disabled="submitBtnState('ruleForm')" @click="submitForm('ruleForm')">Save</el-button>
+        <el-button @click="closePage">{{ $t('webhooks.cancel') }}</el-button>
+        <el-button type="primary" :disabled="submitBtnState('ruleForm')" @click="submitForm('ruleForm')">{{ $t('webhooks.save') }}</el-button>
       </el-form-item>
     </el-form>
   </div>
@@ -44,30 +41,30 @@ import { Vue, Component, Watch } from 'vue-property-decorator'
 import { apiEditWebhook, getEventList, getEnableEventIdList, getProjects, getWebhookInfo } from '@/request/api'
 import { Form } from 'element-ui'
 
-interface IeventsList{
+interface isEventsList{
   id: number,
   triggerEvent: number,
   eventName: string
 }
-interface Iprojects{
+interface isProjects{
   projectId: number,
   projectName: string
 }
-interface Ievents{
+interface isEvents{
   id?: number,
   triggerEvent?: number,
   eventName?: string
 }
-interface IruleForm{
+interface isRuleForm{
   url: string,
   projectId: string,
-  events: Array<Ievents>
+  events: Array<isEvents>
 }
 
 @Component
 export default class userEdit extends Vue {
   id = ''
-  ruleForm: IruleForm= {
+  ruleForm: isRuleForm= {
     url: '',
     projectId: '',
     events: []
@@ -75,15 +72,15 @@ export default class userEdit extends Vue {
 
   rules = {
     url: [
-      { required: true, message: 'Webhook URL cannot be blank', trigger: 'change' }
+      { required: true, message: this.$t('webhooks.urlErr'), trigger: 'change' }
     ],
     events: [
-      { type: 'array', required: true, message: 'Please select at least one event.', trigger: 'change' }
+      { type: 'array', required: true, message: this.$t('webhooks.eventErr'), trigger: 'change' }
     ]
   }
 
-  eventsList: Array<IeventsList> = []
-  projects: Array<Iprojects> = []
+  eventsList: Array<isEventsList> = []
+  projects: Array<isProjects> = []
   showErrTipUrl = false
 
   created () {
@@ -106,8 +103,9 @@ export default class userEdit extends Vue {
   // 保存
   save () {
     this.validUrl()
+    const msg: any = this.$t('webhooks.editInvalid')
     if (this.showErrTipUrl) {
-      this.$message.error('Edit Invalid!')
+      this.$message.error(msg)
     } else {
       this.editWebhook()
     }
@@ -132,6 +130,7 @@ export default class userEdit extends Vue {
 
   // 获取系统事件(添加和编辑时调用)
   getEvents () {
+    const msg: any = this.$t('webhooks.failedConnect')
     getEventList({}).then((res: any) => {
       if (res.code === '200') {
         this.eventsList = []
@@ -139,16 +138,17 @@ export default class userEdit extends Vue {
           this.eventsList.push(res.data[i])
         }
       } else {
-        this.$message.error('Failed to connect.')
+        this.$message.error(msg)
       }
     }).catch(err => {
       console.log(err)
-      this.$message.error('Failed to connect.')
+      this.$message.error(msg)
     })
   }
 
   // 查询已勾选事件id(编辑时调用)
   getCheckedEvents () {
+    const msg: any = this.$t('webhooks.failedConnect')
     getEnableEventIdList(this.id, {}).then((res: any) => {
       if (res.code === '200') {
         this.ruleForm.events = []
@@ -156,16 +156,18 @@ export default class userEdit extends Vue {
           this.ruleForm.events.push(res.data[i])
         }
       } else {
-        this.$message.error('Failed to connect.')
+        this.$message.error(msg)
       }
     }).catch(err => {
       console.log(err)
-      this.$message.error('Failed to connect.')
+      this.$message.error(msg)
     })
   }
 
   // 编辑项目
   editWebhook () {
+    const success: any = this.$t('webhooks.editSuccess')
+    const fail: any = this.$t('webhooks.editFail')
     apiEditWebhook({
       id: this.id,
       projectId: this.ruleForm.projectId,
@@ -174,17 +176,17 @@ export default class userEdit extends Vue {
     }, {}).then((res: any) => {
       if (res.code === '200') {
         this.$message({
-          message: 'Edited Successfully!',
+          message: success,
           type: 'success'
         });
         (this.$parent as any).getList()
         this.closePage()
       } else {
-        this.$message.error('Edit Failed.')
+        this.$message.error(fail)
       }
     }).catch(err => {
       console.log(err)
-      this.$message.error('Edit Failed.')
+      this.$message.error(fail)
     })
   }
 
@@ -206,6 +208,7 @@ export default class userEdit extends Vue {
 
   // 获取当前用户下所有project
   getProjectsList () {
+    const msg: any = this.$t('webhooks.failedConnect')
     getProjects({}).then((res: any) => {
       if (res.code === '200') {
         this.projects = []
@@ -213,11 +216,11 @@ export default class userEdit extends Vue {
           this.projects.push(res.data[i])
         }
       } else {
-        this.$message.error('Failed to connect.')
+        this.$message.error(msg)
       }
     }).catch(err => {
       console.log(err)
-      this.$message.error('Failed to connect.')
+      this.$message.error(msg)
     })
   }
 
@@ -230,6 +233,7 @@ export default class userEdit extends Vue {
 
   // 根据webhookId查询projectId
   getWebhookInfo () {
+    const msg: any = this.$t('webhooks.failedConnect')
     getWebhookInfo(this.id, {}).then((res: any) => {
       if (res.code === '200') {
         if (res.data.projectId !== null) {
@@ -237,11 +241,11 @@ export default class userEdit extends Vue {
         }
         this.ruleForm.url = res.data.url
       } else {
-        this.$message.error('Failed to connect.')
+        this.$message.error(msg)
       }
     }).catch(err => {
       console.log(err)
-      this.$message.error('Failed to connect.')
+      this.$message.error(msg)
     })
   }
 }

+ 55 - 43
src/views/projects/user/webhooks.vue

@@ -1,36 +1,36 @@
 <template>
   <div class="webhooks container">
-    <h1>Webhooks</h1>
+    <h1>{{ $t('webhooks.title') }}</h1>
     <div class="block">
       <router-view></router-view>
-      <div class="top" v-show="showfather">
-        <h2>Webhooks
-          <el-tooltip class="item" effect="dark" content="Webhooks are reverse APIs that automatically send task or file information to the client you specify when our server updates it in the Webhooks paradigm. You can set up Webhooks to avoid periodic calls to APIs." placement="bottom">
+      <div class="top" v-show="showFather">
+        <h2>{{ $t('webhooks.title') }}
+          <el-tooltip class="item" effect="dark" :content="$t('webhooks.tips')" placement="bottom">
             <img src="@/assets/images/common/info_black.svg" alt="info">
           </el-tooltip>
         </h2>
         <router-link :to="{name: 'addWebhook'}">
           <el-button type="primary">
             <img src="@/assets/images/common/add.svg">
-            <span class="max">Add New Webhook</span>
+            <span class="max">{{ $t('webhooks.addWebhook') }}</span>
           </el-button>
         </router-link>
       </div>
-      <div class="keys-table" v-show="showfather">
+      <div class="keys-table" v-show="showFather">
         <el-table :data="tableData" style="width: 100%">
-          <el-table-column prop="url" label="URL" min-width="140">
+          <el-table-column prop="url" :label="$t('webhooks.url')" min-width="140">
             <template slot-scope="scope">
               <p class="url-p" :class="{'url-gray': scope.row.status === -1}" :title="scope.row.url">{{ scope.row.url }}</p>
             </template>
           </el-table-column>
-          <el-table-column prop="events" label="Events" class-name="events" min-width="210">
+          <el-table-column prop="events" :label="$t('webhooks.events')" class-name="events" min-width="210">
             <template slot-scope="scope">
               <span v-for="(item, index) in scope.row.events" :key="index">
                 <el-tag color="#F4F8FF">{{ changeEventTag(item) }}</el-tag>
               </span>
             </template>
           </el-table-column>
-          <el-table-column prop="status" label="Status" min-width="90">
+          <el-table-column prop="status" :label="$t('webhooks.status')" min-width="90">
             <template slot-scope="scope">
               <el-switch
                 v-model="scope.row.status"
@@ -42,23 +42,23 @@
               </el-switch>
             </template>
           </el-table-column>
-          <el-table-column prop="secretToken" label="" class-name="header-with-info" min-width="200">
+          <el-table-column prop="secretToken" class-name="header-with-info" min-width="200">
             <template slot="header">
-              <span>Secret Token</span>
-              <el-tooltip class="item" effect="dark" content="There is a unique Secret Token to make you know the webhook is from us." placement="bottom">
+              <span>{{ $t('webhooks.secretToken') }}</span>
+              <el-tooltip class="item" effect="dark" :content="$t('webhooks.tokenTip')" placement="bottom">
                 <img src="@/assets/images/common/info_white.svg" alt="info">
               </el-tooltip>
             </template>
             <template slot-scope="scope">
               <el-tooltip
                 popper-class="copy-tip"
-                content="Copied"
+                :content="$t('webhooks.copied')"
                 :value="showTooltip === '#secretToken' + scope.$index"
                 :visible-arrow="false"
                 :manual="true"
                 placement="top"
               >
-                <template slot="content"><img src="@/assets/images/common/check_right.svg" class="min" />Copied</template>
+                <template slot="content"><img src="@/assets/images/common/check_right.svg" class="min" />{{ $t('webhooks.copied') }}</template>
                 <div
                   class="key-input secret-key-input"
                   :class="{'copy-active': isCopyActive === '#secretToken' + scope.$index}"
@@ -72,20 +72,20 @@
               </el-tooltip>
             </template>
           </el-table-column>
-          <el-table-column prop="responseTime" label="" class-name="header-with-info" :formatter="formatterDate" min-width="180">
+          <el-table-column prop="responseTime" class-name="header-with-info" :formatter="formatterDate" min-width="180">
             <template slot="header">
-              <span>Last Response</span>
-              <el-tooltip class="item" effect="dark" content="The lastest UTC time we sent you a webhook." placement="bottom">
+              <span>{{ $t('webhooks.lastResponse') }}</span>
+              <el-tooltip class="item" effect="dark" :content="$t('webhooks.responseTip')" placement="bottom">
                 <img src="@/assets/images/common/info_white.svg" alt="info">
               </el-tooltip>
             </template></el-table-column>
-          <el-table-column label="Actions" min-width="80">
+          <el-table-column :label="$t('webhooks.actions')" min-width="80">
             <template slot-scope="scope">
-              <router-link :to="{name: 'editWebhook', params: {id: scope.row.id}}"><img src="@/assets/images/apikeys/edit.svg" alt="edit" class="edit-icon" /></router-link>
-              <img src="@/assets/images/apikeys/delete.svg" alt="delete" @click="handleDelete(scope.$index, scope.row)" class="delete-icon" />
+              <router-link :to="{name: 'editWebhook', params: {id: scope.row.id}}"><img src="@/assets/images/apiKey/edit.svg" alt="edit" class="edit-icon" /></router-link>
+              <img src="@/assets/images/apiKey/delete.svg" alt="delete" @click="handleDelete(scope.$index, scope.row)" class="delete-icon" />
             </template>
           </el-table-column>
-          <p slot="empty">No Data Available</p>
+          <p slot="empty">{{ $t('webhooks.noData') }}</p>
         </el-table>
       </div>
     </div>
@@ -98,7 +98,7 @@ import { getWebhooksList, apiDeleteWebhook, editWebhookStatus } from '@/request/
 import Clipboard from 'clipboard'
 import dayjs from 'dayjs'
 
-interface ItableData{
+interface isTableData{
   id: number,
   userId: number,
   projectId: number|null,
@@ -111,7 +111,7 @@ interface ItableData{
 
 @Component
 export default class webhooks extends Vue {
-  tableData: Array<ItableData> = []
+  tableData: Array<isTableData> = []
   showEye = -1
   pageType = ''
   passItem = {}
@@ -122,12 +122,13 @@ export default class webhooks extends Vue {
     this.getList()
   }
 
-  get showfather () {
-    return (this as any).$route.meta.showfather
+  get showFather () {
+    return (this as any).$route.meta.showFather
   }
 
   // 获取列表
   getList () {
+    const failedConnect: any = this.$t('failedConnect')
     getWebhooksList({
       timeZone: (0 - new Date().getTimezoneOffset() / 60) === 8 ? '8' : '0'
     }).then((res: any) => {
@@ -137,34 +138,39 @@ export default class webhooks extends Vue {
           this.tableData.push(res.data[i])
         }
       } else {
-        this.$message.error('Failed to connect.')
+        this.$message.error(failedConnect)
       }
     }).catch(err => {
       console.log(err)
-      this.$message.error('Failed to connect.')
+      this.$message.error(failedConnect)
     })
   }
 
   // 删除
   handleDelete (index: number, row: any) {
-    this.$confirm('Are you sure you want to delete this information?', {
-      confirmButtonText: 'Delete',
-      cancelButtonText: 'Cancel',
+    const text: any = this.$t('webhooks.sureDelete')
+    const deleteText: any = this.$t('webhooks.delete')
+    const cancelText: any = this.$t('webhooks.cancel')
+    const deleteSuccess: any = this.$t('webhooks.deleted')
+    const deleteFail: any = this.$t('webhooks.deleteFail')
+    this.$confirm(text, {
+      confirmButtonText: deleteText,
+      cancelButtonText: cancelText,
       customClass: 'message-box-delete'
     }).then(() => {
       apiDeleteWebhook(row.id).then((res: any) => {
         if (res.code === '200') {
           this.$message({
             type: 'success',
-            message: 'Deleted!'
+            message: deleteSuccess
           })
           this.tableData.splice(index, 1)
         } else {
-          this.$message.error('Deleted Failed.')
+          this.$message.error(deleteFail)
         }
       }).catch(err => {
         console.log(err)
-        this.$message.error('Deleted Failed.')
+        this.$message.error(deleteFail)
       })
     })
   }
@@ -176,6 +182,7 @@ export default class webhooks extends Vue {
 
   // 点击复制input中的内容
   copy (id: string) {
+    const copiedFail: any = this.$t('webhooks.copiedFail')
     var clipboard = new Clipboard(id)
     clipboard.on('success', () => {
       this.showTooltip = id
@@ -187,18 +194,22 @@ export default class webhooks extends Vue {
       clipboard.destroy()
     })
     clipboard.on('error', () => {
-      this.$message.error('Copied Failed.')
+      this.$message.error(copiedFail)
       clipboard.destroy()
     })
   }
 
   // 转换event标签首字母大写
   changeEventTag (str: string) {
-    str = str.toLowerCase()
-    const arr = str.split('.')
     let res = ''
-    for (const i in arr) {
-      res += arr[i].substring(0, 1).toUpperCase() + arr[i].substring(1) + ''
+    if (this.$i18n.locale === 'en') {
+      str = str.toLowerCase()
+      const arr = str.split('.')
+      for (const i in arr) {
+        res += arr[i].substring(0, 1).toUpperCase() + arr[i].substring(1) + ''
+      }
+    } else {
+      res = (this.$t('webhooks') as any)[str] as string
     }
     return res
   }
@@ -207,7 +218,8 @@ export default class webhooks extends Vue {
   formatterDate (row: any, column: any) {
     const val = row[column.property]
     if (!val) return 'None yet'
-    return dayjs(val).format('MMM D,YYYY HH:mm')
+    const day = this.$i18n.locale === 'en' ? dayjs(val).format('MMM D,YYYY HH:mm') : dayjs(val).format('YYYY年MM月DD日 HH:mm')
+    return day
   }
 
   // 用*隐藏值
@@ -220,10 +232,10 @@ export default class webhooks extends Vue {
   }
 
   changeSwitch (value: number, id: number) {
-    const formdata = new FormData()
-    formdata.append('id', id.toString())
-    formdata.append('status', value.toString())
-    editWebhookStatus(formdata, {})
+    const formData = new FormData()
+    formData.append('id', id.toString())
+    formData.append('status', value.toString())
+    editWebhookStatus(formData, {})
   }
 }
 </script>

BIN
static/images/common/fail@2x.png