Parcourir la source

feature:新增合同管理

liuzl il y a 2 ans
Parent
commit
90ef17b6bd
43 fichiers modifiés avec 4016 ajouts et 221 suppressions
  1. 42 0
      frontend_mobile/api/contract/index.js
  2. 0 4
      frontend_mobile/api/customer/index.js
  3. 28 12
      frontend_mobile/api/project/index.js
  4. 21 0
      frontend_mobile/api/system/index.js
  5. 11 6
      frontend_mobile/components/CustomerContact.vue
  6. 11 6
      frontend_mobile/components/ProjectContact.vue
  7. 12 7
      frontend_mobile/components/SelectUser.vue
  8. 2 2
      frontend_mobile/package.json
  9. 187 142
      frontend_mobile/pages.json
  10. 235 0
      frontend_mobile/pages/contract/collection.vue
  11. 120 0
      frontend_mobile/pages/contract/components/contractCollection.vue
  12. 185 0
      frontend_mobile/pages/contract/components/contractDetail.vue
  13. 118 0
      frontend_mobile/pages/contract/components/contractDynamics.vue
  14. 113 0
      frontend_mobile/pages/contract/components/contractInvoice.vue
  15. 90 0
      frontend_mobile/pages/contract/components/contractProduct.vue
  16. 366 0
      frontend_mobile/pages/contract/detail.vue
  17. 455 0
      frontend_mobile/pages/contract/index.vue
  18. 240 0
      frontend_mobile/pages/contract/invoice.vue
  19. 4 2
      frontend_mobile/pages/customer/add.vue
  20. 3 0
      frontend_mobile/pages/customer/components/contacts.vue
  21. 10 1
      frontend_mobile/pages/customer/details.vue
  22. 1 1
      frontend_mobile/pages/customer/index.vue
  23. 1 1
      frontend_mobile/pages/customer/transfer.vue
  24. 115 11
      frontend_mobile/pages/home/index.vue
  25. 1 1
      frontend_mobile/pages/openSeaCustomer/index.vue
  26. 12 10
      frontend_mobile/pages/project/components/contacts.vue
  27. 41 10
      frontend_mobile/pages/project/details.vue
  28. 1 1
      frontend_mobile/pages/project/index.vue
  29. 1 1
      frontend_mobile/pages/project/transfer.vue
  30. 1 1
      frontend_mobile/pages/publicPages/follow.vue
  31. 1 1
      frontend_mobile/pages/schedule/index.vue
  32. 132 0
      frontend_mobile/uni_modules/lime-echart/changelog.md
  33. 372 0
      frontend_mobile/uni_modules/lime-echart/components/l-echart/canvas.js
  34. 516 0
      frontend_mobile/uni_modules/lime-echart/components/l-echart/l-echart.vue
  35. 74 0
      frontend_mobile/uni_modules/lime-echart/components/l-echart/utils.js
  36. 0 0
      frontend_mobile/uni_modules/lime-echart/components/lime-echart/index.vue
  37. 84 0
      frontend_mobile/uni_modules/lime-echart/package.json
  38. 245 0
      frontend_mobile/uni_modules/lime-echart/readme.md
  39. 0 0
      frontend_mobile/uni_modules/lime-echart/static/ecStat.min.js
  40. 34 0
      frontend_mobile/uni_modules/lime-echart/static/echarts.min.js
  41. 129 0
      frontend_mobile/uni_modules/lime-echart/static/index.html
  42. 0 0
      frontend_mobile/uni_modules/lime-echart/static/uni.webview.1.5.3.js
  43. 2 1
      frontend_mobile/utils/index.js

+ 42 - 0
frontend_mobile/api/contract/index.js

@@ -0,0 +1,42 @@
+/*
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-01-29 16:17:15
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-02-14 16:45:06
+ * @Description: file content
+ * @FilePath: \crm\api\customer\index.js
+ */
+
+import micro_request from '@/utils/micro_request'
+
+const basePath = process.uniEnv.VUE_APP_ParentPath
+export default {
+  // 获取合同列表
+  getList(query) {
+    return micro_request.postRequest(basePath, 'CtrContract', 'List', query)
+  },
+  // 获取合同详情
+  getDetails(query) {
+    return micro_request.postRequest(basePath, 'CtrContract', 'Get', query)
+  },
+  // 活动列表
+  getDynamicsList(query) {
+    return micro_request.postRequest(basePath, 'CtrContract', 'DynamicsList', query)
+  },
+  // 获取回款列表
+  getRePay(query) {
+    return micro_request.postRequest(basePath, 'CtrContractCollection', 'List', query)
+  },
+  // 获取发票列表
+  getInvoice(query) {
+    return micro_request.postRequest(basePath, 'CtrContractInvoice', 'List', query)
+  },
+  // 新增发票
+  addInvoice(query) {
+    return micro_request.postRequest(basePath, 'CtrContractInvoice', 'Add', query)
+  },
+  // 新增回款
+  addCollection(query) {
+    return micro_request.postRequest(basePath, 'CtrContractCollection', 'Add', query)
+  },
+}

+ 0 - 4
frontend_mobile/api/customer/index.js

@@ -55,8 +55,4 @@ export default {
   getListByDay(query) {
     return micro_request.postRequest(basePath, 'FollowUp', 'GetListByDay', query)
   },
-  // 查询省市区
-  getProvinceDetail(query) {
-    return micro_request.postRequest(basePath, 'District', 'GetList', query)
-  },
 }

+ 28 - 12
frontend_mobile/api/project/index.js

@@ -11,16 +11,32 @@ import micro_request from '@/utils/micro_request'
 
 const basePath = process.uniEnv.VUE_APP_ParentPath
 export default {
-    // 项目列表
-    getList(query) {
-        return micro_request.postRequest(basePath, 'Business', 'GetList', query)
+  // 项目列表
+  getList(query) {
+    return micro_request.postRequest(basePath, 'Business', 'GetList', query)
+  },
+  // 项目转移
+  transfer(query) {
+    return micro_request.postRequest(basePath, 'Business', 'BusinessTransfer', query)
+  },
+  // 项目详情
+  getDetail(query) {
+    return micro_request.postRequest(basePath, 'Business', 'GetEntityById', query)
+  },
+  // 项目创建
+  create(query) {
+    return micro_request.postRequest(basePath, 'Business', 'Create', query)
+  },
+  // 转为储备项目
+  toReserve(query) {
+    return micro_request.postRequest(basePath, 'Business', 'ConvertToReserve', query)
+  },
+  // 项目降级
+  downgrade(query) {
+    return micro_request.postRequest(basePath, 'Business', 'BusinessDowngrade', query)
+  },
+    // 项目升级
+   upgrade(query) {
+      return micro_request.postRequest(basePath, 'Business', 'BusinessUpgrade', query)
     },
-    // 项目转移
-    transfer(query) {
-        return micro_request.postRequest(basePath, 'Business', 'BusinessTransfer', query)
-    },
-    // 项目详情
-    getDetail(query) {
-        return micro_request.postRequest(basePath, 'Business', 'GetEntityById', query)
-    },
-}
+}

+ 21 - 0
frontend_mobile/api/system/index.js

@@ -0,0 +1,21 @@
+/*
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2023-02-20 09:56:06
+ * @LastEditors: wanglj
+ * @LastEditTime: 2023-02-20 10:01:11
+ * @Description: file content
+ * @FilePath: \frontend_mobile\api\system\index.js
+ */
+import micro_request from '@/utils/micro_request'
+
+const basePath = process.uniEnv.VUE_APP_ParentPath
+export default {
+  // 指标图表
+  getHomeDataReportData(query) {
+    return micro_request.postRequest(basePath, 'Home', 'GetHomeDataReportData', query)
+  },
+  // 获取首页
+  getHomeReport(query) {
+    return micro_request.postRequest(basePath, 'Home', 'GetHomeConfig', query)
+  },
+}

+ 11 - 6
frontend_mobile/components/CustomerContact.vue

@@ -8,7 +8,7 @@
 -->
 <template>
   <view>
-    <u-popup :show="concatVisible" @close="close">
+    <u-popup :show="selectVisible" @close="close">
       <view class="tit">选择客户联系人</view>
       <view class="search-container">
         <view class="search-input">
