feat(ui): 添加图片放大功能并优化仪表盘布局

实现二维码图片点击放大功能,重构仪表盘顶部布局将联系信息与统计卡片并排显示
添加多语言图片切换功能,根据语言显示不同的赞助和联系方式图片
优化Kiro OAuth流程,增加自动关联凭据到Pools的功能
This commit is contained in:
hex2077 2025-12-21 21:09:16 +08:00
parent abf874b43c
commit 0816de2ba2
10 changed files with 448 additions and 76 deletions

View file

@ -6,6 +6,8 @@ import os from 'os';
import crypto from 'crypto';
import open from 'open';
import { broadcastEvent } from './ui-manager.js';
import { autoLinkProviderConfigs } from './service-manager.js';
import { CONFIG } from './config-manager.js';
/**
* OAuth 提供商配置
@ -197,6 +199,9 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa
timestamp: new Date().toISOString()
});
// 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG);
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(true, '您可以关闭此页面'));
} catch (tokenError) {
@ -439,6 +444,9 @@ async function pollQwenToken(deviceCode, codeVerifier, interval = 5, expiresIn =
timestamp: new Date().toISOString()
});
// 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG);
return data;
}
@ -519,7 +527,7 @@ export async function handleQwenOAuth(currentConfig, options = {}) {
}
// 启动后台轮询获取令牌
const interval = deviceAuth.interval || 5;
const interval = 5;
// const expiresIn = deviceAuth.expires_in || 1800;
const expiresIn = 300;
@ -630,6 +638,13 @@ async function handleKiroSocialAuth(provider, currentConfig, options = {}) {
* Kiro Builder ID - Device Code Flow类似 Qwen OAuth 模式
*/
async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) {
// 停止之前的轮询任务
for (const [existingTaskId] of activeKiroPollingTasks.entries()) {
if (existingTaskId.startsWith('kiro-')) {
stopKiroPollingTask(existingTaskId);
}
}
// 1. 注册 OIDC 客户端
const regResponse = await fetch(`${KIRO_OAUTH_CONFIG.ssoOIDCEndpoint}/client/register`, {
method: 'POST',
@ -673,22 +688,16 @@ async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) {
// 3. 启动后台轮询(类似 Qwen OAuth 的模式)
const taskId = `kiro-${deviceAuth.deviceCode.substring(0, 8)}-${Date.now()}`;
// 停止之前的轮询任务
for (const [existingTaskId] of activeKiroPollingTasks.entries()) {
if (existingTaskId.startsWith('kiro-')) {
stopKiroPollingTask(existingTaskId);
}
}
// 异步轮询
pollKiroBuilderIDToken(
regData.clientId,
regData.clientSecret,
deviceAuth.deviceCode,
deviceAuth.interval || 5,
deviceAuth.expiresIn || 300,
taskId,
regData.clientId,
regData.clientSecret,
deviceAuth.deviceCode,
5,
300,
taskId,
options
).catch(error => {
console.error(`${KIRO_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error);
@ -761,10 +770,11 @@ async function pollKiroBuilderIDToken(clientId, clientSecret, deviceCode, interv
// 保存令牌(符合现有规范)
if (options.saveToConfigs) {
const targetDir = path.join(process.cwd(), 'configs', 'kiro');
await fs.promises.mkdir(targetDir, { recursive: true });
const timestamp = Date.now();
credPath = path.join(targetDir, `${timestamp}_oauth_creds.json`);
const folderName = `${timestamp}_kiro-auth-token`;
const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName);
await fs.promises.mkdir(targetDir, { recursive: true });
credPath = path.join(targetDir, `${folderName}.json`);
}
const tokenData = {
@ -790,6 +800,9 @@ async function pollKiroBuilderIDToken(clientId, clientSecret, deviceCode, interv
timestamp: new Date().toISOString()
});
// 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG);
return tokenData;
}
@ -837,17 +850,17 @@ async function startKiroCallbackServer(codeVerifier, expectedState, options = {}
const portEnd = KIRO_OAUTH_CONFIG.callbackPortEnd;
for (let port = portStart; port <= portEnd; port++) {
// 关闭已存在的服务器
await closeKiroServer(port);
try {
const server = await createKiroHttpCallbackServer(port, codeVerifier, expectedState, options);
activeKiroServers.set(port, server);
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 回调服务器已启动于端口 ${port}`);
return port;
} catch (err) {
// 关闭已存在的服务器
await closeKiroServer(port);
try {
const server = await createKiroHttpCallbackServer(port, codeVerifier, expectedState, options);
activeKiroServers.set(port, server);
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 回调服务器已启动于端口 ${port}`);
return port;
} catch (err) {
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 端口 ${port} 被占用,尝试下一个...`);
}
}
}
throw new Error('所有端口都被占用');
@ -925,10 +938,11 @@ function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options
let credPath = path.join(os.homedir(), KIRO_OAUTH_CONFIG.credentialsDir, KIRO_OAUTH_CONFIG.credentialsFile);
if (options.saveToConfigs) {
const targetDir = path.join(process.cwd(), 'configs', 'kiro');
await fs.promises.mkdir(targetDir, { recursive: true });
const timestamp = Date.now();
credPath = path.join(targetDir, `${timestamp}_oauth_creds.json`);
const folderName = `${timestamp}_kiro-auth-token`;
const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName);
await fs.promises.mkdir(targetDir, { recursive: true });
credPath = path.join(targetDir, `${folderName}.json`);
}
const saveData = {
@ -953,6 +967,9 @@ function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options
timestamp: new Date().toISOString()
});
// 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG);
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(true, '授权成功!您可以关闭此页面'));

