Browse Source

feat:update check-in-photo

lvkun996 10 months ago
parent
commit
ca3ee3e590

+ 2 - 22
README.md

@@ -1,24 +1,4 @@
 # train_sign_in
 
-## Project setup
-```
-yarn install
-```
-
-### Compiles and hot-reloads for development
-```
-yarn serve
-```
-
-### Compiles and minifies for production
-```
-yarn build
-```
-
-### Lints and fixes files
-```
-yarn lint
-```
-
-### Customize configuration
-See [Configuration Reference](https://cli.vuejs.org/config/).
+## 后端接口文档地址
+  https://open.test.luojigou.vip/user-training/doc.html#/

+ 8 - 0
src/api/common.js

@@ -23,3 +23,11 @@ export const getContentList = (params) => {
     params
   })
 }
+
+export const uploadFile = (params) => {
+  return request({
+    url: '/parent//file/uploadFile',
+    method: 'GET',
+    params
+  })
+}

+ 1 - 0
src/server/index.js

@@ -3,6 +3,7 @@ import { useUserStore } from '@/store'
 console.log('process.env:')
 const instance = axios.create({
   baseURL: process.env.NODE_ENV === 'development' ? 'https://open.test.luojigou.vip' : '/zd-api',
+  // baseURL: process.env.NODE_ENV !== 'development' ? 'https://open.test.luojigou.vip' : 'https://open.api.luojigou.vip',
   timeout: 30000,
   headers: {
     'Content-Security-Policy': 'default-src \'*\''

+ 3 - 5
src/store/useCommonStore.js

@@ -5,7 +5,7 @@ import { getSubjectList, getFormList, getContentList } from '@/api/common'
 export const useCommonStore = defineStore('useCommonStore', () => {
   const subjectList = ref([{ name: '全部', id: '', title: '培训科目' }])
   const formList = ref([{ name: '全部', id: '', title: '培训形式' }])
-  const contentList = ref([{ name: '全部', id: '' }])
+  const contentList = ref([])
 
   const _getSubjectList = async () => {
     if (subjectList.value.length === 1) {
@@ -20,10 +20,8 @@ export const useCommonStore = defineStore('useCommonStore', () => {
     }
   }
   const _getContentList = async () => {
-    if (contentList.value.length === 1) {
-      const { data } = await getContentList()
-      contentList.value.push(...data)
-    }
+    const { data } = await getContentList()
+    contentList.value.push(...data)
   }
 
   return {

+ 3 - 4
src/store/useTrainStore.js

@@ -20,10 +20,10 @@ export const useTrainStore = defineStore('useTrainStore', () => {
     current.value = data
   }
 
-  const start = async (id) => {
+  const start = async (id, photos, checkInCount) => {
     const { latitude, longitude } = await getLocation()
     const { status } = await startTrain({
-      id, latitude, longitude
+      id, latitude, longitude, photos, checkInCount
     })
     if (status === 200) {
       // eslint-disable-next-line no-undef
@@ -61,8 +61,7 @@ export const useTrainStore = defineStore('useTrainStore', () => {
     if (status === 200) {
       // eslint-disable-next-line no-undef
       showToast('删除成功')
-      console.log('id: ', id, list.value);
-    
+      console.log('id: ', id, list.value)
     }
   }
 

+ 35 - 18
src/views/create-train.vue

@@ -27,7 +27,7 @@
       </div>
       <div class="check-in-item" >
         <div class="check-in-item-title" >培训性质</div>
-        <div class="check-in-item-input" @click="openModal('nature')">
+        <div :class="['check-in-item-input', formRequireState.natureId ? 'check-in-item-input-warn' : '']" @click="openModal('nature')">
           <div class="placeholder" v-if="!formSate.natureId" >请选择</div>
           <div class="check-in-item-value" v-else >{{formSate.nature}}</div>
           <van-icon name="arrow-down" />
@@ -35,7 +35,7 @@
       </div>
       <div class="check-in-item" >
         <div class="check-in-item-title" >培训形式</div>
-        <div class="check-in-item-input" @click="openModal('method')">
+        <div :class="['check-in-item-input', formRequireState.methodId ? 'check-in-item-input-warn' : '']" @click="openModal('method')">
           <div class="placeholder" v-if="!formSate.methodId" >请选择</div>
           <div class="check-in-item-value" v-else >{{formSate.method}}</div>
           <van-icon name="arrow-down" />
@@ -43,11 +43,17 @@
       </div>
       <div class="check-in-item" >
         <div class="check-in-item-title" >培训内容</div>
-        <div :class="['check-in-item-input',formRequireState.contentId ? 'check-in-item-input-warn' : '' ]" @click="openModal('content')">
+        <van-checkbox-group class="check-in-item-content"  v-model="formSate.contents" direction="horizontal" shape="square">
+          <van-checkbox :name="item.name" style="margin-bottom: 12px"  v-for="item in contentList" :key="item.id" >{{item.name}}</van-checkbox>
+          <van-checkbox :name="formSate.cusContent" style="margin-bottom: 12px"  >
+            <van-field style="border: 1px solid #cdc8c8;height: 40px;" v-model="formSate.cusContent" label="" placeholder="自定义培训内容" />
+          </van-checkbox>
+        </van-checkbox-group>
+        <!-- <div :class="['check-in-item-input',formRequireState.contentId ? 'check-in-item-input-warn' : '' ]" @click="openModal('content')">
           <div class="placeholder" v-if="!formSate.content" >请选择</div>
           <div :class="['check-in-item-value' ]" v-else >{{formSate.content}}</div>
           <van-icon name="arrow-down" />
-        </div>
+        </div> -->
       </div>
       <div class="check-in-item" >
         <div class="check-in-item-title" >培训科目</div>
@@ -94,7 +100,7 @@
         <div class="info-modal-item" >  <div class="title"> 培训性质 </div> <div class="value"> {{formSate.nature}} </div></div>
         <div class="info-modal-item" >  <div class="title"> 培训形式 </div> <div class="value"> {{formSate.method}} </div></div>
         <div class="info-modal-item" >  <div class="title"> 培训形态 </div> <div class="value"> {{formSate.form}} </div></div>
-        <div class="info-modal-item" >  <div class="title"> 培训内容 </div> <div class="value"> {{formSate.content}} </div></div>
+        <div class="info-modal-item" >  <div class="title"> 培训内容 </div> <div class="value"> {{formSate.contents.join('/')}} </div></div>
         <div class="info-modal-item" >  <div class="title"> 园所名称 </div> <div class="value"> {{formSate.schoolName}} </div></div>
         <div class="info-modal-item" >  <div class="title"> 代理商名称 </div> <div class="value"> {{formSate.agentName}} </div></div>
       </div>
@@ -104,7 +110,7 @@
 
 </template>
 <script  setup >
-import { ref, computed, onMounted } from 'vue'
+import { ref, computed, onMounted, watch } from 'vue'
 import CityJson from '@/json/city.json'
 import { useRouter } from 'vue-router'
 import { useUserStore, useCommonStore, useTrainStore } from '@/store'
@@ -128,25 +134,26 @@ const formSate = ref({
   subject: '',
   subjectId: '',
   teacherId: '',
-  content: '',
-  contentId: '',
+  contents: [],
   address: '',
   schoolName: '',
   agentName: '',
-  methodId: 1,
-  method: '线上',
-  natureId: 1,
-  nature: '推广',
-  regions: []
+  methodId: '',
+  method: '',
+  natureId: '',
+  nature: '',
+  regions: [],
+  cusContent: ''
 })
 
 const formRequireState = ref({
   formId: false,
   subjectId: false,
-  contentId: false,
   regions: false,
   schoolName: false,
-  agentName: false
+  agentName: false,
+  methodId: false,
+  natureId: false
 })
 
 const fieldNames = {
@@ -155,9 +162,9 @@ const fieldNames = {
   children: 'children'
 }
 
-const methods = ref([{id: 1, name: '线上'}, {id: 2, name: '本地'}, {id: 3, name: '外出'}])
+const methods = ref([{ id: 1, name: '线上' }, { id: 2, name: '本地' }, { id: 3, name: '外出' }])
 
-const natures = ref([{id: 1, name: '推广'}, {id: 2, name: '售后'}])
+const natures = ref([{ id: 1, name: '推广' }, { id: 2, name: '售后' }])
 
 const router = useRouter()
 
@@ -186,6 +193,13 @@ const sheetAction = computed(() => {
   }
 })
 
+watch(
+  () => formSate.value.content,
+  () => {
+    console.log(formSate.value.content)
+  }
+)
+
 const submit = () => {
   trainStore.add(formSate.value).then(() => {
     router.push({
@@ -235,7 +249,7 @@ const onFinish = (record) => {
   console.log('record.selectedOptions:', formSate.value.regions)
 }
 
-const { run } = useDebounceFn(() => submit(), {wait: 1000}) 
+const { run } = useDebounceFn(() => submit(), { wait: 1000 })
 
 onMounted(() => {
   getSubjectList()
@@ -267,6 +281,9 @@ onMounted(() => {
         font-weight: 600;
         text-align: left;
       }
+      .check-in-item-content {
+        margin: 24px 0;
+      }
       .check-in-disabled {
         background-color: #ccc;
       }

+ 7 - 3
src/views/login.vue

@@ -38,9 +38,13 @@ const forgetPwd = () => {
 }
 
 onMounted(() => {
-  const code = getQueryString('code')
-  if (!userInfo.token && !code) {
-    WxLogin('/login')
+  if (process.env.NODE_ENV === 'development') {
+
+  } else {
+    const code = getQueryString('code')
+    if (!userInfo.token && !code) {
+      WxLogin('/login')
+    }
   }
 })
 

+ 11 - 15
src/views/record.vue

@@ -16,7 +16,7 @@
         <van-icon name="arrow-down" />
       </div>
     </div>
-  
+
       <van-list
         v-model:loading="loading"
         :finished="finished"
@@ -41,9 +41,9 @@
             description="暂无记录"
           />
       </div>
-      
+
       </van-list>
-  
+
     <div class="craete-btn" @click="createTrain" >
       新建培训
     </div>
@@ -127,15 +127,14 @@ const changeSubject = (record) => {
     console.log(dateShow.value)
   } else {
     actionShow.value = true
-  }
-
+  }
 }
 
 const list = ref([])
 const total = ref(0)
 
-const loading = ref(false);
-const finished = ref(false);
+const loading = ref(false)
+const finished = ref(false)
 
 const curId = ref('')
 
@@ -168,16 +167,16 @@ const searchTime = ref({
 const onLoad = () => {
   queryParamsState.value.page++
   getRecordPage().then(() => {
-    loading.value = false;
+    loading.value = false
     if (list.value.length >= total.value) {
-      finished.value = true;
+      finished.value = true
     }
   })
-};
+}
 
 const onConfirm = (key) => {
   list.value = []
-  console.log('key:', key);
+  console.log('key:', key)
   if (key === 'all') {
     searchList.value[2].title = '培训时间'
     searchTime.value.startTime = ''
@@ -194,11 +193,9 @@ const onConfirm = (key) => {
   }
   dateShow.value = false
   queryParamsState.value.page = 1
-  getRecordPage()
-
+  getRecordPage()
 }
 
-
 const onSelect = (record) => {
   list.value = []
   const _targetSearch = searchList.value.find(item => item.key === curSearcht.value.key)
@@ -220,7 +217,6 @@ const trainRouteByStateMap = new Map([
   [3, '/trained']
 ])
 
-
 const createTrain = () => {
   router.push({
     path: '/create-train'

+ 3 - 2
src/views/review.vue

@@ -67,11 +67,12 @@
   </div>
 </template>
 <script setup >
-import { useTrainStore, useUserStore, useVisitorStore } from '@/store'
-import { storeToRefs } from 'pinia'
+/** eslint-disable */
+import { useUserStore, useVisitorStore } from '@/store'
 import { ref, onMounted } from 'vue'
 import { WxLogin, getQueryString } from '@/utils/wx'
 import { useRoute } from 'vue-router'
+import { storeToRefs } from 'pinia'
 
 const route = useRoute()
 

+ 126 - 5
src/views/sign-code.vue

@@ -2,7 +2,7 @@
   <div class="sign-code" >
 
     <!-- qrcode -->
-    <div class="qrcode-container" >
+    <!-- <div class="qrcode-container" >
       <div class="title" > 已有 {{current.checkInCount}} 人签到 </div>
       <div class="qrcode" >
         <img v-if="urlLink" :src="urlLink" alt="" style="width: 160px; height: 160px;" >
@@ -13,6 +13,29 @@
         长按保存二维码
       </div>
       </div>
+    </div> -->
+    <!-- 上传照片 -->
+    <div class="photo-container" >
+      <div class="photo-container-item upload" >
+        <div class="label" >上传照片</div>
+        <div class="content" >
+          <div class="photos" >
+            <div class="photo" v-for="(item, index) in photos" :key="index" >
+              <img :src="item" alt="">
+              <van-icon name="close" class="close-icon" @click="delPhoto(index)" />
+            </div>
+          </div>
+          <div class="photo-upoload" v-if="photos.length !== maxUploadCount"  @click="chooseImage" >
+            <van-icon name="plus" />
+          </div>
+        </div>
+      </div>
+      <div class="photo-container-item upload" >
+        <div class="label" >参与人数</div>
+        <div class="content" >
+          <van-field  style="border: 1px solid #d6cece;" v-model="checkInCount" label="" placeholder="请输入" />
+        </div>
+      </div>
     </div>
     <div class="start-btn" @click="startTrain">开始培训</div>
     <div class="tip" >
@@ -26,6 +49,8 @@ import { ref, onMounted } from 'vue'
 import QRCode from 'qrcodejs2'
 import { useTrainStore } from '@/store'
 import { storeToRefs } from 'pinia'
+import wx from 'weixin-js-sdk'
+import { uploadFile } from '@/api/common'
 
 const trainStore = useTrainStore()
 
@@ -36,6 +61,12 @@ const { byId, start } = trainStore
 const qrcodeDom = ref()
 
 const urlLink = ref()
+
+const maxUploadCount = 3
+
+const photos = ref([require('@/assets/logo.png'), require('@/assets/logo.png'), require('@/assets/logo.png')])
+
+const checkInCount = ref('')
 const createQrecode = () => {
   // 清除之前的二维码
   qrcodeDom.value.innerHTML = ''
@@ -54,13 +85,48 @@ const createQrecode = () => {
 }
 
 const startTrain = () => {
-  start(current.value.id)
+  if (photos.value.length === 0) {
+    // eslint-disable-next-line no-undef
+    showToast('请上传照片')
+    return
+  }
+  if (checkInCount.value.length === 0) {
+    // eslint-disable-next-line no-undef
+    showToast('请输入参与人数')
+    return
+  }
+  start(current.value.id, photos.value, checkInCount.value)
 }
 
-onMounted(() => {
-  byId(current.value.id).then(() => {
-    createQrecode()
+const delPhoto = (index) => {
+  photos.value.splice(index, 1)
+}
+
+const chooseImage = () => {
+  wx.chooseImage({
+    count: maxUploadCount - photos.value.length,
+    sizeType: ['original', 'compressed'],
+    sourceType: ['album'],
+    success: function (res) {
+      handleUploadFile(res.tempFilePaths)
+    }
+  })
+}
+
+const handleUploadFile = async (data) => {
+  const filePath = data.path || data[0]
+  // eslint-disable-next-line no-undef
+  showLoadingToast({
+    message: '上传中...',
+    forbidClick: true
   })
+  await uploadFile({ filePath, name: 'file' })
+  // eslint-disable-next-line no-undef
+  closeToast(true)
+}
+
+onMounted(() => {
+  // byId(current.value.id)
 })
 </script>
 <style lang='less' scoped >
@@ -107,6 +173,61 @@ onMounted(() => {
       }
     }
   }
+  .photo-container {
+    width: 340px;
+    background-color: #fff;
+    border-radius: 20px;
+    margin: 0 auto;
+    padding: 24px;
+    box-sizing: border-box;
+    margin-top: -400px;
+    .upload {
+      display: flex;
+      flex-direction: column;
+      justify-content: flex-start;
+      .label {
+        text-align: left;
+      }
+      .label::before {
+        content: "*";
+        color: red;
+        margin-right: 0.2em; /* 星号和文本之间的距离 */
+      }
+      .content {
+        text-align: left;
+        margin-top: 12px;
+        margin-bottom: 12px;
+        display: flex;
+        .photos {
+          display: flex;
+          .photo {
+            width: 86px;
+            height: 86px;
+            margin-right: 12px;
+            position: relative;
+            .close-icon {
+              position: absolute;
+              top: 6px;
+              right: 6px;
+            }
+            img {
+              width: 100%;
+              height: 100%;
+            }
+          }
+        }
+        .photo-upoload {
+          width: 86px;
+          height: 86px;
+          background-color: #f6f6f6;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          border-radius: 12px;
+        }
+      }
+    }
+  }
   .start-btn {
     width: 280px;
     height: 52px;

+ 31 - 1
src/views/trained.vue

@@ -1,10 +1,20 @@
 <template>
   <div class="trained" >
+    <div
+      class="photo-cantainer"
+    >
+      <div class="photos" >
+        <div class="photo" v-for="(item, index) in [1,2,3,4,5,6]" :key="index" >
+           <img :src="item" alt="">
+         </div>
+      </div>
+    </div>
+
     <div class="trained-status" >
       <div style="font-size: 20px;font-weight: 600;">培训结束</div>
       <!-- <div style="margin: 10px 0px;">用时{{dayjs(current.timestamp).format('HH小时mm分钟ss秒')}}</div> -->
       <div style="margin: 20px 0px;">用时{{current.duration}}</div>
-      <div>共有 {{current.checkInCount}}人签到</div>
+      <div>参与人数 {{current.checkInCount}}</div>
     </div>
 
     <!-- 评价二维码 -->
@@ -78,6 +88,26 @@ onMounted(() => {
   background-color: #eff2f5;
   padding-bottom: 260px;
   box-sizing: border-box;
+  .photo-cantainer {
+    .photos {
+      display: flex;
+      .photo {
+        width: 86px;
+        height: 86px;
+        margin-right: 12px;
+        position: relative;
+        .close-icon {
+          position: absolute;
+          top: 6px;
+          right: 6px;
+        }
+        img {
+          width: 100%;
+          height: 100%;
+        }
+      }
+    }
+  }
   .trained-status {
     width: 340px;
     height: 140px;

+ 127 - 5
src/views/training.vue

@@ -1,5 +1,29 @@
 <template>
   <div class="training" >
+
+    <div class="photo-container" >
+      <div class="photo-container-item upload" >
+        <div class="label" >上传照片(结束培训)</div>
+        <div class="content" >
+          <div class="photos" >
+            <div class="photo" v-for="(item, index) in photos" :key="index" >
+              <img :src="item" alt="">
+              <van-icon name="close" class="close-icon" @click="delPhoto(index)" />
+            </div>
+          </div>
+          <div class="photo-upoload"  @click="chooseImage" >
+            <van-icon name="plus" />
+          </div>
+        </div>
+      </div>
+      <div class="photo-container-item upload" >
+        <div class="label" >参与人数</div>
+        <div class="content" >
+          <van-field  style="border: 1px solid #d6cece;" v-model="checkInCount" label="" placeholder="请输入" />
+        </div>
+      </div>
+    </div>
+
     <div class="schdule" >
       <!-- 0 小时 00 分 05 秒 -->
        {{timeStampText}}
@@ -9,18 +33,17 @@
     <div class="final-btn" @click="endTrain" >结束培训</div>
     <div class="remark" >备注:培训结束前请完成签到,培训结束则不支持签到</div>
     <div class="tip" >提示:点击【结束培训】请确保您所在位置为幼儿园实际位置(自动定位)</div>
-    <div class="qrcode-container" >
-      <div class="title" > 已有 {{current.checkInCount}} 人签到 </div>
+    <!-- <div class="qrcode-container" >
+      <div class="title" > 已有 {{current?.checkInCount}} 人签到 </div>
       <div class="qrcode" >
         <img v-if="urlLink" :src="urlLink" alt="" style="width: 160px; height: 160px;" >
-        <div v-else ref="qrcodeDom"   style="width: 160px; height: 160px;" >
-      </div>
+        <div v-else ref="qrcodeDom"   style="width: 160px; height: 160px;" ></div>
       <div class="desc" >
         现场听众【扫一扫】二维码进行签到
         长按保存二维码
       </div>
       </div>
-    </div>
+    </div> -->
   </div>
 
   <van-dialog
@@ -40,6 +63,8 @@ import { ref, onMounted, onUnmounted } from 'vue'
 import QRCode from 'qrcodejs2'
 import { useTrainStore } from '@/store'
 import { storeToRefs } from 'pinia'
+import wx from 'weixin-js-sdk'
+import { uploadFile } from '@/api/common'
 
 const trainStore = useTrainStore()
 
@@ -67,10 +92,54 @@ const createQrecode = () => {
 }
 
 const show = ref(false)
+
+const maxUploadCount = 3
+
+const photos = ref([])
+
+const checkInCount = ref('')
+
+const delPhoto = (index) => {
+  photos.value.splice(index, 1)
+}
+
 const endTrain = () => {
+  if (photos.value.length === 0) {
+    // eslint-disable-next-line no-undef
+    showToast('请上传照片')
+    return
+  }
+  if (checkInCount.value.length === 0) {
+    // eslint-disable-next-line no-undef
+    showToast('请输入参与人数')
+    return
+  }
   show.value = true
 }
 
+const chooseImage = () => {
+  wx.chooseImage({
+    count: maxUploadCount - photos.value.length,
+    sizeType: ['original', 'compressed'],
+    sourceType: ['album'],
+    success: function (res) {
+      handleUploadFile(res.tempFilePaths)
+    }
+  })
+}
+
+const handleUploadFile = async (data) => {
+  const filePath = data.path || data[0]
+  // eslint-disable-next-line no-undef
+  showLoadingToast({
+    message: '上传中...',
+    forbidClick: true
+  })
+  await uploadFile({ filePath, name: 'file' })
+  // eslint-disable-next-line no-undef
+  closeToast(true)
+}
+
 const reload = () => {
   byId(current.value.id)
 }
@@ -105,6 +174,59 @@ onUnmounted(() => {
 </script>
 <style lang='less' scoped >
 .training {
+  .photo-container {
+    width: 340px;
+    background-color: #fff;
+    border-radius: 20px;
+    margin: 0 auto;
+    padding: 24px;
+    box-sizing: border-box;
+    .upload {
+      display: flex;
+      flex-direction: column;
+      justify-content: flex-start;
+      .label {
+        text-align: left;
+      }
+      .label::before {
+        content: "*";
+        color: red;
+        margin-right: 0.2em; /* 星号和文本之间的距离 */
+      }
+      .content {
+        text-align: left;
+        margin-top: 12px;
+        margin-bottom: 12px;
+        .photos {
+          display: flex;
+          .photo {
+            width: 86px;
+            height: 86px;
+            margin-right: 12px;
+            position: relative;
+            .close-icon {
+              position: absolute;
+              top: 6px;
+              right: 6px;
+            }
+            img {
+              width: 100%;
+              height: 100%;
+            }
+          }
+        }
+        .photo-upoload {
+          width: 86px;
+          height: 86px;
+          background-color: #f6f6f6;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          border-radius: 12px;
+        }
+      }
+    }
+  }
   .schdule {
     width: 340px;
     height: 100px;