更新版

This commit is contained in:
tianfeng 2025-08-15 10:30:12 +08:00
parent aa9c365cdf
commit 11c24e9d0c
8 changed files with 711 additions and 49 deletions

271
public/app-bridge-test.html Normal file
View File

@ -0,0 +1,271 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>APP Bridge Test</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
padding: 20px;
background-color: #f5f5f5;
}
.test-container {
background: white;
padding: 20px;
border-radius: 8px;
max-width: 600px;
margin: 0 auto;
}
.test-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #e0e0e0;
border-radius: 5px;
}
.test-section h3 {
margin-top: 0;
color: #333;
}
button {
background: #007AFF;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #005bb5;
}
.result {
background: #f0f0f0;
padding: 10px;
border-radius: 5px;
margin-top: 10px;
white-space: pre-wrap;
font-family: monospace;
}
.status {
display: inline-block;
padding: 5px 10px;
border-radius: 3px;
margin: 5px;
font-weight: bold;
}
.status.success { background: #d4edda; color: #155724; }
.status.error { background: #f8d7da; color: #721c24; }
.status.warning { background: #fff3cd; color: #856404; }
</style>
</head>
<body>
<div class="test-container">
<h1>APP Bridge 通信测试</h1>
<div class="test-section">
<h3>环境检测</h3>
<div>
<span class="status" id="env-status">检测中...</span>
<span id="env-details"></span>
</div>
<div class="result" id="env-result"></div>
</div>
<div class="test-section">
<h3>APP对象检测</h3>
<button onclick="checkAppObject()">检测APP对象</button>
<div class="result" id="app-object-result"></div>
</div>
<div class="test-section">
<h3>模拟APP调用</h3>
<button onclick="simulateAppCall()">模拟Android APP</button>
<button onclick="simulateIOSCall()">模拟iOS APP</button>
<button onclick="simulateWebCall()">模拟Web环境</button>
<div class="result" id="simulate-result"></div>
</div>
<div class="test-section">
<h3>连接APP测试</h3>
<button onclick="testAppConnection()">测试连接</button>
<div class="result" id="connection-result"></div>
</div>
</div>
<script>
// 检测环境
function detectEnvironment() {
const userAgent = navigator.userAgent;
const isIOS = /iPad|iPhone|iPod/.test(userAgent);
const isAndroid = /Android/.test(userAgent);
const hasAppObject = !!(window.app);
const hasWebkitObject = !!(window.webkit && window.webkit.messageHandlers);
const envStatus = document.getElementById('env-status');
const envDetails = document.getElementById('env-details');
const envResult = document.getElementById('env-result');
let status = 'warning';
let details = '未知环境';
if (isIOS) {
details = 'iOS 环境';
status = hasWebkitObject ? 'success' : 'error';
} else if (isAndroid) {
details = 'Android 环境';
status = hasAppObject ? 'success' : 'error';
} else {
details = 'Web/桌面环境';
status = 'warning';
}
envStatus.className = `status ${status}`;
envStatus.textContent = status === 'success' ? '✓ 支持' : status === 'error' ? '✗ 不支持' : '⚠ 警告';
envDetails.textContent = details;
envResult.textContent = JSON.stringify({
userAgent: userAgent,
isIOS: isIOS,
isAndroid: isAndroid,
hasAppObject: hasAppObject,
hasWebkitObject: hasWebkitObject
}, null, 2);
}
// 检测APP对象
function checkAppObject() {
const result = document.getElementById('app-object-result');
const appInfo = {};
if (window.app) {
appInfo.app = {
exists: true,
methods: Object.getOwnPropertyNames(window.app)
};
if (typeof window.app.getAccessOrigin === 'function') {
try {
appInfo.app.getAccessOriginResult = 'Function available';
} catch (e) {
appInfo.app.getAccessOriginResult = 'Error: ' + e.message;
}
}
} else {
appInfo.app = { exists: false };
}
if (window.webkit && window.webkit.messageHandlers) {
appInfo.webkit = {
exists: true,
messageHandlers: Object.keys(window.webkit.messageHandlers)
};
} else {
appInfo.webkit = { exists: false };
}
result.textContent = JSON.stringify(appInfo, null, 2);
}
// 模拟APP调用
function simulateAppCall() {
const mockData = JSON.stringify({
'Authorization': 'Bearer MOCK_TOKEN_12345',
'Req-Lang': 'en-US',
'Req-App-Intel': 'version=2.1.0;build=100;channel=official;Req-Imei=mock_imei_12345',
'Req-Sys-Origin': 'origin=android;child=phone'
});
// 创建模拟APP对象
window.app = {
getAccessOrigin: function() {
return mockData;
}
};
document.getElementById('simulate-result').textContent =
'Android APP对象已模拟创建\n' +
'Mock数据: ' + mockData;
}
function simulateIOSCall() {
const mockData = JSON.stringify({
'Authorization': 'Bearer MOCK_IOS_TOKEN_67890',
'Req-Lang': 'zh-CN',
'Req-App-Intel': 'version=2.1.0;build=100;channel=appstore;Req-Imei=mock_ios_imei',
'Req-Sys-Origin': 'origin=ios;child=iphone'
});
// 创建模拟WebKit对象
window.webkit = {
messageHandlers: {
getAccessOrigin: {
postMessage: function(message) {
// 模拟异步回调
setTimeout(() => {
if (window.renderData) {
window.renderData(mockData);
}
}, 100);
}
}
}
};
document.getElementById('simulate-result').textContent =
'iOS WebKit对象已模拟创建\n' +
'Mock数据: ' + mockData;
}
function simulateWebCall() {
// 清除模拟对象
delete window.app;
delete window.webkit;
document.getElementById('simulate-result').textContent =
'Web环境已模拟无APP对象';
}
// 测试APP连接
function testAppConnection() {
const result = document.getElementById('connection-result');
result.textContent = '正在测试连接...\n';
// 导入并测试APP桥接模块
import('/src/utils/appBridge.js').then(bridge => {
result.textContent += '✓ APP桥接模块加载成功\n';
const isInApp = bridge.isInApp();
result.textContent += `APP环境检测: ${isInApp}\n`;
// 测试连接
bridge.connectApplication((access) => {
result.textContent += '✓ 收到APP回调\n';
result.textContent += `原始数据: ${access}\n`;
const parseResult = bridge.parseAccessOrigin(access);
if (parseResult.success) {
result.textContent += '✓ 数据解析成功\n';
const headerInfo = bridge.parseHeader(parseResult.data);
result.textContent += '✓ 头部信息解析成功\n';
result.textContent += `解析结果: ${JSON.stringify(headerInfo, null, 2)}\n`;
bridge.setHttpHeaders(headerInfo);
result.textContent += '✓ HTTP头部设置完成\n';
} else {
result.textContent += `✗ 数据解析失败: ${parseResult.error}\n`;
}
});
}).catch(error => {
result.textContent += `✗ 模块加载失败: ${error.message}\n`;
console.error('Error loading app bridge:', error);
});
}
// 页面加载时自动检测环境
document.addEventListener('DOMContentLoaded', detectEnvironment);
</script>
</body>
</html>

View File

@ -95,6 +95,11 @@ const router = createRouter({
name: 'agency-center',
component: () => import('../views/AgencyCenterView.vue'),
},
{
path: '/not_app',
name: 'not-app',
component: () => import('../views/NotAppView.vue'),
},
],
})

162
src/utils/appBridge.js Normal file
View File

@ -0,0 +1,162 @@
/**
* APP桥接工具函数
* 用于H5页面与原生APP之间的通信
*/
/**
* 检测是否为iOS系统
* @returns {boolean}
*/
export function ios() {
return /iPad|iPhone|iPod/.test(navigator.userAgent)
}
/**
* 检测是否为Android系统
* @returns {boolean}
*/
export function android() {
return /Android/.test(navigator.userAgent)
}
/**
* 检测是否在APP环境中
* @returns {boolean}
*/
export function isInApp() {
return !!(window.app || window.webkit)
}
/**
* 连接APP并获取访问参数
* @param {Function} renderFun 回调函数接收APP传递的access参数
*/
export function connectApplication(renderFun) {
window.renderData = renderFun
// 兼容历史版本
window.getIosAccessOriginParam = renderFun
try {
if (ios()) {
// iOS可能需要特殊处理这里先使用通用方法
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.getAccessOrigin) {
window.webkit.messageHandlers.getAccessOrigin.postMessage({})
} else if (window.app && window.app.getAccessOrigin) {
renderFun(window.app.getAccessOrigin())
}
} else if (android()) {
// Android处理
if (window.app && window.app.getAccessOrigin) {
renderFun(window.app.getAccessOrigin())
}
} else {
// 非APP环境可能是浏览器调试
console.warn('Not in APP environment, using mock data for development')
// 开发环境可以提供模拟数据
if (process.env.NODE_ENV === 'development') {
const mockAccess = JSON.stringify({
'Authorization': 'Bearer mock-token-for-development',
'Req-Lang': 'en',
'Req-App-Intel': 'build=1.0;version=2.1;channel=official;Req-Imei=mock-imei',
'Req-Sys-Origin': 'origin=web;child=browser'
})
setTimeout(() => renderFun(mockAccess), 100)
}
}
} catch (error) {
console.error('Failed to connect to APP:', error)
}
}
/**
* 解析APP传递的访问参数
* @param {string} access JSON字符串格式的访问参数
* @returns {Object} 解析后的参数对象
*/
export function parseAccessOrigin(access) {
try {
const accessParam = JSON.parse(access)
return {
success: true,
data: accessParam
}
} catch (error) {
console.error('Failed to parse access origin:', error)
return {
success: false,
error: 'accessOrigin Error.',
data: null
}
}
}
/**
* 解析请求头信息
* @param {Object} head 请求头对象
* @returns {Object} 解析后的头信息
*/
export function parseHeader(head) {
if (!head) {
return {}
}
/**
* 解析键值对字符串 (: "key1=value1;key2=value2")
* @param {string} values
* @returns {Object}
*/
function getKeyValObj(values) {
const keyValBody = {}
if (!values) return keyValBody
const valueArray = values.split(';')
for (let index = 0; index < valueArray.length; index++) {
const keyVal = valueArray[index]
if (keyVal && keyVal.includes('=')) {
const keyValArray = keyVal.split('=')
keyValBody[keyValArray[0]] = keyValArray[1]
}
}
return keyValBody
}
const reqAppIntel = head['Req-App-Intel'] || ''
const reqSysOrigin = head['Req-Sys-Origin'] || ''
const reqAppIntelObj = getKeyValObj(reqAppIntel)
const reqSysOriginObj = getKeyValObj(reqSysOrigin)
return {
reqLang: head['Req-Lang'],
authorization: head['Authorization'],
buildVersion: reqAppIntelObj['build'],
appVersion: reqAppIntelObj['version'],
appChannel: reqAppIntelObj['channel'],
reqImei: reqAppIntelObj['Req-Imei'],
sysOrigin: reqSysOriginObj['origin'],
sysOriginChild: reqSysOriginObj['child']
}
}
/**
* 设置HTTP请求的默认头部信息
* @param {Object} headerInfo 解析后的头部信息
*/
export function setHttpHeaders(headerInfo) {
// 动态导入http.js中的函数来避免循环依赖
import('../utils/http.js').then(httpModule => {
if (httpModule.setHeadersFromApp) {
httpModule.setHeadersFromApp(headerInfo)
}
}).catch(error => {
console.error('Failed to update HTTP headers:', error)
})
console.log('🔑 Authorization token set:', headerInfo.authorization)
console.log('🌍 Language set:', headerInfo.reqLang)
console.log('📱 App info:', {
version: headerInfo.appVersion,
build: headerInfo.buildVersion,
channel: headerInfo.appChannel,
system: headerInfo.sysOrigin
})
}

