aminhuang(黄嘉敏) 7 years ago
commit
58facd926b
49 changed files with 2193 additions and 0 deletions
  1. 15 0
      LICENSE.txt
  2. 33 0
      README.md
  3. 53 0
      app.js
  4. 20 0
      app.json
  5. 23 0
      app.wxss
  6. 125 0
      components/bottom-button/index.js
  7. 3 0
      components/bottom-button/index.json
  8. 17 0
      components/bottom-button/index.wxml
  9. 149 0
      components/bottom-button/index.wxss
  10. 127 0
      components/modal/index.js
  11. 3 0
      components/modal/index.json
  12. 9 0
      components/modal/index.wxml
  13. 73 0
      components/modal/index.wxss
  14. 40 0
      components/play-icon/index.js
  15. 5 0
      components/play-icon/index.json
  16. 17 0
      components/play-icon/index.wxml
  17. 86 0
      components/play-icon/index.wxss
  18. 198 0
      components/result-bubble/index.js
  19. 8 0
      components/result-bubble/index.json
  20. 39 0
      components/result-bubble/index.wxml
  21. 122 0
      components/result-bubble/index.wxss
  22. 85 0
      components/waiting-icon/index.js
  23. 5 0
      components/waiting-icon/index.json
  24. 5 0
      components/waiting-icon/index.wxml
  25. 9 0
      components/waiting-icon/index.wxss
  26. BIN
      image/button_en.png
  27. BIN
      image/button_en_disabled.png
  28. BIN
      image/button_en_press.png
  29. BIN
      image/button_zh.png
  30. BIN
      image/button_zh_disabled.png
  31. BIN
      image/button_zh_press.png
  32. BIN
      image/delete_all.png
  33. BIN
      image/edit.png
  34. BIN
      image/loading.gif
  35. BIN
      image/play_loud.png
  36. BIN
      image/play_loud_1.png
  37. BIN
      image/play_loud_2.png
  38. BIN
      image/qr.jpg
  39. 102 0
      pages/edit/edit.js
  40. 1 0
      pages/edit/edit.json
  41. 10 0
      pages/edit/edit.wxml
  42. 37 0
      pages/edit/edit.wxss
  43. 436 0
      pages/index/index.js
  44. 6 0
      pages/index/index.json
  45. 35 0
      pages/index/index.wxml
  46. 138 0
      pages/index/index.wxss
  47. 36 0
      project.config.json
  48. 88 0
      utils/conf.js
  49. 35 0
      utils/util.js

+ 15 - 0
LICENSE.txt

@@ -0,0 +1,15 @@
+Tencent is pleased to support the open source community by making Face-2-Face Translator available.
+Copyright (C) 2018 THL A29 Limited, a Tencent company.  All rights reserved.
+If you have downloaded a copy of the Face-2-Face Translator binary from Tencent, please note that the Face-2-Face Translator binary is licensed under the MIT License.
+If you have downloaded a copy of the Face-2-Face Translator source code from Tencent, please note that Face-2-Face Translator source code is licensed under the MIT License.  Your integration of Face-2-Face Translator into your own projects may require compliance with the MIT License.
+A copy of the MIT License is included in this file.
+
+
+
+Terms of the MIT License:
+---------------------------------------------------
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 33 - 0
README.md

@@ -0,0 +1,33 @@
+# 面对面翻译小程序
+
+
+面对面翻译小程序是微信团队针对面对面沟通的场景开发的流式语音翻译小程序,通过微信同传接口插件提供了语音识别,文本翻译等功能。
+
+
+## 预览
+![面对面翻译小程序](image/qr.jpg)
+
+
+## 下载与使用
+
+1. 克隆代码
+2. `project.config.json` 中的 `appid` 替换成在公众平台申请的项目 id
+3. 在 `公众平台 → 设置 → 第三方服务 → 插件管理` 中 添加微信同传接口插件 (`wx069ba97219f66d99`)
+4. 打开微信开发者工具中添加项目
+
+
+## 微信版本要求
+
+由于使用插件,需要 基础库版本 >= `1.9.6`
+
+
+## 微信同传接口插件支持功能
+
+- 语音识别 (目前支持 `zh_CN(中国大陆)`,  `en_US(英语)`)
+- 文本翻译 (目前支持 `zh_CN(中国大陆)`,  `zh_TW(中国台湾)`, `zh_HK(中国香港)`, `en_US(英语)`)
+- 语音合成 (目前支持 `zh_CN(中国大陆)`,  `zh_TW(中国台湾)`, `zh_HK(中国香港)`)
+
+
+## License
+
+[The MIT License](./LICENSE.txt)

+ 53 - 0
app.js

@@ -0,0 +1,53 @@
+//app.js
+
+const utils = require('./utils/util.js')
+
+App({
+  onLaunch: function () {
+    wx.getStorage({
+      key: 'history',
+      success: (res) => {
+          this.globalData.history = res.data
+      },
+      fail: (res) => {
+          console.log("get storage failed")
+          console.log(res)
+          this.globalData.history = []
+      }
+    })
+
+  },
+  // 权限询问
+  getRecordAuth: function() {
+    wx.getSetting({
+      success(res) {
+        console.log("succ")
+        console.log(res)
+        if (!res.authSetting['scope.record']) {
+          wx.authorize({
+            scope: 'scope.record',
+            success() {
+                // 用户已经同意小程序使用录音功能,后续调用 wx.startRecord 接口不会弹窗询问
+                console.log("succ auth")
+            }, fail() {
+                console.log("fail auth")
+            }
+          })
+        } else {
+          console.log("record has been authed")
+        }
+      }, fail(res) {
+          console.log("fail")
+          console.log(res)
+      }
+    })
+  },
+
+  onHide: function () {
+    wx.stopBackgroundAudio()
+  },
+  globalData: {
+
+    history: [],
+  }
+})

+ 20 - 0
app.json

@@ -0,0 +1,20 @@
+{
+  "pages":[
+
+    "pages/index/index",
+
+    "pages/edit/edit"
+  ],
+  "window": {
+      "backgroundTextStyle": "light",
+      "navigationBarBackgroundColor": "#FAFAFA",
+      "navigationBarTitleText": "面对面翻译",
+      "navigationBarTextStyle": "black"
+  },
+  "plugins": {
+    "WechatAI": {
+      "version": "0.0.5",
+      "provider": "wx069ba97219f66d99"
+    }
+  }
+}

+ 23 - 0
app.wxss

@@ -0,0 +1,23 @@
+/**app.wxss**/
+.container {
+  height: 100%;
+  display: flex;
+  display: -webkit-flex;
+  flex-direction: column;
+  -webkit-flex-direction: column;
+  justify-content: space-between;
+  -webkit-justify-content: space-between;
+  box-sizing: border-box;
+  position:relative;
+  font-family: "PingFang-SC-Regular","SimSun","Microsoft Yahei";
+  font-variant-ligatures: none;
+}
+page {
+  height: 100%;
+  width: 100%;
+  background-color: #FAFAFA;
+}
+
+input {
+  font-family: "PingFang-SC-Regular","SimSun","Microsoft Yahei";
+}

+ 125 - 0
components/bottom-button/index.js

