Initial commit
This commit is contained in:
@@ -0,0 +1,510 @@
|
||||
# MeshRay 原生前端开发指南
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
web/
|
||||
├── static/
|
||||
│ ├── index.html # 主页面(Vue 应用入口)
|
||||
│ └── js/
|
||||
│ └── app.js # Vue 应用逻辑
|
||||
└── embed.go # Go embed 配置
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 技术栈
|
||||
|
||||
- **Vue 3.4** - 渐进式 JavaScript 框架
|
||||
- **Element Plus** - Vue 3 组件库
|
||||
- **Tailwind CSS** - 实用优先的 CSS 框架
|
||||
- **CDN 加载** - 无需本地安装依赖
|
||||
|
||||
---
|
||||
|
||||
## 🎨 开发示例
|
||||
|
||||
### 1. 添加新页面
|
||||
|
||||
#### 步骤 1: 在 HTML 中添加页面内容
|
||||
|
||||
```html
|
||||
<!-- 在 index.html 的主内容区添加 -->
|
||||
<div v-if="currentPage === 'devices'">
|
||||
<h2 class="text-2xl font-bold mb-6">设备管理</h2>
|
||||
|
||||
<div class="card p-6">
|
||||
<el-button type="primary" @click="showCreateDevice = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加设备
|
||||
</el-button>
|
||||
|
||||
<el-table :data="devices" style="width: 100%; margin-top: 20px;">
|
||||
<el-table-column prop="name" label="名称"></el-table-column>
|
||||
<el-table-column prop="ip" label="IP 地址"></el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="editDevice(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteDevice(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 步骤 2: 在 JS 中添加数据和逻辑
|
||||
|
||||
```javascript
|
||||
// app.js
|
||||
const devices = ref([]);
|
||||
const showCreateDevice = ref(false);
|
||||
|
||||
const loadDevices = async () => {
|
||||
const response = await fetch(`${API_BASE}/devices`);
|
||||
const result = await response.json();
|
||||
devices.value = result.data || [];
|
||||
};
|
||||
|
||||
const editDevice = (device) => {
|
||||
ElementPlus.ElMessageBox.alert(`编辑设备:${device.name}`);
|
||||
};
|
||||
|
||||
const deleteDevice = async (device) => {
|
||||
await ElementPlus.ElMessageBox.confirm('确定删除?', '警告', { type: 'warning' });
|
||||
const response = await fetch(`${API_BASE}/devices/${device.id}`, { method: 'DELETE' });
|
||||
if (response.ok) {
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
await loadDevices();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadDevices();
|
||||
});
|
||||
|
||||
return { currentPage, devices, showCreateDevice, loadDevices, editDevice, deleteDevice };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 调用 API
|
||||
|
||||
#### GET 请求
|
||||
|
||||
```javascript
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/dashboard/stats');
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.data) {
|
||||
stats.networkCount = result.data.network_count;
|
||||
stats.deviceCount = result.data.device_count;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载失败:', error);
|
||||
ElementPlus.ElMessage.error('加载数据失败');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### POST 请求
|
||||
|
||||
```javascript
|
||||
const createNetwork = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/networks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: newNetwork.name,
|
||||
subnet_ipv4: newNetwork.subnet_ipv4,
|
||||
listen_port: newNetwork.listen_port,
|
||||
mesh_mode: newNetwork.mesh_mode
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
ElementPlus.ElMessage.success('创建成功');
|
||||
showCreateNetworkDialog.value = false;
|
||||
await loadNetworks();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(result.error || '创建失败');
|
||||
}
|
||||
} catch (error) {
|
||||
ElementPlus.ElMessage.error('创建失败');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### DELETE 请求
|
||||
|
||||
```javascript
|
||||
const deleteNetwork = async (network) => {
|
||||
try {
|
||||
await ElementPlus.ElMessageBox.confirm(
|
||||
`确定删除 "${network.name}"?`,
|
||||
'警告',
|
||||
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }
|
||||
);
|
||||
|
||||
const response = await fetch(`/api/v1/networks/${network.id}`, { method: 'DELETE' });
|
||||
|
||||
if (response.ok) {
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
await loadNetworks();
|
||||
} else {
|
||||
const result = await response.json();
|
||||
ElementPlus.ElMessage.error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElementPlus.ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 使用 Element Plus 组件
|
||||
|
||||
#### 对话框 (Dialog)
|
||||
|
||||
```html
|
||||
<el-dialog v-model="showDialog" title="标题" width="500px">
|
||||
<el-form :model="formData" label-width="100px">
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="formData.name" placeholder="请输入名称"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
```
|
||||
|
||||
#### 消息提示
|
||||
|
||||
```javascript
|
||||
// 成功消息
|
||||
ElementPlus.ElMessage.success('操作成功');
|
||||
|
||||
// 错误消息
|
||||
ElementPlus.ElMessage.error('操作失败');
|
||||
|
||||
// 警告消息
|
||||
ElementPlus.ElMessage.warning('请注意');
|
||||
|
||||
// 确认对话框
|
||||
await ElementPlus.ElMessageBox.confirm('确定执行?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
|
||||
// 提示框
|
||||
ElementPlus.ElMessageBox.alert('这是一段提示信息', '标题', {
|
||||
confirmButtonText: '确定'
|
||||
});
|
||||
```
|
||||
|
||||
#### 表格 (Table)
|
||||
|
||||
```html
|
||||
<el-table :data="items" style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="name" label="名称"></el-table-column>
|
||||
<el-table-column prop="status" label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'active' ? 'success' : 'danger'">
|
||||
{{ row.status === 'active' ? '运行中' : '已停止' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="view(row)">查看</el-button>
|
||||
<el-button size="small" type="primary" @click="edit(row)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 使用 Tailwind CSS
|
||||
|
||||
#### 布局
|
||||
|
||||
```html
|
||||
<!-- Flexbox 布局 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>左侧内容</div>
|
||||
<div>右侧内容</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid 布局 -->
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>列 1</div>
|
||||
<div>列 2</div>
|
||||
<div>列 3</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 间距
|
||||
|
||||
```html
|
||||
<!-- 外边距 -->
|
||||
<div class="m-4">上下左右 1rem</div>
|
||||
<div class="mt-2">上边距 0.5rem</div>
|
||||
<div class="mb-4">下边距 1rem</div>
|
||||
<div class="mx-auto">水平居中</div>
|
||||
|
||||
<!-- 内边距 -->
|
||||
<div class="p-4">上下左右 1rem</div>
|
||||
<div class="px-6">左右 1.5rem</div>
|
||||
<div class="py-2">上下 0.5rem</div>
|
||||
```
|
||||
|
||||
#### 颜色
|
||||
|
||||
```html
|
||||
<div class="bg-blue-500 text-white">蓝色背景白字</div>
|
||||
<div class="bg-green-100 text-green-800">浅绿背景深绿字</div>
|
||||
<div class="hover:bg-red-500">悬停变红</div>
|
||||
```
|
||||
|
||||
#### 响应式
|
||||
|
||||
```html
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
|
||||
<!-- 手机 1 列,平板 2 列,桌面 4 列 -->
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 常用工具函数
|
||||
|
||||
### 格式化日期
|
||||
|
||||
```javascript
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
// 使用
|
||||
{{ formatDate(network.created_at) }}
|
||||
```
|
||||
|
||||
### 复制文本
|
||||
|
||||
```javascript
|
||||
const copyToClipboard = async (text) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
ElementPlus.ElMessage.success('已复制到剪贴板');
|
||||
} catch (error) {
|
||||
ElementPlus.ElMessage.error('复制失败');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 下载文件
|
||||
|
||||
```javascript
|
||||
const downloadFile = (content, filename, mimeType = 'text/plain') => {
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// 使用:下载 WireGuard 配置
|
||||
downloadFile(configText, 'wg0.conf', 'text/plain');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 完整页面模板
|
||||
|
||||
```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>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
|
||||
<script src="https://unpkg.com/element-plus"></script>
|
||||
<script src="https://unpkg.com/@element-plus/icons-vue"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1 class="text-3xl font-bold">{{ title }}</h1>
|
||||
<button class="bg-blue-500 text-white px-4 py-2 rounded" @click="handleClick">
|
||||
点击我
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { createApp, ref } = Vue;
|
||||
|
||||
createApp({
|
||||
setup() {
|
||||
const title = ref('我的页面');
|
||||
|
||||
const handleClick = () => {
|
||||
ElementPlus.ElMessage.success('按钮被点击了');
|
||||
};
|
||||
|
||||
return { title, handleClick };
|
||||
}
|
||||
}).use(ElementPlus).mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 最佳实践
|
||||
|
||||
### 1. 代码组织
|
||||
|
||||
```javascript
|
||||
// app.js
|
||||
const API_BASE = '/api/v1';
|
||||
|
||||
// 按功能模块组织代码
|
||||
const dashboardModule = {
|
||||
stats: reactive({...}),
|
||||
loadStats: async () => {...}
|
||||
};
|
||||
|
||||
const networkModule = {
|
||||
networks: ref([]),
|
||||
createNetwork: async () => {...},
|
||||
deleteNetwork: async (network) => {...}
|
||||
};
|
||||
|
||||
// 统一导出
|
||||
return {
|
||||
...dashboardModule,
|
||||
...networkModule
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 错误处理
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('网络错误');
|
||||
const result = await response.json();
|
||||
// 处理结果
|
||||
} catch (error) {
|
||||
console.error('详细错误:', error);
|
||||
ElementPlus.ElMessage.error('操作失败:' + error.message);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Loading 状态
|
||||
|
||||
```javascript
|
||||
const loading = ref(false);
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// API 调用
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// UI 显示
|
||||
<el-button :loading="loading">加载数据</el-button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 调试技巧
|
||||
|
||||
### 控制台日志
|
||||
|
||||
```javascript
|
||||
console.log('当前状态:', stats);
|
||||
console.error('发生错误:', error);
|
||||
console.warn('警告信息:', warning);
|
||||
```
|
||||
|
||||
### Vue DevTools
|
||||
|
||||
安装 [Vue DevTools](https://devtools.vuejs.org/) 浏览器扩展:
|
||||
- 查看组件树
|
||||
- 检查响应式数据
|
||||
- 调试事件
|
||||
|
||||
### Network 面板
|
||||
|
||||
浏览器开发者工具 → Network:
|
||||
- 查看 API 请求
|
||||
- 检查请求参数
|
||||
- 分析响应数据
|
||||
|
||||
---
|
||||
|
||||
## 📞 常见问题
|
||||
|
||||
### Q: 如何添加新的 CDN 资源?
|
||||
|
||||
A: 在 `<head>` 标签中添加:
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
||||
```
|
||||
|
||||
### Q: 如何使用自定义样式?
|
||||
|
||||
A: 在 `<style>` 标签中添加:
|
||||
|
||||
```html
|
||||
<style>
|
||||
.custom-class {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### Q: 如何访问后端 API?
|
||||
|
||||
A: 使用相对路径,Gin 会自动代理:
|
||||
|
||||
```javascript
|
||||
fetch('/api/v1/networks') // ✅ 正确
|
||||
fetch('http://localhost:8080/api/v1/networks') // ❌ 错误
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2026-03-20
|
||||
**适用版本**: MeshRay 2.0+
|
||||
@@ -0,0 +1,306 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MeshRay - 简单、高效异地组网</title>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Vue 3 -->
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
|
||||
<!-- Element Plus -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
|
||||
<script src="https://unpkg.com/element-plus"></script>
|
||||
|
||||
<!-- Element Plus Icons -->
|
||||
<script src="https://unpkg.com/@element-plus/icons-vue"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
color: rgba(255,255,255,0.8);
|
||||
padding: 15px 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.menu-item:hover, .menu-item.active {
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
background: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #909399;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* 登录页面样式 */
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.login-box {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
text-align: center;
|
||||
color: #667eea;
|
||||
font-size: 32px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- 登录页面 -->
|
||||
<div v-if="!isLoggedIn" class="login-container">
|
||||
<div class="login-box">
|
||||
<h1 class="login-title">🔺 MeshRay</h1>
|
||||
<p class="login-subtitle">去中心化异地组网平台</p>
|
||||
|
||||
<el-form :model="loginForm" label-width="80px">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="loginForm.username" placeholder="请输入用户名" prefix-icon="User"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="loginForm.password" type="password" placeholder="请输入密码" prefix-icon="Lock" @keyup.enter="handleLogin"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" style="width: 100%" :loading="loggingIn" @click="handleLogin">登录</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主界面 -->
|
||||
<el-container v-else>
|
||||
<!-- 侧边栏 -->
|
||||
<el-aside width="250px" class="sidebar">
|
||||
<div class="logo">🔺 MeshRay</div>
|
||||
<el-menu
|
||||
:default-active="currentPage"
|
||||
background-color="transparent"
|
||||
text-color="rgba(255,255,255,0.8)"
|
||||
active-text-color="#fff"
|
||||
>
|
||||
<el-menu-item index="dashboard" @click="currentPage = 'dashboard'">
|
||||
<el-icon><DataBoard /></el-icon>
|
||||
<span>仪表盘</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="networks" @click="currentPage = 'networks'">
|
||||
<el-icon><Connection /></el-icon>
|
||||
<span>组网管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="devices" @click="currentPage = 'devices'">
|
||||
<el-icon><Cpu /></el-icon>
|
||||
<span>设备管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="services" @click="currentPage = 'services'">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>服务管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="settings" @click="currentPage = 'settings'">
|
||||
<el-icon><Tools /></el-icon>
|
||||
<span>系统设置</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<el-container>
|
||||
<!-- 顶部导航栏 -->
|
||||
<el-header style="background: white; border-bottom: 1px solid #e4e7ed; display: flex; justify-content: space-between; align-items: center; padding: 0 20px;">
|
||||
<div style="font-size: 18px; font-weight: bold;">欢迎使用 MeshRay</div>
|
||||
<el-button type="danger" size="small" @click="handleLogout">退出登录</el-button>
|
||||
</el-header>
|
||||
<el-main class="main-content">
|
||||
<!-- 仪表盘页面 -->
|
||||
<div v-if="currentPage === 'dashboard'">
|
||||
<h2 class="text-2xl font-bold mb-6">仪表盘</h2>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<div class="card stat-card">
|
||||
<div class="stat-number">{{ stats.networkCount }}</div>
|
||||
<div class="stat-label">组网数量</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="card stat-card">
|
||||
<div class="stat-number">{{ stats.deviceCount }}</div>
|
||||
<div class="stat-label">设备数量</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="card stat-card">
|
||||
<div class="stat-number">{{ stats.onlineDevices }}</div>
|
||||
<div class="stat-label">在线设备</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="card stat-card">
|
||||
<div class="stat-number">{{ stats.pendingApprovals }}</div>
|
||||
<div class="stat-label">待审核</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 最近活动 -->
|
||||
<div class="card mt-6 p-6">
|
||||
<h3 class="text-lg font-bold mb-4">最近活动</h3>
|
||||
<el-timeline>
|
||||
<el-timeline-item timestamp="2026-03-20 10:00" type="primary">
|
||||
系统启动成功
|
||||
</el-timeline-item>
|
||||
<el-timeline-item timestamp="2026-03-20 09:30" type="success">
|
||||
组网 "生产网络" 创建成功
|
||||
</el-timeline-item>
|
||||
<el-timeline-item timestamp="2026-03-20 09:00" type="warning">
|
||||
新设备申请加入 "测试网络"
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 组网管理页面 -->
|
||||
<div v-if="currentPage === 'networks'">
|
||||
<h2 class="text-2xl font-bold mb-6">组网管理</h2>
|
||||
|
||||
<div class="card p-6">
|
||||
<el-button type="primary" @click="showCreateNetworkDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
创建组网
|
||||
</el-button>
|
||||
|
||||
<el-table :data="networks" style="width: 100%; margin-top: 20px;">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="name" label="名称"></el-table-column>
|
||||
<el-table-column prop="subnet_ipv4" label="子网"></el-table-column>
|
||||
<el-table-column prop="mesh_mode" label="模式">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.mesh_mode === 'enhanced' ? 'success' : 'info'">
|
||||
{{ row.mesh_mode === 'enhanced' ? '增强' : '原生' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="viewNetwork(row)">详情</el-button>
|
||||
<el-button size="small" type="success" @click="generateMeshSeed(row)">
|
||||
MeshSeed
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteNetwork(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 其他页面占位 -->
|
||||
<div v-if="['devices', 'services', 'settings'].includes(currentPage)">
|
||||
<h2 class="text-2xl font-bold mb-6">
|
||||
{{ currentPage === 'devices' ? '设备管理' :
|
||||
currentPage === 'services' ? '服务管理' : '系统设置' }}
|
||||
</h2>
|
||||
<div class="card p-6 text-center text-gray-500">
|
||||
<p>页面开发中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
|
||||
<!-- 创建组网对话框 -->
|
||||
<el-dialog v-model="showCreateNetworkDialog" title="创建组网" width="500px">
|
||||
<el-form :model="newNetwork" label-width="100px">
|
||||
<el-form-item label="组网名称">
|
||||
<el-input v-model="newNetwork.name" placeholder="请输入组网名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="子网">
|
||||
<el-input v-model="newNetwork.subnet_ipv4" placeholder="例如:10.0.0.0/24"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="监听端口">
|
||||
<el-input-number v-model="newNetwork.listen_port" :min="1024" :max="65535" value="51820"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="组网模式">
|
||||
<el-radio-group v-model="newNetwork.mesh_mode">
|
||||
<el-radio label="native">原生模式</el-radio>
|
||||
<el-radio label="enhanced">增强模式</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showCreateNetworkDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="createNetwork">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,331 @@
|
||||
// MeshRay 前端应用 - 原生 Vue3 + Element Plus
|
||||
|
||||
const { createApp, ref, reactive, onMounted } = Vue;
|
||||
|
||||
// 注册所有 Element Plus 图标
|
||||
const icons = {};
|
||||
for (const [key, value] of Object.entries(ElementPlusIconsVue)) {
|
||||
icons[key] = value;
|
||||
}
|
||||
|
||||
const app = createApp({
|
||||
setup() {
|
||||
// 当前页面
|
||||
const currentPage = ref('dashboard');
|
||||
|
||||
// 登录状态
|
||||
const isLoggedIn = ref(false);
|
||||
const loggingIn = ref(false);
|
||||
|
||||
// 登录表单
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: ''
|
||||
});
|
||||
|
||||
// Token 存储
|
||||
let authToken = localStorage.getItem('meshray_token') || '';
|
||||
|
||||
// 检查是否已登录
|
||||
if (authToken) {
|
||||
isLoggedIn.value = true;
|
||||
// 可以在这里验证 token 是否有效
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
const stats = reactive({
|
||||
networkCount: 0,
|
||||
deviceCount: 0,
|
||||
onlineDevices: 0,
|
||||
pendingApprovals: 0
|
||||
});
|
||||
|
||||
// 组网列表
|
||||
const networks = ref([]);
|
||||
|
||||
// 对话框控制
|
||||
const showCreateNetworkDialog = ref(false);
|
||||
|
||||
// 新组网表单
|
||||
const newNetwork = reactive({
|
||||
name: '',
|
||||
subnet_ipv4: '10.0.0.0/24',
|
||||
listen_port: 51820,
|
||||
mesh_mode: 'enhanced'
|
||||
});
|
||||
|
||||
// API 基础 URL
|
||||
const API_BASE = '/api/v1';
|
||||
|
||||
// 加载统计数据
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/dashboard/stats`);
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.data) {
|
||||
stats.networkCount = result.data.network_count || 0;
|
||||
stats.deviceCount = result.data.device_count || 0;
|
||||
stats.onlineDevices = result.data.online_devices || 0;
|
||||
stats.pendingApprovals = result.data.pending_approvals || 0;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载组网列表
|
||||
const loadNetworks = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/networks`);
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
networks.value = result.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载组网列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 创建组网
|
||||
const createNetwork = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/networks`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(newNetwork)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
ElementPlus.ElMessage.success('组网创建成功');
|
||||
showCreateNetworkDialog.value = false;
|
||||
|
||||
// 重置表单
|
||||
newNetwork.name = '';
|
||||
newNetwork.subnet_ipv4 = '10.0.0.0/24';
|
||||
newNetwork.listen_port = 51820;
|
||||
|
||||
// 刷新列表
|
||||
await loadNetworks();
|
||||
await loadStats();
|
||||
} else {
|
||||
const result = await response.json();
|
||||
ElementPlus.ElMessage.error(result.error || '创建失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建组网失败:', error);
|
||||
ElementPlus.ElMessage.error('创建失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 查看组网详情
|
||||
const viewNetwork = (network) => {
|
||||
ElementPlus.ElMessageBox.alert(
|
||||
`组网名称:${network.name}\n` +
|
||||
`子网:${network.subnet_ipv4}\n` +
|
||||
`模式:${network.mesh_mode === 'enhanced' ? '增强' : '原生'}`,
|
||||
'组网详情',
|
||||
{
|
||||
confirmButtonText: '确定'
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// 生成 MeshSeed
|
||||
const generateMeshSeed = (network) => {
|
||||
ElementPlus.ElMessageBox.prompt(
|
||||
'请输入 MeshSeed 有效期(小时)',
|
||||
'生成 MeshSeed',
|
||||
{
|
||||
confirmButtonText: '生成',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /^\d+$/,
|
||||
inputErrorMessage: '请输入有效的数字'
|
||||
}
|
||||
).then(async ({ value }) => {
|
||||
try {
|
||||
const hours = parseInt(value);
|
||||
const expiresAt = new Date(Date.now() + hours * 3600 * 1000).toISOString();
|
||||
|
||||
const response = await fetch(`${API_BASE}/networks/${network.id}/mesh-seed`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
max_uses: 10,
|
||||
expires_at: expiresAt,
|
||||
ddns_enabled: false
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
const seedUrl = result.data?.seed_url || result.data?.seed;
|
||||
|
||||
if (seedUrl) {
|
||||
ElementPlus.ElMessageBox.alert(
|
||||
`MeshSeed 生成成功:<br><br>` +
|
||||
`<input type="text" value="${seedUrl}" readonly ` +
|
||||
`style="width: 100%; padding: 8px; margin-top: 10px; border: 1px solid #ddd;" ` +
|
||||
`onclick="this.select()">`,
|
||||
'MeshSeed',
|
||||
{
|
||||
dangerouslyUseHTMLString: true,
|
||||
confirmButtonText: '复制并关闭'
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const result = await response.json();
|
||||
ElementPlus.ElMessage.error(result.error || '生成失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成 MeshSeed 失败:', error);
|
||||
ElementPlus.ElMessage.error('生成失败');
|
||||
}
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
// 删除组网
|
||||
const deleteNetwork = async (network) => {
|
||||
try {
|
||||
await ElementPlus.ElMessageBox.confirm(
|
||||
`确定要删除组网 "${network.name}" 吗?此操作不可恢复。`,
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
);
|
||||
|
||||
const response = await fetch(`${API_BASE}/networks/${network.id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
await loadNetworks();
|
||||
await loadStats();
|
||||
} else {
|
||||
const result = await response.json();
|
||||
ElementPlus.ElMessage.error(result.error || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除组网失败:', error);
|
||||
ElementPlus.ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
if (isLoggedIn.value) {
|
||||
loadStats();
|
||||
loadNetworks();
|
||||
}
|
||||
});
|
||||
|
||||
// 处理登录
|
||||
const handleLogin = async () => {
|
||||
if (!loginForm.username || !loginForm.password) {
|
||||
ElementPlus.ElMessage.warning('请输入用户名和密码');
|
||||
return;
|
||||
}
|
||||
|
||||
loggingIn.value = true;
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: loginForm.username,
|
||||
password: loginForm.password
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.data) {
|
||||
// 保存 token
|
||||
authToken = result.data.token || result.data.access_token;
|
||||
localStorage.setItem('meshray_token', authToken);
|
||||
isLoggedIn.value = true;
|
||||
|
||||
ElementPlus.ElMessage.success('登录成功');
|
||||
|
||||
// 加载数据
|
||||
await loadStats();
|
||||
await loadNetworks();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(result.error || '登录失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error);
|
||||
ElementPlus.ElMessage.error('登录失败,请检查网络连接');
|
||||
} finally {
|
||||
loggingIn.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理退出
|
||||
const handleLogout = () => {
|
||||
ElementPlus.ElMessageBox.confirm(
|
||||
'确定要退出登录吗?',
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
// 清除 token
|
||||
localStorage.removeItem('meshray_token');
|
||||
authToken = '';
|
||||
isLoggedIn.value = false;
|
||||
|
||||
// 重置表单
|
||||
loginForm.username = '';
|
||||
loginForm.password = '';
|
||||
|
||||
ElementPlus.ElMessage.success('已退出登录');
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
return {
|
||||
currentPage,
|
||||
isLoggedIn,
|
||||
loggingIn,
|
||||
loginForm,
|
||||
stats,
|
||||
networks,
|
||||
showCreateNetworkDialog,
|
||||
newNetwork,
|
||||
handleLogin,
|
||||
handleLogout,
|
||||
createNetwork,
|
||||
viewNetwork,
|
||||
generateMeshSeed,
|
||||
deleteNetwork
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 使用 Element Plus
|
||||
app.use(ElementPlus);
|
||||
|
||||
// 注册所有图标
|
||||
for (const [key, component] of Object.entries(icons)) {
|
||||
app.component(key, component);
|
||||
}
|
||||
|
||||
// 挂载应用
|
||||
app.mount('#app');
|
||||
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MeshRay 调试页面</title>
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1>Vue 测试页面</h1>
|
||||
<p>测试消息:{{ testMessage }}</p>
|
||||
<p>当前时间:{{ currentTime }}</p>
|
||||
<button @click="updateTime">更新时间</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { createApp, ref } = Vue;
|
||||
|
||||
createApp({
|
||||
setup() {
|
||||
const testMessage = ref('Vue 工作正常!');
|
||||
const currentTime = ref(new Date().toLocaleString());
|
||||
|
||||
const updateTime = () => {
|
||||
currentTime.value = new Date().toLocaleString();
|
||||
alert('时间已更新!');
|
||||
};
|
||||
|
||||
return {
|
||||
testMessage,
|
||||
currentTime,
|
||||
updateTime
|
||||
};
|
||||
}
|
||||
}).mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user