1186 lines
56 KiB
HTML
1186 lines
56 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>股票分析系统</title>
|
||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||
</head>
|
||
<body class="bg-gray-100">
|
||
|
||
<!-- 添加公告栏 -->
|
||
{% if announcement %}
|
||
<div id="announcement-container" class="fixed top-4 right-4 max-w-md z-50 animate-fade-in-down">
|
||
<div class="bg-blue-50 border-l-4 border-blue-500 p-4 rounded-lg shadow-lg">
|
||
<div class="flex">
|
||
<div class="flex-shrink-0">
|
||
<svg class="h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||
</svg>
|
||
</div>
|
||
<div class="ml-3 pr-8">
|
||
<p class="text-sm text-blue-700 whitespace-pre-line" id="announcement-text"></p>
|
||
<p class="text-xs text-gray-500 mt-1" id="announcement-timer"></p>
|
||
</div>
|
||
<button onclick="closeAnnouncement()" class="absolute top-2 right-2 text-gray-400 hover:text-gray-600">
|
||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 处理公告文本中的URL
|
||
const announcementText = `{{ announcement }}`;
|
||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||
document.getElementById('announcement-text').innerHTML = announcementText.replace(
|
||
urlRegex,
|
||
'<a href="$1" target="_blank" class="text-blue-600 hover:text-blue-800 underline">$1</a>'
|
||
);
|
||
|
||
// 设置自动关闭时间(5秒)
|
||
const autoCloseTime = 5;
|
||
let remainingTime = autoCloseTime;
|
||
const timerElement = document.getElementById('announcement-timer');
|
||
|
||
// 更新倒计时显示
|
||
function updateTimer() {
|
||
timerElement.textContent = `${remainingTime}秒后自动关闭`;
|
||
if (remainingTime <= 0) {
|
||
closeAnnouncement();
|
||
} else {
|
||
remainingTime--;
|
||
setTimeout(updateTimer, 1000);
|
||
}
|
||
}
|
||
|
||
// 启动倒计时
|
||
updateTimer();
|
||
|
||
// 关闭公告的函数
|
||
function closeAnnouncement() {
|
||
const container = document.getElementById('announcement-container');
|
||
container.style.opacity = '0';
|
||
container.style.transform = 'translateY(-10px)';
|
||
container.style.transition = 'all 0.3s ease-out';
|
||
setTimeout(() => {
|
||
container.remove();
|
||
}, 300);
|
||
}
|
||
</script>
|
||
{% endif %}
|
||
|
||
<div class="container mx-auto px-4 py-8">
|
||
<h1 class="text-3xl font-bold mb-8 text-center">股票分析系统</h1>
|
||
|
||
<!-- 添加市场时间显示 -->
|
||
<div class="max-w-4xl mx-auto mb-8">
|
||
<div class="bg-white rounded-lg shadow-md p-6">
|
||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||
<!-- 当前时间 -->
|
||
<div class="text-center">
|
||
<p class="text-gray-600 mb-2">当前时间</p>
|
||
<p id="currentTime" class="text-2xl font-bold text-gray-800"></p>
|
||
</div>
|
||
|
||
<!-- A股状态 -->
|
||
<div class="text-center">
|
||
<p class="text-gray-600 mb-2">A股市场</p>
|
||
<p id="cnMarketStatus" class="text-lg font-medium"></p>
|
||
<p id="cnMarketTimer" class="text-sm text-gray-500 mt-1"></p>
|
||
</div>
|
||
|
||
<!-- 港股状态 -->
|
||
<div class="text-center">
|
||
<p class="text-gray-600 mb-2">港股市场</p>
|
||
<p id="hkMarketStatus" class="text-lg font-medium"></p>
|
||
<p id="hkMarketTimer" class="text-sm text-gray-500 mt-1"></p>
|
||
</div>
|
||
|
||
<!-- 美股状态 -->
|
||
<div class="text-center">
|
||
<p class="text-gray-600 mb-2">美股市场</p>
|
||
<p id="usMarketStatus" class="text-lg font-medium"></p>
|
||
<p id="usMarketTimer" class="text-sm text-gray-500 mt-1"></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 更新时间显示
|
||
function updateTime() {
|
||
const now = new Date();
|
||
|
||
// 更新当前时间
|
||
document.getElementById('currentTime').textContent =
|
||
now.toLocaleTimeString('zh-CN', { hour12: false });
|
||
|
||
// 获取中国时间
|
||
const cnOptions = { timeZone: 'Asia/Shanghai', hour12: false };
|
||
const cnTime = now.toLocaleString('zh-CN', cnOptions);
|
||
const cnHour = new Date(cnTime).getHours();
|
||
const cnMinute = new Date(cnTime).getMinutes();
|
||
|
||
// A股市场状态
|
||
const cnMarketOpen = (cnHour === 9 && cnMinute >= 30) || (cnHour === 10) ||
|
||
(cnHour === 11 && cnMinute <= 30) ||
|
||
(cnHour >= 13 && cnHour < 15);
|
||
|
||
updateMarketStatus('cn', cnMarketOpen, cnHour, cnMinute, 9, 30, 15, 0);
|
||
|
||
// 港股市场状态(与A股相同时区)
|
||
const hkMarketOpen = (cnHour === 9 && cnMinute >= 30) ||
|
||
(cnHour === 10) || (cnHour === 11) ||
|
||
(cnHour >= 13 && cnHour < 16);
|
||
|
||
updateMarketStatus('hk', hkMarketOpen, cnHour, cnMinute, 9, 30, 16, 0);
|
||
|
||
// 获取美国东部时间
|
||
const usOptions = { timeZone: 'America/New_York', hour12: false };
|
||
const usTime = now.toLocaleString('zh-CN', usOptions);
|
||
const usHour = new Date(usTime).getHours();
|
||
const usMinute = new Date(usTime).getMinutes();
|
||
|
||
// 美股市场状态
|
||
const usMarketOpen = (usHour >= 9 && usHour < 16) ||
|
||
(usHour === 16 && usMinute === 0);
|
||
|
||
updateMarketStatus('us', usMarketOpen, usHour, usMinute, 9, 30, 16, 0);
|
||
}
|
||
|
||
// 更新市场状态的通用函数
|
||
function updateMarketStatus(market, isOpen, currentHour, currentMinute, openHour, openMinute, closeHour, closeMinute) {
|
||
const element = document.getElementById(`${market}MarketStatus`);
|
||
const timer = document.getElementById(`${market}MarketTimer`);
|
||
|
||
if (isOpen) {
|
||
// ... 现有的开市逻辑保持不变 ...
|
||
} else {
|
||
element.textContent = '已休市';
|
||
element.className = 'text-lg font-medium text-gray-600';
|
||
|
||
// 计算距离下一个开盘时间
|
||
const now = new Date();
|
||
const nextOpen = new Date(now);
|
||
|
||
if (market === 'us') {
|
||
// 获取美东时间的下一个开盘时间
|
||
const usTime = new Date(now.toLocaleString('en-US', { timeZone: 'America/New_York' }));
|
||
const usHour = usTime.getHours();
|
||
|
||
// 如果当前美东时间已过收盘时间,设置为下一个交易日
|
||
if (usHour >= closeHour) {
|
||
nextOpen.setDate(nextOpen.getDate() + 1);
|
||
}
|
||
|
||
// 设置为美东时间的开盘时间
|
||
nextOpen.setHours(openHour + 12, openMinute, 0); // 加12小时转换为北京时间
|
||
} else {
|
||
// 其他市场的逻辑保持不变
|
||
if (currentHour >= closeHour) {
|
||
nextOpen.setDate(nextOpen.getDate() + 1);
|
||
}
|
||
nextOpen.setHours(openHour, openMinute, 0);
|
||
}
|
||
|
||
const timeToOpen = nextOpen - now;
|
||
const hours = Math.floor(timeToOpen / 3600000);
|
||
const minutes = Math.floor((timeToOpen % 3600000) / 60000);
|
||
|
||
// 确保显示的时间始终为正数
|
||
if (hours >= 0 && minutes >= 0) {
|
||
timer.textContent = `距离开盘还有 ${hours}小时${minutes}分钟`;
|
||
} else {
|
||
timer.textContent = '计算开盘时间中...';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 每秒更新一次时间
|
||
setInterval(updateTime, 1000);
|
||
updateTime(); // 立即执行一次
|
||
</script>
|
||
|
||
<div class="max-w-4xl mx-auto">
|
||
<!-- 批量分析 -->
|
||
<div class="bg-white p-6 rounded-lg shadow-md">
|
||
<h2 class="text-xl font-semibold mb-4">股票批量分析</h2>
|
||
|
||
<!-- API配置部分 -->
|
||
<div class="mb-6 border-b pb-6">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h3 class="text-lg font-medium text-gray-700">API配置</h3>
|
||
<button id="toggleApiConfig" class="text-blue-600 hover:text-blue-800 text-sm flex items-center">
|
||
<span id="toggleApiConfigText">显示配置</span>
|
||
<svg id="toggleApiConfigIcon" class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div id="apiConfigPanel" class="hidden space-y-4">
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label for="apiUrl" class="block text-sm font-medium text-gray-700 mb-1">API URL</label>
|
||
<input type="text" id="apiUrl"
|
||
class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="例如: https://api.openai.com"
|
||
value="{{ default_api_url }}"
|
||
oninput="updateFormattedUrl(this.value)">
|
||
<p id="formattedUrl" class="mt-1 text-sm text-gray-500 break-all"></p>
|
||
</div>
|
||
<div>
|
||
<label for="apiModel" class="block text-sm font-medium text-gray-700 mb-1">API 模型</label>
|
||
<input type="text" id="apiModel"
|
||
class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="例如: gpt-3.5-turbo"
|
||
value="{{ default_api_model }}">
|
||
</div>
|
||
</div>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label for="apiKey" class="block text-sm font-medium text-gray-700 mb-1">API Key</label>
|
||
<input type="password" id="apiKey"
|
||
class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="输入您的API Key">
|
||
<p class="mt-1 text-sm text-gray-500">如不填写,将使用系统默认配置</p>
|
||
</div>
|
||
<div>
|
||
<label for="apiTimeout" class="block text-sm font-medium text-gray-700 mb-1">API 超时时间 (秒)</label>
|
||
<input type="number" id="apiTimeout"
|
||
class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="例如: 60"
|
||
value="{{ default_api_timeout }}" min="1" max="300">
|
||
<p class="mt-1 text-sm text-gray-500">请求超时时间,默认60秒</p>
|
||
</div>
|
||
</div>
|
||
<div class="flex justify-between">
|
||
<div class="flex items-center">
|
||
<input type="checkbox" id="saveApiConfig" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||
<label for="saveApiConfig" class="ml-2 block text-sm text-gray-700">保存配置到本地</label>
|
||
</div>
|
||
<div>
|
||
<button id="resetApiConfig" class="text-gray-600 hover:text-gray-800 text-sm mr-3">
|
||
重置为默认
|
||
</button>
|
||
<button id="testApiConfig" class="bg-blue-100 text-blue-700 px-3 py-1 rounded hover:bg-blue-200 text-sm">
|
||
测试连接
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 添加市场类型选择 -->
|
||
<div class="mb-4">
|
||
<label for="marketType" class="block text-sm font-medium text-gray-700 mb-2">
|
||
选择市场类型
|
||
</label>
|
||
<select id="marketType" onchange="handleMarketTypeChange()"
|
||
class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||
<option value="A">A股</option>
|
||
<option value="HK">港股</option>
|
||
<option value="US">美股</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 美股搜索框 -->
|
||
<div id="usStockSearch" class="mb-4 hidden">
|
||
<div class="relative">
|
||
<input type="text"
|
||
id="searchInput"
|
||
class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="输入美股名称搜索(中文和英文都试下)"
|
||
oninput="handleSearchInput(event)">
|
||
<!-- 添加搜索 loading 图标 -->
|
||
<div id="searchLoading" class="absolute right-3 top-2.5 hidden">
|
||
<svg class="animate-spin h-5 w-5 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||
</svg>
|
||
</div>
|
||
<!-- 搜索结果下拉框 -->
|
||
<div id="searchResults"
|
||
class="absolute z-10 w-full mt-1 bg-white border rounded-md shadow-lg hidden">
|
||
</div>
|
||
<!-- 添加错误提示 -->
|
||
<div id="searchError" class="hidden absolute z-10 w-full mt-1 p-3 bg-red-50 text-red-600 rounded-md border border-red-200">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 修改搜索相关函数 -->
|
||
<script>
|
||
// 创建防抖函数
|
||
function debounce(func, wait) {
|
||
let timeout;
|
||
return function (...args) {
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(() => {
|
||
func.apply(this, args);
|
||
}, wait);
|
||
};
|
||
}
|
||
|
||
// 使用防抖包装搜索函数
|
||
const debouncedSearch = debounce(async (keyword) => {
|
||
if (!keyword) {
|
||
document.getElementById('searchResults').classList.add('hidden');
|
||
document.getElementById('searchError').classList.add('hidden');
|
||
return;
|
||
}
|
||
|
||
// 显示 loading
|
||
document.getElementById('searchLoading').classList.remove('hidden');
|
||
// 隐藏之前的错误信息
|
||
document.getElementById('searchError').classList.add('hidden');
|
||
|
||
try {
|
||
const response = await fetch(`/search_us_stocks?keyword=${encodeURIComponent(keyword)}`);
|
||
const data = await response.json();
|
||
|
||
if (!response.ok) {
|
||
throw new Error(data.error || '搜索失败');
|
||
}
|
||
|
||
displaySearchResults(data.results);
|
||
} catch (error) {
|
||
console.error('搜索出错:', error);
|
||
const errorDiv = document.getElementById('searchError');
|
||
errorDiv.textContent = `搜索出错: ${error.message}`;
|
||
errorDiv.classList.remove('hidden');
|
||
document.getElementById('searchResults').classList.add('hidden');
|
||
} finally {
|
||
document.getElementById('searchLoading').classList.add('hidden');
|
||
}
|
||
}, 500); // 设置500ms的防抖延迟
|
||
|
||
// 修改输入事件处理函数
|
||
function handleSearchInput(event) {
|
||
debouncedSearch(event.target.value);
|
||
}
|
||
</script>
|
||
<div class="mb-4">
|
||
<label for="batchStocks" class="block text-sm font-medium text-gray-700 mb-2">
|
||
输入代码
|
||
</label>
|
||
<textarea id="batchStocks"
|
||
class="w-full p-2 border rounded h-32"
|
||
placeholder="输入代码,支持多个代码(用回车或逗号分隔)"></textarea>
|
||
</div>
|
||
|
||
<!-- 添加新的 JavaScript 代码 -->
|
||
<script>
|
||
let searchTimeout;
|
||
|
||
function handleMarketTypeChange() {
|
||
const marketType = document.getElementById('marketType').value;
|
||
const searchDiv = document.getElementById('usStockSearch');
|
||
searchDiv.classList.toggle('hidden', marketType !== 'US');
|
||
|
||
// 清空搜索结果
|
||
document.getElementById('searchResults').innerHTML = '';
|
||
document.getElementById('searchInput').value = '';
|
||
}
|
||
|
||
function debounceSearch(event) {
|
||
clearTimeout(searchTimeout);
|
||
searchTimeout = setTimeout(() => {
|
||
searchUsStocks(event.target.value);
|
||
}, 300);
|
||
}
|
||
|
||
function displaySearchResults(results) {
|
||
const resultsDiv = document.getElementById('searchResults');
|
||
|
||
if (!results || results.length === 0) {
|
||
resultsDiv.classList.add('hidden');
|
||
return;
|
||
}
|
||
|
||
let html = '<div class="py-1">';
|
||
results.forEach(stock => {
|
||
html += `
|
||
<div class="px-4 py-2 hover:bg-gray-100 cursor-pointer flex justify-between items-center"
|
||
onclick="selectStock('${stock.symbol}')">
|
||
<div>
|
||
<div class="font-medium">${stock.name}</div>
|
||
<div class="text-sm text-gray-500">${stock.symbol}</div>
|
||
</div>
|
||
<div class="text-right">
|
||
<div class="font-medium">$${stock.price}</div>
|
||
<div class="text-sm text-gray-500">市值: ${formatMarketValue(stock.market_value)}</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
html += '</div>';
|
||
|
||
resultsDiv.innerHTML = html;
|
||
resultsDiv.classList.remove('hidden');
|
||
}
|
||
|
||
function formatMarketValue(value) {
|
||
if (value >= 1e12) {
|
||
return (value / 1e12).toFixed(2) + '万亿';
|
||
} else if (value >= 1e8) {
|
||
return (value / 1e8).toFixed(2) + '亿';
|
||
} else if (value >= 1e4) {
|
||
return (value / 1e4).toFixed(2) + '万';
|
||
}
|
||
return value.toString();
|
||
}
|
||
|
||
function selectStock(symbol) {
|
||
const textarea = document.getElementById('batchStocks');
|
||
const currentValue = textarea.value.trim();
|
||
|
||
|
||
// 添加到 textarea
|
||
textarea.value = currentValue
|
||
? currentValue + '\n' + symbol
|
||
: symbol;
|
||
|
||
// 清空并隐藏搜索结果
|
||
document.getElementById('searchInput').value = '';
|
||
document.getElementById('searchResults').classList.add('hidden');
|
||
}
|
||
|
||
// 点击外部时隐藏搜索结果
|
||
document.addEventListener('click', function(event) {
|
||
const searchResults = document.getElementById('searchResults');
|
||
const searchInput = document.getElementById('searchInput');
|
||
|
||
if (!searchResults.contains(event.target) &&
|
||
!searchInput.contains(event.target)) {
|
||
searchResults.classList.add('hidden');
|
||
}
|
||
});
|
||
</script>
|
||
<button id="analyzeBtn" onclick="analyzeStocks()"
|
||
class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700 flex items-center justify-center">
|
||
<span>开始分析</span>
|
||
<div id="loadingSpinner" class="hidden ml-2">
|
||
<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||
</svg>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 结果展示 -->
|
||
<div id="results" class="mt-8">
|
||
<div class="flex justify-between items-center mb-6">
|
||
<h2 class="text-2xl font-bold text-gray-800">分析结果</h2>
|
||
<button onclick="copyAnalysisResults()"
|
||
class="flex items-center text-blue-600 hover:text-blue-700">
|
||
<svg class="w-5 h-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/>
|
||
</svg>
|
||
复制分析结果
|
||
</button>
|
||
</div>
|
||
<div id="resultContent" class="space-y-8"></div>
|
||
</div>
|
||
|
||
<script>
|
||
function copyAnalysisResults() {
|
||
const resultContent = document.getElementById('resultContent');
|
||
if (!resultContent.textContent.trim()) {
|
||
alert('暂无分析结果可复制');
|
||
return;
|
||
}
|
||
|
||
// 提取需要复制的文本
|
||
let copyText = '';
|
||
const results = resultContent.querySelectorAll('.bg-white');
|
||
|
||
results.forEach(result => {
|
||
// 获取股票代码
|
||
const stockCode = result.querySelector('h3').textContent.trim();
|
||
copyText += `股票代码:${stockCode}\n`;
|
||
|
||
// 获取主要指标
|
||
const indicators = result.querySelectorAll('.flex.justify-between');
|
||
indicators.forEach(indicator => {
|
||
const label = indicator.querySelector('.text-gray-600').textContent;
|
||
const value = indicator.querySelector('.font-medium').textContent;
|
||
copyText += `${label}:${value}\n`;
|
||
});
|
||
|
||
// 获取 AI 分析内容
|
||
const aiAnalysis = result.querySelector('.prose').textContent;
|
||
copyText += `\nAI分析:\n${aiAnalysis}\n`;
|
||
|
||
copyText += '\n----------------------------------------\n\n';
|
||
});
|
||
|
||
// 复制到剪贴板
|
||
const textarea = document.createElement('textarea');
|
||
textarea.value = copyText;
|
||
document.body.appendChild(textarea);
|
||
textarea.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(textarea);
|
||
|
||
// 显示提示
|
||
alert('分析结果已复制到剪贴板');
|
||
}
|
||
</script>
|
||
<script>
|
||
let isAnalyzing = false;
|
||
let stockAnalysisData = {}; // 存储股票分析数据的对象
|
||
|
||
async function analyzeStocks() {
|
||
if (isAnalyzing) return; // 防止重复点击
|
||
|
||
const stockInput = document.getElementById('batchStocks').value.trim();
|
||
const marketType = document.getElementById('marketType').value;
|
||
const analyzeBtn = document.getElementById('analyzeBtn');
|
||
const loadingSpinner = document.getElementById('loadingSpinner');
|
||
const resultContent = document.getElementById('resultContent');
|
||
|
||
// 获取API配置
|
||
const apiUrl = document.getElementById('apiUrl').value.trim();
|
||
const apiKey = document.getElementById('apiKey').value.trim();
|
||
const apiModel = document.getElementById('apiModel').value.trim();
|
||
const apiTimeout = document.getElementById('apiTimeout').value.trim();
|
||
|
||
if (!stockInput) {
|
||
alert('请输入代码');
|
||
return;
|
||
}
|
||
|
||
const stockCodes = stockInput.split(/[\n,]/)
|
||
.map(code => code.trim())
|
||
.filter(code => code.length > 0);
|
||
|
||
try {
|
||
isAnalyzing = true;
|
||
analyzeBtn.disabled = true;
|
||
loadingSpinner.classList.remove('hidden');
|
||
analyzeBtn.querySelector('span').textContent = '分析中...';
|
||
|
||
// 清空现有结果并初始化分析数据
|
||
resultContent.innerHTML = '';
|
||
stockAnalysisData = {};
|
||
|
||
// 创建结果容器
|
||
const resultsContainer = document.createElement('div');
|
||
resultsContainer.className = 'space-y-6';
|
||
resultContent.appendChild(resultsContainer);
|
||
|
||
// 使用fetch流式API
|
||
const response = await fetch('/analyze', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
stock_codes: stockCodes,
|
||
market_type: marketType,
|
||
api_url: apiUrl,
|
||
api_key: apiKey,
|
||
api_model: apiModel,
|
||
api_timeout: apiTimeout
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json();
|
||
throw new Error(errorData.error || '分析失败');
|
||
}
|
||
|
||
// 设置流式处理
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
let buffer = '';
|
||
|
||
// 持续读取数据流
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
// 解码接收到的数据并添加到缓冲区
|
||
buffer += decoder.decode(value, { stream: true });
|
||
|
||
// 处理完整的行
|
||
const lines = buffer.split('\n');
|
||
buffer = lines.pop() || ''; // 最后一行可能不完整,保留到下一次处理
|
||
|
||
for (const line of lines) {
|
||
if (line.trim() === '') continue;
|
||
|
||
try {
|
||
const chunk = JSON.parse(line);
|
||
handleStreamChunk(chunk, resultsContainer, marketType);
|
||
} catch (e) {
|
||
console.error('解析流数据出错:', e, line);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理可能遗留在缓冲区的最后一行
|
||
if (buffer.trim()) {
|
||
try {
|
||
const chunk = JSON.parse(buffer);
|
||
handleStreamChunk(chunk, resultsContainer, marketType);
|
||
} catch (e) {
|
||
console.error('解析最后一行数据出错:', e, buffer);
|
||
}
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('请求失败:', error);
|
||
resultContent.innerHTML = `
|
||
<div class="p-6 bg-yellow-50 text-yellow-600 rounded-lg text-center">
|
||
分析出错:${error.message}
|
||
</div>
|
||
`;
|
||
} finally {
|
||
isAnalyzing = false;
|
||
analyzeBtn.disabled = false;
|
||
loadingSpinner.classList.add('hidden');
|
||
analyzeBtn.querySelector('span').textContent = '开始分析';
|
||
}
|
||
}
|
||
|
||
// 处理流式数据的函数
|
||
function handleStreamChunk(chunk, container, marketType) {
|
||
// 处理初始化信息
|
||
if (chunk.stream_type) {
|
||
console.log('开始流式分析:', chunk);
|
||
return;
|
||
}
|
||
|
||
// 获取股票代码
|
||
const stockCode = chunk.stock_code;
|
||
|
||
// 如果是错误信息
|
||
if (chunk.error) {
|
||
// 添加或更新显示错误的卡片
|
||
let errorCard = document.getElementById(`error-${stockCode}`);
|
||
if (!errorCard) {
|
||
errorCard = document.createElement('div');
|
||
errorCard.id = `error-${stockCode}`;
|
||
errorCard.className = 'bg-red-50 p-4 rounded-lg text-red-600';
|
||
errorCard.innerHTML = `分析股票 ${stockCode} 出错: ${chunk.error}`;
|
||
container.appendChild(errorCard);
|
||
} else {
|
||
errorCard.innerHTML = `分析股票 ${stockCode} 出错: ${chunk.error}`;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 如果是基本报告结构
|
||
if (!chunk.ai_analysis_chunk) {
|
||
// 存储基本报告数据
|
||
stockAnalysisData[stockCode] = {
|
||
...chunk,
|
||
ai_analysis: ''
|
||
};
|
||
|
||
// 添加或更新股票卡片
|
||
createStockCard(stockCode, container, marketType);
|
||
return;
|
||
}
|
||
|
||
// 如果是AI分析片段
|
||
if (chunk.ai_analysis_chunk) {
|
||
// 确保该股票的数据存在
|
||
if (!stockAnalysisData[stockCode]) {
|
||
stockAnalysisData[stockCode] = {
|
||
stock_code: stockCode,
|
||
ai_analysis: ''
|
||
};
|
||
}
|
||
|
||
// 累加AI分析内容
|
||
stockAnalysisData[stockCode].ai_analysis += chunk.ai_analysis_chunk;
|
||
|
||
// 更新AI分析显示
|
||
updateAIAnalysisDisplay(stockCode);
|
||
}
|
||
}
|
||
|
||
// 创建股票卡片
|
||
function createStockCard(stockCode, container, marketType) {
|
||
const result = stockAnalysisData[stockCode];
|
||
if (!result) return;
|
||
|
||
// 根据市场类型设置货币符号
|
||
const currencySymbol = (() => {
|
||
switch(marketType) {
|
||
case 'US':
|
||
return '$';
|
||
case 'HK':
|
||
return 'HK$';
|
||
case 'A':
|
||
default:
|
||
return '¥';
|
||
}
|
||
})();
|
||
|
||
// 检查是否已存在该股票的卡片
|
||
let stockCard = document.getElementById(`stock-card-${stockCode}`);
|
||
|
||
if (!stockCard) {
|
||
stockCard = document.createElement('div');
|
||
stockCard.id = `stock-card-${stockCode}`;
|
||
stockCard.className = 'bg-white rounded-lg shadow-lg overflow-hidden';
|
||
container.appendChild(stockCard);
|
||
}
|
||
|
||
stockCard.innerHTML = `
|
||
<!-- 头部信息 -->
|
||
<div class="bg-gradient-to-r from-blue-600 to-blue-700 px-6 py-4">
|
||
<h3 class="text-xl font-bold text-white">
|
||
${result.stock_code}
|
||
</h3>
|
||
</div>
|
||
|
||
<!-- 主要指标 -->
|
||
<div class="p-6">
|
||
<div class="grid grid-cols-2 gap-6 mb-6">
|
||
<div class="space-y-3">
|
||
<div class="flex justify-between items-center p-3 bg-gray-50 rounded">
|
||
<span class="text-gray-600">分析时间</span>
|
||
<span class="font-medium">${result.analysis_date}</span>
|
||
</div>
|
||
<div class="flex justify-between items-center p-3 bg-gray-50 rounded">
|
||
<span class="text-gray-600">当前价格</span>
|
||
<span class="font-medium">${currencySymbol}${result.price.toFixed(2)}</span>
|
||
</div>
|
||
<div class="flex justify-between items-center p-3 bg-gray-50 rounded">
|
||
<span class="text-gray-600">价格变动</span>
|
||
<span class="font-medium ${result.price_change >= 0 ? 'text-red-500' : 'text-green-500'}">
|
||
${result.price_change.toFixed(2)}%
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="space-y-3">
|
||
<div class="flex justify-between items-center p-3 bg-gray-50 rounded">
|
||
<span class="text-gray-600">综合评分</span>
|
||
<span class="font-medium text-blue-600">${result.score}分</span>
|
||
</div>
|
||
<div class="flex justify-between items-center p-3 bg-gray-50 rounded">
|
||
<span class="text-gray-600">投资建议</span>
|
||
<span class="font-medium text-purple-600">${result.recommendation}</span>
|
||
</div>
|
||
<div class="flex justify-between items-center p-3 bg-gray-50 rounded">
|
||
<span class="text-gray-600">RSI指标</span>
|
||
<span class="font-medium">${result.rsi.toFixed(2)}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- AI分析部分 -->
|
||
<div class="mt-6">
|
||
<h4 class="text-lg font-semibold text-gray-800 mb-3">AI分析</h4>
|
||
<div id="ai-analysis-${stockCode}" class="prose prose-blue max-w-none bg-gray-50 p-4 rounded-lg relative">
|
||
<!-- 加载动画 -->
|
||
<div class="ai-analysis-loading flex flex-col items-center justify-center py-8">
|
||
<div class="typing-animation mb-3">
|
||
<span></span>
|
||
<span></span>
|
||
<span></span>
|
||
</div>
|
||
<p class="text-gray-500 text-sm">AI正在思考分析中...</p>
|
||
</div>
|
||
<!-- 实际内容容器 -->
|
||
<div class="ai-analysis-content hidden"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 免责声明 -->
|
||
<div class="mt-6 border-t border-gray-100 pt-4">
|
||
<div class="bg-blue-50 p-4 rounded-lg">
|
||
<p class="text-sm text-blue-800 font-semibold mb-1">声明:</p>
|
||
<p class="text-sm text-blue-600">本分析仅基于技术指标和历史数据,不构成投资建议。股市有风险,投资需谨慎。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 更新AI分析显示
|
||
function updateAIAnalysisDisplay(stockCode) {
|
||
const analysisElement = document.getElementById(`ai-analysis-${stockCode}`);
|
||
if (analysisElement && stockAnalysisData[stockCode]) {
|
||
const loadingElement = analysisElement.querySelector('.ai-analysis-loading');
|
||
const contentElement = analysisElement.querySelector('.ai-analysis-content');
|
||
|
||
// 如果有AI分析内容
|
||
if (stockAnalysisData[stockCode].ai_analysis) {
|
||
// 解析Markdown
|
||
const parsedContent = marked.parse(stockAnalysisData[stockCode].ai_analysis);
|
||
|
||
// 检查是否是第一次添加内容
|
||
const isFirstUpdate = contentElement.classList.contains('hidden');
|
||
|
||
// 如果是第一次更新,显示内容区域并隐藏加载动画
|
||
if (isFirstUpdate) {
|
||
contentElement.innerHTML = parsedContent;
|
||
contentElement.classList.remove('hidden');
|
||
contentElement.classList.add('fade-in');
|
||
|
||
// 延迟隐藏加载动画,使过渡更平滑
|
||
setTimeout(() => {
|
||
loadingElement.style.display = 'none';
|
||
}, 300);
|
||
} else {
|
||
// 获取当前内容长度,用于确定新增内容
|
||
const currentLength = contentElement.textContent.length;
|
||
|
||
// 更新内容
|
||
contentElement.innerHTML = parsedContent;
|
||
|
||
// 尝试高亮新增的文本(通过比较长度)
|
||
const allTextNodes = getAllTextNodes(contentElement);
|
||
let totalLength = 0;
|
||
|
||
for (const node of allTextNodes) {
|
||
totalLength += node.textContent.length;
|
||
if (totalLength > currentLength) {
|
||
// 这个节点包含新内容,将其包装在高亮span中
|
||
const newTextSpan = document.createElement('span');
|
||
newTextSpan.className = 'new-text';
|
||
node.parentNode.insertBefore(newTextSpan, node);
|
||
newTextSpan.appendChild(node);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 辅助函数:获取元素内的所有文本节点
|
||
function getAllTextNodes(element) {
|
||
const textNodes = [];
|
||
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
|
||
|
||
let node;
|
||
while (node = walker.nextNode()) {
|
||
textNodes.push(node);
|
||
}
|
||
|
||
return textNodes;
|
||
}
|
||
|
||
// 旧的displayResults函数保留用于兼容
|
||
function displayResults(results) {
|
||
const resultContent = document.getElementById('resultContent');
|
||
|
||
// 清空现有结果
|
||
resultContent.innerHTML = '';
|
||
stockAnalysisData = {};
|
||
|
||
// 创建结果容器
|
||
const resultsContainer = document.createElement('div');
|
||
resultsContainer.className = 'space-y-6';
|
||
resultContent.appendChild(resultsContainer);
|
||
|
||
if (!results || results.length === 0) {
|
||
resultsContainer.innerHTML = '<div class="p-6 bg-yellow-50 text-yellow-600 rounded-lg text-center">没有分析结果</div>';
|
||
return;
|
||
}
|
||
|
||
// 获取市场类型
|
||
const marketType = document.getElementById('marketType').value;
|
||
|
||
// 处理每个结果
|
||
results.forEach(result => {
|
||
stockAnalysisData[result.stock_code] = result;
|
||
createStockCard(result.stock_code, resultsContainer, marketType);
|
||
updateAIAnalysisDisplay(result.stock_code);
|
||
});
|
||
|
||
// 添加 Markdown 样式
|
||
addMarkdownStyles();
|
||
}
|
||
|
||
</script>
|
||
<!-- 添加 marked.js 用于解析 Markdown -->
|
||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||
|
||
<!-- API配置相关脚本 -->
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// API配置面板切换
|
||
const toggleApiConfig = document.getElementById('toggleApiConfig');
|
||
const apiConfigPanel = document.getElementById('apiConfigPanel');
|
||
const toggleApiConfigText = document.getElementById('toggleApiConfigText');
|
||
const toggleApiConfigIcon = document.getElementById('toggleApiConfigIcon');
|
||
const apiUrl = document.getElementById('apiUrl');
|
||
const apiKey = document.getElementById('apiKey');
|
||
const apiModel = document.getElementById('apiModel');
|
||
const apiTimeout = document.getElementById('apiTimeout');
|
||
const saveApiConfig = document.getElementById('saveApiConfig');
|
||
const resetApiConfig = document.getElementById('resetApiConfig');
|
||
const testApiConfig = document.getElementById('testApiConfig');
|
||
|
||
// 从localStorage加载保存的配置
|
||
loadApiConfig();
|
||
|
||
// 切换API配置面板显示/隐藏
|
||
toggleApiConfig.addEventListener('click', function() {
|
||
apiConfigPanel.classList.toggle('hidden');
|
||
|
||
if (apiConfigPanel.classList.contains('hidden')) {
|
||
toggleApiConfigText.textContent = '显示配置';
|
||
toggleApiConfigIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>';
|
||
} else {
|
||
toggleApiConfigText.textContent = '隐藏配置';
|
||
toggleApiConfigIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>';
|
||
}
|
||
});
|
||
|
||
// 重置API配置
|
||
resetApiConfig.addEventListener('click', function() {
|
||
apiUrl.value = '{{ default_api_url }}';
|
||
apiKey.value = '';
|
||
apiModel.value = '{{ default_api_model }}';
|
||
apiTimeout.value = '{{ default_api_timeout }}';
|
||
saveApiConfig.checked = false;
|
||
|
||
// 清除localStorage中的配置
|
||
localStorage.removeItem('apiConfig');
|
||
|
||
alert('已重置为默认配置');
|
||
});
|
||
|
||
// 测试API连接
|
||
testApiConfig.addEventListener('click', async function() {
|
||
const url = apiUrl.value.trim();
|
||
const key = apiKey.value.trim();
|
||
const model = apiModel.value.trim();
|
||
const timeout = apiTimeout.value.trim();
|
||
|
||
if (!url) {
|
||
alert('请输入API URL');
|
||
return;
|
||
}
|
||
|
||
if (!key) {
|
||
alert('请输入API Key');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
testApiConfig.disabled = true;
|
||
testApiConfig.textContent = '测试中...';
|
||
|
||
const response = await fetch('/test_api_connection', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
api_url: url,
|
||
api_key: key,
|
||
api_model: model,
|
||
api_timeout: timeout
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
alert(result.message);
|
||
|
||
// 如果勾选了保存配置,则保存到localStorage
|
||
if (saveApiConfig.checked) {
|
||
saveApiConfigToLocalStorage();
|
||
}
|
||
} else {
|
||
alert(result.message);
|
||
}
|
||
} catch (error) {
|
||
alert(error.message);
|
||
} finally {
|
||
testApiConfig.disabled = false;
|
||
testApiConfig.textContent = '测试连接';
|
||
}
|
||
});
|
||
|
||
// 监听输入变化,自动保存配置
|
||
[apiUrl, apiKey, apiModel, apiTimeout].forEach(input => {
|
||
input.addEventListener('change', function() {
|
||
if (saveApiConfig.checked) {
|
||
saveApiConfigToLocalStorage();
|
||
}
|
||
});
|
||
});
|
||
|
||
// 监听保存配置复选框变化
|
||
saveApiConfig.addEventListener('change', function() {
|
||
if (this.checked) {
|
||
saveApiConfigToLocalStorage();
|
||
} else {
|
||
localStorage.removeItem('apiConfig');
|
||
}
|
||
});
|
||
});
|
||
|
||
// 保存API配置到localStorage
|
||
function saveApiConfigToLocalStorage() {
|
||
const apiConfig = {
|
||
url: document.getElementById('apiUrl').value.trim(),
|
||
model: document.getElementById('apiModel').value.trim(),
|
||
key: document.getElementById('apiKey').value.trim(),
|
||
timeout: document.getElementById('apiTimeout').value.trim(),
|
||
saveEnabled: true
|
||
};
|
||
|
||
localStorage.setItem('apiConfig', JSON.stringify(apiConfig));
|
||
}
|
||
|
||
// 从localStorage加载API配置
|
||
function loadApiConfig() {
|
||
const savedConfig = localStorage.getItem('apiConfig');
|
||
|
||
if (savedConfig) {
|
||
try {
|
||
const config = JSON.parse(savedConfig);
|
||
|
||
if (config.url) document.getElementById('apiUrl').value = config.url;
|
||
if (config.model) document.getElementById('apiModel').value = config.model;
|
||
if (config.key) document.getElementById('apiKey').value = config.key;
|
||
if (config.timeout) document.getElementById('apiTimeout').value = config.timeout;
|
||
|
||
document.getElementById('saveApiConfig').checked = config.saveEnabled || false;
|
||
} catch (error) {
|
||
console.error('加载API配置时出错:', error);
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
<script>
|
||
// 添加 Markdown 样式
|
||
function addMarkdownStyles() {
|
||
// 检查是否已经添加了样式
|
||
if (!document.getElementById('markdown-styles')) {
|
||
const style = document.createElement('style');
|
||
style.id = 'markdown-styles';
|
||
style.textContent = `
|
||
.prose h1 { font-size: 1.5em; margin-top: 1em; margin-bottom: 0.5em; font-weight: bold; }
|
||
.prose h2 { font-size: 1.3em; margin-top: 1em; margin-bottom: 0.5em; font-weight: bold; }
|
||
.prose h3 { font-size: 1.1em; margin-top: 1em; margin-bottom: 0.5em; font-weight: bold; }
|
||
.prose p { margin-bottom: 1em; line-height: 1.6; }
|
||
.prose ul { list-style-type: disc; padding-left: 1.5em; margin-bottom: 1em; }
|
||
.prose ol { list-style-type: decimal; padding-left: 1.5em; margin-bottom: 1em; }
|
||
.prose li { margin-bottom: 0.5em; }
|
||
.prose strong { font-weight: 600; color: #1a56db; }
|
||
.prose em { font-style: italic; }
|
||
.prose blockquote { border-left: 4px solid #e5e7eb; padding-left: 1em; margin: 1em 0; color: #4b5563; }
|
||
.prose code { background-color: #f3f4f6; padding: 0.2em 0.4em; border-radius: 0.25em; font-size: 0.9em; }
|
||
|
||
/* 打字机动画样式 */
|
||
.typing-animation {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.typing-animation span {
|
||
height: 10px;
|
||
width: 10px;
|
||
margin: 0 2px;
|
||
background-color: #3b82f6;
|
||
border-radius: 50%;
|
||
display: inline-block;
|
||
animation: typing 1.5s infinite ease-in-out;
|
||
}
|
||
|
||
.typing-animation span:nth-child(1) {
|
||
animation-delay: 0s;
|
||
}
|
||
|
||
.typing-animation span:nth-child(2) {
|
||
animation-delay: 0.3s;
|
||
}
|
||
|
||
.typing-animation span:nth-child(3) {
|
||
animation-delay: 0.6s;
|
||
}
|
||
|
||
@keyframes typing {
|
||
0% { transform: scale(1); opacity: 0.7; }
|
||
50% { transform: scale(1.5); opacity: 1; }
|
||
100% { transform: scale(1); opacity: 0.7; }
|
||
}
|
||
|
||
/* 内容淡入效果 */
|
||
.ai-analysis-content {
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.ai-analysis-content.fade-in {
|
||
opacity: 0;
|
||
animation: fadeIn 0.5s forwards;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
|
||
/* 高亮新增文本效果 */
|
||
.new-text {
|
||
background-color: rgba(59, 130, 246, 0.1);
|
||
animation: highlightFade 2s forwards;
|
||
}
|
||
|
||
@keyframes highlightFade {
|
||
from { background-color: rgba(59, 130, 246, 0.1); }
|
||
to { background-color: transparent; }
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
}
|
||
}
|
||
|
||
// 页面加载时添加样式
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
addMarkdownStyles();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|
||
|
||
<script>
|
||
function updateFormattedUrl(url) {
|
||
const formattedUrlElement = document.getElementById('formattedUrl');
|
||
if (!url) {
|
||
formattedUrlElement.textContent = '';
|
||
return;
|
||
}
|
||
|
||
let formattedUrl;
|
||
if (url.endsWith('/')) {
|
||
formattedUrl = `${url}chat/completions`;
|
||
} else if (url.endsWith('#')) {
|
||
formattedUrl = url.replace("#", "");
|
||
} else {
|
||
formattedUrl = `${url}/v1/chat/completions`;
|
||
}
|
||
|
||
formattedUrlElement.innerHTML = `
|
||
<span class="font-medium">/结尾忽略v1版本,#结尾强制使用输入地址:</span><br>
|
||
<span class="text-blue-600">${formattedUrl}</span>
|
||
`;
|
||
}
|
||
|
||
// 页面加载时初始化显示
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const apiUrl = document.getElementById('apiUrl');
|
||
if (apiUrl.value) {
|
||
updateFormattedUrl(apiUrl.value);
|
||
}
|
||
});
|
||
</script> |