@@ -0,0 +1,125 @@
+/*
+Tencent is pleased to support the open source community by making Face-2-Face Translator available.
+
+Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved.
+
+Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+http://opensource.org/licenses/MIT
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+*/
+
+import { language } from '../../utils/conf.js'
+
+let buttons = []
+
+// 按钮配置
+language.forEach(item=>{
+  buttons.push({
+    buttonText: item.lang_name,
+    lang: item.lang_content,
+    lto: item.lang_to[0],
+    msg: item.hold_talk,
+    buttonType: 'normal',
+  })
+})
+
+// 按钮对应图片
+let buttonBackground = {
+  zh_CN: {
+    normal: '../../image/button_zh.png',
+    press: '../../image/button_zh_press.png',
+    disabled: '../../image/button_zh_disabled.png',
+  },
+  en_US: {
+    normal: '../../image/button_en.png',
+    press: '../../image/button_en_press.png',
+    disabled: '../../image/button_en_disabled.png',
+  }
+}
+
+Component({
+  properties: {
+
+    buttonDisabled: {
+      type: Boolean,
+      value: false,
+      observer: function(newVal, oldVal){
+        let buttonType = newVal ? 'disabled' : 'normal'
+        this.changeButtonType(buttonType)
+
+      }
+
+    },
+
+    shadowShow: {
+      type: Boolean,
+      value: false,
+    },
+
+  },
+
+  data: {
+    buttons: buttons,
+    buttonBackground: buttonBackground,
+    currentButtonType: 'normal',
+
+  },
+
+  ready: function () {
+    // console.log(this.data.buttonEvent,)
+  },
+
+  methods: {
+
+    /**
+     * 按下按钮开始录音
+     */
+    streamRecord(e) {
+      if(this.data.buttonDisabled) {
+        return
+      }
+      // 先清空背景音
+      wx.stopBackgroundAudio()
+
+      let currentButtonConf = e.currentTarget.dataset.conf
+
+      this.changeButtonType('press', currentButtonConf.lang)
+
+      this.triggerEvent('recordstart', {
+        buttonItem: currentButtonConf
+      })
+
+    },
+
+    /**
+     * 松开按钮结束录音
+     */
+    endStreamRecord(e) {
+      let currentButtonConf = e.currentTarget.dataset.conf
+      console.log("currentButtonConf", currentButtonConf)
+
+      this.triggerEvent('recordend', {
+        buttonItem: currentButtonConf
+      })
+    },
+
+    /**
+     * 修改按钮样式
+     */
+    changeButtonType(buttonType, buttonLang) {
+
+      let tmpButtons = this.data.buttons.slice(0)
+
+      tmpButtons.forEach(button => {
+        if(!buttonLang || buttonLang == button.lang) {
+          button.buttonType = buttonType
+        }
+      })
+
+      this.setData({
+        buttons: tmpButtons
+      })
+    },
+  }
+});

+ 3 - 0
components/bottom-button/index.json

@@ -0,0 +1,3 @@
+{
+  "component": true
+}

+ 17 - 0
components/bottom-button/index.wxml

@@ -0,0 +1,17 @@
+<view class="button-wrap {{shadowShow ? 'button-wrap-shadow' : ''}}" hidden="{{hidden}}">
+  <view class="img-big-wrap">
+    <view class="button-container">
+      <view wx:for="{{buttons}}" wx:for-item="button" wx:key="{{button.lang}}" class="button-item">
+        <view  catchtouchstart="streamRecord"
+               catchtouchend="endStreamRecord"
+               data-conf="{{button}}"
+               class="button-press">
+          <span class="text-in-button {{ button.buttonType == 'press' ? 'text-press': '' }}">{{button.buttonText}}</span>
+          <image class="button-background" src="{{buttonBackground[button.lang][button.buttonType]}}"></image>
+        </view>
+        <view class="button-label">{{button.msg}}</view>
+      </view>
+
+    </view>
+  </view>
+</view>

+ 149 - 0
components/bottom-button/index.wxss

@@ -0,0 +1,149 @@
+.button-wrap {
+  display: -webkit-flex;
+  display: flex;
+  -webkit-justify-content: center;
+  justify-content: center;
+}
+.button-wrap-shadow {
+   box-shadow: -1px 1px 7px 0 rgba(0,0,0,0.13);
+}
+.img-big-wrap {
+  width: 100%;
+  display: -webkit-flex;
+  display: flex;
+  background: #FAFAFA;
+}
+
+.input-language {
+  position: absolute;
+  right: 50%;
+  bottom: 60rpx;
+  height: 159rpx;
+  width: 230rpx;
+  display: flex;
+  display: -webkit-flex;
+  align-items: center;
+  justify-content: center;
+  box-sizing: border-box;
+}
+.output-language{
+  position: absolute;
+  left: 50%;
+  bottom: 60rpx;
+  height: 159rpx;
+  width: 230rpx;
+  display: flex;
+  display: -webkit-flex;
+  align-items: center;
+  justify-content: center;
+}
+.normal-record-icon {
+  height: 30rpx;
+  width: 20rpx;
+}
+.normal-record-text {
+  color: #9B9B9B;
+  font-size: 24rpx;
+  margin: 0 10rpx;
+}
+.press-record-icon {
+  height: 32rpx;
+  width: 20rpx;
+}
+.press-record-text {
+  color: #4A90E2;
+  font-size: 32rpx;
+  margin: 0 10rpx;
+}
+.record-text {
+  color: #9B9B9B;
+  font-size:24rpx;
+  text-align:center;
+  position:absolute;
+  left:50%;
+  margin-left:-160rpx;
+  width:320rpx;
+  bottom:229rpx;/*159 + 60 + 10*/
+}
+.keyboard-button {
+  height:40rpx;
+  width:48rpx;
+  position:absolute;
+  left:50%;
+  margin-left:-302rpx;/*-254 - 48*/
+  bottom:120rpx;
+}
+.keyboard-text {
+  color: #4A4A4A;
+  font-size:32rpx;
+  text-align:center;
+  position:absolute;
+  left:80%;
+  margin-left:-135rpx;
+  width:320rpx;
+  top:175rpx;
+}
+.button-container{
+  display: flex;
+  display: -webkit-flex;
+  height: 100%;
+  width: 100%;
+  box-sizing: border-box;
+  justify-content: space-between;
+  -webkit-justify-content: space-between;
+  align-items: center;
+  -webkit-align-items:center;
+  margin: 0 calc( (100% - 240rpx * 2) / 3 );
+  margin-bottom: 20px;
+  padding: 50rpx 0 38rpx 0;
+}
+.button-item {
+  display: flex;
+  display: -webkit-flex;
+  flex-direction: column;
+  -webkit-flex-direction: column;
+  justify-content: flex-start;
+  -webkit-justify-content: flex-start;
+  align-items: center;
+  -webkit-align-items: center;
+  width: 240rpx;
+  box-sizing: border-box;
+}
+.button-label {
+  font-size: 30rpx;
+  color: #9B9B9B;
+  letter-spacing: 0;
+  margin: 15rpx 0 0 0;
+}
+.button-press {
+  position: relative;
+  display: flex;
+  display: -webkit-flex;
+  height: 100rpx;
+  /* width: 240rpx; */
+  width: 100%;
+  border-radius: 100rpx;
+  justify-content: center;
+  -webkit-justify-content: center;
+  align-items: center;
+  -wekbit-align-items:center;
+}
+.button-background {
+  position: absolute;
+  height: 100rpx;
+  width: 100%;
+  border-radius: 100rpx;
+  left: 0;
+  z-index: 1;
+}
+
+.text-in-button {
+  font-weight: bold;
+  font-size: 34rpx;
+  color: #FFFFFF;
+  z-index: 2;
+}
+
+.text-in-button.text-press {
+  opacity: 0.6;
+}

+ 127 - 0
components/modal/index.js

