Files
ALL-teach_sys/index.html
KQL 60921dbfb9 修复端口检测:使用Vite客户端脚本检测
- 改用 @vite/client 脚本检测,这是Vite服务器必有的资源
- 移除不可靠的图片检测(vite.svg返回HTML)
- 组合script和fetch两种方法
- 修复跨域检测问题
2025-09-24 15:29:41 +08:00

708 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>教务系统 - 产业导航</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.header {
text-align: center;
color: white;
margin-bottom: 30px;
animation: fadeInDown 0.8s ease;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.header p {
font-size: 1.1rem;
opacity: 0.95;
margin-bottom: 20px;
}
.control-panel {
background: white;
border-radius: 15px;
padding: 25px;
margin-bottom: 30px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
animation: fadeIn 0.8s ease;
}
.control-panel h2 {
color: #333;
margin-bottom: 20px;
font-size: 1.3rem;
}
.control-buttons {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
}
.control-btn {
padding: 12px 25px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
color: white;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-start-all {
background: linear-gradient(135deg, #667eea, #764ba2);
}
.btn-start-all:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.btn-stop-all {
background: linear-gradient(135deg, #f093fb, #f5576c);
}
.btn-stop-all:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(245, 87, 108, 0.4);
}
.btn-refresh {
background: linear-gradient(135deg, #4facfe, #00f2fe);
}
.btn-refresh:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(79, 172, 254, 0.4);
}
.btn-launcher {
background: linear-gradient(135deg, #fa709a, #fee140);
}
.btn-launcher:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(250, 112, 154, 0.4);
}
.platform-info {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 15px;
margin-top: 20px;
color: white;
text-align: center;
}
.platform-info code {
background: rgba(0, 0, 0, 0.2);
padding: 2px 8px;
border-radius: 4px;
font-family: 'Consolas', 'Monaco', monospace;
}
.container {
max-width: 1400px;
width: 100%;
}
.industry-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 25px;
margin-bottom: 30px;
animation: fadeInUp 0.8s ease;
}
.industry-card {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.industry-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 5px;
background: linear-gradient(90deg, #667eea, #764ba2);
}
.industry-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 50px rgba(0,0,0,0.15);
}
.industry-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.industry-name {
font-size: 1.2rem;
font-weight: 600;
color: #333;
}
.industry-number {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9rem;
font-weight: bold;
}
.industry-info {
margin-bottom: 20px;
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 8px;
color: #666;
font-size: 0.95rem;
}
.info-label {
font-weight: 500;
margin-right: 8px;
color: #999;
}
.status {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
}
.status-running {
background: #d4f4dd;
color: #2e7d46;
}
.status-stopped {
background: #ffeaa7;
color: #d63031;
}
.status-checking {
background: #e3f2fd;
color: #1976d2;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.status-running .status-dot {
background: #2e7d46;
}
.status-stopped .status-dot {
background: #d63031;
animation: none;
}
.industry-actions {
display: flex;
gap: 10px;
}
.btn {
flex: 1;
padding: 10px;
border: none;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #f5f5f5;
color: #666;
}
.btn-secondary:hover {
background: #e0e0e0;
}
.footer {
text-align: center;
color: white;
margin-top: 40px;
padding: 20px;
opacity: 0.9;
}
.instruction-box {
background: rgba(255, 255, 255, 0.95);
border-radius: 10px;
padding: 20px;
margin-bottom: 25px;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}
.instruction-box h3 {
color: #333;
margin-bottom: 15px;
font-size: 1.1rem;
}
.instruction-box ol {
margin-left: 20px;
color: #666;
line-height: 1.8;
}
.instruction-box code {
background: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
color: #d63031;
font-family: Consolas, Monaco, monospace;
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(46, 125, 70, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(46, 125, 70, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(46, 125, 70, 0);
}
}
</style>
</head>
<body>
<div class="header">
<h1>🎓 教务系统产业管理中心</h1>
<p>智能化多产业教务管理平台 - 支持12个产业独立运行</p>
</div>
<div class="container">
<!-- Windows用户使用说明 -->
<div class="instruction-box">
<h3>💡 Windows 用户快速启动指南</h3>
<ol>
<li><strong>方法一:</strong>双击运行 <code>start-industry.bat</code> 文件</li>
<li><strong>方法二:</strong>打开命令提示符CMD输入 <code>start-industry.bat</code></li>
<li><strong>方法三:</strong>使用 PowerShell 运行 <code>.\start-industry.ps1</code></li>
<li><strong>方法四:</strong>直接在本页面点击下方的控制按钮</li>
</ol>
</div>
<!-- 控制面板 -->
<div class="control-panel">
<h2>🎛️ 快速控制中心</h2>
<div class="control-buttons">
<button class="control-btn btn-start-all" onclick="startAllIndustries()">
▶️ 启动所有产业
</button>
<button class="control-btn btn-stop-all" onclick="stopAllIndustries()">
⏹️ 停止所有产业
</button>
<button class="control-btn btn-refresh" onclick="forceCheckAllStatus()">
🔄 强制刷新状态
</button>
<a href="#" class="control-btn btn-launcher" onclick="openLauncher()">
🚀 打开启动器 (Windows)
</a>
</div>
<div class="platform-info">
<p>💻 当前系统: <span id="platformInfo"></span></p>
<p>📁 项目路径: <code id="projectPath"></code></p>
<p>⏰ 最后检查: <span id="lastCheck">从未检查</span></p>
</div>
</div>
<!-- 产业卡片网格 -->
<div class="industry-grid" id="industryGrid"></div>
<!-- 页脚 -->
<div class="footer">
<p>⏰ 状态每30秒自动刷新 | 📖 <a href="README.md" style="color: white;">查看文档</a></p>
<p style="margin-top: 10px; opacity: 0.8;">© 2024 教务系统 - 智能产业管理平台</p>
</div>
</div>
<script>
// 产业配置
const industries = [
{ id: 1, name: '文旅产业', dir: 'frontend', port: 5150, icon: '🏛️' },
{ id: 2, name: '智能制造', dir: 'frontend_智能制造', port: 5151, icon: '🏭' },
{ id: 3, name: '智能开发', dir: 'frontend_智能开发', port: 5152, icon: '💻' },
{ id: 4, name: '财经商贸', dir: 'frontend_财经商贸', port: 5153, icon: '💰' },
{ id: 5, name: '视觉设计', dir: 'frontend_视觉设计', port: 5154, icon: '🎨' },
{ id: 6, name: '交通物流', dir: 'frontend_交通物流', port: 5155, icon: '🚚' },
{ id: 7, name: '大健康', dir: 'frontend_大健康', port: 5156, icon: '🏥' },
{ id: 8, name: '土木水利', dir: 'frontend_土木水利', port: 5157, icon: '🏗️' },
{ id: 9, name: '食品产业', dir: 'frontend_食品', port: 5158, icon: '🍽️' },
{ id: 10, name: '化工产业', dir: 'frontend_化工', port: 5159, icon: '⚗️' },
{ id: 11, name: '能源产业', dir: 'frontend_能源', port: 5160, icon: '⚡' },
{ id: 12, name: '环保产业', dir: 'frontend_环保', port: 5161, icon: '🌱' }
];
// 状态缓存
const statusCache = {};
// 检测平台信息
function detectPlatform() {
const platform = navigator.platform;
const userAgent = navigator.userAgent;
let osInfo = '未知系统';
if (platform.indexOf('Win') !== -1) {
osInfo = 'Windows';
} else if (platform.indexOf('Mac') !== -1) {
osInfo = 'macOS';
} else if (platform.indexOf('Linux') !== -1) {
osInfo = 'Linux';
}
document.getElementById('platformInfo').textContent = osInfo;
document.getElementById('projectPath').textContent = window.location.pathname.replace('/index-final.html', '');
}
// 创建产业卡片
function createIndustryCard(industry) {
return `
<div class="industry-card">
<div class="industry-header">
<span class="industry-name">${industry.icon} ${industry.name}</span>
<span class="industry-number">${industry.id}</span>
</div>
<div class="industry-info">
<div class="info-item">
<span class="info-label">端口:</span>
<span>${industry.port}</span>
</div>
<div class="info-item">
<span class="info-label">目录:</span>
<span>${industry.dir}</span>
</div>
<div class="info-item">
<span class="info-label">状态:</span>
<span class="status status-checking" id="status-${industry.port}">
<span class="status-dot"></span>
检查中...
</span>
</div>
</div>
<div class="industry-actions">
<button class="btn btn-primary" onclick="openIndustry(${industry.port})">
访问系统
</button>
<button class="btn btn-secondary" onclick="checkSinglePort(${industry.port})">
刷新状态
</button>
</div>
</div>
`;
}
// 初始化页面
function initPage() {
detectPlatform();
const grid = document.getElementById('industryGrid');
grid.innerHTML = industries.map(industry => createIndustryCard(industry)).join('');
// 从localStorage读取缓存的状态
loadStatusFromCache();
// 然后执行实际检查
checkAllStatus();
}
// 从缓存加载状态
function loadStatusFromCache() {
const cached = localStorage.getItem('industryStatus');
if (cached) {
try {
const data = JSON.parse(cached);
const now = Date.now();
// 如果缓存不超过5分钟使用缓存数据
if (data.timestamp && (now - data.timestamp) < 300000) {
Object.keys(data.status).forEach(port => {
updateStatusUI(port, data.status[port]);
});
document.getElementById('lastCheck').textContent = new Date(data.timestamp).toLocaleTimeString();
}
} catch (e) {
console.error('读取缓存失败:', e);
}
}
}
// 保存状态到缓存
function saveStatusToCache() {
const data = {
timestamp: Date.now(),
status: statusCache
};
localStorage.setItem('industryStatus', JSON.stringify(data));
}
// 更新UI状态
function updateStatusUI(port, isRunning) {
const statusEl = document.getElementById(`status-${port}`);
if (statusEl) {
if (isRunning) {
statusEl.className = 'status status-running';
statusEl.innerHTML = '<span class="status-dot"></span> 运行中';
} else {
statusEl.className = 'status status-stopped';
statusEl.innerHTML = '<span class="status-dot"></span> 已停止';
}
}
statusCache[port] = isRunning;
}
// 检查单个端口
async function checkSinglePort(port) {
const statusEl = document.getElementById(`status-${port}`);
statusEl.className = 'status status-checking';
statusEl.innerHTML = '<span class="status-dot"></span> 检查中...';
const isRunning = await checkPort(port);
updateStatusUI(port, isRunning);
saveStatusToCache();
}
// 实际的端口检查逻辑
async function checkPort(port) {
// 使用多种方法组合检测
// 方法1: 检测Vite客户端脚本
const checkViteClient = () => {
return new Promise((resolve) => {
const script = document.createElement('script');
let resolved = false;
const timeout = setTimeout(() => {
if (!resolved) {
resolved = true;
if (script.parentNode) {
script.parentNode.removeChild(script);
}
resolve(false);
}
}, 1500);
script.onload = () => {
if (!resolved) {
resolved = true;
clearTimeout(timeout);
script.parentNode.removeChild(script);
resolve(true);
}
};
script.onerror = () => {
if (!resolved) {
resolved = true;
clearTimeout(timeout);
if (script.parentNode) {
script.parentNode.removeChild(script);
}
resolve(false);
}
};
// Vite开发服务器总是有这个客户端脚本
script.src = `http://localhost:${port}/@vite/client?_=${Date.now()}`;
script.async = true;
document.head.appendChild(script);
});
};
// 方法2: 使用fetch检测根路径
const checkFetch = () => {
return new Promise((resolve) => {
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
resolve(false);
}, 1500);
fetch(`http://localhost:${port}/`, {
signal: controller.signal,
mode: 'no-cors',
cache: 'no-cache'
}).then(() => {
clearTimeout(timeout);
resolve(true);
}).catch(() => {
clearTimeout(timeout);
resolve(false);
});
});
};
// 先尝试Vite客户端检测
let result = await checkViteClient();
// 如果Vite客户端检测失败尝试fetch
if (!result) {
result = await checkFetch();
}
return result;
}
// 检查所有状态
async function checkAllStatus() {
console.log('开始检查所有端口状态...');
const checkTime = new Date().toLocaleTimeString();
// 顺序检查每个端口,避免并发过多
for (const industry of industries) {
const isRunning = await checkPort(industry.port);
updateStatusUI(industry.port, isRunning);
// 添加小延迟,避免请求过快
await new Promise(resolve => setTimeout(resolve, 100));
}
saveStatusToCache();
document.getElementById('lastCheck').textContent = checkTime;
console.log('状态检查完成');
}
// 强制刷新所有状态
function forceCheckAllStatus() {
// 清除缓存
localStorage.removeItem('industryStatus');
// 重新检查
checkAllStatus();
}
// 打开产业系统
function openIndustry(port) {
window.open(`http://localhost:${port}`, '_blank');
}
// 启动所有产业
function startAllIndustries() {
if (confirm('启动所有产业需要使用系统启动器。\n\nWindows: 运行 start-industry.bat 选择 [0]\nMac/Linux: 运行 ./start-industry.sh 选择 [0]\n\n是否打开启动器')) {
openLauncher();
}
}
// 停止所有产业
function stopAllIndustries() {
if (confirm('停止所有产业需要使用系统启动器。\n\nWindows: 运行 start-industry.bat 选择 [A]\nMac/Linux: 运行 ./start-industry.sh 选择 [A]\n\n是否打开启动器')) {
openLauncher();
}
}
// 打开启动器
function openLauncher() {
const platform = navigator.platform;
if (platform.indexOf('Win') !== -1) {
alert('Windows 启动方法:\n\n1. 打开文件资源管理器\n2. 找到 start-industry.bat 文件\n3. 双击运行\n\n或者:\n1. 按 Win+R 打开运行对话框\n2. 输入 cmd 并回车\n3. 导航到项目目录\n4. 输入 start-industry.bat 并回车');
} else {
alert('Mac/Linux 启动方法:\n\n1. 打开终端\n2. 导航到项目目录\n3. 运行: ./start-industry.sh\n\n如果没有执行权限:\nchmod +x start-industry.sh');
}
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', initPage);
// 自动刷新状态每30秒
setInterval(checkAllStatus, 30000);
</script>
</body>
</html>