Files
Meshray-Manager/web/static/README.md
T
2026-06-30 15:14:37 +08:00

511 lines
12 KiB
Markdown
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.
# 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+