hookehuyr

feat(webview): 添加网页预览功能及相关组件

新增webview页面组件,用于预览外部网页链接
添加测试页面用于验证webview功能
更新app.config.js添加webview路由
在PaymentAgreementModal组件中集成webview用于签约流程
添加bindJeePayAPI接口用于处理计全付绑定
...@@ -23,8 +23,10 @@ declare module 'vue' { ...@@ -23,8 +23,10 @@ declare module 'vue' {
23 NutEllipsis: typeof import('@nutui/nutui-taro')['Ellipsis'] 23 NutEllipsis: typeof import('@nutui/nutui-taro')['Ellipsis']
24 NutForm: typeof import('@nutui/nutui-taro')['Form'] 24 NutForm: typeof import('@nutui/nutui-taro')['Form']
25 NutFormItem: typeof import('@nutui/nutui-taro')['FormItem'] 25 NutFormItem: typeof import('@nutui/nutui-taro')['FormItem']
26 + NutIcon: typeof import('@nutui/nutui-taro')['Icon']
26 NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview'] 27 NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview']
27 NutInput: typeof import('@nutui/nutui-taro')['Input'] 28 NutInput: typeof import('@nutui/nutui-taro')['Input']
29 + NutLoading: typeof import('@nutui/nutui-taro')['Loading']
28 NutMenu: typeof import('@nutui/nutui-taro')['Menu'] 30 NutMenu: typeof import('@nutui/nutui-taro')['Menu']
29 NutMenuItem: typeof import('@nutui/nutui-taro')['MenuItem'] 31 NutMenuItem: typeof import('@nutui/nutui-taro')['MenuItem']
30 NutOverlay: typeof import('@nutui/nutui-taro')['Overlay'] 32 NutOverlay: typeof import('@nutui/nutui-taro')['Overlay']
......
1 /* 1 /*
2 * @Date: 2023-12-22 10:29:37 2 * @Date: 2023-12-22 10:29:37
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-08-07 15:28:56 4 + * @LastEditTime: 2025-08-07 17:12:33
5 * @FilePath: /jgdl/src/api/index.js 5 * @FilePath: /jgdl/src/api/index.js
6 * @Description: 用户相关API接口 6 * @Description: 用户相关API接口
7 */ 7 */
...@@ -13,6 +13,7 @@ const Api = { ...@@ -13,6 +13,7 @@ const Api = {
13 UPDATE_PROFILE: '/srv/?a=user&t=update_profile', 13 UPDATE_PROFILE: '/srv/?a=user&t=update_profile',
14 SEND_SMS_CODE: '/srv/?a=sms_code', 14 SEND_SMS_CODE: '/srv/?a=sms_code',
15 GET_PROFILE: '/srv/?a=user&t=get_profile', 15 GET_PROFILE: '/srv/?a=user&t=get_profile',
16 + BIND_JEE_PAY: '/srv/?a=user&t=bind_jee_pay',
16 } 17 }
17 /** 18 /**
18 * @description: 支付 19 * @description: 支付
...@@ -64,3 +65,15 @@ export const sendSmsCodeAPI = (params) => fn(fetch.get(Api.SEND_SMS_CODE, params ...@@ -64,3 +65,15 @@ export const sendSmsCodeAPI = (params) => fn(fetch.get(Api.SEND_SMS_CODE, params
64 * @returns data[{ id,nickname,avatar_url,gender,phone,wechat_id,school_id,real_name_verified,favorite_count,order_count,follower_count }] 65 * @returns data[{ id,nickname,avatar_url,gender,phone,wechat_id,school_id,real_name_verified,favorite_count,order_count,follower_count }]
65 */ 66 */
66 export const getProfileAPI = (params) => fn(fetch.get(Api.GET_PROFILE, params)); 67 export const getProfileAPI = (params) => fn(fetch.get(Api.GET_PROFILE, params));
68 +
69 +/**
70 + * @description: 绑定计全付
71 + * @returns data.is_bind_data_complete boolean 身份信息、银行卡信息是否完整
72 + * @returns data.is_bind_jee_pay boolean 是否绑定计全付
73 + * @returns data.authInfo object 计全付的嘉联渠道返回的绑定信息
74 + * @returns data.auth_sign_url string 用于和计全付的嘉联渠道签约的网址
75 + * @returns data.auth_complement_url string 用于在计全付的嘉联渠道补全信息的连接
76 + * @returns data.is_wait_audit boolean 是否签约待审核
77 + * @returns data.is_finish boolean 是否签约成功
78 + */
79 +export const bindJeePayAPI = (params) => fn(fetch.post(Api.BIND_JEE_PAY, params));
......
...@@ -30,6 +30,8 @@ export default { ...@@ -30,6 +30,8 @@ export default {
30 'pages/search/index', 30 'pages/search/index',
31 'pages/recommendCarList/index', 31 'pages/recommendCarList/index',
32 'pages/collectionSettings/index', 32 'pages/collectionSettings/index',
33 + 'pages/webview/index',
34 + 'pages/webview/test',
33 ], 35 ],
34 subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去 36 subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去
35 { 37 {
......
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
56 color="orange" 56 color="orange"
57 @click="handleAgree" 57 @click="handleAgree"
58 > 58 >
59 - 同意 59 + 同意并签约
60 </nut-button> 60 </nut-button>
61 <nut-button 61 <nut-button
62 v-else 62 v-else
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
65 color="orange" 65 color="orange"
66 @click="handleConfirm" 66 @click="handleConfirm"
67 > 67 >
68 - 确认 68 + {{ bindStatus.is_finish ? '确认' : '确认并签约' }}
69 </nut-button> 69 </nut-button>
70 </div> 70 </div>
71 </div> 71 </div>
...@@ -104,9 +104,10 @@ ...@@ -104,9 +104,10 @@
104 <script setup> 104 <script setup>
105 import { ref, computed, onMounted } from 'vue' 105 import { ref, computed, onMounted } from 'vue'
106 import { useUserStore } from '@/stores/user' 106 import { useUserStore } from '@/stores/user'
107 +import Taro from '@tarojs/taro'
107 108
108 // 导入接口 109 // 导入接口
109 -import { updateProfileAPI, getProfileAPI } from '@/api/index' 110 +import { updateProfileAPI, getProfileAPI, bindJeePayAPI } from '@/api/index'
110 111
111 /** 112 /**
112 * 用户收款说明组件 113 * 用户收款说明组件
...@@ -141,6 +142,14 @@ const isChecked = ref(false) ...@@ -141,6 +142,14 @@ const isChecked = ref(false)
141 // 是否已同意过协议(mock数据) 142 // 是否已同意过协议(mock数据)
142 const hasAgreed = ref(false) 143 const hasAgreed = ref(false)
143 144
145 +// 绑定状态,用于控制按钮文字显示
146 +const bindStatus = ref({
147 + is_finish: false,
148 + is_wait_audit: false,
149 + auth_complement_url: '',
150 + auth_sign_url: ''
151 +})
152 +
144 // 收款说明内容 153 // 收款说明内容
145 const paymentDescription = ref('交易金额会在交易完成后的3个工作日内,入账至你登记的收款账户。') 154 const paymentDescription = ref('交易金额会在交易完成后的3个工作日内,入账至你登记的收款账户。')
146 155
...@@ -182,6 +191,29 @@ const checkAgreementStatus = async () => { ...@@ -182,6 +191,29 @@ const checkAgreementStatus = async () => {
182 } 191 }
183 192
184 /** 193 /**
194 + * 检查绑定状态(仅获取状态,不执行操作)
195 + */
196 +const checkBindStatus = async () => {
197 + try {
198 + const result = await bindJeePayAPI()
199 +
200 + if (result.code && result.data) {
201 + const { auth_complement_url, auth_sign_url, is_wait_audit, is_finish } = result.data
202 +
203 + // 更新绑定状态
204 + bindStatus.value = {
205 + is_finish: is_finish || false,
206 + is_wait_audit: is_wait_audit || false,
207 + auth_complement_url: auth_complement_url || '',
208 + auth_sign_url: auth_sign_url || ''
209 + }
210 + }
211 + } catch (error) {
212 + console.error('获取绑定状态失败:', error)
213 + }
214 +}
215 +
216 +/**
185 * 显示支付协议 217 * 显示支付协议
186 */ 218 */
187 const showProtocol = () => { 219 const showProtocol = () => {
...@@ -206,7 +238,9 @@ const handleAgree = async () => { ...@@ -206,7 +238,9 @@ const handleAgree = async () => {
206 if (userStore.userInfo) { 238 if (userStore.userInfo) {
207 userStore.userInfo.is_signed = true 239 userStore.userInfo.is_signed = true
208 } 240 }
209 - emit('agree') 241 +
242 + // 调用绑定接口
243 + await handleBindJeePay()
210 } else { 244 } else {
211 console.error('更新协议状态失败:', result.message) 245 console.error('更新协议状态失败:', result.message)
212 } 246 }
...@@ -218,8 +252,97 @@ const handleAgree = async () => { ...@@ -218,8 +252,97 @@ const handleAgree = async () => {
218 /** 252 /**
219 * 处理确认按钮点击 253 * 处理确认按钮点击
220 */ 254 */
221 -const handleConfirm = () => { 255 +const handleConfirm = async () => {
256 + // 调用绑定接口
257 + await handleBindJeePay()
258 +}
259 +
260 +/**
261 + * 处理绑定计全付接口调用
262 + */
263 +const handleBindJeePay = async () => {
264 + try {
265 + const result = await bindJeePayAPI()
266 +
267 + if (result.code && result.data) {
268 + const { auth_complement_url, auth_sign_url, is_wait_audit, is_finish } = result.data
269 +
270 + // 更新绑定状态
271 + bindStatus.value = {
272 + is_finish: is_finish || false,
273 + is_wait_audit: is_wait_audit || false,
274 + auth_complement_url: auth_complement_url || '',
275 + auth_sign_url: auth_sign_url || ''
276 + }
277 +
278 + // 1. 如果返回 auth_complement_url 非空时,需要跳转到补全信息网页
279 + if (auth_complement_url) {
280 + Taro.showModal({
281 + title: '提示',
282 + content: '需要跳转到网页完善签约信息',
283 + confirmText: '去完善',
284 + cancelText: '取消',
285 + success: (res) => {
286 + if (res.confirm) {
287 + // 跳转到补全信息网页
288 + Taro.navigateTo({
289 + url: `/pages/webview/index?url=${encodeURIComponent(auth_complement_url)}&title=${encodeURIComponent('完善签约信息')}`
290 + })
291 + }
292 + }
293 + })
294 + return
295 + }
296 +
297 + // 2. 如果返回 auth_sign_url 非空时,需要跳转到签约网页
298 + if (auth_sign_url) {
299 + Taro.showModal({
300 + title: '提示',
301 + content: '需要跳转到网页进行签约',
302 + confirmText: '去签约',
303 + cancelText: '取消',
304 + success: (res) => {
305 + if (res.confirm) {
306 + // 跳转到签约网页
307 + Taro.navigateTo({
308 + url: `/pages/webview/index?url=${encodeURIComponent(auth_sign_url)}&title=${encodeURIComponent('签约确认')}`
309 + })
310 + }
311 + }
312 + })
313 + return
314 + }
315 +
316 + // 3. 如果返回 is_wait_audit 为 true,则提示用户签约正在等待审核
317 + if (is_wait_audit) {
318 + Taro.showModal({
319 + title: '提示',
320 + content: '签约正在等待审核,请耐心等待',
321 + showCancel: false,
322 + confirmText: '关闭'
323 + })
324 + return
325 + }
326 +
327 + // 4. 如果返回 is_finish 为 true 则绑定成功,可以卖车
328 + if (is_finish) {
222 emit('confirm') 329 emit('confirm')
330 + return
331 + }
332 + } else {
333 + console.error('绑定接口调用失败:', result.message)
334 + Taro.showToast({
335 + title: '绑定失败,请重试',
336 + icon: 'none'
337 + })
338 + }
339 + } catch (error) {
340 + console.error('绑定接口调用失败:', error)
341 + Taro.showToast({
342 + title: '网络错误,请重试',
343 + icon: 'none'
344 + })
345 + }
223 } 346 }
224 347
225 /** 348 /**
...@@ -229,9 +352,10 @@ const handleClose = () => { ...@@ -229,9 +352,10 @@ const handleClose = () => {
229 emit('close') 352 emit('close')
230 } 353 }
231 354
232 -// 组件挂载时检查协议状态 355 +// 组件挂载时检查协议状态和绑定状态
233 onMounted(async () => { 356 onMounted(async () => {
234 - checkAgreementStatus() 357 + await checkAgreementStatus()
358 + await checkBindStatus()
235 }) 359 })
236 </script> 360 </script>
237 361
......
1 +export default {
2 + navigationBarTitleText: '网页预览',
3 + navigationStyle: 'custom',
4 + enablePullDownRefresh: false,
5 + backgroundTextStyle: 'dark'
6 +}
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <view class="webview-container">
3 + <!-- 导航栏 -->
4 + <view class="nav-bar">
5 + <view class="nav-left" @click="handleBack">
6 + <view class="back-icon">←</view>
7 + <text class="back-text">返回</text>
8 + </view>
9 + <view class="nav-title">{{ pageTitle }}</view>
10 + <view class="nav-right"></view>
11 + </view>
12 +
13 + <!-- WebView内容 -->
14 + <web-view
15 + v-if="webUrl"
16 + :src="webUrl"
17 + class="web-view"
18 + @message="handleMessage"
19 + @load="handleLoad"
20 + @error="handleError"
21 + ></web-view>
22 +
23 + <!-- 加载状态 -->
24 + <view v-if="loading" class="loading-container">
25 + <view class="loading-spinner">⏳</view>
26 + <view class="loading-text">加载中...</view>
27 + </view>
28 +
29 + <!-- 错误状态 -->
30 + <view v-if="error" class="error-container">
31 + <view class="error-icon">⚠️</view>
32 + <view class="error-text">页面加载失败</view>
33 + <nut-button type="primary" size="small" @click="handleRetry">重试</nut-button>
34 + </view>
35 + </view>
36 +</template>
37 +
38 +<script setup>
39 +import { ref, onMounted } from 'vue'
40 +import Taro from '@tarojs/taro'
41 +
42 +/**
43 + * WebView页面组件
44 + * 用于预览外部网页链接
45 + */
46 +
47 +// 页面状态
48 +const webUrl = ref('')
49 +const pageTitle = ref('网页预览')
50 +const loading = ref(true)
51 +const error = ref(false)
52 +
53 +/**
54 + * 获取页面参数
55 + */
56 +const getPageParams = () => {
57 + const instance = Taro.getCurrentInstance()
58 + const params = instance.router?.params || {}
59 +
60 + if (params.url) {
61 + webUrl.value = decodeURIComponent(params.url)
62 + }
63 +
64 + if (params.title) {
65 + pageTitle.value = decodeURIComponent(params.title)
66 + }
67 +}
68 +
69 +/**
70 + * 处理返回按钮点击
71 + */
72 +const handleBack = () => {
73 + Taro.navigateBack()
74 +}
75 +
76 +/**
77 + * 处理WebView加载完成
78 + */
79 +const handleLoad = (e) => {
80 + console.log('WebView加载完成:', e)
81 + loading.value = false
82 + error.value = false
83 +}
84 +
85 +/**
86 + * 处理WebView加载错误
87 + */
88 +const handleError = (e) => {
89 + console.error('WebView加载错误:', e)
90 + loading.value = false
91 + error.value = true
92 +
93 + Taro.showToast({
94 + title: '页面加载失败',
95 + icon: 'none'
96 + })
97 +}
98 +
99 +/**
100 + * 处理WebView消息
101 + */
102 +const handleMessage = (e) => {
103 + console.log('WebView消息:', e)
104 + // 可以在这里处理来自WebView的消息
105 +}
106 +
107 +/**
108 + * 重试加载
109 + */
110 +const handleRetry = () => {
111 + loading.value = true
112 + error.value = false
113 + // 重新设置URL触发重新加载
114 + const currentUrl = webUrl.value
115 + webUrl.value = ''
116 + setTimeout(() => {
117 + webUrl.value = currentUrl
118 + }, 100)
119 +}
120 +
121 +// 页面挂载时获取参数
122 +onMounted(() => {
123 + getPageParams()
124 +
125 + // 如果没有URL参数,显示错误
126 + if (!webUrl.value) {
127 + loading.value = false
128 + error.value = true
129 + Taro.showToast({
130 + title: '缺少URL参数',
131 + icon: 'none'
132 + })
133 + }
134 +})
135 +</script>
136 +
137 +<script>
138 +export default {
139 + name: 'WebViewPage'
140 +}
141 +</script>
142 +
143 +<style lang="less">
144 +.webview-container {
145 + width: 100%;
146 + height: 100vh;
147 + display: flex;
148 + flex-direction: column;
149 + background-color: #fff;
150 +}
151 +
152 +.nav-bar {
153 + display: flex;
154 + align-items: center;
155 + justify-content: space-between;
156 + height: 88rpx;
157 + padding: 0 32rpx;
158 + background-color: #fff;
159 + border-bottom: 1rpx solid #eee;
160 + position: sticky;
161 + top: 0;
162 + z-index: 100;
163 +}
164 +
165 +.nav-left {
166 + display: flex;
167 + align-items: center;
168 + cursor: pointer;
169 +
170 + .back-icon {
171 + font-size: 32rpx;
172 + color: #333;
173 + font-weight: bold;
174 + }
175 +
176 + .back-text {
177 + margin-left: 8rpx;
178 + font-size: 28rpx;
179 + color: #333;
180 + }
181 +}
182 +
183 +.nav-title {
184 + font-size: 32rpx;
185 + font-weight: 600;
186 + color: #333;
187 + text-align: center;
188 + flex: 1;
189 +}
190 +
191 +.nav-right {
192 + width: 80rpx;
193 +}
194 +
195 +.web-view {
196 + flex: 1;
197 + width: 100%;
198 +}
199 +
200 +.loading-container {
201 + position: absolute;
202 + top: 50%;
203 + left: 50%;
204 + transform: translate(-50%, -50%);
205 + display: flex;
206 + flex-direction: column;
207 + align-items: center;
208 + justify-content: center;
209 +}
210 +
211 +.loading-spinner {
212 + font-size: 48rpx;
213 + margin-bottom: 16rpx;
214 + animation: spin 1s linear infinite;
215 +}
216 +
217 +.loading-text {
218 + font-size: 28rpx;
219 + color: #666;
220 +}
221 +
222 +@keyframes spin {
223 + 0% { transform: rotate(0deg); }
224 + 100% { transform: rotate(360deg); }
225 +}
226 +
227 +.error-container {
228 + position: absolute;
229 + top: 50%;
230 + left: 50%;
231 + transform: translate(-50%, -50%);
232 + display: flex;
233 + flex-direction: column;
234 + align-items: center;
235 + justify-content: center;
236 + text-align: center;
237 +}
238 +
239 +.error-icon {
240 + font-size: 80rpx;
241 + margin-bottom: 24rpx;
242 +}
243 +
244 +.error-text {
245 + font-size: 28rpx;
246 + color: #666;
247 + margin-bottom: 32rpx;
248 +}
249 +</style>
...\ No newline at end of file ...\ No newline at end of file
1 +export default {
2 + navigationBarTitleText: 'WebView测试页面',
3 + enablePullDownRefresh: false,
4 + backgroundTextStyle: 'dark'
5 +}
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <view class="test-container">
3 + <view class="test-header">
4 + <text class="test-title">WebView页面测试</text>
5 + </view>
6 +
7 + <view class="test-content">
8 + <view class="test-section">
9 + <text class="section-title">测试链接:</text>
10 + <nut-button
11 + type="primary"
12 + size="small"
13 + @click="testWebView('https://www.baidu.com', '百度搜索')"
14 + class="test-btn"
15 + >
16 + 测试百度
17 + </nut-button>
18 +
19 + <nut-button
20 + type="success"
21 + size="small"
22 + @click="testWebView('https://m.taobao.com', '淘宝网')"
23 + class="test-btn"
24 + >
25 + 测试淘宝
26 + </nut-button>
27 +
28 + <nut-button
29 + type="warning"
30 + size="small"
31 + @click="testWebView('https://github.com', 'GitHub')"
32 + class="test-btn"
33 + >
34 + 测试GitHub
35 + </nut-button>
36 + </view>
37 +
38 + <view class="test-section">
39 + <text class="section-title">自定义URL测试:</text>
40 + <nut-input
41 + v-model="customUrl"
42 + placeholder="请输入要测试的URL"
43 + class="url-input"
44 + />
45 + <nut-input
46 + v-model="customTitle"
47 + placeholder="请输入页面标题"
48 + class="title-input"
49 + />
50 + <nut-button
51 + type="primary"
52 + @click="testCustomUrl"
53 + class="test-btn"
54 + >
55 + 测试自定义URL
56 + </nut-button>
57 + </view>
58 + </view>
59 + </view>
60 +</template>
61 +
62 +<script setup>
63 +import { ref } from 'vue'
64 +import Taro from '@tarojs/taro'
65 +
66 +/**
67 + * WebView测试页面
68 + */
69 +
70 +const customUrl = ref('')
71 +const customTitle = ref('')
72 +
73 +/**
74 + * 测试WebView页面
75 + * @param {string} url - 要测试的URL
76 + * @param {string} title - 页面标题
77 + */
78 +const testWebView = (url, title) => {
79 + Taro.navigateTo({
80 + url: `/pages/webview/index?url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}`
81 + })
82 +}
83 +
84 +/**
85 + * 测试自定义URL
86 + */
87 +const testCustomUrl = () => {
88 + if (!customUrl.value) {
89 + Taro.showToast({
90 + title: '请输入URL',
91 + icon: 'none'
92 + })
93 + return
94 + }
95 +
96 + const url = customUrl.value.startsWith('http') ? customUrl.value : `https://${customUrl.value}`
97 + const title = customTitle.value || '自定义页面'
98 +
99 + testWebView(url, title)
100 +}
101 +</script>
102 +
103 +<script>
104 +export default {
105 + name: 'WebViewTest'
106 +}
107 +</script>
108 +
109 +<style lang="less">
110 +.test-container {
111 + padding: 32rpx;
112 + background-color: #f5f5f5;
113 + min-height: 100vh;
114 +}
115 +
116 +.test-header {
117 + text-align: center;
118 + margin-bottom: 48rpx;
119 +}
120 +
121 +.test-title {
122 + font-size: 36rpx;
123 + font-weight: 600;
124 + color: #333;
125 +}
126 +
127 +.test-content {
128 + background-color: #fff;
129 + border-radius: 16rpx;
130 + padding: 32rpx;
131 +}
132 +
133 +.test-section {
134 + margin-bottom: 48rpx;
135 +
136 + &:last-child {
137 + margin-bottom: 0;
138 + }
139 +}
140 +
141 +.section-title {
142 + display: block;
143 + font-size: 28rpx;
144 + font-weight: 600;
145 + color: #333;
146 + margin-bottom: 24rpx;
147 +}
148 +
149 +.test-btn {
150 + margin-right: 16rpx;
151 + margin-bottom: 16rpx;
152 +}
153 +
154 +.url-input,
155 +.title-input {
156 + margin-bottom: 16rpx;
157 +}
158 +</style>
...\ No newline at end of file ...\ No newline at end of file