fix(tools): 修复时间格式化函数处理时区的问题
重构 formatDatetime 函数,增加对时区后缀的处理,确保时间显示为当地时间 统一使用 dayjs 进行时间格式化,提高代码可维护性
Showing
2 changed files
with
74 additions
and
49 deletions
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2024-01-15 16:25:51 | 2 | * @Date: 2024-01-15 16:25:51 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2026-01-12 22:23:03 | 4 | + * @LastEditTime: 2026-01-16 15:12:09 |
| 5 | * @FilePath: /xyxBooking-weapp/src/pages/submit/index.vue | 5 | * @FilePath: /xyxBooking-weapp/src/pages/submit/index.vue |
| 6 | * @Description: 预约人员信息 | 6 | * @Description: 预约人员信息 |
| 7 | --> | 7 | --> |
| 8 | <template> | 8 | <template> |
| 9 | - <view class="submit-page"> | 9 | + <view class="submit-page"> |
| 10 | - <view @tap="goToBooking" class="visit-time"> | 10 | + <view @tap="goToBooking" class="visit-time"> |
| 11 | - <view>参访时间</view> | 11 | + <view>参访时间</view> |
| 12 | - <view><text style="font-size: 30rpx;">{{ date }} {{ time }}</text> <IconFont name="rect-right" /></view> | 12 | + <view class="flex items-center"> |
| 13 | + <text style="font-size: 30rpx;">{{ date }} {{ time }}</text> | ||
| 14 | + <IconFont name="rect-right" class="ml-1" /> | ||
| 13 | </view> | 15 | </view> |
| 14 | - <view @tap="goToVisitor" class="add-visitors"> | 16 | + </view> |
| 15 | - <view><IconFont name="plus" /> 添加参观者</view> | 17 | + <view @tap="goToVisitor" class="add-visitors"> |
| 18 | + <view> | ||
| 19 | + <IconFont name="plus" /> 添加参观者 | ||
| 16 | </view> | 20 | </view> |
| 17 | - <view v-if="visitorList.length" class="visitors-list"> | 21 | + </view> |
| 18 | - <view v-for="(item, index) in visitorList" :key="index" @tap="addVisitor(item)" class="visitor-item"> | 22 | + <view v-if="visitorList.length" class="visitors-list"> |
| 23 | + <view v-for="(item, index) in visitorList" :key="index" @tap="addVisitor(item)" class="visitor-item"> | ||
| 19 | <view style="margin-right: 32rpx;"> | 24 | <view style="margin-right: 32rpx;"> |
| 20 | <image v-if="!checked_visitors.includes(item.id)" :src="icon_check1" style="width: 38rpx; height: 38rpx;" /> | 25 | <image v-if="!checked_visitors.includes(item.id)" :src="icon_check1" style="width: 38rpx; height: 38rpx;" /> |
| 21 | <image v-else :src="icon_check2" style="width: 38rpx; height: 38rpx;" /> | 26 | <image v-else :src="icon_check2" style="width: 38rpx; height: 38rpx;" /> |
| ... | @@ -23,12 +28,14 @@ | ... | @@ -23,12 +28,14 @@ |
| 23 | <view> | 28 | <view> |
| 24 | <view style="color: #A67939;">{{ item.name }}</view> | 29 | <view style="color: #A67939;">{{ item.name }}</view> |
| 25 | <view>证件号:{{ formatId(item.id_number) }}</view> | 30 | <view>证件号:{{ formatId(item.id_number) }}</view> |
| 26 | - <view v-if="item.is_reserve === RESERVE_STATUS.ENABLE" style="color: #9C9A9A; font-size: 26rpx;">*已预约过{{ date }}参观,请不要重复预约</view> | 31 | + <view v-if="item.is_reserve === RESERVE_STATUS.ENABLE" style="color: #9C9A9A; font-size: 26rpx;">*已预约过{{ date |
| 32 | + }}参观,请不要重复预约</view> | ||
| 27 | </view> | 33 | </view> |
| 28 | </view> | 34 | </view> |
| 29 | </view> | 35 | </view> |
| 30 | <view v-else class="no-visitors-list"> | 36 | <view v-else class="no-visitors-list"> |
| 31 | - <image src="https://cdn.ipadbiz.cn/xys/booking/%E6%9A%82%E6%97%A0@2x.png" style="width: 320rpx; height: 320rpx;" /> | 37 | + <image src="https://cdn.ipadbiz.cn/xys/booking/%E6%9A%82%E6%97%A0@2x.png" |
| 38 | + style="width: 320rpx; height: 320rpx;" /> | ||
| 32 | <view class="no-visitors-list-title">您还没有添加过参观者</view> | 39 | <view class="no-visitors-list-title">您还没有添加过参观者</view> |
| 33 | </view> | 40 | </view> |
| 34 | <view style="height: 160rpx;"></view> | 41 | <view style="height: 160rpx;"></view> |
| ... | @@ -36,7 +43,9 @@ | ... | @@ -36,7 +43,9 @@ |
| 36 | <view class="control-wrapper"> | 43 | <view class="control-wrapper"> |
| 37 | <view class="left"> | 44 | <view class="left"> |
| 38 | <view style="margin-left: 32rpx; display: flex;align-items: center;"> | 45 | <view style="margin-left: 32rpx; display: flex;align-items: center;"> |
| 39 | - 订单金额 <view style="color: #FF1919;display: inline-block;">¥<view style="font-size: 48rpx;display: inline-block;"> {{ total }}</view></view> | 46 | + 订单金额 <view style="color: #FF1919;display: inline-block;">¥<view |
| 47 | + style="font-size: 48rpx;display: inline-block;"> {{ total }}</view> | ||
| 48 | + </view> | ||
| 40 | </view> | 49 | </view> |
| 41 | </view> | 50 | </view> |
| 42 | <view @tap="submitBtn" class="right">提交订单</view> | 51 | <view @tap="submitBtn" class="right">提交订单</view> |
| ... | @@ -127,10 +136,10 @@ const submitBtn = async () => { | ... | @@ -127,10 +136,10 @@ const submitBtn = async () => { |
| 127 | if (!pay_id) { // TAG: 提交订单, 如果没有待支付订单ID, 则创建一个新的订单 | 136 | if (!pay_id) { // TAG: 提交订单, 如果没有待支付订单ID, 则创建一个新的订单 |
| 128 | Taro.showLoading({ title: '提交中...' }); | 137 | Taro.showLoading({ title: '提交中...' }); |
| 129 | const { code, data, msg } = await addReserveAPI({ | 138 | const { code, data, msg } = await addReserveAPI({ |
| 130 | - reserve_date: date.value, | 139 | + reserve_date: date.value, |
| 131 | - begin_time: time.value.split('-')[0], | 140 | + begin_time: time.value.split('-')[0], |
| 132 | - end_time: time.value.split('-')[1], | 141 | + end_time: time.value.split('-')[1], |
| 133 | - person_id_list: JSON.stringify(checked_visitors.value) | 142 | + person_id_list: JSON.stringify(checked_visitors.value) |
| 134 | }); | 143 | }); |
| 135 | 144 | ||
| 136 | Taro.hideLoading(); | 145 | Taro.hideLoading(); |
| ... | @@ -150,25 +159,25 @@ const submitBtn = async () => { | ... | @@ -150,25 +159,25 @@ const submitBtn = async () => { |
| 150 | Taro.hideLoading(); | 159 | Taro.hideLoading(); |
| 151 | 160 | ||
| 152 | if (payParams.code) { | 161 | if (payParams.code) { |
| 153 | - let pay_params = payParams.data; | 162 | + let pay_params = payParams.data; |
| 154 | - Taro.requestPayment({ | 163 | + Taro.requestPayment({ |
| 155 | - timeStamp: pay_params.timeStamp, | 164 | + timeStamp: pay_params.timeStamp, |
| 156 | - nonceStr: pay_params.nonceStr, | 165 | + nonceStr: pay_params.nonceStr, |
| 157 | - package: pay_params.package, | 166 | + package: pay_params.package, |
| 158 | - signType: pay_params.signType, | 167 | + signType: pay_params.signType, |
| 159 | - paySign: pay_params.paySign, | 168 | + paySign: pay_params.paySign, |
| 160 | - success (res) { | 169 | + success(res) { |
| 161 | - go('/success', { pay_id }); | 170 | + go('/success', { pay_id }); |
| 162 | - }, | 171 | + }, |
| 163 | - fail (res) { | 172 | + fail(res) { |
| 164 | - // 支付取消或失败,保留 pending_pay_id,允许用户再次点击按钮尝试支付同一订单 | 173 | + // 支付取消或失败,保留 pending_pay_id,允许用户再次点击按钮尝试支付同一订单 |
| 165 | - // 只有当 wxPayAPI 获取支付参数失败时(如下面的 else 分支),才重置 ID 以便重新创建订单 | 174 | + // 只有当 wxPayAPI 获取支付参数失败时(如下面的 else 分支),才重置 ID 以便重新创建订单 |
| 166 | - Taro.showToast({ title: '支付失败,请重试', icon: 'none' }); | 175 | + Taro.showToast({ title: '支付失败,请重试', icon: 'none' }); |
| 167 | - } | 176 | + } |
| 168 | - }) | 177 | + }) |
| 169 | } else { | 178 | } else { |
| 170 | - pending_pay_id.value = null; // 支付参数获取失败,重置订单ID | 179 | + pending_pay_id.value = null; // 支付参数获取失败,重置订单ID |
| 171 | - Taro.showToast({ title: payParams.msg || '获取支付信息失败', icon: 'none' }); | 180 | + Taro.showToast({ title: payParams.msg || '获取支付信息失败', icon: 'none' }); |
| 172 | } | 181 | } |
| 173 | } else { | 182 | } else { |
| 174 | // 金额等于零, 直接跳转成功页 | 183 | // 金额等于零, 直接跳转成功页 |
| ... | @@ -183,10 +192,10 @@ useDidShow(async () => { | ... | @@ -183,10 +192,10 @@ useDidShow(async () => { |
| 183 | price.value = params.price || 0; | 192 | price.value = params.price || 0; |
| 184 | 193 | ||
| 185 | if (date.value && time.value) { | 194 | if (date.value && time.value) { |
| 186 | - const { code, data } = await personListAPI({ reserve_date: date.value, begin_time: time.value.split('-')[0], end_time: time.value.split('-')[1] }); | 195 | + const { code, data } = await personListAPI({ reserve_date: date.value, begin_time: time.value.split('-')[0], end_time: time.value.split('-')[1] }); |
| 187 | - if (code) { | 196 | + if (code) { |
| 188 | - visitorList.value = data || []; | 197 | + visitorList.value = data || []; |
| 189 | - } | 198 | + } |
| 190 | } | 199 | } |
| 191 | }); | 200 | }); |
| 192 | </script> | 201 | </script> |
| ... | @@ -195,6 +204,7 @@ useDidShow(async () => { | ... | @@ -195,6 +204,7 @@ useDidShow(async () => { |
| 195 | .submit-page { | 204 | .submit-page { |
| 196 | margin: 32rpx; | 205 | margin: 32rpx; |
| 197 | position: relative; | 206 | position: relative; |
| 207 | + | ||
| 198 | .visit-time { | 208 | .visit-time { |
| 199 | background-color: #FFF; | 209 | background-color: #FFF; |
| 200 | display: flex; | 210 | display: flex; |
| ... | @@ -203,6 +213,7 @@ useDidShow(async () => { | ... | @@ -203,6 +213,7 @@ useDidShow(async () => { |
| 203 | padding: 24rpx; | 213 | padding: 24rpx; |
| 204 | border-radius: 16rpx; | 214 | border-radius: 16rpx; |
| 205 | } | 215 | } |
| 216 | + | ||
| 206 | .add-visitors { | 217 | .add-visitors { |
| 207 | border: 2rpx dashed #A67939; | 218 | border: 2rpx dashed #A67939; |
| 208 | color: #A67939; | 219 | color: #A67939; |
| ... | @@ -212,6 +223,7 @@ useDidShow(async () => { | ... | @@ -212,6 +223,7 @@ useDidShow(async () => { |
| 212 | margin: 32rpx 0; | 223 | margin: 32rpx 0; |
| 213 | font-size: 37rpx; | 224 | font-size: 37rpx; |
| 214 | } | 225 | } |
| 226 | + | ||
| 215 | .visitors-list { | 227 | .visitors-list { |
| 216 | .visitor-item { | 228 | .visitor-item { |
| 217 | background-color: #FFF; | 229 | background-color: #FFF; |
| ... | @@ -222,21 +234,25 @@ useDidShow(async () => { | ... | @@ -222,21 +234,25 @@ useDidShow(async () => { |
| 222 | align-items: center; | 234 | align-items: center; |
| 223 | } | 235 | } |
| 224 | } | 236 | } |
| 237 | + | ||
| 225 | .no-visitors-list { | 238 | .no-visitors-list { |
| 226 | display: flex; | 239 | display: flex; |
| 227 | justify-content: center; | 240 | justify-content: center; |
| 228 | align-items: center; | 241 | align-items: center; |
| 229 | flex-direction: column; | 242 | flex-direction: column; |
| 243 | + | ||
| 230 | img { | 244 | img { |
| 231 | margin-top: 32rpx; | 245 | margin-top: 32rpx; |
| 232 | margin-bottom: 32rpx; | 246 | margin-bottom: 32rpx; |
| 233 | width: 320rpx; | 247 | width: 320rpx; |
| 234 | } | 248 | } |
| 249 | + | ||
| 235 | .no-visitors-list-title { | 250 | .no-visitors-list-title { |
| 236 | color: #A67939; | 251 | color: #A67939; |
| 237 | font-size: 34rpx; | 252 | font-size: 34rpx; |
| 238 | } | 253 | } |
| 239 | } | 254 | } |
| 255 | + | ||
| 240 | .submit-wrapper { | 256 | .submit-wrapper { |
| 241 | position: fixed; | 257 | position: fixed; |
| 242 | bottom: 0; | 258 | bottom: 0; |
| ... | @@ -247,19 +263,21 @@ useDidShow(async () => { | ... | @@ -247,19 +263,21 @@ useDidShow(async () => { |
| 247 | // padding: 32rpx; | 263 | // padding: 32rpx; |
| 248 | justify-content: space-between; | 264 | justify-content: space-between; |
| 249 | flex-direction: column; | 265 | flex-direction: column; |
| 250 | - box-shadow: 0 -10rpx 8rpx 0 rgba(0,0,0,0.12); | 266 | + box-shadow: 0 -10rpx 8rpx 0 rgba(0, 0, 0, 0.12); |
| 251 | 267 | ||
| 252 | .control-wrapper { | 268 | .control-wrapper { |
| 253 | display: flex; | 269 | display: flex; |
| 254 | justify-content: space-between; | 270 | justify-content: space-between; |
| 255 | align-items: center; | 271 | align-items: center; |
| 256 | } | 272 | } |
| 273 | + | ||
| 257 | .left { | 274 | .left { |
| 258 | display: flex; | 275 | display: flex; |
| 259 | justify-content: center; | 276 | justify-content: center; |
| 260 | align-items: center; | 277 | align-items: center; |
| 261 | flex-wrap: nowrap; | 278 | flex-wrap: nowrap; |
| 262 | } | 279 | } |
| 280 | + | ||
| 263 | .right { | 281 | .right { |
| 264 | background-color: #A67939; | 282 | background-color: #A67939; |
| 265 | color: #FFF; | 283 | color: #FFF; | ... | ... |
| ... | @@ -87,20 +87,27 @@ const strExist = (array, str) => { | ... | @@ -87,20 +87,27 @@ const strExist = (array, str) => { |
| 87 | */ | 87 | */ |
| 88 | const formatDatetime = (data) => { | 88 | const formatDatetime = (data) => { |
| 89 | // 格式化日期 | 89 | // 格式化日期 |
| 90 | - if (!data) return ''; | 90 | + if (!data || !data.begin_time || !data.end_time) return ''; |
| 91 | 91 | ||
| 92 | - // 截取时间字符串最后6位(去除毫秒/时区等冗余部分) | 92 | + // 预处理函数:移除时区后缀,统一格式为 ISO 8601 (YYYY-MM-DDTHH:mm:ss) |
| 93 | - const begin_time = data?.begin_time?.slice(0, -6) || ''; | 93 | + // 这样做是为了让 dayjs 把它当作本地时间处理,避免时区转换导致的时间偏差(始终显示景点当地时间/字面时间) |
| 94 | - const end_time = data?.end_time?.slice(0, -6) || ''; | 94 | + const normalize = (timeStr) => { |
| 95 | + if (!timeStr) return ''; | ||
| 96 | + // 移除 +08, +08:00 等时区后缀 | ||
| 97 | + let clean = timeStr.split('+')[0]; | ||
| 98 | + // 移除 Z 后缀 | ||
| 99 | + clean = clean.split('Z')[0]; | ||
| 100 | + // 将空格替换为 T,确保兼容性 | ||
| 101 | + clean = clean.trim().replace(/\s+/, 'T'); | ||
| 102 | + return clean; | ||
| 103 | + }; | ||
| 95 | 104 | ||
| 96 | - // 拆分时间片段并拼接目标格式:日期 开始时间-结束时间 | 105 | + const start = dayjs(normalize(data.begin_time)); |
| 97 | - const [date, beginTime] = begin_time.split('T'); | 106 | + const end = dayjs(normalize(data.end_time)); |
| 98 | - const [, endTime] = end_time.split('T'); | ||
| 99 | 107 | ||
| 100 | - // 兼容时间拆分失败的情况,避免返回 NaN/-undefined 等异常 | 108 | + if (!start.isValid() || !end.isValid()) return ''; |
| 101 | - if (!date || !beginTime || !endTime) return ''; | ||
| 102 | 109 | ||
| 103 | - return `${date} ${beginTime}-${endTime}`; | 110 | + return `${start.format('YYYY-MM-DD')} ${start.format('HH:mm')}-${end.format('HH:mm')}`; |
| 104 | }; | 111 | }; |
| 105 | 112 | ||
| 106 | export { formatDate, wxInfo, parseQueryString, strExist, formatDatetime }; | 113 | export { formatDate, wxInfo, parseQueryString, strExist, formatDatetime }; | ... | ... |
-
Please register or login to post a comment