刘洋 1 an în urmă
părinte
comite
98476e36f2

+ 2 - 2
.env.development

@@ -2,7 +2,7 @@
 NODE_ENV = development
 
 # 接口地址,注意协议,如果你没有配置 ssl,需要将 https 改为 http
-VUE_APP_BASE_API  = 'http://localhost:8100'
+VUE_APP_BASE_API  = 'http://192.168.10.83:7710'
 
 # 分享的前缀
-VUE_APP_SHARE_PREFIX = 'http://127.0.0.1:8101'
+VUE_APP_SHARE_PREFIX = ''

+ 1 - 1
.gitignore

@@ -4,7 +4,7 @@ unpackage/
 dist/
 .hbuilderx/
 
-
+package-lock.json
 *.lock
 # local env files
 .env.local

+ 5 - 2
package.json

@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "private": true,
   "scripts": {
-    "start":"npm run serve",
+    "start": "npm run serve",
     "serve": "npm run dev:mp-weixin",
     "build": "npm run build:mp-weixin",
     "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .eslintignore",
@@ -75,6 +75,7 @@
     "@dcloudio/uni-quickapp-webview": "2.0.2-3080420230530001",
     "@dcloudio/uni-stacktracey": "^2.0.2-3080420230530001",
     "@dcloudio/uni-stat": "2.0.2-3080420230530001",
+    "@dcloudio/uni-ui": "^1.4.28",
     "@vue/shared": "^3.2.34",
     "better-mock": "^0.3.6",
     "core-js": "^3.22.5",
@@ -84,9 +85,9 @@
     "lodash-es": "^4.17.21",
     "luch-request": "3.0.8",
     "qs": "^6.10.1",
+    "spark-md5": "^3.0.2",
     "uni-simple-router": "^2.0.7",
     "uview-ui": "^1.8.8",
-    "@dcloudio/uni-ui": "^1.4.28",
     "vue": "^2.6.14",
     "vuex": "^3.6.2"
   },
@@ -122,6 +123,8 @@
     "sass": "^1.60.0",
     "sass-loader": "^8.0.2",
     "uni-read-pages": "^1.0.5",