@@ -0,0 +1,127 @@
+/*
+Tencent is pleased to support the open source community by making Face-2-Face Translator available.
+
+Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved.
+
+Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+http://opensource.org/licenses/MIT
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+*/
+
+
+import { language } from '../../utils/conf.js'
+
+const tips_language = language[0]
+
+let modalItems = [
+    {
+      type: 'copySource',
+      text: tips_language.copy_source_text
+    },
+    {
+      type: 'delete',
+      text: tips_language.delete_item
+    },
+    {
+      type: 'copyTarget',
+      text: tips_language.copy_target_text
+    },
+]
+
+Component({
+  properties: {
+
+    item: {
+      type: Object,
+      value: {},
+    },
+
+    modalShow: {
+      type: Boolean,
+      value: true,
+    },
+
+    index: {
+      type: Number,
+    },
+
+  },
+
+  data: {
+    // tips_language: language[0], // 目前只有中文
+
+    modalItems: modalItems,
+  },
+
+  ready: function () {
+  },
+
+  methods: {
+
+
+    deleteBubbleModal: function() {
+      this.triggerEvent('modaldelete', {
+        item: this.data.item,
+        index: this.data.index,
+      },{ bubbles: true, composed: true })
+      this.leaveBubbleModal()
+    },
+
+    /**
+     * 点击
+     */
+    itemTap: function(e) {
+      let itemType = e.currentTarget.dataset.type
+
+      let item = this.data.item
+
+      switch(itemType) {
+        case 'copySource':
+          this.setClip(item.text)
+          break;
+        case 'copyTarget':
+          this.setClip(item.translateText)
+          break
+        case 'delete':
+          this.deleteBubbleModal()
+          break
+        default:
+          break
+      }
+    },
+
+    /**
+     * 复制到剪贴板
+     *
+     * @param      {string}  text    需要复制到剪贴板的文字
+     */
+    setClip: function(text) {
+
+      wx.setClipboardData({
+        data: text,
+        success:  (res) => {
+          this.leaveBubbleModal()
+          wx.showToast({
+            title: "已复制到剪切板",
+            icon: "success",
+            duration: 1000,
+            success: function (res) {
+              console.log("show succ");
+            },
+            fail: function (res) {
+              console.log(res);
+            }
+          });
+        }
+      })
+    },
+
+    leaveBubbleModal: function() {
+      this.triggerEvent('modalleave', {
+        modalShow: this.data.modalShow
+      })
+    },
+
+  }
+});

+ 3 - 0
components/modal/index.json

@@ -0,0 +1,3 @@
+{
+  "component": true
+}

+ 9 - 0
components/modal/index.wxml

@@ -0,0 +1,9 @@
+<view wx:if="{{modalShow}}" style="height:100%;width:100%">
+  <view class="modal-wrapper" >
+    <view class="modal-triangle" ></view>
+    <view class="menu-modal">
+      <view wx:for="{{modalItems}}" wx:key="type" class="menu-modal-item  " data-type="{{item.type}}" bindtap="itemTap">{{item.text}}</view>
+    </view>
+  </view>
+</view>
+<view wx:if="{{modalShow}}" class="modal-hidden" bindtouchstart="leaveBubbleModal"></view>

+ 73 - 0
components/modal/index.wxss

@@ -0,0 +1,73 @@
+.modal-wrapper {
+  position:relative;
+  color:#FFFFFF;
+  height:32px;
+  width:80%;
+  margin: 0 auto;
+  z-index:70;
+  opacity:0.9;
+}
+.modal-triangle {
+  position:relative;
+  margin: 0 auto;
+  top: 26px;
+  height:0;
+  width:0;
+  border:5px solid #000000;
+  transform:rotate(45deg);
+}
+.modal-hidden {
+  position:fixed;
+  top:0;
+  left:0;
+  width:100%;
+  height:100%;
+  background-color:#FFFFFF;
+  opacity:0;
+  z-index:69
+}
+.menu-modal {
+  height:32px;
+  font-size:14px;
+  position:absolute;
+  top:0;
+  width:100%;
+  display:flex;
+  display: -webkit-flex;
+  -webkit-align-items:center;
+  align-items:center;
+  box-sizing:border-box;
+
+}
+.menu-modal-item {
+  color:#FFFFFF;
+  position:relative;
+  width:35%;
+  height:100%;
+  display:flex;
+  display: -webkit-flex;
+  align-items:center;
+  -webkit-align-items:center;
+  justify-content:center;
+  -webkit-justify-content:center;
+  background-clip: content-box;
+  background-color:#000000;
+}
+
+.menu-modal-item:first-child {
+  border-top-left-radius: 8px;
+  border-bottom-left-radius: 8px;
+}
+
+.menu-modal-item:last-child {
+  border-top-right-radius: 8px;
+  border-bottom-right-radius: 8px;
+}
+
+.menu-modal-item + .menu-modal-item {
+  border-left: 1rpx solid #FFFFFF;
+}
+
+.menu-modal-item:active {
+  background-color: #9e9e9e;
+}

+ 40 - 0
components/play-icon/index.js

@@ -0,0 +1,40 @@
+/*
+Tencent is pleased to support the open source community by making Face-2-Face Translator available.
+
+Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved.
+
+Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+http://opensource.org/licenses/MIT
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+*/
+
+Component({
+
+  properties: {
+    playType: {
+      type: String,
+      value: 'wait',
+    }
+  },
+
+  data: {
+
+
+  },
+
+  ready: function () {
+
+  },
+
+  // 组件生命周期函数,在组件实例被从页面节点树移除时执行
+  detached: function() {
+
+  },
+
+  methods: {
+
+
+
+  }
+});

+ 5 - 0
components/play-icon/index.json

@@ -0,0 +1,5 @@
+{
+  "component": true,
+  "usingComponents": {
+  }
+}

+ 17 - 0
components/play-icon/index.wxml

@@ -0,0 +1,17 @@
+<view  wx:if="{{playType != 'loading'}}" class="play-loud-icon" >
+
+  <image  src="../../image/play_loud.png" class="play-loud-icon  play-loud-img" ></image>
+
+  <block wx:if="{{playType=='playing'}}">
+      <image  src="../../image/play_loud_1.png" class="play-loud-icon play-loud-img play-animation "  ></image>
+      <image  src="../../image/play_loud_2.png"  class="play-loud-icon play-loud-img play-animation1"  ></image>
+  </block>
+  <block wx:else>
+      <image  src="../../image/play_loud_1.png" class="play-loud-icon play-loud-img" ></image>
+  </block>
+
+</view>
+
+<view wx:if="{{playType=='loading'}}"  class="play-loud-icon" >
+  <image  src="../../image/loading.gif" class="play-loading-img play-loud-img" ></image>
+</view>

+ 86 - 0
components/play-icon/index.wxss

@@ -0,0 +1,86 @@
+.play-loud-icon {
+  position: relative;
+  display: flex;
+  align-items: center;
+  height:32rpx;
+  width:39rpx;
+}
+
+.play-loading-img {
+    position: relative;
+    display: flex;
+    align-items: center;
+    height:32rpx;
+    width:32rpx;
+}
+
+.play-loud-img {
+  position: absolute;
+  left: 0;
+
+}
+
+.play-animation {
+  -webkit-animation-delay:200ms;
+  animation-delay:200ms;
+
+
+  -webkit-animation: tranOpacity 1200ms ease-in-out infinite;
+  animation: tranOpacity 1200ms ease-in-out  infinite;
+
+}
+
+.play-animation1 {
+  -webkit-animation-delay:200ms;
+  animation-delay:200ms;
+
+  -webkit-animation: tranOpacity1 1200ms ease-in-out  infinite;
+  animation: tranOpacity1 1200ms ease-in-out infinite;
+}
+
+
+@-webkit-keyframes tranOpacity {
+  0% {
+    opacity:  0;
+  }
+  35% {
+    opacity:  1;
+  }
+  100% {
+    opacity:  1;
+  }
+}
+@keyframes tranOpacity {
+  0% {
+    opacity:  0;
+  }
+  35% {
+    opacity:  1;
+  }
+  100% {
+    opacity:  1;
+  }
+}
+
+@-webkit-keyframes tranOpacity1 {
+  0% {
+    opacity:  0;
+  }
+  35% {
+    opacity:  0;
+  }
+  100% {
+    opacity:  1;
+  }
+}
+@keyframes tranOpacity1 {
+  0% {
+    opacity:  0;
+  }
+  35% {
+    opacity:  0;
+  }
+  100% {
+    opacity:  1;
+  }
+}