View File

@ -1,8 +1,8 @@
// HTTP请求配置
const API_BASE_URL = 'https://api.likeichat.com'
// 公共请求头配置
const COMMON_HEADERS = {
// 默认公共请求头配置
let COMMON_HEADERS = {
'authorization': 'Bearer F77126B08E13987C3AD88B6015C5B777.djIlM0ExOTU0MDU5NzkzOTYzMzkzMDI1JTNBTElLRUklM0ExNzU3NzYzODU5NzE1JTNBMTc1NTE3MTg1OTcxNQ==',
'req-app-intel': 'version=1.0.0;build=1;model=SM-G9550;sysVersion=9;channel=Google',
'req-client': 'Android',
@ -13,6 +13,64 @@ const COMMON_HEADERS = {
'req-zone': 'Asia/Shanghai',
}
/**
* 更新公共请求头
* @param {object} headers - 要更新的请求头
*/
export function updateCommonHeaders(headers) {
COMMON_HEADERS = {
...COMMON_HEADERS,
...headers
}
console.log('🔄 Updated common headers:', COMMON_HEADERS)
}
/**
* 从APP头部信息设置HTTP请求头
* @param {object} headerInfo - APP传递的头部信息
*/
export function setHeadersFromApp(headerInfo) {
const updatedHeaders = {}
// 设置Authorization
if (headerInfo.authorization) {
updatedHeaders.authorization = headerInfo.authorization
}
// 设置语言
if (headerInfo.reqLang) {
updatedHeaders['req-lang'] = headerInfo.reqLang
}
// 设置APP信息
if (headerInfo.appVersion || headerInfo.buildVersion || headerInfo.appChannel) {
const appIntelParts = []
if (headerInfo.appVersion) appIntelParts.push(`version=${headerInfo.appVersion}`)
if (headerInfo.buildVersion) appIntelParts.push(`build=${headerInfo.buildVersion}`)
if (headerInfo.appChannel) appIntelParts.push(`channel=${headerInfo.appChannel}`)
if (appIntelParts.length > 0) {
updatedHeaders['req-app-intel'] = appIntelParts.join(';')
}
}
// 设置设备信息
if (headerInfo.reqImei) {
updatedHeaders['req-imei'] = headerInfo.reqImei
}
// 设置系统来源
if (headerInfo.sysOrigin) {
const sysOriginParts = [`origin=${headerInfo.sysOrigin}`]
if (headerInfo.sysOriginChild) {
sysOriginParts.push(`child=${headerInfo.sysOriginChild}`)
}
updatedHeaders['req-sys-origin'] = sysOriginParts.join(';')
}
updateCommonHeaders(updatedHeaders)
}
/**
* 通用HTTP请求函数
* @param {string} url - 请求URL

View File

@ -7,6 +7,16 @@
<div class="content">
<!-- 用户信息卡片 -->
<div class="user-card">
<!-- APP连接状态指示 -->
<div v-if="isInApp() && !appConnected" class="app-status connecting">
<div class="status-indicator"></div>
<span>正在连接APP...</span>
</div>
<div v-else-if="isInApp() && appConnected" class="app-status connected">
<div class="status-indicator"></div>
<span>已连接</span>
</div>
<div class="user-info">
<div class="avatar">
<span>{{ userInfo.name.charAt(0) }}</span>
@ -14,6 +24,11 @@
<div class="user-details">
<h3>{{ userInfo.name }}</h3>
<p>ID: {{ userInfo.id }}</p>
<!-- 显示系统信息 -->
<p v-if="headerInfo.sysOrigin" class="system-info">
{{ headerInfo.sysOrigin }}
<span v-if="headerInfo.appVersion"> v{{ headerInfo.appVersion }}</span>
</p>
</div>
<button class="notification-btn" @click="goToNotification">
<span></span>
@ -118,12 +133,15 @@ import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
import { getBankBalance, getUserIdentity } from '../api/wallet.js'
import { connectApplication, parseAccessOrigin, parseHeader, setHttpHeaders, isInApp } from '../utils/appBridge.js'
const router = useRouter()
//
const currentIdentity = ref('host')
const loading = ref(false)
const appConnected = ref(false)
const headerInfo = ref({})
//
const identities = [
@ -132,6 +150,43 @@ const identities = [
{ label: 'Coin Seller', value: 'coin-seller' }
]
// APP
const connectToApp = () => {
if (!isInApp()) {
console.warn('Not running in APP environment')
// APP
appConnected.value = true
checkUserIdentity()
return
}
connectApplication(access => {
console.log('Received access from APP:', access)
const result = parseAccessOrigin(access)
if (result.success) {
//
headerInfo.value = parseHeader(result.data)
// HTTP
setHttpHeaders(headerInfo.value)
//
appConnected.value = true
console.log('APP connection established:', headerInfo.value)
//
checkUserIdentity()
fetchBankBalance()
} else {
console.error('Failed to parse access origin:', result.error)
//
router.push({ path: '/not_app', query: { message: result.error }})
}
})
}
//
const fetchBankBalance = async () => {
loading.value = true
@ -232,10 +287,15 @@ const transfer = () => {
router.push('/transfer')
}
//
// APP
onMounted(() => {
fetchBankBalance()
checkUserIdentity()
// APP
connectToApp()
// APP
if (!isInApp()) {
fetchBankBalance()
}
})
</script>
@ -261,6 +321,48 @@ onMounted(() => {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
/* APP连接状态 */
.app-status {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 8px;
margin-bottom: 12px;
font-size: 12px;
font-weight: 500;
}
.app-status.connecting {
background-color: #fef3c7;
color: #d97706;
}
.app-status.connected {
background-color: #d1fae5;
color: #059669;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: currentColor;
}
.app-status.connecting .status-indicator {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.user-info {
display: flex;
align-items: center;
@ -298,6 +400,12 @@ onMounted(() => {
color: #666;
}
.system-info {
font-size: 12px !important;
color: #8B5CF6 !important;
margin-top: 2px !important;
}
.edit-btn {
width: 32px;
height: 32px;

View File

@ -22,35 +22,7 @@
</div>
</div>
<div class="team-info">
<div class="team-id-section">
<h3>Team ID:</h3>
<div class="team-id-display">
<span class="team-id">{{ teamId }}</span>
<button class="copy-btn" @click="copyTeamId">
📋
</button>
</div>
</div>
<div class="share-section">
<h3>Share:</h3>
<div class="share-buttons">
<button class="share-btn" @click="shareToWeChat">
<span class="share-icon">💬</span>
WeChat
</button>
<button class="share-btn" @click="shareLink">
<span class="share-icon">🔗</span>
Link
</button>
<button class="share-btn" @click="shareQRCode">
<span class="share-icon">📱</span>
QR Code
</button>
</div>
</div>
</div>
</div>
</div>
</template>
@ -88,7 +60,7 @@ const shareToWeChat = () => {
//
const shareLink = () => {
const shareText = `Join my team! Team ID: ${teamId.value}\nDownload Likei APP: https://likei.app`
if (navigator.share) {
navigator.share({
title: 'Join my team on Likei',

View File

@ -17,7 +17,7 @@
<div class="user-info">
<div class="user-avatar">
<img v-if="message.avatar" :src="message.avatar" :alt="message.name" />
<span v-else>{{ message.name.charAt(0) }}</span>
<span v-else>{{ message.name }}</span>
</div>
<div class="user-details">
<h4>{{ message.name }}</h4>
@ -55,18 +55,6 @@ const messages = ref([])
//
const mockMessages = [
{
id: '1234567890',
name: 'User1',
avatar: '',
type: 'team_join_request'
},
{
id: '1234567890',
name: 'User1',
avatar: '',
type: 'team_join_request'
},
{
id: '1234567890',
name: 'User1',
@ -80,8 +68,7 @@ const fetchMessages = async () => {
loading.value = true
try {
const response = await getApplyRecord('1954059793963393025', 'WAIT') //
// { data: [...] }
messages.value = response.data || response //
loading.value = false
} catch (error) {
} finally {

99
src/views/NotAppView.vue Normal file
View File

@ -0,0 +1,99 @@
<template>
<div class="not-app-page">
<div class="error-container">
<div class="error-icon"></div>
<h2>连接错误</h2>
<p class="error-message">{{ errorMessage }}</p>
<div class="error-details">
<p>请确保您正在使用官方APP访问此页面</p>
</div>
<button class="retry-btn" @click="retry">重试</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
const errorMessage = ref('未能连接到APP')
onMounted(() => {
if (route.query.message) {
errorMessage.value = route.query.message
}
})
const retry = () => {
router.go(-1) //
}
</script>
<style scoped>
.not-app-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #f1f2f3;
padding: 20px;
}
.error-container {
background-color: white;
padding: 40px 20px;
border-radius: 12px;
text-align: center;
max-width: 400px;
width: 100%;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.error-icon {
font-size: 48px;
margin-bottom: 16px;
}
h2 {
margin: 0 0 16px 0;
font-size: 24px;
color: #333;
}
.error-message {
font-size: 16px;
color: #666;
margin-bottom: 20px;
}
.error-details {
background-color: #f9fafb;
padding: 16px;
border-radius: 8px;
margin-bottom: 24px;
}
.error-details p {
margin: 0;
font-size: 14px;
color: #666;
}
.retry-btn {
background-color: #8B5CF6;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s;
}
.retry-btn:hover {
background-color: #7C3AED;
}
</style>