zuypeng 1 рік тому
батько
коміт
19fcaf974d

+ 11 - 1
src/components/login-bar/index.vue

@@ -1,9 +1,13 @@
 <template>
   <view class="login-bar flex flex-inline" v-if="!state.isLogin">
     <text class="login-bar_title" >   请先登录以享受更多洗车功能</text>
-    <uv-button class="login-bar_btn" size="mini" shape="circle" type="primary" open-type="getPhoneNumber"
+<!--    <uv-button class="login-bar_btn" size="mini" shape="circle" type="primary" open-type="getPhoneNumber"
                color="#19A497"
                @getphonenumber="handleGetPhone">去登录
+    </uv-button>-->
+    <uv-button class="login-bar_btn" size="mini" shape="circle" type="primary"
+               color="#19A497"
+               @click="handleGotoLoginPage">去登录
     </uv-button>
   </view>
 </template>
@@ -56,6 +60,12 @@ const handleGetPhone = (e:any) => {
       state.isLogin =true;
   })
 }
+
+const handleGotoLoginPage = () => {
+  uni.navigateTo({
+    url: '/pages-user/login/index'
+  })
+}
 </script>
 
 <style scoped lang="scss">

+ 73 - 0
src/pages-user/agreement/index.vue

@@ -0,0 +1,73 @@
+<template>
+  <view class="page">
+    <uv-collapse @change="handleChange" @close="close" @open="open">
+      <uv-collapse-item :title="(index+1)+'.'+item.question" :name="item.question"  v-for="(item, index) in state.questions" :key="index">
+        <uv-parse :content="item.answer" :selectable="true"></uv-parse>
+      </uv-collapse-item>
+    </uv-collapse>
+  </view>
+</template>
+
+<script setup lang="ts">
+import {onHide, onShow} from "@dcloudio/uni-app";
+import {reactive} from "vue";
+import {body} from "@/utils/https"
+
+
+const initState = () => ({
+  questions: [] as any[],
+  servicerPhone: "",
+})
+
+const state = reactive(initState())
+
+const handleChange = () => {
+
+}
+
+const close = () => {
+
+}
+
+const open = () => {
+
+}
+
+const call = () => {
+  uni.makePhoneCall({
+    phoneNumber: state.servicerPhone,
+  });
+};
+
+const toggle = (index: number) => {
+  state.questions = state.questions.map((item, i) => {
+    return {
+      ...item,
+      open: item.open ? false : i === index,
+    };
+  });
+};
+
+onShow((options:any) => {
+  if (options) {
+    state.servicerPhone = options.servicerPhone;
+  }
+  body(`/faq/list`).then((res: any) => {
+    state.questions = res.list;
+  })
+});
+
+onHide(() => {
+  Object.assign(state, initState());
+})
+</script>
+
+<style lang="scss">
+.page {
+  min-height: 100vh;
+  background: #fff;
+  box-sizing: border-box;
+  padding: 40rpx 32rpx;
+}
+
+</style>

+ 214 - 0
src/pages-user/login/index.vue

@@ -0,0 +1,214 @@
+<template>
+  <div class="login-container">
+    <!-- Logo区域 -->
+    <div class="logo-box">
+      <img src="@/static/logo.png" alt="logo" class="logo">
+      <div class="app-name">iWash洗车</div>
+    </div>
+
+    <!-- 登录按钮 -->
+    <div class="login-box">
+<!--      <div class="wechat-login-btn" @click="handleWechatLogin">
+        <img src="@/assets/wechat-icon.png" alt="微信" class="wechat-icon">
+        微信一键登录
+      </div>-->
+      <uv-button :disabled="!isAgreePrivacy||isAgreePrivacy.length===0"
+                 class="phone-login-btn" type="primary"        color="#19A497"
+                 @getphonenumber="handleGetPhone"  open-type="getPhoneNumber">
+        手机号一键登录
+      </uv-button>
+    </div>
+
+    <!-- 隐私协议 -->
+    <div class="privacy-box">
+      <view style="display: inline-flex" class="agreement">
+<!--        <uv-checkbox   v-model="isAgreePrivacy"    @change="handlePrivacyChange"></uv-checkbox>-->
+        <uv-checkbox-group
+            v-model="isAgreePrivacy"
+            shape="circle">
+          <uv-checkbox
+              :name="'aa'"
+          ></uv-checkbox>
+        </uv-checkbox-group>
+        <view class="text">
+          我已阅读并同意
+          <view class="link" @click="showUserAgreement">《用户协议》</view>
+          和
+          <view class="link" @click="showPrivacyPolicy">《隐私政策》</view>
+        </view>
+      </view>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useRouter } from 'vue-router'
+import {login} from "@/utils/auth";
+import {onShow} from "@dcloudio/uni-app";
+
+const router = useRouter()
+const isAgreePrivacy = ref([])
+const redirectUrl =ref ("")
+const shortId =ref ("")
+
+
+onShow((options:any)=>{
+  console.log(options)
+  redirectUrl.value = options?.query?.redirectUrl
+  shortId.value = options?.query?.shortId
+})
+// 处理微信登录
+const handleWechatLogin = () => {
+  if (!isAgreePrivacy.value) {
+    alert('请先同意用户协议和隐私政策')
+    return
+  }
+  // 这里添加微信登录逻辑
+  console.log('微信登录')
+}
+
+// 处理手机号登录
+const handlePhoneLogin = () => {
+  if (!isAgreePrivacy.value) {
+    alert('请先同意用户协议和隐私政策')
+    return
+  }
+  router.push('/phone-login')
+}
+
+// 处理隐私协议变更
+const handlePrivacyChange = () => {
+  console.log('隐私协议同意状态:', isAgreePrivacy.value)
+}
+
+// 显示用户协议
+const showUserAgreement = () => {
+  uni.navigateTo({
+    url:'/pages-user/agreement/index'
+  })
+}
+
+// 显示隐私政策
+const showPrivacyPolicy = () => {
+  uni.navigateTo({
+    url:'/pages-user/policy/index'
+  })
+}
+
+const handleGetPhone = (e:any) => {
+  console.log(e)
+  if(shortId.value){
+    e.shortId=shortId.value;
+  }
+  login(e).then((token: string) => {
+    console.log(">>>>>>>>>",token)
+    if(redirectUrl.value){
+      uni.redirectTo({
+        url:redirectUrl.value
+      })
+    }else{
+      uni.switchTab({
+        url:"/pages/index/index"
+      })
+    }
+  })
+}
+</script>
+
+<style scoped lang="scss">
+.login-container {
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 60px 30px;
+  background: #ffffff;
+}
+
+.logo-box {
+  margin-bottom: 80px;
+  text-align: center;
+}
+
+.logo {
+  width: 120px;
+  height: 120px;
+  margin-bottom: 20px;
+}
+
+.app-name {
+  font-size: 24px;
+  font-weight: bold;
+  color: #333;
+}
+
+.login-box {
+  width: 100%;
+  margin-bottom: 40px;
+}
+
+.wechat-login-btn,
+.phone-login-btn {
+  color:#19A497 !important;
+  width: 100%;
+  height: 44px;
+  border-radius: 2px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 16px;
+  margin-bottom: 15px;
+  cursor: pointer;
+}
+
+.wechat-login-btn {
+  background: #07c160;
+  color: white;
+}
+
+.phone-login-btn {
+  background: #f5f5f5;
+  color: #333;
+}
+
+.wechat-icon {
+  width: 24px;
+  height: 24px;
+  margin-right: 8px;
+}
+
+.privacy-box {
+  position: fixed;
+  bottom: 30px;
+  left: 0;
+  right: 0;
+  padding: 0 30px;
+}
+
+.agreement {
+  display: flex;
+  align-items: center;
+  font-size: 14px;
+  color: #666;
+  position: relative;
+}
+
+.agreement input {
+  position: absolute;
+  opacity: 0;
+  cursor: pointer;
+  height: 0;
+  width: 0;
+}
+
+
+.text {
+  display: inline-flex;
+  line-height: 1.4;
+}
+
+.link {
+  color:$uni-color-primary
+}
+</style>

