first commit
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 381 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 219 KiB |
@@ -0,0 +1,152 @@
|
||||
# 微信验证页面浏览器访问解决方案
|
||||
|
||||
## 问题描述
|
||||
|
||||
微信的安全验证页面(如 `https://weixin110.qq.com/security/acct/newreadtemplate?t=extdevsignin/slaveverify&ticket=xxx`)通常只能在微信内置浏览器中打开,在普通浏览器中会报错。
|
||||
|
||||
## 解决方案
|
||||
|
||||
本方案通过以下技术手段解决了浏览器访问限制:
|
||||
|
||||
### 1. User-Agent模拟
|
||||
- 使用微信浏览器的User-Agent头部
|
||||
- 模拟iPhone微信浏览器环境
|
||||
- 绕过微信服务器的浏览器检测
|
||||
|
||||
### 2. 代理接口
|
||||
创建了代理接口 `/api/v1/login/ProxyWechatVerify`,该接口:
|
||||
- 接收ticket参数
|
||||
- 设置正确的微信浏览器User-Agent
|
||||
- 代理请求微信验证页面
|
||||
- 返回可在浏览器中正常显示的页面
|
||||
|
||||
### 3. 验证页面封装
|
||||
提供了 `/api/v1/login/GetWechatVerifyPage` 接口,返回包含多种验证方式的HTML页面:
|
||||
- **方法一**:直接在iframe中显示验证页面(推荐)
|
||||
- **方法二**:新窗口打开验证页面
|
||||
- **方法三**:复制链接手动打开
|
||||
|
||||
## 在Swagger中的集成
|
||||
|
||||
### 自动检测功能
|
||||
- 当调用获取二维码接口后,系统自动开始监控登录状态
|
||||
- 检测到需要验证时,自动显示浮动验证按钮
|
||||
- 点击按钮即可打开验证弹窗
|
||||
|
||||
### 使用流程
|
||||
1. 在Swagger中调用获取二维码接口
|
||||
2. 用户扫描二维码
|
||||
3. 如果需要安全验证,右下角会出现"🔐 安全验证"按钮
|
||||
4. 点击按钮打开验证弹窗
|
||||
5. 在弹窗中完成滑块验证
|
||||
6. 验证成功后自动关闭弹窗,继续登录流程
|
||||
|
||||
## API接口说明
|
||||
|
||||
### 1. 代理微信验证页面
|
||||
```
|
||||
GET /api/v1/login/ProxyWechatVerify?ticket={ticket}
|
||||
```
|
||||
|
||||
**参数:**
|
||||
- `ticket`: 微信验证票据(从检测扫码状态接口获取)
|
||||
|
||||
**返回:**
|
||||
- 微信验证页面的HTML内容(已处理CORS和路径问题)
|
||||
|
||||
### 2. 获取微信验证页面
|
||||
```
|
||||
GET /api/v1/login/GetWechatVerifyPage?ticket={ticket}
|
||||
```
|
||||
|
||||
**参数:**
|
||||
- `ticket`: 微信验证票据
|
||||
|
||||
**返回:**
|
||||
- 包含多种验证方式的自定义HTML页面
|
||||
|
||||
### 3. 检测扫码状态(已增强)
|
||||
```
|
||||
GET /api/v1/login/CheckLoginStatus?key={key}
|
||||
```
|
||||
|
||||
**返回数据包含:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"state": 0, // 0: 等待扫码, 1: 已扫码, 2: 登录成功
|
||||
"loginState": "waiting",
|
||||
"data62": "...",
|
||||
"ticket": "3_890d910a803559e0a6681089cbf70205" // 验证票据
|
||||
},
|
||||
"text": "等待扫码"
|
||||
}
|
||||
```
|
||||
|
||||
## 技术特点
|
||||
|
||||
### 1. 微信浏览器模拟
|
||||
```javascript
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.42(0x18002a2f) NetType/WIFI Language/zh_CN")
|
||||
```
|
||||
|
||||
### 2. 自动路径修复
|
||||
- 将相对路径转换为绝对路径
|
||||
- 处理CORS跨域问题
|
||||
- 确保资源正确加载
|
||||
|
||||
### 3. 实时状态监控
|
||||
- 自动轮询检测登录状态
|
||||
- 智能识别验证需求
|
||||
- 用户友好的交互体验
|
||||
|
||||
## 使用建议
|
||||
|
||||
1. **推荐方法一**:直接在iframe中验证,体验最佳
|
||||
2. **备用方案**:如果iframe加载失败,使用新窗口方式
|
||||
3. **手动方式**:复制链接到新标签页打开(最后选择)
|
||||
|
||||
## 安全说明
|
||||
|
||||
- 所有验证过程在用户浏览器中完成
|
||||
- 不存储任何用户凭据信息
|
||||
- 仅作为微信验证页面的技术代理
|
||||
- 验证票据具有时效性,过期需重新获取
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
1. **验证页面无法加载**
|
||||
- 检查ticket是否有效
|
||||
- 尝试使用不同的验证方法
|
||||
- 确保网络连接正常
|
||||
|
||||
2. **验证后无响应**
|
||||
- 检查是否完成了验证操作
|
||||
- 等待几秒钟让系统检测状态变化
|
||||
- 可手动调用状态检查接口
|
||||
|
||||
3. **弹窗被浏览器拦截**
|
||||
- 允许浏览器弹窗
|
||||
- 使用iframe方式验证
|
||||
|
||||
### 日志调试
|
||||
```javascript
|
||||
// 在浏览器控制台中查看
|
||||
console.log(window.WechatVerifyIntegration);
|
||||
|
||||
// 手动触发状态检查
|
||||
window.WechatVerifyIntegration.checkLoginStatusOnce();
|
||||
```
|
||||
|
||||
## 更新记录
|
||||
|
||||
- **v1.0** - 初始版本,支持基本的微信验证页面代理
|
||||
- **v1.1** - 添加Swagger集成,自动检测和弹窗功能
|
||||
- **v1.2** - 优化用户体验,增加多种验证方式
|
||||
|
||||
---
|
||||
|
||||
> 本解决方案仅供学习交流使用,请遵守相关法律法规和平台规则。
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
生成二维码!
|
||||
|
After Width: | Height: | Size: 542 B |
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,763 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Swagger UI</title>
|
||||
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
|
||||
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
|
||||
<style>
|
||||
/* Reset */
|
||||
html,
|
||||
body {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Swagger UI Container */
|
||||
#swagger-ui {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Floating Window */
|
||||
.floating-window {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 25px;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
z-index: 2;
|
||||
animation: glow 2s infinite alternate;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
font-size: 18px;
|
||||
color: #e74c3c;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Form and Input Styling */
|
||||
#xxcaibi {
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px auto;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.xxcaibi-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.xxcaibi-label {
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.xxcaibi-input,
|
||||
.xxcaibi-switch {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.xxcaibi-input:focus {
|
||||
border-color: #4CAF50;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.xxcaibi-switch {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
appearance: none;
|
||||
background-color: #ccc;
|
||||
/* 默认状态下的开关背景颜色 */
|
||||
outline: none;
|
||||
border-radius: 20px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.xxcaibi-switch:checked {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
.xxcaibi-switch::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.xxcaibi-switch:checked::before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
/* Radio Button Group */
|
||||
.radio-group {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.radio-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.radio-group input[type="radio"] {
|
||||
margin-right: 10px;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.radio-group input[type="radio"]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Glow Animation */
|
||||
@keyframes glow {
|
||||
from {
|
||||
box-shadow: 0 0 10px rgba(0, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
to {
|
||||
box-shadow: 0 0 20px rgba(0, 255, 255, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
/* Link Card */
|
||||
.card {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100px;
|
||||
z-index: 2;
|
||||
transition: transform 0.5s ease, filter 0.5s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: scale(1.2);
|
||||
filter: drop-shadow(0 0 10px rgba(0, 255, 255, 0.7));
|
||||
}
|
||||
|
||||
/* Card Movement Animation */
|
||||
@keyframes cardMove {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(50vw, 50vh);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.base-url {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Telegram Link Container */
|
||||
.tg-link {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Telegram Button */
|
||||
.tg-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
background-color: #5cadff !important;
|
||||
color: white !important;
|
||||
font-size: 16px;
|
||||
text-decoration: none;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* Telegram Button Hover Effect */
|
||||
.tg-button:hover {
|
||||
background-color: #007ab8 !important;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Telegram Icon */
|
||||
.tg-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container input[type="text"] {
|
||||
flex: 1;
|
||||
width: 300px;
|
||||
/* 初始宽度 */
|
||||
max-width: 500px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.container button {
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.container button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.container button:hover:not(:disabled) {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
#messages {
|
||||
margin-top: 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
<div id="xxcaibi" class="wrapper" style="display: none;">
|
||||
<div class="xxcaibi-group">
|
||||
<label for="admin-key" class="xxcaibi-label">ADMIN_KEY</label>
|
||||
<input type="text" id="admin-key" class="xxcaibi-input" placeholder="管理员接口 ADMIN_KEY">
|
||||
</div>
|
||||
|
||||
<div class="xxcaibi-group">
|
||||
<label for="token-key" class="xxcaibi-label">TOKEN_KEY</label>
|
||||
<input type="text" id="token-key" class="xxcaibi-input" placeholder="普通API TOKEN_KEY">
|
||||
</div>
|
||||
|
||||
<div class="xxcaibi-group">
|
||||
<label class="xxcaibi-label">Swagger 展开方式</label>
|
||||
<div class="radio-group">
|
||||
<label>
|
||||
<input type="radio" name="expand-options" value="none" checked>不展开
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="expand-options" value="list">只展开API
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="expand-options" value="full">展开所有
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="xxcaibi-group">
|
||||
<label for="copy-base-url" class="xxcaibi-label">是否复制根路径</label>
|
||||
<input style="flex: none !important" type="checkbox" id="copy-base-url" class="xxcaibi-switch" checked>
|
||||
<span style="margin-left: 20px;" id="BASE_URL"></span>
|
||||
</div>
|
||||
|
||||
<h4 style="text-align: center;line-height: 1;margin-bottom: 10px">WS 客户端 (同步消息)</h4>
|
||||
<div class="container">
|
||||
<input id="urlInput" type="text" placeholder="输入 WebSocket URL" />
|
||||
<button id="connectButton">连接</button>
|
||||
<button id="disconnectButton" disabled>取消连接</button>
|
||||
<button id="clearMessagesButton">清理消息</button>
|
||||
</div>
|
||||
<div id="messages"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let socket;
|
||||
document.getElementById('urlInput').value = window.location.origin.replace(/^http/, 'ws') + '/ws/GetSyncMsg?key='
|
||||
|
||||
// 更新按钮文本
|
||||
function updateButton(button, text, isLoading) {
|
||||
button.innerHTML = isLoading ? text + '...' : text;
|
||||
button.disabled = isLoading;
|
||||
}
|
||||
|
||||
// 连接按钮点击事件
|
||||
document.getElementById('connectButton').onclick = function () {
|
||||
const url = document.getElementById('urlInput').value;
|
||||
|
||||
// 如果已经有连接,先关闭
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.close();
|
||||
}
|
||||
|
||||
// 更新按钮状态为连接中
|
||||
updateButton(this, '连接中', true);
|
||||
|
||||
// 创建 WebSocket 连接
|
||||
socket = new WebSocket(url);
|
||||
|
||||
// 连接成功时
|
||||
socket.onopen = function () {
|
||||
console.log('已连接到服务器');
|
||||
document.getElementById('messages').innerHTML += '<p>已连接到服务器</p>';
|
||||
updateButton(document.getElementById('connectButton'), '连接', false); // 重置连接按钮
|
||||
document.getElementById('disconnectButton').disabled = false; // 启用取消连接按钮
|
||||
document.getElementById('connectButton').disabled = true;
|
||||
};
|
||||
|
||||
// 接收消息
|
||||
socket.onmessage = function (event) {
|
||||
console.log('收到来自服务器的消息:', event.data);
|
||||
document.getElementById('messages').innerHTML += '<p>服务器: ' + event.data + '</p>';
|
||||
};
|
||||
|
||||
// 连接关闭时
|
||||
socket.onclose = function () {
|
||||
console.log('与服务器的连接已关闭');
|
||||
document.getElementById('messages').innerHTML += '<p>与服务器的连接已关闭</p>';
|
||||
updateButton(document.getElementById('connectButton'), '连接', false); // 启用连接按钮
|
||||
document.getElementById('disconnectButton').innerHTML = "取消连接",
|
||||
document.getElementById('disconnectButton').disabled = true; // 启用连接按钮
|
||||
document.getElementById('connectButton').disabled = false; // 启用连接按钮
|
||||
};
|
||||
|
||||
// 处理错误
|
||||
socket.onerror = function (error) {
|
||||
console.error('WebSocket 错误:', error);
|
||||
document.getElementById('messages').innerHTML += '<p>WebSocket 错误: ' + error.message + '</p>';
|
||||
updateButton(document.getElementById('connectButton'), '连接', false); // 重置连接按钮
|
||||
};
|
||||
};
|
||||
|
||||
// 取消连接按钮点击事件
|
||||
document.getElementById('disconnectButton').onclick = function () {
|
||||
updateButton(this, '取消中', true); // 更新为取消中状态
|
||||
if (socket) {
|
||||
setTimeout(function () {
|
||||
socket.close();
|
||||
}, 30)
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
// 清理消息按钮点击事件
|
||||
document.getElementById('clearMessagesButton').onclick = function () {
|
||||
document.getElementById('messages').innerHTML = ''; // 清空消息区域
|
||||
};
|
||||
</script>
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<!-- Floating window -->
|
||||
<div class="floating-window" style="display: none !important">
|
||||
</div>
|
||||
|
||||
<script src="./swagger-ui-bundle.js" charset="UTF-8"></script>
|
||||
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"></script>
|
||||
<script src="./jquery-3.7.1.slim.min.js"></script>
|
||||
<!-- 微信验证集成脚本 -->
|
||||
<script src="./wechat-verify-integration.js"></script>
|
||||
|
||||
<script>
|
||||
function closeDiv(element) {
|
||||
element.parentElement.style.display = 'none';
|
||||
}
|
||||
|
||||
function isAdminApi(pathKey) {
|
||||
if (pathKey.startsWith("/admin/")) {
|
||||
return true
|
||||
}
|
||||
return window.xxcaibi.adminUrls[pathKey]
|
||||
}
|
||||
|
||||
const CustomPlugin = function (system) {
|
||||
return {
|
||||
statePlugins: {
|
||||
spec: {
|
||||
actions: {
|
||||
updateQueryParam: (paramName, paramValue, isAdminKey = false) => (state) => {
|
||||
const spec = state.specSelectors.specJson().toJS();
|
||||
const paths = spec.paths;
|
||||
|
||||
Object.keys(paths).forEach(pathKey => {
|
||||
if (!isAdminKey && isAdminApi(pathKey)) {
|
||||
// 非 ADMIN_KEY 并且是 ADMIN 接口
|
||||
return;
|
||||
}
|
||||
if (isAdminKey && !isAdminApi(pathKey)) {
|
||||
// 是 ADMIN_KEY 并且 不是 ADMIN 接口
|
||||
return;
|
||||
}
|
||||
Object.keys(paths[pathKey]).forEach(method => {
|
||||
const params = paths[pathKey][method].parameters;
|
||||
params.forEach(param => {
|
||||
if (param.name === paramName && param.in === 'query') {
|
||||
param.default = paramValue;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
state.specActions.updateSpec(JSON.stringify(spec));
|
||||
},
|
||||
updateConfigs: (spec) => (state) => {
|
||||
state.specActions.updateSpec(JSON.stringify(spec));
|
||||
},
|
||||
},
|
||||
wrapActions: {
|
||||
updateSpec: (oriAction, system) => (...args) => {
|
||||
return oriAction(...args)
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// 检查 Swagger-UI 内的 img.opblock-loading-animation 请求体节点是否加载完成
|
||||
function checkSwaggerLoadingComplete(targetFunc) {
|
||||
const targetNode = document.getElementById('swagger-ui');
|
||||
const config = { childList: true, subtree: true };
|
||||
const callback = function (mutationsList, observer) {
|
||||
const loadingElements = targetNode.querySelectorAll('img.opblock-loading-animation');
|
||||
if (loadingElements.length === 0) {
|
||||
console.log('Swagger UI loaded successfully');
|
||||
observer.disconnect(); // 停止观察
|
||||
targetFunc && targetFunc();
|
||||
}
|
||||
};
|
||||
const observer = new MutationObserver(callback);
|
||||
|
||||
setTimeout(function () {
|
||||
$("#swagger-ui").append(" ")
|
||||
}, 100)
|
||||
observer.observe(targetNode, config); // DOM 节点无变化则不会触发监听
|
||||
}
|
||||
|
||||
// 粘贴到系统剪切板1
|
||||
function copy(text) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
// navigator.clipboard 只能在 HTTPS 环境使用; 旧版浏览器兼容性不好
|
||||
navigator.clipboard.writeText(text).then(function () {
|
||||
}).catch(function (error) {
|
||||
console.error('Failed to copy text: ', error);
|
||||
fallbackCopy(text);
|
||||
});
|
||||
} else {
|
||||
fallbackCopy(text);
|
||||
}
|
||||
}
|
||||
|
||||
// 粘贴到系统剪切板2
|
||||
function fallbackCopy(text) {
|
||||
// 使用 textarea 标签 可以复制 pre 代码标签里的换行
|
||||
let textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.error('Fallback copy failed: ', err);
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
// 复制 API 的路径即描述
|
||||
function addCopy1(selector) {
|
||||
let pathElements;
|
||||
// opblock-summary-path opblock-summary-description
|
||||
if (selector) {
|
||||
pathElements = $(selector).parent().find(".opblock-summary-description");
|
||||
} else {
|
||||
pathElements = $(".opblock-summary-description");
|
||||
addCopy2();
|
||||
}
|
||||
if (pathElements.length === 0) {
|
||||
return;
|
||||
}
|
||||
console.log("API数量:", pathElements.length);
|
||||
|
||||
$(".opblock-summary-post").click(function () {
|
||||
let status = $(this).children().first().attr("aria-expanded");
|
||||
if (status === "true") {
|
||||
return;
|
||||
}
|
||||
let curId = $(this).parent().attr("id");
|
||||
// console.log(curId, status);
|
||||
setTimeout(function () {
|
||||
addCopy2(`#${curId}`);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
pathElements.each(function (index, pathElement) {
|
||||
pathElement = $(pathElement);
|
||||
let isCopy = pathElement.attr("copy");
|
||||
if (isCopy === "true") { // 已经添加过 copy 按钮
|
||||
return;
|
||||
}
|
||||
pathElement.after(
|
||||
$("<button>复制</button>").css("cursor", "pointer").css("color", "#FFF").css("background", "#2b85e4").css("border-color", "#2b85e4").css("padding", "4px 8px").css("border-radius", "4px").css("margin-right", "10px").click(function () {
|
||||
// 获取接口文本
|
||||
let api = this.parentNode.children[1].dataset.path;
|
||||
let desc = this.parentNode.children[2].textContent;
|
||||
let text = api + " " + desc;
|
||||
if (window.xxcaibi.copy) {
|
||||
text = window.xxcaibi.BASE_URL + text;
|
||||
}
|
||||
// console.log(api, desc)
|
||||
// 调用函数,复制到剪切板
|
||||
text = text.replace(/([^:])\/+/g, '$1/')
|
||||
copy(text);
|
||||
// 提示复制成功
|
||||
$(this).text("复制成功!");
|
||||
// 定时改变显示文字
|
||||
setTimeout(() => {
|
||||
$(this).text("复制");
|
||||
}, 500);
|
||||
return false;
|
||||
})
|
||||
);
|
||||
pathElement.attr("copy", "true");
|
||||
});
|
||||
}
|
||||
|
||||
// 复制 POST 请求的 JSON 请求体
|
||||
function addCopy2(selector) {
|
||||
if (!selector) {
|
||||
selector = "#swagger-ui"
|
||||
}
|
||||
let lis = $(selector + ' li[role="presentation"]');
|
||||
if (lis.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
lis.each(function (index, element) {
|
||||
if (index % 2 === 0) {
|
||||
return;
|
||||
}
|
||||
element = $(element);
|
||||
let isCopy = element.attr("copy");
|
||||
if (isCopy === "true") { // 已经添加过 copy 按钮
|
||||
return;
|
||||
}
|
||||
element.after(
|
||||
$("<button>复制</button>").css("cursor", "pointer").css("color", "#FFF").css("background", "#515a6e").css("border-color", "#17233d").css("padding", "4px 10px").css("border-radius", "4px").css("margin-right", "10px").click(function () {
|
||||
// 获取接口文本
|
||||
let preTag = $(selector + " .highlight-code pre")[0];
|
||||
if (!preTag) {
|
||||
alert("无法复制");
|
||||
return;
|
||||
}
|
||||
let text = preTag.textContent;
|
||||
// 调用函数,复制到剪切板
|
||||
copy(text);
|
||||
// 提示复制成功
|
||||
$(this).text("复制成功!");
|
||||
// 定时改变显示文字
|
||||
setTimeout(() => {
|
||||
$(this).text("复制");
|
||||
}, 500);
|
||||
})
|
||||
);
|
||||
element.attr("copy", "true");
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
// Initialize Swagger UI
|
||||
const ui = SwaggerUIBundle({
|
||||
url: "swagger.json",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl,
|
||||
CustomPlugin,
|
||||
],
|
||||
docExpansion: "none", // none(不展开) list(展开operations) full(全部展开)
|
||||
onComplete: function () {
|
||||
// Swagger-UI 渲染完成后回调; updateSpec 成功也会调用
|
||||
// 但是如果数据太多会导致部分 HTML 元素在此时还未加载完成
|
||||
console.log("Swagger UI onComplete...")
|
||||
console.log("Swagger UI Version:", window.versions.swaggerUi.version)
|
||||
parseBaseUrl();
|
||||
addXXCaiBi();
|
||||
checkSwaggerLoadingComplete(function () {
|
||||
addCopy1();
|
||||
addCopy2();
|
||||
|
||||
// Operations API 接口的点击事件
|
||||
$(".opblock-tag").click(function () {
|
||||
checkSwaggerLoadingComplete(function () {
|
||||
addCopy1();
|
||||
addCopy2();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
// parameterMacro: function (operation, parameter) {
|
||||
// // 展开 API 对应的 operations 时触发, 可以在此修改 指定参数的默认值
|
||||
// console.log(operation, parameter);
|
||||
// return "";
|
||||
// },
|
||||
});
|
||||
window.ui = ui;
|
||||
|
||||
window.xxcaibi = {
|
||||
"BASE_URL": "",
|
||||
"copy": true,
|
||||
|
||||
"ADMIN_KEY": "",
|
||||
"TOKEN_KEY": "",
|
||||
|
||||
"adminUrls": {
|
||||
"/login/GenAuthKey": true,
|
||||
"/login/GenAuthKey2": true,
|
||||
},
|
||||
}
|
||||
|
||||
// 监听状态变化
|
||||
$('#copy-base-url').change(function () {
|
||||
window.xxcaibi.copy = $(this).is(':checked');
|
||||
// console.log(window.xxcaibi);
|
||||
});
|
||||
$('#admin-key').change(function () {
|
||||
window.xxcaibi.ADMIN_KEY = $(this).val().trim();
|
||||
// console.log(window.xxcaibi);
|
||||
|
||||
ui.specActions.updateQueryParam("key", window.xxcaibi.ADMIN_KEY, true);
|
||||
});
|
||||
$('#token-key').change(function () {
|
||||
window.xxcaibi.TOKEN_KEY = $(this).val().trim();
|
||||
// console.log(window.xxcaibi);
|
||||
if (window.xxcaibi.TOKEN_KEY) {
|
||||
document.getElementById('urlInput').value = window.location.origin.replace(/^http/, 'ws') + '/ws/GetSyncMsg?key=' + window.xxcaibi.TOKEN_KEY
|
||||
}
|
||||
|
||||
ui.specActions.updateQueryParam("key", window.xxcaibi.TOKEN_KEY);
|
||||
});
|
||||
$('.radio-group input[type="radio"]').change(function () {
|
||||
let value = $(this).val();
|
||||
let configs = ui.getConfigs();
|
||||
configs.docExpansion = value
|
||||
|
||||
// // 更新整个 UI 数据; 会刷新 UI 导致 onComplete 回调
|
||||
// const spec = ui.specSelectors.specJson().toJS();
|
||||
// ui.specActions.updateConfigs(spec);
|
||||
|
||||
// 只更新配置; 不会刷新 UI 导致 onComplete 回调
|
||||
ui.configsActions.toggle();
|
||||
checkSwaggerLoadingComplete(function () {
|
||||
addCopy1();
|
||||
addCopy2();
|
||||
|
||||
// Operations API 接口的点击事件
|
||||
$(".opblock-tag").click(function () {
|
||||
checkSwaggerLoadingComplete(function () {
|
||||
addCopy1();
|
||||
addCopy2();
|
||||
});
|
||||
});
|
||||
}); // DOM 节点无变化不会触发监听
|
||||
});
|
||||
|
||||
let parseBaseUrl = function () {
|
||||
let config = window.ui.specSelectors.specJson().toJS(); // 获取 spec 对象
|
||||
let basePath = config.basePath || ''; // 获取 basePath
|
||||
basePath = "" ? basePath === "/" : basePath;
|
||||
|
||||
// 获取当前网址的协议、域名和端口
|
||||
let protocol = window.location.protocol;
|
||||
let hostname = window.location.hostname;
|
||||
let port = window.location.port;
|
||||
|
||||
// 组合协议、域名和端口
|
||||
const baseUrl = protocol + "//" + hostname + (port ? ":" + port : "") + basePath;
|
||||
|
||||
// 显示结果
|
||||
console.log("BASE_URL:", baseUrl);
|
||||
window.xxcaibi.BASE_URL = baseUrl;
|
||||
}
|
||||
|
||||
let addXXCaiBi = function () {
|
||||
$("#xxcaibi").insertAfter('.information-container:first').css('display', 'block');
|
||||
// 判断 tg-link 不存在
|
||||
if (!document.getElementsByClassName("tg-link").length) {
|
||||
$(".info__contact").append(
|
||||
'<div class="tg-link">' +
|
||||
'<a href="https://t.me/wx_ipad" target="_blank" class="tg-button">' +
|
||||
'<img src="https://upload.wikimedia.org/wikipedia/commons/8/82/Telegram_logo.svg" alt="Telegram Logo" class="tg-icon">' +
|
||||
'加入 Telegram 群' +
|
||||
'</a>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
$("#BASE_URL").html("BASE_URL: " + window.xxcaibi.BASE_URL);
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,75 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<title>Swagger UI: OAuth2 Redirect</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
'use strict';
|
||||
function run () {
|
||||
var oauth2 = window.opener.swaggerUIRedirectOauth2;
|
||||
var sentState = oauth2.state;
|
||||
var redirectUrl = oauth2.redirectUrl;
|
||||
var isValid, qp, arr;
|
||||
|
||||
if (/code|token|error/.test(window.location.hash)) {
|
||||
qp = window.location.hash.substring(1);
|
||||
} else {
|
||||
qp = location.search.substring(1);
|
||||
}
|
||||
|
||||
arr = qp.split("&");
|
||||
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
|
||||
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
||||
function (key, value) {
|
||||
return key === "" ? value : decodeURIComponent(value);
|
||||
}
|
||||
) : {};
|
||||
|
||||
isValid = qp.state === sentState;
|
||||
|
||||
if ((
|
||||
oauth2.auth.schema.get("flow") === "accessCode" ||
|
||||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
|
||||
oauth2.auth.schema.get("flow") === "authorization_code"
|
||||
) && !oauth2.auth.code) {
|
||||
if (!isValid) {
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "warning",
|
||||
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
|
||||
});
|
||||
}
|
||||
|
||||
if (qp.code) {
|
||||
delete oauth2.state;
|
||||
oauth2.auth.code = qp.code;
|
||||
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
||||
} else {
|
||||
let oauthErrorMsg;
|
||||
if (qp.error) {
|
||||
oauthErrorMsg = "["+qp.error+"]: " +
|
||||
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
|
||||
(qp.error_uri ? "More info: "+qp.error_uri : "");
|
||||
}
|
||||
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "error",
|
||||
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
|
||||
});
|
||||
}
|
||||
} else {
|
||||
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
|
||||
}
|
||||
window.close();
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
run();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,20 @@
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
.box{width:300px; text-align:center; font-szie:18px;}
|
||||
.box img {width:100%;}
|
||||
body{text-align:center}
|
||||
#aa{display:block;
|
||||
position:relative;
|
||||
margin:auto;
|
||||
}
|
||||
</style>
|
||||
<meta charset="utf-8">
|
||||
<title>错误</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box" id="aa">
|
||||
{{.showlog}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
.box{width:300px; text-align:center; font-szie:18px;}
|
||||
.box img {width:100%;}
|
||||
body{text-align:center}
|
||||
#aa{display:block;
|
||||
position:relative;
|
||||
margin:auto;
|
||||
}
|
||||
</style>
|
||||
<meta charset="utf-8">
|
||||
<title>登陆二维码</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box" id="aa">
|
||||
<img src="/static/qrcode/{{.img}}.png" align="center">
|
||||
请使用手机微信扫码登陆
|
||||
{{.resp}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>系统信息</title>
|
||||
</head>
|
||||
<body>
|
||||
<h4>{{.message}}</h4>
|
||||
</body>
|
||||
</html>
|
||||