mirror of
https://github.com/wisdgod/cursor-api.git
synced 2025-10-05 06:36:52 +08:00
refactor: remove large test file and publish frontend assets
- Remove cursor-tokens.7z (too large for repository) - Add frontend files to static/ directory: - api.html, build_key.html, config.html - logs.html, proxies.html, tokens.html - shared-styles.css, shared.js - Note: frontend files lag behind backend development - Switch default branch to Releases Branch due to low code activity and maintenance challenges
This commit is contained in:
BIN
cursor-tokens.7z
BIN
cursor-tokens.7z
Binary file not shown.
1945
static/api.html
Normal file
1945
static/api.html
Normal file
File diff suppressed because it is too large
Load Diff
1668
static/build_key.html
Normal file
1668
static/build_key.html
Normal file
File diff suppressed because it is too large
Load Diff
428
static/config.html
Normal file
428
static/config.html
Normal file
@@ -0,0 +1,428 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="data:image/x-icon;," />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>配置管理</title>
|
||||
<!-- 引入共享样式 -->
|
||||
<link rel="stylesheet" href="/static/shared-styles.css" />
|
||||
<script src="/static/shared.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>配置管理</h1>
|
||||
|
||||
<div class="container">
|
||||
<div class="form-group">
|
||||
<label>路径:</label>
|
||||
<select id="path">
|
||||
<option value="/">根路径 (/)</option>
|
||||
<option value="/logs">日志页面 (/logs)</option>
|
||||
<option value="/config">配置页面 (/config)</option>
|
||||
<option value="/tokens">Token 管理页面 (/tokens)</option>
|
||||
<option value="/proxies">代理管理页面 (/proxies)</option>
|
||||
<option value="/static/shared-styles.css">
|
||||
共享样式 (/static/shared-styles.css)
|
||||
</option>
|
||||
<option value="/static/shared.js">
|
||||
共享脚本 (/static/shared.js)
|
||||
</option>
|
||||
<option value="/about">关于页面 (/about)</option>
|
||||
<option value="/readme">ReadMe文档 (/readme)</option>
|
||||
<option value="/api">api调用 (/api)</option>
|
||||
<option value="/build-key">构建动态 Key (/build-key)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>内容类型:</label>
|
||||
<select id="content_type">
|
||||
<option value="default">默认</option>
|
||||
<option value="not_found">404 页面</option>
|
||||
<option value="redirect">重定向</option>
|
||||
<option value="plain_text">纯文本</option>
|
||||
<option value="html">HTML</option>
|
||||
<option value="css">CSS</option>
|
||||
<option value="js">JavaScript</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>内容:</label>
|
||||
<textarea id="content"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>图片处理能力:</label>
|
||||
<select id="vision_ability">
|
||||
<option value="">保持不变</option>
|
||||
<option value="none">禁用</option>
|
||||
<option value="base64">仅 Base64</option>
|
||||
<option value="all">Base64 + HTTP</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>慢速池:</label>
|
||||
<select id="enable_slow_pool">
|
||||
<option value="">保持不变</option>
|
||||
<option value="true">启用</option>
|
||||
<option value="false">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>长上下文:</label>
|
||||
<select id="enable_long_context">
|
||||
<option value="">保持不变</option>
|
||||
<option value="true">启用</option>
|
||||
<option value="false">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>使用量检查模型规则:</label>
|
||||
<select id="usage_check_models_type">
|
||||
<option value="">保持不变</option>
|
||||
<option value="none">禁用</option>
|
||||
<option value="default">默认</option>
|
||||
<option value="all">所有</option>
|
||||
<option value="list">自定义列表</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
id="usage_check_models_list"
|
||||
placeholder="模型列表,以逗号分隔"
|
||||
style="display: none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>是否允许动态配置Key:</label>
|
||||
<select id="enable_dynamic_key">
|
||||
<option value="">保持不变</option>
|
||||
<option value="true">启用</option>
|
||||
<option value="false">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>包含网络引用:</label>
|
||||
<select id="include_web_references">
|
||||
<option value="">保持不变</option>
|
||||
<option value="true">启用</option>
|
||||
<option value="false">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>模型获取模式:</label>
|
||||
<select id="fetch_raw_models">
|
||||
<option value="">保持不变</option>
|
||||
<option value="truncate">覆盖现有数据</option>
|
||||
<option value="append:truncate">追加并部分覆盖</option>
|
||||
<option value="append">仅追加新数据</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>共享令牌(空表示禁用):</label>
|
||||
<input type="text" id="shareToken" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>认证令牌:</label>
|
||||
<input type="password" id="authToken" />
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button onclick="updateConfig('get')">获取配置</button>
|
||||
<button onclick="updateConfig('update')">更新配置</button>
|
||||
<button onclick="updateConfig('reset')" class="secondary">
|
||||
重置配置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="message"></div>
|
||||
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
// 配置缓存对象
|
||||
let configCache = {};
|
||||
|
||||
/**
|
||||
* 比较当前配置与缓存配置,返回变更的配置项
|
||||
* @returns {Object} 变更的配置对象
|
||||
*/
|
||||
function getChangedConfig() {
|
||||
const currentConfig = {
|
||||
path: document.getElementById("path").value,
|
||||
content: {
|
||||
type: document.getElementById("content_type").value,
|
||||
value: document.getElementById("content").value,
|
||||
},
|
||||
vision_ability: document.getElementById("vision_ability").value,
|
||||
enable_slow_pool: parseBooleanFromString(
|
||||
document.getElementById("enable_slow_pool").value,
|
||||
),
|
||||
enable_long_context: parseBooleanFromString(
|
||||
document.getElementById("enable_long_context").value,
|
||||
),
|
||||
usage_check_models: {
|
||||
type: document.getElementById("usage_check_models_type").value,
|
||||
content: document.getElementById("usage_check_models_list").value,
|
||||
},
|
||||
enable_dynamic_key: parseBooleanFromString(
|
||||
document.getElementById("enable_dynamic_key").value,
|
||||
),
|
||||
include_web_references: parseBooleanFromString(
|
||||
document.getElementById("include_web_references").value,
|
||||
),
|
||||
fetch_raw_models: document.getElementById("fetch_raw_models").value,
|
||||
share_token: document.getElementById("shareToken").value.trim(),
|
||||
};
|
||||
|
||||
const changes = {
|
||||
path: currentConfig.path,
|
||||
};
|
||||
|
||||
// 比较内容配置
|
||||
if (
|
||||
currentConfig.content.type !== "default" &&
|
||||
(configCache.content?.type !== currentConfig.content.type ||
|
||||
configCache.content?.value !== currentConfig.content.value)
|
||||
) {
|
||||
changes.content = currentConfig.content;
|
||||
} else if (
|
||||
currentConfig.content.type === "default" &&
|
||||
configCache.content?.type !== "default"
|
||||
) {
|
||||
changes.content = { type: "default", value: "" };
|
||||
}
|
||||
|
||||
// 比较其他配置项
|
||||
const simpleFields = [
|
||||
"vision_ability",
|
||||
"enable_slow_pool",
|
||||
"enable_long_context",
|
||||
"enable_dynamic_key",
|
||||
"include_web_references",
|
||||
"fetch_raw_models",
|
||||
"share_token",
|
||||
];
|
||||
|
||||
simpleFields.forEach((field) => {
|
||||
const value = currentConfig[field];
|
||||
if (value !== null && value !== "" && value !== configCache[field]) {
|
||||
changes[field] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// 比较使用量检查模型配置
|
||||
if (
|
||||
currentConfig.usage_check_models.type &&
|
||||
(configCache.usage_check_models?.type !==
|
||||
currentConfig.usage_check_models.type ||
|
||||
(currentConfig.usage_check_models.type === "list" &&
|
||||
configCache.usage_check_models?.content !==
|
||||
currentConfig.usage_check_models.content))
|
||||
) {
|
||||
changes.usage_check_models = currentConfig.usage_check_models;
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从服务器获取配置
|
||||
*/
|
||||
async function fetchConfig() {
|
||||
try {
|
||||
const path = document.getElementById("path").value;
|
||||
const requestData = {
|
||||
action: "get",
|
||||
path: path,
|
||||
};
|
||||
|
||||
const response = await makeAuthenticatedRequest("/config", {
|
||||
body: JSON.stringify(requestData),
|
||||
});
|
||||
|
||||
if (response && response.data) {
|
||||
const data = response.data;
|
||||
|
||||
// 更新内容配置
|
||||
const contentType = data.content?.type || "default";
|
||||
const contentValue = data.content?.value || "";
|
||||
|
||||
document.getElementById("content_type").value = contentType;
|
||||
const contentTextarea = document.getElementById("content");
|
||||
|
||||
if (contentType === "default") {
|
||||
// 如果是默认类型,尝试从路径获取内容
|
||||
try {
|
||||
const pathResponse = await fetch(path);
|
||||
contentTextarea.value = await pathResponse.text();
|
||||
} catch (err) {
|
||||
console.error("获取默认内容失败:", err);
|
||||
contentTextarea.value = "";
|
||||
}
|
||||
contentTextarea.disabled = true;
|
||||
} else {
|
||||
contentTextarea.value = contentValue;
|
||||
contentTextarea.disabled = false;
|
||||
}
|
||||
|
||||
// 更新其他配置项
|
||||
document.getElementById("vision_ability").value =
|
||||
data.vision_ability || "";
|
||||
document.getElementById("enable_slow_pool").value =
|
||||
parseStringFromBoolean(data.enable_slow_pool, "");
|
||||
document.getElementById("enable_long_context").value =
|
||||
parseStringFromBoolean(data.enable_long_context, "");
|
||||
document.getElementById("enable_dynamic_key").value =
|
||||
parseStringFromBoolean(data.enable_dynamic_key, "");
|
||||
document.getElementById("include_web_references").value =
|
||||
parseStringFromBoolean(data.include_web_references, "");
|
||||
document.getElementById("fetch_raw_models").value =
|
||||
data.fetch_raw_models || "";
|
||||
document.getElementById("shareToken").value =
|
||||
data.share_token || "";
|
||||
|
||||
// 处理使用量检查模型
|
||||
const usageCheckType = data.usage_check_models?.type || "";
|
||||
document.getElementById("usage_check_models_type").value =
|
||||
usageCheckType;
|
||||
|
||||
const usageCheckList = document.getElementById(
|
||||
"usage_check_models_list",
|
||||
);
|
||||
usageCheckList.value =
|
||||
usageCheckType === "list"
|
||||
? data.usage_check_models?.content || ""
|
||||
: "";
|
||||
usageCheckList.style.display =
|
||||
usageCheckType === "list" ? "inline-block" : "none";
|
||||
|
||||
// 更新缓存
|
||||
configCache = { ...data };
|
||||
|
||||
showGlobalMessage(`成功获取 ${path} 的配置`, false);
|
||||
}
|
||||
} catch (error) {
|
||||
showGlobalMessage(error.message || "获取配置失败", true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
* @param {string} action - 操作类型: 'get' | 'update' | 'reset'
|
||||
*/
|
||||
async function updateConfig(action) {
|
||||
try {
|
||||
if (action === "get") {
|
||||
await fetchConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "reset") {
|
||||
const requestData = {
|
||||
action: "reset",
|
||||
path: document.getElementById("path").value,
|
||||
};
|
||||
|
||||
const result = await makeAuthenticatedRequest("/config", {
|
||||
body: JSON.stringify(requestData),
|
||||
});
|
||||
|
||||
if (result) {
|
||||
showGlobalMessage(result.message, false);
|
||||
await fetchConfig();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取变更的配置
|
||||
const changes = getChangedConfig();
|
||||
|
||||
// 检查是否有实际变更
|
||||
if (Object.keys(changes).length <= 1) {
|
||||
showGlobalMessage("没有配置发生变更", false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建请求数据
|
||||
const requestData = {
|
||||
action: "update",
|
||||
...changes,
|
||||
};
|
||||
|
||||
console.log("发送的配置数据:", requestData);
|
||||
|
||||
const result = await makeAuthenticatedRequest("/config", {
|
||||
body: JSON.stringify(requestData),
|
||||
});
|
||||
|
||||
if (result) {
|
||||
showGlobalMessage(result.message, false);
|
||||
await fetchConfig();
|
||||
}
|
||||
} catch (error) {
|
||||
showGlobalMessage(error.message || "操作失败", true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理内容类型变更
|
||||
*/
|
||||
function handleContentTypeChange() {
|
||||
const contentType = document.getElementById("content_type").value;
|
||||
const textarea = document.getElementById("content");
|
||||
const isEditable = contentType !== "default";
|
||||
|
||||
textarea.disabled = !isEditable;
|
||||
|
||||
if (contentType === "redirect") {
|
||||
textarea.placeholder = "请输入重定向URL";
|
||||
} else {
|
||||
textarea.placeholder = "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理使用量检查模型类型变更
|
||||
*/
|
||||
function handleUsageCheckTypeChange() {
|
||||
const type = document.getElementById("usage_check_models_type").value;
|
||||
const listInput = document.getElementById("usage_check_models_list");
|
||||
listInput.style.display = type === "list" ? "inline-block" : "none";
|
||||
}
|
||||
|
||||
// 事件监听器
|
||||
document.getElementById("path").addEventListener("change", fetchConfig);
|
||||
document
|
||||
.getElementById("content_type")
|
||||
.addEventListener("change", handleContentTypeChange);
|
||||
document
|
||||
.getElementById("usage_check_models_type")
|
||||
.addEventListener("change", handleUsageCheckTypeChange);
|
||||
|
||||
// 初始化
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// 初始化 token 处理
|
||||
initializeTokenHandling("authToken");
|
||||
|
||||
// 加载初始配置
|
||||
try {
|
||||
await fetchConfig();
|
||||
showGlobalMessage("页面加载完成", false);
|
||||
} catch (error) {
|
||||
showGlobalMessage("初始化配置加载失败", true);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
2529
static/logs.html
Normal file
2529
static/logs.html
Normal file
File diff suppressed because it is too large
Load Diff
2014
static/proxies.html
Normal file
2014
static/proxies.html
Normal file
File diff suppressed because it is too large
Load Diff
528
static/shared-styles.css
Normal file
528
static/shared-styles.css
Normal file
@@ -0,0 +1,528 @@
|
||||
:root {
|
||||
/* 基础颜色变量 */
|
||||
--primary-color: #2196f3;
|
||||
--primary-dark: #1976d2;
|
||||
--primary-color-alpha: rgba(33, 150, 243, 0.1);
|
||||
--success-color: #4caf50;
|
||||
--error-color: #f44336;
|
||||
--background-color: #f5f5f5;
|
||||
--card-background: #ffffff;
|
||||
--text-primary: #333333;
|
||||
--text-secondary: #757575;
|
||||
--border-color: #e0e0e0;
|
||||
--disabled-bg: #f5f5f5;
|
||||
|
||||
/* 布局变量 */
|
||||
--border-radius: 8px;
|
||||
--spacing: 20px;
|
||||
|
||||
/* 动画变量 */
|
||||
--transition-fast: 0.2s;
|
||||
--transition-slow: 0.3s;
|
||||
}
|
||||
|
||||
/* 暗色模式 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--primary-color: #90caf9;
|
||||
--primary-dark: #64b5f6;
|
||||
--background-color: #121212;
|
||||
--card-background: #1e1e1e;
|
||||
--text-primary: #e0e0e0;
|
||||
--text-secondary: #9e9e9e;
|
||||
--border-color: #404040;
|
||||
--disabled-bg: #2d2d2d;
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
/* 基础样式 */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing);
|
||||
background: var(--background-color);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 容器样式 */
|
||||
.container {
|
||||
background: var(--card-background);
|
||||
padding: var(--spacing);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: var(--spacing);
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.container:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
color: var(--text-primary);
|
||||
margin-top: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 表单元素样式 */
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 标签样式 */
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background: var(--card-background);
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
transition: all var(--transition-fast);
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
appearance: auto;
|
||||
}
|
||||
|
||||
input[type="checkbox"] + label {
|
||||
cursor: pointer;
|
||||
color: var(--text-primary);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
input:hover,
|
||||
select:hover,
|
||||
textarea:hover,
|
||||
.form-control:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus,
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px var(--primary-color-alpha);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* 禁用状态 */
|
||||
input:disabled,
|
||||
select:disabled,
|
||||
textarea:disabled,
|
||||
.form-control:disabled {
|
||||
background-color: var(--disabled-bg);
|
||||
border-color: var(--border-color);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 错误状态 */
|
||||
input.error,
|
||||
select.error,
|
||||
textarea.error,
|
||||
.form-control.error {
|
||||
border-color: var(--error-color);
|
||||
}
|
||||
|
||||
/* Select 特殊样式 */
|
||||
select {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23757575'%3E%3Cpath d='M7 10l5 5 5-5H7z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: 20px;
|
||||
padding-right: 36px;
|
||||
}
|
||||
|
||||
/* Textarea 特殊样式 */
|
||||
textarea {
|
||||
min-height: 150px;
|
||||
resize: vertical;
|
||||
font-family: monospace;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 按钮基础样式 */
|
||||
button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 44px;
|
||||
padding: 8px 24px;
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* 按钮状态 */
|
||||
button:hover {
|
||||
background: var(--primary-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px var(--primary-color-alpha);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: var(--disabled-bg);
|
||||
color: var(--text-secondary);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 次要按钮样式 */
|
||||
button.secondary {
|
||||
background: transparent;
|
||||
border: 1px solid var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
background: var(--primary-color-alpha);
|
||||
border-color: var(--primary-dark);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background: var(--error-color);
|
||||
border: none;
|
||||
}
|
||||
|
||||
button.danger:hover {
|
||||
background: #d32f2f;
|
||||
/* 深红色 */
|
||||
box-shadow: 0 4px 12px rgba(244, 67, 54, 0.2);
|
||||
}
|
||||
|
||||
/* 激活状态的按钮 */
|
||||
button.active {
|
||||
background: var(--primary-dark);
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
button.secondary.active {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border-color: var(--primary-dark);
|
||||
}
|
||||
|
||||
/* 按钮组 */
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 按钮组中的按钮间距调整 */
|
||||
.button-group button {
|
||||
height: 38px;
|
||||
min-width: 100px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.button-group button .context-menu-shortcut {
|
||||
margin-left: 5px;
|
||||
opacity: 0.7;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 消息容器 - 固定在顶部中间 */
|
||||
.message-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
/* 允许点击穿透 */
|
||||
}
|
||||
|
||||
/* 单个消息样式 */
|
||||
.message {
|
||||
padding: 12px 20px;
|
||||
border-radius: 4px;
|
||||
background: var(--card-background);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
margin-bottom: 10px;
|
||||
pointer-events: auto;
|
||||
/* 允许消息本身可以交互 */
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
animation: messageIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #f0f9eb;
|
||||
border: 1px solid #e1f3d8;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #fef0f0;
|
||||
border: 1px solid #fde2e2;
|
||||
}
|
||||
|
||||
@keyframes messageIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes messageOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 深色模式适配 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.message {
|
||||
background: #2c2c2c;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #294929;
|
||||
border-color: #1c321c;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #4d2c2c;
|
||||
border-color: #321c1c;
|
||||
}
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: var(--spacing);
|
||||
background: var(--card-background);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--text-secondary);
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* 辅助类 */
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.mt-0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mb-0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 托盘消息容器 */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
z-index: 1000;
|
||||
max-width: 350px;
|
||||
max-height: 80vh;
|
||||
overflow-y: hidden;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
background: var(--card-background);
|
||||
color: var(--text-primary);
|
||||
padding: 10px 16px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition:
|
||||
opacity 0.4s cubic-bezier(0.25, 0.8, 0.25, 1),
|
||||
transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
margin-left: auto;
|
||||
will-change: transform, opacity;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.toast.info {
|
||||
border-left: 4px solid #2196f3;
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toast.warning {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
:root {
|
||||
--spacing: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
.form-control {
|
||||
font-size: 16px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
562
static/shared.js
Normal file
562
static/shared.js
Normal file
@@ -0,0 +1,562 @@
|
||||
// Token 管理功能
|
||||
/**
|
||||
* 保存认证令牌到本地存储
|
||||
* @param {string} token - 要保存的认证令牌
|
||||
* @returns {void}
|
||||
*/
|
||||
function saveAuthToken(token) {
|
||||
const expiryTime = new Date().getTime() + 24 * 60 * 60 * 1000; // 24小时后过期
|
||||
localStorage.setItem("authToken", token);
|
||||
localStorage.setItem("authTokenExpiry", expiryTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储的认证令牌
|
||||
* @returns {string|null} 如果令牌有效则返回令牌,否则返回 null
|
||||
*/
|
||||
function getAuthToken() {
|
||||
const token = localStorage.getItem("authToken");
|
||||
const expiry = localStorage.getItem("authTokenExpiry");
|
||||
|
||||
if (!token || !expiry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (new Date().getTime() > parseInt(expiry)) {
|
||||
localStorage.removeItem("authToken");
|
||||
localStorage.removeItem("authTokenExpiry");
|
||||
return null;
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
// 消息显示功能
|
||||
/**
|
||||
* 在指定元素中显示消息
|
||||
* @param {string} elementId - 目标元素的 ID
|
||||
* @param {string} text - 要显示的消息文本
|
||||
* @param {boolean} [isError=false] - 是否为错误消息
|
||||
* @returns {void}
|
||||
*/
|
||||
function showMessage(elementId, text, isError = false) {
|
||||
let msg = document.getElementById(elementId);
|
||||
|
||||
// 如果消息元素不存在,创建一个新的
|
||||
if (!msg) {
|
||||
msg = document.createElement("div");
|
||||
msg.id = elementId;
|
||||
document.body.appendChild(msg);
|
||||
}
|
||||
|
||||
msg.className = `floating-message ${isError ? "error" : "success"}`;
|
||||
msg.innerHTML = text.replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
// 确保消息容器存在
|
||||
/**
|
||||
* 确保消息容器存在于 DOM 中
|
||||
* @returns {HTMLElement} 消息容器元素
|
||||
*/
|
||||
function ensureMessageContainer() {
|
||||
let container = document.querySelector(".message-container");
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.className = "message-container";
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示全局消息提示
|
||||
* @param {string} text - 要显示的消息文本
|
||||
* @param {boolean} [isError=false] - 是否为错误消息
|
||||
* @param {number} [timeout=3000] - 消息显示时长(毫秒)
|
||||
* @returns {void}
|
||||
*/
|
||||
function showGlobalMessage(text, isError = false, timeout = 3000) {
|
||||
const container = ensureMessageContainer();
|
||||
|
||||
const msgElement = document.createElement("div");
|
||||
msgElement.className = `message ${isError ? "error" : "success"}`;
|
||||
msgElement.textContent = text;
|
||||
|
||||
container.appendChild(msgElement);
|
||||
|
||||
// 设置淡出动画和移除
|
||||
setTimeout(() => {
|
||||
msgElement.style.animation = "messageOut 0.3s ease-in-out";
|
||||
setTimeout(() => {
|
||||
msgElement.remove();
|
||||
// 如果容器为空,也移除容器
|
||||
if (container.children.length === 0) {
|
||||
container.remove();
|
||||
}
|
||||
}, 300);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
// Token 输入框自动填充和事件绑定
|
||||
function initializeTokenHandling(inputId) {
|
||||
// 直接尝试填充,如果DOM未准备好会在事件中再试一次
|
||||
const tryFillToken = () => {
|
||||
const tokenInput = document.getElementById(inputId);
|
||||
if (tokenInput) {
|
||||
const authToken = getAuthToken();
|
||||
if (authToken) {
|
||||
tokenInput.value = authToken;
|
||||
}
|
||||
|
||||
// 绑定change事件
|
||||
tokenInput.addEventListener("change", (e) => {
|
||||
if (e.target.value) {
|
||||
saveAuthToken(e.target.value);
|
||||
} else {
|
||||
localStorage.removeItem("authToken");
|
||||
localStorage.removeItem("authTokenExpiry");
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 立即尝试执行
|
||||
if (!tryFillToken()) {
|
||||
// 如果元素还不存在,等待DOM加载完成
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener("DOMContentLoaded", tryFillToken);
|
||||
} else {
|
||||
// DOM已加载但元素不存在,可能需要等待一下
|
||||
setTimeout(tryFillToken, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// API 请求通用处理
|
||||
async function makeAuthenticatedRequest(url, options = {}) {
|
||||
const tokenId = options.tokenId || "authToken";
|
||||
const token = document.getElementById(tokenId).value;
|
||||
|
||||
if (!token) {
|
||||
showGlobalMessage("请输入 AUTH_TOKEN", true);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!/^[A-Za-z0-9\-._~+/]+=*$/.test(cleanToken)) {
|
||||
showGlobalMessage("TOKEN格式无效,请检查是否包含特殊字符", true);
|
||||
return null;
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { ...defaultOptions, ...options });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
showGlobalMessage(`请求失败: ${error.message}`, true);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从字符串解析布尔值
|
||||
* @param {string} str - 要解析的字符串
|
||||
* @param {boolean|null} defaultValue - 解析失败时的默认值
|
||||
* @returns {boolean|null} 解析结果,如果无法解析则返回默认值
|
||||
*/
|
||||
function parseBooleanFromString(str, defaultValue = null) {
|
||||
if (typeof str !== "string") {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const lowercaseStr = str.toLowerCase().trim();
|
||||
|
||||
if (lowercaseStr === "true" || lowercaseStr === "1") {
|
||||
return true;
|
||||
} else if (lowercaseStr === "false" || lowercaseStr === "0") {
|
||||
return false;
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将布尔值转换为字符串
|
||||
* @param {boolean|undefined|null} value - 要转换的布尔值
|
||||
* @param {string} defaultValue - 转换失败时的默认值
|
||||
* @returns {string} 转换结果,如果输入无效则返回默认值
|
||||
*/
|
||||
function parseStringFromBoolean(value, defaultValue = null) {
|
||||
if (typeof value !== "boolean") {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return value ? "true" : "false";
|
||||
}
|
||||
|
||||
/**
|
||||
* 将会员类型代码转换为显示名称
|
||||
* @param {string|null} type - 会员类型代码,如 'free_trial', 'pro', 'free', 'enterprise' 等
|
||||
* @returns {string} 格式化后的会员类型显示名称
|
||||
* @example
|
||||
* formatMembershipType('free_trial') // 返回 'Pro Trial'
|
||||
* formatMembershipType('pro') // 返回 'Pro'
|
||||
* formatMembershipType(null) // 返回 '-'
|
||||
* formatMembershipType('custom_type') // 返回 'Custom Type'
|
||||
*/
|
||||
function formatMembershipType(type) {
|
||||
if (!type) return "-";
|
||||
switch (type) {
|
||||
case "free_trial":
|
||||
return "Pro Trial";
|
||||
case "pro":
|
||||
return "Pro";
|
||||
case "free":
|
||||
return "Free";
|
||||
case "enterprise":
|
||||
return "Business";
|
||||
default:
|
||||
return type
|
||||
.split("_")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
// 复制文本功能
|
||||
/**
|
||||
* 复制文本到剪贴板
|
||||
* @param {string} text - 要复制的文本
|
||||
* @param {Object} [options={}] - 配置选项
|
||||
* @param {boolean} [options.showMessage=true] - 是否显示复制结果消息
|
||||
* @param {string} [options.successMessage='已复制到剪贴板'] - 复制成功时的消息
|
||||
* @param {string} [options.errorMessage='复制失败,请手动复制'] - 复制失败时的消息
|
||||
* @param {Function} [options.onSuccess] - 复制成功时的回调函数
|
||||
* @param {Function} [options.onError] - 复制失败时的回调函数
|
||||
* @param {HTMLElement} [options.sourceElement] - 触发复制的源元素(用于显示临时状态)
|
||||
* @returns {Promise<boolean>} 返回复制是否成功
|
||||
* @example
|
||||
* // 基础用法
|
||||
* copyToClipboard('Hello World');
|
||||
*
|
||||
* // 自定义消息
|
||||
* copyToClipboard('代理地址', {
|
||||
* successMessage: '代理地址已复制',
|
||||
* errorMessage: '无法复制代理地址'
|
||||
* });
|
||||
*
|
||||
* // 带回调函数
|
||||
* copyToClipboard('敏感信息', {
|
||||
* showMessage: false,
|
||||
* onSuccess: () => console.log('复制成功'),
|
||||
* onError: (err) => console.error('复制失败:', err)
|
||||
* });
|
||||
*
|
||||
* // 与按钮配合使用
|
||||
* const button = document.getElementById('copyBtn');
|
||||
* copyToClipboard('文本内容', { sourceElement: button });
|
||||
*/
|
||||
async function copyToClipboard(text, options = {}) {
|
||||
const {
|
||||
showMessage = true,
|
||||
successMessage = "已复制到剪贴板",
|
||||
errorMessage = "复制失败,请手动复制",
|
||||
onSuccess,
|
||||
onError,
|
||||
sourceElement,
|
||||
} = options;
|
||||
|
||||
// 验证输入
|
||||
if (typeof text !== "string") {
|
||||
console.error("copyToClipboard: 文本必须是字符串类型");
|
||||
if (showMessage) {
|
||||
showGlobalMessage("无效的复制内容", true);
|
||||
}
|
||||
if (onError) {
|
||||
onError(new Error("Invalid text type"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果文本为空,给出警告
|
||||
if (!text.trim()) {
|
||||
console.warn("copyToClipboard: 尝试复制空文本");
|
||||
if (showMessage) {
|
||||
showGlobalMessage("没有可复制的内容", true);
|
||||
}
|
||||
if (onError) {
|
||||
onError(new Error("Empty text"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 优先使用现代 Clipboard API
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
handleCopySuccess();
|
||||
return true;
|
||||
} else {
|
||||
// 降级到传统方法
|
||||
const success = fallbackCopyToClipboard(text);
|
||||
if (success) {
|
||||
handleCopySuccess();
|
||||
return true;
|
||||
} else {
|
||||
throw new Error("Fallback copy failed");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("复制到剪贴板失败:", error);
|
||||
|
||||
if (showMessage) {
|
||||
showGlobalMessage(errorMessage, true);
|
||||
}
|
||||
|
||||
if (onError) {
|
||||
onError(error);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 处理复制成功
|
||||
function handleCopySuccess() {
|
||||
if (showMessage) {
|
||||
showGlobalMessage(successMessage);
|
||||
}
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
|
||||
// 如果提供了源元素,可以添加临时的视觉反馈
|
||||
if (sourceElement) {
|
||||
addTemporaryClass(sourceElement, "copied", 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 传统的复制方法(用于不支持 Clipboard API 的浏览器)
|
||||
* @private
|
||||
* @param {string} text - 要复制的文本
|
||||
* @returns {boolean} 是否复制成功
|
||||
*/
|
||||
function fallbackCopyToClipboard(text) {
|
||||
// 创建临时文本区域
|
||||
const textArea = document.createElement("textarea");
|
||||
|
||||
// 设置样式使其不可见但可复制
|
||||
textArea.value = text;
|
||||
textArea.style.position = "fixed";
|
||||
textArea.style.top = "0";
|
||||
textArea.style.left = "0";
|
||||
textArea.style.width = "2em";
|
||||
textArea.style.height = "2em";
|
||||
textArea.style.padding = "0";
|
||||
textArea.style.border = "none";
|
||||
textArea.style.outline = "none";
|
||||
textArea.style.boxShadow = "none";
|
||||
textArea.style.background = "transparent";
|
||||
textArea.style.opacity = "0";
|
||||
textArea.style.pointerEvents = "none";
|
||||
|
||||
// 防止移动设备上的缩放
|
||||
textArea.style.fontSize = "12pt";
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
|
||||
try {
|
||||
// 选择文本
|
||||
textArea.select();
|
||||
textArea.setSelectionRange(0, text.length);
|
||||
|
||||
// 执行复制
|
||||
const successful = document.execCommand("copy");
|
||||
|
||||
// 清理
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
return successful;
|
||||
} catch (error) {
|
||||
console.error("传统复制方法失败:", error);
|
||||
// 确保清理
|
||||
if (document.body.contains(textArea)) {
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为元素临时添加 CSS 类
|
||||
* @private
|
||||
* @param {HTMLElement} element - 目标元素
|
||||
* @param {string} className - 要添加的类名
|
||||
* @param {number} duration - 持续时间(毫秒)
|
||||
*/
|
||||
function addTemporaryClass(element, className, duration) {
|
||||
if (!element || !className) return;
|
||||
|
||||
element.classList.add(className);
|
||||
setTimeout(() => {
|
||||
element.classList.remove(className);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制表格单元格内容
|
||||
* @param {HTMLElement} cell - 表格单元格元素
|
||||
* @param {Object} [options={}] - 复制选项(同 copyToClipboard)
|
||||
* @returns {Promise<boolean>} 是否复制成功
|
||||
* @example
|
||||
* // 在表格单元格点击事件中使用
|
||||
* td.onclick = () => copyTableCellContent(td);
|
||||
*/
|
||||
async function copyTableCellContent(cell, options = {}) {
|
||||
if (!cell) {
|
||||
console.error("copyTableCellContent: 未提供有效的单元格元素");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取纯文本内容(去除 HTML 标签)
|
||||
const text = cell.textContent || cell.innerText || "";
|
||||
|
||||
return copyToClipboard(text.trim(), {
|
||||
...options,
|
||||
sourceElement: cell,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带复制功能的按钮
|
||||
* @param {string} text - 要复制的文本
|
||||
* @param {Object} [options={}] - 按钮配置选项
|
||||
* @param {string} [options.buttonText='复制'] - 按钮文本
|
||||
* @param {string} [options.buttonClass='copy-button'] - 按钮CSS类
|
||||
* @param {string} [options.copiedText='已复制'] - 复制成功后的按钮文本
|
||||
* @param {number} [options.resetDelay=2000] - 按钮文本重置延迟(毫秒)
|
||||
* @returns {HTMLButtonElement} 创建的按钮元素
|
||||
* @example
|
||||
* // 创建一个复制按钮
|
||||
* const copyBtn = createCopyButton('要复制的文本', {
|
||||
* buttonText: '复制密钥',
|
||||
* copiedText: '✓ 已复制'
|
||||
* });
|
||||
* document.getElementById('container').appendChild(copyBtn);
|
||||
*/
|
||||
function createCopyButton(text, options = {}) {
|
||||
const {
|
||||
buttonText = "复制",
|
||||
buttonClass = "copy-button",
|
||||
copiedText = "已复制",
|
||||
resetDelay = 2000,
|
||||
} = options;
|
||||
|
||||
const button = document.createElement("button");
|
||||
button.textContent = buttonText;
|
||||
button.className = buttonClass;
|
||||
button.type = "button";
|
||||
|
||||
button.addEventListener("click", async () => {
|
||||
const originalText = button.textContent;
|
||||
|
||||
const success = await copyToClipboard(text, {
|
||||
sourceElement: button,
|
||||
showMessage: true,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
button.textContent = copiedText;
|
||||
button.disabled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
button.disabled = false;
|
||||
}, resetDelay);
|
||||
}
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查剪贴板 API 是否可用
|
||||
* @returns {boolean} 是否支持 Clipboard API
|
||||
* @example
|
||||
* if (isClipboardSupported()) {
|
||||
* console.log('浏览器支持现代剪贴板 API');
|
||||
* }
|
||||
*/
|
||||
function isClipboardSupported() {
|
||||
return !!(navigator.clipboard && window.isSecureContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从剪贴板读取文本(需要用户权限)
|
||||
* @param {Object} [options={}] - 配置选项
|
||||
* @param {boolean} [options.showMessage=true] - 是否显示结果消息
|
||||
* @param {Function} [options.onSuccess] - 读取成功时的回调
|
||||
* @param {Function} [options.onError] - 读取失败时的回调
|
||||
* @returns {Promise<string|null>} 剪贴板中的文本,失败时返回 null
|
||||
* @example
|
||||
* const text = await readFromClipboard();
|
||||
* if (text) {
|
||||
* console.log('剪贴板内容:', text);
|
||||
* }
|
||||
*/
|
||||
async function readFromClipboard(options = {}) {
|
||||
const { showMessage = true, onSuccess, onError } = options;
|
||||
|
||||
if (!isClipboardSupported()) {
|
||||
const error = new Error("浏览器不支持剪贴板 API");
|
||||
if (showMessage) {
|
||||
showGlobalMessage("浏览器不支持读取剪贴板", true);
|
||||
}
|
||||
if (onError) {
|
||||
onError(error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess(text);
|
||||
}
|
||||
|
||||
return text;
|
||||
} catch (error) {
|
||||
console.error("读取剪贴板失败:", error);
|
||||
|
||||
if (showMessage) {
|
||||
if (error.name === "NotAllowedError") {
|
||||
showGlobalMessage("需要您的许可才能读取剪贴板", true);
|
||||
} else {
|
||||
showGlobalMessage("无法读取剪贴板内容", true);
|
||||
}
|
||||
}
|
||||
|
||||
if (onError) {
|
||||
onError(error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
3743
static/tokens.html
Normal file
3743
static/tokens.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user