+    "unocss-preset-weapp": "^0.55.2",
+    "unocss-webpack-uniapp2": "^0.2.2",
     "vue-template-compiler": ">= 2.6.14 < 2.7"
   },
   "browserslist": [

+ 11 - 0
src/App.vue

@@ -15,4 +15,15 @@
 <style lang="scss">
   /* 注意要写在第一行,同时给style标签加入lang="scss"属性 */
   @import 'uview-ui/index.scss';
+  @import './styles/global.scss';
+  page {
+    background: #f2f3f5;
+  }
+  .uno-start {
+    --un: 0;
+  }
+  /* unocss 代码生成在这 */
+  .uno-end {
+    --un: 0;
+  }
 </style>

+ 2 - 0
src/api/common.js

@@ -0,0 +1,2 @@
+import { http } from '@/service/request.js'
+export const getServiceUnit = () => http.post('/api/admin/common/query_service_unit')

+ 10 - 0
src/api/user.js

@@ -1,4 +1,14 @@
 import { http } from '@/service/request.js'
+import { getBase64 } from '@/utils/crypto'
+//模拟PC端的账号密码登录先调试,仅前期调试使用,该接口后期会弃用
+export const pcLogin = () =>
+  http.post(
+    '/api/admin/common/login',
+    { loginName: 'admin1', password: getBase64('123456'), type: 'ACCOUNT' },
+    {
+      custom: { noAuth: true }
+    }
+  )
 
 /**
  * 微信小程序模拟登录

+ 7 - 0
src/api/workbenches.js

@@ -0,0 +1,7 @@
+import { http } from '@/service/request.js'
+
+/**
+ * 我的工作台:消息和公告接口
+ */
+export const getMyMessages = (params) => http.post(`/api/sys/message/pageByTypes`, {}, { params, custom: { loading: true } })
+export const getMyWaits = (params) => http.post(`/api/admin/flow/task/list`, {}, { params, custom: { loading: true } })

+ 25 - 0
src/components/date-range.vue

@@ -0,0 +1,25 @@
+<template>
+  <view>
+    <u-input :value="dateStr" type="select" :border="true" placeholder="请输入" clearable @click="show = true" />
+    <u-calendar v-model="show" mode="range" @change="change"></u-calendar>
+  </view>
+</template>
+<script>
+  export default {
+    name: 'DateRange',
+    props: ['value'],
+    data() {
+      return {
+        show: false,
+        dateStr: ''
+      }
+    },
+    methods: {
+      change(val) {
+        this.dateStr = val.startDate + ' - ' + val.endDate
+        this.$emit('update:value', [new Date(val.startDate).getTime(), new Date(val.endDate).getTime()])
+      }
+    }
+  }
+</script>
+<style lang="scss" scoped></style>

+ 43 - 0
src/components/message-type.vue

@@ -0,0 +1,43 @@
+<template>
+  <!-- <u-select
+    v-model="showSelect"
+    :list="dictToOptionList(MESSAGE_TYPE)"
+    :mask-close-able="false"
+    @cancel="$emit('update:show', false)"
+    @confirm="confirm"
+  ></u-select> -->
+  <MultPicker
+    :show="showSelect"
+    :columns="dictToOptionList(MESSAGE_TYPE) || []"
+    @confirm="confirm"
+    @cancel="$emit('update:show', false)"
+  ></MultPicker>
+</template>
+<script>
+  import { dictToOptionList } from '@/utils/utils'
+  import { MESSAGE_TYPE } from '@/utils/constants'
+  import MultPicker from '@/components/mult-picker.vue'
+  export default {
+    name: 'messageType',
+    components: { MultPicker },
+    props: ['show'],
+    data() {
+      return {
+        dictToOptionList,
+        MESSAGE_TYPE,
+        showSelect: false
+      }
+    },
+    methods: {
+      confirm(obj) {
+        this.$emit('update:value', obj.value)
+        this.$emit('update:show', false)
+      }
+    },
+    watch: {
+      show(val) {
+        this.showSelect = val
+      }
+    }
+  }
+</script>

+ 258 - 0
src/components/mult-picker.vue

@@ -0,0 +1,258 @@
+<template>
+  <view class="popup" v-show="show">
+    <view class="bg" @tap="cancelMultiple"></view>
+    <view class="selectMultiple" :animation="animationData">
+      <view class="multipleBody">
+        <view class="title">
+          <view class="close" @tap="cancelMultiple"> 取消 </view>
+          <view class="name">
+            {{ title }}
+          </view>
+          <view class="confirm" @tap="confirmMultiple"> 确认 </view>
+        </view>
+        <view class="list">
+          <view class="mask mask-top"></view>
+          <view class="mask mask-bottom"></view>
+          <scroll-view class="diet-list" scroll-y="true">
+            <view v-for="(item, index) in list" :class="['item', item.selected ? 'checked' : '']" @tap="onChange(index, item)" :key="index">
+              <span>{{ item.label }}</span>
+              <view class="icon" v-show="item.selected">
+                <icon type="success_no_circle" size="16" color="#2D8DFF" />
+              </view>
+            </view>
+          </scroll-view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'multiple-picker',
+    data() {
+      return {
+        // 选中值
+        value: [],
+        // 选中列表
+        selected: [],
+        // 列表数据
+        list: [],
+        // 出场动画
+        animationData: {}
+      }
+    },
+    props: {
+      // 是否显示
+      show: {
+        type: Boolean,
+        default: false
+      },
+      // 标题
+      title: {
+        type: String,
+        default: ''
+      },
+      //数据列表
+      columns: {
+        type: Array,
+        default: [
+          {
+            label: '测试1',
+            value: '1'
+          }
+        ]
+      },
+      // 默认选中
+      defaultIndex: {
+        type: Array,
+        default: []
+      }
+    },
+    watch: {
+      // 监听是否显示
+      show(val) {
+        if (val) {
+          this.openMultiple()
+        }
+      }
+    },
+    methods: {
+      // 列点击事件
+      onChange(index, item) {
+        // 是否已选中
+        if (this.value.indexOf(item.value.toString()) >= 0) {
+          this.list[index].selected = false
+        } else {
+          this.list[index].selected = true
+        }
+
+        // 筛选已勾选数据
+        this.value = []
+        this.selected = []
+        this.list.forEach((col_item, col_index) => {
+          if (col_item.selected) {
+            this.value.push(col_item.value.toString())
+            this.selected.push({
+              label: col_item.label,
+              value: col_item.value
+            })
+          }
+        })
+        this.$emit('change', { selected: this.selected, value: this.value })
+      },
+      // 弹出框开启触发事件
+      openMultiple() {
+        // 初始化列表数据,默认勾选数据
+        this.value = this.defaultIndex
+        this.columns.forEach((item, index) => {
+          this.$set(item, 'selected', false)
+          if (this.value.indexOf(item.value.toString()) >= 0) {
+            item.selected = true
+          }
+        })
+        this.list = Object.assign([], this.columns)
+        // 弹出动画
+        this.openAnimation()
+      },
+      // 确认
+      confirmMultiple() {
+        this.$emit('confirm', { selected: this.selected, value: this.value })
+      },
+      // 关闭/取消
+      cancelMultiple() {
+        this.$emit('cancel')
+      },
+      // 展开动画
+      openAnimation() {
+        var animation = uni.createAnimation()
+        animation.translate(0, 300).step({ duration: 0 })
+        this.animationData = animation.export()
+        this.$nextTick(() => {
+          animation.translate(0, 0).step({ duration: 300, timingFunction: 'ease' })
+          this.animationData = animation.export()
+        })
+      }
+    }
+  }
+</script>
+
+<style scoped lang="scss">
+  .popup {
+    width: 100%;
+    height: 100vh;
+    position: fixed;
+    z-index: 99999;
+    left: 0;
+    bottom: 0;
+
+    .bg {
+      width: 100%;
+      height: 100%;
+      background-color: rgba(black, 0.5);
+    }
+  }
+  .selectMultiple {
+    width: 100%;
+    position: absolute;
+    left: 0;
+    bottom: 0;
+    background-color: white;
+
+    .multipleBody {
+      width: 100%;
+      padding: 30rpx;
+      box-sizing: border-box;
+      padding-bottom: 80rpx;
+
+      .title {
+        font-size: 28rpx;
+        display: flex;
+        flex-direction: row;
+
+        .close {
+          width: 80rpx;
+          //   opacity: 0.5;
+          color: #606266;
+        }
+        .name {
+          width: 530rpx;
+          text-align: center;
+          overflow: hidden;
+          display: -webkit-box;
+          -webkit-box-orient: vertical;
+          -webkit-line-clamp: 1;
+        }
+        .confirm {
+          width: 80rpx;
+          text-align: right;
+          color: #2979ff;
+        }
+      }
+      .list {
+        width: 100%;
+        padding-top: 30rpx;
+        position: relative;
+
+        .mask {
+          width: 100%;
+          height: 120rpx;
+          position: absolute;
+          left: 0;
+          z-index: 2;
+          pointer-events: none;
+
+          &.mask-top {
+            top: 30rpx;
+            background-image: linear-gradient(to bottom, #fff, rgba(#fff, 0));
+          }
+          &.mask-bottom {
+            bottom: 0;
+            background-image: linear-gradient(to bottom, rgba(#fff, 0), #fff);
+          }
+        }
+
+        .diet-list {
+          max-height: 400rpx;
+        }
+
+        .item {
+          position: relative;
+          width: 100%;
+          line-height: 40rpx;
+          border-bottom: 1px solid rgba($color: #000000, $alpha: 0.05);
+          padding: 20rpx 0;
+          font-size: 30rpx;
+          box-sizing: border-box;
+          text-align: center;
+
+          span {
+            overflow: hidden;
+            display: -webkit-box;
+            -webkit-box-orient: vertical;
+            -webkit-line-clamp: 1;
+            padding: 0 40rpx;
+          }
+
+          .icon {
+            position: absolute;
+            right: 10rpx;
+            top: 50%;
+            transform: translateY(-50%);
+            height: 16px;
+          }
+          &.checked {
+            color: #2d8dff;
+          }
+          &:last-child {
+            border-bottom: none;
+            margin-bottom: 60rpx;
+          }
+          &:first-child {
+            margin-top: 60rpx;
+          }
+        }
+      }
+    }
+  }
+</style>

+ 93 - 0
src/components/my-popup.vue

@@ -0,0 +1,93 @@
+<template>
+  <view class="popup" v-show="show">
+    <view class="bg" @tap="cancelMultiple"></view>
+    <view class="selectMultiple" :animation="animationData">
+      <view class="multipleBody">
+        <slot></slot>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'MyPopup',
+    data() {
+      return {
+        // 出场动画
+        animationData: {}
+      }
+    },
+    props: {
+      // 是否显示
+      show: {
+        type: Boolean,
+        default: false
+      },
+      // 标题
+      title: {
+        type: String,
+        default: ''
+      }
+    },
+    watch: {
+      // 监听是否显示
+      show(val) {
+        if (val) {
+          this.openMultiple()
+        }
+      }
+    },
+    methods: {
+      // 弹出框开启触发事件
+      openMultiple() {
+        // 弹出动画
+        this.openAnimation()
+      },
+      // 关闭/取消
+      cancelMultiple() {
+        this.$emit('cancel')
+      },
+      // 展开动画
+      openAnimation() {
+        var animation = uni.createAnimation()
+        animation.translate(0, 300).step({ duration: 0 })
+        this.animationData = animation.export()
+        this.$nextTick(() => {
+          animation.translate(0, 0).step({ duration: 300, timingFunction: 'ease' })
+          this.animationData = animation.export()
+        })
+      }
+    }
+  }
+</script>
+
+<style scoped lang="scss">
+  .popup {
+    width: 100%;
+    height: 100vh;
+    position: fixed;
+    z-index: 99999;
+    left: 0;
+    bottom: 0;
+
+    .bg {
+      width: 100%;
+      height: 100%;
+      background-color: rgba(black, 0.6);
+    }
+    .selectMultiple {
+      width: 100%;
+      position: absolute;
+      left: 0;
+      bottom: 0;
+      background-color: white;
+
+      .multipleBody {
+        width: 100%;
+        padding: 30rpx;
+        box-sizing: border-box;
+      }
+    }
+  }
+</style>

+ 39 - 0
src/components/service-unit.vue

@@ -0,0 +1,39 @@
+<template>
+  <view>
+    <u-input :value="serviceLabel" type="select" :border="true" placeholder="请选择" @click="show = !show" />
+    <u-select v-model="show" :list="list" @confirm="confirm"></u-select>
+  </view>
+</template>
+<script>
+  import { getServiceUnit } from '@/api/common'
+  export default {
+    name: 'ServiceUnit',
+    props: ['value'],
+    computed: {
+      serviceLabel() {
+        return this.list.find((item) => item.value == this.value)?.label || ''
+      }
+    },
+    data() {
+      return {
+        show: false,
+        list: []
+      }
+    },
+    methods: {
+      confirm(arr) {
+        this.$emit('update:value', arr[0].value)
+      }
+    },
+    mounted() {
+      getServiceUnit().then((res) => {
+        this.list = (res || []).map((item) => {
+          return {
+            value: item.id,
+            label: item.name
+          }
+        })
+      })
+    }
+  }
+</script>

+ 5 - 2
src/main.js

@@ -7,11 +7,12 @@ import uView from 'uview-ui'
 
 // 此处为演示vuex使用,非uView的功能部分
 import store from '@/store'
-
+import { pcLogin } from './api/user'
 // 引入uView对小程序分享的mixin封装
 const mpShare = require('uview-ui/libs/mixin/mpShare.js')
 // 引入uView提供的对vuex的简写法文件
 import vuexStore from '@/store/$u.mixin.js'
+import { sleep } from './utils/utils'
 Vue.mixin(vuexStore)
 Vue.mixin(mpShare)
 
@@ -24,7 +25,9 @@ Vue.config.productionTip = false
 
 // 小程序页面组件和这个 App.vue 组件的写法和引入方式是一致的,为了区分两者,需要设置mpType值
 App.mpType = 'app'
-
+pcLogin().then((res) => {
+  uni.setStorageSync('user', res)
+})
 const app = new Vue({
   store,
   ...App

+ 23 - 10
src/pages.json

@@ -22,14 +22,21 @@
     {
       "path": "pages/index/index",
       "style": {
-        "navigationBarTitleText": "首页",
+        "navigationBarTitleText": "工作台",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/sop/sop",
+      "style": {
+        "navigationBarTitleText": "SOP",
         "enablePullDownRefresh": false
       }
     },
     {
       "path": "pages/home/home",
       "style": {
-        "navigationBarTitleText": "我的",
+        "navigationBarTitleText": "考勤",
         "enablePullDownRefresh": false
       }
     }
@@ -56,22 +63,28 @@
     "backgroundColor": "#FFFFFF"
   },
   "tabBar": {
-    "color": "#909399",
-    "selectedColor": "#303133",
+    "color": "#262626",
+    "selectedColor": "#165DFF",
     "backgroundColor": "#FFFFFF",
     "borderStyle": "black",
     "list": [
       {
         "pagePath": "pages/index/index",
-        "iconPath": "static/icon/tab/icon_home.png",
-        "selectedIconPath": "static/icon/tab/icon_home_c.png",
-        "text": "首页"
+        "iconPath": "static/icon/tab/tab1.png",
+        "selectedIconPath": "static/icon/tab/tab1_active.png",
+        "text": "工作台"
+      },
+      {
+        "pagePath": "pages/sop/sop",
+        "iconPath": "static/icon/tab/tab2.png",
+        "selectedIconPath": "static/icon/tab/tab2_active.png",
+        "text": "SOP"
       },
       {
         "pagePath": "pages/home/home",
-        "iconPath": "static/icon/tab/icon_wode.png",
-        "selectedIconPath": "static/icon/tab/icon_wode_c.png",
-        "text": "我的"
+        "iconPath": "static/icon/tab/tab3.png",
+        "selectedIconPath": "static/icon/tab/tab3_active.png",
+        "text": "考勤"
       }
     ]
   }

+ 215 - 0
src/pages/home/home-demo.vue

@@ -0,0 +1,215 @@
+<template>
+  <view class="home">
+    <!-- <camera class="camera" device-position="front" flash="off"></camera> -->
+    <image v-if="tempImg" mode="widthFix" :src="tempImg" />
+  </view>
+</template>
+
+<script>
+  import { imgToBase64 } from '@/utils/utils'
+  export default {
+    name: 'HomePage',
+    data() {
+      return {
+        cameraAuth: false,
+        locationAuth: false,
+        tempImg: '',
+        VKSession: null,
+        videoCtx: null,
+        listener: null
+      }
+    },
+    onShow() {
+      this.getLocationAuth()
+    },
+    onUnload() {
+      this.VKSession?.destroy()
+      this.listener?.stop({
+        complete: (res) => {
+          console.log('listener.stop', res)
+        }
+      })
+      this.VKSession = null
+      this.listener = null
+    },
+    methods: {
+      getMyLocation() {
+        console.log('getMyLocation:')
+        uni.getLocation({
+          success(res) {
+            console.log('位置信息', res)
+          },
+          fail(err) {
+            console.log('fail:', err)
+          }
+        })
+      },
+      async openSetting() {
+        const _this = this
+        let promise = new Promise((resolve, reject) => {
+          uni.showModal({
+            title: '授权',
+            content: '请先授权获取摄像头权限',
+            success(res) {
+              if (res.confirm) {
+                uni.openSetting({
+                  success(res) {
+                    if (res.authSetting['scope.camera']) {
+                      // 用户打开了授权开关
+                      resolve(true)
+                    } else {
+                      // 用户没有打开授权开关, 继续打开设置页面
+                      _this.openSetting().then((res) => {
+                        resolve(true)
+                      })
+                    }
+                  },
+                  fail(res) {
+                    console.log(res)
+                  }
+                })
+              } else if (res.cancel) {
+                // setTimeout(() => {
+                //   _this.openSetting().then((res) => {
+                //     resolve(true)
+                //   })
+                // }, 3000)
+              }
+            }
+          })
+        })
+        return promise
+      },
+      async getLocationAuth() {
+        const _this = this
+        uni.getSetting({
+          success(res) {
+            console.log('authSetting:', res.authSetting)
+
+            if (res.authSetting['scope.userLocation']) {
+              // 用户已经授权
+              _this.locationAuth = true
+              _this.getMyLocation()
+            } else {
+              uni.authorize({
+                scope: 'scope.userLocation',
+                success() {
+                  // 用户同意授权
+                  _this.locationAuth = true
+                  _this.getMyLocation()
+                },
+                fail() {
+                  // 用户不同意授权
+                  // _this.openSetting().then((res) => {
+                  //   console.log('终于授权了')
+                  //   _this.cameraAuth = true
+                  // })
+                  _this.locationAuth = false
+                }
+              })
+            }
+          }
+        })
+      },
+      //获取摄像头权限,按需调用,如果该用户被配置为需要人脸打卡,再引导用户开启
+      getCameraAuth() {
+        const _this = this
+        uni.getSetting({
+          success(res) {
+            if (res.authSetting['scope.camera']) {
+              // 用户已经授权
+              _this.cameraAuth = true
+              _this.initFaceData()
+            } else {
+              uni.authorize({
+                scope: 'scope.camera',
+                success() {
+                  // 用户同意授权
+                  _this.cameraAuth = true
+                  _this.initFaceData()
+                },
+                fail() {
+                  // 用户不同意授权
+                  // _this.openSetting().then((res) => {
+                  //   console.log('终于授权了')
+                  //   _this.cameraAuth = true
+                  // })
+                  _this.cameraAuth = false
+                }
+              })
+            }
+          }
+        })
+      },
+      async detectFace(frame) {
+        this.VKSession.detectFace({
+          frameBuffer: frame.data,
+          width: frame.width,
+          height: frame.height,
+          scoreThreshold: 0.8,
+          sourceType: 0,
+          modelMode: 1
+        })
+      },
+      initFaceData() {
+        if (this.VKSession && this.listener) {
+          return
+        }
+        this.videoCtx = wx.createCameraContext()
+        console.log('videoCtx:', this.videoCtx)
+        let count = 0
+        this.listener = this.videoCtx.onCameraFrame((frame) => {
+          count++
+          if (count === 10) {
+            this.detectFace(frame)
+            count = 0
+          }
+        })
+        this.VKSession = wx.createVKSession({
+          version: 'v1',
+          track: {
+            plane: {
+              mode: 1
+            },
+            face: { mode: 2 }
+          }
+        })
+        this.VKSession.on('updateAnchors', (anchors) => {
+          console.log('anchors', anchors)
+          if (anchors.length && !this.tempImg) {
+            this.videoCtx.takePhoto({
+              quality: 'high',
+              success: (res) => {
+                this.tempImg = res.tempImagePath
+                imgToBase64(this.tempImg).then((base64) => {
+                  console.log('人脸拍照图片 base64', base64)
+                })
+              }
+            })
+          }
+        })
+        this.VKSession.start((error) => {
+          if (error) {
+            this.$u.toast('VKSession start error')
+            // 如果失败,将返回 errno
+          }
+        })
+        this.listener.start()
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .home {
+    width: 100%;
+    height: 100%;
+    text-align: center;
+    .camera {
+      width: 400upx;
+      height: 400upx;
+      border-radius: 50%;
+      margin: 40rpx auto;
+    }
+  }
+</style>

+ 8 - 205
src/pages/home/home.vue

@@ -1,215 +1,18 @@
 <template>
-  <view class="home">
-    <!-- <camera class="camera" device-position="front" flash="off"></camera> -->
-    <image v-if="tempImg" mode="widthFix" :src="tempImg" />
-  </view>
+  <view> 考勤 </view>
 </template>
 
 <script>
-  import { imgToBase64 } from '@/utils/utils'
   export default {
-    name: 'HomePage',
+    name: 'Home',
+
     data() {
-      return {
-        cameraAuth: false,
-        locationAuth: false,
-        tempImg: '',
-        VKSession: null,
-        videoCtx: null,
-        listener: null
-      }
-    },
-    onShow() {
-      this.getLocationAuth()
-    },
-    onUnload() {
-      this.VKSession?.destroy()
-      this.listener?.stop({
-        complete: (res) => {
-          console.log('listener.stop', res)
-        }
-      })
-      this.VKSession = null
-      this.listener = null
+      return {}
     },
-    methods: {
-      getMyLocation() {
-        console.log('getMyLocation:')
-        uni.getLocation({
-          success(res) {
-            console.log('位置信息', res)
-          },
-          fail(err) {
-            console.log('fail:', err)
-          }
-        })
-      },
-      async openSetting() {
-        const _this = this
-        let promise = new Promise((resolve, reject) => {
-          uni.showModal({
-            title: '授权',
-            content: '请先授权获取摄像头权限',
-            success(res) {
-              if (res.confirm) {
-                uni.openSetting({
-                  success(res) {
-                    if (res.authSetting['scope.camera']) {
-                      // 用户打开了授权开关
-                      resolve(true)
-                    } else {
-                      // 用户没有打开授权开关, 继续打开设置页面
-                      _this.openSetting().then((res) => {
-                        resolve(true)
-                      })
-                    }
-                  },
-                  fail(res) {
-                    console.log(res)
-                  }
-                })
-              } else if (res.cancel) {
-                // setTimeout(() => {
-                //   _this.openSetting().then((res) => {
-                //     resolve(true)
-                //   })
-                // }, 3000)
-              }
-            }
-          })
-        })
-        return promise
-      },
-      async getLocationAuth() {
-        const _this = this
-        uni.getSetting({
-          success(res) {
-            console.log('authSetting:', res.authSetting)
-
-            if (res.authSetting['scope.userLocation']) {
-              // 用户已经授权
-              _this.locationAuth = true
-              _this.getMyLocation()
-            } else {
-              uni.authorize({
-                scope: 'scope.userLocation',
-                success() {
-                  // 用户同意授权
-                  _this.locationAuth = true
-                  _this.getMyLocation()
-                },
-                fail() {
-                  // 用户不同意授权
-                  // _this.openSetting().then((res) => {
-                  //   console.log('终于授权了')
-                  //   _this.cameraAuth = true
-                  // })
-                  _this.locationAuth = false
-                }
-              })
-            }
-          }
-        })
-      },
-      //获取摄像头权限,按需调用,如果该用户被配置为需要人脸打卡,再引导用户开启
-      getCameraAuth() {
-        const _this = this
-        uni.getSetting({
-          success(res) {
-            if (res.authSetting['scope.camera']) {
-              // 用户已经授权
-              _this.cameraAuth = true
-              _this.initFaceData()
-            } else {
-              uni.authorize({
-                scope: 'scope.camera',
-                success() {
-                  // 用户同意授权
-                  _this.cameraAuth = true
-                  _this.initFaceData()
-                },
-                fail() {
-                  // 用户不同意授权
-                  // _this.openSetting().then((res) => {
-                  //   console.log('终于授权了')
-                  //   _this.cameraAuth = true
-                  // })
-                  _this.cameraAuth = false
-                }
-              })
-            }
-          }
-        })
-      },
-      async detectFace(frame) {
-        this.VKSession.detectFace({
-          frameBuffer: frame.data,
-          width: frame.width,
-          height: frame.height,
-          scoreThreshold: 0.8,
-          sourceType: 0,
-          modelMode: 1
-        })
-      },
-      initFaceData() {
-        if (this.VKSession && this.listener) {
-          return
-        }
-        this.videoCtx = wx.createCameraContext()
-        console.log('videoCtx:', this.videoCtx)
-        let count = 0
-        this.listener = this.videoCtx.onCameraFrame((frame) => {
-          count++
-          if (count === 10) {
-            this.detectFace(frame)
-            count = 0
-          }
-        })
-        this.VKSession = wx.createVKSession({
-          version: 'v1',
-          track: {
-            plane: {
-              mode: 1
-            },
-            face: { mode: 2 }
-          }
-        })
-        this.VKSession.on('updateAnchors', (anchors) => {
-          console.log('anchors', anchors)
-          if (anchors.length && !this.tempImg) {
-            this.videoCtx.takePhoto({
-              quality: 'high',
-              success: (res) => {
-                this.tempImg = res.tempImagePath
-                imgToBase64(this.tempImg).then((base64) => {
-                  console.log('人脸拍照图片 base64', base64)
-                })
-              }
-            })
-          }
-        })
-        this.VKSession.start((error) => {
-          if (error) {
-            this.$u.toast('VKSession start error')
-            // 如果失败,将返回 errno
-          }
-        })
-        this.listener.start()
-      }
-    }
+    created() {},
+    onShow() {},
+    methods: {}
   }
 </script>
 
-<style lang="scss" scoped>
-  .home {
-    width: 100%;
-    height: 100%;
-    text-align: center;
-    .camera {
-      width: 400upx;
-      height: 400upx;
-      border-radius: 50%;
-      margin: 40rpx auto;
-    }
-  }
-</style>
+<style></style>

+ 31 - 18
src/pages/index/index.vue

@@ -1,34 +1,47 @@
 <template>
-  <view>
-    <u-button @click="doRouter('/pages/login/login')"> 去登录页</u-button>
+  <view class="work">
+    <u-tabs :list="tabList" :is-scroll="false" :current="current" @change="tabChange"></u-tabs>
+    <Tab1 v-if="current == 0"></Tab1>
+    <Tab2 v-else-if="current == 1"></Tab2>
+    <Tab3 v-else-if="current == 2"></Tab3>
   </view>
 </template>
 
 <script>
+  import Tab1 from './tab1-content.vue'
+  import Tab2 from './tab2-content.vue'
+  import Tab3 from './tab3-content.vue'
   export default {
     name: 'IndexPage',
-
+    components: { Tab1, Tab2, Tab3 },
     data() {
-      return {}
+      return {
+        tabList: [{ name: '消息提醒' }, { name: 'SOP待办' }, { name: '通知公告' }],
+        current: 0
+      }
     },
     created() {},
-    onShow() {},
     methods: {
-      /**
-       * 做路由跳转
-       * @param url
-       * @param params
-       */
-      doRouter(url) {
-        // 路由跳转
-        this.$Router.push({ path: url, query: { name: 'sop' } })
-        // 在onLoad中使用this.$Route.query获取参数
-      },
-      jumpToSwitchTab(url) {
-        this.$Router.pushTab({ path: url, query: { name: 'sop' } })
+      upper() {},
+      lower() {},
+      scroll() {},
+      tabChange(index) {
+        this.current = index
       }
+      // doRouter(url) {
+      //   // 路由跳转
+      //   this.$Router.push({ path: url, query: { name: 'sop' } })
+      //   // 在onLoad中使用this.$Route.query获取参数
+      // },
+      // jumpToSwitchTab(url) {
+      //   this.$Router.pushTab({ path: url, query: { name: 'sop' } })
+      // }
     }
   }
 </script>
 
-<style></style>
+<style lang="scss" scoped>
+  .work {
+    height: 100vh;
+  }
+</style>

+ 281 - 0
src/pages/index/tab1-content.vue

@@ -0,0 +1,281 @@
+<template>
+  <view class="tab1 flex flex-col">
+    <view class="tag-box">
+      <view class="tag-item" :class="{ active: params.status === 'undefined' }" @click="toggleStatus('undefined')">全部</view>
+      <view class="tag-item" :class="{ active: params.status === false }" @click="toggleStatus(false)"
+        >未读
+        <view class="red"></view>
+      </view>
+      <view class="tag-item" :class="{ active: params.status === true }" @click="toggleStatus(true)">已读</view>
+    </view>
+    <scroll-view
+      :scroll-top="scrollTop"
+      scroll-y="true"
+      refresher-enabled="true"
+      class="scroll-Y"
+      :refresher-triggered="triggered"
+      refresher-background="transparent"
+      @refresherrefresh="onRefresh"
+      @scrolltolower="scrolltolower"
+    >
+      <view class="msg-item" v-for="(item, index) in list" :key="index">
+        <view class="m-head flex items-center justify-between">
+          <view class="m-title">
+            <text class="title">{{ MESSAGE_TYPE[item.messageType] }}</text>
+            <!-- <view class="tag">未读</view> -->
+            <u-tag text="未读" v-if="!item.readStatus" type="error" size="mini" />
+          </view>
+          <view class="m-time">{{ dateFormat(item.sendTime, 'yyyy-MM-dd hh:mm') }}</view>
+        </view>
+        <view class="m-body rd-12rpx p-24rpx flex flex-wrap">
+          <view class="key-value"> 发起人:{{ item.formUser }} </view>
+          <view class="key-value"> 服务单元:{{ item.service }} </view>
+          <view class="key-value"> 客户类型:{{ item.customType }} </view>
+          <view class="key-value"> 客户名称:{{ item.custom }} </view>
+        </view>
+        <view class="m-foot text-right">
+          <u-button type="primary" size="mini" plain>查看</u-button>
+        </view>
+      </view>
+      <view class="bottom">
+        <u-loading v-if="loadingFlag == 1" mode="flower" size="44"></u-loading>
+        <view v-if="loadingFlag == 2" class="text flex items-center"> <view class="line"></view>我是有底线的<view class="line"></view></view>
+      </view>
+    </scroll-view>
+    <view class="bottom-space">
+      <u-button type="primary" @click="open">筛选条件</u-button>
+    </view>
+    <u-toast ref="uToast" />
+    <u-popup v-model="showPopup" mode="bottom" :mask-close-able="false">
+      <!-- <MyPopup :show="showPopup"> -->
+      <view class="search-box">
+        <view class="form-item">
+          <view class="label">消息类型</view>
+          <u-input :value="typesLabel" type="select" :border="true" placeholder="请选择" @click="showParamsType = !showParamsType" />
+          <MessageType :show.sync="showParamsType" :value.sync="params.types"></MessageType>
+        </view>
+        <view class="form-item">
+          <view class="label">服务单元</view>
+          <ServiceUnit :show.sync="showParamsServiceId" :value.sync="params.serviceId"></ServiceUnit>
+        </view>
+        <view class="form-item">
+          <view class="label">客户名称</view>
+          <u-input v-model="params.custom" type="input" :border="true" placeholder="请输入" clearable />
+        </view>
+        <view class="flex justify-between items-center">
+          <u-button class="flex-1" @click="showPopup = false" :throttle-time="1">取消</u-button>
+          <u-button class="flex-1 m-l-30rpx" type="primary" @click="searchByParams" :throttle-time="1">搜索</u-button>
+        </view>
+      </view>
+      <!-- </MyPopup> -->
+    </u-popup>
+  </view>
+</template>
+
+<script>
+  import { getMyMessages } from '@/api/workbenches'
+  import { MESSAGE_TYPE } from '@/utils/constants'
+  import { dictToOptionList } from '@/utils/utils'
+  import { dateFormat } from '@/utils/utils'
+  import ServiceUnit from '@/components/service-unit.vue'
+  import MessageType from '@/components/message-type.vue'
+  export default {
+    name: 'TabContent1',
+    components: { ServiceUnit, MessageType },
+    computed: {
+      transParams() {
+        let types = this.params.types.join(',')
+        if (!types.length && this.params.status === 'undefined') {
+          types = Object.keys(MESSAGE_TYPE).join()
+        }
+        let status = this.params.status === 'undefined' ? '' : this.params.status
+        return { ...this.params, types, status }
+      },
+      typesLabel() {
+        return this.params.types.map((item) => MESSAGE_TYPE[item]).join('、')
+      }
+    },
+    data() {
+      return {
+        MESSAGE_TYPE,
+        dateFormat,
+        dictToOptionList,
+        params: { types: [], serviceId: '', custom: '', status: 'undefined' },
+        pageNumber: 1,
+        pageSize: 10,
+        loadingFlag: 0,
+        list: [],
+        triggered: false,
+        showPopup: false,
+        showParamsType: false,
+        showParamsServiceId: false,
+        serviceList: []
+      }
+    },
+    mounted() {
+      this.search()
+    },
+    methods: {
+      open() {
+        console.log('open')
+        this.showPopup = true
+      },
+      searchByParams() {
+        this.showPopup = false
+        this.search()
+      },
+      toggleStatus(status) {
+        this.params.status = status
+        this.search()
+      },
+      scrolltolower() {
+        if (this.loadingFlag > 0) {
+          return
+        }
+
+        this.loadingFlag = 1
+        this.pageNumber++
+        this.getList()
+      },
+      search(bool) {
+        this.pageNumber = 1
+        this.list = []
+        this.getList(bool)
+      },
+      getList(bool) {
+        getMyMessages({ ...this.transParams, pageNumber: this.pageNumber, pageSize: this.pageSize }).then((res) => {
+          this.loadingFlag = res.pages == this.pageNumber ? 2 : 0
+          console.log('this.loadingFlag:', this.loadingFlag)
+          this.list.push(...(res.records || []))
+          if (bool) {
+            this.triggered = false
+            this.$refs.uToast.show({
+              title: '刷新成功',
+              type: 'success'
+            })
+          }
+        })
+      },
+      onRefresh() {
+        if (this.triggered) return
+        this.triggered = true
+        this.search(true)
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .tab1 {
+    height: calc(100% - 80rpx);
+    padding: 24rpx;
+    .search-box {
+      padding: 30rpx 20rpx;
+    }
+    .tag-box {
+      height: 84rpx;
+      display: flex;
+      .tag-item {
+        height: 60rpx;
+        width: 96rpx;
+        font-size: 24rpx;
+        text-align: center;
+        line-height: 60rpx;
+        border-radius: 30rpx;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        margin-right: 24rpx;
+        background: #fff;
+        position: relative;
+        color: #bfbfbf;
+        &.active {
+          background: #165dff;
+          color: #fff;
+        }
+        .red {
+          position: absolute;
+          width: 12rpx;
+          height: 12rpx;
+          right: 10rpx;
+          top: 10rpx;
+          background: red;
+          border-radius: 6px;
+        }
+      }
+    }
+    .scroll-Y {
+      height: calc(100% - 184rpx);
+      .msg-item {
+        padding: 24rpx;
+        border-radius: 12rpx;
+        background: #fff;
+        margin-bottom: 24rpx;
+        .m-foot {
+          padding-top: 14rpx;
+        }
+        .m-body {
+          background: #f7f7f7;
+          margin-top: 16rpx;
+          .key-value {
+            color: #8c8c8c;
+            font-size: 24rpx;
+            width: 50%;
+            &:nth-child(1),
+            &:nth-child(2) {
+              margin-bottom: 12rpx;
+            }
+          }
+        }
+        .m-head {
+          .m-time {
+            color: #8c8c8c;
+            font-size: 24rpx;
+          }
+          .m-title {
+            display: flex;
+            align-items: center;
+            .title {
+              color: #262626;
+              font-size: 32rpx;
+              margin-right: 10rpx;
+            }
+            // .tag {
+            //   background: #ffece8;
+            //   border-radius: 6rpx;
+            //   text-align: center;
+            //   line-height: 18px;
+            //   height: 18px;
+            //   color: #f53f3f;
+            //   font-size: 20rpx;
+            //   padding: 0 8rpx;
+            //   margin-left: 10rpx;
+            // }
+          }
+        }
+      }
+      .bottom {
+        padding: 20rpx 0 30rpx 0;
+        text-align: center;
+        .text {
+          color: #aaa;
+          font-size: 24rpx;
+          .line {
+            border-bottom: 1px dashed #ccc;
+            flex: 1;
+            &:first-child {
+              margin-right: 20rpx;
+            }
+            &:last-child {
+              margin-left: 20rpx;
+            }
+          }
+        }
+      }
+    }
+    .bottom-space {
+      height: 100rpx;
+      padding-top: 20rpx;
+    }
+  }
+</style>

+ 280 - 0
src/pages/index/tab2-content.vue

@@ -0,0 +1,280 @@
+<template>
+  <view class="tab2 flex flex-col">
+    <view class="tag-box">
+      <view class="tag-item" :class="{ active: params.flowTaskTypeEnum === 'ALL' }" @click="toggleStatus('ALL')">全部</view>
+      <view class="tag-item" :class="{ active: params.flowTaskTypeEnum === 'OVER_TIME' }" @click="toggleStatus('OVER_TIME')">超时 </view>
+      <view class="tag-item" :class="{ active: params.flowTaskTypeEnum === 'DRAFT' }" @click="toggleStatus('DRAFT')">暂存</view>
+    </view>
+    <scroll-view
+      :scroll-top="scrollTop"
+      scroll-y="true"
+      refresher-enabled="true"
+      class="scroll-Y"
+      :refresher-triggered="triggered"
+      refresher-background="transparent"
+      @refresherrefresh="onRefresh"
+      @scrolltolower="scrolltolower"
+    >
+      <view class="msg-item" v-for="(item, index) in list" :key="index">
+        <view class="m-head flex items-center justify-between">
+          <view class="m-title">
+            <text class="title">{{ item.taskName }}</text>
+            <u-tag text="正常" v-if="item.diffTime == 0" type="success" size="mini" />
+            <template v-else>
+              <u-tag text="已超时" type="error" size="mini" />
+              <u-tag :text="item.diffTime <= 30 ? '30天' : '大于30天'" type="error" size="mini" class="m-l-10rpx" />
+            </template>
+          </view>
+          <view class="m-time">{{ dateFormat(item.flowTime, 'yyyy-MM-dd hh:mm') }}</view>
+        </view>
+        <view class="m-body rd-12rpx p-24rpx flex flex-wrap">
+          <view class="key-value"> 发起人:{{ item.createRealName }} </view>
+          <view class="key-value"> 服务单元:{{ item.serviceName }} </view>
+          <view class="key-value"> 客户类型:{{ CUSTOMER_TYPE[item.customType] }} </view>
+          <view class="key-value"> 客户名称:{{ item.customName }} </view>
+        </view>
+        <view class="m-foot text-right">
+          <u-button type="primary" size="mini" plain>查看</u-button>
+        </view>
+      </view>
+      <view class="bottom">
+        <u-loading v-if="loadingFlag == 1" mode="flower" size="44"></u-loading>
+        <view v-if="loadingFlag == 2" class="text flex items-center"> <view class="line"></view>我是有底线的<view class="line"></view></view>
+      </view>
+    </scroll-view>
+    <view class="bottom-space">
+      <u-button type="primary" @click="showPopup = true">筛选条件</u-button>
+    </view>
+    <u-toast ref="uToast" />
+    <u-popup v-model="showPopup" mode="bottom" :mask-close-able="false">
+      <view class="search-box">
+        <view class="form-item">
+          <view class="label">待办类型</view>
+          <u-input :value="typesLabel" type="select" :border="true" placeholder="请选择" @click="showParamsType = !showParamsType" />
+          <u-select
+            v-model="showParamsType"
+            :list="dictToOptionList(WAIT_HANDLE_TYPE)"
+            :mask-close-able="false"
+            @cancel="showParamsType = false"
+            @confirm="typeConfirm"
+          ></u-select>
+        </view>
+        <view class="form-item">
+          <view class="label">服务单元</view>
+          <ServiceUnit :show.sync="showParamsServiceId" :value.sync="params.serviceId"></ServiceUnit>
+
+          <!-- <u-input :value="serviceLabel" type="select" :border="true" placeholder="请选择" @click="showParamsServiceId = !showParamsServiceId" /> -->
+        </view>
+        <view class="form-item">
+          <view class="label">客户名称</view>
+          <u-input v-model="params.custom" type="input" :border="true" placeholder="请输入" clearable />
+        </view>
+        <view class="flex justify-between items-center">
+          <u-button class="flex-1" @click="showPopup = false">取消</u-button>
+          <u-button class="flex-1 m-l-30rpx" type="primary" @click="searchByParams">搜索</u-button>
+        </view>
+      </view>
+    </u-popup>
+
+    <!-- 其实u-select可以放在u-cell-item里面进行封装,
+    但是该页面的cell是放在popup里的,select的遮罩层和popup的遮罩层会发生共用,导致一些问题,所以放在外面了 -->
+  </view>
+</template>
+
+<script>
+  import { getMyWaits } from '@/api/workbenches'
+  import { WAIT_HANDLE_TYPE } from '@/utils/constants'
+  import { dictToOptionList } from '@/utils/utils'
+  import { dateFormat } from '@/utils/utils'
+  import ServiceUnit from '@/components/service-unit.vue'
+  import { CUSTOMER_TYPE } from '@/utils/constants'
+  export default {
+    name: 'TabContent2',
+    components: { ServiceUnit },
+    computed: {
+      typesLabel() {
+        return this.WAIT_HANDLE_TYPE[this.params.type]
+      }
+    },
+    data() {
+      return {
+        CUSTOMER_TYPE,
+        WAIT_HANDLE_TYPE,
+        dateFormat,
+        dictToOptionList,
+        params: { type: '', serviceId: '', customName: '', flowTaskTypeEnum: 'ALL' },
+        pageNumber: 1,
+        pageSize: 10,
+        loadingFlag: 0,
+        list: [],
+        triggered: false,
+        showPopup: false,
+        showParamsType: false,
+        showParamsServiceId: false
+      }
+    },
+    mounted() {
+      this.search()
+    },
+    methods: {
+      typeConfirm(arr) {
+        this.params.type = arr[0].value
+      },
+      searchByParams() {
+        this.showPopup = false
+        this.search()
+      },
+      toggleStatus(status) {
+        this.params.flowTaskTypeEnum = status
+        this.search()
+      },
+      scrolltolower() {
+        if (this.loadingFlag > 0) {
+          return
+        }
+        this.loadingFlag = 1
+        this.pageNumber++
+        this.getList()
+      },
+      search(bool) {
+        this.pageNumber = 1
+        this.list = []
+        this.getList(bool)
+      },
+      getList(bool) {
+        getMyWaits({ ...this.params, pageNumber: this.pageNumber, pageSize: this.pageSize }).then((res) => {
+          this.loadingFlag = res.pages == this.pageNumber ? 2 : 0
+          console.log('this.loadingFlag:', this.loadingFlag)
+          this.list.push(...(res.records || []))
+          if (bool) {
+            this.triggered = false
+            this.$refs.uToast.show({
+              title: '刷新成功',
+              type: 'success'
+            })
+          }
+        })
+      },
+      onRefresh() {
+        if (this.triggered) return
+        this.triggered = true
+        this.search(true)
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .tab2 {
+    height: calc(100% - 80rpx);
+    padding: 24rpx;
+    .search-box {
+      padding: 30rpx 20rpx;
+    }
+    .tag-box {
+      height: 84rpx;
+      display: flex;
+      .tag-item {
+        height: 60rpx;
+        width: 96rpx;
+        font-size: 24rpx;
+        text-align: center;
+        line-height: 60rpx;
+        border-radius: 30rpx;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        margin-right: 24rpx;
+        background: #fff;
+        position: relative;
+        color: #bfbfbf;
+        &.active {
+          background: #165dff;
+          color: #fff;
+        }
+        .red {
+          position: absolute;
+          width: 12rpx;
+          height: 12rpx;
+          right: 10rpx;
+          top: 10rpx;
+          background: red;
+          border-radius: 6px;
+        }
+      }
+    }
+    .scroll-Y {
+      height: calc(100% - 184rpx);
+      .msg-item {
+        padding: 24rpx;
+        border-radius: 12rpx;
+        background: #fff;
+        margin-bottom: 24rpx;
+        .m-foot {
+          padding-top: 14rpx;
+        }
+        .m-body {
+          background: #f7f7f7;
+          margin-top: 16rpx;
+          .key-value {
+            color: #8c8c8c;
+            font-size: 24rpx;
+            width: 50%;
+            &:nth-child(1),
+            &:nth-child(2) {
+              margin-bottom: 12rpx;
+            }
+          }
+        }
+        .m-head {
+          .m-time {
+            color: #8c8c8c;
+            font-size: 24rpx;
+          }
+          .m-title {
+            display: flex;
+            align-items: center;
+            .title {
+              color: #262626;
+              font-size: 32rpx;
+              margin-right: 10rpx;
+            }
+            .tag {
+              background: #ffece8;
+              border-radius: 6rpx;
+              text-align: center;
+              line-height: 18px;
+              height: 18px;
+              color: #f53f3f;
+              font-size: 20rpx;
+              padding: 0 8rpx;
+              margin-left: 10rpx;
+            }
+          }
+        }
+      }
+      .bottom {
+        padding: 20rpx 0 30rpx 0;
+        text-align: center;
+        .text {
+          color: #aaa;
+          font-size: 24rpx;
+          .line {
+            border-bottom: 1px dashed #ccc;
+            flex: 1;
+            &:first-child {
+              margin-right: 20rpx;
+            }
+            &:last-child {
+              margin-left: 20rpx;
+            }
+          }
+        }
+      }
+    }
+    .bottom-space {
+      height: 100rpx;
+      padding-top: 20rpx;
+    }
+  }
+</style>

+ 242 - 0
src/pages/index/tab3-content.vue

@@ -0,0 +1,242 @@
+<template>
+  <view class="tab3 flex flex-col">
+    <view class="tag-box">
+      <view class="tag-item" :class="{ active: params.status === 'undefined' }" @click="toggleStatus('undefined')">全部</view>
+      <view class="tag-item" :class="{ active: params.status === false }" @click="toggleStatus(false)"
+        >未读
+        <view class="red"></view>
+      </view>
+      <view class="tag-item" :class="{ active: params.status === true }" @click="toggleStatus(true)">已读</view>
+    </view>
+    <scroll-view
+      :scroll-top="scrollTop"
+      scroll-y="true"
+      refresher-enabled="true"
+      class="scroll-Y"
+      :refresher-triggered="triggered"
+      refresher-background="transparent"
+      @refresherrefresh="onRefresh"
+      @scrolltolower="scrolltolower"
+    >
+      <view class="msg-item flex items-center" v-for="(item, index) in list" :key="index">
+        <view class="msg-left">
+          <view class="m-title">
+            <text class="title truncate">{{ item.title }}</text>
+            <u-tag text="未读" v-if="!item.readStatus" type="error" size="mini" />
+          </view>
+          <view class="m-time">{{ dateFormat(item.sendTime, 'yyyy-MM-dd hh:mm') }}</view>
+        </view>
+        <view class="msg-right">
+          <u-button type="primary" size="mini" plain>查看</u-button>
+        </view>
+      </view>
+      <view class="bottom">
+        <u-loading v-if="loadingFlag == 1" mode="flower" size="44"></u-loading>
+        <view v-if="loadingFlag == 2" class="text flex items-center"> <view class="line"></view>我是有底线的<view class="line"></view></view>
+      </view>
+    </scroll-view>
+    <view class="bottom-space">
+      <u-button type="primary" @click="showPopup = true">筛选条件</u-button>
+    </view>
+    <u-toast ref="uToast" />
+    <u-popup v-model="showPopup" mode="bottom" :mask-close-able="false">
+      <view class="search-box">
+        <view class="form-item">
+          <view class="label">发送日期</view>
+          <DateRange :value.sync="params.time"></DateRange>
+        </view>
+        <view class="form-item">
+          <view class="label">标题关键字</view>
+          <u-input v-model="params.title" type="input" :border="true" placeholder="请输入" clearable />
+        </view>
+        <view class="flex justify-between items-center">
+          <u-button class="flex-1" @click="showPopup = false">取消</u-button>
+          <u-button class="flex-1 m-l-30rpx" type="primary" @click="searchByParams">搜索</u-button>
+        </view>
+      </view>
+    </u-popup>
+  </view>
+</template>
+
+<script>
+  import { getMyMessages } from '@/api/workbenches'
+  import { dictToOptionList } from '@/utils/utils'
+  import { dateFormat } from '@/utils/utils'
+  import DateRange from '@/components/date-range.vue'
+  import { omit } from 'lodash-es'
+  export default {
+    name: 'TabContent3',
+    components: { DateRange },
+    computed: {
+      transParams() {
+        let types = this.params.types.join(',')
+
+        let status = this.params.status === 'undefined' ? '' : this.params.status
+
+        return {
+          ...omit(this.params, 'time'),
+          types,
+          status,
+          startTime: this.params.time[0],
+          endType: this.params.time[1]
+        }
+      }
+    },
+    data() {
+      return {
+        dateFormat,
+        dictToOptionList,
+        params: { types: ['SYSTEM'], title: '', time: [], status: 'undefined' },
+        pageNumber: 1,
+        pageSize: 10,
+        loadingFlag: 0,
+        list: [],
+        triggered: false,
+        showPopup: false,
+        showParamsType: false
+      }
+    },
+    mounted() {
+      this.search()
+    },
+    methods: {
+      searchByParams() {
+        this.showPopup = false
+        this.search()
+      },
+      toggleStatus(status) {
+        this.params.status = status
+        this.search()
+      },
+      scrolltolower() {
+        if (this.loadingFlag > 0) {
+          return
+        }
+
+        this.loadingFlag = 1
+        this.pageNumber++
+        this.getList()
+      },
+      search(bool) {
+        this.pageNumber = 1
+        this.list = []
+        this.getList(bool)
+      },
+      getList(bool) {
+        getMyMessages({ ...this.transParams, pageNumber: this.pageNumber, pageSize: this.pageSize }).then((res) => {
+          this.loadingFlag = res.pages == this.pageNumber ? 2 : 0
+          console.log('this.loadingFlag:', this.loadingFlag)
+          this.list.push(...(res.records || []))
+          if (bool) {
+            this.triggered = false
+            this.$refs.uToast.show({
+              title: '刷新成功',
+              type: 'success'
+            })
+          }
+        })
+      },
+      onRefresh() {
+        if (this.triggered) return
+        this.triggered = true
+        this.search(true)
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .tab3 {
+    height: calc(100% - 80rpx);
+    padding: 24rpx;
+    .search-box {
+      padding: 30rpx 20rpx;
+    }
+    .tag-box {
+      height: 84rpx;
+      display: flex;
+      .tag-item {
+        height: 60rpx;
+        width: 96rpx;
+        font-size: 24rpx;
+        text-align: center;
+        line-height: 60rpx;
+        border-radius: 30rpx;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        margin-right: 24rpx;
+        background: #fff;
+        position: relative;
+        color: #bfbfbf;
+        &.active {
+          background: #165dff;
+          color: #fff;
+        }
+        .red {
+          position: absolute;
+          width: 12rpx;
+          height: 12rpx;
+          right: 10rpx;
+          top: 10rpx;
+          background: red;
+          border-radius: 6px;
+        }
+      }
+    }
+    .scroll-Y {
+      height: calc(100% - 184rpx);
+      .msg-item {
+        padding: 24rpx;
+        border-radius: 12rpx;
+        background: #fff;
+        margin-bottom: 24rpx;
+        .msg-right {
+          width: 80rpx;
+          display: flex;
+          align-items: center;
+        }
+        .msg-left {
+          width: calc(100% - 80rpx);
+          .m-time {
+            margin-top: 20rpx;
+            color: #8c8c8c;
+            font-size: 24rpx;
+          }
+          .m-title {
+            display: flex;
+            align-items: center;
+            .title {
+              color: #262626;
+              font-size: 32rpx;
+              margin-right: 10rpx;
+              max-width: calc(100% - 110rpx);
+            }
+          }
+        }
+      }
+      .bottom {
+        padding: 20rpx 0 30rpx 0;
+        text-align: center;
+        .text {
+          color: #aaa;
+          font-size: 24rpx;
+          .line {
+            border-bottom: 1px dashed #ccc;
+            flex: 1;
+            &:first-child {
+              margin-right: 20rpx;
+            }
+            &:last-child {
+              margin-left: 20rpx;
+            }
+          }
+        }
+      }
+    }
+    .bottom-space {
+      height: 100rpx;
+      padding-top: 20rpx;
+    }
+  }
+</style>

+ 18 - 0
src/pages/index/tab4-content.vue

@@ -0,0 +1,18 @@
+<template>
+  <view> tab4 </view>
+</template>
+
+<script>
+  export default {
+    name: 'TabContent5',
+
+    data() {
+      return {}
+    },
+    created() {},
+    onShow() {},
+    methods: {}
+  }
+</script>
+
+<style></style>

+ 18 - 0
src/pages/sop/sop.vue

@@ -0,0 +1,18 @@
+<template>
+  <view> SOP </view>
+</template>
+
+<script>
+  export default {
+    name: 'Sop',
+
+    data() {
+      return {}
+    },
+    created() {},
+    onShow() {},
+    methods: {}
+  }
+</script>
+
+<style></style>

+ 48 - 25
src/service/request.js

@@ -6,14 +6,37 @@
 
 import Request from 'luch-request'
 import Vue from '@/main'
-const getTokenStorage = () => {
-  let token = ''
-  try {
-    token = uni.getStorageSync('token')
-  } catch (e) {
-    console.log(e)
+import { getAuthorization, DEVICE_ID } from '@/utils/crypto'
+
+// const getTokenStorage = () => {
+//   let token = ''
+//   try {
+//     token = uni.getStorageSync('token')
+//   } catch (e) {
+//     console.log(e)
+//   }
+//   return token
+// }
+
+//先模拟PC端的登录
+function setAuth(config) {
+  let userSession = uni.getStorageSync('user')
+  if (userSession && !config.custom?.noAuth) {
+    let user = userSession
+    const timestamp = Date.now()
+    const authorization = getAuthorization(
+      {
+        method: config.method,
+        uri: config.url.split('?')[0],
+        timestamp,
+        sessionId: user.sessionId,
+        token: user.accessToken
+      },
+      'token'
+    )
+    config.header['Authorization'] = authorization
+    config.header['time'] = timestamp
   }
-  return token
 }
 
 const http = new Request()
@@ -22,20 +45,21 @@ http.setConfig((config) => {
   config.baseURL = process.env.VUE_APP_BASE_API
   config.header = {
     ...config.header,
-    a: 1, // 演示
-    b: 2 // 演示
+    platform: 'WEB',
+    deviceId: DEVICE_ID
   }
   return config
 })
 
 http.interceptors.request.use(
   (config) => {
+    setAuth(config)
     /* 请求之前拦截器。可以使用async await 做异步操作 */
-    config.header = {
-      ...config.header,
-      // token: getTokenStorage()
-      Authorization: `Bearer ${Vue.vuex_token}`
-    }
+    // config.header = {
+    //   ...config.header,
+    //   // token: getTokenStorage()
+    //   Authorization: `Bearer ${Vue.vuex_token}`
+    // }
     /*
        if (!token) { // 如果token不存在,return Promise.reject(config) 会取消本次请求
          return Promise.reject(config)
@@ -47,9 +71,9 @@ http.interceptors.request.use(
     // if (config.custom.auth) {
     //   config.header.token = '123456'
     // }
-    // if (config.custom.loading) {
-    //   uni.showLoading()
-    // }
+    if (config.custom.loading) {
+      uni.showLoading()
+    }
     return config
   },
   (config) => {
@@ -59,15 +83,14 @@ http.interceptors.request.use(
 
 http.interceptors.response.use(
   async (response) => {
-    // if (response.config.custom.loading) {
-    //    uni.hideLoading()
-    //  }
+    if (response.config.custom.loading) {
+      uni.hideLoading()
+    }
     // if (response.data.code !== 200) { // 服务端返回的状态码不等于200,则reject()
     //   return Promise.reject(response)
     // }
-    console.log('response', response)
     if (response.statusCode === 200) {
-      return response.data
+      return response.data?.data
     }
     if (response.statusCode === 401) {
       Vue.$u.vuex('vuex_token', '')
@@ -87,9 +110,9 @@ http.interceptors.response.use(
   },
   (err) => {
     console.log(err)
-    // if (response.config.custom.loading) {
-    //    uni.hideLoading()
-    //  }
+    if (response.config.custom.loading) {
+      uni.hideLoading()
+    }
     return Promise.reject(err)
   }
 )

BIN
src/static/icon/tab/icon_home.png


BIN
src/static/icon/tab/icon_home_c.png


BIN
src/static/icon/tab/icon_wode.png


BIN
src/static/icon/tab/icon_wode_c.png


BIN
src/static/icon/tab/tab1.png


BIN
src/static/icon/tab/tab1_active.png


BIN
src/static/icon/tab/tab2.png


BIN
src/static/icon/tab/tab2_active.png


BIN
src/static/icon/tab/tab3.png


BIN
src/static/icon/tab/tab3_active.png


+ 12 - 0
src/styles/global.scss

@@ -0,0 +1,12 @@
+.form-item{
+    padding-bottom:20rpx;
+}
+.label{
+    color:#595959;
+    font-size:28rpx;
+    line-height:44rpx;
+    padding-bottom:12rpx;
+}
+.u-drawer .u-drawer{
+    overflow:visible !important;
+}

+ 214 - 0
src/utils/constants.js

@@ -0,0 +1,214 @@
+export const ROLE_TYPE = {
+  ADMIN: '系统管理员',
+  PMO: '总负责人',
+  BUSSINESS: '业务线负责人',
+  REGION_MANAGER: '大区经理',
+  REGION_COORDINATOR: '区域协调人',
+  EFFECT_ENGINEER: '实施工程师',
+  ASSISTANT_ENGINEER: '助理工程师',
+  ACCOUNT_MANAGER: '客户经理',
+  QA: 'QA',
+  CUSTOM: '技术客服',
+  DEFINED: '自定义'
+}
+export const GENDER_TYPE = {
+  MAN: '男',
+  WOMAN: '女'
+}
+export const EDUCATION_TYPE = {
+  BACHELOR_DEGREE: '本科及以上',
+  JUNIOR_COLLEGE: '大专',
+  HIGH_SCHOOL: '高中',
+  MIDDLE_SCHOOL: '初中'
+}
+// 启用/禁用
+export const ABLE_TYPE = {
+  false: '禁用',
+  true: '启用'
+}
+// 审核结果
+export const AUDITING_RESULT = {
+  NOT_PASS: '未通过',
+  PASS: '通过'
+}
+// 系统管理 ------->
+// 客户类型
+export const CUSTOMER_TYPE = {
+  OFFICE: '高校教务处',
+  CLOUD_MARK: '研究生'
+}
+// 供应商类型
+export const SUPPLIER_TYPE = {
+  HUMAN: '人力供应商',
+  DEVICE: '设备供应商'
+}
+// 设备状态
+export const RUNNING_STATUS = {
+  NORMAL: '正常',
+  BREAK_DOWN: '故障'
+}
+// 公告类型
+export const NOTICE_TYPE = {
+  SYSTEM: '公告',
+  SERVICE: '服务单元通知',
+  SUPPLIER: '供应商通知'
+}
+export const PUBLISH_STATUS = {
+  PUBLISH: '已发布',
+  UN_PUBLISH: '未发布'
+}
+// 数据管理
+export const DATA_TASK_STATUS = {
+  INIT: '未开始',
+  RUNNING: '进行中',
+  FINISH: '已完成'
+}
+export const DATA_TASK_RESULT = {
+  SUCCESS: '成功',
+  ERROR: '失败'
+}
+export const DATA_TASK_TYPE = {
+  USER_ARCHIVES_IMPORT: '人员档案导入',
+  USER_ARCHIVES_EXPORT: '档案导出'
+}
+// 服务单元管理 ------->
+// 服务单元管理
+export const SERVICE_UNIT_STATUS = {
+  NEW: '新建',
+  PUBLISH: '已发布',
+  FINISH: '已完结',
+  CANCEL: '已作废'
+}
+
+// 项目质量管理 ------->
+// 问题类型
+export const ISSUES_TYPE = {
+  UPDATE: '修正类',
+  OPTIMIZE: '优化类',
+  NO_PROBLEM: '不是问题'
+}
+// 问题原因类型
+export const ISSUES_REASON_TYPE = {
+  EXEC: '执行类',
+  MANAGER: '管理协调类',
+  FLOW: '流程制度类',
+  PRODUCT_PROBLEM: '产品缺陷类',
+  PRODUCT_MANAGER: '产品运维类',
+  OTHER: '其它'
+}
+export const ISSUES_INFLUENCE_DEGREE = ['A', 'B', 'C', 'D']
+
+// 工时管理
+// 考勤类型
+export const ATTENDANCE_TYPE = {
+  IN: '签到',
+  OUT: '签退'
+}
+export const ATTENDANCE_RESULT = {
+  NORMAL: '正常',
+  EXCEPTION: '异常'
+}
+// 考勤提交
+export const ATTENDANCE_SUBMIT_STATUS = {
+  WILL_SUBMIT: '待提交',
+  ALREADY_SUBMIT: '已提交',
+  APPLY_WITHDRAW: '已提交',
+  AGREE_WITHDRAW: '待提交',
+  AGREE_DING: '审核通过'
+}
+// 工时统计
+export const ATTENDANCE_STATISTICS_SUBMIT_STATUS = {
+  WILL_SUBMIT: '--',
+  ALREADY_SUBMIT: '已提交',
+  APPLY_WITHDRAW: '申请撤回',
+  AGREE_WITHDRAW: '已撤回',
+  AGREE_DING: '审核通过'
+}
+// 资源保障
+// 人员档案管理
+export const AUTHENTICATION_STATUS = {
+  true: '有效',
+  false: '无效'
+}
+export const AUTHENTICATION_ROLE = {
+  REGION_COORDINATOR: '区域协调人',
+  EFFECT_ENGINEER: '实施工程师',
+  ASSISTANT_ENGINEER: '助理工程师'
+}
+// 出入库
+export const INOUT_TYPE = {
+  IN: '入库',
+  OUT: '出库'
+}
+export const DEVICE_USAGE_TYPE = {
+  PROJECT: '项目',
+  OTHER: '其他'
+}
+
+// SOP
+export const FLOW_STATUS = {
+  START: '已开始',
+  AUDITING: '审核中',
+  REJECT: '已驳回',
+  END: '已终止',
+  FINISH: '已结束'
+}
+//工作台-消息类型
+export const MESSAGE_TYPE = {
+  BEFORE: '提前提醒',
+  AFTER: '延期提醒',
+  // OFFICE_SOP: '教务处SOP',
+  // CLOUD_MARK_SOP: '云阅卷SOP',
+  QUALITY: '质量问题提醒',
+  EXCEPTION_APPROVE: '异常审核提醒',
+  VIOLATION: '违规提醒',
+  SYSTEM_PLAN_CHANGE: '系统计划变更提醒'
+  // SYSTEM: '系统公告',
+}
+
+//工作台-待办类型
+export const WAIT_HANDLE_TYPE = {
+  OFFICE_SOP_FLOW: '教务处SOP',
+  CLOUD_MARK_SOP_FLOW: '云阅卷SOP',
+  DING_EXCEPTION_FLOW: '考勤异常审核流程',
+  PROJECT_EXCHANGE_FLOW: '项目计划变更流程',
+  QUALITY_PROBLEM_FLOW: '质量问题反馈流程'
+}
+// SOP管理-延期预警-预警类型
+export const WARN_TYPE = {
+  PLAN: '关键信息及计划',
+  TIME: '处理时限'
+  // CANCEL: '取消',
+}
+
+//SOP管理-延期预警-跟进状态
+export const WARN_FLOW_STATUS = {
+  NOT_START: '未跟进',
+  FOLLOW: '跟进',
+  CLOSE: '关闭',
+  RESTART: '重启'
+}
+
+//违规登记类型
+export const VIOLATION_TYPE = {
+  CONTENT_ERROR: '内容错误虚假',
+  LOGIC_ERROR: '逻辑不合理',
+  OTHER: '其他'
+}
+//SOP管理-违规登记-跟进状态
+export const VIOLATION_FLOW_STATUS = {
+  NOT_START: '未跟进',
+  FOLLOW: '跟进',
+  CLOSE: '关闭',
+  RESTART: '重启'
+}
+//项目计划变更-变更类型
+export const PLAN_CHANGE_TYPE = {
+  PLAN: '关键信息及计划变更',
+  CANCEL: '项目取消'
+}
+//流程状态
+export const FLOW_CHECK_STATUS = {
+  UN_APPROVE: '待审核',
+  APPROVE: '已审核'
+}

+ 75 - 0
src/utils/crypto.js

@@ -0,0 +1,75 @@
+// const CryptoJS = require('crypto-js');
+import Base64 from 'crypto-js/enc-base64'
+import Utf8 from 'crypto-js/enc-utf8'
+import AES from 'crypto-js/aes'
+import SHA1 from 'crypto-js/sha1'
+import MD5 from 'crypto-js/md5'
+import SparkMD5 from 'spark-md5'
+
+if (!uni.getStorageSync('deviceId')) {
+  uni.setStorageSync('deviceId', Math.floor(Math.random() * 1000) + '-' + Date.now())
+}
+export const DEVICE_ID = uni.getStorageSync('deviceId')
+
+export const getBase64 = (content) => {
+  const words = Utf8.parse(content)
+  const base64Str = Base64.stringify(words)
+
+  return base64Str
+}
+
+export const getAES = (content) => {
+  const KEY = '1234567890123456'
+  const IV = '1234567890123456'
+
+  var key = Utf8.parse(KEY)
+  var iv = Utf8.parse(IV)
+  var encrypted = AES.encrypt(content, key, { iv: iv })
+  return encrypted.toString()
+}
+
+/**
+ * 获取authorisation
+ * @param {Object} infos 相关信息
+ * @param {String} type 类别:secret、token两种
+ */
+export const getAuthorization = (infos, type) => {
+  if (type === 'secret') {
+    const str = `${infos.method.toLowerCase()}&${infos.uri}&${infos.timestamp}&${infos.accessSecret}`
+    const sign = Base64.stringify(SHA1(str))
+    return `Secret ${infos.accessKey}:${sign}`
+  } else if (type === 'token') {
+    const str = `${infos.method.toLowerCase()}&${infos.uri}&${infos.timestamp}&${infos.token}`
+    const sign = Base64.stringify(SHA1(str))
+    return `Token ${infos.sessionId}:${sign}`
+  }
+}
+
+/**
+ *
+ * @param {any} str 字符串
+ */
+export const getMD5 = (content) => {
+  return MD5(content)
+}
+
+export const getFileMD5 = (dataFile) => {
+  return new Promise((rs, rj) => {
+    var fileReader = new FileReader()
+    var spark = new SparkMD5() //创建md5对象(基于SparkMD5)
+    if (dataFile.size > 1024 * 1024 * 10) {
+      var data1 = dataFile.slice(0, 1024 * 1024 * 10) //将文件进行分块 file.slice(start,length)
+      fileReader.readAsBinaryString(data1) //将文件读取为二进制码
+    } else {
+      fileReader.readAsBinaryString(dataFile)
+    }
+    fileReader.onload = function (e) {
+      spark.appendBinary(e.target.result)
+      var md5 = spark.end()
+      rs(md5)
+    }
+    fileReader.onerror = function (err) {
+      rj(err)
+    }
+  })
+}

+ 28 - 0
src/utils/syncServerTime.js

@@ -0,0 +1,28 @@
+let initLocalTime = null
+let initServerTime = null
+
+function getStorgeTime() {
+  const st = uni.getStorage('st')
+  const unvalidVals = ['Infinity', 'NaN', 'null', 'undefined']
+  if (unvalidVals.includes(st + '')) {
+    return [Date.now(), Date.now()]
+  } else {
+    const [s, t] = st.split('_')
+    return [s * 1, t * 1]
+  }
+}
+
+const [serverTime, localTime] = getStorgeTime()
+initSyncTime(serverTime, localTime)
+
+function initSyncTime(serverTime, localTime = Date.now()) {
+  initLocalTime = localTime
+  initServerTime = serverTime
+  uni.setStorageSync('st', `${initServerTime}_${initLocalTime}`)
+}
+
+function fetchTime() {
+  return Date.now() + initServerTime - initLocalTime
+}
+
+export { initSyncTime, fetchTime }

+ 49 - 0
src/utils/utils.js

@@ -548,3 +548,52 @@ export function imgToBase64(filePath) {
     }
   })
 }
+export function sleep(delay) {
+  var start = new Date().getTime()
+  while (new Date().getTime() - start < delay) {
+    continue
+  }
+}
+/**
+ * 字典数据转成option list
+ * @param {Object} data 字典数据
+ * @returns list
+ */
+export const dictToOptionList = (data) => {
+  return Object.keys(data).map((k) => {
+    const kstr = typeof k === 'number' ? k : k + ''
+    return { value: kstr, label: data[k] }
+  })
+}
+/* 日期格式化 */
+export const dateFormat = (date, fmt = 'yyyy-MM-dd hh:mm:ss', isDefault = '-') => {
+  if (!date) {
+    return '-'
+  }
+  if (date.toString().length === 10) {
+    date *= 1000
+  }
+  date = new Date(date)
+
+  if (date.valueOf() < 1) {
+    return isDefault
+  }
+  const o = {
+    'M+': date.getMonth() + 1, // 月份
+    'd+': date.getDate(), // 日
+    'h+': date.getHours(), // 小时
+    'm+': date.getMinutes(), // 分
+    's+': date.getSeconds(), // 秒
+    'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
+    S: date.getMilliseconds() // 毫秒
+  }
+  if (/(y+)/.test(fmt)) {
+    fmt = fmt.replace(RegExp.$1, `${date.getFullYear()}`.substr(4 - RegExp.$1.length))
+  }
+  for (const k in o) {
+    if (new RegExp(`(${k})`).test(fmt)) {
+      fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : `00${o[k]}`.substr(`${o[k]}`.length))
+    }
+  }
+  return fmt
+}

+ 28 - 0
unocss.config.js

@@ -0,0 +1,28 @@
+import presetWeapp from 'unocss-preset-weapp'
+import { extractorAttributify, transformerClass } from 'unocss-preset-weapp/transformer'
+
+const { transformerAttributify, presetWeappAttributify } = extractorAttributify()
+
+export default {
+  presets: [
+    presetWeapp({
+      platform: 'uniapp',
+      isH5: process.env.UNI_PLATFORM === 'h5'
+    }),
+
+    presetWeappAttributify()
+  ],
+  shortcuts: [
+    {
+      'border-base': 'border border-gray-500_10',
+      center: 'flex justify-center items-center'
+    }
+  ],
+  transformers: [
+    // https://github.com/MellowCo/unocss-preset-weapp/tree/main/src/transformer/transformerAttributify
+    transformerAttributify(),
+
+    // https://github.com/MellowCo/unocss-preset-weapp/tree/main/src/transformer/transformerClass
+    transformerClass()
+  ]
+}

+ 3 - 1
vue.config.js

@@ -1,5 +1,6 @@
 const TransformPages = require('uni-read-pages')
 const { webpack } = new TransformPages()
+const UnoCSS = require('unocss-webpack-uniapp2').default
 module.exports = {
   // productionSourceMap: false,
   lintOnSave: process.env.NODE_ENV === 'development',
@@ -13,7 +14,8 @@ module.exports = {
           })
           return JSON.stringify(tfPages.routes)
         }, true)
-      })
+      }),
+      UnoCSS()
     ]
   }
 }