+ 198 - 0
components/result-bubble/index.js

@@ -0,0 +1,198 @@
+/*
+Tencent is pleased to support the open source community by making Face-2-Face Translator available.
+
+Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved.
+
+Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+http://opensource.org/licenses/MIT
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+*/
+
+import { language } from '../../utils/conf.js'
+
+
+Component({
+
+  properties: {
+    /*
+    item 格式
+    {
+      create: '04/27 15:37',
+      text: '一二三四五',
+      translateText: '12345',
+      voicePath: '',
+      translateVoicePath: '',
+      id: 0,
+    },*/
+    item: {
+      type: Object,
+      value: {},
+      observer: function(newVal, oldVal){
+
+        // 翻译完成后,文字有改变触发重新翻译
+        if(this.data.recordStatus == 2 && oldVal.text && oldVal.text != '' && newVal.text != oldVal.text) {
+          this.triggerEvent('translate', {
+            item: this.data.item,
+            index: this.data.index,
+          })
+        }
+
+        // 翻译内容改变触发播放
+        if(newVal.autoPlay && newVal.translateVoicePath != oldVal.translateVoicePath){
+          this.autoPlayTranslateVoice()
+        }
+
+      }
+    },
+    editShow: {
+      type: Boolean,
+      value: false,
+    },
+    index: {
+      type: Number,
+    },
+
+    currentTranslateVoice: {
+      type: String,
+      observer: function(newVal, oldVal){
+        if(newVal != '' && newVal != this.data.item.translateVoicePath) {
+          this.playAnimationEnd()
+        }
+      },
+    },
+
+    recordStatus: {
+      type: Number,
+      value: 2, // 0:正在识别,1:正在翻译,2:翻译完成
+    },
+  },
+
+  data: {
+
+    tips_language: language[0], // 目前只有中文
+
+    modalShow: false, // 展示悬浮框
+
+    playType: 'wait', // 语音播放状态
+
+
+    waiting_animation: {},
+    waiting_animation_1: {},
+
+    edit_icon_path: '../../image/edit.png'
+
+
+  },
+
+  ready: function () {
+    if(this.data.item.autoPlay) {
+      this.autoPlayTranslateVoice()
+    }
+
+  },
+
+  // 组件生命周期函数,在组件实例被从页面节点树移除时执行
+  detached: function() {
+    // console.log("detach")
+
+  },
+
+  methods: {
+
+    /**
+     * 显示悬浮框
+     */
+    showModal: function() {
+      this.setData({modalShow: true})
+    },
+
+    /**
+     * 离开悬浮框
+     */
+    modalLeave: function() {
+      this.setData({modalShow: false})
+    },
+
+
+    /**
+     * 点击播放图标
+     */
+    playTranslateVoice: function() {
+
+      let nowTime = parseInt(+ new Date() / 1000)
+      let voiceExpiredTime = this.data.item.translateVoiceExpiredTime || 0
+
+      if(this.data.playType == 'playing') {
+        wx.stopBackgroundAudio()
+        this.playAnimationEnd()
+      } else if(nowTime < voiceExpiredTime) {
+        this.autoPlayTranslateVoice()
+      } else {
+        this.setData({
+          playType: 'loading',
+        })
+        this.triggerEvent('expired', {
+          item: this.data.item,
+          index: this.data.index,
+        })
+      }
+    },
+
+    /**
+     * 播放背景音乐
+     */
+    autoPlayTranslateVoice: function (path,index) {
+      let play_path = this.data.item.translateVoicePath
+
+      if(!play_path) {
+        console.warn("no translate voice path")
+        return
+      }
+
+
+      wx.onBackgroundAudioStop(res => {
+        console.log("play voice end",res)
+        this.playAnimationEnd()
+      })
+
+      this.playAnimationStart()
+
+      wx.playBackgroundAudio({
+        dataUrl: play_path,
+        title: '',
+        success: (res) => {
+          this.playAnimationStart()
+        },
+        fail: (res) => {
+            // fail
+            console.log("failed played", play_path);
+            this.playAnimationEnd()
+        },
+        complete: function (res) {
+            console.log("complete played");
+        }
+      })
+    },
+
+    /**
+     * 开始播放
+     */
+    playAnimationStart: function() {
+      this.setData({
+        playType: 'playing',
+      })
+
+    },
+
+    /**
+     * 结束播放
+     */
+    playAnimationEnd: function() {
+        this.setData({
+          playType: 'wait',
+        })
+    },
+
+  }
+});

+ 8 - 0
components/result-bubble/index.json

@@ -0,0 +1,8 @@
+{
+  "component": true,
+  "usingComponents": {
+    "modal": "/components/modal/index",
+    "waiting-icon": "/components/waiting-icon/index",
+    "play-icon": "/components/play-icon/index"
+  }
+}

+ 39 - 0
components/result-bubble/index.wxml

@@ -0,0 +1,39 @@
+<view class="bubble-wrap" bindlongpress="showModal" >
+  <view class="modal-wrap" wx:if="{{recordStatus == 2}}">
+    <modal modal-show="{{modalShow}}"
+      index="{{index}}"
+      item="{{item}}"
+      bindmodalleave="modalLeave"></modal>
+  </view>
+
+  <view class="create-time">{{item.create}}</view>
+  <view class="section-body" data-index="{{index}}" >
+    <view class="send-message">
+      <view data-id="{{item.id}}"  class="text-content"  data-index="{{index}}" >
+        <view class="text-detail  text-detail-{{item.lfrom}}" >{{item.text}}<waiting-icon wx:if="{{recordStatus == 0}}"></waiting-icon></view>
+      </view>
+      <navigator
+        hover-class="navigator-hover"
+        data-text="{{item.text}}"
+        data-id="{{item.id}}"
+        data-index="{{index}}"
+        class="edit-icon"
+        wx:if="{{editShow}}"
+        data-item="{{item}}"
+        url="{{'/pages/edit/edit?content='+item.text+'&index='+index}}">
+          <image class="edit-icon-img" src="{{edit_icon_path}}" ></image>
+      </navigator>
+    </view>
+    <view class="line-between"  wx:if="{{recordStatus > 0}}"></view>
+    <view class="translate-message" >
+      <view class="text-content">
+        <view class="text-detail text-detail-{{item.lto}}">{{item.translateText}}<waiting-icon wx:if="{{recordStatus == 1}}"></waiting-icon>
+      </view>
+      </view>
+      <view class="play-icon" catchtap="playTranslateVoice" cattouchstart="playTranslateVoice" wx:if="{{recordStatus == 2}}">
+        <play-icon play-type="{{playType}}"></play-icon>
+      </view>
+    </view>
+  </view>
+</view>
+

+ 122 - 0
components/result-bubble/index.wxss

@@ -0,0 +1,122 @@
+.bubble-wrap {
+  position: relative;
+}
+.wait-point {
+  display:inline-block;
+  width:6px;
+  height:6px;
+  border-radius:3px;
+  background-color: #ddd;
+  margin: 0 2px;
+
+}
+.loading {
+  position: relative;
+}
+
+.line-between {
+  height: 1px;
+  width: 100%;
+  background: #F1F1F1;
+  overflow: hidden;
+  margin: 30rpx 0;
+}
+
+.create-time {
+  font-size:28rpx;
+  color: #B2B2B2;
+  margin-bottom:5px;
+  display: flex;
+  display: -webkit-flex;
+  justify-content: center;
+  -webkit-justify-content: center;
+}
+
+.section-body{
+  word-wrap: break-word;
+  position: relative;
+  width:100%;
+  background: #FFFFFF;
+  box-shadow: 0 2px 16px 2px rgba(0,0,0,0.03);
+  padding:50rpx 60rpx;
+  box-sizing: border-box;
+  min-height: 260rpx;
+}
+
+.text-detail {
+  font-size: 36rpx;
+  line-height: 1.231;
+  vertical-align: text-bottom;
+  box-sizing: border-box;
+  font-family: "PingFang-SC-Regular","SimSun","Microsoft Yahei";
+  font-variant-ligatures: none;
+}
+
+.text-detail-en_US {
+  line-height: 1.231;
+}
+
+.text-detail-zh_CN {
+  line-height: 1.41;
+}
+
+.translate-message,
+.send-message {
+  position: relative;
+  padding: 0 2px;
+}
+
+.send-message  .text-detail {
+  color: #9B9B9B;
+}
+
+.edit-icon {
+  position: absolute;
+  display: flex;
+  align-items: center;
+  right: 10rpx;
+  bottom: 0;
+  padding: 0 8rpx;
+  bottom: 7rpx;
+}
+
+.edit-icon-img {
+  height:31rpx;
+  width:31rpx
+}
+
+.play-icon {
+  position: absolute;
+  right: 3rpx;
+  bottom: 7rpx;
+  padding: 0 8rpx;
+  display: flex;
+  align-items: center;
+}
+
+.edit-icon::before
+.play-icon::before {
+  content:"";
+  position:absolete;
+  top:-10rpx;
+  left:-10rpx;
+  bottom:-10rpx;
+  right:-10rpx;
+}
+
+
+.text-content {
+  margin: 0 48px 0 0;
+  box-sizing: border-box;
+}
+
+
+.modal-wrap {
+  position: absolute;
+  width: 100%;
+  box-sizing:border-box;
+}
+/* 重置navigator样式 */
+.navigator-hover {
+  background-color: #fff;
+}

+ 85 - 0
components/waiting-icon/index.js

@@ -0,0 +1,85 @@
+/*
+Tencent is pleased to support the open source community by making Face-2-Face Translator available.
+
+Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved.
+
+Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+http://opensource.org/licenses/MIT
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+*/
+
+Component({
+
+  properties: {
+
+  },
+
+  data: {
+    waiting_animation: {},
+    waiting_animation_1: {},
+  },
+
+  ready: function () {
+    console.log("ready waitting")
+      this.waiting_animation = wx.createAnimation({
+          duration: 600
+      })
+      this.waiting_animation_1 = wx.createAnimation({
+          duration: 400
+      })
+
+      this.setWaitInterval()
+
+  },
+
+  // 组件生命周期函数,在组件实例被从页面节点树移除时执行
+  detached: function() {
+    console.log("detach")
+    this.clearAnimation()
+  },
+
+  methods: {
+
+    clearAnimation: function() {
+      this.endWaitAnimation()
+
+
+    },
+
+    /**
+     * 清楚等待区域以及翻译区域的动画
+     */
+    endWaitAnimation: function() {
+        clearInterval(this.data.waiting_interval)
+
+        this.setData({ waiting_animation : {}})
+        this.setData({ waiting_animation_1: {} })
+    },
+    startWaitAnimation: function () {
+
+      this.waiting_animation.opacity(0).scale(1.2, 1.2).step()
+      this.waiting_animation.opacity(1).scale(1, 1).step()
+      this.setData({ waiting_animation: this.waiting_animation.export() })
+
+      this.waiting_animation_1.opacity(0).scale(1.2, 1.2).step()
+      this.waiting_animation_1.opacity(1).scale(1, 1).step()
+      this.setData({ waiting_animation_1: this.waiting_animation_1.export() })
+
+    },
+
+    /**
+     * 设置识别,翻译部分的等待动画
+     */
+    setWaitInterval: function() {
+       this.endWaitAnimation()
+
+       this.data.waiting_interval = setInterval( ()=>{
+        this.startWaitAnimation()
+       },600 )
+
+    },
+
+
+  }
+});

+ 5 - 0
components/waiting-icon/index.json

@@ -0,0 +1,5 @@
+{
+  "component": true,
+  "usingComponents": {
+  }
+}

+ 5 - 0
components/waiting-icon/index.wxml

@@ -0,0 +1,5 @@
+<view class="loading">
+  <view class="loading-icon">.</view>
+  <view animation="{{waiting_animation}}" class="loading-icon">.</view>
+  <view animation="{{waiting_animation_1}}" class="loading-icon">.</view>
+</view>

+ 9 - 0
components/waiting-icon/index.wxss

@@ -0,0 +1,9 @@
+
+.loading {
+  position: relative;
+  display: inline;
+}
+
+.loading-icon {
+  display: inline;
+}

BIN
image/button_en.png


BIN
image/button_en_disabled.png


BIN
image/button_en_press.png


BIN
image/button_zh.png


BIN
image/button_zh_disabled.png


BIN
image/button_zh_press.png


BIN
image/delete_all.png


BIN
image/edit.png


BIN
image/loading.gif


BIN
image/play_loud.png


BIN
image/play_loud_1.png


BIN
image/play_loud_2.png


BIN
image/qr.jpg


+ 102 - 0
pages/edit/edit.js

@@ -0,0 +1,102 @@
+/*
+Tencent is pleased to support the open source community by making Face-2-Face Translator available.
+
+Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved.
+
+Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+http://opensource.org/licenses/MIT
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+*/
+
+var app = getApp()
+Page({
+
+  /**
+   * 页面的初始数据
+   */
+  data: {
+    edit_text_max: 200,
+    remain_length: 200,
+    edit_text: "",
+    is_focus: false,
+    tips: "",
+    index: -1,
+  },
+  /**
+   * 获得最大文本长度
+   */
+  getEditTextMax: function () {
+    return this.data.edit_text_max
+  },
+  /**
+   * 更新剩余长度
+   */
+  updateRemainLength: function (now_content) {
+    this.data.remain_length = this.getEditTextMax() - now_content.length
+    this.data.tips = "还可以输入" + this.data.remain_length + "字..."
+    this.setData({ tips: this.data.tips })
+  },
+  /**
+   * 设置初始内容
+   */
+  setEditText: function (text) {
+    this.data.edit_text = text
+    this.setData({ edit_text: this.data.edit_text })
+    //更新剩余长度显示
+    this.updateRemainLength(text)
+    this.setData({ is_focus: true })
+  },
+  /**
+   * bindinput
+   */
+  editInput: function (event) {
+    console.log(event)
+    if (event.detail.value.length > this.getEditTextMax()) {
+
+    } else {
+      this.data.edit_text = event.detail.value
+      this.updateRemainLength(this.data.edit_text)
+    }
+  },
+  /**
+   * bindconfirm
+   */
+  editConfirm: function (event) {
+    if (this.data.edit_text.length > 0 && this.data.edit_text != this.data.oldText) {
+      // 得到页面栈
+      let pages = getCurrentPages();
+      let prevPage = pages[pages.length - 2];  //上一个页面
+      let dialogList = prevPage.data.dialogList.slice(0)
+      let editItem = dialogList[dialogList.length - 1]
+      editItem.text = this.data.edit_text
+
+      prevPage.setData({
+        dialogList: dialogList,
+        recordStatus: 2,
+      })
+      wx.navigateBack()
+    } else {
+      //文本输入为空时提示
+    }
+  },
+  /**
+   * 清空内容
+   */
+  deleteContent: function () {
+    this.setEditText("")
+  },
+
+  /**
+   * 生命周期函数--监听页面加载
+   */
+  onLoad: function (options) {
+    this.setEditText(options.content)
+    let index = parseInt(options.index)
+    this.setData({
+        index: index,
+        oldText:options.content,
+    })
+  },
+
+})

+ 1 - 0
pages/edit/edit.json

@@ -0,0 +1 @@
+{}

+ 10 - 0
pages/edit/edit.wxml

@@ -0,0 +1,10 @@
+<!--pages/edit/edit.wxml-->
+<view class="container edit-container">
+  <textarea maxlength="{{edit_text_max}}" auto-height="{{true}}" class="edit_textarea" cursor-spacing="20" focus="{{is_focus}}" bindinput="editInput"  bindconfirm="editConfirm" value="{{edit_text}}"></textarea>
+  <view class="tips-wrapper">
+    <textarea class="edit-tips" value="{{tips}}" auto-height="{{true}}" disabled="{{true}}"></textarea>
+    <view class="delete-content"  bindtap="deleteContent">
+      <image src="../../image/delete_all.png" class="img-delete-all"></image>
+    </view>
+  </view>
+</view>

+ 37 - 0
pages/edit/edit.wxss

@@ -0,0 +1,37 @@
+/* pages/edit/edit.wxss */
+.edit-container {
+  padding:20px 20px;
+  justify-content:flex-start;
+  -webkit-justify-content:flex-start
+}
+.edit_textarea {
+  max-height:400rpx;
+  width:100%;
+  box-sizing:border-box;
+  font-size:36rpx;
+  line-height:50rpx;
+}
+.tips-wrapper {
+  width:100%;
+  display:flex;
+  display: -webkit-flex;
+  justify-content:space-between;
+  -webkit-justify-content: spacce-between;
+
+  padding: 0 25rpx 0 0;
+  box-sizing: border-box;
+  align-items: center;
+  -webkit-align-items: center;
+}
+.edit-tips {
+  font-size:30rpx;
+  color:#B2B2B2;
+  line-height: 50rpx;
+}
+.img-delete-all {
+  height:30rpx;
+  width:28rpx;
+}
+.delete-content {
+    padding:20rpx 20rpx;
+}

+ 436 - 0
pages/index/index.js

