hookehuyr

fix(tools): 修复时间格式化函数处理时区的问题

重构 formatDatetime 函数,增加对时区后缀的处理,确保时间显示为当地时间
统一使用 dayjs 进行时间格式化,提高代码可维护性
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>&nbsp;<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 - 订单金额&nbsp;&nbsp;<view style="color: #FF1919;display: inline-block;">¥<view style="font-size: 48rpx;display: inline-block;">&nbsp;{{ total }}</view></view> 46 + 订单金额&nbsp;&nbsp;<view style="color: #FF1919;display: inline-block;">¥<view
47 + style="font-size: 48rpx;display: inline-block;">&nbsp;{{ 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 };
......