+ 138 - 0
src/pages-user/policy/index.vue

@@ -0,0 +1,138 @@
+<template>
+  <view class="page">
+    <view>
+
+      # 小程序隐私政策
+
+      欢迎使用[小程序名称](以下简称“本小程序”)。本隐私政策旨在向您说明我们在您使用本小程序时如何收集、使用、存储、共享和保护您的个人信息,以及您所享有的相关权利。
+
+      ## 一、信息收集
+
+      1. **收集信息的范围**
+      为了向您提供服务,我们可能会收集以下类型的个人信息:
+      - **基本信息**:微信昵称、头像。
+      - **联系方式**:手机号码。
+      - **位置信息**:用于提供基于位置的服务。
+      - **其他信息**:根据具体功能需要,可能还会收集其他必要信息,如照片、视频等。
+
+      2. **收集信息的目的**
+      我们仅在实现小程序功能所必要的范围内收集信息,例如:
+      - 使用微信昵称和头像进行用户身份识别。
+      - 使用手机号码进行账号注册或验证。
+      - 使用位置信息提供精准服务。
+
+      ## 二、信息使用
+
+      1. 我们将根据收集的个人信息类型,按照合法、正当、必要的原则使用个人信息,包括但不限于以下用途:
+      - 提供和优化本小程序的服务。
+      - 用于用户身份验证和安全保护。
+      - 分析用户行为以改进用户体验。
+
+      2. 如需将您的信息用于本隐私政策未明确的其他用途,我们将再次征得您的明示同意。
+
+      ## 三、信息存储和保护
+
+      1. 我们将按照相关法律法规和标准要求,对收集的用户个人信息进行安全存储和保护。
+      2. 我们承诺,除法律法规另有规定外,对您的信息的保存期限为实现处理目的所必要的最短时间。
+      3. 我们将采取必要的技术和管理措施,确保用户个人信息的保密性、完整性、可用性和可追溯性。
+      4. 如发生个人信息泄露等安全事件,我们将立即采取补救措施并及时告知用户,同时依法向相关部门报告。
+
+      ## 四、第三方链接和数据共享
+
+      1. 本小程序可能包含第三方链接或服务,用户点击后可能会跳转到其他网站或应用。请用户在使用前仔细阅读相关隐私政策或服务条款。
+      2. 我们将按照法律法规和用户意愿,在以下情况下共享或转让个人信息:
+      - 经用户明确同意。
+      - 为实现特定功能,接入第三方插件或SDK时,将按照第三方的隐私政策处理信息。
+      3. 我们将与第三方共享必要的信息时,要求第三方遵守相关法律法规和本政策的约定,对个人信息进行保护。
+
+      ## 五、用户权益
+
+      1. **查阅、复制、更正、删除个人信息**
+      您可以通过以下方式与我们联系,行使查阅、复制、更正、删除等法定权利:
+      - 邮箱:[具体邮箱地址]。
+      - 小程序内设置路径:小程序主页右上角“…”—“设置”—“小程序已获取的信息”。
+
+      2. **撤回授权**
+      您可以通过小程序内的设置页面,撤回对某些权限的授权。
+
+      3. **账号注销**
+      若您在小程序中注册了账号,可以通过以下方式与我们联系,申请注销账号:
+      - 邮箱:[具体邮箱地址]。
+      - 我们承诺在十五个工作日内完成核查和处理,并按照法律法规要求处理您的相关信息。
+
+      ## 六、隐私政策的修改和更新
+
+      我们保留随时修改本政策的权利,如有必要,我们会及时告知用户并对本政策进行更新。更新日期:[具体日期]。
+
+      ## 七、联系方式
+
+      如果您对本隐私政策有任何疑问或建议,或认为我们未遵守上述约定,您可以通过以下方式与我们联系:
+      - 邮箱:[具体邮箱地址]。
+
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import {onHide, onShow} from "@dcloudio/uni-app";
+import {reactive} from "vue";
+import {body} from "@/utils/https"
+
+
+const initState = () => ({
+  questions: [] as any[],
+  servicerPhone: "",
+})
+
+const state = reactive(initState())
+
+const handleChange = () => {
+
+}
+
+const close = () => {
+
+}
+
+const open = () => {
+
+}
+
+const call = () => {
+  uni.makePhoneCall({
+    phoneNumber: state.servicerPhone,
+  });
+};
+
+const toggle = (index: number) => {
+  state.questions = state.questions.map((item, i) => {
+    return {
+      ...item,
+      open: item.open ? false : i === index,
+    };
+  });
+};
+
+onShow((options:any) => {
+  if (options) {
+    state.servicerPhone = options.servicerPhone;
+  }
+  body(`/faq/list`).then((res: any) => {
+    state.questions = res.list;
+  })
+});
+
+onHide(() => {
+  Object.assign(state, initState());
+})
+</script>
+
+<style lang="scss">
+.page {
+  min-height: 100vh;
+  background: #fff;
+  box-sizing: border-box;
+  padding: 40rpx 32rpx;
+}
+
+</style>