View file

@ -71,6 +71,10 @@ import {
refreshUsage
} from './usage-manager.js';
import {
initImageZoom
} from './image-zoom.js';
/**
* 加载初始数据
*/
@ -105,6 +109,7 @@ function initApp() {
initRoutingExamples(); // 初始化路径路由示例功能
initUploadConfigManager(); // 初始化上传配置管理功能
initUsageManager(); // 初始化用量管理功能
initImageZoom(); // 初始化图片放大功能
loadInitialData();
// 显示欢迎消息

View file

@ -190,6 +190,77 @@ async function handleGenerateCreds(event) {
const providerType = button.getAttribute('data-provider');
const targetInputId = button.getAttribute('data-target');
try {
// 如果是 Kiro OAuth先显示认证方式选择对话框
if (providerType === 'claude-kiro-oauth') {
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.style.display = 'flex';
modal.innerHTML = `
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3><i class="fas fa-key"></i> <span data-i18n="oauth.kiro.selectMethod">${t('oauth.kiro.selectMethod')}</span></h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="auth-method-options" style="display: flex; flex-direction: column; gap: 12px;">
<!--<button class="auth-method-btn" data-method="google" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<i class="fab fa-google" style="font-size: 24px; color: #4285f4;"></i>
<div style="text-align: left;">
<div style="font-weight: 600; color: #333;" data-i18n="oauth.kiro.google">${t('oauth.kiro.google')}</div>
<div style="font-size: 12px; color: #666;" data-i18n="oauth.kiro.googleDesc">${t('oauth.kiro.googleDesc')}</div>
</div>
</button>
<button class="auth-method-btn" data-method="github" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<i class="fab fa-github" style="font-size: 24px; color: #333;"></i>
<div style="text-align: left;">
<div style="font-weight: 600; color: #333;" data-i18n="oauth.kiro.github">${t('oauth.kiro.github')}</div>
<div style="font-size: 12px; color: #666;" data-i18n="oauth.kiro.githubDesc">${t('oauth.kiro.githubDesc')}</div>
</div>
</button> -->
<button class="auth-method-btn" data-method="builder-id" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<i class="fab fa-aws" style="font-size: 24px; color: #ff9900;"></i>
<div style="text-align: left;">
<div style="font-weight: 600; color: #333;" data-i18n="oauth.kiro.awsBuilder">${t('oauth.kiro.awsBuilder')}</div>
<div style="font-size: 12px; color: #666;" data-i18n="oauth.kiro.awsBuilderDesc">${t('oauth.kiro.awsBuilderDesc')}</div>
</div>
</button>
</div>
</div>
<div class="modal-footer">
<button class="modal-cancel" data-i18n="modal.provider.cancel">${t('modal.provider.cancel')}</button>
</div>
</div>
`;
document.body.appendChild(modal);
const closeModal = () => modal.remove();
modal.querySelector('.modal-close').onclick = closeModal;
modal.querySelector('.modal-cancel').onclick = closeModal;
modal.querySelectorAll('.auth-method-btn').forEach(btn => {
btn.onclick = async () => {
const method = btn.dataset.method;
closeModal();
await proceedWithAuth(providerType, targetInputId, { method });
};
});
return;
}
await proceedWithAuth(providerType, targetInputId, {});
} catch (error) {
console.error('生成凭据失败:', error);
showToast(t('common.error'), t('modal.provider.auth.failed') + `: ${error.message}`, 'error');
}
}
/**
* 实际执行授权逻辑
*/
async function proceedWithAuth(providerType, targetInputId, extraOptions = {}) {
try {
showToast(t('common.info'), t('modal.provider.auth.initializing'), 'info');
@ -200,7 +271,8 @@ async function handleGenerateCreds(event) {
`/providers/${encodeURIComponent(providerType)}/generate-auth-url`,
{
saveToConfigs: true,
providerDir: providerDir
providerDir: providerDir,
...extraOptions
}
);

View file

@ -50,8 +50,12 @@ const translations = {
'dashboard.contact.title': '联系与赞助',
'dashboard.contact.wechat': '扫码进群,注明来意',
'dashboard.contact.wechatDesc': '添加微信获取更多技术支持和交流',
'dashboard.contact.x': '关注 X.com',
'dashboard.contact.xDesc': '在 X 上关注我们获取最新动态',
'dashboard.contact.sponsor': '扫码赞助',
'dashboard.contact.sponsorDesc': '您的赞助是项目持续发展的动力',
'dashboard.contact.coffee': 'Buy me a coffee',
'dashboard.contact.coffeeDesc': 'If you like this project, buy me a coffee!',
// OAuth
'oauth.modal.title': 'OAuth 授权',
@ -76,6 +80,18 @@ const translations = {
'oauth.processing': '正在完成授权...',
'oauth.invalid.url': '该 URL 似乎不包含有效的授权代码',
'oauth.error.format': '无效的 URL 格式',
'oauth.kiro.selectMethod': '选择认证方式',
'oauth.kiro.google': 'Google 账号登录',
'oauth.kiro.googleDesc': '使用 Google 账号进行社交登录',
'oauth.kiro.github': 'GitHub 账号登录',
'oauth.kiro.githubDesc': '使用 GitHub 账号进行社交登录',
'oauth.kiro.awsBuilder': 'AWS Builder ID',
'oauth.kiro.awsBuilderDesc': '使用 AWS Builder ID 进行设备码授权',
'oauth.kiro.authMethodLabel': '认证方式:',
'oauth.kiro.step1': '点击下方按钮在浏览器中打开授权链接',
'oauth.kiro.step2': '使用您的 {method} 账号登录',
'oauth.kiro.step3': '授权完成后页面会自动关闭',
'oauth.kiro.step4': '刷新本页面查看凭据文件',
// Config
'config.title': '配置管理',
@ -396,11 +412,15 @@ const translations = {
'dashboard.routing.nodeName.kiro': 'Claude Kiro OAuth',
'dashboard.routing.nodeName.openai': 'OpenAI Custom',
'dashboard.routing.nodeName.qwen': 'Qwen OAuth',
'dashboard.contact.title': 'Contact & Sponsor',
'dashboard.contact.title': 'Contact & Support',
'dashboard.contact.wechat': 'Scan to Join Group',
'dashboard.contact.wechatDesc': 'Add WeChat for more technical support and communication',
'dashboard.contact.sponsor': 'Scan to Sponsor',
'dashboard.contact.sponsorDesc': 'Your sponsorship is the driving force for the project\'s continuous development',
'dashboard.contact.x': 'Follow on X.com',
'dashboard.contact.xDesc': 'Follow us on X for latest updates',
'dashboard.contact.sponsor': 'Scan to Support',
'dashboard.contact.sponsorDesc': 'Your support is the driving force for the project\'s continuous development',
'dashboard.contact.coffee': 'Buy me a coffee',
'dashboard.contact.coffeeDesc': 'If you like this project, buy me a coffee!',
// OAuth
'oauth.modal.title': 'OAuth Authorization',
@ -425,6 +445,18 @@ const translations = {
'oauth.processing': 'Completing authorization...',
'oauth.invalid.url': 'This URL does not seem to contain a valid auth code',
'oauth.error.format': 'Invalid URL format',
'oauth.kiro.selectMethod': 'Select Authentication Method',
'oauth.kiro.google': 'Google Account Login',
'oauth.kiro.googleDesc': 'Login with Google account',
'oauth.kiro.github': 'GitHub Account Login',
'oauth.kiro.githubDesc': 'Login with GitHub account',
'oauth.kiro.awsBuilder': 'AWS Builder ID',
'oauth.kiro.awsBuilderDesc': 'Device code authorization via AWS Builder ID',
'oauth.kiro.authMethodLabel': 'Auth Method:',
'oauth.kiro.step1': 'Click the button below to open the authorization link in your browser',
'oauth.kiro.step2': 'Log in with your {method} account',
'oauth.kiro.step3': 'The page will close automatically after authorization',
'oauth.kiro.step4': 'Refresh this page to view the credentials file',
// Config
'config.title': 'Configuration Management',
@ -721,11 +753,89 @@ export function setLanguage(lang) {
currentLanguage = lang;
localStorage.setItem('language', lang);
updatePageLanguage();
// 更新图片
updateDashboardImages(lang);
// 触发语言切换事件
window.dispatchEvent(new CustomEvent('languageChanged', { detail: { language: lang } }));
}
}
// 更新仪表盘图片
function updateDashboardImages(lang) {
const sponsorImg = document.getElementById('sponsor-img');
const sponsorTitle = document.getElementById('sponsor-title');
const sponsorDesc = document.getElementById('sponsor-desc');
const wechatImg = document.getElementById('wechat-img');
const wechatIcon = document.getElementById('wechat-icon');
const wechatTitle = document.getElementById('wechat-title');
const wechatDesc = document.getElementById('wechat-desc');
if (lang === 'en-US') {
// 更新赞助图片
if (sponsorImg) {
sponsorImg.src = 'static/coffee.png';
sponsorImg.alt = 'Buy me a coffee';
if (sponsorTitle) {
sponsorTitle.setAttribute('data-i18n', 'dashboard.contact.coffee');
sponsorTitle.textContent = translations['en-US']['dashboard.contact.coffee'];
}
if (sponsorDesc) {
sponsorDesc.setAttribute('data-i18n', 'dashboard.contact.coffeeDesc');
sponsorDesc.textContent = translations['en-US']['dashboard.contact.coffeeDesc'];
}
}
// 更新联系方式图片 (WeChat -> X.com)
if (wechatImg) {
wechatImg.src = 'static/x.com.png';
wechatImg.alt = 'X.com';
if (wechatIcon) {
wechatIcon.className = 'fab fa-x-twitter';
}
if (wechatTitle) {
wechatTitle.setAttribute('data-i18n', 'dashboard.contact.x');
wechatTitle.textContent = translations['en-US']['dashboard.contact.x'] || 'Follow on X.com';
}
if (wechatDesc) {
wechatDesc.setAttribute('data-i18n', 'dashboard.contact.xDesc');
wechatDesc.textContent = translations['en-US']['dashboard.contact.xDesc'] || 'Follow us on X for latest updates';
}
}
} else {
// 更新赞助图片
if (sponsorImg) {
sponsorImg.src = 'static/sponsor.png';
sponsorImg.alt = '赞助二维码';
if (sponsorTitle) {
sponsorTitle.setAttribute('data-i18n', 'dashboard.contact.sponsor');
sponsorTitle.textContent = translations['zh-CN']['dashboard.contact.sponsor'];
}
if (sponsorDesc) {
sponsorDesc.setAttribute('data-i18n', 'dashboard.contact.sponsorDesc');
sponsorDesc.textContent = translations['zh-CN']['dashboard.contact.sponsorDesc'];
}
}
// 更新联系方式图片 (X.com -> WeChat)
if (wechatImg) {
wechatImg.src = 'static/wechat.png';
wechatImg.alt = '微信二维码';
if (wechatIcon) {
wechatIcon.className = 'fab fa-weixin';
}
if (wechatTitle) {
wechatTitle.setAttribute('data-i18n', 'dashboard.contact.wechat');
wechatTitle.textContent = translations['zh-CN']['dashboard.contact.wechat'];
}
if (wechatDesc) {
wechatDesc.setAttribute('data-i18n', 'dashboard.contact.wechatDesc');
wechatDesc.textContent = translations['zh-CN']['dashboard.contact.wechatDesc'];
}
}
}
}
// 获取当前语言
export function getCurrentLanguage() {
return currentLanguage;
@ -783,6 +893,8 @@ function updatePageLanguage() {
export function initI18n() {
// 设置初始语言
updatePageLanguage();
// 设置初始图片
updateDashboardImages(currentLanguage);
// 监听 DOM 变化,自动翻译新添加的元素
const observer = new MutationObserver((mutations) => {

45
static/app/image-zoom.js Normal file
View file

@ -0,0 +1,45 @@
/**
* 图像点击放大功能模块
*/
export function initImageZoom() {
// 创建放大图层
const overlay = document.createElement('div');
overlay.className = 'image-zoom-overlay';
overlay.innerHTML = '<img src="" alt="Zoomed Image">';
document.body.appendChild(overlay);
const zoomedImg = overlay.querySelector('img');
// 监听点击事件
document.addEventListener('click', (e) => {
const target = e.target;
// 如果点击的是可放大的二维码
if (target.classList.contains('clickable-qr')) {
zoomedImg.src = target.src;
overlay.style.display = 'flex';
setTimeout(() => {
overlay.classList.add('show');
}, 10);
}
// 如果点击的是放大图层(或者其中的图片),则关闭
if (overlay.classList.contains('show') && (target === overlay || target === zoomedImg)) {
overlay.classList.remove('show');
setTimeout(() => {
overlay.style.display = 'none';
}, 300);
}
});
// ESC 键关闭
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && overlay.classList.contains('show')) {
overlay.classList.remove('show');
setTimeout(() => {
overlay.style.display = 'none';
}, 300);
}
});
}

View file

@ -362,36 +362,36 @@ function showKiroAuthMethodSelector(providerType) {
modal.innerHTML = `
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3><i class="fas fa-key"></i> </h3>
<h3><i class="fas fa-key"></i> <span data-i18n="oauth.kiro.selectMethod">${t('oauth.kiro.selectMethod')}</span></h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="auth-method-options" style="display: flex; flex-direction: column; gap: 12px;">
<button class="auth-method-btn" data-method="google" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<!-- <button class="auth-method-btn" data-method="google" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<i class="fab fa-google" style="font-size: 24px; color: #4285f4;"></i>
<div style="text-align: left;">
<div style="font-weight: 600; color: #333;">Google 账号登录</div>
<div style="font-size: 12px; color: #666;">使用 Google 账号进行社交登录</div>
<div style="font-weight: 600; color: #333;" data-i18n="oauth.kiro.google">${t('oauth.kiro.google')}</div>
<div style="font-size: 12px; color: #666;" data-i18n="oauth.kiro.googleDesc">${t('oauth.kiro.googleDesc')}</div>
</div>
</button>
<button class="auth-method-btn" data-method="github" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<i class="fab fa-github" style="font-size: 24px; color: #333;"></i>
<div style="text-align: left;">
<div style="font-weight: 600; color: #333;">GitHub 账号登录</div>
<div style="font-size: 12px; color: #666;">使用 GitHub 账号进行社交登录</div>
<div style="font-weight: 600; color: #333;" data-i18n="oauth.kiro.github">${t('oauth.kiro.github')}</div>
<div style="font-size: 12px; color: #666;" data-i18n="oauth.kiro.githubDesc">${t('oauth.kiro.githubDesc')}</div>
</div>
</button>
</button> -->
<button class="auth-method-btn" data-method="builder-id" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<i class="fab fa-aws" style="font-size: 24px; color: #ff9900;"></i>
<div style="text-align: left;">
<div style="font-weight: 600; color: #333;">AWS Builder ID</div>
<div style="font-size: 12px; color: #666;">使用 AWS Builder ID 进行设备码授权</div>
<div style="font-weight: 600; color: #333;" data-i18n="oauth.kiro.awsBuilder">${t('oauth.kiro.awsBuilder')}</div>
<div style="font-size: 12px; color: #666;" data-i18n="oauth.kiro.awsBuilderDesc">${t('oauth.kiro.awsBuilderDesc')}</div>
</div>
</button>
</div>
</div>
<div class="modal-footer">
<button class="modal-cancel">${t('modal.provider.cancel')}</button>
<button class="modal-cancel" data-i18n="modal.provider.cancel">${t('modal.provider.cancel')}</button>
</div>
</div>
`;
@ -502,15 +502,16 @@ function showAuthModal(authUrl, authInfo) {
`;
} else if (authInfo.provider === 'claude-kiro-oauth') {
const methodDisplay = authInfo.authMethod === 'builder-id' ? 'AWS Builder ID' : `Social (${authInfo.socialProvider || 'Google'})`;
const methodAccount = authInfo.authMethod === 'builder-id' ? 'AWS Builder ID' : authInfo.socialProvider || 'Google';
instructionsHtml = `
<div class="auth-instructions">
<h4 data-i18n="oauth.modal.steps">${t('oauth.modal.steps')}</h4>
<p><strong>认证方式:</strong> ${methodDisplay}</p>
<p><strong data-i18n="oauth.kiro.authMethodLabel">${t('oauth.kiro.authMethodLabel')}</strong> ${methodDisplay}</p>
<ol>
<li>点击下方按钮在浏览器中打开授权链接</li>
<li>使用您的 ${authInfo.authMethod === 'builder-id' ? 'AWS Builder ID' : authInfo.socialProvider || 'Google'} 账号登录</li>
<li>授权完成后页面会自动关闭</li>
<li>刷新本页面查看凭据文件</li>
<li data-i18n="oauth.kiro.step1">${t('oauth.kiro.step1')}</li>
<li data-i18n="oauth.kiro.step2" data-i18n-params='{"method":"${methodAccount}"}'>${t('oauth.kiro.step2', { method: methodAccount })}</li>
<li data-i18n="oauth.kiro.step3">${t('oauth.kiro.step3')}</li>
<li data-i18n="oauth.kiro.step4">${t('oauth.kiro.step4')}</li>
</ol>
</div>
`;

View file

@ -2990,6 +2990,66 @@ input:checked + .toggle-slider:before {
}
}
/* Dashboard Top Row Layout */
.dashboard-top-row {
display: flex;
gap: 1.5rem;
margin-bottom: 2rem;
align-items: stretch;
justify-content: flex-start;
}
.dashboard-top-row .stats-grid {
flex: 1;
margin-bottom: 0;
display: flex;
flex-direction: column;
}
.dashboard-top-row .stats-grid .stat-card {
flex: 1;
height: 100%;
}
.dashboard-top-row .dashboard-contact {
flex: 1;
margin-top: 0;
padding-top: 0;
border-top: none;
}
.dashboard-top-row .dashboard-contact .contact-grid {
margin-top: 0;
height: 100%;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.dashboard-top-row .dashboard-contact .contact-card {
padding: 1.25rem;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.dashboard-top-row .dashboard-contact .qr-container {
margin: 0.75rem 0;
}
.dashboard-top-row .dashboard-contact .qr-code {
width: 100px;
height: 100px;
}
.dashboard-top-row .dashboard-contact .contact-card h3 {
font-size: 1rem;
}
.dashboard-top-row .dashboard-contact .qr-description {
font-size: 0.75rem;
}
/* Contact and Sponsor Section */
.contact-grid {
display: grid;
@ -3051,6 +3111,45 @@ input:checked + .toggle-slider:before {
transform: scale(1.05);
}
.clickable-qr {
cursor: zoom-in;
}
/* Image Zoom Overlay */
.image-zoom-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
display: none;
justify-content: center;
align-items: center;
z-index: 2000;
cursor: zoom-out;
opacity: 0;
transition: opacity 0.3s ease;
}
.image-zoom-overlay.show {
display: flex;
opacity: 1;
}
.image-zoom-overlay img {
max-width: 90%;
max-height: 90%;
border-radius: 0.5rem;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
transform: scale(0.9);
transition: transform 0.3s ease;
}
.image-zoom-overlay.show img {
transform: scale(1);
}
.qr-description {
font-size: 0.875rem;
color: var(--text-secondary);
@ -3059,6 +3158,21 @@ input:checked + .toggle-slider:before {
}
/* 响应式调整 */
@media (max-width: 1024px) {
.dashboard-top-row {
flex-direction: column;
}
.dashboard-top-row .stats-grid {
flex: none;
}
.dashboard-top-row .dashboard-contact .qr-code {
width: 160px;
height: 160px;
}
}
@media (max-width: 768px) {
.contact-grid {
grid-template-columns: 1fr;

BIN
static/coffee.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -62,17 +62,40 @@
<!-- Dashboard Section -->
<section id="dashboard" class="section active" aria-labelledby="dashboard-title">
<h2 id="dashboard-title" data-i18n="dashboard.title">系统概览</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-clock"></i>
<div class="dashboard-top-row">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-clock"></i>
</div>
<div class="stat-info">
<h3 id="uptime">--</h3>
<p data-i18n="dashboard.uptime">运行时间</p>
</div>
</div>
<div class="stat-info">
<h3 id="uptime">--</h3>
<p data-i18n="dashboard.uptime">运行时间</p>
</div>
<!-- Contact and Sponsor Section -->
<div class="contact-section dashboard-contact">
<div class="contact-grid">
<div class="contact-card">
<h3><i id="wechat-icon" class="fab fa-weixin"></i> <span id="wechat-title" data-i18n="dashboard.contact.wechat">扫码进群,注明来意</span></h3>
<div class="qr-container">
<img src="static/wechat.png" id="wechat-img" alt="微信二维码" class="qr-code clickable-qr">
</div>
<p class="qr-description" id="wechat-desc" data-i18n="dashboard.contact.wechatDesc">添加微信获取更多技术支持和交流</p>
</div>
<div class="contact-card" id="sponsor-card">
<h3><i class="fas fa-heart"></i> <span id="sponsor-title" data-i18n="dashboard.contact.sponsor">扫码赞助</span></h3>
<div class="qr-container">
<img src="static/sponsor.png" id="sponsor-img" alt="赞助二维码" class="qr-code clickable-qr">
</div>
<p class="qr-description" id="sponsor-desc" data-i18n="dashboard.contact.sponsorDesc">您的赞助是项目持续发展的动力</p>
</div>
</div>
</div>
</div>
<!-- System Information Panel -->
<div class="system-info-panel">
<h3 data-i18n="dashboard.systemInfo">系统信息</h3>
@ -434,26 +457,6 @@
</div>
</div>
<!-- Contact and Sponsor Section -->
<div class="contact-section">
<h3><i class="fas fa-qrcode"></i> <span data-i18n="dashboard.contact.title">联系与赞助</span></h3>
<div class="contact-grid">
<div class="contact-card">
<h3><i class="fab fa-weixin"></i> <span data-i18n="dashboard.contact.wechat">扫码进群,注明来意</span></h3>
<div class="qr-container">
<img src="static/wechat.png" alt="微信二维码" class="qr-code">
</div>
<p class="qr-description" data-i18n="dashboard.contact.wechatDesc">添加微信获取更多技术支持和交流</p>
</div>
<div class="contact-card">
<h3><i class="fas fa-heart"></i> <span data-i18n="dashboard.contact.sponsor">扫码赞助</span></h3>
<div class="qr-container">
<img src="static/sponsor.png" alt="赞助二维码" class="qr-code">
</div>
<p class="qr-description" data-i18n="dashboard.contact.sponsorDesc">您的赞助是项目持续发展的动力</p>
</div>
</div>
</div>
</section>
<!-- Configuration Section -->
@ -633,6 +636,9 @@
<button type="button" class="btn btn-outline upload-btn" data-target="kiroOauthCredsFilePath" data-i18n-title="common.upload" title="上传文件" aria-label="上传文件" data-i18n-aria-label="common.upload">
<i class="fas fa-upload"></i>
</button>
<button type="button" class="btn btn-outline generate-creds-btn" data-target="kiroOauthCredsFilePath" data-provider="claude-kiro-oauth" data-i18n-title="common.generate" title="生成凭据文件">
<i class="fas fa-magic"></i>
</button>
</div>
<small class="form-text">
<i class="fas fa-info-circle"></i>

BIN
static/x.com.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB