Files
esengine/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/admin-backend/public/index.html
2025-06-19 18:32:32 +08:00

686 lines
31 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>Cocos ECS Extension - 热更新管理后台</title>
<link href="https://cdn.jsdelivr.net/npm/element-plus@2.4.4/dist/index.css" rel="stylesheet">
<style>
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
.header { background: #409eff; color: white; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.container { max-width: 1200px; margin: 20px auto; padding: 0 20px; }
.card { background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin-bottom: 20px; }
.upload-area {
border: 2px dashed #dcdfe6;
border-radius: 8px;
padding: 40px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.upload-area:hover { border-color: #409eff; background: #f0f8ff; }
.version-list { max-height: 400px; overflow-y: auto; }
.version-item {
padding: 16px;
border-bottom: 1px solid #ebeef5;
display: flex;
justify-content: space-between;
align-items: center;
}
.version-info h4 { margin: 0 0 8px 0; color: #303133; }
.version-info p { margin: 0; color: #909399; font-size: 14px; }
.status-tag {
padding: 4px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: bold;
}
.status-active { background: #f0f9ff; color: #409eff; }
.status-draft { background: #fdf6ec; color: #e6a23c; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }
.stat-card {
padding: 24px;
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px;
}
.stat-number { font-size: 32px; font-weight: bold; margin-bottom: 8px; }
.stat-label { font-size: 14px; opacity: 0.9; }
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.btn-primary { background: #409eff; color: white; }
.btn-primary:hover { background: #337ecc; }
.btn-danger { background: #f56c6c; color: white; }
.btn-danger:hover { background: #dd6161; }
.btn-success { background: #67c23a; color: white; }
.btn-success:hover { background: #5daf34; }
.plugin-card:hover { border-color: #409eff; background: #f0f8ff; }
.plugin-active { border-color: #409eff !important; background: #f0f8ff !important; }
.filtered-out { opacity: 0.3; }
/* 模态框样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
width: 600px;
max-width: 90%;
max-height: 90%;
overflow-y: auto;
}
.modal-header {
padding: 20px 24px;
border-bottom: 1px solid #e4e7ed;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
color: #303133;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #909399;
padding: 0;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.modal-close:hover {
background: #f5f7fa;
color: #606266;
}
.modal-body {
padding: 24px;
}
.modal-footer {
padding: 20px 24px;
border-top: 1px solid #e4e7ed;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #606266;
font-weight: 500;
}
.form-group small {
display: block;
margin-top: 4px;
color: #909399;
font-size: 12px;
}
.btn.btn-default {
background: #fff;
color: #606266;
border: 1px solid #dcdfe6;
}
.btn.btn-default:hover {
color: #409eff;
border-color: #c6e2ff;
background: #ecf5ff;
}
</style>
</head>
<body>
<div id="app">
<div class="header">
<h1 style="margin: 0; font-size: 24px;">🚀 Cocos ECS Extension - 热更新管理后台</h1>
</div>
<div class="container">
<!-- 插件选择器 -->
<div class="card">
<div style="padding: 20px;">
<h2>🔌 插件管理</h2>
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
<div
v-for="plugin in plugins"
:key="plugin.id"
@click="selectPlugin(plugin.id)"
:class="'plugin-card ' + (currentPlugin === plugin.id ? 'plugin-active' : '')"
style="flex: 1; min-width: 250px; padding: 20px; border: 2px solid #e4e7ed; border-radius: 8px; cursor: pointer; transition: all 0.3s;"
>
<div style="display: flex; align-items: center; gap: 12px;">
<span style="font-size: 32px;">{{ plugin.icon }}</span>
<div style="flex: 1;">
<h3 style="margin: 0; color: #303133;">{{ plugin.displayName }}</h3>
<p style="margin: 4px 0 0 0; color: #909399; font-size: 14px;">{{ plugin.description || '暂无描述' }}</p>
<div style="margin: 8px 0 0 0; display: flex; gap: 16px; font-size: 12px;">
<span style="color: #67c23a;">{{ plugin.status === 'active' ? '✅ 活跃' : '⏸️ 暂停' }}</span>
<span style="color: #409eff;">📦 版本: {{ plugin.versionCount || 0 }}</span>
<span style="color: #e6a23c;">📥 下载: {{ plugin.totalDownloads || 0 }}</span>
</div>
<p style="margin: 4px 0 0 0; color: #c0c4cc; font-size: 12px;">{{ plugin.author || '未知作者' }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 统计数据 -->
<div class="card">
<div style="padding: 20px;">
<h2>📊 系统统计</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number">{{ stats.totalVersions }}</div>
<div class="stat-label">总版本数</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ stats.activeUsers }}</div>
<div class="stat-label">活跃用户</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ stats.todayUpdates }}</div>
<div class="stat-label">今日更新</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ stats.successRate }}%</div>
<div class="stat-label">成功率</div>
</div>
</div>
</div>
</div>
<!-- 上传新版本 -->
<div class="card">
<div style="padding: 20px;">
<h2>📦 上传新版本</h2>
<div class="upload-area" @click="selectFile">
<div v-if="!uploading">
<i style="font-size: 48px; color: #c0c4cc;">📁</i>
<p style="margin: 16px 0 0 0; color: #606266;">点击或拖拽文件到此处上传插件包</p>
<p style="margin: 8px 0 0 0; color: #909399; font-size: 14px;">支持 .zip 格式,最大 100MB</p>
</div>
<div v-else>
<i style="font-size: 48px; color: #409eff;"></i>
<p style="margin: 16px 0 0 0; color: #409eff;">上传中... {{ uploadProgress }}%</p>
</div>
</div>
<input type="file" ref="fileInput" @change="handleFileSelect" accept=".zip" style="display: none;">
<div v-if="selectedFile" style="margin-top: 20px; padding: 16px; background: #f5f7fa; border-radius: 4px;">
<h4 style="margin: 0 0 12px 0;">版本信息</h4>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
<div>
<label>目标插件:</label>
<select v-model="versionInfo.pluginId" style="width: 100%; padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px;">
<option v-for="plugin in plugins" :key="plugin.id" :value="plugin.id">
{{ plugin.icon }} {{ plugin.displayName }}
</option>
</select>
</div>
<div>
<label>版本号:</label>
<input v-model="versionInfo.version" style="width: 100%; padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px;">
</div>
<div>
<label>发布渠道:</label>
<select v-model="versionInfo.channel" style="width: 100%; padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px;">
<option value="stable">稳定版</option>
<option value="beta">测试版</option>
<option value="dev">开发版</option>
</select>
</div>
</div>
<div style="margin-top: 16px;">
<label>更新说明:</label>
<textarea v-model="versionInfo.description" rows="3" style="width: 100%; padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px;"></textarea>
</div>
<div style="margin-top: 16px;">
<label>
<input type="checkbox" v-model="versionInfo.mandatory"> 强制更新
</label>
</div>
<div style="margin-top: 16px;">
<button class="btn btn-primary" @click="uploadVersion" :disabled="uploading">
{{ uploading ? '上传中...' : '发布版本' }}
</button>
</div>
</div>
</div>
</div>
<!-- 版本管理 -->
<div class="card">
<div style="padding: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2>📋 版本管理 - {{ getCurrentPluginName() }}</h2>
<div style="display: flex; gap: 12px; align-items: center;">
<button class="btn btn-primary" @click="showCreatePlugin = true">+ 创建新项目</button>
<select v-model="currentPlugin" @change="filterVersions" style="padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px;">
<option value="">全部插件</option>
<option v-for="plugin in plugins" :key="plugin.id" :value="plugin.id">
{{ plugin.icon }} {{ plugin.displayName }}
</option>
</select>
<button class="btn btn-success" @click="refreshVersions">刷新列表</button>
</div>
</div>
<div class="version-list">
<div v-for="version in filteredVersions" :key="version.id" class="version-item">
<div class="version-info">
<h4>
<span style="margin-right: 8px;">{{ getPluginIcon(version.pluginId) }}</span>
{{ version.version }} ({{ getChannelName(version.channel) }})
<span style="font-size: 12px; color: #909399; margin-left: 8px;">- {{ getPluginDisplayName(version.pluginId) }}</span>
</h4>
<p>{{ version.description }}</p>
<p>发布时间: {{ formatDate(version.release_date) }} | 文件大小: {{ formatSize(version.file_size) }}</p>
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<span :class="'status-tag status-' + version.status">
{{ getStatusName(version.status) }}
</span>
<button class="btn btn-primary" @click="editVersion(version)">编辑</button>
<button class="btn btn-danger" @click="deleteVersion(version)">删除</button>
</div>
</div>
<div v-if="filteredVersions.length === 0" style="text-align: center; padding: 40px; color: #909399;">
{{ currentPlugin ? '该插件暂无版本数据' : '暂无版本数据' }}
</div>
</div>
</div>
</div>
</div>
<!-- 创建项目模态框 -->
<div v-if="showCreatePlugin" class="modal-overlay" @click="showCreatePlugin = false">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>🎯 创建新项目</h3>
<button @click="showCreatePlugin = false" class="modal-close">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>项目ID *</label>
<input v-model="newPlugin.id" placeholder="例如: my-awesome-plugin" style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;">
<small>项目ID必须是唯一的建议使用小写字母和短横线</small>
</div>
<div class="form-group">
<label>项目名称 *</label>
<input v-model="newPlugin.name" placeholder="例如: my-awesome-plugin" style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;">
</div>
<div class="form-group">
<label>显示名称 *</label>
<input v-model="newPlugin.displayName" placeholder="例如: My Awesome Plugin" style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;">
</div>
<div class="form-group">
<label>项目描述</label>
<textarea v-model="newPlugin.description" rows="3" placeholder="描述你的项目功能和特点..." style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;"></textarea>
</div>
<div class="form-group">
<label>作者</label>
<input v-model="newPlugin.author" placeholder="例如: Your Name" style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;">
</div>
<div class="form-group">
<label>仓库地址</label>
<input v-model="newPlugin.repository" placeholder="例如: https://github.com/user/repo" style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;">
</div>
<div class="form-group">
<label>图标 (Emoji)</label>
<input v-model="newPlugin.icon" placeholder="📦" maxlength="2" style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;">
</div>
</div>
<div class="modal-footer">
<button @click="showCreatePlugin = false" class="btn btn-default">取消</button>
<button @click="createPlugin" class="btn btn-primary" :disabled="creatingPlugin">
{{ creatingPlugin ? '创建中...' : '创建项目' }}
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.8/dist/vue.global.js"></script>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
uploading: false,
uploadProgress: 0,
selectedFile: null,
currentPlugin: 'cocos-ecs-extension', // 当前选中的插件
showCreatePlugin: false,
creatingPlugin: false,
newPlugin: {
id: '',
name: '',
displayName: '',
description: '',
author: '',
repository: '',
icon: '📦'
},
plugins: [],
versionInfo: {
pluginId: 'cocos-ecs-extension',
version: '',
channel: 'stable',
description: '',
mandatory: false
},
stats: {
totalVersions: 0,
activeUsers: 0,
todayUpdates: 0,
successRate: 100.0
},
versions: []
}
},
computed: {
filteredVersions() {
if (!this.currentPlugin) {
return this.versions;
}
return this.versions.filter(v => v.pluginId === this.currentPlugin);
}
},
methods: {
selectPlugin(pluginId) {
this.currentPlugin = pluginId;
this.versionInfo.pluginId = pluginId;
},
filterVersions() {
// 触发计算属性重新计算
},
getCurrentPluginName() {
if (!this.currentPlugin) return '全部插件';
const plugin = this.plugins.find(p => p.id === this.currentPlugin);
return plugin ? plugin.displayName : '未知插件';
},
getPluginIcon(pluginId) {
const plugin = this.plugins.find(p => p.id === pluginId);
return plugin ? plugin.icon : '📦';
},
getPluginDisplayName(pluginId) {
const plugin = this.plugins.find(p => p.id === pluginId);
return plugin ? plugin.displayName : pluginId;
},
selectFile() {
this.$refs.fileInput.click();
},
handleFileSelect(event) {
const file = event.target.files[0];
if (file) {
this.selectedFile = file;
// 自动提取版本号(如果文件名包含版本信息)
const match = file.name.match(/(\d+\.\d+\.\d+)/);
if (match) {
this.versionInfo.version = match[1];
}
}
},
async uploadVersion() {
if (!this.selectedFile) return;
this.uploading = true;
this.uploadProgress = 0;
try {
const formData = new FormData();
formData.append('package', this.selectedFile);
formData.append('pluginId', this.versionInfo.pluginId);
formData.append('version', this.versionInfo.version);
formData.append('channel', this.versionInfo.channel);
formData.append('description', this.versionInfo.description);
formData.append('mandatory', this.versionInfo.mandatory.toString());
// 模拟上传进度
const progressInterval = setInterval(() => {
this.uploadProgress = Math.min(this.uploadProgress + Math.random() * 15, 90);
}, 200);
// 调用真实的上传API
const response = await fetch('/api/upload/package', {
method: 'POST',
body: formData
});
clearInterval(progressInterval);
this.uploadProgress = 100;
const result = await response.json();
if (result.success) {
alert(`版本上传成功!\n插件: ${result.data.pluginId}\n版本: ${result.data.version}\n文件数: ${result.data.filesCount}`);
// 重置表单
this.selectedFile = null;
this.versionInfo = {
pluginId: this.currentPlugin,
version: '',
channel: 'stable',
description: '',
mandatory: false
};
this.$refs.fileInput.value = '';
// 刷新数据
await this.refreshVersions();
await this.refreshStats();
} else {
alert('上传失败: ' + result.error);
}
} catch (error) {
alert('上传失败: ' + error.message);
} finally {
this.uploading = false;
}
},
async refreshVersions() {
try {
const params = new URLSearchParams();
if (this.currentPlugin) {
params.set('pluginId', this.currentPlugin);
}
const response = await fetch(`/api/versions?${params}`);
const result = await response.json();
if (result.success) {
this.versions = result.data.map(v => ({
id: v.id,
pluginId: v.pluginId,
version: v.version,
channel: v.channel,
description: v.description,
release_date: v.releaseDate,
file_size: v.fileSize,
status: v.status
}));
} else {
console.error('获取版本列表失败:', result.error);
}
} catch (error) {
console.error('刷新版本列表失败:', error);
}
},
async refreshStats() {
try {
const response = await fetch('/api/stats');
const result = await response.json();
if (result.success) {
this.stats = result.data;
} else {
console.error('获取统计数据失败:', result.error);
}
} catch (error) {
console.error('刷新统计数据失败:', error);
}
},
editVersion(version) {
// 编辑版本
console.log('编辑版本:', version);
},
deleteVersion(version) {
if (confirm('确定要删除版本 ' + version.version + ' 吗?')) {
// 删除版本
console.log('删除版本:', version);
}
},
getChannelName(channel) {
const names = { stable: '稳定版', beta: '测试版', dev: '开发版' };
return names[channel] || channel;
},
getStatusName(status) {
const names = { active: '已发布', draft: '草稿' };
return names[status] || status;
},
formatDate(dateString) {
return new Date(dateString).toLocaleString('zh-CN');
},
formatSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return size.toFixed(1) + ' ' + units[unitIndex];
}
},
async mounted() {
// 加载插件列表
await this.loadPlugins();
// 加载初始数据
await this.refreshStats();
await this.refreshVersions();
},
async loadPlugins() {
try {
const response = await fetch('/api/plugins');
const result = await response.json();
if (result.success) {
this.plugins = result.data.map(p => ({
id: p.id,
name: p.name,
displayName: p.displayName,
description: p.description,
author: p.author,
repository: p.repository,
icon: p.icon,
status: p.status,
versionCount: p.versionCount || 0,
totalDownloads: p.totalDownloads || 0
}));
// 如果没有选中插件且有插件数据,自动选中第一个
if (!this.currentPlugin && this.plugins.length > 0) {
this.currentPlugin = this.plugins[0].id;
this.versionInfo.pluginId = this.plugins[0].id;
}
} else {
console.error('获取插件列表失败:', result.error);
}
} catch (error) {
console.error('加载插件列表失败:', error);
}
},
async createPlugin() {
if (!this.newPlugin.id || !this.newPlugin.name || !this.newPlugin.displayName) {
alert('请填写必要的字段项目ID、项目名称、显示名称');
return;
}
this.creatingPlugin = true;
try {
const response = await fetch('/api/plugins', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(this.newPlugin)
});
const result = await response.json();
if (result.success) {
alert(`项目创建成功!\n项目名称: ${result.data.displayName}\n项目ID: ${result.data.id}`);
// 重置表单
this.newPlugin = {
id: '',
name: '',
displayName: '',
description: '',
author: '',
repository: '',
icon: '📦'
};
// 关闭模态框
this.showCreatePlugin = false;
// 刷新插件列表
await this.loadPlugins();
await this.refreshStats();
} else {
alert('创建失败: ' + result.error);
}
} catch (error) {
alert('创建失败: ' + error.message);
} finally {
this.creatingPlugin = false;
}
}
}).mount('#app');
</script>
</body>
</html>