+ 15 - 3
src/pages-user/wallet/index.vue

@@ -28,9 +28,10 @@
 
     <view class="wallet-body">
       <uv-tabs :list="tabs" :current="tab" @click="handleTabClick"></uv-tabs>
+
       <uv-list>
         <uv-list-item
-            clickable :show-arrow="item.type===3" v-for="(item,index) in dataList" :key="index" @click="handleClickDetail(item)">
+            clickable show-arrow :class="item.type!==3?'arrow-hide':''" v-for="(item,index) in dataList" :key="index" @click="handleClickDetail(item)">
           <template #default>
             <view class="wallet-item">
               <view class="wallet-item_header">
@@ -50,7 +51,7 @@
           </template>
         </uv-list-item>
       </uv-list>
-      <uv-empty mode="order" v-if="!dataList||dataList.length===0" text="暂无数据"></uv-empty>
+      <uv-empty mode="order" v-if="!dataList||dataList.length===0" text="暂无数据" :marginTop="200"></uv-empty>
     </view>
 
     <view class="wallet-bottom">
@@ -143,7 +144,7 @@ const loadData = () => {
 }
 
 const handleClickDetail = (walletDetail: any) => {
-  if(walletDetail.type!=3){
+  if (walletDetail.type != 3) {
     return;
   }
   uni.navigateTo({
@@ -273,7 +274,18 @@ const handleClickDetail = (walletDetail: any) => {
     height: 36px;
   }
 
+}
+
 
+.arrow-hide {
+  :deep(.uv-list-item .uv-icon) {
+    visibility: hidden !important;
+  }
 }
 
+.arrow-hide{
+  .uv-icon {
+    visibility: hidden !important;
+  }
+}
 </style>

+ 11 - 1
src/pages-wash/device/index.vue

@@ -83,10 +83,15 @@ onLoad((options: any) => {
     }
   }
   state.device = getApp<any>().globalData.last.device;
-  tryLogin().then((token) => {
+  checkLogin().then((token) => {
     setTimeout(() => {
       loadDeviceDetail(id);
     }, 200)
+  }).catch(e=>{
+    console.error("onLoad 校验登录失败,自动跳转登录页")
+    uni.navigateTo({
+      url:`/pages-user/login/index?shortId=${state.deviceId}&redirectUrl=/pages-wash/device/index`
+    })
   })
 });
 
@@ -109,6 +114,11 @@ onShow(() => {
     }else{
       loadDeviceDetail(state.deviceId);
     }
+  }).catch(e=>{
+    console.error("校验登录失败,自动跳转登录页")
+    uni.navigateTo({
+      url:`/pages-user/login/index?shortId=${state.deviceId}&redirectUrl=/pages-wash/device/index`
+    })
   })
 
 

+ 8 - 2
src/pages-wash/station/index.vue

@@ -11,7 +11,7 @@
     <view class="device-box w100">
       <view class="device-item" v-for="device in state.deviceList" :key="device.id" @click="handleClickDevice(device)">
         <view class="device-item_header">
-          <text>No.{{ device.shortId }}</text>
+          <text class="device-item_header-short">No.{{ device.shortId }}</text>
           <text class="device-item_header-status">{{ fmtDictName('WashDevice.state', device.state) }}</text>
         </view>
         <view class="device-item_func">
@@ -153,6 +153,11 @@ const handleClickDevice = (device: any) => {
     font-size: 13px;
     padding: 10rpx 0;
 
+    &-short{
+      font-size: 16px;
+      font-weight: 500;
+    }
+
     &-status {
       font-size: 20rpx;
       background-color: $uni-color-primary;
@@ -172,7 +177,8 @@ const handleClickDevice = (device: any) => {
     }
 
     &-tag {
-      margin-right: 10px;
+      margin-right: 6px;
+      transform: scale(0.8);
     }
   }
 }

+ 27 - 0
src/pages.json

@@ -101,6 +101,33 @@
             "navigationBarTitleText": "我的退款",
             "navigationBarBackgroundColor": "#ffffff"
           }
+        },
+        {
+          "path": "login/index",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "登录",
+            "navigationBarBackgroundColor": "#ffffff",
+            "enablePullDownRefresh": true
+          }
+        },
+        {
+          "path": "agreement/index",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "用户协议",
+            "navigationBarBackgroundColor": "#ffffff",
+            "enablePullDownRefresh": true
+          }
+        },
+        {
+          "path": "policy/index",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "隐私政策",
+            "navigationBarBackgroundColor": "#ffffff",
+            "enablePullDownRefresh": true
+          }
         }
       ]
     },

+ 3 - 8
src/pages/index/index.vue

@@ -7,15 +7,12 @@
           <text class="title">{{ title }}</text>
         </view>-->
     <view style="width: 100%;" class="swiper-content">
-<!--      {{state.swiperList}}-->
-      <cover-view>
         <swiper class="swiper" circular :indicator-dots="true" :autoplay="true" :interval="3000"
                 :duration="500">
           <swiper-item v-for="(item,idx) in state.swiperList" :key="idx">
             <image :src="item" mode="widthFix" style="width: 100%" @click="handleBannerClick(idx)"></image>
           </swiper-item>
         </swiper>
-      </cover-view>
 
 <!--      <uv-swiper
           style="width: 100%"
@@ -45,8 +42,6 @@
       </uv-grid>
     </view>
 
-<!--    <image v-if="state.swiperList.length>0" :src="state.swiperList[0]" mode="widthFix" style="width: 100%"></image>-->
-
     <view class="w100 gap"></view>
 
     <!--    站点清单  start-->
@@ -54,7 +49,7 @@
       <template v-if="isLogin">
         <WashStation v-for="item in state.stationList" :key="item.id" :item="item"></WashStation>
       </template>
-      <uv-empty v-else mode="order" text="请先登录"></uv-empty>
+      <uv-empty v-else mode="order" text="请先登录" :marginTop="100"></uv-empty>
     </view>
     <!--    站点清单  end-->
 
@@ -359,7 +354,7 @@ page {
 }
 
 .swiper-content {
-  height: 240rpx;
+  height: 300rpx;
   overflow: hidden;
 }
 
@@ -374,7 +369,7 @@ page {
 
 .content_station {
 
-  height: calc(100vh - 750rpx);
+  height: calc(100vh - 810rpx);
   overflow: scroll;
 }
 </style>

BIN
src/static/logo.png


+ 1 - 0
src/utils/auth.ts

@@ -76,6 +76,7 @@ export function login(e: any): Promise<string> {
                     code: res.code,
                     avatar: "",
                     nickname: "",
+                    shortId:e.shortId
                 }
                 body(`/user/wxLogin`, data).then(async (res: any) => {
                     let {satoken, userId} = res;