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