@@ -44,7 +44,7 @@
     name: 'OmsCustomerContact',
     data() {
       return {
-        concatVisible: false,
+        selectVisible: false,
         userList: [],
         queryForm: {
           custId: 0,
@@ -64,17 +64,22 @@
         let params = this.queryForm
         const [err, res] = await to(userApi.getContact(params))
         if (err) return
-        if (res.code == 200 && res.data.list.length > 0)
-          this.userList = res.data.list.map((item) => ({ id: item.id, label: item.cuctName }))
+        if (res.code == 200) {
+          if (res.data.list.length > 0) {
+            this.userList = res.data.list.map((item) => ({ ...item, label: item.cuctName }))
+          } else {
+            this.userList = []
+          }
+        }
       },
       open(id) {
         console.log(id)
         this.queryForm.custId = id
-        this.concatVisible = true
+        this.selectVisible = true
         this.getUserList()
       },
       close() {
-        this.concatVisible = false
+        this.selectVisible = false
         this.$emit('close', this.selected)
       },
       radioChange(n) {

+ 11 - 6
frontend_mobile/components/ProjectContact.vue

@@ -8,7 +8,7 @@
 -->
 <template>
   <view>
-    <u-popup :show="concatVisible" @close="close">
+    <u-popup :show="selectVisible" @close="close">
       <view class="tit">选择项目联系人</view>
       <view class="search-container">
         <view class="search-input">
@@ -44,7 +44,7 @@
     name: 'OmsCustomerContact',
     data() {
       return {
-        concatVisible: false,
+        selectVisible: false,
         userList: [],
         queryForm: {
           busId: 0,
@@ -64,17 +64,22 @@
         let params = this.queryForm
         const [err, res] = await to(userApi.getProjectContact(params))
         if (err) return
-        if (res.code == 200 && res.data.list.length > 0)
-          this.userList = res.data.list.map((item) => ({ id: item.id, label: item.cuctName }))
+        if (res.code == 200) {
+          if (res.data.list.length > 0) {
+            this.userList = res.data.list.map((item) => ({ id: item.id, label: item.cuctName }))
+          } else {
+            this.userList = []
+          }
+        }
       },
       open(busId, custId) {
         this.queryForm.busId = busId
         this.queryForm.custId = custId
-        this.concatVisible = true
+        this.selectVisible = true
         this.getUserList()
       },
       close() {
-        this.concatVisible = false
+        this.selectVisible = false
         this.$emit('close', this.selected)
       },
       radioChange(n) {

+ 12 - 7
frontend_mobile/components/SelectUser.vue

@@ -8,7 +8,7 @@
 -->
 <template>
   <view>
-    <u-popup :show="concatVisible" @close="close">
+    <u-popup :show="selectVisible" @close="close">
       <view class="tit">选择员工</view>
       <view class="search-container">
         <view class="search-input">
@@ -19,7 +19,7 @@
             v-model="queryForm.keyWords"
             prefixIcon="search"
             prefixIconStyle="font-size: 22px;color: #909399"
-            placeholder="请输入客户名称"
+            placeholder="请输入姓名"
             shape="circle"
             border="surround"></u-input>
         </view>
@@ -52,7 +52,7 @@
     },
     data() {
       return {
-        concatVisible: false,
+        selectVisible: false,
         userList: [],
         queryForm: {
           keyWords: '',
@@ -70,15 +70,20 @@
         let params = Object.assign(this.queryForm, this.queryParams)
         const [err, res] = await to(userApi.getList(params))
         if (err) return
-        if (res.code == 200 && res.data.list.length > 0)
-          this.userList = res.data.list.map((item) => ({ id: item.id, label: item.nickName }))
+        if (res.code == 200) {
+          if (res.data.list) {
+            this.userList = res.data.list.map((item) => ({ id: item.id, label: item.nickName }))
+          } else {
+            this.userList = []
+          }
+        }
       },
       open() {
-        this.concatVisible = true
+        this.selectVisible = true
         this.getUserList()
       },
       close() {
-        this.concatVisible = false
+        this.selectVisible = false
         this.$emit('close', this.selected)
       },
       radioChange(n) {

+ 2 - 2
frontend_mobile/package.json

@@ -6,14 +6,14 @@
     "dependencies": {
         "await-to-js": "^3.0.0",
         "axios": "^0.27.2",
+        "echarts": "^5.4.1",
         "qs": "^6.11.0",
         "uview-ui": "^2.0.31",
         "vuex": "^4.1.0"
     },
-    "devDependencies": {},
     "scripts": {
         "test": "echo \"Error: no test specified\" && exit 1"
     },
     "author": "",
     "license": "ISC"
-}
+}

+ 187 - 142
frontend_mobile/pages.json

@@ -1,146 +1,191 @@
 {
-  "easycom": {
-    "^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
-  },
-  "pages": [
-    {
-      "path": "pages/project/index",
-      "style": {
-        "navigationStyle": "custom"
-      },
-      "tit": "项目管理首页"
+    "easycom": {
+        "^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
     },
-    {
-      "path": "pages/project/transfer",
-      "style": {
-        "navigationStyle": "custom"
-      },
-      "tit": "转移项目"
+    "pages": [{
+            "path": "pages/login/index",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "登录"
+        },
+        {
+            "path": "pages/home/index",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "首页"
+        },
+        {
+            "path": "pages/contract/index",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "合同管理"
+        },
+        {
+            "path": "pages/contract/detail",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "合同详情"
+        },
+        {
+            "path": "pages/contract/invoice",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "新建发票"
+        },
+        {
+            "path": "pages/contract/collection",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "新建回款"
+        },
+        {
+            "path": "pages/project/index",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "项目管理首页"
+        },
+        {
+            "path": "pages/project/transfer",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "转移项目"
+        },
+        {
+            "path": "pages/project/details",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "项目详情"
+        },
+        {
+            "path": "pages/customer/details",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "客户详情"
+        },
+        {
+            "path": "pages/customer/index",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "储备客户"
+        },
+        {
+            "path": "pages/openSeaCustomer/index",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "公海客户"
+        },
+        {
+            "path": "pages/publicPages/follow",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "填写跟进"
+        },
+        {
+            "path": "pages/customer/transfer",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "转移客户"
+        },
+        {
+            "path": "pages/customer/add",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "客户添加"
+        },
+        {
+            "path": "pages/project/create",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "创建项目"
+        },
+        {
+            "path": "pages/project/downgrade",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "项目降级"
+        },
+        {
+            "path": "pages/project/upgrade",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "项目升级"
+        },
+        {
+            "path": "pages/message/index",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "信息"
+        },
+        {
+            "path": "pages/my/index",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "我的"
+        },
+        {
+            "path": "pages/schedule/index",
+            "style": {
+                "navigationStyle": "custom"
+            },
+            "tit": "日程"
+        }
+    ],
+    "tabBar": {
+        "color": "#7A7E83",
+        "selectedColor": "#3E7EF8",
+        "borderStyle": "black",
+        "backgroundColor": "#ffffff",
+        "list": [{
+                "pagePath": "pages/home/index",
+                "iconPath": "static/images/tabBar/home.png",
+                "selectedIconPath": "static/images/tabBar/home-active.png",
+                "text": "首页"
+            },
+            {
+                "pagePath": "pages/schedule/index",
+                "iconPath": "static/images/tabBar/schedule.png",
+                "selectedIconPath": "static/images/tabBar/schedule-active.png",
+                "text": "日程"
+            },
+            {
+                "pagePath": "pages/message/index",
+                "iconPath": "static/images/tabBar/message.png",
+                "selectedIconPath": "static/images/tabBar/message-active.png",
+                "text": "消息"
+            },
+            {
+                "pagePath": "pages/my/index",
+                "iconPath": "static/images/tabBar/my.png",
+                "selectedIconPath": "static/images/tabBar/my-active.png",
+                "text": "我的"
+            }
+        ]
     },
-    {
-      "path": "pages/project/details",
-      "style": {
-        "navigationStyle": "custom"
-      },
-      "tit": "项目详情"
+    "globalStyle": {
+        "navigationBarTextStyle": "black",
+        "navigationBarTitleText": "oms",
+        "navigationBarBackgroundColor": "#F8F8F8",
+        "backgroundColor": "#F8F8F8"
     },
-    {
-      "path": "pages/customer/details",
-      "style": {
-        "navigationStyle": "custom"
-      },
-      "tit": "客户详情"
-    },
-    {
-      "path": "pages/customer/index",
-      "style": {
-        "navigationStyle": "custom"
-      },
-      "tit": "储备客户"
-    },
-    {
-      "path": "pages/openSeaCustomer/index",
-      "style": {
-        "navigationStyle": "custom"
-      },
-      "tit": "公海客户"
-    },
-    {
-      "path": "pages/login/index",
-      "style": {
-        "navigationStyle": "custom"
-      },
-      "tit": "登录"
-    },
-
-    {
-      "path": "pages/home/index",
-      "style": {
-        "navigationStyle": "custom"
-      },
-      "tit": "首页"
-    },
-
-    {
-      "path": "pages/publicPages/follow",
-      "style": {
-        "navigationStyle": "custom"
-      },
-      "tit": "填写跟进"
-    },
-    {
-      "path": "pages/customer/transfer",
-      "style": {
-        "navigationStyle": "custom"
-      },
-      "tit": "转移客户"
-    },
-    {
-      "path": "pages/customer/add",
-      "style": {
-        "navigationStyle": "custom"
-      },
-      "tit": "客户添加"
-    },
-    {
-      "path": "pages/message/index",
-      "style": {
-        "navigationStyle": "custom"
-      },
-      "tit": "信息"
-    },
-    {
-      "path": "pages/my/index",
-      "style": {
-        "navigationStyle": "custom"
-      },
-      "tit": "我的"
-    },
-    {
-      "path": "pages/schedule/index",
-      "style": {
-        "navigationStyle": "custom"
-      },
-      "tit": "日程"
-    }
-  ],
-  "tabBar": {
-    "color": "#7A7E83",
-    "selectedColor": "#3E7EF8",
-    "borderStyle": "black",
-    "backgroundColor": "#ffffff",
-    "list": [
-      {
-        "pagePath": "pages/home/index",
-        "iconPath": "static/images/tabBar/home.png",
-        "selectedIconPath": "static/images/tabBar/home-active.png",
-        "text": "首页"
-      },
-      {
-        "pagePath": "pages/schedule/index",
-        "iconPath": "static/images/tabBar/schedule.png",
-        "selectedIconPath": "static/images/tabBar/schedule-active.png",
-        "text": "日程"
-      },
-      {
-        "pagePath": "pages/message/index",
-        "iconPath": "static/images/tabBar/message.png",
-        "selectedIconPath": "static/images/tabBar/message-active.png",
-        "text": "消息"
-      },
-      {
-        "pagePath": "pages/my/index",
-        "iconPath": "static/images/tabBar/my.png",
-        "selectedIconPath": "static/images/tabBar/my-active.png",
-        "text": "我的"
-      }
-    ]
-  },
-  "globalStyle": {
-    "navigationBarTextStyle": "black",
-    "navigationBarTitleText": "oms",
-    "navigationBarBackgroundColor": "#F8F8F8",
-    "backgroundColor": "#F8F8F8"
-  },
-  "uniIdRouter": {}
-}
+    "uniIdRouter": {}
+}

+ 235 - 0
frontend_mobile/pages/contract/collection.vue

@@ -0,0 +1,235 @@
+<template>
+  <!-- 新建回款 -->
+  <view class="home">
+    <view class="nav">
+      <view :style="{ paddingTop }">
+        <view class="title" :style="[{ height }, { lineHeight: height }]">
+          <view class="back" @click="goBack()">
+            <u-icon name="arrow-left" color="#ffffff" size="22"></u-icon>
+          </view>
+          <text>新建回款</text>
+        </view>
+      </view>
+    </view>
+    <view class="main">
+      <u-form :model="form" :rules="rules" ref="form" label-width="0">
+        <u-form-item prop="contractCode">
+          <view class="form-label flex_l">
+            <view class="label-tag"></view>
+            合同编号
+          </view>
+          <u-input v-model="form.contractCode" disabled />
+        </u-form-item>
+        <u-form-item prop="collectionAmount">
+          <view class="form-label flex_l">
+            <view class="label-tag"></view>
+            回款金额
+          </view>
+          <u-input v-model.number="form.collectionAmount" placeholder="请输入回款金额" />
+        </u-form-item>
+        <u-form-item prop="collectionDatetime" @click="show = true">
+          <view class="form-label flex_l">
+            <view class="label-tag"></view>
+            回款日期
+          </view>
+          <u-input v-model="form.collectionDatetime" disabled disabledColor="#ffffff" placeholder="请选择回款日期"></u-input>
+        </u-form-item>
+        <u-form-item prop="collectionType" @click="showPicker = true">
+          <view class="form-label flex_l">
+            <view class="label-tag"></view>
+            回款方式
+          </view>
+          <u-input v-model="form.collectionName" disabled disabledColor="#ffffff" placeholder="请选择回款方式" />
+        </u-form-item>
+        <u-form-item prop="remark">
+          <view class="form-label flex_l">
+            <view class="label-tag"></view>
+            备注
+          </view>
+          <u-textarea fontSize="26rpx" v-model="form.remark" placeholder="输入备注" height="180" :count="true"
+            maxlength="300"></u-textarea>
+        </u-form-item>
+      </u-form>
+      <view class="save" @click="save">保存</view>
+    </view>
+    <u-calendar :show="show" @confirm="confirmCalendar" @close="show = false"></u-calendar>
+    <u-action-sheet :actions="collectionTypeOption" @select="selectClick" :show="showPicker"></u-action-sheet>
+    <u-toast ref="uToast"></u-toast>
+  </view>
+</template>
+
+<script>
+  import api from '@/api/contract'
+  import to from 'await-to-js'
+  export default {
+    data() {
+      return {
+        height: '',
+        paddingTop: '',
+        form: {
+          planId: null, //计划回款id
+          collectionDatetime: '', //回款日期
+          contractCode: '', //合同编号
+          contractId: null, //合同id
+          collectionAmount: '', //回款金额
+          collectionType: '', //回款方式
+          collectionName:'',
+          remark: '', //备注
+        },
+        rules: {
+          contractCode: [{
+            required: true,
+            trigger: 'blur',
+            message: '请选择合同'
+          }],
+          collectionAmount: [{
+            required: true,
+            trigger: 'blur',
+            message: '请输入回款金额'
+          }],
+          collectionDatetime: [{
+            required: true,
+            trigger: 'change',
+            message: '请选择回款日期'
+          }, ],
+        },
+        show: false,
+        showPicker: false,
+        collectionTypeOption: []
+      }
+    },
+    created() {
+      const navData = uni.getMenuButtonBoundingClientRect()
+      this.height = navData.height + 'px'
+      this.paddingTop = navData.top + 'px'
+      this.getOptions()
+    },
+    onLoad(option) {
+      console.log(option.id) //打印出上个页面传递的参数。
+      this.form.contractId = parseInt(option.id)
+      this.form.contractCode = option.code
+    },
+    methods: {
+      getOptions() {
+        Promise.all([this.getDicts('collection_type')])
+          .then(([collectionType]) => {
+            this.collectionTypeOption = collectionType.data.values || []
+            this.collectionTypeOption.forEach(item => item.name = item.value)
+          })
+          .catch((err) => console.log(err))
+      },
+      closeMoveInModel() {
+
+      },
+      confirmCalendar(val) {
+        this.form.collectionDatetime = val[0]
+        this.show = false
+      },
+      selectClick(val) {
+        this.form.collectionType = val.key
+        this.form.collectionName = val.value
+        this.showPicker = false
+      },
+      async save() {
+        let params = {
+          ...this.form
+        }
+        delete params.collectionName
+        params.collectionAmount = parseInt(params.collectionAmount)
+        this.$refs.form.validate().then(async valid => {
+          if (valid) {
+            console.log(valid);
+            const [err, res] = await to(api.addCollection(params))
+            if (err) return
+            this.$refs.uToast.show({
+              type: 'success',
+              message: '创建成功',
+              complete: () => {
+                this.goBack()
+              },
+            })
+          }
+        }).catch(errors => {})
+      },
+      goBack() {
+        uni.navigateBack({
+          //关闭当前页面,返回上一页面或多级页面。
+          delta: 1,
+        })
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  .home {
+    padding-top: 188rpx;
+
+    .nav {
+      position: absolute;
+      left: 0;
+      top: 0;
+      width: 100%;
+      height: 284rpx;
+      background: #3e7ef8;
+
+      .title {
+        position: relative;
+        text-align: center;
+        font-size: 32rpx;
+        font-weight: bold;
+        color: #ffffff;
+
+        .back {
+          position: absolute;
+          top: 0;
+          bottom: 0;
+          margin: auto;
+          left: 70rpx;
+          display: flex;
+        }
+      }
+    }
+
+    .main {
+      position: absolute;
+      width: 100%;
+      height: calc(100vh - 280rpx);
+      background: #ffffff;
+      border-radius: 31rpx 31rpx 0 0;
+      padding: 0 32rpx;
+      overflow: auto;
+      padding-bottom: 64rpx;
+
+      .form-label {
+        font-size: 32rpx;
+        font-weight: bold;
+        color: #323232;
+        padding-bottom: 18rpx;
+
+        .label-tag {
+          width: 15rpx;
+          height: 15rpx;
+          background: rgba(62, 126, 248, 0.6);
+          border-radius: 50%;
+          margin-right: -4rpx;
+        }
+      }
+
+      .save {
+        position: fixed;
+        bottom: 0;
+        left: 0;
+        width: 100%;
+        height: 92rpx;
+        background: #3e7ef8;
+        border-radius: 31rpx 31rpx 0 0;
+        margin: 116rpx auto 0;
+        font-size: 32rpx;
+        color: #ffffff;
+        text-align: center;
+        line-height: 92rpx;
+      }
+    }
+  }
+</style>

+ 120 - 0
frontend_mobile/pages/contract/components/contractCollection.vue

@@ -0,0 +1,120 @@
+<template>
+  <view class="todo-list">
+    <u-empty mode="list" text="暂无记录" v-if="collectList.length == 0"></u-empty>
+    <view v-else v-for="(v, i) in collectList" :key="i" class="todo-item">
+      <view class="cust-name">
+        <u-text :bold="true" size="28rpx" :text="v.custName"></u-text>
+      </view>
+      <u-row>
+        <u-col span="12">
+          <view class="header">
+            <view class="content flex">
+              <text>回款金额:{{ formatPrice(v.collectionAmount) }}</text>
+            </view>
+            <view class="content flex">
+              <text>回款方式:{{ collectionTypeOption.filter((item) => item.key == v.collectionType)[0].value || '-' }}</text>
+            </view>
+            <view class="content flex">
+              <text>合同编号:{{ v.contractCode }}</text>
+            </view>
+            <view class="content-footer flex1">
+              <text class="date">{{ v.collectionDatetime }}</text>
+              <view class="flex flex-middle">
+                <text>审核状态:{{ v.approStatus == '10' ? '未回款' : '已回款' }}</text>
+              </view>
+            </view>
+          </view>
+        </u-col>
+      </u-row>
+    </view>
+  </view>
+</template>
+
+<script>
+  import to from 'await-to-js'
+  import api from '@/api/contract'
+  export default {
+    name: 'OmsContractCollection',
+    props: {
+      contractId: {
+        type: [String, Number],
+        default: '0',
+      },
+    },
+    data() {
+      return {
+        collectList: [],
+        collectionTypeOption: [], //,回款方式
+      }
+    },
+    created() {
+      this.getOptions()
+    },
+    mounted() {
+      this.fetchData()
+    },
+    methods: {
+      async getOptions() {
+        Promise.all([this.getDicts('collection_type')])
+          .then(([collectionType]) => {
+            this.collectionTypeOption = collectionType.data.values || []
+          })
+          .catch((err) => console.log(err))
+      },
+      async fetchData() {
+        const [err, res] = await to(api.getRePay({
+          contractId: this.contractId,
+          pageNum: 0
+        }))
+        if (err) return
+        this.collectList = res.data.list
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .todo-list {
+    height: 100%;
+    overflow: auto;
+
+    .cust-name {
+      font-size: 28rpx;
+    }
+
+    .todo-item {
+      padding: 12rpx 40rpx 12rpx 46rpx;
+      background: #f2f3f5;
+      border-radius: 15rpx;
+      margin-bottom: 32rpx;
+
+      .tit-txt {
+        font-size: 28rpx;
+        font-weight: bold;
+        color: #323232;
+      }
+
+      .content {
+        margin-top: 10rpx;
+        font-size: 28rpx;
+        color: #646464;
+      }
+
+      .content-footer {
+        padding: 10rpx 0 12rpx 0;
+        font-size: 24rpx;
+
+        .date {
+          color: #969696;
+        }
+
+        .user-img {
+          width: 46rpx;
+          height: 46rpx;
+          border-radius: 50%;
+          margin-right: 15rpx;
+        }
+      }
+    }
+  }
+</style>

+ 185 - 0
frontend_mobile/pages/contract/components/contractDetail.vue

@@ -0,0 +1,185 @@
+<!--
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-02-15 16:25:58
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-02-16 11:17:12
+ * @Description: file content
+ * @FilePath: \oms\pages\customer\components\customerDetail.vue
+-->
+<template>
+  <view>
+    <view class="info-item">
+      <u-row justify="space-between" gutter="10">
+        <u-col span="12">
+          <view class="flex_l">
+            <view class="label">合同编号:</view>
+            <view class="desc">{{ detail.contractCode }}</view>
+          </view>
+        </u-col>
+      </u-row>
+    </view>
+    <view class="info-item">
+      <u-row>
+        <u-col span="12">
+          <view class="flex_l">
+            <view class="label">合同名称:</view>
+            <view class="desc">{{ detail.contractName }}</view>
+          </view>
+        </u-col>
+      </u-row>
+    </view>
+    <view class="info-item">
+      <u-row>
+        <u-col span="12">
+          <view class="flex_l">
+            <view class="label">客户名称:</view>
+            <view class="desc">{{ detail.custName }}</view>
+          </view>
+        </u-col>
+      </u-row>
+    </view>
+    <view class="info-item">
+      <u-row justify="space-between" gutter="10">
+        <u-col span="6">
+          <view class="flex_l">
+            <view class="label">项目名称:</view>
+            <view class="desc">{{ detail.nboName }}</view>
+          </view>
+        </u-col>
+        <u-col span="6">
+          <view class="flex_l">
+            <view class="label">合同金额:</view>
+            <view class="desc">{{ formatPrice(detail.contractAmount)}}</view>
+          </view>
+        </u-col>
+      </u-row>
+    </view>
+    <view class="info-item">
+      <u-row justify="space-between" gutter="10">
+        <u-col span="6">
+          <view class="flex_l">
+            <view class="label">负责人:</view>
+            <view class="desc">{{ detail.inchargeName }}</view>
+          </view>
+        </u-col>
+        <u-col span="6">
+          <view class="flex_l">
+            <view class="label">公司签约人:</view>
+            <view class="desc">{{ detail.signatoryName }}</view>
+          </view>
+        </u-col>
+      </u-row>
+    </view>
+    <view class="info-item">
+      <u-row justify="space-between" gutter="10">
+        <u-col span="6">
+          <view class="flex_l">
+            <view class="label">开始时间:</view>
+            <view class="desc">{{ parseTime(detail.contractStartTime, '{y}-{m}-{d}') }}</view>
+          </view>
+        </u-col>
+        <u-col span="6">
+          <view class="flex_l">
+            <view class="label">结束时间:</view>
+            <view class="desc">{{ parseTime(detail.contractEndTime, '{y}-{m}-{d}') }}</view>
+          </view>
+        </u-col>
+      </u-row>
+    </view>
+    <view class="info-item">
+      <u-row justify="space-between" gutter="10">
+        <u-col span="6">
+          <view class="flex_l">
+            <view class="label">合同类型:</view>
+            <view class="desc">{{ detail.contractType }}</view>
+          </view>
+        </u-col>
+        <u-col span="6">
+          <view class="flex_l">
+            <view class="label">创建人:</view>
+            <view class="desc">{{ detail.createdName }}</view>
+          </view>
+        </u-col>
+      </u-row>
+    </view>
+    <view class="info-item">
+      <u-row justify="space-between" gutter="10">
+        <u-col span="6">
+          <view class="flex_l">
+            <view class="label">创建时间:</view>
+            <view class="desc">{{ parseTime(detail.createdTime, '{y}-{m}-{d}') }}</view>
+          </view>
+        </u-col>
+        <u-col span="6">
+          <view class="flex_l">
+            <view class="label">更新时间:</view>
+            <view class="desc">{{ parseTime(detail.updatedTime, '{y}-{m}-{d}') }}</view>
+          </view>
+        </u-col>
+      </u-row>
+    </view>
+    <view class="info-item">
+      <u-row justify="space-between" gutter="10">
+        <u-col span="12">
+          <view class="flex_l">
+            <view class="label">备注:</view>
+            <view class="desc">{{ detail.remark }}</view>
+          </view>
+        </u-col>
+      </u-row>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'OmsContractDetail',
+    props: {
+      detail: {
+        type: [Object],
+        default: {},
+      },
+      abstract: {
+        type: [Object],
+        default: {},
+      },
+    },
+    data() {
+      return {
+        // abstract: {
+        //   followContent: '', //跟进次数
+        //   notFollowDay: '', //未跟进天数
+        //   business: '', //项目数量
+        //   businessTotal: '', //项目总额
+        //   dealCotal: '', //成交次数
+        //   dealTotal: '', //成交总额
+        //   paymentTotal: '', //回款总额
+        //   notPaymentTotal: '', //未回款总额
+        //   drawTotal: '', //开票总额
+        // },
+      }
+    },
+
+    mounted() {
+      console.log(this.abstract)
+    },
+
+    methods: {},
+  }
+</script>
+
+<style lang="scss" scoped>
+  .info-item {
+    padding: 20rpx;
+
+    .label {
+      color: #646464;
+      font-size: 26rpx;
+    }
+
+    .desc {
+      font-size: 26rpx;
+      text-indent: 20rpx;
+    }
+  }
+</style>

+ 118 - 0
frontend_mobile/pages/contract/components/contractDynamics.vue

@@ -0,0 +1,118 @@
+<template>
+  <view class="todo-list">
+    <u-empty mode="list" text="暂无记录" v-if="records.length == 0"></u-empty>
+    <view v-else v-for="(v, k) in records" :key="i">
+      <view class="follow-date">
+        <u-text :bold="true" size="26rpx" :text="k"></u-text>
+      </view>
+      <view class="todo-item" v-for="item in records[k]" :key="item.id">
+        <u-row>
+          <u-col span="12">
+            <view class="header">
+              <u-row>
+                <u-col span="12">
+                  <view class="flex_l">
+                    <text class="tit-txt text-ellipsis flex_1">
+                      {{ item.opnPeople }} {{ item.opnType }}
+                    </text>
+                  </view>
+                </u-col>
+              </u-row>
+              <view class="content flex">
+                <text>{{ item.followContent }}</text>
+              </view>
+              <view class="content-footer flex1">
+                <text class="date">{{ item.opnDate }}</text>
+                <!-- <view class="flex flex-middle">
+                  <text class="user-txt">联系人:{{ item.contactsName }}</text>
+                </view> -->
+              </view>
+            </view>
+          </u-col>
+        </u-row>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  import to from 'await-to-js'
+  import api from '@/api/contract'
+  export default {
+    name: 'OmsContractDynamics',
+    props: {
+      contractId: {
+        type: [String, Number],
+        default: '0',
+      },
+    },
+    data() {
+      return {
+        records: {},
+      }
+    },
+
+    mounted() {
+      this.fetchData()
+      console.log('contractId', this.contractId)
+    },
+
+    methods: {
+      async fetchData() {
+        let params = {
+          contractId: this.contractId,
+          DaysBeforeToday: 9999,
+        }
+        const [err, res] = await to(api.getDynamicsList(params))
+        if (err) return
+        if (res.code == 200) this.records = res.data.list || []
+      },
+      formatType(val) {
+        let str = ''
+        if (val == 10) str = '电话'
+        else if (val == 20) str = '邮件'
+        else if (val == 30) str = '拜访'
+        return str
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  .todo-list {
+    height: 100%;
+    overflow: auto;
+    .follow-date {
+      padding: 20rpx 0 20rpx 20rpx;
+    }
+    .todo-item {
+      padding: 12rpx 40rpx 12rpx 46rpx;
+      background: #f2f3f5;
+      border-radius: 15rpx;
+      margin-bottom: 32rpx;
+      .tit-txt {
+        font-size: 28rpx;
+        font-weight: bold;
+        color: #323232;
+      }
+      .content {
+        padding: 10rpx 0;
+        font-size: 28rpx;
+        color: #646464;
+      }
+      .content-footer {
+        padding: 0 0 12rpx 0;
+        font-size: 24rpx;
+        .date {
+          color: #969696;
+        }
+        .user-img {
+          width: 46rpx;
+          height: 46rpx;
+          border-radius: 50%;
+          margin-right: 15rpx;
+        }
+      }
+    }
+  }
+</style>

+ 113 - 0
frontend_mobile/pages/contract/components/contractInvoice.vue

@@ -0,0 +1,113 @@
+<template>
+  <view class="contact-main">
+    <view class="todo-list">
+      <u-empty mode="list" v-if="list.length == 0"></u-empty>
+      <view v-else v-for="(v, i) in list" :key="i">
+        <view class="contact-item">
+          <view class="flex_l">
+            <view class="label">合同编号:</view>
+            <view class="val">{{ v.contractCode }}</view>
+          </view>
+          <view class="flex_l">
+            <view class="label">开票金额:</view>
+            <view class="val">{{ formatPrice(v.invoiceAmount) }}</view>
+          </view>
+          <view class="flex_l">
+            <view class="label">开票日期:</view>
+            <view class="val">{{ v.invoiceDate }}</view>
+          </view>
+          <view class="flex_l">
+            <view class="label">开票类型:</view>
+            <view class="val">{{ invoiceTypeData.filter((item) => item.key == v.invoiceType)[0].value || '-' }}</view>
+          </view>
+          <view class="flex_l">
+            <view class="label">审核状态:</view>
+            <view class="val"> {{
+              v.approStatus == '10'
+                ? '待提交审核'
+                : v.approStatus == '20'
+                ? '待审核'
+                : v.approStatus == '30'
+                ? '审核已同意'
+                : v.approStatus == '40'
+                ? '审核已拒绝'
+                : v.approStatus == '50'
+                ? '审核已撤销'
+                : ''
+            }}</view>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  import to from 'await-to-js'
+  import api from '@/api/contract'
+  export default {
+    name: 'OmsContractInvoice',
+    props: {
+      contractId: {
+        type: [String, Number],
+        default: '0',
+      },
+    },
+    data() {
+      return {
+        invoiceTypeData: [], //发票类型
+        list: []
+      }
+    },
+    mounted() {
+      this.getOptions()
+      this.fetchData()
+    },
+    methods: {
+      getOptions() {
+        Promise.all([this.getDicts('invoice_type')])
+          .then(([invoiceType]) => {
+            this.invoiceTypeData = invoiceType.data.values || []
+          })
+          .catch((err) => console.log(err))
+      },
+      async fetchData() {
+        const [err, res] = await to(api.getInvoice({
+          contractId: this.contractId,
+          pageNum: 0
+        }))
+        if (err) return
+        this.list = res.data.list || []
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .contact-main {
+    height: 100%;
+
+    .todo-list {
+      height: 100%;
+      overflow: auto;
+      padding: 20rpx 20rpx 0;
+
+      .contact-item {
+        padding: 20rpx;
+        margin-bottom: 20rpx;
+        box-shadow: 0 6rpx 19rpx 2rpx rgba(0, 45, 132, 0.15);
+        border-radius: 8px;
+
+        .label,
+        .val {
+          font-size: 28rpx;
+          padding-bottom: 14rpx;
+        }
+
+        .label {
+          color: #646464;
+        }
+      }
+    }
+  }
+</style>

+ 90 - 0
frontend_mobile/pages/contract/components/contractProduct.vue

@@ -0,0 +1,90 @@
+<template>
+  <view class="contact-main">
+    <view class="todo-list">
+      <u-empty mode="list" v-if="list.length == 0"></u-empty>
+      <view v-else v-for="(v, i) in list" :key="i">
+        <view class="contact-item">
+          <view class="flex_l">
+            <view class="label">产品名称:</view>
+            <view class="val">{{ v.prodName }}</view>
+          </view>
+          <view class="flex_l">
+            <view class="label">产品型号:</view>
+            <view class="val">{{ prodCode }}</view>
+          </view>
+          <view class="flex_l">
+            <view class="label">单价:</view>
+            <view class="val">{{ formatPrice(v.tranPrice) }}</view>
+          </view>
+          <view class="flex_l">
+            <view class="label">产品数量:</view>
+            <view class="val">{{ v.prodNum }}</view>
+          </view>
+          <view class="flex_l">
+            <view class="label">合计:</view>
+            <view class="val"> {{calculatedDiscount(v.tranPrice, v.prodNum)}}</view>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  import to from 'await-to-js'
+  import api from '@/api/contract'
+  export default {
+    name: 'OmsContractInvoice',
+    props: {
+      list: {
+        type: Array,
+        default: () => {
+          return []
+        },
+      },
+    },
+    data() {
+      return {
+      }
+    },
+    mounted() {},
+    methods: {
+      // 计算总价
+      calculatedDiscount(price, count) {
+        let intPrice = null
+        if (typeof price === 'string') intPrice = this.delcommafy(price) * 100
+        else intPrice = price * 100
+        return this.formatPrice((intPrice * count) / 100)
+      },
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .contact-main {
+    height: 100%;
+
+    .todo-list {
+      height: 100%;
+      overflow: auto;
+      padding: 20rpx 20rpx 0;
+
+      .contact-item {
+        padding: 20rpx;
+        margin-bottom: 20rpx;
+        box-shadow: 0 6rpx 19rpx 2rpx rgba(0, 45, 132, 0.15);
+        border-radius: 8px;
+
+        .label,
+        .val {
+          font-size: 28rpx;
+          padding-bottom: 14rpx;
+        }
+
+        .label {
+          color: #646464;
+        }
+      }
+    }
+  }
+</style>

+ 366 - 0
frontend_mobile/pages/contract/detail.vue

@@ -0,0 +1,366 @@
+<!--
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-01-12 11:57:48
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-02-15 18:21:43
+ * @Description: file content
+ * @FilePath: \oms\pages\customer\details.vue
+-->
+<template>
+  <view class="home">
+    <view class="nav">
+      <view :style="{ paddingTop }">
+        <view class="title" :style="[{ height }, { lineHeight: height }]">
+          <view class="back" @click="goBack()">
+            <u-icon name="arrow-left" color="#ffffff" size="22"></u-icon>
+          </view>
+          <text>合同详情</text>
+        </view>
+      </view>
+    </view>
+    <view class="main">
+      <view class="main-top">
+        <view class="customer-box">
+          <view class="header flex1">
+            <view class="name flex_l">
+              <image class="img" src="../../static/images/menu1.png" mode="scaleToFill" />
+              <text>{{ detail.contractName }}</text>
+            </view>
+            <view class="date">{{ parseTime(detail.createdTime, '{y}-{m}-{d} {h}:{i}') }}</view>
+          </view>
+          <view class="info">
+            <view class="info-item">
+              <u-row>
+                <u-col span="7">
+                  <view class="flex_l">
+                    <view class="info-label">客户名称:</view>
+                    <text class="info-txt">{{ detail.custName }}</text>
+                  </view>
+                </u-col>
+                <u-col span="5">
+                  <view class="flex_l">
+                    <view class="info-label">负责人:</view>
+                    <text class="info-txt">{{ detail.inchargeName }}</text>
+                  </view>
+                </u-col>
+              </u-row>
+            </view>
+            <view class="info-item">
+              <u-row justify="space-between">
+                <u-col span="7">
+                  <view class="flex_l">
+                    <view class="info-label">合同金额:</view>
+                    <text class="info-txt">{{ formatPrice(detail.contractAmount) }}</text>
+                  </view>
+                </u-col>
+                <u-col span="5">
+                  <view class="flex_l">
+                    <view class="info-label">回款金额:</view>
+                    <text class="info-txt">{{ formatPrice(detail.collectedAmount) }}</text>
+                  </view>
+                </u-col>
+              </u-row>
+            </view>
+          </view>
+        </view>
+        <view class="tabs">
+          <u-tabs @change="changeTabs" lineWidth="8" :current="curTabIndex" lineHeight="8" :scrollable="false"
+            :list="list" :lineColor="`url(${lineBg}) 100% 100%`" :activeStyle="{
+              color: '#3E7EF8',
+              fontWeight: 'bold',
+            }" :inactiveStyle="{
+              color: '#969696',
+            }" itemStyle="height: 90rpx;"></u-tabs>
+        </view>
+      </view>
+      <view class="data-list">
+        <!-- 产品 -->
+        <contract-product v-if="curTabIndex == 0" :list="product"></contract-product>
+        <!-- 回款记录 -->
+        <contract-collection v-else-if="curTabIndex == 1" :contractId="contractId"></contract-collection>
+        <!-- 详情 -->
+        <contract-detail v-else-if="curTabIndex == 2" :detail="detail">
+        </contract-detail>
+        <!-- 发票 -->
+        <contract-invoice v-else-if="curTabIndex == 3" :contractId="contractId"></contract-invoice>
+        <!-- 活动 -->
+        <contract-dynamics v-else-if="curTabIndex == 4" :contractId="contractId"></contract-dynamics>
+      </view>
+    </view>
+    <!-- 新增按钮 -->
+    <view class="fixed-btn-group" :style="{ width: openBtnWidth ? '320rpx' : '90rpx' }">
+      <view class="flex1" v-if="openBtnWidth">
+        <view class="btn center" @click="linkToCollection()">回</view>
+        <view class="btn center" @click="linkToInvoice()">发</view>
+      </view>
+      <view class="btn center" @click="openBtnWidth = !openBtnWidth">
+        <u-icon name="plus" color="#fff" size="20"></u-icon>
+      </view>
+    </view>
+  </view>
+</template>
+<script>
+  import customerApi from '../../api/customer'
+  import contApi from '../../api/contract'
+  import ContractDetail from './components/contractDetail'
+  import ContractCollection from './components/contractCollection'
+  import ContractInvoice from './components/contractInvoice'
+  import ContractProduct from './components/contractProduct'
+  import ContractDynamics from './components/contractDynamics'
+  import to from 'await-to-js'
+  export default {
+    name: 'omsIndex',
+    components: {
+      ContractDetail,
+      ContractCollection,
+      ContractInvoice,
+      ContractProduct,
+      ContractDynamics
+    },
+    data() {
+      return {
+        openBtnWidth: false,
+        lineBg: require('../../static/images/up.png'),
+        curTabIndex: 0,
+        fllowList: [], //跟进数据
+        list: [{
+            name: '产品',
+            index: 0,
+          },
+          {
+            name: '回款',
+            index: 1,
+          },
+          {
+            name: '详情',
+            index: 2,
+          },
+          {
+            name: '发票',
+            index: 3,
+          },
+          {
+            name: '活动',
+            index: 4,
+          },
+        ],
+        height: '',
+        paddingTop: '',
+        detail: {},
+        abstractDetail: null,
+        custStatus: {
+          10: '正常',
+          20: '异常',
+        },
+        contractId: 0, //客户id
+        product:[]
+      }
+    },
+    onLoad(option) {
+      this.contractId = parseInt(option.id)
+    },
+    created() {
+      const navData = uni.getMenuButtonBoundingClientRect()
+      this.height = navData.height + 'px'
+      this.paddingTop = navData.top + 'px'
+    },
+    onShow() {
+      this.getCustomerDetail()
+    },
+    methods: {
+      async getCustomerDetail() {
+        const [err, res] = await to(contApi.getDetails({
+          id: this.contractId
+        }))
+        if (err) return
+        if (res && res.code == 200) {
+          let {
+            product,
+            ...data
+          } = res.data
+          this.product = product
+          this.detail = data
+        }
+      },
+      // 改变tab
+      changeTabs(data) {
+        console.log(data)
+        this.curTabIndex = data.index
+      },
+      // 打开转移
+      openFollow() {
+        this.$store.commit('setDetails', this.detail)
+        uni.navigateTo({
+          //保留当前页面,跳转到应用内的某个页面
+          url: '/pages/publicPages/follow?targetType=10',
+        })
+      },
+      // 跳转到新建回款
+      linkToInvoice() {
+        uni.navigateTo({
+          //保留当前页面,跳转到应用内的某个页面
+          url: '/pages/contract/invoice?id=' + this.contractId + '&code=' + this.detail.contractCode,
+        })
+      },
+      linkToCollection() {
+        uni.navigateTo({
+          //保留当前页面,跳转到应用内的某个页面
+          url: '/pages/contract/collection?id=' + this.contractId + '&code=' + this.detail.contractCode,
+        })
+      },
+      goBack() {
+        uni.navigateBack({
+          //关闭当前页面,返回上一页面或多级页面。
+          delta: 1,
+        })
+      },
+    },
+  }
+</script>
+<style>
+  page {
+    background: #f2f3f5;
+  }
+</style>
+<style lang="scss" scoped>
+  .home {
+    padding-top: 200rpx;
+
+    .nav {
+      position: absolute;
+      left: 0;
+      top: 0;
+      width: 100%;
+      height: 356rpx;
+      background: #3e7ef8;
+      border-radius: 0 0 31rpx 31rpx;
+
+      .title {
+        position: relative;
+        text-align: center;
+        font-size: 32rpx;
+        font-weight: bold;
+        color: #ffffff;
+
+        .back {
+          position: absolute;
+          top: 0;
+          bottom: 0;
+          margin: auto;
+          left: 70rpx;
+          display: flex;
+        }
+      }
+    }
+
+    .main {
+      position: absolute;
+      width: 100%;
+      height: calc(100vh - 200rpx);
+      overflow: hidden;
+      padding-bottom: 64rpx;
+
+      .main-top {
+        padding: 0 32rpx;
+      }
+
+      .customer-box {
+        width: 100%;
+        background: #ffffff;
+        box-shadow: 0 6rpx 19rpx 2rpx rgba(0, 45, 132, 0.15);
+        border-radius: 32rpx;
+        padding: 22rpx 38rpx 68rpx 40rpx;
+
+        .header {
+          .name {
+            .img {
+              width: 46rpx;
+              height: 46rpx;
+              border-radius: 50%;
+              margin-right: 8rpx;
+            }
+
+            text {
+              font-size: 28rpx;
+              font-weight: bold;
+              color: #323232;
+            }
+          }
+
+          .date {
+            font-size: 24rpx;
+            color: #3e7ef8;
+          }
+        }
+
+        .info {
+          .info-item {
+            margin-top: 18rpx;
+
+            .info-label {
+              text-align: left;
+              font-size: 24rpx;
+              color: #646464;
+            }
+
+            .info-txt {
+              flex: 1;
+              font-size: 24rpx;
+              color: #323232;
+            }
+          }
+        }
+      }
+
+      .data-list {
+        margin-top: 16rpx;
+        width: 100%;
+        height: calc(100vh - 532rpx);
+        background: #ffffff;
+        padding: 32rpx;
+        overflow: auto;
+        padding-bottom: 145rpx;
+
+        .status1 {
+          color: #4096fb;
+          background: rgba(64, 150, 251, 0.2);
+        }
+
+        .status2 {
+          background: rgba(255, 184, 60, 0.2);
+          color: #ffb83c;
+        }
+
+        .status3 {
+          color: #fe6936;
+          background: rgba(254, 105, 54, 0.2);
+        }
+      }
+    }
+
+    .fixed-btn-group {
+      position: fixed;
+      display: flex;
+      justify-content: space-around;
+      align-items: center;
+      width: 90rpx;
+      height: 90rpx;
+      bottom: 50rpx;
+      right: 50rpx;
+      box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+      border-radius: 20px;
+      transition: all 0.2s;
+
+      .btn {
+        width: 60rpx;
+        height: 60rpx;
+        background: #3e7ef8;
+        border-radius: 50%;
+        margin: 10rpx;
+        font-size: 26rpx;
+        font-weight: bold;
+        color: #ffffff;
+      }
+    }
+  }
+</style>

+ 455 - 0
frontend_mobile/pages/contract/index.vue

@@ -0,0 +1,455 @@
+<!--
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-01-12 11:57:48
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-02-15 09:43:14
+ * @Description: file content
+ * @FilePath: \oms\pages\customer\index.vue
+-->
+<template>
+  <view class="home">
+    <view class="nav">
+      <view :style="{ paddingTop }">
+        <view class="title" :style="[{ height }, { lineHeight: height }]">
+          <view class="back" @click="goBack()">
+            <u-icon name="arrow-left" color="#ffffff" size="22"></u-icon>
+          </view>
+          <text>合同管理</text>
+        </view>
+      </view>
+    </view>
+    <view class="main">
+      <view class="query-wrap">
+        <view class="search-container">
+          <view class="search-input">
+            <u-input clearable placeholderStyle="font-size:26rpx" :customStyle="{ height: '66rpx' }"
+              v-model="contractName" prefixIcon="search" prefixIconStyle="font-size: 22px;color: #909399"
+              placeholder="请输入合同名称" shape="circle" border="surround"></u-input>
+          </view>
+          <view class="search-btn" @click="openFilter">筛选</view>
+          <view class="search-btn" @click="searchList">搜索</view>
+        </view>
+        <!-- 筛选 -->
+        <view class="filter-popup" v-if="filterVisible">
+          <view class="filter-wrap">
+            <view class="filter-item">
+              <view class="tit">审批状态</view>
+              <view class="menu-list">
+                <view class="menu-item" v-for="(item, index) in approStatusOption" :key="index">
+                  <u-tag shape="circle" :text="item.value" :plain="industryChecked != item.key" :name="item.key"
+                    @click="radioClick(item, 'industryChecked')"></u-tag>
+                </view>
+              </view>
+            </view>
+            <view class="btn-box">
+              <view class="reset" @click="reset()">重置</view>
+              <view class="submit" @click="confirmFilter()">确定</view>
+            </view>
+          </view>
+        </view>
+      </view>
+      <u-empty v-if="list.length == 0" mode="list" text="暂无数据"></u-empty>
+      <scroll-view :scroll-y="true" class="data-list" @scrolltolower="lower" v-else>
+        <view>
+          <view class="data-item" v-for="(v, i) in list" :key="i" @click="toDetails(v)">
+            <view class="customer-name flex">
+              <text class="name">{{ v.contractName }}</text>
+              <view class="user-code">
+                <text>{{ v.contractCode }}</text>
+              </view>
+            </view>
+            <view class="customer-info flex">
+              <view class="info-left flex_1">
+                <view class="info-row flex_l">
+                  <text class="info-label">所在省市:</text>
+                  <u-text color="#323232" size="24rpx" :text="v.custProvince + '' + v.custCity"></u-text>
+                </view>
+                <view class="info-row flex_l">
+                  <text class="info-label">合同类型:</text>
+                  <u-text color="#323232" size="24rpx" :text="contractOptions[v.contractType]"></u-text>
+                </view>
+                <view class="info-row flex_l">
+                  <text class="info-label">客户名称:</text>
+                  <u-text color="#323232" size="24rpx" :text="v.custName"></u-text>
+                </view>
+                <!-- <view class="flex_l">
+                  <view class="transfer-btn mr20" @click.stop="linkToTransfer(v.id)">
+                    <u-button type="primary" size="small" text="转移客户"></u-button>
+                  </view>
+                  <view class="transfer-btn mr20" @click.stop="$refs.moveCust.open(v.id)">
+                    <u-button type="primary" size="small" text="移入公海"></u-button>
+                  </view>
+                </view> -->
+              </view>
+            </view>
+          </view>
+          <u-loadmore :status="loadStatus" />
+        </view>
+      </scroll-view>
+    </view>
+    <!-- 新增按钮 -->
+    <view class="fixed-btn center" @click="openAdd()">
+      <u-icon name="plus" color="#fff" size="20"></u-icon>
+    </view>
+    <!-- 消息提示 -->
+    <u-toast ref="uToast"></u-toast>
+  </view>
+</template>
+<script>
+  import contApi from '../../api/contract'
+  import to from 'await-to-js'
+  export default {
+    name: 'omsIndex',
+    data() {
+      return {
+        custStatus: {
+          10: '正常',
+          20: '异常',
+        },
+        height: '',
+        paddingTop: '',
+        pageNum: 0,
+        pageSize: 10,
+        contractName: '',
+        list: [], //客户列表
+        customerDataTotal: 0, //列表元素数量
+        loadStatus: '', //加载状态
+        filterVisible: false, //筛选组件状态
+        industryChecked: '', //选择的行业id
+        industryOptions: [],
+        contractOptions: {}, //合同类型
+        contractList: [],
+        approStatusOption: [{
+            value: '待提交审核',
+            key: '10'
+          },
+          {
+            value: '待审核',
+            key: '20'
+          },
+          {
+            value: '审核已同意',
+            key: '30'
+          },
+          {
+            value: '审核已拒绝',
+            key: '40'
+          },
+          {
+            value: '审核已撤销',
+            key: '50'
+          },
+        ]
+      }
+    },
+    created() {
+      const navData = uni.getMenuButtonBoundingClientRect()
+      this.height = navData.height + 'px'
+      this.paddingTop = navData.top + 'px'
+    },
+    onShow() {
+      this.getOptions()
+      this.searchList()
+    },
+    methods: {
+      getOptions() {
+        Promise.all([this.getDicts('cust_idy'), this.getDicts('contract_type')])
+          .then(([industry, contract]) => {
+            // this.levelOptions = level.data.values || []
+            this.industryOptions = industry.data.values || []
+            this.contractList = contract.data.values || []
+            this.contractOptions = {}
+            contract.data.values.filter((i) => {
+              this.contractOptions[i.key] = i.value
+            })
+          })
+          .catch((err) => console.log(err))
+      },
+      // 筛选下的单选
+      radioClick(item, target) {
+        console.log(item.key, target)
+        this[target] = item.key
+      },
+      // 选择日期
+      async pickDate(e) {
+        console.log(e)
+        this.followUpDate = e.fulldate
+      },
+      // 重置
+      reset() {
+        this.followUpDate = ''
+        this.industryChecked = ''
+        // this.levelChecked = 0
+      },
+      // 确认
+      confirmFilter() {
+        this.filterVisible = false
+        this.searchList()
+      },
+      // 上拉滚动
+      lower() {
+        console.log(this.list.length)
+        console.log(this.customerDataTotal)
+        console.log(this.loadStatus)
+        if (this.list.length < this.customerDataTotal && this.loadStatus != 'loading') {
+          this.$u.throttle(this.fetchData(), 2000, false)
+        }
+      },
+      // 查询列表
+      searchList() {
+        this.pageNum = 0
+        this.fetchData(true)
+      },
+      async fetchData(reset) {
+        this.loadStatus = 'loading'
+        this.pageNum++
+        let params = {
+          approStatus: this.industryChecked,
+          isPublic: false,
+          pageNum: this.pageNum,
+          pageSize: this.pageSize,
+          contractName: this.contractName
+        }
+        const [err, res] = await to(contApi.getList(params))
+        if (err) {
+          this.loadStatus = 'nomore'
+          return
+        }
+        if (res && res.code == 200) {
+          if (reset) {
+            this.list = res.data.list || []
+          } else {
+            this.list = [...this.list, ...(res.data.list || [])]
+          }
+          this.customerDataTotal = res.data.total
+          this.loadStatus = this.list.length == this.customerDataTotal ? 'nomore' : 'loadmore'
+          console.log(this.loadStatus)
+        } else {
+          this.loadStatus = 'nomore'
+        }
+      },
+      openFilter() {
+        if (this.filterVisible) {
+          this.reset()
+          this.filterVisible = false
+        } else {
+          this.filterVisible = true
+        }
+      },
+      goBack() {
+        uni.navigateBack({
+          //关闭当前页面,返回上一页面或多级页面。
+          delta: 1,
+        })
+      },
+      openAdd() {
+        uni.navigateTo({
+          //保留当前页面,跳转到应用内的某个页面
+          url: '/pages/customer/add',
+        })
+      },
+      toDetails(v) {
+        uni.navigateTo({
+          //保留当前页面,跳转到应用内的某个页面
+          url: '/pages/contract/detail?id=' + v.id + '&type=private',
+        })
+      },
+    },
+  }
+</script>
+<style>
+  page {
+    background: #f2f3f5;
+  }
+</style>
+<style lang="scss" scoped>
+  .home {
+    padding-top: 188rpx;
+
+    .nav {
+      position: absolute;
+      left: 0;
+      top: 0;
+      width: 100%;
+      height: 284rpx;
+      background: #3e7ef8;
+
+      .title {
+        position: relative;
+        text-align: center;
+        font-size: 32rpx;
+        font-weight: bold;
+        color: #ffffff;
+
+        .back {
+          position: absolute;
+          top: 0;
+          bottom: 0;
+          margin: auto;
+          left: 70rpx;
+          display: flex;
+        }
+      }
+    }
+
+    .main {
+      position: absolute;
+      width: 100%;
+      height: calc(100vh - 188rpx);
+      background: #ffffff;
+      box-shadow: 0 6rpx 19rpx 2rpx rgba(0, 45, 132, 0.15);
+      border-radius: 31rpx 31rpx 0 0;
+      padding: 0 32rpx;
+      overflow: hidden;
+      padding-bottom: 64rpx;
+
+      .query-wrap {
+        padding-top: 20rpx;
+
+        .search-container {
+          display: flex;
+          align-items: center;
+
+          .search-input {
+            flex: 1;
+          }
+
+          .search-btn {
+            text-align: center;
+            line-height: 60rpx;
+            border-radius: 12rpx;
+            width: 100rpx;
+            height: 60rpx;
+            font-size: 26rpx;
+            margin: 0 0 0 12rpx;
+            background: $u-primary;
+            color: #ffffff;
+          }
+        }
+      }
+
+      .data-list {
+        width: 100%;
+        height: calc(100vh - 372rpx);
+        overflow: auto;
+
+        .data-item {
+          background: rgba(242, 243, 245, 0.5);
+          border-radius: 15rpx;
+          padding: 28rpx 40rpx 28rpx 38rpx;
+          margin-top: 32rpx;
+
+          .customer-name {
+            .name {
+              flex: 1;
+              color: #323232;
+              font-weight: bold;
+              font-size: 28rpx;
+              margin-right: 12rpx;
+            }
+
+            .user-code {
+              width: 180rpx;
+              height: 32rpx;
+              font-size: 24rpx;
+              color: #323232;
+              line-height: 32rpx;
+            }
+          }
+
+          .customer-info {
+            .info-left {
+              .transfer-btn {
+                margin-top: 20rpx;
+                width: 150rpx;
+              }
+
+              .info-row {
+                margin-top: 12rpx;
+
+                .info-label {
+                  color: #646464;
+                  font-size: 24rpx;
+                }
+              }
+            }
+
+            .info-right {
+              padding-top: 30rpx;
+
+              .user-img {
+                border-radius: 50%;
+                width: 46rpx;
+                height: 46rpx;
+              }
+            }
+          }
+        }
+      }
+    }
+
+    .filter-popup {
+      background: rgba(0, 0, 0, 0.8);
+      position: fixed;
+      width: 100%;
+      height: 100%;
+      left: 0;
+      z-index: 1;
+
+      .filter-wrap {
+        width: 100%;
+        padding: 20rpx;
+        background: #ffffff;
+
+        .filter-item {
+          padding-bottom: 30rpx;
+
+          .tit {
+            font-size: 26rpx;
+            color: #323232;
+            font-weight: bold;
+            padding-bottom: 20rpx;
+          }
+
+          .menu-list {
+            display: flex;
+            flex-wrap: wrap;
+
+            .menu-item {
+              margin-right: 40rpx;
+              margin-bottom: 20rpx;
+            }
+          }
+        }
+      }
+
+      .btn-box {
+        width: 698rpx;
+        height: 75px;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+
+        >view {
+          width: 320rpx;
+          height: 40px;
+          border-radius: 40px;
+          border: solid 1rpx #ec652b;
+          align-items: center;
+          justify-content: center;
+          text-align: center;
+          line-height: 40px;
+        }
+
+        .reset {
+          color: #ec652b;
+        }
+
+        .submit {
+          color: #fff;
+          background-color: #ec652b;
+        }
+      }
+    }
+  }
+</style>

+ 240 - 0
frontend_mobile/pages/contract/invoice.vue

@@ -0,0 +1,240 @@
+<template>
+  <!-- 新建发票 -->
+  <view class="home">
+    <view class="nav">
+      <view :style="{ paddingTop }">
+        <view class="title" :style="[{ height }, { lineHeight: height }]">
+          <view class="back" @click="goBack()">
+            <u-icon name="arrow-left" color="#ffffff" size="22"></u-icon>
+          </view>
+          <text>新建发票</text>
+        </view>
+      </view>
+    </view>
+    <view class="main">
+      <u-form :model="form" :rules="rules" ref="form" label-width="0">
+        <u-form-item prop="contractCode">
+          <view class="form-label flex_l">
+            <view class="label-tag"></view>
+            合同编号
+          </view>
+          <u-input v-model="form.contractCode" disabled />
+        </u-form-item>
+        <u-form-item prop="invoiceAmount">
+          <view class="form-label flex_l">
+            <view class="label-tag"></view>
+            开票金额
+          </view>
+          <u-input v-model.number="form.invoiceAmount" placeholder="请输入开票金额"/>
+        </u-form-item>
+        <u-form-item prop="invoiceDate" @click="show = true">
+          <view class="form-label flex_l">
+            <view class="label-tag"></view>
+            开票日期
+          </view>
+          <u--input v-model="form.invoiceDate" disabled disabledColor="#ffffff" placeholder="请选择开票日期"></u--input>
+        </u-form-item>
+        <u-form-item prop="invoiceType" @click="showPicker = true">
+          <view class="form-label flex_l">
+            <view class="label-tag"></view>
+            开票类型
+          </view>
+          <u-input v-model="form.invoiceName" disabled disabledColor="#ffffff" placeholder="请选择开票类型" />
+        </u-form-item>
+        <u-form-item prop="remark">
+          <view class="form-label flex_l">
+            <view class="label-tag"></view>
+            备注
+          </view>
+          <u-textarea fontSize="26rpx" v-model="form.remark" placeholder="输入备注" height="180" :count="true"
+            maxlength="300"></u-textarea>
+        </u-form-item>
+      </u-form>
+      <view class="save" @click="save">保存</view>
+    </view>
+    <u-calendar :show="show" @confirm="confirmCalendar" @close="show = false" minDate="1990-1-1"></u-calendar>
+    <u-action-sheet :actions="collectionTypeOption" @select="selectClick" :show="showPicker"></u-action-sheet>
+    <u-toast ref="uToast"></u-toast>
+  </view>
+</template>
+
+<script>
+  import api from '@/api/contract'
+  import to from 'await-to-js'
+  export default {
+    data() {
+      return {
+        height: '',
+        paddingTop: '',
+        form: {
+          invoiceDate: '', //开票日期
+          contractCode: '', //合同编号
+          contractId: null, //合同id
+          invoiceAmount: '', //开票金额
+          invoiceType: '', //开票类型
+          invoiceName: '', //开票类型名称
+          remark: '', //备注
+        },
+        rules: {
+          contractCode: [{
+            required: true,
+            trigger: 'blur',
+            message: '请选择合同'
+          }],
+          invoiceAmount: [{
+            type:'number',
+            required: true,
+            trigger: 'blur',
+            message: '请输入开票金额'
+          }],
+          invoiceDate: [{
+            required: true,
+            trigger: 'change',
+            message: '请选择开票日期'
+          }],
+          invoiceType: [{
+            required: true,
+            trigger: 'chgange',
+            message: '请输入开票类型'
+          }],
+        },
+        show: false,
+        showPicker: false,
+        collectionTypeOption: []
+      }
+    },
+    created() {
+      const navData = uni.getMenuButtonBoundingClientRect()
+      this.height = navData.height + 'px'
+      this.paddingTop = navData.top + 'px'
+      this.getOptions()
+    },
+    onLoad(option) {
+      console.log(option.id) //打印出上个页面传递的参数。
+      this.form.contractId = parseInt(option.id)
+      this.form.contractCode = option.code
+    },
+    methods: {
+      getOptions() {
+        Promise.all([this.getDicts('invoice_type')])
+          .then(([collectionType]) => {
+            this.collectionTypeOption = collectionType.data.values || []
+            this.collectionTypeOption.forEach(item => item.name = item.value)
+          })
+          .catch((err) => console.log(err))
+      },
+      closeMoveInModel() {
+
+      },
+      confirmCalendar(val) {
+        this.form.invoiceDate = val[0]
+        this.show = false
+      },
+      selectClick(val) {
+        this.form.invoiceType = val.key
+        this.form.invoiceName = val.value
+        this.showPicker = false
+      },
+      async save() {
+        let params = {
+          ...this.form
+        }
+        delete params.invoiceName
+        params.invoiceAmount = parseInt(params.invoiceAmount)
+        this.$refs.form.validate().then(async valid => {
+          if (valid) {
+            console.log(valid);
+            const [err, res] = await to(api.addInvoice(params))
+            if (err) return
+            this.$refs.uToast.show({
+              type: 'success',
+              message: '创建成功',
+              complete: () => {
+                this.goBack()
+              },
+            })
+          }
+        }).catch(errors => {})
+      },
+      goBack() {
+        uni.navigateBack({
+          //关闭当前页面,返回上一页面或多级页面。
+          delta: 1,
+        })
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  .home {
+    padding-top: 188rpx;
+
+    .nav {
+      position: absolute;
+      left: 0;
+      top: 0;
+      width: 100%;
+      height: 284rpx;
+      background: #3e7ef8;
+
+      .title {
+        position: relative;
+        text-align: center;
+        font-size: 32rpx;
+        font-weight: bold;
+        color: #ffffff;
+
+        .back {
+          position: absolute;
+          top: 0;
+          bottom: 0;
+          margin: auto;
+          left: 70rpx;
+          display: flex;
+        }
+      }
+    }
+
+    .main {
+      position: absolute;
+      width: 100%;
+      height: calc(100vh - 280rpx);
+      background: #ffffff;
+      border-radius: 31rpx 31rpx 0 0;
+      padding: 0 32rpx;
+      overflow: auto;
+      padding-bottom: 64rpx;
+
+      .form-label {
+        font-size: 32rpx;
+        font-weight: bold;
+        color: #323232;
+        padding-bottom: 18rpx;
+
+        .label-tag {
+          width: 15rpx;
+          height: 15rpx;
+          background: rgba(62, 126, 248, 0.6);
+          border-radius: 50%;
+          margin-right: -4rpx;
+        }
+      }
+
+      .save {
+        position: fixed;
+        bottom: 0;
+        left: 0;
+        width: 100%;
+        height: 92rpx;
+        background: #3e7ef8;
+        border-radius: 31rpx 31rpx 0 0;
+        margin: 116rpx auto 0;
+        font-size: 32rpx;
+        color: #ffffff;
+        text-align: center;
+        line-height: 92rpx;
+      }
+    }
+  }
+</style>

+ 4 - 2
frontend_mobile/pages/customer/add.vue

@@ -149,8 +149,10 @@
   </view>
 </template>
 <script>
+  import distrApi from '../../api/base/distr'
   import customerApi from '../../api/customer'
   import to from 'await-to-js'
+
   export default {
     name: 'omsIndex',
     data() {
@@ -224,7 +226,7 @@
       // 获取字典数据
       getOptions() {
         Promise.all([
-          customerApi.getProvinceDetail(), //省市区
+          distrApi.getProvinceInfo(), //省市区
           // this.getDicts('cust_level'), //级别
           this.getDicts('cust_idy'), //行业
           this.getDicts('cust_source'), //来源
@@ -435,7 +437,7 @@
       box-shadow: 0 6rpx 19rpx 2rpx rgba(0, 45, 132, 0.15);
       border-radius: 31rpx 31rpx 0 0;
       padding: 0 32rpx;
-      overflow: hidden;
+      overflow: auto;
       padding-bottom: 64rpx;
       .form-label {
         font-size: 32rpx;

+ 3 - 0
frontend_mobile/pages/customer/components/contacts.vue

@@ -82,6 +82,8 @@
         let params = {
           custId: this.customerId,
           cuctName: this.cuctName,
+          pageNum: 1,
+          pageSize: 9999,
         }
         const [err, res] = await to(userApi.getContact(params))
         if (err) return
@@ -122,6 +124,7 @@
         padding: 20rpx;
         box-shadow: 0 6rpx 19rpx 2rpx rgba(0, 45, 132, 0.15);
         border-radius: 8px;
+        margin: 30rpx 0;
         .label,
         .val {
           font-size: 28rpx;

+ 10 - 1
frontend_mobile/pages/customer/details.vue

@@ -92,8 +92,9 @@
       </view>
     </view>
     <!-- 新增按钮 -->
-    <view class="fixed-btn-group" v-if="!isPublic" :style="{ width: openBtnWidth ? '320rpx' : '90rpx' }">
+    <view class="fixed-btn-group" v-if="!isPublic" :style="{ width: openBtnWidth ? '400rpx' : '90rpx' }">
       <view class="flex1" v-if="openBtnWidth">
+        <view class="btn center" @click="linkToProject()">项</view>
         <view class="btn center" @click="linkToTransfer()">转</view>
         <view class="btn center" @click="$refs.moveCust.open(customerId)">公</view>
         <view class="btn center" @click="openFollow()">跟</view>
@@ -204,6 +205,13 @@
           url: '/pages/customer/transfer?id=' + this.customerId,
         })
       },
+      // 跳转到创建项目
+      linkToProject() {
+        uni.navigateTo({
+          //保留当前页面,跳转到应用内的某个页面
+          url: '/pages/project/create?id=' + this.customerId,
+        })
+      },
       goBack() {
         uni.navigateBack({
           //关闭当前页面,返回上一页面或多级页面。
@@ -332,6 +340,7 @@
       box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
       border-radius: 20px;
       transition: all 0.2s;
+      background: #fff;
       .btn {
         width: 60rpx;
         height: 60rpx;

+ 1 - 1
frontend_mobile/pages/customer/index.vue

@@ -317,7 +317,7 @@
       box-shadow: 0 6rpx 19rpx 2rpx rgba(0, 45, 132, 0.15);
       border-radius: 31rpx 31rpx 0 0;
       padding: 0 32rpx;
-      overflow: hidden;
+      overflow: auto;
       padding-bottom: 64rpx;
       .query-wrap {
         padding-top: 20rpx;

+ 1 - 1
frontend_mobile/pages/customer/transfer.vue

@@ -194,7 +194,7 @@
       box-shadow: 0 6rpx 19rpx 2rpx rgba(0, 45, 132, 0.15);
       border-radius: 31rpx 31rpx 0 0;
       padding: 0 32rpx;
-      overflow: hidden;
+      overflow: auto;
       padding-bottom: 64rpx;
       .form-label {
         font-size: 32rpx;

+ 115 - 11
frontend_mobile/pages/home/index.vue

@@ -1,10 +1,10 @@
 <!--
  * @Author: liuzhenlin 461480418@qq.ocm
  * @Date: 2023-01-12 11:57:48
- * @LastEditors: liuzhenlin
- * @LastEditTime: 2023-01-17 10:14:51
+ * @LastEditors: wanglj
+ * @LastEditTime: 2023-02-20 10:05:52
  * @Description: file content
- * @FilePath: \opms\pages\home\index.vue
+ * @FilePath: \frontend_mobile\pages\home\index.vue
 -->
 <template>
   <view class="home">
@@ -77,7 +77,7 @@
         </u-row>
         <u-row justify="space-between" gutter="14">
           <u-col span="6">
-            <view class="menu-item flex_l flex-colunm">
+            <view class="menu-item flex_l flex-colunm" @click="handleHomeLink('/pages/project/index')">
               <view class="menu-img-box">
                 <image class="menu-img" src="../../static/images/menu2.png" mode="scaleToFill" />
               </view>
@@ -88,7 +88,7 @@
             </view>
           </u-col>
           <u-col span="6">
-            <view class="menu-item flex_l flex-colunm">
+            <view class="menu-item flex_l flex-colunm" @click="handleHomeLink('/pages/contract/index')">
               <view class="menu-img-box">
                 <image class="menu-img" src="../../static/images/menu3.png" mode="scaleToFill" />
               </view>
@@ -124,7 +124,17 @@
           </u-col>
         </u-row>
       </view>
-      <view class="rank-wrap">
+      <view class="echarts-container">
+        <h4>个人报表</h4>
+        <view class="echarts" v-for="item in echarts" :key="item.id">
+          <h4>{{item.report_name}}</h4>
+          <view class="chart">
+            <l-echart :ref="item.id + ''" @finished="init(item.id)"></l-echart>
+          </view>
+        </view>
+      </view>
+
+      <!-- <view class="rank-wrap">
         <view class="rank-tit flex_l">
           <image class="rank-tit-img" src="../../static/images/rank-img.png" mode="scaleToFill" />
           <text>回款排名</text>
@@ -169,12 +179,16 @@
           </view>
           <view class="view-all">查看全部排名 ></view>
         </view>
-      </view>
+      </view> -->
     </view>
   </view>
 </template>
 
 <script>
+  import indexApi from '@/api/system/index.js'
+  import to from 'await-to-js'
+  import LEchart from '@/uni_modules/lime-echart/components/l-echart/l-echart.vue';
+  import * as echarts from '@/uni_modules/lime-echart/static/echarts.min'
   export default {
     name: 'omsIndex',
     data() {
@@ -182,8 +196,7 @@
         height: '',
         paddingTop: '',
         tabDate: 'week',
-        rankList: [
-          {
+        rankList: [{
             name: '邱国辉',
             money: '100000',
           },
@@ -204,6 +217,7 @@
             money: '100000000',
           },
         ],
+        echarts: [],
       }
     },
     created() {
@@ -211,8 +225,12 @@
       this.height = navData.height + 'px'
       this.paddingTop = navData.top + 'px'
     },
-    mounted() {},
-
+    mounted() {
+      this.getEcharts()
+    },
+    components: {
+      LEchart
+    },
     methods: {
       handleHomeLink(url) {
         uni.navigateTo({
@@ -220,6 +238,59 @@
           url,
         })
       },
+      async getEcharts() {
+        const [err, res] = await to(indexApi.getHomeReport({
+          module_code: 'HomePage'
+        }))
+        if (err) return
+        const obj = JSON.parse(res.data.configInfo)
+        this.echarts = obj.data_report_config || []
+      },
+      async init(id) {
+        console.log(id, 'id')
+        const [err, res] = await to(indexApi.getHomeDataReportData({
+          id
+        }))
+        if (err) return
+        const option = {
+          grid: {
+            bottom: 10,
+            top: 20,
+            right: 10,
+            containLabel: true
+          },
+          tooltip: {
+            trigger: 'axis',
+          },
+          xAxis: [{
+            type: 'category',
+            data: res.data.data.xData,
+            axisTick: {
+              alignWithLabel: true,
+            },
+          }, ],
+          yAxis: [{
+            type: 'value',
+            name: '(元)',
+            nameLocation: 'start'
+          }, ],
+          series: [{
+              name: '销售指标',
+              type: 'bar',
+              data: res.data.data.yDataTarget,
+            },
+            {
+              name: '销售额度',
+              type: 'bar',
+              data: res.data.data.yDataReal,
+            },
+          ],
+        }
+        console.log(this.$refs[id][0], 'id')
+        this.$refs[id][0].init(echarts, chart => {
+          chart.setOption(option);
+        });
+      },
     },
   }
 </script>
@@ -300,6 +371,8 @@
         border-radius: 31rpx 31rpx 31rpx 31rpx;
         margin: 0 auto;
 
+
+
         .tab-date-wrap {
           padding: 8rpx 0 24rpx 38rpx;
 
@@ -433,6 +506,37 @@
           height: 60rpx;
         }
       }
+
+      .echarts-container {
+        background-color: #fff;
+        box-shadow: 0 6rpx 19rpx 2rpx rgba(0, 45, 132, 0.15);
+        border-radius: 31rpx 31rpx 31rpx 31rpx;
+        margin: 0 auto;
+        margin-top: 32rpx;
+        padding: 0 20rpx 20rpx;
+
+        h4 {
+          font-size: 28rpx;
+          line-height: 80rpx;
+          font-weight: bold;
+          color: #323232;
+          padding-left: 40rpx;
+        }
+
+        .echarts {
+          height: 480rpx;
+          box-shadow: 0 6rpx 19rpx 2rpx rgba(0, 45, 132, 0.15);
+          border-radius: 31rpx 31rpx 31rpx 31rpx;
+          + .echarts {
+            margin-top: 32rpx;
+          }
+          .chart {
+            height: 400rpx;
+          }
+
+
+        }
+      }
     }
   }
 </style>

+ 1 - 1
frontend_mobile/pages/openSeaCustomer/index.vue

@@ -320,7 +320,7 @@
       box-shadow: 0 6rpx 19rpx 2rpx rgba(0, 45, 132, 0.15);
       border-radius: 31rpx 31rpx 0 0;
       padding: 0 32rpx;
-      overflow: hidden;
+      overflow: auto;
       padding-bottom: 64rpx;
       .query-wrap {
         padding-top: 20rpx;

+ 12 - 10
frontend_mobile/pages/project/components/contacts.vue

@@ -2,9 +2,9 @@
  * @Author: liuzhenlin 461480418@qq.ocm
  * @Date: 2023-02-16 11:31:40
  * @LastEditors: liuzhenlin
- * @LastEditTime: 2023-02-16 11:47:42
+ * @LastEditTime: 2023-02-21 10:00:48
  * @Description: file content
- * @FilePath: \oms\pages\customer\components\contacts.vue
+ * @FilePath: \frontend_mobile\pages\project\components\contacts.vue
 -->
 <template>
   <view class="contact-main">
@@ -14,7 +14,7 @@
           clearable
           placeholderStyle="font-size:26rpx"
           :customStyle="{ height: '66rpx' }"
-          v-model="cuctName"
+          v-model="queryForm.cuctName"
           prefixIcon="search"
           prefixIconStyle="font-size: 22px;color: #909399"
           placeholder="请输入姓名"
@@ -60,14 +60,19 @@
   export default {
     name: 'OmsCustomerDetail',
     props: {
-      customerId: {
+      projectId: {
         type: [String, Number],
         default: '0',
       },
     },
     data() {
       return {
-        cuctName: '',
+        queryForm: {
+          busId: this.projectId,
+          cuctName: '',
+          pageNum: 1,
+          pageSize: 9999,
+        },
         contactList: [],
       }
     },
@@ -79,11 +84,7 @@
 
     methods: {
       async getContacts() {
-        let params = {
-          custId: this.customerId,
-          cuctName: this.cuctName,
-        }
-        const [err, res] = await to(userApi.getContact(params))
+        const [err, res] = await to(userApi.getProjectContact(this.queryForm))
         if (err) return
         if (res.code == 200) this.contactList = res.data.list || []
       },
@@ -122,6 +123,7 @@
         padding: 20rpx;
         box-shadow: 0 6rpx 19rpx 2rpx rgba(0, 45, 132, 0.15);
         border-radius: 8px;
+        margin: 30rpx 0;
         .label,
         .val {
           font-size: 28rpx;

+ 41 - 10
frontend_mobile/pages/project/details.vue

@@ -2,9 +2,9 @@
  * @Author: liuzhenlin 461480418@qq.ocm
  * @Date: 2023-01-12 11:57:48
  * @LastEditors: liuzhenlin
- * @LastEditTime: 2023-02-17 17:53:19
+ * @LastEditTime: 2023-02-21 12:00:38
  * @Description: file content
- * @FilePath: \oms\pages\project\details.vue
+ * @FilePath: \frontend_mobile\pages\project\details.vue
 -->
 <template>
   <view class="home">
@@ -88,20 +88,25 @@
         <!-- 详情 -->
         <project-detail v-if="curTabIndex == 1" :detail="projectData"></project-detail>
         <!-- 联系人 -->
-        <!-- <contacts v-if="curTabIndex == 2" :projectId="projectId"></contacts> -->
+        <contacts v-if="curTabIndex == 2" :projectId="projectId"></contacts>
       </view>
     </view>
     <!-- 新增按钮 -->
-    <view class="fixed-btn-group" :style="{ width: openBtnWidth ? '320rpx' : '90rpx' }">
+    <view class="fixed-btn-group" :style="{ width: openBtnWidth ? '560rpx' : '90rpx' }">
       <view class="flex1" v-if="openBtnWidth">
-        <view class="btn center">转</view>
-        <view class="btn center">公</view>
+        <view class="btn center" @click="linkToLevel(1)">升</view>
+        <view class="red btn center" @click="linkToLevel(0)">降</view>
+        <view class="btn center" @click="$refs.reserve.open(projectId)">储</view>
+        <view class="btn center" @click="linkToTransfer()">转</view>
+        <view class="btn center" @click="createProject()">增</view>
         <view class="btn center" @click="openFollow()">跟</view>
       </view>
       <view class="btn center" @click="openBtnWidth = !openBtnWidth">
         <u-icon name="plus" color="#fff" size="20"></u-icon>
       </view>
     </view>
+    <!-- 转储备 -->
+    <transfer-reserve ref="reserve"></transfer-reserve>
   </view>
 </template>
 <script>
@@ -109,10 +114,11 @@
   import to from 'await-to-js'
   import projectDetail from './components/projectDetail'
   import followRecords from './components/followRecords'
-  // import Contacts from 'pages/customer/components/contacts'
+  import Contacts from './components/contacts'
+  import TransferReserve from './components/transferReserve'
   export default {
     name: 'omsIndex',
-    components: { projectDetail, followRecords },
+    components: { projectDetail, followRecords, Contacts, TransferReserve },
     data() {
       return {
         openBtnWidth: false,
@@ -136,7 +142,7 @@
         height: '',
         paddingTop: '',
         projectData: {}, //项目详情
-        projectId: 0, //客户id
+        projectId: 0, //项目id
         // salesModelOptions: [],
         productLineOptions: [], //产品线
         // sourceOptions: [],
@@ -192,7 +198,28 @@
       linkToTransfer() {
         uni.navigateTo({
           //保留当前页面,跳转到应用内的某个页面
-          url: '/pages/customer/transfer?id=' + this.projectId,
+          url: '/pages/project/transfer?id=' + this.projectId,
+        })
+      },
+      // 升降级
+      linkToLevel(type) {
+        if (type === 1) {
+          uni.navigateTo({
+            //保留当前页面,跳转到应用内的某个页面
+            url: '/pages/project/upgrade?id=' + this.projectId,
+          })
+        } else {
+          uni.navigateTo({
+            //保留当前页面,跳转到应用内的某个页面
+            url: '/pages/project/downgrade?id=' + this.projectId,
+          })
+        }
+      },
+      // 跳转到新建项目
+      createProject() {
+        uni.navigateTo({
+          //保留当前页面,跳转到应用内的某个页面
+          url: '/pages/project/create',
         })
       },
       goBack() {
@@ -323,6 +350,10 @@
       box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
       border-radius: 20px;
       transition: all 0.2s;
+      background: #fff;
+      .red {
+        background: #ff4d4f !important;
+      }
       .btn {
         width: 60rpx;
         height: 60rpx;

+ 1 - 1
frontend_mobile/pages/project/index.vue

@@ -303,7 +303,7 @@
       box-shadow: 0 6rpx 19rpx 2rpx rgba(0, 45, 132, 0.15);
       border-radius: 31rpx 31rpx 0 0;
       padding: 0 32rpx;
-      overflow: hidden;
+      overflow: auto;
       padding-bottom: 64rpx;
       .query-wrap {
         padding-top: 20rpx;

+ 1 - 1
frontend_mobile/pages/project/transfer.vue

@@ -184,7 +184,7 @@
       box-shadow: 0 6rpx 19rpx 2rpx rgba(0, 45, 132, 0.15);
       border-radius: 31rpx 31rpx 0 0;
       padding: 0 32rpx;
-      overflow: hidden;
+      overflow: auto;
       padding-bottom: 64rpx;
       .form-label {
         font-size: 32rpx;

+ 1 - 1
frontend_mobile/pages/publicPages/follow.vue

@@ -392,7 +392,7 @@
       box-shadow: 0 6rpx 19rpx 2rpx rgba(0, 45, 132, 0.15);
       border-radius: 31rpx 31rpx 0 0;
       padding: 0 32rpx;
-      overflow: hidden;
+      overflow: auto;
       padding-bottom: 64rpx;
       .form-label {
         font-size: 32rpx;

+ 1 - 1
frontend_mobile/pages/schedule/index.vue

@@ -135,7 +135,7 @@
       box-shadow: 0 6rpx 19rpx 2rpx rgba(0, 45, 132, 0.15);
       border-radius: 31rpx 31rpx 0 0;
       padding: 0 32rpx;
-      overflow: hidden;
+      overflow: auto;
       padding-bottom: 64rpx;
 
       .tabs {

+ 132 - 0
frontend_mobile/uni_modules/lime-echart/changelog.md

@@ -0,0 +1,132 @@
+## 0.6.5(2022-11-03)
+- fix: 某些手机touches为对象,导致无法交互。
+## 0.6.4(2022-10-28)
+- fix: 优化点击事件的触发条件
+## 0.6.3(2022-10-26)
+- fix: 修复 dataZoom 拖动问题
+## 0.6.2(2022-10-23)
+- fix: 修复 飞书小程序 尺寸问题
+## 0.6.1(2022-10-19)
+- fix: 修复 PC mousewheel 事件 鼠标位置不准确的BUG,不兼容火狐!
+- feat: showLoading 增加传参
+## 0.6.0(2022-09-16)
+- feat: 增加PC的mousewheel事件
+## 0.5.4(2022-09-16)
+- fix: 修复 nvue 动态数据不显示问题
+## 0.5.3(2022-09-16)
+- feat: 增加enableHover属性, 在PC端时当鼠标进入显示tooltip,不必按下。
+- chore: 更新文档
+## 0.5.2(2022-09-16)
+- feat: 增加enableHover属性, 在PC端时当鼠标进入显示tooltip,不必按下。
+## 0.5.1(2022-09-16)
+- fix: 修复nvue报错
+## 0.5.0(2022-09-15)
+- feat: init(echarts, theme?:string, opts?:{}, callback: function(chart))
+## 0.4.8(2022-09-11)
+- feat: 增加 @finished
+## 0.4.7(2022-08-24)
+- chore: 去掉 stylus
+## 0.4.6(2022-08-24)
+- feat: 增加 beforeDelay
+## 0.4.5(2022-08-12)
+- chore: 更新文档
+## 0.4.4(2022-08-12)
+- fix: 修复 resize 无参数时报错
+## 0.4.3(2022-08-07)
+# 评论有说本插件对新手不友好,让我做不好就不要发出来。 还有的说跟官网一样,发出来做什么,给我整无语了。
+# 所以在此提醒一下准备要下载的你,如果你从未使用过 echarts 请不要下载 或 谨慎下载。
+# 如果你确认要下载,麻烦看完文档。还有请注意插件是让echarts在uniapp能运行,API 配置请自行去官网查阅!
+# 如果你不会echarts 但又需要图表,市场上有个很优秀的图表插件 uchart 你可以去使用这款插件,uchart的作者人很好,也热情。
+# 每个人都有自己的本职工作,如果你能力强可以自行兼容,如果使用了他人的插件也麻烦尊重他人的成果和劳动时间。谢谢。
+# 为了心情愉悦,本人已经使用插件屏蔽差评。
+- chore: 更新文档
+## 0.4.2(2022-07-20)
+- feat: 增加 resize
+## 0.4.1(2022-06-07)
+- fix: 修复 canvasToTempFilePath 不生效问题
+## 0.4.0(2022-06-04)
+- chore 为了词云 增加一个canvas 标签
+- 词云下载地址[echart-wordcloud](https://ext.dcloud.net.cn/plugin?id=8430)
+## 0.3.9(2022-06-02)
+- chore: 更新文档
+- tips: lines 不支持 `trailLength`
+## 0.3.8(2022-05-31)
+- fix: 修复 因mouse事件冲突tooltip跳动问题
+## 0.3.7(2022-05-26)
+- chore: 更新文档
+- chore: 设置默认宽高300px
+- fix: 修复 vue3 微信小程序 拖影BUG
+- chore: 支持PC
+## 0.3.5(2022-04-28)
+- chore: 更新使用方式
+- 🔔 必须使用hbuilderx 3.4.8-alpha以上
+## 0.3.4(2021-08-03)
+- chore: 增加 setOption的参数值
+## 0.3.3(2021-07-22)
+- fix: 修复 径向渐变报错的问题
+## 0.3.2(2021-07-09)
+- chore: 统一命名规范,无须主动引入组件
+## [代码示例站点1](https://limeui.qcoon.cn/#/echart-example)
+## [代码示例站点2](http://liangei.gitee.io/limeui/#/echart-example)
+## 0.3.1(2021-06-21)
+- fix: 修复 app-nvue ios is-enable 无效的问题
+## [代码示例站点1](https://limeui.qcoon.cn/#/echart-example)
+## [代码示例站点2](http://liangei.gitee.io/limeui/#/echart-example)
+## 0.3.0(2021-06-14)
+- fix: 修复 头条系小程序 2d 报 JSON.stringify 的问题
+- 目前 头条系小程序 2d 无法在开发工具上预览,划动图表页面无法滚动,axisLabel 字体颜色无法更改,建议使用非2d。
+## 0.2.9(2021-06-06)
+- fix: 修复 头条系小程序 2d 放大的BUG 
+- 头条系小程序 2d 无法在开发工具上预览,也存在划动图表页面无法滚动的问题。
+## [代码示例:http://liangei.gitee.io/limeui/#/echart-example](http://liangei.gitee.io/limeui/#/echart-example)
+## 0.2.8(2021-05-19)
+- fix: 修复 微信小程序 PC 显示过大的问题
+## 0.2.7(2021-05-19)
+- fix: 修复 微信小程序 PC 不显示问题
+## [代码示例:http://liangei.gitee.io/limeui/#/echart-example](http://liangei.gitee.io/limeui/#/echart-example)
+## 0.2.6(2021-05-14)
+- feat: 支持 `image`
+- feat: props 增加 `ec.clear`,更新时是否先删除图表样式 
+- feat: props 增加 `isDisableScroll` ,触摸图表时是否禁止页面滚动
+- feat: props 增加 `webviewStyles` ,webview 的样式, 仅nvue有效
+## 0.2.5(2021-05-13)
+- docs: 插件用到了css 预编译器 [stylus](https://ext.dcloud.net.cn/plugin?name=compile-stylus) 请安装它
+## 0.2.4(2021-05-12)
+- fix: 修复 百度平台 多个图表ctx 和 渐变色 bug
+- ## [代码示例:http://liangei.gitee.io/limeui/#/echart-example](http://liangei.gitee.io/limeui/#/echart-example)
+## 0.2.3(2021-05-10)
+- feat: 增加 `canvasToTempFilePath` 方法,用于生成图片
+```js
+this.$refs.chart.canvasToTempFilePath({success: (res) => {
+	console.log('tempFilePath:', res.tempFilePath)
+}})
+```
+## 0.2.2(2021-05-10)
+- feat: 增加 `dispose` 方法,用于销毁实例
+- feat: 增加 `isClickable` 是否派发点击
+- feat: 实验性的支持 `nvue` 使用要慎重考虑
+- ## [代码示例:http://liangei.gitee.io/limeui/#/echart-example](http://liangei.gitee.io/limeui/#/echart-example)
+## 0.2.1(2021-05-06)
+- fix:修复 微信小程序 json 报错
+- chore: `reset` 更改为 `setChart`
+- feat: 增加 `isEnable` 开启初始化 启用这个后 无须再使用`init`方法
+```html
+<l-echart ref="chart" is-enable />
+```
+```js
+// 显示加载
+this.$refs.chart.showLoading()
+// 使用实例回调
+this.$refs.chart.setChart(chart => ...code)
+// 直接设置图表配置
+this.$refs.chart.setOption(data)
+```
+## 0.2.0(2021-05-05)
+- fix:修复 头条 百度 偏移的问题
+- docs: 更新文档
+## [代码示例:http://liangei.gitee.io/limeui/#/echart-example](http://liangei.gitee.io/limeui/#/echart-example)
+## 0.1.0(2021-05-02)
+- chore:  第一次上传,基本全端兼容,使用方法与官网一致。
+- 已知BUG:非2d 无法使用背景色,已反馈官方
+- 已知BUG:头条 百度 有许些偏移
+- 后期计划:兼容nvue

+ 372 - 0
frontend_mobile/uni_modules/lime-echart/components/l-echart/canvas.js

@@ -0,0 +1,372 @@
+const cacheChart = {}
+const fontSizeReg = /([\d\.]+)px/;
+class EventEmit {
+	constructor() {
+		this.__events = {};
+	}
+	on(type, listener) {
+		if (!type || !listener) {
+			return;
+		}
+		const events = this.__events[type] || [];
+		events.push(listener);
+		this.__events[type] = events;
+	}
+	emit(type, e) {
+		if (type.constructor === Object) {
+			e = type;
+			type = e && e.type;
+		}
+		if (!type) {
+			return;
+		}
+		const events = this.__events[type];
+		if (!events || !events.length) {
+			return;
+		}
+		events.forEach((listener) => {
+			listener.call(this, e);
+		});
+	}
+	off(type, listener) {
+		const __events = this.__events;
+		const events = __events[type];
+		if (!events || !events.length) {
+			return;
+		}
+		if (!listener) {
+			delete __events[type];
+			return;
+		}
+		for (let i = 0, len = events.length; i < len; i++) {
+			if (events[i] === listener) {
+				events.splice(i, 1);
+				i--;
+			}
+		}
+	}
+}
+class Image {
+	constructor() {
+		this.currentSrc = null
+		this.naturalHeight = 0
+		this.naturalWidth = 0
+		this.width = 0
+		this.height = 0
+		this.tagName = 'IMG'
+	}
+	set src(src) {
+		this.currentSrc = src
+		uni.getImageInfo({
+			src,
+			success: (res) => {
+				this.naturalWidth = this.width = res.width
+				this.naturalHeight = this.height = res.height
+				this.onload()
+			},
+			fail: () => {
+				this.onerror()
+			}
+		})
+	}
+	get src() {
+		return this.currentSrc
+	}
+}
+class OffscreenCanvas {
+	constructor(ctx, com, canvasId) {
+		this.tagName = 'canvas'
+		this.com = com
+		this.canvasId = canvasId
+		this.ctx = ctx
+	}
+	set width(w) {
+		this.com.offscreenWidth = w
+	}
+	set height(h) {
+		this.com.offscreenHeight = h
+	}
+	get width() {
+		return this.com.offscreenWidth || 0
+	}
+	get height() {
+		return this.com.offscreenHeight || 0
+	}
+	getContext(type) {
+		return this.ctx
+	}
+	getImageData() {
+		return new Promise((resolve, reject) => {
+			this.com.$nextTick(() => {
+				uni.canvasGetImageData({
+					x:0,
+					y:0,
+					width: this.com.offscreenWidth,
+					height: this.com.offscreenHeight,
+					canvasId: this.canvasId,
+					success: (res) => {
+						resolve(res)
+					},
+					fail: (err) => {
+						reject(err)
+					},
+				}, this.com)
+			})
+		})
+	}
+}
+export class Canvas {
+	constructor(ctx, com, isNew, canvasNode={}) {
+		cacheChart[com.canvasId] = {ctx}
+		this.canvasId = com.canvasId;
+		this.chart = null;
+		this.isNew = isNew
+		this.tagName = 'canvas'
+		this.canvasNode = canvasNode;
+		this.com = com;
+		if (!isNew) {this._initStyle(ctx)}
+		this._initEvent();
+		this._ee = new EventEmit()
+	}
+	getContext(type) {
+		if (type === '2d') {
+			return this.ctx;
+		}
+	}
+	setChart(chart) {
+		this.chart = chart;
+	}
+	createOffscreenCanvas(param){
+		if(!this.children) {
+			this.com.isOffscreenCanvas = true
+			this.com.offscreenWidth = param.width||300
+			this.com.offscreenHeight = param.height||300
+			const com = this.com
+			const canvasId = this.com.offscreenCanvasId
+			const context = uni.createCanvasContext(canvasId, this.com)
+			this._initStyle(context)
+			this.children = new OffscreenCanvas(context, com, canvasId)
+		} 
+		return this.children
+	}
+	appendChild(child) {
+		console.log('child', child)
+	}
+	dispatchEvent(type, e) {
+		if(typeof type == 'object') {
+			this._ee.emit(type.type, type);
+		} else {
+			this._ee.emit(type, e);
+		}
+		return true
+	}
+	attachEvent() {
+	}
+	detachEvent() {
+	}
+	addEventListener(type, listener) {
+		this._ee.on(type, listener)
+	}
+	removeEventListener(type, listener) {
+		this._ee.off(type, listener)
+	}
+	_initCanvas(zrender, ctx) {
+		zrender.util.getContext = function() {
+			return ctx;
+		};
+		zrender.util.$override('measureText', function(text, font) {
+			ctx.font = font || '12px sans-serif';
+			return ctx.measureText(text, font);
+		});
+	}
+	_initStyle(ctx, child) {
+		const styles = [
+			'fillStyle',
+			'strokeStyle',
+			'fontSize',
+			'globalAlpha',
+			'opacity',
+			'textAlign',
+			'textBaseline',
+			'shadow',
+			'lineWidth',
+			'lineCap',
+			'lineJoin',
+			'lineDash',
+			'miterLimit',
+			'font'
+		];
+		const colorReg = /#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])\b/g;
+		styles.forEach(style => {
+			Object.defineProperty(ctx, style, {
+				set: value => {
+					if (style === 'font' && fontSizeReg.test(value)) {
+						const match = fontSizeReg.exec(value);
+						ctx.setFontSize(match[1]);
+						return;
+					}
+					if (style === 'opacity') {
+						ctx.setGlobalAlpha(value)
+						return;
+					}
+					if (style !== 'fillStyle' && style !== 'strokeStyle' || value !== 'none' && value !== null) {
+						// #ifdef H5 || APP-PLUS || MP-BAIDU
+						if(typeof value == 'object') {
+							if (value.hasOwnProperty('colorStop') || value.hasOwnProperty('colors')) {
+								ctx['set' + style.charAt(0).toUpperCase() + style.slice(1)](value);
+							}
+							return
+						} 
+						// #endif
+						// #ifdef MP-TOUTIAO
+						if(colorReg.test(value)) {
+							value = value.replace(colorReg, '#$1$1$2$2$3$3')
+						}
+						// #endif
+						ctx['set' + style.charAt(0).toUpperCase() + style.slice(1)](value);
+					}
+				}
+			});
+		});
+		if(!this.isNew && !child) {
+			ctx.uniDrawImage = ctx.drawImage
+			ctx.drawImage = (...a) => {
+				a[0] = a[0].src
+				ctx.uniDrawImage(...a)
+			}
+		}
+		if(!ctx.createRadialGradient) {
+			ctx.createRadialGradient = function() {
+				return ctx.createCircularGradient(...[...arguments].slice(-3))
+			};
+		}
+		// 字节不支持
+		if (!ctx.strokeText) {
+			ctx.strokeText = (...a) => {
+				ctx.fillText(...a)
+			}
+		}
+		// 钉钉不支持 
+		if (!ctx.measureText) {
+			const strLen = (str) => {
+				let len = 0;
+				for (let i = 0; i < str.length; i++) {
+					if (str.charCodeAt(i) > 0 && str.charCodeAt(i) < 128) {
+						len++;
+					} else {
+						len += 2;
+					}
+				}
+				return len;
+			}
+			ctx.measureText = (text, font) => {
+				let fontSize = 12;
+				if (font) {
+					fontSize = parseInt(font.match(/([\d\.]+)px/)[1])
+				}
+				fontSize /= 2;
+				return {
+					width: strLen(text) * fontSize
+				};
+			}
+		}
+	}
+
+	_initEvent(e) {
+		this.event = {};
+		const eventNames = [{
+			wxName: 'touchStart',
+			ecName: 'mousedown'
+		}, {
+			wxName: 'touchMove',
+			ecName: 'mousemove'
+		}, {
+			wxName: 'touchEnd',
+			ecName: 'mouseup'
+		}, {
+			wxName: 'touchEnd',
+			ecName: 'click'
+		}];
+
+		eventNames.forEach(name => {
+			this.event[name.wxName] = e => {
+				const touch = e.touches[0];
+				this.chart.getZr().handler.dispatch(name.ecName, {
+					zrX: name.wxName === 'tap' ? touch.clientX : touch.x,
+					zrY: name.wxName === 'tap' ? touch.clientY : touch.y
+				});
+			};
+		});
+	}
+
+	set width(w) {
+		this.canvasNode.width = w
+	}
+	set height(h) {
+		this.canvasNode.height = h
+	}
+
+	get width() {
+		return this.canvasNode.width || 0
+	}
+	get height() {
+		return this.canvasNode.height || 0
+	}
+	get ctx() {
+		return cacheChart[this.canvasId]['ctx'] || null
+	}
+	set chart(chart) {
+		cacheChart[this.canvasId]['chart'] = chart
+	}
+	get chart() {
+		return cacheChart[this.canvasId]['chart'] || null
+	}
+}
+
+export function dispatch(name, {x,y, wheelDelta}) {
+	this.dispatch(name, {
+		zrX: x,
+		zrY: y,
+		zrDelta: wheelDelta,
+		preventDefault: () => {},
+		stopPropagation: () =>{}
+	});
+}
+export function setCanvasCreator(echarts, {canvas, node}) {
+	// echarts.setCanvasCreator(() => canvas);
+	echarts.registerPreprocessor(option => {
+		if (option && option.series) {
+			if (option.series.length > 0) {
+				option.series.forEach(series => {
+					series.progressive = 0;
+				});
+			} else if (typeof option.series === 'object') {
+				option.series.progressive = 0;
+			}
+		}
+	});
+	function loadImage(src, onload, onerror) {
+		let img = null
+		if(node && node.createImage) {
+			img = node.createImage()
+			img.onload = onload.bind(img);
+			img.onerror = onerror.bind(img);
+			img.src = src;
+			return img
+		} else {
+			img = new Image()
+			img.onload = onload.bind(img)
+			img.onerror = onerror.bind(img);
+			img.src = src
+			return img
+		}
+	}
+	if(echarts.setPlatformAPI) {
+		echarts.setPlatformAPI({
+			loadImage: canvas.setChart ? loadImage : null,
+			createCanvas(){
+				return canvas
+			}
+		})
+	}
+}

+ 516 - 0
frontend_mobile/uni_modules/lime-echart/components/l-echart/l-echart.vue

@@ -0,0 +1,516 @@
+<template>
+	<view class="lime-echart" :style="customStyle" v-if="canvasId" ref="limeEchart">
+		<!-- #ifndef APP-NVUE -->
+		<canvas
+			class="lime-echart__canvas"
+			v-if="use2dCanvas"
+			type="2d"
+			:id="canvasId"
+			:style="canvasStyle"
+			:disable-scroll="isDisableScroll"
+			@touchstart="touchStart"
+			@touchmove="touchMove"
+			@touchend="touchEnd"
+		/>
+		<canvas
+			class="lime-echart__canvas"
+			v-else-if="isPc"
+			:style="canvasStyle"
+			:id="canvasId"
+			:canvas-id="canvasId"
+			:disable-scroll="isDisableScroll"
+			@mousedown="touchStart"
+			@mousemove="touchMove"
+			@mouseup="touchEnd"
+		/>
+		<canvas
+			class="lime-echart__canvas"
+			v-else
+			:width="nodeWidth"
+			:height="nodeHeight"
+			:style="canvasStyle"
+			:canvas-id="canvasId"
+			:id="canvasId"
+			:disable-scroll="isDisableScroll"
+			@touchstart="touchStart"
+			@touchmove="touchMove"
+			@touchend="touchEnd"
+		/>
+		<canvas v-if="isOffscreenCanvas" :style="offscreenStyle" :canvas-id="offscreenCanvasId"></canvas>
+		<!-- #endif -->
+		<!-- #ifdef APP-NVUE -->
+		<web-view
+			class="lime-echart__canvas"
+			:id="canvasId"
+			:style="canvasStyle"
+			:webview-styles="webviewStyles"
+			ref="webview"
+			src="/uni_modules/lime-echart/static/index.html"
+			@pagefinish="finished = true"
+			@onPostMessage="onMessage"
+		></web-view>
+		<!-- #endif -->
+	</view>
+</template>
+
+<script>
+// #ifdef VUE3
+// #ifdef APP-PLUS
+global = {}
+// #endif
+// #endif
+// #ifndef APP-NVUE
+import {Canvas, setCanvasCreator, dispatch} from './canvas';
+import { compareVersion, wrapTouch, devicePixelRatio ,sleep} from './utils';
+// #endif
+// #ifdef APP-NVUE
+import { base64ToPath, sleep } from './utils';
+// #endif
+const charts = {}
+const echartsObj = {}
+export default {
+	name: 'lime-echart',
+	props: {
+		// #ifdef MP-WEIXIN || MP-TOUTIAO
+		type: {
+			type: String,
+			default: '2d'
+		},
+		// #endif
+		// #ifdef APP-NVUE
+		webviewStyles: Object,
+		// hybrid: Boolean,
+		// #endif
+		customStyle: String,
+		isDisableScroll: Boolean,
+		isClickable: {
+			type: Boolean,
+			default: true
+		},
+		enableHover: Boolean,
+		beforeDelay: {
+			type: Number,
+			default: 30
+		}
+	},
+	data() {
+		return {
+			// #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
+			use2dCanvas: true,
+			// #endif
+			// #ifndef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
+			use2dCanvas: false,
+			// #endif
+			width: null,
+			height: null,
+			nodeWidth: null,
+			nodeHeight: null,
+			canvasNode: null,
+			config: {},
+			inited: false,
+			finished: false,
+			file: '',
+			platform: '',
+			isPc: false,
+			isDown: false,
+			isOffscreenCanvas: false,
+			offscreenWidth: 0,
+			offscreenHeight: 0
+		};
+	},
+	computed: {
+		canvasId() {
+			return `lime-echart${this._ && this._.uid || this._uid}`
+		},
+		offscreenCanvasId() {
+			return `${this.canvasId}_offscreen`
+		},
+		offscreenStyle() {
+			return `width:${this.offscreenWidth}px;height: ${this.offscreenHeight}px; position: fixed; left: 99999px; background: red`
+		},
+		canvasStyle() {
+			return  this.width && this.height ? ('width:' + this.width + 'px;height:' + this.height + 'px') : ''
+		}
+	},
+	beforeDestroy() {
+		this.clear()
+		this.dispose()
+		// #ifdef H5
+		if(this.isPc) {
+			document.removeEventListener('mousewheel')
+		}
+		// #endif
+	},
+	created() {
+		// #ifdef H5
+		if(!('ontouchstart' in window)) {
+			this.isPc = true
+			document.addEventListener('mousewheel', (e) => {
+				if(this.chart) {
+					const touch = this.getTouch(e)
+					const handler = this.chart.getZr().handler;
+					dispatch.call(handler, 'mousewheel', touch)
+				}
+			})
+		}
+		// #endif
+		// #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
+		const { SDKVersion, version, platform, environment } = uni.getSystemInfoSync();
+		// #endif
+		// #ifdef MP-WEIXIN
+		this.isPC = /windows/i.test(platform)
+		this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '2.9.2') >= 0 && !((/ios/i.test(platform) && /7.0.20/.test(version)) || /wxwork/i.test(environment)) && !this.isPC;
+		// #endif
+		// #ifdef MP-TOUTIAO
+		this.isPC = /devtools/i.test(platform)
+		this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '1.78.0') >= 0;
+		// #endif
+		// #ifdef MP-ALIPAY
+		this.use2dCanvas = this.type === '2d' && compareVersion(my.SDKVersion, '2.7.0') >= 0;
+		// #endif
+	},
+	mounted() {
+		this.$nextTick(() => {
+			this.$emit('finished')
+		})
+	},
+	methods: {
+		// #ifdef APP-NVUE
+		onMessage(e) {
+			const res = e?.detail?.data[0] || null;
+			if (res?.event) {
+				if(res.event === 'inited') {
+					this.inited = true
+				}
+				this.$emit(res.event, JSON.parse(res.data));
+			} else if(res?.file){
+				this.file = res.data
+			} else if(!res[0] && JSON.stringify(res[0]) != '{}'){
+				console.error(res);
+			} else {
+				console.log(...res)
+			}
+		},
+		// #endif
+		setChart(callback) {
+			if(!this.chart) {
+				console.warn(`组件还未初始化,请先使用 init`)
+				return
+			}
+			if(typeof callback === 'function' && this.chart) {
+				callback(this.chart);
+			}
+			// #ifdef APP-NVUE
+			if(typeof callback === 'function') {
+				this.$refs.webview.evalJs(`setChart(${JSON.stringify(callback.toString())}, ${JSON.stringify(this.roptions)})`);
+			}
+			// #endif
+		},
+		setOption() {
+			if (!this.chart || !this.chart.setOption) {
+				console.warn(`组件还未初始化,请先使用 init`)
+				return
+			}
+			// #ifndef APP-NVUE
+			this.chart.setOption(...arguments);
+			// #endif
+			// #ifdef APP-NVUE
+			this.$refs.webview.evalJs(`setOption(${JSON.stringify(arguments)})`);
+			// #endif
+		},
+		showLoading() {
+			if(this.chart) {
+				// #ifndef APP-NVUE
+				this.chart.showLoading(...arguments)
+				// #endif
+				// #ifdef APP-NVUE
+				this.$refs.webview.evalJs(`showLoading(${JSON.stringify(arguments)})`);
+				// #endif
+			}
+		},
+		hideLoading() {
+			if(this.chart) {
+				// #ifndef APP-NVUE
+				this.chart.hideLoading()
+				// #endif
+				// #ifdef APP-NVUE
+				this.$refs.webview.evalJs(`hideLoading()`);
+				// #endif
+			}
+		},
+		clear() {
+			if(this.chart) {
+				// #ifndef APP-NVUE
+				this.chart.clear()
+				// #endif
+				// #ifdef APP-NVUE
+				this.$refs.webview.evalJs(`clear()`);
+				// #endif
+			}
+		},
+		dispose() {
+			if(this.chart) {
+				// #ifndef APP-NVUE
+				this.chart.dispose()
+				// #endif
+				// #ifdef APP-NVUE
+				this.$refs.webview.evalJs(`dispose()`);
+				// #endif
+			}
+		},
+		resize(size) {
+			if(size && size.width && size.height) {
+				this.height = size.height
+				this.width = size.width
+				if(this.chart) {this.chart.resize(size)}
+				// #ifdef APP-NVUE
+				this.$refs.webview.evalJs(`resize(${size})`);
+				// #endif
+			} else {
+				this.$nextTick(() => {
+					// #ifndef APP-NVUE
+					uni.createSelectorQuery()
+						.in(this)
+						.select(`.lime-echart`)
+						.boundingClientRect()
+						.exec(res => {
+							if (res) {
+								let { width, height } = res[0];
+								this.width = width = width || 300;
+								this.height = height = height || 300;
+								this.chart.resize({width, height})
+							}
+						});
+					// #endif
+					// #ifdef APP-NVUE
+					this.$refs.webview.evalJs(`resize()`);
+					// #endif
+				})
+				
+			}
+			
+		},
+		canvasToTempFilePath(args = {}) {
+			// #ifndef APP-NVUE
+			const { use2dCanvas, canvasId, canvasNode } = this;
+			return new Promise((resolve, reject) => {
+				const copyArgs = Object.assign({
+					canvasId,
+					success: resolve,
+					fail: reject
+				}, args);
+				if (use2dCanvas) {
+					delete copyArgs.canvasId;
+					copyArgs.canvas = canvasNode;
+				}
+				uni.canvasToTempFilePath(copyArgs, this);
+			});
+			// #endif
+			// #ifdef APP-NVUE
+			this.file = ''
+			this.$refs.webview.evalJs(`canvasToTempFilePath()`);
+			return new Promise((resolve, reject) => {
+				this.$watch('file', async (file) => {
+					if(file) {
+						const tempFilePath = await base64ToPath(file)
+						resolve(args.success({tempFilePath}))
+					} else {
+						reject(args.fail({error: ``}))
+					}
+				})
+			})
+			// #endif
+		},
+		async init(echarts, ...args) {
+			// #ifdef APP-NVUE
+			if(arguments && !arguments.length) {
+				console.error('缺少参数:init(theme?:string, opts?: object, callback: function)')
+				return
+			}
+			// #endif
+			// #ifndef APP-NVUE
+			if(arguments && arguments.length < 2) {
+				console.error('缺少参数:init(echarts, theme?:string, opts?: object, callback: function)')
+				return
+			}
+			// #endif
+			let theme=null,opts={},callback;
+			
+			Array.from(arguments).forEach(item => {
+				if(typeof item === 'function') {
+					callback = item
+				}
+				if(['string'].includes(typeof item)) {
+					theme = item
+				}
+				if(typeof item === 'object') {
+					opts = item
+				}
+			})
+			
+			if(this.beforeDelay) {
+				await sleep(this.beforeDelay)
+			}
+			let config = await this.getContext();
+			// #ifndef APP-NVUE
+			if(typeof callback === 'function') {
+				setCanvasCreator(echarts, config)
+				this.chart = echarts.init(config.canvas, theme, Object.assign({}, config, opts))
+				callback(this.chart)
+			} else {
+				console.error('callback 非 function')
+			}
+			// #endif
+			// #ifdef APP-NVUE
+			if(callback) {
+				this.chart = {
+					setOption: (options) => {
+						this.roptions = options
+					}
+				}
+				callback(this.chart)
+				this.$refs.webview.evalJs(`init(${JSON.stringify(callback.toString())}, ${JSON.stringify(this.roptions)}, ${JSON.stringify(opts)}, ${theme})`)
+			} else {
+				console.error('callback 非 function')
+			}
+			// #endif
+		},
+		getContext() {
+			// #ifdef APP-NVUE
+			if(this.finished) {
+				return Promise.resolve(this.finished)
+			}
+			return new Promise(resolve => {
+				this.$watch('finished', (val) => {
+					if(val) {
+						resolve(this.finished)
+					}
+				})
+			})
+			// #endif
+			// #ifndef APP-NVUE
+			const { use2dCanvas} = this;
+			let dpr = devicePixelRatio
+			if (use2dCanvas) {
+				return new Promise(resolve => {
+					uni.createSelectorQuery()
+						.in(this)
+						.select(`#${this.canvasId}`)
+						.fields({
+							node: true,
+							size: true
+						})
+						.exec(res => {
+							let { node, width, height } = res[0];
+							this.width = width = width || 300;
+							this.height = height = height || 300;
+							const ctx = node.getContext('2d');
+							const canvas = new Canvas(ctx, this, true, node);
+							this.canvasNode = node
+							resolve({ canvas, width, height, devicePixelRatio: dpr, node });
+						});
+				});
+			}
+			return new Promise(resolve => {
+				uni.createSelectorQuery()
+					.in(this)
+					.select(`#${this.canvasId}`)
+					.boundingClientRect()
+					.exec(res => {
+						if (res) {
+							let { width, height } = res[0];
+							this.width = width = width || 300;
+							this.height = height = height || 300;
+							// #ifdef MP-TOUTIAO
+							dpr = !this.isPC ? devicePixelRatio : 1// 1.25
+							// #endif
+							// #ifndef MP-ALIPAY || MP-TOUTIAO
+							dpr = this.isPC ? devicePixelRatio : 1
+							// #endif
+							// #ifdef MP-ALIPAY || MP-LARK
+							dpr = devicePixelRatio
+							// #endif
+							this.rect = res[0]
+							this.nodeWidth = width * dpr;
+							this.nodeHeight = height * dpr;
+							const ctx = uni.createCanvasContext(this.canvasId, this);
+							const canvas =  new Canvas(ctx, this, false);
+							resolve({ canvas, width, height, devicePixelRatio: dpr });
+						}
+					});
+			});
+			// #endif
+		},
+		// #ifndef APP-NVUE
+		getRelative(e) {
+			return {x: e.pageX - this.rect.left, y: e.pageY - this.rect.top, wheelDelta: e.wheelDelta}
+		},
+		getTouch(e) {
+			return e.touches && e.touches[0] && e.touches[0].x ? e.touches[0] : this.getRelative(e);
+		},
+		touchStart(e) {
+			this.isDown = true
+			if (this.chart && ((e.touches.length > 0 || e.touches['0'])  && e.type != 'mousemove' || e.type == 'mousedown')) {
+				const touch = this.getTouch(e)
+				this.startX = touch.x
+				this.startY = touch.y
+				this.startT = new Date()
+				const handler = this.chart.getZr().handler;
+				dispatch.call(handler, 'mousedown', touch)
+				dispatch.call(handler, 'mousemove', touch)
+				handler.processGesture(wrapTouch(e), 'start');
+				clearTimeout(this.endTimer);
+			}
+		},
+		touchMove(e) {
+			if(this.isPc && this.enableHover && !this.isDown) {this.isDown = true}
+			if (this.chart && ((e.touches.length > 0 || e.touches['0']) && e.type != 'mousemove' || e.type == 'mousemove' && this.isDown)) {
+				const handler = this.chart.getZr().handler;
+				dispatch.call(handler, 'mousemove', this.getTouch(e))
+				handler.processGesture(wrapTouch(e), 'change');
+			}
+		},
+		touchEnd(e) {
+			this.isDown = false
+			if (this.chart) {
+				const {x} = e.changedTouches && e.changedTouches[0] || {}
+				const touch = (x ? e.changedTouches[0] : this.getRelative(e)) || {};
+				const handler = this.chart.getZr().handler;
+				const isClick = Math.abs(touch.x - this.startX) < 10 && new Date() - this.startT < 200;
+				dispatch.call(handler, 'mouseup', touch)
+				handler.processGesture(wrapTouch(e), 'end');
+				if(isClick) {
+					dispatch.call(handler, 'click', touch)
+				} else {
+					this.endTimer = setTimeout(() => {
+						dispatch.call(handler, 'mousemove', {x: 999999999,y: 999999999});
+						dispatch.call(handler, 'mouseup', {x: 999999999,y: 999999999});
+					},50)
+				}
+			}
+		}
+		// #endif
+	}
+};
+</script>
+<style scoped>
+.lime-echart {
+	position: relative;
+	/* #ifndef APP-NVUE */
+	width: 100%;
+	height: 100%;
+	/* #endif */
+	/* #ifdef APP-NVUE */
+	flex: 1;
+	/* #endif */
+}
+.lime-echart__canvas {
+	/* #ifndef APP-NVUE */
+	width: 100%;
+	height: 100%;
+	/* #endif */
+	/* #ifdef APP-NVUE */
+	flex: 1;
+	/* #endif */
+}
+</style>

+ 74 - 0
frontend_mobile/uni_modules/lime-echart/components/l-echart/utils.js

@@ -0,0 +1,74 @@
+// #ifndef APP-NVUE
+// 计算版本
+export function compareVersion(v1, v2) {
+	v1 = v1.split('.')
+	v2 = v2.split('.')
+	const len = Math.max(v1.length, v2.length)
+	while (v1.length < len) {
+		v1.push('0')
+	}
+	while (v2.length < len) {
+		v2.push('0')
+	}
+	for (let i = 0; i < len; i++) {
+		const num1 = parseInt(v1[i], 10)
+		const num2 = parseInt(v2[i], 10)
+
+		if (num1 > num2) {
+			return 1
+		} else if (num1 < num2) {
+			return -1
+		}
+	}
+	return 0
+}
+
+export function wrapTouch(event) {
+  for (let i = 0; i < event.touches.length; ++i) {
+    const touch = event.touches[i];
+    touch.offsetX = touch.x;
+    touch.offsetY = touch.y;
+  }
+  return event;
+}
+export const devicePixelRatio = wx.getSystemInfoSync().pixelRatio
+// #endif
+// #ifdef APP-NVUE
+export function base64ToPath(base64) {
+	return new Promise((resolve, reject) => {
+		const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64) || [];
+		const bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now())
+		bitmap.loadBase64Data(base64, () => {
+			if (!format) {
+				reject(new Error('ERROR_BASE64SRC_PARSE'))
+			}
+			const time = new Date().getTime();
+			const filePath = `_doc/uniapp_temp/${time}.${format}`
+			
+			bitmap.save(filePath, {}, 
+				() => {
+					bitmap.clear()
+					resolve(filePath)
+				}, 
+				(error) => {
+					bitmap.clear()
+					console.error(`${JSON.stringify(error)}`)
+					reject(error)
+				})
+		}, (error) => {
+			bitmap.clear()
+			console.error(`${JSON.stringify(error)}`)
+			reject(error)
+		})
+	})
+}
+// #endif
+
+
+export function sleep(time) {
+	return new Promise((resolve) => {
+		setTimeout(() => {
+			resolve(true)
+		},time)
+	})
+}

+ 0 - 0
frontend_mobile/uni_modules/lime-echart/components/lime-echart/index.vue


+ 84 - 0
frontend_mobile/uni_modules/lime-echart/package.json

@@ -0,0 +1,84 @@
+{
+  "id": "lime-echart",
+  "displayName": "百度图表 echarts",
+  "version": "0.6.5",
+  "description": "echarts 全端兼容,一款使echarts图表能跑在uniapp各端中的插件",
+  "keywords": [
+    "echarts",
+    "canvas",
+    "图表",
+    "可视化"
+],
+  "repository": "https://gitee.com/liangei/lime-echart",
+  "engines": {
+    "HBuilderX": "^3.6.4"
+  },
+"dcloudext": {
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "",
+    "type": "component-vue"
+  },
+  "uni_modules": {
+    "dependencies": [],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y"
+      },
+      "client": {
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "y"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "u",
+          "IE": "u",
+          "Edge": "u",
+          "Firefox": "u",
+          "Safari": "u"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "y",
+          "字节跳动": "y",
+        "QQ": "y",
+        "钉钉": "u",
+        "快手": "u",
+        "飞书": "u",
+        "京东": "u"
+        },
+        "快应用": {
+          "华为": "u",
+          "联盟": "u"
+        },
+        "Vue": {
+            "vue2": "y",
+            "vue3": "y"
+        }
+      }
+    }
+  }
+}

+ 245 - 0
frontend_mobile/uni_modules/lime-echart/readme.md

@@ -0,0 +1,245 @@
+# echarts 图表 <span style="font-size:16px;">👑👑👑👑👑 <span style="background:#ff9d00;padding:2px 4px;color:#fff;font-size:10px;border-radius: 3px;">全端</span></span>
+> 一个基于 JavaScript 的开源可视化图表库   [查看更多 站点1](https://limeui.qcoon.cn/#/echart) |  [查看更多 站点2](http://liangei.gitee.io/limeui/#/echart)  <br>
+> 基于 echarts 做了兼容处理,更多示例请访问  [uni示例 站点1](https://limeui.qcoon.cn/#/echart-example) | [uni示例 站点2](http://liangei.gitee.io/limeui/#/echart-example) | [官方示例](https://echarts.apache.org/examples/zh/index.html)     <br>
+> Q群:1046793420 <br>
+
+## 平台兼容
+
+| H5  | 微信小程序 | 支付宝小程序 | 百度小程序 | 头条小程序 | QQ 小程序 | App  |
+| --- | ---------- | ------------ | ---------- | ---------- | --------- | ---- |
+| √   | √          | √         | √      | √       | √      | √ |
+
+
+## 安装
+- 第一步、在uniapp 插件市场 找到 [百度图表](https://ext.dcloud.net.cn/plugin?id=4899) 导入
+- 第二步、安装 echarts 或者直接使用插件内的echarts.min文件
+```cmd
+pnpm add echarts
+ -or-
+npm install echarts
+```
+
+
+**注意** 
+* 🔔 必须使用hbuilderx 3.4.8-alpha及以上
+* 🔔 echarts 5.3.0及以上
+* 🔔 如果是 `cli` 项目需要主动 `import` 插件
+```js
+import LEchart from '@/uni_modules/lime-echart/components/l-echart/l-echart.vue';
+export default {
+	components: {LEchart}
+}
+```
+
+## 代码演示
+### 基础用法
+```html
+<view><l-echart ref="chart" @finished="init"></l-echart></view>
+```
+
+```js
+// 如果你使用插件内提供的echarts.min
+// 也可以自行去官网下载自定义覆盖
+// 这种方式仅限于vue2
+import * as echarts from '@/uni_modules/lime-echart/static/echarts.min'
+//---or----------------------------------
+
+// 如果你使用 npm 安装了 echarts --------- 使用以下方式
+// 引入全量包
+import * as echarts from 'echarts'
+//---or----------------------------------
+
+// 按需引入 开始
+import * as echarts from 'echarts/core';
+import {LineChart, BarChart} from 'echarts/charts';
+import {TitleComponent,TooltipComponent,GridComponent, DatasetComponent, TransformComponent, LegendComponent } from 'echarts/components';
+// 标签自动布局,全局过渡动画等特性
+import {LabelLayout,UniversalTransition} from 'echarts/features';
+// 引入 Canvas 渲染器,注意引入 CanvasRenderer 是必须的一步
+import {CanvasRenderer} from 'echarts/renderers';
+
+// 注册必须的组件
+echarts.use([
+	LegendComponent,
+	TitleComponent,
+	TooltipComponent,
+	GridComponent,
+	DatasetComponent,
+	TransformComponent,
+	LineChart,
+	BarChart,
+	LabelLayout,
+	UniversalTransition,
+	CanvasRenderer
+]);
+//-------------按需引入结束------------------------
+
+
+export default {
+	data() {
+		return {
+			option: {
+				tooltip: {
+					trigger: 'axis',
+					axisPointer: {
+						type: 'shadow' 
+					},
+					confine: true
+				},
+				legend: {
+					data: ['热度', '正面', '负面']
+				},
+				grid: {
+					left: 20,
+					right: 20,
+					bottom: 15,
+					top: 40,
+					containLabel: true
+				},
+				xAxis: [
+					{
+						type: 'value',
+						axisLine: {
+							lineStyle: {
+								color: '#999999'
+							}
+						},
+						axisLabel: {
+							color: '#666666'
+						}
+					}
+				],
+				yAxis: [
+					{
+						type: 'category',
+						axisTick: { show: false },
+						data: ['汽车之家', '今日头条', '百度贴吧', '一点资讯', '微信', '微博', '知乎'],
+						axisLine: {
+							lineStyle: {
+								color: '#999999'
+							}
+						},
+						axisLabel: {
+							color: '#666666'
+						}
+					}
+				],
+				series: [
+					{
+						name: '热度',
+						type: 'bar',
+						label: {
+							normal: {
+								show: true,
+								position: 'inside'
+							}
+						},
+						data: [300, 270, 340, 344, 300, 320, 310],
+					},
+					{
+						name: '正面',
+						type: 'bar',
+						stack: '总量',
+						label: {
+							normal: {
+								show: true
+							}
+						},
+						data: [120, 102, 141, 174, 190, 250, 220]
+					},
+					{
+						name: '负面',
+						type: 'bar',
+						stack: '总量',
+						label: {
+							normal: {
+								show: true,
+								position: 'left'
+							}
+						},
+						data: [-20, -32, -21, -34, -90, -130, -110]
+					}
+				]
+			},
+		};
+	},
+	// 组件能被调用必须是组件的节点已经被渲染到页面上
+	// 1、在页面mounted里调用,有时候mounted 组件也未必渲染完成
+	mounted() {
+		// init(echarts, theme?:string, opts?:{}, chart => {})
+		// echarts 必填, 非nvue必填,nvue不用填
+		// theme 可选,应用的主题,目前只支持名称,如:'dark'
+		// opts = { // 可选
+		//	locale?: string  // 从 `5.0.0` 开始支持
+		// }
+		// chart => {} , callback 必填,返回图表实例
+		this.$refs.chart.init(echarts, chart => {
+			chart.setOption(this.option);
+		});
+	},
+	// 2、或者使用组件的finished事件里调用
+	methods: {
+		init() {
+			this.$refs.chart.init(echarts, chart => {
+				chart.setOption(this.option);
+			});
+		}
+	}
+}
+```
+
+## 数据更新
+- 使用 `ref` 可获取`setOption`设置更新
+
+```js
+this.$refs.chart.setOption(data)
+```
+
+## 图表大小
+- 在有些场景下,我们希望当容器大小改变时,图表的大小也相应地改变。
+
+```js
+// 默认获取容器尺寸
+this.$refs.chart.resize()
+// 指定尺寸
+this.$refs.chart.resize({width: 375, height: 375})
+```
+
+
+## 常见问题
+- 微信小程序 `2d` 只支持 真机调试2.0
+- 微信开发工具会出现canvas不跟随页面的情况,真机不影响
+- toolbox 不支持 `saveImage`
+- echarts 5.3.0 的 lines 不支持 trailLength,故需设置为 `0`
+- dataZoom H5不要设置 `showDetail` 
+
+
+## Props
+
+| 参数             | 说明                                                            | 类型             | 默认值        | 版本 	|
+| ---------------  | --------                                                        | -------         | ------------ | ----- 	|
+| custom-style     | 自定义样式                                                      |   `string`       | -            | -     	|
+| type             | 指定 canvas 类型                                				 |    `string`      | `2d`         |   	    |
+| is-disable-scroll | 触摸图表时是否禁止页面滚动                                       |    `boolean`     | `false`     |   	    |
+| beforeDelay       |  延迟初始化 (毫秒)                       						|    `number`     | `30`     |   	    |
+| enableHover       |  PC端使用鼠标悬浮                       						|    `boolean`     | `false`     |   	    |
+
+## 事件
+
+| 参数                    | 说明                                                                                                             |
+| ---------------        | ---------------                                                                                                  |
+| init(echarts, chart => {})  | 初始化调用函数,第一个参数是传入`echarts`,第二个参数是回调函数,回调函数的参数是 `chart` 实例                                           |  
+| setChart(chart => {})        | 已经初始化后,请使用这个方法,是个回调函数,参数是 `chart` 实例                  |  
+| setOption(data)        | [图表配置项](https://echarts.apache.org/zh/option.html#title),用于更新 ,传递是数据 `option`  |  
+| clear()                | 清空当前实例,会移除实例中所有的组件和图表。  |  
+| dispose()              | 销毁实例  |  
+| showLoading()          | 显示加载  |  
+| hideLoading()          | 隐藏加载  |  
+| [canvasToTempFilePath](https://uniapp.dcloud.io/api/canvas/canvasToTempFilePath.html#canvastotempfilepath)(opt)  | 用于生成图片,与官方使用方法一致,但不需要传`canvasId`  |  
+
+
+## 打赏
+如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。  
+
+![输入图片说明](https://static-6d65bd90-8508-4d6c-abbc-a4ef5c8e49e7.bspapp.com/image/222521_bb543f96_518581.jpeg "微信图片编辑_20201122220352.jpg")
+![输入图片说明](https://static-6d65bd90-8508-4d6c-abbc-a4ef5c8e49e7.bspapp.com/image/wxplay.jpg "wxplay.jpg")

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
frontend_mobile/uni_modules/lime-echart/static/ecStat.min.js


Fichier diff supprimé car celui-ci est trop grand
+ 34 - 0
frontend_mobile/uni_modules/lime-echart/static/echarts.min.js


+ 129 - 0
frontend_mobile/uni_modules/lime-echart/static/index.html

@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<html lang="zh">
+	<head>
+		<meta charset="UTF-8">
+		<meta name="viewport"
+			content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
+		<meta http-equiv="X-UA-Compatible" content="ie=edge">
+		<title></title>
+		<style type="text/css">
+			html,
+			body,
+			.canvas {
+				padding: 0;
+				margin: 0;
+				overflow-y: hidden;
+				background-color: transparent;
+				width: 100%;
+				height: 100%;
+			}
+		</style>
+	</head>
+	<body>
+		<div class="canvas" id="limeChart"></div>
+		<script type="text/javascript" src="./uni.webview.1.5.3.js"></script>
+		<script type="text/javascript" src="./echarts.min.js"></script>
+		<script type="text/javascript" src="./ecStat.min.js"></script>
+		<!-- <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts-liquidfill@latest/dist/echarts-liquidfill.min.js"></script> -->
+		<script>
+			let chart = null;
+			let cache = [];
+			console.log = function(...agrs) {
+				postMessage(agrs)
+			}
+			function emit(event, data) {
+				let dataStr = JSON.stringify(data, stringify)
+				postMessage({
+					event,
+					data: dataStr
+				})
+				cache = []
+			}
+			function postMessage(data) {
+				uni.postMessage({
+					data
+				});
+			}
+			function stringify(key, value) {
+				if (typeof value === 'object' && value !== null) {
+					if (cache.indexOf(value) !== -1) {
+						return;
+					}
+					cache.push(value);
+				}
+				return value;
+			}
+			function parse(name, callback, options) {
+				const optionNameReg = /[\w]+\.setOption\(([\w]+\.)?([\w]+)\)/
+				if (optionNameReg.test(callback)) {
+					const optionNames = callback.match(optionNameReg)
+					if(optionNames[1]) {
+						const _this = optionNames[1].split('.')[0]
+						window[_this] = {}
+						window[_this][optionNames[2]] = options
+						return optionNames[2]
+					} else {
+						return null
+					}
+				}
+				return null
+			}
+			function init(callback, options, opts = {}, theme = null) {
+				if(!chart) {
+					chart = echarts.init(document.getElementById('limeChart'), theme, opts)
+					if(options) {
+						chart.setOption(options)
+					}
+					// const name = parse('a', callback, options)
+					// console.log('options::', callback)
+					// if(name) this[name] = options
+					// eval(`a = ${callback};`)
+					// if(a) {a(chart)}
+				}
+			}
+			
+			function setChart(callback, options) {
+				if(!callback) return
+				if(chart && callback && options) {
+					var r = null
+					const name = parse('r', callback, options)
+					if(name) this[name] = options
+					eval(`r = ${callback};`)
+					if(r) {r(chart)}
+				}
+			}
+			function setOption(data) {
+				if (chart) chart.setOption(data[0], data[1])
+			}
+			function showLoading(data) {
+				if (chart) chart.showLoading(data[0], data[1])
+			}
+			
+			function hideLoading() {
+				if (chart) chart.hideLoading()
+			}
+			
+			function clear() {
+				if (chart) chart.clear()
+			
+			}
+			
+			function dispose() {
+				if (chart) chart.dispose()
+			}
+			function resize(size) {
+				if (chart) chart.resize(size)
+			}
+			
+			function canvasToTempFilePath(opt = {}) {
+				if (chart) {
+				  const src = chart.getDataURL(opt)
+				  postMessage({
+					  file: true,
+					  data: src
+				  })
+				}
+			}
+		</script>
+	</body>
+</html>

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
frontend_mobile/uni_modules/lime-echart/static/uni.webview.1.5.3.js


+ 2 - 1
frontend_mobile/utils/index.js

@@ -266,7 +266,8 @@ export function shuffle(array) {
  */
 export function formatPrice(price, currency = 'CNY') {
   if (!price) price = 0
-  return price.toLocaleString('zh-CN', { style: 'currency', currency })
+  // return price.toLocaleString('zh-CN', { style: 'currency', currency })
+  return '¥' + String(price).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
 }
 
 // 回显数据字典

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff