fix:修复bug

This commit is contained in:
2025-10-14 19:20:26 +08:00
parent 30e298d9d2
commit 4408673438
21 changed files with 1153 additions and 549 deletions
+92 -41
View File
@@ -21,18 +21,22 @@
<view class="form-title">填写快递归还信息</view>
<view class="form-item">
<view class="item-label">联系电话</view>
<input class="item-input" type="number" v-model="phone" placeholder="请输入联系电话" maxlength="20" />
<input class="item-input" type="number" v-model="phone" placeholder="请输入联系电话" maxlength="20"
:focus-class="'item-input-focus'" />
</view>
<view class="form-item">
<view class="item-label">快递单号</view>
<input class="item-input" type="text" v-model="trackingNumber" placeholder="请输入快递单号" maxlength="40" />
<input class="item-input" type="text" v-model="trackingNumber"
:placeholder="isFillMode ? '请输入需要补填的快递单号' : '请输入快递单号(可先留空)'" maxlength="40"
:focus-class="'item-input-focus'" />
</view>
<view class="tips" v-if="tipsText">{{ tipsText }}</view>
</view>
<!-- 提交操作条 -->
<view class="bottom-bar">
<view class="action-item primary" @click="handleSubmit">提交信息</view>
<view class="action-item primary" :class="{ disabled: submitting }" hover-class="primary-hover"
@click="!submitting && handleSubmit()">{{ isFillMode ? '确认补填' : '提交信息' }}</view>
</view>
</view>
</template>
@@ -45,13 +49,13 @@
import {
onLoad
} from '@dcloudio/uni-app'
import {
queryById,
applyExpressReturn,
getExpressReturnByOrder,
getExpressReturnDetail,
fillExpressTrackingNumber
} from '@/config/user.js'
import {
queryById,
applyExpressReturn,
getExpressReturnByOrder,
getExpressReturnDetail,
fillExpressTrackingNumber
} from '@/config/user.js'
const orderId = ref('')
const recordId = ref('')
@@ -65,6 +69,7 @@ import {
const phone = ref('')
const trackingNumber = ref('')
const tipsText = ref('')
const submitting = ref(false)
onLoad(async (options) => {
orderId.value = options?.orderId || ''
@@ -117,7 +122,9 @@ import {
const loadRecordAndOrderByRecord = async () => {
try {
uni.showLoading({ title: '加载中' })
uni.showLoading({
title: '加载中'
})
const res = await getExpressReturnDetail(recordId.value)
if (res?.code === 200 && res.data) {
if (res.data.orderId) {
@@ -129,7 +136,10 @@ import {
throw new Error(res?.msg || '获取记录失败')
}
} catch (e) {
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
uni.showToast({
title: e.message || '加载失败',
icon: 'none'
})
} finally {
uni.hideLoading()
}
@@ -149,15 +159,22 @@ import {
cancelText: '取消',
success: (r) => {
if (r.confirm) {
uni.redirectTo({ url: `/pages/expressReturn/addExpressReturn?id=${rec.id}` })
uni.redirectTo({
url: `/pages/expressReturn/addExpressReturn?id=${rec.id}`
})
}
}
})
return
} else {
uni.showToast({ title: '已有归还记录', icon: 'none' })
uni.showToast({
title: '已有归还记录',
icon: 'none'
})
setTimeout(() => {
uni.redirectTo({ url: `/pages/expressReturn/detail?id=${rec.id}` })
uni.redirectTo({
url: `/pages/expressReturn/detail?id=${rec.id}`
})
}, 800)
}
}
@@ -186,32 +203,47 @@ import {
return true
}
const handleSubmit = async () => {
if (!validate()) return
try {
uni.showLoading({ title: isFillMode.value ? '补填中' : '提交中' })
let res
if (isFillMode.value) {
res = await fillExpressTrackingNumber({ id: Number(recordId.value), logisticsTrackingNumber: trackingNumber.value })
} else {
res = await applyExpressReturn({
orderId: orderId.value,
logisticsTrackingNumber: trackingNumber.value,
remark: ''
})
}
if (res && res.code === 200) {
uni.showToast({ title: isFillMode.value ? '补填成功' : '提交成功', icon: 'success' })
setTimeout(() => { uni.navigateBack() }, 800)
} else {
throw new Error(res?.msg || (isFillMode.value ? '补填失败' : '提交失败'))
}
} catch (e) {
uni.showToast({ title: e.message || (isFillMode.value ? '补填失败' : '提交失败'), icon: 'none' })
} finally {
uni.hideLoading()
}
}
const handleSubmit = async () => {
if (!validate() || submitting.value) return
submitting.value = true
try {
uni.showLoading({
title: isFillMode.value ? '补填中' : '提交中'
})
let res
if (isFillMode.value) {
res = await fillExpressTrackingNumber({
id: Number(recordId.value),
logisticsTrackingNumber: trackingNumber.value
})
} else {
res = await applyExpressReturn({
orderId: orderId.value,
logisticsTrackingNumber: trackingNumber.value,
remark: ''
})
}
if (res && res.code === 200) {
uni.showToast({
title: isFillMode.value ? '补填成功' : '提交成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 800)
} else {
throw new Error(res?.msg || (isFillMode.value ? '补填失败' : '提交失败'))
}
} catch (e) {
uni.showToast({
title: e.message || (isFillMode.value ? '补填失败' : '提交失败'),
icon: 'none'
})
} finally {
submitting.value = false
uni.hideLoading()
}
}
</script>
<style lang="scss" scoped>
@@ -287,6 +319,16 @@ const handleSubmit = async () => {
font-size: 28rpx;
color: #111827;
line-height: 1.2;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.item-input::placeholder {
color: #9ca3af;
}
.item-input-focus {
border-color: #2563eb;
box-shadow: 0 0 0 4rpx rgba(37, 99, 235, 0.15);
}
}
@@ -322,6 +364,15 @@ const handleSubmit = async () => {
background: #1f2937;
color: #fff;
}
&.disabled {
opacity: 0.6;
pointer-events: none;
}
}
.primary-hover {
background: #111827;
}
}
</style>
+273 -183
View File
@@ -1,40 +1,38 @@
<template>
<view class="container">
<!-- 顶部Logo区域 -->
<view class="header-section">
<view class="logo-container">
<image class="logo-image" src="/static/logo.png" mode="aspectFit" />
<text class="app-name">共享风扇</text>
</view>
<uv-notice-bar :text="noticeText" mode="link" :speed="50" :show-icon="true" color="#2196F3"
bg-color="#E3F2FD" icon="volume"></uv-notice-bar>
<view class="container fullscreen">
<view class="" style="font-size: 32rpx;font-weight: 600;margin: 15rpx 20rpx;">风电者共享风扇&充电宝</view>
<view class="map-notice" v-if="noticeText">
<uv-notice-bar :text="noticeText" :speed="50" :show-icon="true" color="#07c160" bg-color="#E8F8EF"
icon="volume"></uv-notice-bar>
</view>
<!-- 地图标题 -->
<!-- <view class="map-title">
<text>附近场地</text>
</view> -->
<!-- 地图组件 -->
<!-- 全屏地图组件 -->
<MapComponent v-if="!isLoading && userLocation" ref="mapRef" :userLocation="userLocation"
:positionList="positionList" :filteredPositions="filteredPositions" :searchKeyword="searchKeyword"
@relocate="handleRelocate" @scan="handleScan" @showList="showLocationList" @markerTap="selectPosition"
@mapCenterChange="onMapCenterChange" />
<!-- 操作步骤指引常驻显示 -->
<view class="steps-guide">
<view class="guide-header">
<text class="guide-title">使用指南</text>
</view>
<view class="steps-container">
<view class="step-item" v-for="(step, index) in guideSteps" :key="index">
<view class="step-number">{{ index + 1 }}</view>
<view class="step-content">
<text class="step-title">{{ step.title }}</text>
<text class="step-desc">{{ step.desc }}</text>
</view>
<!-- 底部操作栏附近设备 / 扫码使用 / 我的 -->
<view class="bottom-actions">
<view class="action-btn secondary small btn-nearby" @click="showLocationList">
<view class="icon-wrap">
<image class="action-icon" src="/static/map.png" mode="aspectFit" />
</view>
<text class="action-label">附近设备</text>
</view>
<view class="action-btn primary btn-scan" @click="handleScan">
<view class="icon-wrap">
<image class="action-icon" src="/static/scan-icon.png" mode="aspectFit" />
</view>
<text class="action-label">扫码使用</text>
</view>
<view class="action-btn secondary small btn-my" @click="goMy">
<view class="icon-wrap">
<image class="action-icon" src="/static/user-active.png" mode="aspectFit" />
</view>
<text class="action-label">个人中心</text>
</view>
</view>
@@ -46,6 +44,8 @@
</view>
</view>
<!-- 场地列表弹窗 -->
<view class="location-popup" v-if="showLocationPopup">
<view class="popup-mask" @click="hideLocationList"></view>
@@ -128,18 +128,23 @@
</template>
<script setup>
const redirectToLogin = () => {
try {
const pages = getCurrentPages()
const current = pages && pages.length ? pages[pages.length - 1] : null
const route = current && current.route ? ('/' + current.route) : '/pages/index/index'
const query = current && current.options ? Object.keys(current.options).map(k => `${k}=${encodeURIComponent(current.options[k])}`).join('&') : ''
const redirect = encodeURIComponent(query ? `${route}?${query}` : route)
uni.reLaunch({ url: `/pages/login/index?redirect=${redirect}` })
} catch (e) {
uni.reLaunch({ url: '/pages/login/index' })
}
}
const redirectToLogin = () => {
try {
const pages = getCurrentPages()
const current = pages && pages.length ? pages[pages.length - 1] : null
const route = current && current.route ? ('/' + current.route) : '/pages/index/index'
const query = current && current.options ? Object.keys(current.options).map(k =>
`${k}=${encodeURIComponent(current.options[k])}`).join('&') : ''
const redirect = encodeURIComponent(query ? `${route}?${query}` : route)
uni.reLaunch({
url: `/pages/login/index?redirect=${redirect}`
})
} catch (e) {
uni.reLaunch({
url: '/pages/login/index'
})
}
}
import {
ref,
computed,
@@ -155,7 +160,8 @@
} from "../../config/url.js"
import {
getDeviceInfo,
getPotionsDetail
getPotionsDetail,
getNoticeTextData
} from '../../config/user.js'
// 导入地图工具函数
import {
@@ -167,6 +173,13 @@
// 同样需要使用相对路径引入组件
// 注意:从 pages/index/ 目录访问 components/ 需要使用 ../../components/ 路径
import MapComponent from '../../components/MapComponent.vue'
// 开启右上角分享菜单(仅 mp-weixin 有效)
// #ifdef MP-WEIXIN
wx.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline']
})
// #endif
// 响应式数据
const searchKeyword = ref('')
@@ -180,27 +193,20 @@
const showLocationPopup = ref(false)
// 使用指南步骤
const guideSteps = ref([{
title: '扫码使用',
desc: '找到附近设备,扫描设备上的二维码即可开始租借'
},
{
title: '免押金支付',
desc: '无需支付押金,使用支付分免押即可完成租借'
},
{
title: '开始使用',
desc: '设备自动解锁,风扇弹出后取出即可开始使用'
},
{
title: '归还设备',
desc: '使用完毕后,按照设备规格要求将风扇还入即可结束订单'
}
])
// 使用指南已取消
// 滚动通知内容
const noticeText = ref('消费规则:每小时5元,不足1小时按1小时计费,最高24小时封顶,请爱护设备,使用后请及时归还')
// const noticeText = ref('消费规则:每小时5元,不足1小时按1小时计费,最高24小时封顶,请爱护设备,使用后请及时归还')
const noticeText = ref('')
const getNoticeText = async()=>{
const parasm = {
'title':'用户端公告'
}
const res = await getNoticeTextData(parasm);
noticeText.value = res.data.noticeContent;
}
// 距离格式化函数
const formatDistance = (distanceInMeters) => {
if (distanceInMeters < 1000) {
@@ -239,7 +245,8 @@
if (process.env.NODE_ENV === 'development') {
testDistanceCalculation()
}
await getNoticeText();
// 1. 先获取用户位置
await getUserLocationAndAddress()
@@ -268,7 +275,7 @@
longitude: location.longitude,
latitude: location.latitude
}
console.log(userLocation.value);
// 将经纬度写入本地缓存(基础信息)
try {
@@ -329,11 +336,6 @@
const loadPositions = async () => {
try {
if (!uni.getStorageSync('token')) {
redirectToLogin()
return
}
const res = await uni.request({
url: `${URL}/device/position/app/list`,
method: 'GET',
@@ -348,7 +350,10 @@
})
console.log(res);
if (res.statusCode === 200 && res.data.code === 200) {
if (res.statusCode === 401 || res.data?.code === 401 || res.data?.code === 40101) {
redirectToLogin()
return
} else if (res.statusCode === 200 && res.data.code === 200) {
positionList.value = res.data.rows || []
calculateDistances()
filteredPositions.value = [...positionList.value]
@@ -399,14 +404,9 @@
const loadPositionsByCenter = async (center) => {
try {
if (!uni.getStorageSync('token')) {
redirectToLogin()
return
}
// 使用原有接口获取所有场地
const res = await uni.request({
url: `${URL}/device/position/list`,
url: `${URL}/device/position/app/list`,
method: 'GET',
header: {
'Authorization': "Bearer " + uni.getStorageSync('token'),
@@ -414,7 +414,10 @@
}
})
if (res.statusCode === 200 && res.data.code === 200) {
if (res.statusCode === 401 || res.data?.code === 401 || res.data?.code === 40101) {
redirectToLogin()
return
} else if (res.statusCode === 200 && res.data.code === 200) {
positionList.value = res.data.rows || []
// 基于地图中心计算距离
calculateDistances(center)
@@ -437,17 +440,43 @@
}
const handleRelocate = async () => {
uni.showLoading({
title: '定位中...'
})
// 直接重新加载当前页面
uni.reLaunch({
url: '/pages/index/index'
})
try {
uni.showLoading({
title: '定位中...'
})
const loc = await getUserLocation()
const center = {
longitude: Number(loc.longitude),
latitude: Number(loc.latitude)
}
userLocation.value = center
try {
uni.setStorageSync('userLocation', center)
} catch (_) {}
if (mapRef.value && typeof mapRef.value.moveToLocation === 'function') {
mapRef.value.moveToLocation(center)
}
await loadPositionsByCenter(center)
} catch (e) {
uni.showToast({
title: '定位失败',
icon: 'none'
})
} finally {
uni.hideLoading()
}
}
const onMapCenterChange = (center) => {
if (center && typeof center.longitude !== 'undefined' && typeof center.latitude !== 'undefined') {
userLocation.value = {
longitude: Number(center.longitude),
latitude: Number(center.latitude)
}
try {
uni.setStorageSync('userLocation', userLocation.value)
} catch (_) {}
}
loadPositionsByCenter(center)
}
@@ -467,6 +496,12 @@
})
}
const goMy = () => {
uni.navigateTo({
url: '/pages/my/index'
})
}
const selectPositionFromPopup = (position) => {
// 先关闭弹窗
hideLocationList()
@@ -524,11 +559,6 @@
return
}
if (!uni.getStorageSync('token')) {
redirectToLogin()
return
}
// 检查是否有使用中的订单
const inUseRes = await uni.request({
url: `${URL}/app/order/inUse`,
@@ -539,7 +569,10 @@
}
})
if (inUseRes.statusCode == 200 && inUseRes.data.code == 200 && inUseRes.data.data) {
if (inUseRes.statusCode === 401 || inUseRes.data?.code === 401 || inUseRes.data?.code === 40101) {
redirectToLogin()
return
} else if (inUseRes.statusCode == 200 && inUseRes.data.code == 200 && inUseRes.data.data) {
const inUseOrder = inUseRes.data.data
uni.reLaunch({
url: `/pages/return/index?orderId=${inUseOrder.orderId}&deviceId=${deviceNo || inUseOrder.deviceNo}`
@@ -557,7 +590,10 @@
}
})
if (orderRes.statusCode == 200 && orderRes.data.code == 200 && orderRes.data.data) {
if (orderRes.statusCode === 401 || orderRes.data?.code === 401 || orderRes.data?.code === 40101) {
redirectToLogin()
return
} else if (orderRes.statusCode == 200 && orderRes.data.code == 200 && orderRes.data.data) {
const unpaidOrder = orderRes.data.data
uni.navigateTo({
url: `/pages/order/payment?orderId=${unpaidOrder.orderId}`
@@ -603,10 +639,10 @@
}
} catch (error) {
console.error('扫码处理失败:', error)
uni.showToast({
title: '扫码失败',
icon: 'none'
})
// uni.showToast({
// title: '扫码失败',
// icon: 'none'
// })
}
}
@@ -625,15 +661,39 @@
}
</script>
<script>
// 选用 Page 级分享钩子(uni-app 需普通 script 导出)
export default {
// 分享给朋友
onShareAppMessage() {
return {
title: '风电者 - 共享风扇暖手充电宝',
path: '/pages/index/index',
// imageUrl: '/static/logo.png'
}
},
// 朋友圈
onShareTimeline() {
return {
title: '风电者 - 共享风扇暖手充电宝',
query: '',
// imageUrl: '/static/logo.png'
}
}
}
</script>
<style lang="scss" scoped>
.container {
height: 100%;
width: 100%;
background-color: #f6f7fb;
height: 100vh;
width: 100vw;
background-color: #fff;
display: flex;
flex-direction: column;
// align-items: center;
padding-top: 20rpx;
}
.fullscreen {
padding: 0;
}
/* 顶部Logo和通知栏 */
@@ -954,6 +1014,97 @@
}
}
/* 底部操作栏 */
.bottom-actions {
position: fixed;
left: 20rpx;
right: 20rpx;
bottom: 40rpx;
z-index: 1200;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
// gap: 16rpx;
padding: 0;
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
align-content: center;
justify-content: center;
font-size: 26rpx;
font-weight: 600;
&.primary {
background: #07c160;
color: #fff;
border-radius: 64rpx;
height: 100rpx;
min-width: 320rpx;
// box-shadow: 0 16rpx 40rpx rgba(7, 193, 96, 0.35);
padding: 12rpx 24rpx;
.icon-wrap {
width: 50rpx;
height: 50rpx;
border-radius: 50%;
// background: rgba(255, 255, 255, 0.25);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10rpx;
}
}
&.secondary {
// background: rgba(255, 255, 255, 0.95);
color: #333;
border-radius: 24rpx;
height: 100rpx;
min-width: 180rpx;
// box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
padding: 12rpx 16rpx;
.icon-wrap {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background: #f7f8fa;
border: 2rpx solid #eaeaea;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10rpx;
}
}
&.small {
height: 120rpx;
min-width: 180rpx;
border-radius: 28rpx;
}
}
.btn-nearby,
.btn-my {
gap: 10rpx;
}
.btn-scan {
flex-direction: row;
gap: 14rpx;
.icon-wrap {
margin-bottom: 0;
margin-right: 12rpx;
// background: rgba(255, 255, 255, 0.25);
box-shadow: none;
}
}
}
/* 加载状态 */
.loading-overlay {
position: fixed;
@@ -1119,90 +1270,29 @@
}
}
/* 操作步骤指引 */
.steps-guide {
align-items: center;
align-content: center;
background-color: rgba(255, 255, 255, 0.95);
.action-icon {
width: 42rpx;
height: 42rpx;
filter: none;
box-shadow: none;
}
.btn-scan .action-icon {
filter: brightness(0) invert(1);
}
.action-label {
line-height: 1;
}
.map-notice {
margin: 0 20rpx;
// position: absolute;
// left: 20rpx;
// right: 20rpx;
// top: 20rpx;
border-radius: 20rpx;
padding: 0;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
z-index: 10;
// max-width: calc(100% - 40rpx);
backdrop-filter: blur(15rpx);
border: 1rpx solid rgba(255, 255, 255, 0.9);
overflow: hidden;
width: 92%;
margin: 0 auto 20rpx;
}
.guide-header {
padding: 20rpx 24rpx;
background: linear-gradient(135deg, #2196F3, #1976D2);
border-bottom: 1rpx solid rgba(255, 255, 255, 0.2);
.guide-title {
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
text-align: center;
display: block;
}
}
.steps-container {
display: flex;
flex-direction: column;
gap: 16rpx;
padding: 24rpx;
background-color: rgba(255, 255, 255, 0.9);
}
.step-item {
display: flex;
align-items: flex-start;
width: 100%;
padding: 16rpx 0;
border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
&:last-child {
border-bottom: none;
}
.step-number {
width: 40rpx;
height: 40rpx;
background: linear-gradient(135deg, #2196F3, #1976D2);
color: #ffffff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
font-weight: bold;
margin-right: 20rpx;
flex-shrink: 0;
box-shadow: 0 4rpx 12rpx rgba(33, 150, 243, 0.4);
}
.step-content {
flex: 1;
padding-top: 4rpx;
.step-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
display: block;
}
.step-desc {
font-size: 26rpx;
color: #666;
display: block;
line-height: 1.5;
}
}
z-index: 15;
}
</style>
+134
View File
@@ -0,0 +1,134 @@
<template>
<view class="legal-page">
<view class="header">
<view class="title">用户协议</view>
<view class="subtitle">适用于风电者共享风扇租借服务最后更新{{ effectiveDate }}</view>
</view>
<scroll-view class="content" scroll-y>
<view class="h1">导言</view>
<view class="p">欢迎您使用{{ brandName }}共享风扇产品与相关服务用户协议下称本协议由您与{{ companyName }}我们就您使用{{ brandName }}小程序及租借共享风扇服务所订立</view>
<view class="p">在使用{{ brandName }}请您务必仔细阅读并充分理解本协议全部内容尤其是以加粗方式提示的条款包括但不限于责任限制争议解决适用法律未成年人保护等您点击登录/使用或实际使用服务即视为您已阅读并同意受本协议约束</view>
<view class="h1">账号与登录</view>
<view class="p">2.1 您可通过微信授权登录使用本服务为完成免押租借与订单结算您同意我们基于微信支付分进行信用评估及订单后结等必要处理</view>
<view class="p">2.2 您应保证提供信息真实准确完整并及时更新因您提供的信息不真实或未及时更新导致的服务受限订单异常或损失由您自行承担</view>
<view class="p">2.3 您应对账户下的全部行为负责妥善保管设备与账户凭证不得转借出租或以其他方式提供给他人使用</view>
<view class="h1">租借与使用规范</view>
<view class="p">3.1 租借流程{{ brandName }}小程序中发起租借 在设备端取用风扇 使用完毕后按指引在归还点归还或通过快递归还</view>
<view class="p">3.2 使用规范请合理使用设备避免进水摔落私自拆卸或改装请勿靠近明火与高温环境室外雨天请避免使用儿童应在监护下使用</view>
<view class="p">3.3 禁止行为将设备用于违法或不当用途以任何方式影响设备或系统的正常运行通过非正常手段规避计费或归还流程</view>
<view class="h1">计费与结算含微信支付分</view>
<view class="p"><text class="bold">4.1 计费规则</text>以小程序展示的实时计费规则为准可能包含时长计费封顶价服务费等订单生成后将据此计费</view>
<view class="p"><text class="bold">4.2 微信支付分免押</text>若您开通并通过信用评估可享受免押租借如评估未通过可能需预授权或押金具体以页面提示为准</view>
<view class="p">4.3 结算与扣款订单结束后我们将基于实际使用情况与平台规则完成结算并通过微信支付分/微信支付进行扣款</view>
<view class="p">4.4 异常与争议如对计费或结算有异议请在订单完成后48小时内通过我的-客服提交逾期可能影响处理结果</view>
<view class="h1">设备归还与逾期处理</view>
<view class="p">5.1 归还方式按照小程序指引在指定网点归还或通过快递归还功能寄回非指定方式可能导致订单异常与额外费用</view>
<view class="p">5.2 逾期处理未在规定时间内归还的系统将持续计费或按规则收取逾期费用长时间未归还的可能依约进行赔偿处理</view>
<view class="p">5.3 验收与结单归还后平台将进行完好性验收验收完成且费用结清后订单方可完结</view>
<view class="h1">违规损坏与赔偿</view>
<view class="p">6.1 设备损坏丢失若因不当使用故意破坏或未按规范保管导致设备损坏丢失您需按平台公示标准或实际维修/折损成本承担赔偿责任</view>
<view class="p">6.2 清洁与部件因人为污损缺失附件等造成的额外成本将据实向您收取</view>
<view class="p">6.3 风险控制如出现涉嫌恶意拖欠欺诈等平台可采取冻结服务追偿依法维权等措施</view>
<view class="h1">用户行为规范</view>
<view class="p">7.1 您承诺遵守法律法规与公序良俗不发表不传播违法违规或不当内容不干扰或破坏平台与设备的正常运行</view>
<view class="p">7.2 您不得对本小程序进行反向工程抓取或未经授权的自动化访问</view>
<view class="h1">知识产权</view>
<view class="p">8.1 {{ companyName }}及关联方对本小程序与服务中的商标标识界面文字图片代码等享有相应知识产权或合法授权</view>
<view class="p">8.2 未经书面许可任何人不得以任何方式使用复制传播或改作上述内容</view>
<view class="h1">免责声明与责任限制</view>
<view class="p"><text class="bold">9.1 由于不可抗力网络故障第三方服务稳定性等原因导致的服务中断或受限{{ companyName }}在法律允许范围内不承担责任但将尽力恢复服务</text></view>
<view class="p">9.2 您应对自身使用行为负责因您违反本协议或不当保管使用设备造成的损失由您自行承担或向相关方赔偿</view>
<view class="h1">隐私与个人信息保护</view>
<view class="p">10.1 我们严格按照隐私政策处理您的个人信息包括微信登录信息手机号经您授权后获取设备与订单信息位置与网点信息等</view>
<view class="p">10.2 详情请查阅本小程序内的隐私政策</view>
<view class="h1">十一服务变更与终止</view>
<view class="p">11.1 我们可能基于业务调整法律合规或用户体验优化对服务内容功能或规则进行变更或终止重要变更将通过小程序公告或站内消息提示</view>
<view class="p">11.2 如您不同意变更可停止使用并申请注销相关账户/信息受法律法规与账务结算限制</view>
<view class="h1">十二未成年人保护</view>
<view class="p">12.1 未成年人的监护人应指导其正确理解并遵守本协议未成年人使用服务应在监护下进行避免在危险环境中使用设备</view>
<view class="h1">十三通知与联系</view>
<view class="p">13.1 联系方式请通过小程序我的-客服与我们联系我们将尽快处理您的问题或争议</view>
<view class="h1">十四法律适用与争议解决</view>
<view class="p">14.1 本协议的订立生效履行解释与争议解决适用中华人民共和国法律不含冲突规范</view>
<view class="p">14.2 因本协议产生的争议优先友好协商协商不成的提交{{ disputeVenue }}有管辖权的人民法院诉讼解决</view>
<view class="h1">十五附则</view>
<view class="p">15.1 本协议自{{ effectiveDate }}起生效并长期有效除非我们另行发布版本更新</view>
<view class="p">15.2 协议条款如被认定无效或不可执行不影响其他条款的效力与执行</view>
</scroll-view>
<view class="footer">如对本协议有疑问请前往我的-客服咨询</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
const brandName = '风电者'
const companyName = '深圳乐慕智云科技有限公司'
const effectiveDate = '2025-10-13'
const disputeVenue = '平台所在地'
</script>
<style lang="scss" scoped>
.legal-page {
min-height: 100vh;
background: #f8f8f8;
padding: 24rpx 24rpx 40rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
.header {
margin-bottom: 16rpx;
.title {
font-size: 40rpx;
font-weight: 600;
color: #222;
margin-bottom: 8rpx;
}
.subtitle {
font-size: 24rpx;
color: #888;
}
}
.content {
// width: 100%;
background: #fff;
border-radius: 16rpx;
padding: 16rpx 20rpx;
flex: 1;
min-height: 0;
box-sizing: border-box;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04);
}
.h1 { font-size: 30rpx; font-weight: 600; color: #222; margin: 18rpx 0 12rpx; }
.p { font-size: 26rpx; color: #444; line-height: 1.8; margin-bottom: 10rpx; }
.bold { font-weight: 600; color: #222; }
.footer {
text-align: center;
margin-top: 16rpx;
font-size: 24rpx;
color: #888;
}
}
</style>
+145
View File
@@ -0,0 +1,145 @@
<template>
<view class="legal-page">
<view class="header">
<view class="title">隐私政策</view>
<view class="subtitle">适用于风电者共享风扇租借服务最后更新{{ effectiveDate }}</view>
</view>
<view class="card notice">
<view class="p">我们深知个人信息对您的重要性并会尽全力保护您的个人信息安全请您在使用{{ brandName }}服务前仔细阅读并理解本隐私政策</view>
</view>
<scroll-view class="content" scroll-y>
<view class="h1">适用范围</view>
<view class="p">
本政策由{{ companyName }}制定并发布适用于{{ brandName }}小程序及其提供的共享风扇租借服务我们在本文特指{{ companyName }}并将按照合法正当必要的原则处理您的个人信息
</view>
<view class="h1">我们收集的信息</view>
<view class="p">2.1 账号信息微信登录标识如openId/unionId昵称头像经您授权手机号经您授权后通过微信获取</view>
<view class="p">2.2 订单与设备信息租借记录使用时长费用归还点位设备状态异常记录等</view>
<view class="p">2.3 位置与网点信息在您授权后用于查找附近网点与导航不会在未授权情况下获取</view>
<view class="p">2.4 日志信息为保障服务安全与稳定我们可能记录操作日志网络请求与错误信息</view>
<view class="h1">信息使用目的</view>
<view class="p">3.1 提供核心功能身份验证免押租借微信支付分评估订单计费结算客服与售后</view>
<view class="p">3.2 安全风控防范欺诈违规与风险控制保障系统与设备安全</view>
<view class="p">3.3 产品优化统计与分析以改进体验在去标识化/匿名化后进行</view>
<view class="h1">微信支付分与支付</view>
<view class="p">4.1 为实现免押租借我们将与微信支付分进行必要的数据交互如信用评估结果订单结算相关数据处理遵循微信支付与微信支付分的规则</view>
<view class="p">4.2 如您未通过评估可能需进行预授权或押金处理以页面提示为准</view>
<view class="h1">共享转移与公开披露</view>
<view class="p">5.1 我们不会向第三方出售您的个人信息</view>
<view class="p">5.2 在实现必要功能时我们可能与合作方共享必要信息如支付与物流服务商并要求其按不低于本政策的标准保护您的信息</view>
<view class="p">5.3 因合并分立重组或破产清算导致的转移我们将要求新持有方继续受本政策约束否则将重新征得您的同意</view>
<view class="p">5.4 仅在法律法规或监管要求诉讼争议处理保护人身财产安全等情形下可能依法进行披露</view>
<view class="h1">信息存储与安全</view>
<view class="p">6.1 存储地点您的个人信息原则上存储于中华人民共和国境内如需跨境传输将遵循法律法规并征得您的同意</view>
<view class="p">6.2 存储期限为实现目的所必需的最短期限超期将删除或匿名化处理法律法规另有规定的除外</view>
<view class="p">6.3 安全措施我们采取加密传输访问控制最小化授权监控审计等措施保护您的信息安全</view>
<view class="h1">您的权利</view>
<view class="p">7.1 访问与更正您可通过我的-个人信息/客服访问或更正部分信息</view>
<view class="p">7.2 删除与撤回同意在符合法律与账务结算纠纷处理等必要条件时您可申请删除或撤回授权撤回后部分功能可能无法提供</view>
<view class="p">7.3 账号注销在符合条件并完成费用结清设备归还争议处理后您可申请注销账号</view>
<view class="h1">未成年人保护</view>
<view class="p">8.1 未成年人使用服务应在监护下进行我们不会在明知的情况下收集未成年人不必要的个人信息</view>
<view class="h1">政策更新与通知</view>
<view class="p">9.1 我们可能因功能迭代法律监管变化而更新本政策重要变更将通过小程序公告或站内消息提示更新后继续使用即视为您同意</view>
<view class="h1">联系我们</view>
<view class="p">10.1 您可通过我的-客服与我们联系以行使前述权利或就本政策提出疑问</view>
</scroll-view>
<view class="footer">如对本政策有疑问请前往我的-客服咨询</view>
</view>
</template>
<script setup>
import {
ref
} from 'vue'
const brandName = '风电者'
const companyName = '深圳乐慕智云科技有限公司'
const effectiveDate = '2025-10-13'
</script>
<style lang="scss" scoped>
.legal-page {
min-height: 100vh;
background: #f8f8f8;
padding: 24rpx 24rpx 40rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
.header {
margin-bottom: 16rpx;
}
.title {
font-size: 40rpx;
font-weight: 600;
color: #222;
margin-bottom: 8rpx;
}
.subtitle {
font-size: 24rpx;
color: #888;
}
.card {
background: #fff;
border-radius: 16rpx;
padding: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
}
.notice {
margin-bottom: 16rpx;
}
.content {
background: #fff;
border-radius: 16rpx;
padding: 16rpx 20rpx;
flex: 1;
min-height: 0;
box-sizing: border-box;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
}
.h1 {
font-size: 30rpx;
font-weight: 600;
color: #222;
margin: 18rpx 0 12rpx;
}
.p {
font-size: 26rpx;
color: #444;
line-height: 1.8;
margin-bottom: 10rpx;
}
.bold {
font-weight: 600;
color: #222;
}
.footer {
text-align: center;
margin-top: 16rpx;
font-size: 24rpx;
color: #888;
}
}
</style>
+25 -14
View File
@@ -2,7 +2,7 @@
<view class="login-container">
<view class="logo">
<image src="/static/logo.png" mode="aspectFit" />
<text class="app-name">共享风扇</text>
<text class="app-name">风电者共享风扇&充电宝</text>
</view>
<view class="title">登录您的账号</view>
@@ -14,9 +14,14 @@
</button>
<!-- 仅微信登录不授权手机号时使用 -->
<button class="btn outline" @click="onWeChatLogin">仅微信登录</button>
<!-- <button class="btn outline" @click="onWeChatLogin">仅微信登录</button> -->
<view class="tips">登录即表示同意用户协议隐私政策</view>
<view class="tips">
登录即表示同意
<text class="link" @tap="go('/pages/legal/agreement')">用户协议</text>
<text class="link" @tap="go('/pages/legal/privacy')">隐私政策</text>
</view>
</view>
</template>
@@ -37,7 +42,7 @@
const target = '/pages/index/index'
const tabPages = ['/pages/index/index', '/pages/my/index']
if (tabPages.includes(target)) {
uni.switchTab({ url: target })
uni.reLaunch({ url: target })
return
}
uni.reLaunch({ url: target })
@@ -77,6 +82,10 @@
} catch (_) {}
}
})
const go = (url) => {
uni.navigateTo({ url })
}
</script>
<style lang="scss" scoped>
@@ -129,21 +138,23 @@
margin-bottom: 24rpx;
}
.primary {
background: #1976D2;
color: #fff;
}
.primary {
background: #07c160;
color: #fff;
box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.3);
}
.outline {
background: #fff;
color: #1976D2;
border: 2rpx solid #1976D2;
}
.outline {
background: #fff;
color: #07c160;
border: 2rpx solid #07c160;
}
.tips {
.tips {
margin-top: 24rpx;
font-size: 22rpx;
color: #999;
.link { color: #07c160; }
}
}
</style>
+58 -17
View File
@@ -88,6 +88,17 @@
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</view>
<view class="function-item" @click="doLogout()">
<view class="item-left">
<view class="item-icon">
<image src="/static/logout.png" mode="aspectFit"></image>
</view>
<text class="item-title">退出登录</text>
</view>
<view class="item-right">
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</view>
</view>
<!--
@@ -107,7 +118,7 @@
</view>
</view> -->
<!-- <u-popup ref="authPopup" mode="center" border-radius="15" width="600rpx" @open="onPopupOpen" @close="onPopupClose">
<!-- <u-popup ref="authPopup" mode="center" border-radius="15" width="600rpx" @open="onPopupOpen" @close="onPopupClose">
<view class="auth-popup">
<view class="auth-title">授权登录</view>
<view class="auth-desc">获取您的微信头像昵称等公开信息</view>
@@ -131,6 +142,9 @@
wxLogin,
getUserInfo
} from '../../util/index.js';
import {
userLogout
} from '@/config/user.js'
// 响应式状态
const userInfo = ref({});
@@ -147,16 +161,13 @@
// 获取用户信息
const getInfo = async () => {
try {
const token = uni.getStorageSync('token');
if (!token) {
redirectToLogin()
return
}
const res = await getUserInfo();
console.log('User info response:', res);
if (res.code == 200) {
if (res.code == 401 || res.code == 40101) {
redirectToLogin()
return
} else if (res.code == 200) {
// 保存openId
if (res.data.openId) {
openId.value = res.data.openId;
@@ -188,11 +199,16 @@
const pages = getCurrentPages()
const current = pages && pages.length ? pages[pages.length - 1] : null
const route = current && current.route ? ('/' + current.route) : '/pages/index/index'
const query = current && current.options ? Object.keys(current.options).map(k => `${k}=${encodeURIComponent(current.options[k])}`).join('&') : ''
const query = current && current.options ? Object.keys(current.options).map(k =>
`${k}=${encodeURIComponent(current.options[k])}`).join('&') : ''
const redirect = encodeURIComponent(query ? `${route}?${query}` : route)
uni.reLaunch({ url: `/pages/login/index?redirect=${redirect}` })
uni.reLaunch({
url: `/pages/login/index?redirect=${redirect}`
})
} catch (e) {
uni.reLaunch({ url: '/pages/login/index' })
uni.reLaunch({
url: '/pages/login/index'
})
}
}
@@ -345,6 +361,31 @@
// 只处理11位手机号
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}
const doLogout = async () => {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: async (res) => {
if (res.confirm) {
const response = await userLogout();
if (response.code == 200) {
uni.showToast({
title:'退出成功',
icon:'none'
})
setTimeout(() => {
uni.removeStorageSync('token');
uni.removeStorageSync('userInfo');
uni.redirectTo({
url: '/pages/login/index'
})
}, 1500)
}
}
}
})
}
</script>
<style lang="scss" scoped>
@@ -357,11 +398,11 @@
/* Header Section */
.header-section {
padding: 40rpx;
background: linear-gradient(135deg, #4facfe, #00f2fe);
background: linear-gradient(135deg, #07c160, #05a14e);
position: relative;
border-radius: 0 0 30rpx 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 10rpx 30rpx rgba(79, 172, 254, 0.2);
box-shadow: 0 10rpx 30rpx rgba(7, 193, 96, 0.25);
}
.user-profile {
@@ -437,11 +478,11 @@
.balance-amount {
font-size: 48rpx;
font-weight: 600;
color: #4facfe;
color: #07c160;
}
.action-button {
background: linear-gradient(135deg, #4facfe, #00f2fe);
background: linear-gradient(135deg, #07c160, #05a14e);
border-radius: 40rpx;
height: 80rpx;
display: flex;
@@ -451,7 +492,7 @@
color: white;
font-weight: 500;
font-size: 30rpx;
box-shadow: 0 8rpx 16rpx rgba(79, 172, 254, 0.2);
box-shadow: 0 8rpx 16rpx rgba(7, 193, 96, 0.25);
&:active {
opacity: 0.9;
@@ -566,7 +607,7 @@
}
.confirm-btn {
background: linear-gradient(135deg, #4facfe, #00f2fe);
background: linear-gradient(135deg, #07c160, #05a14e);
color: white;
}
</style>
+9 -9
View File
@@ -432,7 +432,7 @@
z-index: 10;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
.tab-item {
.tab-item {
flex: 1;
height: 90rpx;
display: flex;
@@ -443,7 +443,7 @@
position: relative;
&.active {
color: #1976D2;
color: #07c160;
font-weight: 500;
&::after {
@@ -454,7 +454,7 @@
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background: #1976D2;
background: #07c160;
border-radius: 2rpx;
}
}
@@ -494,8 +494,8 @@
color: #FF9800;
}
&.status-using {
color: #2196F3;
&.status-using {
color: #07c160;
}
&.status-finished {
@@ -647,10 +647,10 @@
justify-content: center;
margin-bottom: 10rpx;
&.primary {
background: #1976D2;
color: #fff;
}
&.primary {
background: #07c160;
color: #fff;
}
&.secondary {
background: #f5f5f5;
+79 -9
View File
@@ -177,8 +177,9 @@
currentStatusChecks: 0,
statusCheckInterval: 5000, // 5秒检查一次
isPageActive: false, // 跟踪页面是否活跃
// 倒计时与快递归还触发(默认4小时=14400秒,可被配置覆盖)
// countdownRemaining: 14400,
// 快递归还阈值(默认4小时=14400秒,可被系统配置覆盖),倒计时基于开始时间实时计算
// expressThresholdSeconds: 14400,
expressThresholdSeconds: 180,
countdownRemaining: 0,
showExpressAction: false,
countdownTimer: null
@@ -227,6 +228,13 @@
}
})
},
// 页面重新可见时,基于开始时间恢复倒计时(不重置阈值)
onShow() {
this.isPageActive = true
if (this.orderInfo.orderStatus === 'in_used') {
this.startExpressCountdown()
}
},
// 添加onHide生命周期,处理页面隐藏时的清理工作
onHide() {
console.log('归还页面隐藏,清理计时器资源和监控服务')
@@ -265,9 +273,13 @@
if (res && res.code === 200 && res.data && typeof res.data.expressReturnCountdownSeconds === 'number') {
const seconds = res.data.expressReturnCountdownSeconds
if (seconds > 0) {
this.countdownRemaining = seconds
this.expressThresholdSeconds = seconds
}
}
// 配置加载后根据开始时间重新计算倒计时
if (this.orderInfo.orderStatus === 'in_used' && this.orderInfo.startTime) {
this.recomputeExpressCountdownFromStartTime()
}
} catch (e) {
// 后端未实现或网络错误时,沿用默认
}
@@ -275,8 +287,10 @@
// 启动快递归还倒计时
startExpressCountdown() {
this.clearExpressCountdown()
this.showExpressAction = false
// 使用当前设定的倒计时(可能已被系统配置覆盖)
// 基于开始时间重新计算剩余倒计时
this.recomputeExpressCountdownFromStartTime()
// 若已到达阈值,直接展示快递归还入口
if (this.showExpressAction) return
this.countdownTimer = setInterval(() => {
if (!this.isPageActive) {
this.clearExpressCountdown()
@@ -286,10 +300,9 @@
this.clearExpressCountdown()
return
}
if (this.countdownRemaining > 0) {
this.countdownRemaining -= 1
} else {
this.showExpressAction = true
// 每秒基于开始时间重新计算,避免计时误差累积
this.recomputeExpressCountdownFromStartTime()
if (this.showExpressAction) {
this.clearExpressCountdown()
}
}, 1000)
@@ -302,6 +315,62 @@
this.countdownTimer = null
}
},
// 解析开始时间字符串为时间戳(毫秒),兼容常见格式及 iOS/微信环境
parseStartTimeToMs(timeStr) {
if (!timeStr) return NaN
if (typeof timeStr === 'number') {
return timeStr < 1e12 ? timeStr * 1000 : timeStr
}
let normalized = String(timeStr).trim()
normalized = normalized.replace('T', ' ').replace(/\.\d+Z?$/, '')
const candidates = [
normalized,
normalized.replace(/-/g, '/')
]
for (let i = 0; i < candidates.length; i++) {
const ts = Date.parse(candidates[i])
if (!isNaN(ts)) return ts
}
const m = normalized.match(/(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})\s+(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?/)
if (m) {
const y = parseInt(m[1])
const mon = parseInt(m[2]) - 1
const d = parseInt(m[3])
const h = parseInt(m[4])
const min = parseInt(m[5])
const s = m[6] ? parseInt(m[6]) : 0
return new Date(y, mon, d, h, min, s).getTime()
}
const num = Number(normalized)
if (!isNaN(num)) {
return num < 1e12 ? num * 1000 : num
}
return NaN
},
// 基于开始时间与阈值计算剩余倒计时,仅在使用中生效
recomputeExpressCountdownFromStartTime() {
if (this.orderInfo.orderStatus !== 'in_used') {
this.showExpressAction = false
this.countdownRemaining = 0
return
}
const startMs = this.parseStartTimeToMs(this.orderInfo.startTime)
if (isNaN(startMs)) {
this.showExpressAction = false
this.countdownRemaining = 0
return
}
const nowMs = Date.now()
const elapsedSeconds = Math.max(0, Math.floor((nowMs - startMs) / 1000))
const remaining = this.expressThresholdSeconds - elapsedSeconds
if (remaining <= 0) {
this.countdownRemaining = 0
this.showExpressAction = true
} else {
this.countdownRemaining = remaining
this.showExpressAction = false
}
},
// 从订单监控服务中移除当前订单
removeFromOrderMonitor() {
if (this.orderInfo.orderId && this.$orderMonitor) {
@@ -331,6 +400,7 @@
// 清理定时器
this.clearTimer()
this.clearStatusCheckTimer()
this.clearExpressCountdown()
// 显示归还成功弹窗
uni.showModal({
+13 -15
View File
@@ -56,37 +56,35 @@ export default {
}
},
onShow() {
this.checkLoginStatus()
this.loadUserInfo()
},
methods: {
async loadUserInfo() {
try {
const res = await getUserInfo()
if (res.code === 200) {
if (res.code === 401 || res.code === 40101) {
// 无提示跳转至登录
try {
const pages = getCurrentPages()
const current = pages && pages.length ? pages[pages.length - 1] : null
const route = current && current.route ? ('/' + current.route) : '/pages/index/index'
const query = current && current.options ? Object.keys(current.options).map(k => `${k}=${encodeURIComponent(current.options[k])}`).join('&') : ''
const redirect = encodeURIComponent(query ? `${route}?${query}` : route)
uni.reLaunch({ url: `/pages/login/index?redirect=${redirect}` })
} catch (e) {
uni.reLaunch({ url: '/pages/login/index' })
}
} else if (res.code === 200) {
this.userInfo = res.data
this.isLogin = true
} else {
this.isLogin = false
uni.showToast({
title: '获取用户信息失败',
icon: 'none'
})
}
} catch (error) {
console.error('加载用户信息失败:', error)
this.isLogin = false
}
},
checkLoginStatus() {
const token = uni.getStorageSync('token')
this.isLogin = !!token
if (!this.isLogin) {
uni.redirectTo({
url: '/pages/login/index'
})
}
},
navigateTo(url) {
uni.navigateTo({ url })
},