@@ -0,0 +1,436 @@
+/*
+Tencent is pleased to support the open source community by making Face-2-Face Translator available.
+
+Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved.
+
+Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+http://opensource.org/licenses/MIT
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+*/
+
+const app = getApp()
+
+const util = require('../../utils/util.js')
+
+const plugin = requirePlugin("WechatAI")
+
+import { language } from '../../utils/conf.js'
+
+
+// 获取**全局唯一**的语音识别管理器**recordRecoManager**
+const manager = plugin.getRecordRecognitionManager()
+
+
+Page({
+  data: {
+    dialogList: [
+      // {
+      //   // 当前语音输入内容
+      //   create: '04/27 15:37',
+      //   lfrom: 'zh_CN',
+      //   lto: 'en_US',
+      //   text: '这是测试这是测试这是测试这是测试',
+      //   translateText: 'this is test.this is test.this is test.this is test.',
+      //   voicePath: '',
+      //   translateVoicePath: '',
+      //   autoPlay: false, // 自动播放背景音乐
+      //   id: 0,
+      // },
+    ],
+    scroll_top: 10000, // 竖向滚动条位置
+
+    bottomButtonDisabled: false, // 底部按钮disabled
+
+    tips_language: language[0], // 目前只有中文
+
+    initTranslate: {
+      // 为空时的卡片内容
+      create: '04/27 15:37',
+      text: '等待说话',
+    },
+
+    currentTranslate: {
+      // 当前语音输入内容
+      create: '04/27 15:37',
+      text: '等待说话',
+    },
+    recording: false,  // 正在录音
+    recordStatus: 0,   // 状态: 0 - 录音中 1- 翻译中 2 - 翻译完成/二次翻译
+
+    toView: 'fake',  // 滚动位置
+    lastId: -1,    // dialogList 最后一个item的 id
+    currentTranslateVoice: '', // 当前播放语音路径
+
+  },
+
+  longpressEvent: function(e) {
+    console.log("长按", e)
+  },
+
+
+
+  /**
+   * 按住按钮开始语音识别
+   */
+  streamRecord: function(e) {
+    console.log("streamrecord" ,e)
+    let detail = e.detail
+    let buttonItem = detail.buttonItem
+    manager.start({
+      lang: buttonItem.lang,
+    })
+
+    this.setData({
+      recordStatus: 0,
+      recording: true,
+      currentTranslate: {
+        // 当前语音输入内容
+        create: util.recordTime(new Date()),
+        text: '正在聆听中',
+        lfrom: buttonItem.lang,
+        lto: buttonItem.lto,
+      },
+    })
+    this.scrollToNew();
+
+    wx.reportAnalytics('record_and_translate_event', {
+      lfrom: buttonItem.lang,
+      lto: buttonItem.lto,
+    });
+  },
+
+
+  /**
+   * 松开按钮结束语音识别
+   */
+  streamRecordEnd: function(e) {
+
+    console.log("streamRecordEnd" ,e)
+    let detail = e.detail  // 自定义组件触发事件时提供的detail对象
+    let buttonItem = detail.buttonItem
+
+    // 防止重复触发stop函数
+    if(!this.data.recording || this.data.recordStatus != 0) {
+      console.warn("has finished!")
+      return
+    }
+
+    manager.stop()
+
+    this.setData({
+      bottomButtonDisabled: true,
+    })
+  },
+
+
+  /**
+   * 翻译
+   */
+  translateText: function(item, index) {
+    let lfrom =  item.lfrom || 'zh_CN'
+    let lto = item.lto || 'en_US'
+
+    plugin.translate({
+      lfrom: lfrom,
+      lto: lto,
+      content: item.text,
+      tts: true,
+      success: (resTrans)=>{
+
+        let passRetcode = [
+          0, // 成功
+          -10006, // 翻译成功,合成失败
+        ]
+
+        if(passRetcode.indexOf(resTrans.retcode) >= 0 ) {
+          let tmpDialogList = this.data.dialogList.slice(0)
+
+          if(!isNaN(index)) {
+
+            let tmpTranslate = Object.assign({}, item, {
+              autoPlay: true, // 自动播放背景音乐
+              translateText: resTrans.result,
+              translateVoicePath: resTrans.filename || "",
+              translateVoiceExpiredTime: resTrans.expired_time || 0
+            })
+
+            tmpDialogList[index] = tmpTranslate
+
+
+            this.setData({
+              dialogList: tmpDialogList,
+              bottomButtonDisabled: false,
+              recording: false,
+            })
+
+            this.scrollToNew();
+
+          } else {
+            console.error("index error", resTrans, item)
+          }
+        } else {
+          console.warn("翻译失败", resTrans, item)
+        }
+
+      },
+      fail: function(resTrans) {
+        console.log("调用失败",resTrans, item)
+      },
+      complete: resTrans => {
+        this.setData({
+          recordStatus: 1,
+        })
+        wx.hideLoading()
+      }
+    })
+
+  },
+
+
+  /**
+   * 修改文本信息之后触发翻译操作
+   */
+  translateTextAction: function(e) {
+    // console.log("translateTextAction" ,e)
+    let detail = e.detail  // 自定义组件触发事件时提供的detail对象
+    let item = detail.item
+    let index = detail.index
+
+    this.translateText(item, index)
+
+  },
+
+  /**
+   * 语音文件过期,重新合成语音文件
+   */
+  expiredAction: function(e) {
+    let detail = e.detail  // 自定义组件触发事件时提供的detail对象
+    let item = detail.item
+    let index = detail.index
+
+    let lto = item.lto || 'en_US'
+
+    plugin.textToSpeech({
+      lang: lto,
+      content: item.translateText,
+      success: resTrans => {
+        if(resTrans.retcode == 0) {
+          let tmpDialogList = this.data.dialogList.slice(0)
+
+          let tmpTranslate = Object.assign({}, item, {
+            autoPlay: true, // 自动播放背景音乐
+            translateVoicePath: resTrans.filename,
+            translateVoiceExpiredTime: resTrans.expired_time || 0
+          })
+
+          tmpDialogList[index] = tmpTranslate
+
+
+          this.setData({
+            dialogList: tmpDialogList,
+          })
+
+        } else {
+          console.warn("语音合成失败", resTrans, item)
+        }
+      },
+      fail: function(resTrans) {
+        console.warn("语音合成失败", resTrans, item)
+      }
+  })
+  },
+
+
+  /**
+   * 删除卡片
+   */
+  deleteItem: function(e) {
+    // console.log("deleteItem" ,e)
+    let detail = e.detail
+    let item = detail.item
+
+    let tmpDialogList = this.data.dialogList.slice(0)
+    let arrIndex = detail.index
+    tmpDialogList.splice(arrIndex, 1)
+    // 不使用setTImeout可能会触发 Error: Expect END descriptor with depth 0 but get another
+    setTimeout( ()=>{
+      this.setData({
+        dialogList: tmpDialogList
+      })
+    }, 0)
+
+  },
+
+
+  /**
+   * 识别内容为空时的反馈
+   */
+  showRecordEmptyTip: function() {
+    this.setData({
+      recording: false,
+      bottomButtonDisabled: false,
+    })
+    wx.showToast({
+      title: this.data.tips_language.recognize_nothing,
+      icon: 'success',
+      duration: 1000,
+      success: function (res) {
+
+      },
+      fail: function (res) {
+        console.log(res);
+      }
+    });
+  },
+
+
+  /**
+   * 初始化语音识别回调
+   * 绑定语音播放开始事件
+   */
+  initRecord: function() {
+    //有新的识别内容返回,则会调用此事件
+    manager.onRecognize = (res) => {
+      let currentData = Object.assign({}, this.data.currentTranslate, {
+                        text: res.result,
+                      })
+      this.setData({
+        currentTranslate: currentData,
+      })
+      this.scrollToNew();
+    }
+
+    // 识别结束事件
+    manager.onStop = (res) => {
+
+      let text = res.result
+
+      if(text == '') {
+        this.showRecordEmptyTip()
+        return
+      }
+
+      let lastId = this.data.lastId + 1
+
+      let currentData = Object.assign({}, this.data.currentTranslate, {
+                        text: res.result,
+                        translateText: '正在翻译中',
+                        id: lastId,
+                        voicePath: res.tempFilePath
+                      })
+
+      this.setData({
+        currentTranslate: currentData,
+        recordStatus: 1,
+        lastId: lastId,
+      })
+
+      this.scrollToNew();
+
+      this.translateText(currentData, this.data.dialogList.length)
+    }
+
+    // 识别错误事件
+    manager.onError = (res) => {
+
+      this.setData({
+        recording: false,
+        bottomButtonDisabled: false,
+      })
+
+    }
+
+    // 语音播放开始事件
+    wx.onBackgroundAudioPlay(res=>{
+
+      const backgroundAudioManager = wx.getBackgroundAudioManager()
+      let src = backgroundAudioManager.src
+
+      this.setData({
+        currentTranslateVoice: src
+      })
+
+    })
+  },
+
+  /**
+   * 设置语音识别历史记录
+   */
+  setHistory: function() {
+    try {
+      let dialogList = this.data.dialogList
+      dialogList.forEach(item => {
+        item.autoPlay = false
+      })
+      wx.setStorageSync('history',dialogList)
+
+    } catch (e) {
+
+      console.error("setStorageSync setHistory failed")
+    }
+  },
+
+  /**
+   * 得到历史记录
+   */
+  getHistory: function() {
+    try {
+      let history = wx.getStorageSync('history')
+      if (history) {
+          let len = history.length;
+          let lastId = this.data.lastId
+          if(len > 0) {
+            lastId = history[len-1].id || -1;
+          }
+          this.setData({
+            dialogList: history,
+            toView: this.data.toView,
+            lastId: lastId,
+          })
+      }
+
+    } catch (e) {
+      // Do something when catch error
+      this.setData({
+        dialogList: []
+      })
+    }
+  },
+
+  /**
+   * 重新滚动到底部
+   */
+  scrollToNew: function() {
+    this.setData({
+      toView: this.data.toView
+    })
+  },
+
+  onShow: function() {
+    this.scrollToNew();
+
+    if(this.data.recordStatus == 2) {
+      wx.showLoading({
+        // title: '',
+        mask: true,
+      })
+    }
+
+  },
+
+  onLoad: function () {
+    this.getHistory()
+    this.initRecord()
+
+
+    this.setData({toView: this.data.toView})
+
+
+    app.getRecordAuth()
+  },
+
+  onHide: function() {
+    this.setHistory()
+  },
+})

+ 6 - 0
pages/index/index.json

@@ -0,0 +1,6 @@
+{
+  "usingComponents": {
+    "bottom-button": "/components/bottom-button/index",
+    "result-bubble": "/components/result-bubble/index"
+  }
+}

+ 35 - 0
pages/index/index.wxml

@@ -0,0 +1,35 @@
+<!--index.wxml-->
+<view class="container">
+  <scroll-view id="scroll-content"
+    catchlongpress="longpressEvent"
+    scroll-top="{{scroll_top}}"
+    scroll-y="true"
+    class="dialog-part"
+    scroll-into-view="translate-{{toView}}"
+    enable-back-to-top="true"
+    scroll-with-animation="true">
+    <view class="dialog-wrap" id="translate-empty" wx:if="{{!recording && dialogList.length == 0}}">
+      <result-bubble item="{{initTranslate}}" record-status="0"></result-bubble>
+    </view>
+    <view wx:for="{{dialogList}}" wx:key="id" class="dialog-wrap" data-index="{{index}}" catchmodaldelete="deleteItem">
+      <result-bubble item="{{item}}"
+        edit-show="{{index==dialogList.length-1}}"
+        index="{{index}}"
+        current-translate-voice="{{currentTranslateVoice}}"
+        bindtranslate="translateTextAction"
+        bindexpired="expiredAction"></result-bubble>
+    </view>
+    <view class="dialog-wrap" id="translate-recording" wx:if="{{recording}}">
+      <result-bubble item="{{currentTranslate}}" record-status="{{recordStatus}}"></result-bubble>
+    </view>
+    <view id="translate-fake"></view>
+
+  </scroll-view>
+
+  <view class="foot-group" catchlongpress="catchTapEvent">
+    <bottom-button button-disabled="{{bottomButtonDisabled}}"
+      shadow-show="{{isScroll}}"
+      bindrecordstart="streamRecord"
+      bindrecordend="streamRecordEnd"></bottom-button>
+  </view>
+</view>

+ 138 - 0
pages/index/index.wxss

@@ -0,0 +1,138 @@
+/**index.wxss**/
+
+.flex-column {
+  display: flex;
+  display: -webkit-flex;
+  -webkit-flex-direction: column;
+  flex-direction: column;
+  align-items: center;
+  -webkit-align-items: center;
+  justify-content: space-between;
+  -webkit-justify-content: space-between;
+}
+
+.dialog-wrap {
+  position: relative;
+  padding: 0rpx 40rpx 50rpx 40rpx;
+  box-sizing: border-box;
+  display: flex;
+  display: -webkit-flex;
+  width: 100%;
+  flex-direction: column;
+  -webkit-flex-direction: column;
+}
+
+
+.send-message .text-detail {
+  color: #9B9B9B;
+}
+
+.dialog-part {
+  position: absolute;
+  left: 0;
+  top: 0;
+  bottom: 257rpx;
+  right: 0;
+  z-index:1;
+}
+
+.user-input {
+  flex: 1;
+  height: 60rpx;
+  box-sizing: border-box;
+  margin: 0 10px;
+  border-radius: 10rpx;
+}
+
+.text-content {
+  margin: 0 48px 0 0;
+  box-sizing: border-box;
+}
+
+
+.edit-icon {
+  position: absolute;
+  right: 10rpx;
+  bottom: 0;
+  padding: 0 8rpx;
+}
+
+.play-icon {
+  position: absolute;
+  right: 10rpx;
+  bottom: 14rpx;
+  padding: 0 8rpx;
+  display: flex;
+  align-items: center;
+}
+
+.play-loud-icon {
+  position: absolute;
+  right: 0;
+  bottom: 14rpx;
+  padding: 0 8rpx;
+  display: flex;
+  align-items: center;
+}
+
+
+.text-detail {
+  font-size: 18px;
+  line-height: 24px;
+  /*margin-right: 25px;*/
+  box-sizing: border-box;
+  font-family: "PingFang-SC-Regular", "SimSun", "Microsoft Yahei";
+  font-variant-ligatures: none;
+}
+
+
+.translate-message {
+  position: relative;
+}
+
+
+.send-message {
+  position: relative;
+}
+
+.create-time {
+  font-size: 14px;
+  color: #B2B2B2;
+  margin-bottom: 5px;
+  display: flex;
+  display: -webkit-flex;
+  justify-content: center;
+  -webkit-justify-content: center;
+}
+
+.filter-blur {
+  -webkit-filter: blur(5px);
+  filter: blur(5px);
+}
+
+.empty-tip {
+  position: absolute;
+  margin: auto;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  height: 24px;
+  width: 100px;
+  font-size: 24px;
+  color: #000000;
+  opacity: 0.1
+}
+
+.translate-fake {
+  width:100%;
+  height:1px
+}
+
+.foot-group {
+  position: fixed;
+  left: 0;
+  bottom: 0;
+  z-index: 40;
+  width: 100%;
+}

+ 36 - 0
project.config.json

@@ -0,0 +1,36 @@
+{
+	"description": "项目配置文件。",
+	"packOptions": {
+		"ignore": []
+	},
+	"setting": {
+		"urlCheck": false,
+		"es6": true,
+		"postcss": true,
+		"minified": true,
+		"newFeature": true
+	},
+	"compileType": "miniprogram",
+	"libVersion": "1.9.98",
+	"appid": "",
+	"projectname": "Face-2-Face Translator",
+	"isGameTourist": false,
+	"condition": {
+		"search": {
+			"current": -1,
+			"list": []
+		},
+		"conversation": {
+			"current": -1,
+			"list": []
+		},
+		"game": {
+			"currentL": -1,
+			"list": []
+		},
+		"miniprogram": {
+			"current": -1,
+			"list": []
+		}
+	}
+}

+ 88 - 0
utils/conf.js

@@ -0,0 +1,88 @@
+/*
+Tencent is pleased to support the open source community by making Face-2-Face Translator available.
+
+Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved.
+
+Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+http://opensource.org/licenses/MIT
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+*/
+
+let language = [
+  {
+    id: 0,
+    lang_name: "中文",
+    lang_content: "zh_CN",
+    lang_to: [ "en_US", ],
+    max_length:300,
+    source_language:"输入文字",
+    target_language:"输出文字",
+    hold_talk:"长按说话",
+    keyboard_input:"键盘输入",
+    type_here:"输入文字",
+    bg_content:"请输入翻译内容",
+    record_failed:"录制失败",
+    recognize_nothing:"识别内容为空",
+    time_left:"录音输入倒数",
+    text_left:"剩余文本长度",
+    prompt_time:"提示秒数",
+    upload_failed:"上传失败",
+    translating:"翻译中",
+    text_limit:"限制长度",
+    input_tip:"请输入有效文字",
+    request_failed:"请求失败",
+    delete_tip:"删除该项",
+    cancel:"取消",
+    bubble_tip:"请输入文本",
+    bg_bubble:"正在听你说话",
+    copy_source_text: "复制原文",
+    copy_target_text: "复制译文",
+    delete_item: "删除",
+    exceed_network:"网络请求失败",
+    retry_network:"尝试重新连接",
+    wait_last_record:"请等待翻译结束",
+    access_auth:"请检查权限",
+    access_network:"网络错误",
+    login:"登录",
+  },
+  {
+    id: 1,
+    lang_name: "EN",
+    lang_content: "en_US",
+    lang_to: [ "zh_CN", ],
+    max_length: 1800,
+    source_language: "Source Language",
+    target_language: "Target Language",
+    hold_talk: "Hold To Talk",
+    keyboard_input: "Keyboard",
+    type_here: "Type here...",
+    bg_content: "Please enter the content to be translated",
+    record_failed: "Audio recording failed",
+    recognize_nothing: "Nothing recognized",
+    time_left: "Recording time left",
+    text_left: "Inputing text left",
+    prompt_time: "Prompt time",
+    upload_failed: "Upload failed",
+    translating: "Translating",
+    text_limit: "Text length has reached the limit",
+    input_tip: "Please enter valid text",
+    request_failed: "Request failed",
+    delete_tip: "Delete this item",
+    cancel:"Cancel",
+    bubble_tip:"Please input English content",
+    bg_bubble:"Please speak English",
+    copy_source_text: "Copy Source",
+    copy_target_text: "Copy Target",
+    delete_item: "Delete",
+    exceed_network: "Network request failed",
+    retry_network: "Retry connect",
+    access_auth:"Please checkout authorization",
+    access_network:"Network error",
+    login:"Login",
+  }
+]
+
+module.exports = {
+  language: language
+}

+ 35 - 0
utils/util.js

@@ -0,0 +1,35 @@
+
+const formatTime = date => {
+  const year = date.getFullYear()
+  const month = date.getMonth() + 1
+  const day = date.getDate()
+  const hour = date.getHours()
+  const minute = date.getMinutes()
+  const second = date.getSeconds()
+
+  return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':')
+}
+
+function recordTime(date) {
+
+  var month = date.getMonth() + 1
+  var day = date.getDate()
+
+  var hour = date.getHours()
+  var minute = date.getMinutes()
+
+  return [month, day].map(formatNumber).join('/') + ' ' + [hour, minute].map(formatNumber).join(':')
+}
+
+const formatNumber = n => {
+  n = n.toString()
+  return n[1] ? n : '0' + n
+}
+
+
+
+
+module.exports = {
+  formatTime: formatTime,
+  recordTime: recordTime,
+}