hookehuyr

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

新增webview页面组件,用于预览外部网页链接
添加测试页面用于验证webview功能
更新app.config.js添加webview路由
在PaymentAgreementModal组件中集成webview用于签约流程
添加bindJeePayAPI接口用于处理计全付绑定
......@@ -23,8 +23,10 @@ declare module 'vue' {
NutEllipsis: typeof import('@nutui/nutui-taro')['Ellipsis']
NutForm: typeof import('@nutui/nutui-taro')['Form']
NutFormItem: typeof import('@nutui/nutui-taro')['FormItem']
NutIcon: typeof import('@nutui/nutui-taro')['Icon']
NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview']
NutInput: typeof import('@nutui/nutui-taro')['Input']
NutLoading: typeof import('@nutui/nutui-taro')['Loading']
NutMenu: typeof import('@nutui/nutui-taro')['Menu']
NutMenuItem: typeof import('@nutui/nutui-taro')['MenuItem']
NutOverlay: typeof import('@nutui/nutui-taro')['Overlay']
......
/*
* @Date: 2023-12-22 10:29:37
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-08-07 15:28:56
* @LastEditTime: 2025-08-07 17:12:33
* @FilePath: /jgdl/src/api/index.js
* @Description: 用户相关API接口
*/
......@@ -13,6 +13,7 @@ const Api = {
UPDATE_PROFILE: '/srv/?a=user&t=update_profile',
SEND_SMS_CODE: '/srv/?a=sms_code',
GET_PROFILE: '/srv/?a=user&t=get_profile',
BIND_JEE_PAY: '/srv/?a=user&t=bind_jee_pay',
}
/**
* @description: 支付
......@@ -64,3 +65,15 @@ export const sendSmsCodeAPI = (params) => fn(fetch.get(Api.SEND_SMS_CODE, params
* @returns data[{ id,nickname,avatar_url,gender,phone,wechat_id,school_id,real_name_verified,favorite_count,order_count,follower_count }]
*/
export const getProfileAPI = (params) => fn(fetch.get(Api.GET_PROFILE, params));
/**
* @description: 绑定计全付
* @returns data.is_bind_data_complete boolean 身份信息、银行卡信息是否完整
* @returns data.is_bind_jee_pay boolean 是否绑定计全付
* @returns data.authInfo object 计全付的嘉联渠道返回的绑定信息
* @returns data.auth_sign_url string 用于和计全付的嘉联渠道签约的网址
* @returns data.auth_complement_url string 用于在计全付的嘉联渠道补全信息的连接
* @returns data.is_wait_audit boolean 是否签约待审核
* @returns data.is_finish boolean 是否签约成功
*/
export const bindJeePayAPI = (params) => fn(fetch.post(Api.BIND_JEE_PAY, params));
......
......@@ -30,6 +30,8 @@ export default {
'pages/search/index',
'pages/recommendCarList/index',
'pages/collectionSettings/index',
'pages/webview/index',
'pages/webview/test',
],
subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去
{
......
......@@ -56,7 +56,7 @@
color="orange"
@click="handleAgree"
>
同意
同意并签约
</nut-button>
<nut-button
v-else
......@@ -65,7 +65,7 @@
color="orange"
@click="handleConfirm"
>
确认
{{ bindStatus.is_finish ? '确认' : '确认并签约' }}
</nut-button>
</div>
</div>
......@@ -104,9 +104,10 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
import Taro from '@tarojs/taro'
// 导入接口
import { updateProfileAPI, getProfileAPI } from '@/api/index'
import { updateProfileAPI, getProfileAPI, bindJeePayAPI } from '@/api/index'
/**
* 用户收款说明组件
......@@ -141,6 +142,14 @@ const isChecked = ref(false)
// 是否已同意过协议(mock数据)
const hasAgreed = ref(false)
// 绑定状态,用于控制按钮文字显示
const bindStatus = ref({
is_finish: false,
is_wait_audit: false,
auth_complement_url: '',
auth_sign_url: ''
})
// 收款说明内容
const paymentDescription = ref('交易金额会在交易完成后的3个工作日内,入账至你登记的收款账户。')
......@@ -182,6 +191,29 @@ const checkAgreementStatus = async () => {
}
/**
* 检查绑定状态(仅获取状态,不执行操作)
*/
const checkBindStatus = async () => {
try {
const result = await bindJeePayAPI()
if (result.code && result.data) {
const { auth_complement_url, auth_sign_url, is_wait_audit, is_finish } = result.data
// 更新绑定状态
bindStatus.value = {
is_finish: is_finish || false,
is_wait_audit: is_wait_audit || false,
auth_complement_url: auth_complement_url || '',
auth_sign_url: auth_sign_url || ''
}
}
} catch (error) {
console.error('获取绑定状态失败:', error)
}
}
/**
* 显示支付协议
*/
const showProtocol = () => {
......@@ -206,7 +238,9 @@ const handleAgree = async () => {
if (userStore.userInfo) {
userStore.userInfo.is_signed = true
}
emit('agree')
// 调用绑定接口
await handleBindJeePay()
} else {
console.error('更新协议状态失败:', result.message)
}
......@@ -218,8 +252,97 @@ const handleAgree = async () => {
/**
* 处理确认按钮点击
*/
const handleConfirm = () => {
emit('confirm')
const handleConfirm = async () => {
// 调用绑定接口
await handleBindJeePay()
}
/**
* 处理绑定计全付接口调用
*/
const handleBindJeePay = async () => {
try {
const result = await bindJeePayAPI()
if (result.code && result.data) {
const { auth_complement_url, auth_sign_url, is_wait_audit, is_finish } = result.data
// 更新绑定状态
bindStatus.value = {
is_finish: is_finish || false,
is_wait_audit: is_wait_audit || false,
auth_complement_url: auth_complement_url || '',
auth_sign_url: auth_sign_url || ''
}
// 1. 如果返回 auth_complement_url 非空时,需要跳转到补全信息网页
if (auth_complement_url) {
Taro.showModal({
title: '提示',
content: '需要跳转到网页完善签约信息',
confirmText: '去完善',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
// 跳转到补全信息网页
Taro.navigateTo({
url: `/pages/webview/index?url=${encodeURIComponent(auth_complement_url)}&title=${encodeURIComponent('完善签约信息')}`
})
}
}
})
return
}
// 2. 如果返回 auth_sign_url 非空时,需要跳转到签约网页
if (auth_sign_url) {
Taro.showModal({
title: '提示',
content: '需要跳转到网页进行签约',
confirmText: '去签约',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
// 跳转到签约网页
Taro.navigateTo({
url: `/pages/webview/index?url=${encodeURIComponent(auth_sign_url)}&title=${encodeURIComponent('签约确认')}`
})
}
}
})
return
}
// 3. 如果返回 is_wait_audit 为 true,则提示用户签约正在等待审核
if (is_wait_audit) {
Taro.showModal({
title: '提示',
content: '签约正在等待审核,请耐心等待',
showCancel: false,
confirmText: '关闭'
})
return
}
// 4. 如果返回 is_finish 为 true 则绑定成功,可以卖车
if (is_finish) {
emit('confirm')
return
}
} else {
console.error('绑定接口调用失败:', result.message)
Taro.showToast({
title: '绑定失败,请重试',
icon: 'none'
})
}
} catch (error) {
console.error('绑定接口调用失败:', error)
Taro.showToast({
title: '网络错误,请重试',
icon: 'none'
})
}
}
/**
......@@ -229,9 +352,10 @@ const handleClose = () => {
emit('close')
}
// 组件挂载时检查协议状态
// 组件挂载时检查协议状态和绑定状态
onMounted(async () => {
checkAgreementStatus()
await checkAgreementStatus()
await checkBindStatus()
})
</script>
......
export default {
navigationBarTitleText: '网页预览',
navigationStyle: 'custom',
enablePullDownRefresh: false,
backgroundTextStyle: 'dark'
}
\ No newline at end of file
<template>
<view class="webview-container">
<!-- 导航栏 -->
<view class="nav-bar">
<view class="nav-left" @click="handleBack">
<view class="back-icon">←</view>
<text class="back-text">返回</text>
</view>
<view class="nav-title">{{ pageTitle }}</view>
<view class="nav-right"></view>
</view>
<!-- WebView内容 -->
<web-view
v-if="webUrl"
:src="webUrl"
class="web-view"
@message="handleMessage"
@load="handleLoad"
@error="handleError"
></web-view>
<!-- 加载状态 -->
<view v-if="loading" class="loading-container">
<view class="loading-spinner">⏳</view>
<view class="loading-text">加载中...</view>
</view>
<!-- 错误状态 -->
<view v-if="error" class="error-container">
<view class="error-icon">⚠️</view>
<view class="error-text">页面加载失败</view>
<nut-button type="primary" size="small" @click="handleRetry">重试</nut-button>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Taro from '@tarojs/taro'
/**
* WebView页面组件
* 用于预览外部网页链接
*/
// 页面状态
const webUrl = ref('')
const pageTitle = ref('网页预览')
const loading = ref(true)
const error = ref(false)
/**
* 获取页面参数
*/
const getPageParams = () => {
const instance = Taro.getCurrentInstance()
const params = instance.router?.params || {}
if (params.url) {
webUrl.value = decodeURIComponent(params.url)
}
if (params.title) {
pageTitle.value = decodeURIComponent(params.title)
}
}
/**
* 处理返回按钮点击
*/
const handleBack = () => {
Taro.navigateBack()
}
/**
* 处理WebView加载完成
*/
const handleLoad = (e) => {
console.log('WebView加载完成:', e)
loading.value = false
error.value = false
}
/**
* 处理WebView加载错误
*/
const handleError = (e) => {
console.error('WebView加载错误:', e)
loading.value = false
error.value = true
Taro.showToast({
title: '页面加载失败',
icon: 'none'
})
}
/**
* 处理WebView消息
*/
const handleMessage = (e) => {
console.log('WebView消息:', e)
// 可以在这里处理来自WebView的消息
}
/**
* 重试加载
*/
const handleRetry = () => {
loading.value = true
error.value = false
// 重新设置URL触发重新加载
const currentUrl = webUrl.value
webUrl.value = ''
setTimeout(() => {
webUrl.value = currentUrl
}, 100)
}
// 页面挂载时获取参数
onMounted(() => {
getPageParams()
// 如果没有URL参数,显示错误
if (!webUrl.value) {
loading.value = false
error.value = true
Taro.showToast({
title: '缺少URL参数',
icon: 'none'
})
}
})
</script>
<script>
export default {
name: 'WebViewPage'
}
</script>
<style lang="less">
.webview-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
background-color: #fff;
}
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 32rpx;
background-color: #fff;
border-bottom: 1rpx solid #eee;
position: sticky;
top: 0;
z-index: 100;
}
.nav-left {
display: flex;
align-items: center;
cursor: pointer;
.back-icon {
font-size: 32rpx;
color: #333;
font-weight: bold;
}
.back-text {
margin-left: 8rpx;
font-size: 28rpx;
color: #333;
}
}
.nav-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
text-align: center;
flex: 1;
}
.nav-right {
width: 80rpx;
}
.web-view {
flex: 1;
width: 100%;
}
.loading-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loading-spinner {
font-size: 48rpx;
margin-bottom: 16rpx;
animation: spin 1s linear infinite;
}
.loading-text {
font-size: 28rpx;
color: #666;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.error-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
}
.error-text {
font-size: 28rpx;
color: #666;
margin-bottom: 32rpx;
}
</style>
\ No newline at end of file
export default {
navigationBarTitleText: 'WebView测试页面',
enablePullDownRefresh: false,
backgroundTextStyle: 'dark'
}
\ No newline at end of file
<template>
<view class="test-container">
<view class="test-header">
<text class="test-title">WebView页面测试</text>
</view>
<view class="test-content">
<view class="test-section">
<text class="section-title">测试链接:</text>
<nut-button
type="primary"
size="small"
@click="testWebView('https://www.baidu.com', '百度搜索')"
class="test-btn"
>
测试百度
</nut-button>
<nut-button
type="success"
size="small"
@click="testWebView('https://m.taobao.com', '淘宝网')"
class="test-btn"
>
测试淘宝
</nut-button>
<nut-button
type="warning"
size="small"
@click="testWebView('https://github.com', 'GitHub')"
class="test-btn"
>
测试GitHub
</nut-button>
</view>
<view class="test-section">
<text class="section-title">自定义URL测试:</text>
<nut-input
v-model="customUrl"
placeholder="请输入要测试的URL"
class="url-input"
/>
<nut-input
v-model="customTitle"
placeholder="请输入页面标题"
class="title-input"
/>
<nut-button
type="primary"
@click="testCustomUrl"
class="test-btn"
>
测试自定义URL
</nut-button>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import Taro from '@tarojs/taro'
/**
* WebView测试页面
*/
const customUrl = ref('')
const customTitle = ref('')
/**
* 测试WebView页面
* @param {string} url - 要测试的URL
* @param {string} title - 页面标题
*/
const testWebView = (url, title) => {
Taro.navigateTo({
url: `/pages/webview/index?url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}`
})
}
/**
* 测试自定义URL
*/
const testCustomUrl = () => {
if (!customUrl.value) {
Taro.showToast({
title: '请输入URL',
icon: 'none'
})
return
}
const url = customUrl.value.startsWith('http') ? customUrl.value : `https://${customUrl.value}`
const title = customTitle.value || '自定义页面'
testWebView(url, title)
}
</script>
<script>
export default {
name: 'WebViewTest'
}
</script>
<style lang="less">
.test-container {
padding: 32rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
.test-header {
text-align: center;
margin-bottom: 48rpx;
}
.test-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.test-content {
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
}
.test-section {
margin-bottom: 48rpx;
&:last-child {
margin-bottom: 0;
}
}
.section-title {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 24rpx;
}
.test-btn {
margin-right: 16rpx;
margin-bottom: 16rpx;
}
.url-input,
.title-input {
margin-bottom: 16rpx;
}
</style>
\ No newline at end of file