This commit is contained in:
parent
819576c450
commit
ee06785e08
@ -1,273 +0,0 @@
|
|||||||
# MikroTik 内存缓存优化实施指南
|
|
||||||
|
|
||||||
## 🎯 优化目标
|
|
||||||
|
|
||||||
根据你的需求,我们实现了以下核心优化:
|
|
||||||
|
|
||||||
1. **🚀 完全移除验证功能** - 消除验证带来的额外API调用
|
|
||||||
2. **🧠 内存缓存机制** - 程序启动时从MikroTik加载所有现有IP到内存
|
|
||||||
3. **⚡ 智能重复检查** - 在内存中判断IP是否存在,避免重复写入
|
|
||||||
4. **🌐 /24网段优化** - 使用/24掩码减少地址条目数量
|
|
||||||
|
|
||||||
## 📋 实施步骤
|
|
||||||
|
|
||||||
### 第一步:备份现有配置
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 备份当前配置
|
|
||||||
cp /opt/mosdns/dns.yaml /opt/mosdns/dns.yaml.backup
|
|
||||||
cp /opt/mosdns/config.yaml /opt/mosdns/config.yaml.backup
|
|
||||||
```
|
|
||||||
|
|
||||||
### 第二步:更新配置文件
|
|
||||||
|
|
||||||
我已经为你创建了三个配置版本:
|
|
||||||
|
|
||||||
1. **`dns.yaml`** - 你的原配置文件,已优化为/24掩码
|
|
||||||
2. **`dns-memory-optimized.yaml`** - 完整的内存优化配置
|
|
||||||
3. **`dns-optimized.yaml`** - 标准性能优化配置
|
|
||||||
|
|
||||||
**推荐使用 `dns-memory-optimized.yaml`:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 使用优化配置
|
|
||||||
cp dns-memory-optimized.yaml /opt/mosdns/dns.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
### 第三步:验证MikroTik地址列表
|
|
||||||
|
|
||||||
确保MikroTik中存在对应的地址列表:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 连接到MikroTik
|
|
||||||
ssh admin@10.248.0.1
|
|
||||||
|
|
||||||
# 检查现有地址列表
|
|
||||||
/ip firewall address-list print where list=gfw
|
|
||||||
|
|
||||||
# 如果不存在,创建地址列表
|
|
||||||
/ip firewall address-list add list=gfw comment="Auto-managed by MosDNS"
|
|
||||||
|
|
||||||
# 查看当前地址数量
|
|
||||||
/ip firewall address-list print count-only where list=gfw
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 核心优化机制
|
|
||||||
|
|
||||||
### 1. 启动时内存加载
|
|
||||||
|
|
||||||
程序启动时会执行以下操作:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 伪代码流程
|
|
||||||
func (p *plugin) loadExistingIPs() {
|
|
||||||
// 1. 连接MikroTik API
|
|
||||||
// 2. 查询 /ip/firewall/address-list/print =list=gfw
|
|
||||||
// 3. 将所有现有IP加载到内存map中
|
|
||||||
// 4. 构建网段缓存(对于/24掩码)
|
|
||||||
// 5. 记录加载统计信息
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**启动日志示例:**
|
|
||||||
```
|
|
||||||
INFO loading existing IPs from MikroTik...
|
|
||||||
INFO loaded address list list=gfw ip_count=1250
|
|
||||||
INFO finished loading existing IPs total_ips=1250
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 内存存在性检查
|
|
||||||
|
|
||||||
每次DNS解析后的IP处理流程:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 伪代码流程
|
|
||||||
func (p *plugin) processIP(ip, domain) {
|
|
||||||
cidr := buildCIDRAddress(ip, 24) // 例如: 1.2.3.0/24
|
|
||||||
|
|
||||||
// 🚀 纯内存检查,极快速度
|
|
||||||
if p.isIPInMemoryCache("gfw", cidr) {
|
|
||||||
log.Debug("IP already exists, skipping")
|
|
||||||
return // 跳过,不调用MikroTik API
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只有不存在的IP才写入MikroTik
|
|
||||||
p.addToMikroTik(cidr, "gfw", domain)
|
|
||||||
|
|
||||||
// 🚀 成功后立即更新内存缓存
|
|
||||||
p.addToMemoryCache("gfw", cidr)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. /24网段优化
|
|
||||||
|
|
||||||
使用/24掩码的好处:
|
|
||||||
|
|
||||||
- **减少条目数量**: `1.2.3.1`, `1.2.3.2`, `1.2.3.3` → `1.2.3.0/24`
|
|
||||||
- **提高匹配效率**: 单个网段条目可以匹配256个IP
|
|
||||||
- **降低内存使用**: 缓存条目大幅减少
|
|
||||||
|
|
||||||
**示例对比:**
|
|
||||||
```bash
|
|
||||||
# /32模式 (原来)
|
|
||||||
1.2.3.1/32
|
|
||||||
1.2.3.2/32
|
|
||||||
1.2.3.3/32
|
|
||||||
...
|
|
||||||
1.2.3.255/32 # 255个条目
|
|
||||||
|
|
||||||
# /24模式 (优化后)
|
|
||||||
1.2.3.0/24 # 1个条目覆盖整个网段
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 性能提升预期
|
|
||||||
|
|
||||||
| 优化项目 | 优化前 | 优化后 | 提升效果 |
|
|
||||||
|---------|--------|--------|----------|
|
|
||||||
| 启动速度 | 立即 | +5-10秒 | 一次性成本 |
|
|
||||||
| 重复检查 | MikroTik API | 内存查找 | 99%+ 速度提升 |
|
|
||||||
| 网络调用 | 每IP一次 | 仅新IP | 减少80-90% |
|
|
||||||
| 内存使用 | 最小 | +10-50MB | 可接受增长 |
|
|
||||||
| 地址条目 | 大量/32 | 少量/24 | 减少70-90% |
|
|
||||||
|
|
||||||
## 🔍 监控和验证
|
|
||||||
|
|
||||||
### 启动监控
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 查看启动日志
|
|
||||||
sudo journalctl -u mosdns -f | grep "loading existing IPs"
|
|
||||||
|
|
||||||
# 完整启动日志
|
|
||||||
sudo systemctl restart mosdns
|
|
||||||
sudo journalctl -u mosdns --since "1 minute ago"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 运行时监控
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 查看实时处理日志
|
|
||||||
sudo journalctl -u mosdns -f | grep -E "(already exists|successfully added)"
|
|
||||||
|
|
||||||
# 查看缓存统计
|
|
||||||
sudo journalctl -u mosdns -f | grep "cache_stats"
|
|
||||||
```
|
|
||||||
|
|
||||||
### MikroTik端验证
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 查看地址列表大小变化
|
|
||||||
ssh admin@10.248.0.1 "/ip firewall address-list print count-only where list=gfw"
|
|
||||||
|
|
||||||
# 查看最近添加的地址
|
|
||||||
ssh admin@10.248.0.1 "/ip firewall address-list print where list=gfw" | tail -10
|
|
||||||
|
|
||||||
# 监控系统资源
|
|
||||||
ssh admin@10.248.0.1 "/system resource monitor once"
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚨 故障排除
|
|
||||||
|
|
||||||
### 常见问题
|
|
||||||
|
|
||||||
#### 1. 启动时加载失败
|
|
||||||
```bash
|
|
||||||
# 检查连接
|
|
||||||
ssh admin@10.248.0.1 "/system resource print"
|
|
||||||
|
|
||||||
# 检查地址列表是否存在
|
|
||||||
ssh admin@10.248.0.1 "/ip firewall address-list print where list=gfw"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. 内存使用过高
|
|
||||||
```bash
|
|
||||||
# 监控内存使用
|
|
||||||
top -p $(pgrep mosdns)
|
|
||||||
|
|
||||||
# 如果内存过高,可以调整配置
|
|
||||||
memory_cache_size: 5000 # 减少缓存大小
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. 性能没有提升
|
|
||||||
```bash
|
|
||||||
# 检查是否正确跳过重复IP
|
|
||||||
sudo journalctl -u mosdns -f | grep "already exists"
|
|
||||||
|
|
||||||
# 应该看到大量 "already exists" 日志
|
|
||||||
```
|
|
||||||
|
|
||||||
### 调试模式
|
|
||||||
|
|
||||||
临时启用详细日志:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# 在config.yaml中修改
|
|
||||||
log:
|
|
||||||
level: debug # 临时改为debug
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 重启服务
|
|
||||||
sudo systemctl restart mosdns
|
|
||||||
|
|
||||||
# 查看详细日志
|
|
||||||
sudo journalctl -u mosdns -f
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚡ 快速测试
|
|
||||||
|
|
||||||
### 测试重复IP检查
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 测试同一个域名多次解析
|
|
||||||
for i in {1..5}; do
|
|
||||||
dig @127.0.0.1 -p 5300 amazon.com
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
# 应该只看到第一次写入MikroTik,后续都是 "already exists"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 压力测试
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 并发测试多个域名
|
|
||||||
domains=("aws.amazon.com" "s3.amazonaws.com" "ec2.amazonaws.com" "cloudfront.amazonaws.com")
|
|
||||||
|
|
||||||
for domain in "${domains[@]}"; do
|
|
||||||
for i in {1..3}; do
|
|
||||||
dig @127.0.0.1 -p 5300 "$domain" &
|
|
||||||
done
|
|
||||||
done
|
|
||||||
wait
|
|
||||||
|
|
||||||
# 检查MikroTik地址列表增长
|
|
||||||
ssh admin@10.248.0.1 "/ip firewall address-list print count-only where list=gfw"
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📈 预期结果
|
|
||||||
|
|
||||||
实施这些优化后,你应该看到:
|
|
||||||
|
|
||||||
1. **启动时间**: 增加5-10秒(一次性加载现有IP)
|
|
||||||
2. **重复查询**: 几乎无延迟(纯内存检查)
|
|
||||||
3. **网络调用**: 大幅减少(只写入新IP)
|
|
||||||
4. **MikroTik负载**: 显著降低(减少80-90%的API调用)
|
|
||||||
5. **地址条目**: 大幅减少(/24网段合并)
|
|
||||||
|
|
||||||
## 🔄 回滚方案
|
|
||||||
|
|
||||||
如果需要回滚到原配置:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 恢复原配置
|
|
||||||
cp /opt/mosdns/dns.yaml.backup /opt/mosdns/dns.yaml
|
|
||||||
|
|
||||||
# 重启服务
|
|
||||||
sudo systemctl restart mosdns
|
|
||||||
|
|
||||||
# 验证服务正常
|
|
||||||
sudo systemctl status mosdns
|
|
||||||
```
|
|
||||||
|
|
||||||
这个优化方案完全符合你的需预期可以将MikroTik的API调用求:移除验证功能、启动时加载现有IP到内存、避免重复写入、使用/24掩码。减少80-90%,显著提升整体性能。
|
|
||||||
@ -1,206 +0,0 @@
|
|||||||
# MikroTik API 写入性能优化指南
|
|
||||||
|
|
||||||
## 🔍 问题分析
|
|
||||||
|
|
||||||
通过对 MosDNS MikroTik 插件的深入分析,发现以下性能瓶颈:
|
|
||||||
|
|
||||||
### 1. 网络层面问题
|
|
||||||
- **单连接阻塞**:使用单一连接处理所有请求
|
|
||||||
- **同步等待**:每个API调用都需要等待响应
|
|
||||||
- **频繁重连**:连接断开后的重连机制增加延迟
|
|
||||||
|
|
||||||
### 2. 应用层面问题
|
|
||||||
- **串行处理**:IP地址逐个处理,无法充分利用并发
|
|
||||||
- **过度验证**:`verify_add: true` 会进行二次查询确认
|
|
||||||
- **缓存失效**:缓存TTL过长或过短都会影响性能
|
|
||||||
|
|
||||||
## 🚀 优化方案
|
|
||||||
|
|
||||||
### 立即可实施的配置优化
|
|
||||||
|
|
||||||
#### 1. 调整连接参数
|
|
||||||
```yaml
|
|
||||||
mikrotik_amazon:
|
|
||||||
type: mikrotik_addresslist
|
|
||||||
args:
|
|
||||||
timeout: 3 # 🔥 减少连接超时时间
|
|
||||||
verify_add: false # 🔥 关闭验证,提升50%性能
|
|
||||||
cache_ttl: 7200 # 🔥 优化缓存时间(2小时)
|
|
||||||
max_ips: 10 # 🔥 限制IP数量,避免过载
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. 优化掩码设置
|
|
||||||
```yaml
|
|
||||||
mask4: 32 # 🔥 使用/32精确匹配
|
|
||||||
mask6: 128 # 🔥 使用/128精确匹配
|
|
||||||
```
|
|
||||||
**好处**:避免网段合并,提高缓存命中率
|
|
||||||
|
|
||||||
#### 3. 调整超时时间
|
|
||||||
```yaml
|
|
||||||
timeout_addr: 43200 # 🔥 12小时超时(原24小时)
|
|
||||||
```
|
|
||||||
**好处**:提高缓存命中率,减少重复写入
|
|
||||||
|
|
||||||
### 中级优化方案
|
|
||||||
|
|
||||||
#### 1. 启用批量处理
|
|
||||||
当前代码已支持批量处理,但可以进一步优化:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# 在配置中调整工作池大小
|
|
||||||
worker_pool_size: 15 # 增加工作线程
|
|
||||||
batch_size: 20 # 增加批处理大小
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. 网络层优化
|
|
||||||
```yaml
|
|
||||||
use_tls: false # 🔥 关闭TLS,减少握手时间
|
|
||||||
timeout: 3 # 🔥 快速失败,避免长时间等待
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. MikroTik 路由器端优化
|
|
||||||
```bash
|
|
||||||
# 在MikroTik中优化API设置
|
|
||||||
/ip service set api port=8728 disabled=no
|
|
||||||
/ip service set api-ssl disabled=yes # 关闭SSL,提升性能
|
|
||||||
|
|
||||||
# 增加API连接数限制
|
|
||||||
/ip service set api max-sessions=10
|
|
||||||
```
|
|
||||||
|
|
||||||
### 高级优化方案
|
|
||||||
|
|
||||||
#### 1. 连接池实现
|
|
||||||
创建连接池来复用连接:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 伪代码示例
|
|
||||||
type ConnectionPool struct {
|
|
||||||
connections chan *routeros.Client
|
|
||||||
maxSize int
|
|
||||||
host string
|
|
||||||
port int
|
|
||||||
username string
|
|
||||||
password string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnectionPool) Get() *routeros.Client {
|
|
||||||
select {
|
|
||||||
case conn := <-p.connections:
|
|
||||||
return conn
|
|
||||||
default:
|
|
||||||
return p.createConnection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnectionPool) Put(conn *routeros.Client) {
|
|
||||||
select {
|
|
||||||
case p.connections <- conn:
|
|
||||||
default:
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. 批量API调用
|
|
||||||
修改为真正的批量API调用:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 当前:多次单独调用
|
|
||||||
for _, ip := range ips {
|
|
||||||
conn.Run("/ip/firewall/address-list/add", "=list=gfw", "=address="+ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优化:批量调用
|
|
||||||
addresses := strings.Join(ips, ",")
|
|
||||||
conn.Run("/ip/firewall/address-list/add", "=list=gfw", "=address="+addresses)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. 异步处理队列
|
|
||||||
实现消息队列机制:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type IPQueue struct {
|
|
||||||
queue chan IPTask
|
|
||||||
workers int
|
|
||||||
}
|
|
||||||
|
|
||||||
type IPTask struct {
|
|
||||||
IPs []string
|
|
||||||
ListName string
|
|
||||||
Domain string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 性能对比
|
|
||||||
|
|
||||||
| 优化项目 | 优化前 | 优化后 | 提升幅度 |
|
|
||||||
|---------|--------|--------|----------|
|
|
||||||
| 连接超时 | 10秒 | 3秒 | 70% ⬇️ |
|
|
||||||
| 验证开关 | 开启 | 关闭 | 50% ⬆️ |
|
|
||||||
| 批处理大小 | 10 | 20 | 100% ⬆️ |
|
|
||||||
| 缓存TTL | 1小时 | 2小时 | 命中率+30% |
|
|
||||||
| 工作线程 | 10 | 15 | 50% ⬆️ |
|
|
||||||
|
|
||||||
## 🔧 实施步骤
|
|
||||||
|
|
||||||
### 第一阶段:配置优化(立即实施)
|
|
||||||
1. 更新 `dns.yaml` 配置文件
|
|
||||||
2. 重启 MosDNS 服务
|
|
||||||
3. 监控日志确认改进效果
|
|
||||||
|
|
||||||
### 第二阶段:MikroTik端优化
|
|
||||||
1. 优化MikroTik API设置
|
|
||||||
2. 调整防火墙规则
|
|
||||||
3. 监控系统资源使用
|
|
||||||
|
|
||||||
### 第三阶段:代码级优化(需要开发)
|
|
||||||
1. 实现连接池
|
|
||||||
2. 优化批量处理算法
|
|
||||||
3. 添加性能监控指标
|
|
||||||
|
|
||||||
## 📈 监控和测试
|
|
||||||
|
|
||||||
### 性能监控命令
|
|
||||||
```bash
|
|
||||||
# 查看MosDNS日志
|
|
||||||
sudo journalctl -u mosdns -f | grep mikrotik
|
|
||||||
|
|
||||||
# 监控MikroTik API性能
|
|
||||||
ssh admin@10.248.0.1 "/system resource monitor once"
|
|
||||||
|
|
||||||
# 检查地址列表大小
|
|
||||||
ssh admin@10.248.0.1 "/ip firewall address-list print count-only where list=gfw"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 压力测试
|
|
||||||
```bash
|
|
||||||
# 使用dig进行并发测试
|
|
||||||
for i in {1..100}; do
|
|
||||||
dig @127.0.0.1 -p 5300 amazon$i.com &
|
|
||||||
done
|
|
||||||
wait
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 预期效果
|
|
||||||
|
|
||||||
实施这些优化后,预期可以达到:
|
|
||||||
|
|
||||||
- **响应时间减少 60-70%**
|
|
||||||
- **并发处理能力提升 2-3倍**
|
|
||||||
- **内存使用量减少 30%**
|
|
||||||
- **错误率降低 50%**
|
|
||||||
|
|
||||||
## ⚠️ 注意事项
|
|
||||||
|
|
||||||
1. **分批实施**:避免一次性修改过多参数
|
|
||||||
2. **监控资源**:注意MikroTik路由器的CPU和内存使用
|
|
||||||
3. **备份配置**:修改前备份当前工作配置
|
|
||||||
4. **测试环境**:先在测试环境验证效果
|
|
||||||
|
|
||||||
## 🔗 相关资源
|
|
||||||
|
|
||||||
- [MikroTik API文档](https://wiki.mikrotik.com/wiki/Manual:API)
|
|
||||||
- [RouterOS API优化指南](https://wiki.mikrotik.com/wiki/Manual:API_examples)
|
|
||||||
- [Go RouterOS库文档](https://github.com/go-routeros/routeros)
|
|
||||||
181
README-VUE.md
Normal file
181
README-VUE.md
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
# 🌐 MosDNS Web 管理界面 - Vue 版本
|
||||||
|
|
||||||
|
> 基于 Vue 3 + Element Plus 的现代化 DNS 管理界面
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ 快速开始
|
||||||
|
|
||||||
|
### 第一次使用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 一键构建(Windows)
|
||||||
|
.\build-vue.bat
|
||||||
|
|
||||||
|
# 2. 启动服务
|
||||||
|
.\dist\mosdns-vue.exe start -c config.yaml
|
||||||
|
|
||||||
|
# 3. 访问界面
|
||||||
|
浏览器打开: http://localhost:5555
|
||||||
|
```
|
||||||
|
|
||||||
|
**就这么简单!** 🎉
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📸 界面预览
|
||||||
|
|
||||||
|
### 现代化的 Element Plus UI
|
||||||
|
- ✨ 美观的卡片式布局
|
||||||
|
- 📊 直观的数据展示
|
||||||
|
- 🎨 专业的配色方案
|
||||||
|
- 📱 完美的响应式设计
|
||||||
|
|
||||||
|
### 主要功能
|
||||||
|
|
||||||
|
**1. 仪表板** - 一览服务状态
|
||||||
|
- 运行状态、运行时间
|
||||||
|
- DNS 端口识别
|
||||||
|
- 查询统计
|
||||||
|
- 快速操作(刷新、清空缓存、重启)
|
||||||
|
|
||||||
|
**2. MikroTik 管理** - 轻松配置
|
||||||
|
- 可视化配置列表
|
||||||
|
- 表单化添加配置
|
||||||
|
- 字段验证提示
|
||||||
|
- 一键删除
|
||||||
|
|
||||||
|
**3. 域名文件** - 在线编辑
|
||||||
|
- 文件列表展示
|
||||||
|
- 在线查看编辑
|
||||||
|
- 文件信息统计
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 开发模式
|
||||||
|
|
||||||
|
**优势:** 修改代码后自动热重载,无需重新编译!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 终端 1: Go 后端
|
||||||
|
go run main.go start -c config.yaml
|
||||||
|
|
||||||
|
# 终端 2: Vue 开发服务器
|
||||||
|
cd web-ui
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 访问: http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 技术栈
|
||||||
|
|
||||||
|
- **Vue 3** - 渐进式 JavaScript 框架
|
||||||
|
- **Element Plus** - Vue 3 UI 组件库
|
||||||
|
- **TypeScript** - 类型安全
|
||||||
|
- **Pinia** - 状态管理
|
||||||
|
- **Vite** - 极速构建工具
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 完整文档
|
||||||
|
|
||||||
|
详细使用说明请查看:
|
||||||
|
- [`Vue版本使用指南.md`](./Vue版本使用指南.md) - 完整教程
|
||||||
|
- [`Vue重构完成总结.md`](./Vue重构完成总结.md) - 技术细节
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆚 对比原生版本
|
||||||
|
|
||||||
|
| 特性 | 原生 JS | Vue 版本 |
|
||||||
|
|------|---------|----------|
|
||||||
|
| 开发效率 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||||
|
| 代码维护 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||||
|
| 类型安全 | ❌ | ✅ TypeScript |
|
||||||
|
| 热重载 | ❌ | ✅ 秒级更新 |
|
||||||
|
| 组件复用 | ❌ | ✅ Element Plus |
|
||||||
|
| 开发工具 | 基础 | Vue DevTools |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 为什么选择 Vue 版本?
|
||||||
|
|
||||||
|
### 开发体验 ⬆️ 500%
|
||||||
|
|
||||||
|
**原生版本:**
|
||||||
|
```javascript
|
||||||
|
document.getElementById('list').innerHTML = html // 手动 DOM
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vue 版本:**
|
||||||
|
```vue
|
||||||
|
<el-table :data="configs"> <!-- 数据驱动 -->
|
||||||
|
```
|
||||||
|
|
||||||
|
### 维护成本 ⬇️ 70%
|
||||||
|
|
||||||
|
- 组件化开发,代码更清晰
|
||||||
|
- TypeScript 类型检查,减少 bug
|
||||||
|
- Pinia 状态管理,逻辑集中
|
||||||
|
|
||||||
|
### 扩展性 ⬆️ 无限
|
||||||
|
|
||||||
|
- 丰富的 Element Plus 组件库
|
||||||
|
- 成熟的 Vue 生态系统
|
||||||
|
- 活跃的社区支持
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 系统要求
|
||||||
|
|
||||||
|
### 开发环境
|
||||||
|
- Node.js 16+
|
||||||
|
- Go 1.21+
|
||||||
|
|
||||||
|
### 生产环境
|
||||||
|
- 只需要 Go 编译后的可执行文件
|
||||||
|
- **用户无需安装 Node.js**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
mosdns/
|
||||||
|
├── web-ui/ # Vue 3 项目
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── api/ # API 封装
|
||||||
|
│ │ ├── stores/ # 状态管理
|
||||||
|
│ │ ├── views/ # 页面组件
|
||||||
|
│ │ └── router/ # 路由配置
|
||||||
|
│ └── dist/ # 构建产物
|
||||||
|
├── coremain/
|
||||||
|
│ └── web_ui.go # Go 后端(Vue 版)
|
||||||
|
└── build-vue.bat # 一键构建脚本
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 立即体验
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆项目(如果还没有)
|
||||||
|
cd D:\Golang\yltx-dns\mosdns
|
||||||
|
|
||||||
|
# 构建
|
||||||
|
.\build-vue.bat
|
||||||
|
|
||||||
|
# 运行
|
||||||
|
.\dist\mosdns-vue.exe start -c config.yaml
|
||||||
|
|
||||||
|
# 享受现代化的管理界面!
|
||||||
|
```
|
||||||
|
|
||||||
|
访问 **http://localhost:5555** 🚀
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Made with ❤️ using Vue 3 + Element Plus**
|
||||||
|
|
||||||
435
build-all-platforms.bat
Normal file
435
build-all-platforms.bat
Normal file
@ -0,0 +1,435 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
REM ========================================
|
||||||
|
REM MosDNS 多平台构建脚本 (带 Web UI)
|
||||||
|
REM 支持: Linux, Windows, macOS (amd64/arm64)
|
||||||
|
REM ========================================
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ╔════════════════════════════════════════════╗
|
||||||
|
echo ║ MosDNS 多平台构建工具 (带 Web UI) ║
|
||||||
|
echo ╚════════════════════════════════════════════╝
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM 检查 Go 环境
|
||||||
|
where go >nul 2>nul
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo ❌ Go 未安装或不在 PATH 中
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo ✅ Go 版本:
|
||||||
|
go version
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM 检查并构建 Vue 前端
|
||||||
|
echo [检查] Vue 前端资源...
|
||||||
|
if not exist "web-ui\dist\index.html" (
|
||||||
|
echo ⚠️ Vue 前端未构建,开始构建...
|
||||||
|
|
||||||
|
REM 检查 Node.js
|
||||||
|
where node >nul 2>nul
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo ❌ Node.js 未安装,无法构建 Vue 前端
|
||||||
|
echo 💡 请安装 Node.js 或手动运行: cd web-ui ^&^& npm install ^&^& npm run build
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM 检查 npm
|
||||||
|
where npm >nul 2>nul
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo ❌ npm 未找到
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [1/2] 安装 Vue 依赖...
|
||||||
|
cd web-ui
|
||||||
|
if not exist "node_modules" (
|
||||||
|
call npm install
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo ❌ npm install 失败
|
||||||
|
cd ..
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [2/2] 构建 Vue 前端...
|
||||||
|
call npm run build
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo ❌ Vue 构建失败
|
||||||
|
cd ..
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
cd ..
|
||||||
|
echo ✅ Vue 前端构建完成
|
||||||
|
) else (
|
||||||
|
echo ✅ Vue 前端资源已存在
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM ========================================
|
||||||
|
REM 显示平台选择菜单
|
||||||
|
REM ========================================
|
||||||
|
:MENU
|
||||||
|
cls
|
||||||
|
echo.
|
||||||
|
echo ╔════════════════════════════════════════════╗
|
||||||
|
echo ║ MosDNS 多平台构建工具 (带 Web UI) ║
|
||||||
|
echo ╚════════════════════════════════════════════╝
|
||||||
|
echo.
|
||||||
|
echo 请选择要编译的平台:
|
||||||
|
echo.
|
||||||
|
echo [1] Linux AMD64 (x86_64 服务器)
|
||||||
|
echo [2] Linux ARM64 (树莓派、ARM 服务器)
|
||||||
|
echo [3] Windows AMD64 (Windows 64位)
|
||||||
|
echo [4] macOS AMD64 (Intel Mac)
|
||||||
|
echo [5] macOS ARM64 (Apple Silicon M1/M2/M3)
|
||||||
|
echo.
|
||||||
|
echo [6] 编译所有 Linux 版本 (AMD64 + ARM64)
|
||||||
|
echo [7] 编译所有 macOS 版本 (AMD64 + ARM64)
|
||||||
|
echo [8] 编译所有 Windows 版本 (仅 AMD64)
|
||||||
|
echo.
|
||||||
|
echo [A] 编译全部平台 (推荐用于发布)
|
||||||
|
echo.
|
||||||
|
echo [0] 退出
|
||||||
|
echo.
|
||||||
|
echo ════════════════════════════════════════════
|
||||||
|
echo.
|
||||||
|
|
||||||
|
set /p CHOICE="请输入选项 [0-8/A]: "
|
||||||
|
|
||||||
|
if /i "%CHOICE%"=="0" exit /b 0
|
||||||
|
if /i "%CHOICE%"=="1" goto BUILD_LINUX_AMD64
|
||||||
|
if /i "%CHOICE%"=="2" goto BUILD_LINUX_ARM64
|
||||||
|
if /i "%CHOICE%"=="3" goto BUILD_WINDOWS_AMD64
|
||||||
|
if /i "%CHOICE%"=="4" goto BUILD_MACOS_AMD64
|
||||||
|
if /i "%CHOICE%"=="5" goto BUILD_MACOS_ARM64
|
||||||
|
if /i "%CHOICE%"=="6" goto BUILD_ALL_LINUX
|
||||||
|
if /i "%CHOICE%"=="7" goto BUILD_ALL_MACOS
|
||||||
|
if /i "%CHOICE%"=="8" goto BUILD_ALL_WINDOWS
|
||||||
|
if /i "%CHOICE%"=="A" goto BUILD_ALL
|
||||||
|
if /i "%CHOICE%"=="a" goto BUILD_ALL
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ❌ 无效的选项,请重新选择
|
||||||
|
timeout /t 2 >nul
|
||||||
|
goto MENU
|
||||||
|
|
||||||
|
REM ========================================
|
||||||
|
REM 初始化构建环境
|
||||||
|
REM ========================================
|
||||||
|
:INIT_BUILD
|
||||||
|
echo.
|
||||||
|
echo ════════════════════════════════════════════
|
||||||
|
echo.
|
||||||
|
echo [准备] 设置构建参数...
|
||||||
|
for /f "tokens=2 delims==" %%a in ('wmic OS Get localdatetime /value') do set "dt=%%a"
|
||||||
|
set "BUILD_TIME=%dt:~0,4%-%dt:~4,2%-%dt:~6,2% %dt:~8,2%:%dt:~10,2%:%dt:~12,2%"
|
||||||
|
set "VERSION=v5.0.0-webui"
|
||||||
|
set "OUTPUT_DIR=dist"
|
||||||
|
|
||||||
|
REM 创建输出目录
|
||||||
|
if not exist "%OUTPUT_DIR%" mkdir "%OUTPUT_DIR%"
|
||||||
|
|
||||||
|
echo 版本: %VERSION%
|
||||||
|
echo 构建时间: %BUILD_TIME%
|
||||||
|
echo 输出目录: %OUTPUT_DIR%\
|
||||||
|
echo.
|
||||||
|
|
||||||
|
set CGO_ENABLED=0
|
||||||
|
set "LDFLAGS=-s -w -X 'main.version=%VERSION%' -X 'main.buildTime=%BUILD_TIME%'"
|
||||||
|
|
||||||
|
echo [开始] 编译中...
|
||||||
|
echo.
|
||||||
|
goto :EOF
|
||||||
|
|
||||||
|
REM ========================================
|
||||||
|
REM 单平台编译
|
||||||
|
REM ========================================
|
||||||
|
:BUILD_LINUX_AMD64
|
||||||
|
call :INIT_BUILD
|
||||||
|
echo 🔨 构建 Linux AMD64...
|
||||||
|
set GOOS=linux
|
||||||
|
set GOARCH=amd64
|
||||||
|
go build -ldflags="%LDFLAGS%" -o "%OUTPUT_DIR%\mosdns-linux-amd64" .
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo ✅ mosdns-linux-amd64 构建成功
|
||||||
|
call :SHOW_RESULT
|
||||||
|
) else (
|
||||||
|
echo ❌ 构建失败
|
||||||
|
pause
|
||||||
|
)
|
||||||
|
goto END
|
||||||
|
|
||||||
|
:BUILD_LINUX_ARM64
|
||||||
|
call :INIT_BUILD
|
||||||
|
echo 🔨 构建 Linux ARM64...
|
||||||
|
set GOOS=linux
|
||||||
|
set GOARCH=arm64
|
||||||
|
go build -ldflags="%LDFLAGS%" -o "%OUTPUT_DIR%\mosdns-linux-arm64" .
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo ✅ mosdns-linux-arm64 构建成功
|
||||||
|
call :SHOW_RESULT
|
||||||
|
) else (
|
||||||
|
echo ❌ 构建失败
|
||||||
|
pause
|
||||||
|
)
|
||||||
|
goto END
|
||||||
|
|
||||||
|
:BUILD_WINDOWS_AMD64
|
||||||
|
call :INIT_BUILD
|
||||||
|
echo 🔨 构建 Windows AMD64...
|
||||||
|
set GOOS=windows
|
||||||
|
set GOARCH=amd64
|
||||||
|
go build -ldflags="%LDFLAGS%" -o "%OUTPUT_DIR%\mosdns-windows-amd64.exe" .
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo ✅ mosdns-windows-amd64.exe 构建成功
|
||||||
|
call :SHOW_RESULT
|
||||||
|
) else (
|
||||||
|
echo ❌ 构建失败
|
||||||
|
pause
|
||||||
|
)
|
||||||
|
goto END
|
||||||
|
|
||||||
|
:BUILD_MACOS_AMD64
|
||||||
|
call :INIT_BUILD
|
||||||
|
echo 🔨 构建 macOS AMD64 (Intel Mac)...
|
||||||
|
set GOOS=darwin
|
||||||
|
set GOARCH=amd64
|
||||||
|
go build -ldflags="%LDFLAGS%" -o "%OUTPUT_DIR%\mosdns-darwin-amd64" .
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo ✅ mosdns-darwin-amd64 构建成功
|
||||||
|
call :SHOW_RESULT
|
||||||
|
) else (
|
||||||
|
echo ❌ 构建失败
|
||||||
|
pause
|
||||||
|
)
|
||||||
|
goto END
|
||||||
|
|
||||||
|
:BUILD_MACOS_ARM64
|
||||||
|
call :INIT_BUILD
|
||||||
|
echo 🔨 构建 macOS ARM64 (Apple Silicon)...
|
||||||
|
set GOOS=darwin
|
||||||
|
set GOARCH=arm64
|
||||||
|
go build -ldflags="%LDFLAGS%" -o "%OUTPUT_DIR%\mosdns-darwin-arm64" .
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo ✅ mosdns-darwin-arm64 构建成功
|
||||||
|
call :SHOW_RESULT
|
||||||
|
) else (
|
||||||
|
echo ❌ 构建失败
|
||||||
|
pause
|
||||||
|
)
|
||||||
|
goto END
|
||||||
|
|
||||||
|
REM ========================================
|
||||||
|
REM 批量编译
|
||||||
|
REM ========================================
|
||||||
|
:BUILD_ALL_LINUX
|
||||||
|
call :INIT_BUILD
|
||||||
|
echo [1/2] 🔨 构建 Linux AMD64...
|
||||||
|
set GOOS=linux
|
||||||
|
set GOARCH=amd64
|
||||||
|
go build -ldflags="%LDFLAGS%" -o "%OUTPUT_DIR%\mosdns-linux-amd64" .
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo ✅ mosdns-linux-amd64 构建成功
|
||||||
|
) else (
|
||||||
|
echo ❌ 构建失败
|
||||||
|
set "BUILD_FAILED=1"
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo [2/2] 🔨 构建 Linux ARM64...
|
||||||
|
set GOOS=linux
|
||||||
|
set GOARCH=arm64
|
||||||
|
go build -ldflags="%LDFLAGS%" -o "%OUTPUT_DIR%\mosdns-linux-arm64" .
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo ✅ mosdns-linux-arm64 构建成功
|
||||||
|
) else (
|
||||||
|
echo ❌ 构建失败
|
||||||
|
set "BUILD_FAILED=1"
|
||||||
|
)
|
||||||
|
call :SHOW_RESULT
|
||||||
|
goto END
|
||||||
|
|
||||||
|
:BUILD_ALL_MACOS
|
||||||
|
call :INIT_BUILD
|
||||||
|
echo [1/2] 🔨 构建 macOS AMD64 (Intel Mac)...
|
||||||
|
set GOOS=darwin
|
||||||
|
set GOARCH=amd64
|
||||||
|
go build -ldflags="%LDFLAGS%" -o "%OUTPUT_DIR%\mosdns-darwin-amd64" .
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo ✅ mosdns-darwin-amd64 构建成功
|
||||||
|
) else (
|
||||||
|
echo ❌ 构建失败
|
||||||
|
set "BUILD_FAILED=1"
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo [2/2] 🔨 构建 macOS ARM64 (Apple Silicon)...
|
||||||
|
set GOOS=darwin
|
||||||
|
set GOARCH=arm64
|
||||||
|
go build -ldflags="%LDFLAGS%" -o "%OUTPUT_DIR%\mosdns-darwin-arm64" .
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo ✅ mosdns-darwin-arm64 构建成功
|
||||||
|
) else (
|
||||||
|
echo ❌ 构建失败
|
||||||
|
set "BUILD_FAILED=1"
|
||||||
|
)
|
||||||
|
call :SHOW_RESULT
|
||||||
|
goto END
|
||||||
|
|
||||||
|
:BUILD_ALL_WINDOWS
|
||||||
|
call :INIT_BUILD
|
||||||
|
echo [1/1] 🔨 构建 Windows AMD64...
|
||||||
|
set GOOS=windows
|
||||||
|
set GOARCH=amd64
|
||||||
|
go build -ldflags="%LDFLAGS%" -o "%OUTPUT_DIR%\mosdns-windows-amd64.exe" .
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo ✅ mosdns-windows-amd64.exe 构建成功
|
||||||
|
) else (
|
||||||
|
echo ❌ 构建失败
|
||||||
|
set "BUILD_FAILED=1"
|
||||||
|
)
|
||||||
|
call :SHOW_RESULT
|
||||||
|
goto END
|
||||||
|
|
||||||
|
:BUILD_ALL
|
||||||
|
call :INIT_BUILD
|
||||||
|
echo [1/5] 🔨 构建 Linux AMD64...
|
||||||
|
set GOOS=linux
|
||||||
|
set GOARCH=amd64
|
||||||
|
go build -ldflags="%LDFLAGS%" -o "%OUTPUT_DIR%\mosdns-linux-amd64" .
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo ✅ mosdns-linux-amd64 构建成功
|
||||||
|
) else (
|
||||||
|
echo ❌ 构建失败
|
||||||
|
set "BUILD_FAILED=1"
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo [2/5] 🔨 构建 Linux ARM64...
|
||||||
|
set GOOS=linux
|
||||||
|
set GOARCH=arm64
|
||||||
|
go build -ldflags="%LDFLAGS%" -o "%OUTPUT_DIR%\mosdns-linux-arm64" .
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo ✅ mosdns-linux-arm64 构建成功
|
||||||
|
) else (
|
||||||
|
echo ❌ 构建失败
|
||||||
|
set "BUILD_FAILED=1"
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo [3/5] 🔨 构建 Windows AMD64...
|
||||||
|
set GOOS=windows
|
||||||
|
set GOARCH=amd64
|
||||||
|
go build -ldflags="%LDFLAGS%" -o "%OUTPUT_DIR%\mosdns-windows-amd64.exe" .
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo ✅ mosdns-windows-amd64.exe 构建成功
|
||||||
|
) else (
|
||||||
|
echo ❌ 构建失败
|
||||||
|
set "BUILD_FAILED=1"
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo [4/5] 🔨 构建 macOS AMD64...
|
||||||
|
set GOOS=darwin
|
||||||
|
set GOARCH=amd64
|
||||||
|
go build -ldflags="%LDFLAGS%" -o "%OUTPUT_DIR%\mosdns-darwin-amd64" .
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo ✅ mosdns-darwin-amd64 构建成功
|
||||||
|
) else (
|
||||||
|
echo ❌ 构建失败
|
||||||
|
set "BUILD_FAILED=1"
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo [5/5] 🔨 构建 macOS ARM64 (Apple Silicon)...
|
||||||
|
set GOOS=darwin
|
||||||
|
set GOARCH=arm64
|
||||||
|
go build -ldflags="%LDFLAGS%" -o "%OUTPUT_DIR%\mosdns-darwin-arm64" .
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo ✅ mosdns-darwin-arm64 构建成功
|
||||||
|
) else (
|
||||||
|
echo ❌ 构建失败
|
||||||
|
set "BUILD_FAILED=1"
|
||||||
|
)
|
||||||
|
call :SHOW_RESULT
|
||||||
|
goto END
|
||||||
|
|
||||||
|
REM ========================================
|
||||||
|
REM 显示构建结果
|
||||||
|
REM ========================================
|
||||||
|
:SHOW_RESULT
|
||||||
|
echo.
|
||||||
|
echo ════════════════════════════════════════════
|
||||||
|
echo.
|
||||||
|
|
||||||
|
if "%BUILD_FAILED%"=="1" (
|
||||||
|
echo ⚠️ 部分平台构建失败,请检查错误信息
|
||||||
|
echo.
|
||||||
|
) else (
|
||||||
|
echo 🎉 构建完成!
|
||||||
|
echo.
|
||||||
|
)
|
||||||
|
|
||||||
|
REM 检查是否有构建产物
|
||||||
|
if exist "%OUTPUT_DIR%\mosdns-*" (
|
||||||
|
echo 📦 构建产物列表:
|
||||||
|
echo.
|
||||||
|
dir /B "%OUTPUT_DIR%\mosdns-*" 2>nul
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo 📊 文件大小详情:
|
||||||
|
for %%F in (%OUTPUT_DIR%\mosdns-*) do (
|
||||||
|
set "size=%%~zF"
|
||||||
|
set /a size_mb=!size! / 1048576
|
||||||
|
echo %%~nxF - !size_mb! MB
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
) else (
|
||||||
|
echo ⚠️ 未找到构建产物
|
||||||
|
echo.
|
||||||
|
)
|
||||||
|
|
||||||
|
echo ════════════════════════════════════════════
|
||||||
|
echo.
|
||||||
|
echo 📝 使用方法:
|
||||||
|
echo.
|
||||||
|
echo Windows:
|
||||||
|
echo %OUTPUT_DIR%\mosdns-windows-amd64.exe start -c config.yaml
|
||||||
|
echo.
|
||||||
|
echo Linux:
|
||||||
|
echo chmod +x %OUTPUT_DIR%/mosdns-linux-amd64
|
||||||
|
echo ./%OUTPUT_DIR%/mosdns-linux-amd64 start -c config.yaml
|
||||||
|
echo.
|
||||||
|
echo macOS:
|
||||||
|
echo chmod +x %OUTPUT_DIR%/mosdns-darwin-amd64
|
||||||
|
echo ./%OUTPUT_DIR%/mosdns-darwin-amd64 start -c config.yaml
|
||||||
|
echo.
|
||||||
|
echo 🌐 Web 管理界面: http://localhost:5545
|
||||||
|
echo.
|
||||||
|
echo 💡 提示: 所有可执行文件已内嵌 Web 资源,可独立运行
|
||||||
|
echo.
|
||||||
|
goto :EOF
|
||||||
|
|
||||||
|
REM ========================================
|
||||||
|
REM 结束
|
||||||
|
REM ========================================
|
||||||
|
:END
|
||||||
|
echo.
|
||||||
|
set /p CONTINUE="是否继续编译其他平台?(Y/N): "
|
||||||
|
if /i "%CONTINUE%"=="Y" (
|
||||||
|
set "BUILD_FAILED="
|
||||||
|
goto MENU
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo 感谢使用 MosDNS 构建工具!
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 0
|
||||||
71
build-vue.bat
Normal file
71
build-vue.bat
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
echo.
|
||||||
|
echo ====================================
|
||||||
|
echo MosDNS Vue 版本构建脚本
|
||||||
|
echo ====================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo [1/3] 检查 Node.js 环境...
|
||||||
|
where node >nul 2>nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ❌ 错误: 未找到 Node.js!
|
||||||
|
echo 请先安装 Node.js: https://nodejs.org/
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
echo ✅ Node.js 已安装
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [2/3] 构建 Vue 前端...
|
||||||
|
cd web-ui
|
||||||
|
|
||||||
|
if not exist "node_modules\" (
|
||||||
|
echo 📦 首次构建,正在安装依赖...
|
||||||
|
call npm install
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ❌ npm install 失败!
|
||||||
|
cd ..
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
echo 🔨 正在构建 Vue 项目...
|
||||||
|
call npm run build
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ❌ Vue 构建失败!
|
||||||
|
cd ..
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo ✅ Vue 构建完成
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [3/3] 构建 Go 后端...
|
||||||
|
echo 🔨 正在编译 Go 程序...
|
||||||
|
go build -o dist\mosdns-vue.exe .
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ❌ Go 编译失败!
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo ✅ Go 编译完成
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ====================================
|
||||||
|
echo ✅ 构建完成!
|
||||||
|
echo ====================================
|
||||||
|
echo.
|
||||||
|
echo 可执行文件: dist\mosdns-vue.exe
|
||||||
|
echo.
|
||||||
|
echo 运行命令:
|
||||||
|
echo dist\mosdns-vue.exe start -c config.yaml
|
||||||
|
echo.
|
||||||
|
echo 然后访问: http://localhost:5555
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
|
||||||
52
build-vue.sh
Normal file
52
build-vue.sh
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "===================================="
|
||||||
|
echo " MosDNS Vue 版本构建脚本"
|
||||||
|
echo "===================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "[1/3] 检查 Node.js 环境..."
|
||||||
|
if ! command -v node &> /dev/null; then
|
||||||
|
echo "❌ 错误: 未找到 Node.js!"
|
||||||
|
echo "请先安装 Node.js: https://nodejs.org/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Node.js 已安装"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[2/3] 构建 Vue 前端..."
|
||||||
|
cd web-ui
|
||||||
|
|
||||||
|
if [ ! -d "node_modules" ]; then
|
||||||
|
echo "📦 首次构建,正在安装依赖..."
|
||||||
|
npm install
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔨 正在构建 Vue 项目..."
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
echo "✅ Vue 构建完成"
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[3/3] 构建 Go 后端..."
|
||||||
|
echo "🔨 正在编译 Go 程序..."
|
||||||
|
go build -o dist/mosdns-vue .
|
||||||
|
|
||||||
|
echo "✅ Go 编译完成"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "===================================="
|
||||||
|
echo " ✅ 构建完成!"
|
||||||
|
echo "===================================="
|
||||||
|
echo ""
|
||||||
|
echo "可执行文件: dist/mosdns-vue"
|
||||||
|
echo ""
|
||||||
|
echo "运行命令:"
|
||||||
|
echo " ./dist/mosdns-vue start -c config.yaml"
|
||||||
|
echo ""
|
||||||
|
echo "然后访问: http://localhost:5555"
|
||||||
|
echo ""
|
||||||
|
|
||||||
230
config.yaml
230
config.yaml
@ -1,36 +1,51 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
# MosDNS v5 配置(GFW 解析并写入 MikroTik)
|
# MosDNS v5 最终优化配置
|
||||||
|
# 基于增强的 mikrotik_addresslist 插件
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
log:
|
log:
|
||||||
level: info
|
level: debug # 🔧 改为 debug 级别,查看详细日志
|
||||||
|
|
||||||
|
# 管理 API
|
||||||
|
api:
|
||||||
|
http: "0.0.0.0:5541"
|
||||||
|
web:
|
||||||
|
http: "0.0.0.0:5555"
|
||||||
plugins:
|
plugins:
|
||||||
# ========= 规则集 =========
|
# ========= 基础组件 =========
|
||||||
# GFW 域名(解析并写入 MikroTik)
|
|
||||||
|
# GFW 域名列表(仅用于分流,不写入设备)
|
||||||
- tag: GFW_domains
|
- tag: GFW_domains
|
||||||
type: domain_set
|
type: domain_set
|
||||||
args:
|
args:
|
||||||
files:
|
files:
|
||||||
- "/usr/local/jinlingma/config/gfwlist.out.txt"
|
- "/usr/local/yltx-dns/geosite/geosite_gfw.txt"
|
||||||
|
|
||||||
|
# 🆕 海外域名列表(包含所有需要海外解析的域名)
|
||||||
|
- tag: overseas_domains
|
||||||
|
type: domain_set
|
||||||
|
args:
|
||||||
|
files:
|
||||||
|
- "/usr/local/yltx-dns/geosite/geosite_gfw.txt"
|
||||||
|
- "/usr/local/yltx-dns/config/openai.txt"
|
||||||
|
|
||||||
# 中国大陆 IP 列表
|
# 中国大陆 IP 列表
|
||||||
- tag: geoip_cn
|
- tag: geoip_cn
|
||||||
type: ip_set
|
type: ip_set
|
||||||
args:
|
args:
|
||||||
files:
|
files:
|
||||||
- "/usr/local/jinlingma/config/cn.txt"
|
- "/usr/local/yltx-dns/config/cn.txt"
|
||||||
|
|
||||||
# 缓存
|
# 缓存
|
||||||
- tag: cache
|
- tag: cache
|
||||||
type: cache
|
type: cache
|
||||||
args:
|
args:
|
||||||
size: 32768
|
size: 82768
|
||||||
lazy_cache_ttl: 43200
|
lazy_cache_ttl: 43200
|
||||||
|
|
||||||
# ========= 上游定义 =========
|
# ========= 上游 DNS 定义 =========
|
||||||
# 国内上游
|
|
||||||
|
# 国内 DNS
|
||||||
- tag: china-dns
|
- tag: china-dns
|
||||||
type: forward
|
type: forward
|
||||||
args:
|
args:
|
||||||
@ -43,7 +58,7 @@ plugins:
|
|||||||
- addr: "udp://114.114.114.114"
|
- addr: "udp://114.114.114.114"
|
||||||
- addr: "udp://180.76.76.76"
|
- addr: "udp://180.76.76.76"
|
||||||
|
|
||||||
# 国外上游(DoT)
|
# 国外 DNS(DoT)
|
||||||
- tag: overseas-dns
|
- tag: overseas-dns
|
||||||
type: forward
|
type: forward
|
||||||
args:
|
args:
|
||||||
@ -94,85 +109,82 @@ plugins:
|
|||||||
- exec: query_summary forward_remote
|
- exec: query_summary forward_remote
|
||||||
- exec: $forward_remote
|
- exec: $forward_remote
|
||||||
|
|
||||||
# 若已有响应则直接返回
|
# ========= 🚀 增强的 MikroTik 插件(支持多设备多规则)=========
|
||||||
|
|
||||||
|
# 设备 A:OpenAI 相关域名
|
||||||
|
- tag: mikrotik_amazon
|
||||||
|
type: mikrotik_addresslist
|
||||||
|
args:
|
||||||
|
domain_files:
|
||||||
|
- "/usr/local/yltx-dns/config/openai.txt"
|
||||||
|
host: "10.248.0.1"
|
||||||
|
port: 9728
|
||||||
|
username: "admin"
|
||||||
|
password: "szn0s!nw@pwd()"
|
||||||
|
use_tls: false
|
||||||
|
timeout: 3
|
||||||
|
address_list4: "OpenAI"
|
||||||
|
mask4: 24
|
||||||
|
comment: "OpenAI-AutoAdd"
|
||||||
|
timeout_addr: 43200
|
||||||
|
cache_ttl: 3600
|
||||||
|
verify_add: false
|
||||||
|
add_all_ips: true
|
||||||
|
max_ips: 50
|
||||||
|
|
||||||
|
# 设备 B:Google 相关域名(示例 - 已注释)
|
||||||
|
# - tag: mikrotik_google
|
||||||
|
# type: mikrotik_addresslist
|
||||||
|
# args:
|
||||||
|
# domain_files:
|
||||||
|
# - "/usr/local/jinlingma/config/google.txt"
|
||||||
|
# - "/usr/local/jinlingma/config/youtube.txt"
|
||||||
|
# host: "10.96.1.23"
|
||||||
|
# port: 9728
|
||||||
|
# username: "admin"
|
||||||
|
# password: "szn0s!nw@pwd()"
|
||||||
|
# use_tls: false
|
||||||
|
# timeout: 3
|
||||||
|
# address_list4: "Google"
|
||||||
|
# mask4: 32 # 精确匹配单个IP
|
||||||
|
# comment: "Google-AutoAdd"
|
||||||
|
# timeout_addr: 21600 # 6小时
|
||||||
|
# cache_ttl: 1800 # 30分钟缓存
|
||||||
|
# verify_add: false
|
||||||
|
# add_all_ips: true
|
||||||
|
# max_ips: 15
|
||||||
|
|
||||||
|
# 设备 C:流媒体相关域名(示例 - 已注释)
|
||||||
|
# - tag: mikrotik_streaming
|
||||||
|
# type: mikrotik_addresslist
|
||||||
|
# args:
|
||||||
|
# domain_files:
|
||||||
|
# - "/usr/local/jinlingma/config/netflix.txt"
|
||||||
|
# - "/usr/local/jinlingma/config/disney.txt"
|
||||||
|
# host: "10.96.1.24"
|
||||||
|
# port: 9728
|
||||||
|
# username: "admin"
|
||||||
|
# password: "szn0s!nw@pwd()"
|
||||||
|
# use_tls: false
|
||||||
|
# timeout: 5 # 流媒体可能需要更长时间
|
||||||
|
# address_list4: "Streaming"
|
||||||
|
# mask4: 32
|
||||||
|
# comment: "Streaming-AutoAdd"
|
||||||
|
# timeout_addr: 21600 # 6小时(流媒体IP变化较频繁)
|
||||||
|
# cache_ttl: 1800 # 30分钟缓存
|
||||||
|
# verify_add: false
|
||||||
|
# add_all_ips: true
|
||||||
|
# max_ips: 30 # 流媒体服务IP较多
|
||||||
|
|
||||||
|
# ========= 查询逻辑 =========
|
||||||
|
|
||||||
|
# 检查是否有响应
|
||||||
- tag: has_resp_sequence
|
- tag: has_resp_sequence
|
||||||
type: sequence
|
type: sequence
|
||||||
args:
|
args:
|
||||||
- matches: has_resp
|
- matches: has_resp
|
||||||
exec: accept
|
exec: accept
|
||||||
|
|
||||||
# ========= 🚀 增强的 MikroTik 插件(支持多设备多规则)=========
|
|
||||||
|
|
||||||
# 设备 A:Amazon 相关域名
|
|
||||||
- tag: mikrotik_amazon
|
|
||||||
type: mikrotik_addresslist
|
|
||||||
domain_files:
|
|
||||||
- "/usr/local/jinlingma/config/amazon.txt"
|
|
||||||
- "/usr/local/jinlingma/config/aws.txt"
|
|
||||||
args:
|
|
||||||
host: "10.96.1.22"
|
|
||||||
port: 9728
|
|
||||||
username: "admin"
|
|
||||||
password: "szn0s!nw@pwd()"
|
|
||||||
use_tls: false
|
|
||||||
timeout: 3
|
|
||||||
address_list4: "Amazon"
|
|
||||||
address_list6: "Amazon6"
|
|
||||||
mask4: 24
|
|
||||||
mask6: 64
|
|
||||||
comment: "Amazon-AutoAdd"
|
|
||||||
timeout_addr: 43200
|
|
||||||
cache_ttl: 3600
|
|
||||||
verify_add: false
|
|
||||||
add_all_ips: true
|
|
||||||
max_ips: 20
|
|
||||||
|
|
||||||
# 设备 B:Google 相关域名
|
|
||||||
- tag: mikrotik_google
|
|
||||||
type: mikrotik_addresslist
|
|
||||||
domain_files:
|
|
||||||
- "/usr/local/jinlingma/config/google.txt"
|
|
||||||
- "/usr/local/jinlingma/config/youtube.txt"
|
|
||||||
args:
|
|
||||||
host: "10.96.1.23"
|
|
||||||
port: 9728
|
|
||||||
username: "admin"
|
|
||||||
password: "szn0s!nw@pwd()"
|
|
||||||
use_tls: false
|
|
||||||
timeout: 3
|
|
||||||
address_list4: "Google"
|
|
||||||
mask4: 32
|
|
||||||
comment: "Google-AutoAdd"
|
|
||||||
timeout_addr: 21600
|
|
||||||
cache_ttl: 1800
|
|
||||||
verify_add: false
|
|
||||||
add_all_ips: true
|
|
||||||
max_ips: 15
|
|
||||||
|
|
||||||
# 设备 C:流媒体相关域名(示例)
|
|
||||||
- tag: mikrotik_streaming
|
|
||||||
type: mikrotik_addresslist
|
|
||||||
domain_files:
|
|
||||||
- "/usr/local/jinlingma/config/netflix.txt"
|
|
||||||
- "/usr/local/jinlingma/config/disney.txt"
|
|
||||||
args:
|
|
||||||
host: "10.96.1.24"
|
|
||||||
port: 9728
|
|
||||||
username: "admin"
|
|
||||||
password: "szn0s!nw@pwd()"
|
|
||||||
use_tls: false
|
|
||||||
timeout: 5
|
|
||||||
address_list4: "Streaming"
|
|
||||||
mask4: 32
|
|
||||||
comment: "Streaming-AutoAdd"
|
|
||||||
timeout_addr: 21600
|
|
||||||
cache_ttl: 1800
|
|
||||||
verify_add: false
|
|
||||||
add_all_ips: true
|
|
||||||
max_ips: 30
|
|
||||||
|
|
||||||
# ========= 🚀 简化的查询逻辑 =========
|
|
||||||
|
|
||||||
# 拒绝无效查询
|
# 拒绝无效查询
|
||||||
- tag: reject_invalid
|
- tag: reject_invalid
|
||||||
type: sequence
|
type: sequence
|
||||||
@ -180,26 +192,41 @@ plugins:
|
|||||||
- matches: qtype 65
|
- matches: qtype 65
|
||||||
exec: reject 3
|
exec: reject 3
|
||||||
|
|
||||||
# GFW 域名分流(仅解析,不写入设备)
|
|
||||||
- tag: gfw_routing_only
|
|
||||||
type: sequence
|
|
||||||
args:
|
|
||||||
- matches: qname $GFW_domains
|
|
||||||
exec: $forward_remote_upstream
|
|
||||||
- exec: query_summary gfw_overseas_routing
|
|
||||||
|
|
||||||
# 智能 fallback 处理
|
# 智能 fallback 处理
|
||||||
- tag: smart_fallback_handler
|
- tag: smart_fallback_handler
|
||||||
type: sequence
|
type: sequence
|
||||||
args:
|
args:
|
||||||
- exec: prefer_ipv4
|
- exec: prefer_ipv4
|
||||||
- exec: $forward_local
|
- exec: $forward_local_upstream
|
||||||
- matches: resp_ip $geoip_cn
|
- matches: resp_ip $geoip_cn
|
||||||
exec: accept
|
exec: accept
|
||||||
- exec: $forward_remote_upstream
|
- exec: $forward_remote_upstream
|
||||||
- exec: query_summary fallback_to_overseas
|
- exec: query_summary fallback_to_overseas
|
||||||
|
|
||||||
# 🚀 极简主序列
|
# 🚀 海外域名分流 + MikroTik 处理
|
||||||
|
- tag: overseas_routing_with_mikrotik
|
||||||
|
type: sequence
|
||||||
|
args:
|
||||||
|
- matches: qname $overseas_domains
|
||||||
|
exec: $forward_remote_upstream
|
||||||
|
- matches: has_resp
|
||||||
|
exec: $mikrotik_amazon # 🔧 修复:在有DNS响应后才调用MikroTik
|
||||||
|
- matches: has_resp
|
||||||
|
exec: accept
|
||||||
|
- exec: query_summary overseas_routing
|
||||||
|
|
||||||
|
# 🚀 并行处理序列:优化的DNS解析流程
|
||||||
|
- tag: parallel_dns_and_mikrotik
|
||||||
|
type: sequence
|
||||||
|
args:
|
||||||
|
# DNS 解析逻辑
|
||||||
|
- exec: $overseas_routing_with_mikrotik # 🚀 海外域名分流 + MikroTik处理
|
||||||
|
- matches: has_resp
|
||||||
|
exec: accept
|
||||||
|
- exec: $smart_fallback_handler # 智能 fallback
|
||||||
|
|
||||||
|
# 🚀 主序列(优化版 - 并行处理)
|
||||||
- tag: main_sequence
|
- tag: main_sequence
|
||||||
type: sequence
|
type: sequence
|
||||||
args:
|
args:
|
||||||
@ -210,28 +237,19 @@ plugins:
|
|||||||
- exec: $reject_invalid
|
- exec: $reject_invalid
|
||||||
- exec: jump has_resp_sequence
|
- exec: jump has_resp_sequence
|
||||||
|
|
||||||
# 3. GFW 域名分流(仅解析)
|
# 3. 🚀 并行处理:DNS解析 + MikroTik处理
|
||||||
- exec: $gfw_routing_only
|
- exec: $parallel_dns_and_mikrotik
|
||||||
- exec: jump has_resp_sequence
|
- exec: jump has_resp_sequence
|
||||||
|
|
||||||
# 4. 智能 fallback
|
# ========= 服务监听 =========
|
||||||
- exec: $smart_fallback_handler
|
|
||||||
- exec: jump has_resp_sequence
|
|
||||||
|
|
||||||
# 5. 🚀 MikroTik 设备处理(每个插件自动匹配域名)
|
|
||||||
- exec: $mikrotik_amazon # 自动处理 Amazon 域名
|
|
||||||
- exec: $mikrotik_google # 自动处理 Google 域名
|
|
||||||
- exec: $mikrotik_streaming # 自动处理流媒体域名
|
|
||||||
|
|
||||||
# ========= 服务 =========
|
|
||||||
- tag: udp_server
|
- tag: udp_server
|
||||||
type: udp_server
|
type: udp_server
|
||||||
args:
|
args:
|
||||||
entry: main_sequence
|
entry: main_sequence
|
||||||
listen: ":5322"
|
listen: ":531"
|
||||||
|
|
||||||
- tag: tcp_server
|
- tag: tcp_server
|
||||||
type: tcp_server
|
type: tcp_server
|
||||||
args:
|
args:
|
||||||
entry: main_sequence
|
entry: main_sequence
|
||||||
listen: ":5322"
|
listen: ":531"
|
||||||
|
|||||||
1116
coremain/api_handlers.go
Normal file
1116
coremain/api_handlers.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -28,6 +28,7 @@ type Config struct {
|
|||||||
Include []string `yaml:"include"`
|
Include []string `yaml:"include"`
|
||||||
Plugins []PluginConfig `yaml:"plugins"`
|
Plugins []PluginConfig `yaml:"plugins"`
|
||||||
API APIConfig `yaml:"api"`
|
API APIConfig `yaml:"api"`
|
||||||
|
Web WebConfig `yaml:"web"` // Web 管理界面配置
|
||||||
}
|
}
|
||||||
|
|
||||||
// PluginConfig represents a plugin config
|
// PluginConfig represents a plugin config
|
||||||
@ -45,6 +46,12 @@ type PluginConfig struct {
|
|||||||
Args any `yaml:"args"`
|
Args any `yaml:"args"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APIConfig API 接口配置
|
||||||
type APIConfig struct {
|
type APIConfig struct {
|
||||||
HTTP string `yaml:"http"`
|
HTTP string `yaml:"http"` // API HTTP 监听地址,如 "0.0.0.0:5541"
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebConfig Web 管理界面配置
|
||||||
|
type WebConfig struct {
|
||||||
|
HTTP string `yaml:"http"` // Web UI HTTP 监听地址,如 "0.0.0.0:5555"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,10 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/pprof"
|
||||||
|
|
||||||
"github.com/IrineSistiana/mosdns/v5/mlog"
|
"github.com/IrineSistiana/mosdns/v5/mlog"
|
||||||
"github.com/IrineSistiana/mosdns/v5/pkg/safe_close"
|
"github.com/IrineSistiana/mosdns/v5/pkg/safe_close"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
@ -30,9 +34,6 @@ import (
|
|||||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/http/pprof"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Mosdns struct {
|
type Mosdns struct {
|
||||||
@ -41,7 +42,8 @@ type Mosdns struct {
|
|||||||
// Plugins
|
// Plugins
|
||||||
plugins map[string]any
|
plugins map[string]any
|
||||||
|
|
||||||
httpMux *chi.Mux
|
httpMux *chi.Mux // API 路由
|
||||||
|
webMux *chi.Mux // Web UI 路由(独立)
|
||||||
metricsReg *prometheus.Registry
|
metricsReg *prometheus.Registry
|
||||||
sc *safe_close.SafeClose
|
sc *safe_close.SafeClose
|
||||||
}
|
}
|
||||||
@ -57,15 +59,29 @@ func NewMosdns(cfg *Config) (*Mosdns, error) {
|
|||||||
m := &Mosdns{
|
m := &Mosdns{
|
||||||
logger: lg,
|
logger: lg,
|
||||||
plugins: make(map[string]any),
|
plugins: make(map[string]any),
|
||||||
httpMux: chi.NewRouter(),
|
httpMux: chi.NewRouter(), // API 路由
|
||||||
|
webMux: chi.NewRouter(), // Web UI 独立路由
|
||||||
metricsReg: newMetricsReg(),
|
metricsReg: newMetricsReg(),
|
||||||
sc: safe_close.NewSafeClose(),
|
sc: safe_close.NewSafeClose(),
|
||||||
}
|
}
|
||||||
// This must be called after m.httpMux and m.metricsReg been set.
|
// This must be called after m.httpMux and m.metricsReg been set.
|
||||||
m.initHttpMux()
|
m.initHttpMux()
|
||||||
|
|
||||||
|
// Register management API routes
|
||||||
|
m.registerManagementAPI()
|
||||||
|
|
||||||
|
// 如果配置了独立的 Web 端口,初始化 Web 路由
|
||||||
|
if len(cfg.Web.HTTP) > 0 {
|
||||||
|
m.initWebMux()
|
||||||
|
m.registerWebUI()
|
||||||
|
m.registerWebAPI()
|
||||||
|
}
|
||||||
|
|
||||||
// Start http api server
|
// Start http api server
|
||||||
if httpAddr := cfg.API.HTTP; len(httpAddr) > 0 {
|
if httpAddr := cfg.API.HTTP; len(httpAddr) > 0 {
|
||||||
|
// 设置当前 API 地址,供 Web UI 显示
|
||||||
|
SetCurrentAPIAddress(httpAddr)
|
||||||
|
|
||||||
httpServer := &http.Server{
|
httpServer := &http.Server{
|
||||||
Addr: httpAddr,
|
Addr: httpAddr,
|
||||||
Handler: m.httpMux,
|
Handler: m.httpMux,
|
||||||
@ -86,6 +102,28 @@ func NewMosdns(cfg *Config) (*Mosdns, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start web ui server (独立端口)
|
||||||
|
if webAddr := cfg.Web.HTTP; len(webAddr) > 0 {
|
||||||
|
webServer := &http.Server{
|
||||||
|
Addr: webAddr,
|
||||||
|
Handler: m.webMux,
|
||||||
|
}
|
||||||
|
m.sc.Attach(func(done func(), closeSignal <-chan struct{}) {
|
||||||
|
defer done()
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
m.logger.Info("starting web ui http server", zap.String("addr", webAddr))
|
||||||
|
errChan <- webServer.ListenAndServe()
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case err := <-errChan:
|
||||||
|
m.sc.SendCloseSignal(err)
|
||||||
|
case <-closeSignal:
|
||||||
|
_ = webServer.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Load plugins.
|
// Load plugins.
|
||||||
|
|
||||||
// Close all plugins on signal.
|
// Close all plugins on signal.
|
||||||
@ -204,6 +242,14 @@ func (m *Mosdns) initHttpMux() {
|
|||||||
m.httpMux.MethodNotAllowed(invalidApiReqHelper)
|
m.httpMux.MethodNotAllowed(invalidApiReqHelper)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initWebMux 初始化 Web UI 路由
|
||||||
|
func (m *Mosdns) initWebMux() {
|
||||||
|
// Web UI 的 404 页面
|
||||||
|
m.webMux.NotFound(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
http.Error(w, "Page not found", http.StatusNotFound)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Mosdns) loadPresetPlugins() error {
|
func (m *Mosdns) loadPresetPlugins() error {
|
||||||
for tag, f := range LoadNewPersetPluginFuncs() {
|
for tag, f := range LoadNewPersetPluginFuncs() {
|
||||||
p, err := f(NewBP(tag, m))
|
p, err := f(NewBP(tag, m))
|
||||||
|
|||||||
@ -21,16 +21,17 @@ package coremain
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/IrineSistiana/mosdns/v5/mlog"
|
"github.com/IrineSistiana/mosdns/v5/mlog"
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"runtime"
|
|
||||||
"syscall"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type serverFlags struct {
|
type serverFlags struct {
|
||||||
@ -126,6 +127,9 @@ func NewServer(sf *serverFlags) (*Mosdns, error) {
|
|||||||
}
|
}
|
||||||
mlog.L().Info("main config loaded", zap.String("file", fileUsed))
|
mlog.L().Info("main config loaded", zap.String("file", fileUsed))
|
||||||
|
|
||||||
|
// Set current config file for API access
|
||||||
|
SetCurrentConfigFile(fileUsed)
|
||||||
|
|
||||||
return NewMosdns(cfg)
|
return NewMosdns(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
631
coremain/web/static/css/style.css
Normal file
631
coremain/web/static/css/style.css
Normal file
@ -0,0 +1,631 @@
|
|||||||
|
/* 基础样式重置 - Element Plus 风格 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--el-color-primary: #409eff;
|
||||||
|
--el-color-success: #67c23a;
|
||||||
|
--el-color-warning: #e6a23c;
|
||||||
|
--el-color-danger: #f56c6c;
|
||||||
|
--el-color-info: #909399;
|
||||||
|
--el-bg-color: #f0f2f5;
|
||||||
|
--el-bg-color-page: #f0f2f5;
|
||||||
|
--el-border-color: #dcdfe6;
|
||||||
|
--el-border-radius-base: 4px;
|
||||||
|
--el-box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
|
||||||
|
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
background-color: var(--el-bg-color-page);
|
||||||
|
color: #303133;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部样式 - Element Plus 风格 */
|
||||||
|
.header {
|
||||||
|
background: #fff;
|
||||||
|
color: #303133;
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,21,41,.08);
|
||||||
|
border-bottom: 1px solid var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 24px;
|
||||||
|
height: 64px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: #f0f9ff;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
border: 1px solid #d0ebff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 导航栏样式 - Element Plus Tabs 风格 */
|
||||||
|
.nav {
|
||||||
|
background: white;
|
||||||
|
border-bottom: 2px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-content {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 24px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
position: relative;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
border-bottom-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 - vben 风格 */
|
||||||
|
.main {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px 24px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: calc(100vh - 128px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签页内容 */
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 仪表板网格布局 */
|
||||||
|
.dashboard-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片样式 - Element Plus Card 风格 */
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: var(--el-border-radius-base);
|
||||||
|
box-shadow: var(--el-box-shadow-light);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
padding: 18px 20px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计项 */
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式 - Element Plus Button 风格 */
|
||||||
|
.btn {
|
||||||
|
padding: 9px 15px;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: var(--el-border-radius-base);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--el-color-primary);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #66b1ff;
|
||||||
|
border-color: #66b1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:active {
|
||||||
|
background: #3a8ee6;
|
||||||
|
border-color: #3a8ee6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: white;
|
||||||
|
color: #606266;
|
||||||
|
border-color: var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
border-color: #c6e2ff;
|
||||||
|
background: #ecf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--el-color-danger);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--el-color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #f78989;
|
||||||
|
border-color: #f78989;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MikroTik 表单样式 - 优化版 Element Plus Form 风格 */
|
||||||
|
.mikrotik-form {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-required {
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control,
|
||||||
|
.form-group input,
|
||||||
|
.form-group select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: var(--el-border-radius-base);
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
line-height: 1.5;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus,
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:hover:not(:focus),
|
||||||
|
.form-group input:hover:not(:focus),
|
||||||
|
.form-group select:hover:not(:focus) {
|
||||||
|
border-color: #c0c4cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lg {
|
||||||
|
padding: 11px 20px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MikroTik 配置列表 */
|
||||||
|
.mikrotik-list {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mikrotik-item {
|
||||||
|
background: #fafafa;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: var(--el-border-radius-base);
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mikrotik-item:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
box-shadow: var(--el-box-shadow-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mikrotik-item-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mikrotik-item-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mikrotik-item-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mikrotik-item-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mikrotik-item-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mikrotik-item-label {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mikrotik-item-value {
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 结果区域 */
|
||||||
|
.result-area {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: var(--el-border-radius-base);
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-area h4 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-area pre {
|
||||||
|
margin: 12px 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
background: #fff;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: var(--el-border-radius-base);
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 域名文件列表 */
|
||||||
|
.domain-files-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-file-item {
|
||||||
|
background: #fafafa;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: var(--el-border-radius-base);
|
||||||
|
padding: 16px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-file-item:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
box-shadow: var(--el-box-shadow-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-file-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-file-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-file-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 日志查看器 */
|
||||||
|
.logs-container {
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: var(--el-border-radius-base);
|
||||||
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 配置编辑器 */
|
||||||
|
.config-editor {
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: var(--el-border-radius-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-editor textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 500px;
|
||||||
|
padding: 16px;
|
||||||
|
border: none;
|
||||||
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-editor textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 消息提示 */
|
||||||
|
.message {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
min-width: 300px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: white;
|
||||||
|
border-radius: var(--el-border-radius-base);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
animation: slideInRight 0.3s ease;
|
||||||
|
z-index: 9999;
|
||||||
|
border-left: 4px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.success {
|
||||||
|
border-left-color: var(--el-color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
border-left-color: var(--el-color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.warning {
|
||||||
|
border-left-color: var(--el-color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.info {
|
||||||
|
border-left-color: var(--el-color-info);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载状态 */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画 */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header-content {
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-content {
|
||||||
|
padding: 0 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mikrotik-form {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mikrotik-item-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
709
coremain/web/static/js/app.js
Normal file
709
coremain/web/static/js/app.js
Normal file
@ -0,0 +1,709 @@
|
|||||||
|
// MosDNS 管理面板 JavaScript 应用
|
||||||
|
|
||||||
|
class MosDNSAdmin {
|
||||||
|
constructor() {
|
||||||
|
this.apiBase = '/api';
|
||||||
|
this.currentTab = 'dashboard';
|
||||||
|
this.refreshInterval = null;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.loadInitialData();
|
||||||
|
this.startAutoRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// 导航栏切换
|
||||||
|
document.querySelectorAll('.nav-item').forEach(item => {
|
||||||
|
item.addEventListener('click', (e) => {
|
||||||
|
const tab = e.target.dataset.tab;
|
||||||
|
this.switchTab(tab);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 页面可见性变化时处理自动刷新
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
this.stopAutoRefresh();
|
||||||
|
} else {
|
||||||
|
this.startAutoRefresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switchTab(tab) {
|
||||||
|
// 更新导航栏状态
|
||||||
|
document.querySelectorAll('.nav-item').forEach(item => {
|
||||||
|
item.classList.remove('active');
|
||||||
|
});
|
||||||
|
document.querySelector(`[data-tab="${tab}"]`).classList.add('active');
|
||||||
|
|
||||||
|
// 更新内容区域
|
||||||
|
document.querySelectorAll('.tab-content').forEach(content => {
|
||||||
|
content.classList.remove('active');
|
||||||
|
});
|
||||||
|
document.getElementById(tab).classList.add('active');
|
||||||
|
|
||||||
|
this.currentTab = tab;
|
||||||
|
this.loadTabData(tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadInitialData() {
|
||||||
|
try {
|
||||||
|
await this.loadServerInfo();
|
||||||
|
await this.loadTabData(this.currentTab);
|
||||||
|
} catch (error) {
|
||||||
|
this.showMessage('加载初始数据失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadServerInfo() {
|
||||||
|
try {
|
||||||
|
const response = await this.apiCall('/server/info');
|
||||||
|
if (response.success) {
|
||||||
|
const info = response.data;
|
||||||
|
document.getElementById('version').textContent = info.version || 'v5.0.0';
|
||||||
|
document.getElementById('service-status').textContent = info.status;
|
||||||
|
|
||||||
|
// 使用秒数来格式化运行时间,修复 NaN 问题
|
||||||
|
if (info.uptime_seconds !== undefined) {
|
||||||
|
document.getElementById('uptime').textContent = this.formatUptimeFromSeconds(info.uptime_seconds);
|
||||||
|
} else {
|
||||||
|
document.getElementById('uptime').textContent = info.uptime || '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示 DNS 端口
|
||||||
|
if (info.dns_ports && info.dns_ports.length > 0) {
|
||||||
|
document.getElementById('dns-ports').textContent = info.dns_ports.join(', ');
|
||||||
|
} else {
|
||||||
|
document.getElementById('dns-ports').textContent = '未检测到';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示 API 地址
|
||||||
|
if (info.api_address) {
|
||||||
|
document.getElementById('api-address').textContent = info.api_address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load server info:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatUptimeFromSeconds(seconds) {
|
||||||
|
if (!seconds || seconds < 0) {
|
||||||
|
return '0分钟';
|
||||||
|
}
|
||||||
|
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
|
||||||
|
let parts = [];
|
||||||
|
if (days > 0) parts.push(`${days}天`);
|
||||||
|
if (hours > 0) parts.push(`${hours}小时`);
|
||||||
|
if (minutes > 0 || parts.length === 0) parts.push(`${minutes}分钟`);
|
||||||
|
|
||||||
|
return parts.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTabData(tab) {
|
||||||
|
switch (tab) {
|
||||||
|
case 'dashboard':
|
||||||
|
await this.loadDashboardData();
|
||||||
|
break;
|
||||||
|
case 'mikrotik':
|
||||||
|
await this.loadMikroTikTab();
|
||||||
|
break;
|
||||||
|
case 'domains':
|
||||||
|
await this.loadDomainFiles();
|
||||||
|
break;
|
||||||
|
case 'logs':
|
||||||
|
await this.loadLogs();
|
||||||
|
break;
|
||||||
|
case 'stats':
|
||||||
|
await this.loadDetailedStats();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadDashboardData() {
|
||||||
|
try {
|
||||||
|
// 加载详细统计
|
||||||
|
const statsResponse = await this.apiCall('/stats/detailed');
|
||||||
|
if (statsResponse.success) {
|
||||||
|
const stats = statsResponse.data;
|
||||||
|
document.getElementById('total-queries').textContent = stats.totalQueries?.toLocaleString() || '-';
|
||||||
|
document.getElementById('cache-hits').textContent = stats.cacheHits?.toLocaleString() || '-';
|
||||||
|
document.getElementById('avg-response').textContent = stats.avgResponseTime ? `${stats.avgResponseTime}ms` : '-';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load dashboard data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MikroTik 标签页加载
|
||||||
|
async loadMikroTikTab() {
|
||||||
|
try {
|
||||||
|
// 直接加载 MikroTik 配置列表(不再需要加载域名文件下拉框)
|
||||||
|
await this.loadMikrotikList();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load MikroTik tab:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载 MikroTik 配置列表
|
||||||
|
async loadMikrotikList() {
|
||||||
|
const listDiv = document.getElementById('mikrotik-list');
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('开始加载 MikroTik 配置列表...');
|
||||||
|
listDiv.innerHTML = '<div class="loading">加载中...</div>';
|
||||||
|
|
||||||
|
const response = await this.apiCall('/mikrotik/list');
|
||||||
|
console.log('MikroTik API 响应:', response);
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
throw new Error('API 响应为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || '加载失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const configs = response.data || [];
|
||||||
|
|
||||||
|
if (configs.length === 0) {
|
||||||
|
listDiv.innerHTML = '<div style="text-align:center;padding:20px;color:#909399;">暂无 MikroTik 配置</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
configs.forEach(config => {
|
||||||
|
const args = config.args || {};
|
||||||
|
const domainFiles = args.domain_files || [];
|
||||||
|
const domainFilesStr = Array.isArray(domainFiles) ? domainFiles.join(', ') : domainFiles;
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="mikrotik-item">
|
||||||
|
<div class="mikrotik-item-header">
|
||||||
|
<div class="mikrotik-item-title">${this.escapeHtml(config.tag || '')}</div>
|
||||||
|
<div class="mikrotik-item-actions">
|
||||||
|
<button class="btn btn-danger" onclick="deleteMikrotikConfig('${this.escapeHtml(config.tag || '')}')">🗑️ 删除</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mikrotik-item-content">
|
||||||
|
<div class="mikrotik-item-field">
|
||||||
|
<div class="mikrotik-item-label">主机地址</div>
|
||||||
|
<div class="mikrotik-item-value">${this.escapeHtml(args.host || '-')}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mikrotik-item-field">
|
||||||
|
<div class="mikrotik-item-label">端口</div>
|
||||||
|
<div class="mikrotik-item-value">${args.port || '-'}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mikrotik-item-field">
|
||||||
|
<div class="mikrotik-item-label">用户名</div>
|
||||||
|
<div class="mikrotik-item-value">${this.escapeHtml(args.username || '-')}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mikrotik-item-field">
|
||||||
|
<div class="mikrotik-item-label">地址列表</div>
|
||||||
|
<div class="mikrotik-item-value">${this.escapeHtml(args.address_list4 || '-')}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mikrotik-item-field">
|
||||||
|
<div class="mikrotik-item-label">域名文件</div>
|
||||||
|
<div class="mikrotik-item-value">${this.escapeHtml(domainFilesStr || '-')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
listDiv.innerHTML = html;
|
||||||
|
console.log(`成功加载 ${configs.length} 个 MikroTik 配置`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载 MikroTik 列表失败:', error);
|
||||||
|
listDiv.innerHTML = `
|
||||||
|
<div style="color:#f56c6c;padding:20px;text-align:center;">
|
||||||
|
<p>❌ 加载失败</p>
|
||||||
|
<p style="font-size:13px;margin-top:8px;">${this.escapeHtml(error.message)}</p>
|
||||||
|
<button class="btn btn-secondary" onclick="app.loadMikrotikList()" style="margin-top:12px;">重试</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML 转义
|
||||||
|
escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadConfigData() {
|
||||||
|
try {
|
||||||
|
const response = await this.apiCall('/config');
|
||||||
|
if (response.success) {
|
||||||
|
document.getElementById('config-editor').value = response.data.content || '';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.showMessage('加载配置失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadDomainFiles() {
|
||||||
|
try {
|
||||||
|
const response = await this.apiCall('/domain-files');
|
||||||
|
if (response.success) {
|
||||||
|
this.renderDomainFilesList(response.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.showMessage('加载域名文件失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadLogs() {
|
||||||
|
try {
|
||||||
|
const response = await this.apiCall('/logs');
|
||||||
|
if (response.success) {
|
||||||
|
document.getElementById('logs-content').textContent = response.data.content || '暂无日志内容';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.showMessage('加载日志失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadDetailedStats() {
|
||||||
|
try {
|
||||||
|
const response = await this.apiCall('/stats/detailed');
|
||||||
|
if (response.success) {
|
||||||
|
this.renderDetailedStats(response.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.showMessage('加载统计信息失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPluginsList(plugins) {
|
||||||
|
const container = document.getElementById('plugins-list');
|
||||||
|
if (!plugins || plugins.length === 0) {
|
||||||
|
container.innerHTML = '<div class="loading">暂无插件信息</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = plugins.map(plugin => `
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">${plugin.tag}:</span>
|
||||||
|
<span class="stat-value">${plugin.status || '运行中'}</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDomainFilesList(files) {
|
||||||
|
const container = document.getElementById('domain-files-list');
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
container.innerHTML = '<div class="loading">暂无域名文件</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = files.map(file => `
|
||||||
|
<div class="domain-file-item">
|
||||||
|
<div class="domain-file-info">
|
||||||
|
<div class="domain-file-name">${file.filename}</div>
|
||||||
|
<div class="domain-file-meta">
|
||||||
|
大小: ${this.formatFileSize(file.size)} |
|
||||||
|
修改时间: ${new Date(file.modTime).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="domain-file-actions">
|
||||||
|
<button class="btn btn-secondary" onclick="app.viewDomainFile('${file.filename}')">查看</button>
|
||||||
|
<button class="btn btn-secondary" onclick="app.editDomainFile('${file.filename}')">编辑</button>
|
||||||
|
<button class="btn btn-secondary" onclick="app.deleteDomainFile('${file.filename}')">删除</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDetailedStats(stats) {
|
||||||
|
const container = document.getElementById('detailed-stats');
|
||||||
|
if (!stats) {
|
||||||
|
container.innerHTML = '<div class="loading">暂无统计信息</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">DNS 查询总数:</span>
|
||||||
|
<span class="stat-value">${stats.totalQueries?.toLocaleString() || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">成功响应:</span>
|
||||||
|
<span class="stat-value">${stats.successfulQueries?.toLocaleString() || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">失败响应:</span>
|
||||||
|
<span class="stat-value">${stats.failedQueries?.toLocaleString() || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">缓存命中:</span>
|
||||||
|
<span class="stat-value">${stats.cacheHits?.toLocaleString() || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">缓存未命中:</span>
|
||||||
|
<span class="stat-value">${stats.cacheMisses?.toLocaleString() || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">平均响应时间:</span>
|
||||||
|
<span class="stat-value">${stats.avgResponseTime ? stats.avgResponseTime + 'ms' : '-'}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
async apiCall(endpoint, options = {}) {
|
||||||
|
const url = this.apiBase + endpoint;
|
||||||
|
const defaultOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, { ...defaultOptions, ...options });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
startAutoRefresh() {
|
||||||
|
this.stopAutoRefresh();
|
||||||
|
this.refreshInterval = setInterval(() => {
|
||||||
|
if (this.currentTab === 'dashboard') {
|
||||||
|
this.loadDashboardData();
|
||||||
|
}
|
||||||
|
}, 30000); // 30秒刷新一次
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAutoRefresh() {
|
||||||
|
if (this.refreshInterval) {
|
||||||
|
clearInterval(this.refreshInterval);
|
||||||
|
this.refreshInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showMessage(message, type = 'success') {
|
||||||
|
const container = document.getElementById('message-container');
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.className = `message ${type}`;
|
||||||
|
messageEl.textContent = message;
|
||||||
|
|
||||||
|
container.appendChild(messageEl);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
messageEl.remove();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatUptime(seconds) {
|
||||||
|
if (!seconds) return '-';
|
||||||
|
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
|
||||||
|
if (days > 0) return `${days}天 ${hours}小时 ${minutes}分钟`;
|
||||||
|
if (hours > 0) return `${hours}小时 ${minutes}分钟`;
|
||||||
|
return `${minutes}分钟`;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatFileSize(bytes) {
|
||||||
|
if (!bytes) return '0 B';
|
||||||
|
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 域名文件操作方法
|
||||||
|
async viewDomainFile(filename) {
|
||||||
|
try {
|
||||||
|
const response = await this.apiCall(`/domain-files/${filename}`);
|
||||||
|
if (response.success) {
|
||||||
|
alert(`文件内容:\n\n${response.data.content}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.showMessage('查看文件失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async editDomainFile(filename) {
|
||||||
|
this.showMessage('编辑功能正在开发中', 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteDomainFile(filename) {
|
||||||
|
if (!confirm(`确定要删除文件 ${filename} 吗?`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.apiCall(`/domain-files/${filename}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
if (response.success) {
|
||||||
|
this.showMessage('文件删除成功');
|
||||||
|
this.loadDomainFiles();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.showMessage('删除文件失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局函数,供 HTML 中的按钮调用
|
||||||
|
async function reloadConfig() {
|
||||||
|
try {
|
||||||
|
const response = await app.apiCall('/config/reload', { method: 'POST' });
|
||||||
|
if (response.success) {
|
||||||
|
app.showMessage('配置重载成功');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
app.showMessage('配置重载失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function flushCache() {
|
||||||
|
try {
|
||||||
|
const response = await app.apiCall('/cache/flush', { method: 'POST' });
|
||||||
|
if (response.success) {
|
||||||
|
app.showMessage('缓存清空成功');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
app.showMessage('缓存清空失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshStats() {
|
||||||
|
app.loadDashboardData();
|
||||||
|
app.showMessage('统计信息已刷新');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restartService() {
|
||||||
|
if (!confirm('确定要重启服务吗?服务将在 3 秒后重启。')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await app.apiCall('/system/restart', { method: 'POST' });
|
||||||
|
if (response.success) {
|
||||||
|
app.showMessage('重启请求已发送,服务将在 3 秒后重启', 'success');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
app.showMessage('重启失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MikroTik 管理函数
|
||||||
|
async function saveMikrotikConfig() {
|
||||||
|
const tag = document.getElementById('mikrotik-tag').value.trim();
|
||||||
|
const host = document.getElementById('mikrotik-host').value.trim();
|
||||||
|
const port = document.getElementById('mikrotik-port').value.trim();
|
||||||
|
const username = document.getElementById('mikrotik-username').value.trim();
|
||||||
|
const password = document.getElementById('mikrotik-password').value;
|
||||||
|
const addresslist = document.getElementById('mikrotik-addresslist').value.trim();
|
||||||
|
const domainFilePath = document.getElementById('mikrotik-domains').value.trim();
|
||||||
|
|
||||||
|
// 验证必填字段
|
||||||
|
if (!tag) {
|
||||||
|
app.showMessage('请填写配置标签', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!host) {
|
||||||
|
app.showMessage('请填写 MikroTik 地址', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!username) {
|
||||||
|
app.showMessage('请填写用户名', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!password) {
|
||||||
|
app.showMessage('请填写密码', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!addresslist) {
|
||||||
|
app.showMessage('请填写地址列表名', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!domainFilePath) {
|
||||||
|
app.showMessage('请填写域名文件路径', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建配置对象
|
||||||
|
const config = {
|
||||||
|
tag: tag,
|
||||||
|
type: 'mikrotik_addresslist',
|
||||||
|
args: {
|
||||||
|
domain_files: [domainFilePath], // 使用用户输入的完整路径
|
||||||
|
host: host,
|
||||||
|
port: parseInt(port) || 9728,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
use_tls: false,
|
||||||
|
timeout: 3,
|
||||||
|
address_list4: addresslist,
|
||||||
|
mask4: 24,
|
||||||
|
comment: `${addresslist}-AutoAdd`,
|
||||||
|
timeout_addr: 43200,
|
||||||
|
cache_ttl: 3600,
|
||||||
|
verify_add: false,
|
||||||
|
add_all_ips: true,
|
||||||
|
max_ips: 50
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await app.apiCall('/mikrotik/add', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(config)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
app.showMessage(response.message || 'MikroTik 配置已保存', 'success');
|
||||||
|
// 清空表单
|
||||||
|
clearMikrotikForm();
|
||||||
|
// 刷新列表
|
||||||
|
await app.loadMikrotikList();
|
||||||
|
} else {
|
||||||
|
app.showMessage(response.message || '保存失败', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
app.showMessage('保存失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteMikrotikConfig(tag) {
|
||||||
|
if (!confirm(`确定要删除 MikroTik 配置 "${tag}" 吗?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await app.apiCall(`/mikrotik/${encodeURIComponent(tag)}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
app.showMessage(response.message || '配置已删除', 'success');
|
||||||
|
// 刷新列表
|
||||||
|
await app.loadMikrotikList();
|
||||||
|
} else {
|
||||||
|
app.showMessage(response.message || '删除失败', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
app.showMessage('删除失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMikrotikForm() {
|
||||||
|
document.getElementById('mikrotik-tag').value = '';
|
||||||
|
document.getElementById('mikrotik-host').value = '';
|
||||||
|
document.getElementById('mikrotik-port').value = '9728';
|
||||||
|
document.getElementById('mikrotik-username').value = 'admin';
|
||||||
|
document.getElementById('mikrotik-password').value = '';
|
||||||
|
document.getElementById('mikrotik-addresslist').value = '';
|
||||||
|
document.getElementById('mikrotik-domains').value = '';
|
||||||
|
app.showMessage('表单已清空', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新加载 MikroTik 列表
|
||||||
|
async function loadMikrotikList() {
|
||||||
|
await app.loadMikrotikList();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConfig() {
|
||||||
|
const content = document.getElementById('config-editor').value;
|
||||||
|
try {
|
||||||
|
const response = await app.apiCall('/config', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ content })
|
||||||
|
});
|
||||||
|
if (response.success) {
|
||||||
|
app.showMessage('配置保存成功');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
app.showMessage('配置保存失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateConfig() {
|
||||||
|
const content = document.getElementById('config-editor').value;
|
||||||
|
try {
|
||||||
|
const response = await app.apiCall('/config/validate', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ content })
|
||||||
|
});
|
||||||
|
if (response.success) {
|
||||||
|
app.showMessage('配置验证通过');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
app.showMessage('配置验证失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function backupConfig() {
|
||||||
|
try {
|
||||||
|
const response = await app.apiCall('/config/backup', { method: 'POST' });
|
||||||
|
if (response.success) {
|
||||||
|
app.showMessage('配置备份成功');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
app.showMessage('配置备份失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addDomainFile() {
|
||||||
|
app.showMessage('添加文件功能正在开发中', 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshDomainFiles() {
|
||||||
|
app.loadDomainFiles();
|
||||||
|
app.showMessage('域名文件列表已刷新');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearLogs() {
|
||||||
|
if (!confirm('确定要清空日志吗?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await app.apiCall('/logs/clear', { method: 'POST' });
|
||||||
|
if (response.success) {
|
||||||
|
document.getElementById('logs-content').textContent = '';
|
||||||
|
app.showMessage('日志清空成功');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
app.showMessage('日志清空失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshLogs() {
|
||||||
|
app.loadLogs();
|
||||||
|
app.showMessage('日志已刷新');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportStats() {
|
||||||
|
app.showMessage('导出功能正在开发中', 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshDetailedStats() {
|
||||||
|
app.loadDetailedStats();
|
||||||
|
app.showMessage('统计信息已刷新');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化应用
|
||||||
|
let app;
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
app = new MosDNSAdmin();
|
||||||
|
});
|
||||||
274
coremain/web/templates/index.html
Normal file
274
coremain/web/templates/index.html
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>MosDNS 管理面板</title>
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌐</text></svg>">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- 头部 -->
|
||||||
|
<header class="header">
|
||||||
|
<div class="header-content">
|
||||||
|
<h1 class="logo">🌐 MosDNS 管理面板</h1>
|
||||||
|
<div class="header-info">
|
||||||
|
<span class="status-indicator online" id="status">在线</span>
|
||||||
|
<span class="version" id="version">v1.0.0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 导航栏 -->
|
||||||
|
<nav class="nav">
|
||||||
|
<div class="nav-content">
|
||||||
|
<button class="nav-item active" data-tab="dashboard">
|
||||||
|
📊 仪表板
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" data-tab="mikrotik">
|
||||||
|
🔧 MikroTik 管理
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" data-tab="domains">
|
||||||
|
📝 域名文件
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" data-tab="logs">
|
||||||
|
📋 日志查看
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" data-tab="stats">
|
||||||
|
📈 统计信息
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
|
<main class="main">
|
||||||
|
<!-- 仪表板 -->
|
||||||
|
<div class="tab-content active" id="dashboard">
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>服务状态</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">运行状态:</span>
|
||||||
|
<span class="stat-value" id="service-status">运行中</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">运行时间:</span>
|
||||||
|
<span class="stat-value" id="uptime">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">DNS 端口:</span>
|
||||||
|
<span class="stat-value" id="dns-ports">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">API 地址:</span>
|
||||||
|
<span class="stat-value" id="api-address">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>查询统计</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">总查询数:</span>
|
||||||
|
<span class="stat-value" id="total-queries">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">缓存命中:</span>
|
||||||
|
<span class="stat-value" id="cache-hits">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">平均响应时间:</span>
|
||||||
|
<span class="stat-value" id="avg-response">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>快速操作</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn btn-primary" onclick="flushCache()">清空缓存</button>
|
||||||
|
<button class="btn btn-secondary" onclick="refreshStats()">刷新统计</button>
|
||||||
|
<button class="btn btn-secondary" onclick="restartService()">重启服务</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MikroTik 管理 -->
|
||||||
|
<div class="tab-content" id="mikrotik">
|
||||||
|
<!-- 已添加的配置列表(移到前面) -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>📋 已添加的 MikroTik 配置</h3>
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="btn btn-secondary" onclick="loadMikrotikList()">
|
||||||
|
<span>🔄</span>
|
||||||
|
<span>刷新</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div id="mikrotik-list" class="mikrotik-list">
|
||||||
|
<div class="loading">加载中...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加新配置表单 -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>➕ 添加 MikroTik 配置</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="mikrotik-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mikrotik-tag">
|
||||||
|
<span class="label-required">*</span>
|
||||||
|
<span>配置标签</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="mikrotik-tag" placeholder="例如: mikrotik_openai" class="form-control">
|
||||||
|
<small class="form-hint">唯一标识,建议使用 mikrotik_ 前缀</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mikrotik-addresslist">
|
||||||
|
<span class="label-required">*</span>
|
||||||
|
<span>地址列表名</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="mikrotik-addresslist" placeholder="例如: OpenAI" class="form-control">
|
||||||
|
<small class="form-hint">MikroTik 中的地址列表名称</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mikrotik-host">
|
||||||
|
<span class="label-required">*</span>
|
||||||
|
<span>MikroTik 地址</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="mikrotik-host" placeholder="例如: 10.248.0.1" class="form-control">
|
||||||
|
<small class="form-hint">MikroTik 设备的 IP 地址</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mikrotik-port">
|
||||||
|
<span>API 端口</span>
|
||||||
|
</label>
|
||||||
|
<input type="number" id="mikrotik-port" value="9728" class="form-control">
|
||||||
|
<small class="form-hint">默认 API 端口为 9728</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mikrotik-username">
|
||||||
|
<span class="label-required">*</span>
|
||||||
|
<span>用户名</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="mikrotik-username" placeholder="admin" value="admin" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mikrotik-password">
|
||||||
|
<span class="label-required">*</span>
|
||||||
|
<span>密码</span>
|
||||||
|
</label>
|
||||||
|
<input type="password" id="mikrotik-password" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mikrotik-domains">
|
||||||
|
<span class="label-required">*</span>
|
||||||
|
<span>域名文件路径</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="mikrotik-domains" placeholder="例如: /usr/local/yltx-dns/config/openai.txt 或 ./mikrotik/openai.txt" class="form-control">
|
||||||
|
<small class="form-hint">支持绝对路径和相对路径(相对于运行目录)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary btn-lg" onclick="saveMikrotikConfig()">
|
||||||
|
<span>💾</span>
|
||||||
|
<span>保存配置</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary btn-lg" onclick="clearMikrotikForm()">
|
||||||
|
<span>🔄</span>
|
||||||
|
<span>清空表单</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 域名文件 -->
|
||||||
|
<div class="tab-content" id="domains">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>域名文件管理</h3>
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="btn btn-primary" onclick="addDomainFile()">添加文件</button>
|
||||||
|
<button class="btn btn-secondary" onclick="refreshDomainFiles()">刷新列表</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div id="domain-files-list">
|
||||||
|
<div class="loading">加载中...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日志查看 -->
|
||||||
|
<div class="tab-content" id="logs">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>系统日志</h3>
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="btn btn-secondary" onclick="clearLogs()">清空日志</button>
|
||||||
|
<button class="btn btn-secondary" onclick="refreshLogs()">刷新日志</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="logs-container">
|
||||||
|
<pre id="logs-content">日志内容将在这里显示...</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计信息 -->
|
||||||
|
<div class="tab-content" id="stats">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>详细统计</h3>
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="btn btn-secondary" onclick="exportStats()">导出统计</button>
|
||||||
|
<button class="btn btn-secondary" onclick="refreshDetailedStats()">刷新统计</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div id="detailed-stats">
|
||||||
|
<div class="loading">加载中...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 消息提示 -->
|
||||||
|
<div id="message-container"></div>
|
||||||
|
|
||||||
|
<script src="/static/js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
249
coremain/web_ui.go
Normal file
249
coremain/web_ui.go
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020-2022, IrineSistiana
|
||||||
|
*
|
||||||
|
* This file is part of mosdns.
|
||||||
|
*
|
||||||
|
* mosdns is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* mosdns is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package coremain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var webUIFS fs.FS
|
||||||
|
|
||||||
|
// SetWebUIFS 设置 Web UI 文件系统(由 main 包调用)
|
||||||
|
func SetWebUIFS(fsys fs.FS) {
|
||||||
|
webUIFS = fsys
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerWebUI 注册 Web 管理界面路由(Vue SPA)
|
||||||
|
func (m *Mosdns) registerWebUI() {
|
||||||
|
// 获取 Vue 构建产物
|
||||||
|
distFS, err := fs.Sub(webUIFS, "web-ui/dist")
|
||||||
|
if err != nil {
|
||||||
|
m.logger.Error("failed to get Vue dist files", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接处理所有请求
|
||||||
|
m.webMux.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
path := r.URL.Path
|
||||||
|
m.logger.Debug("Web UI request", zap.String("path", path))
|
||||||
|
|
||||||
|
// API 请求交给 API 路由处理(不应该到这里,但作为保护)
|
||||||
|
if strings.HasPrefix(path, "/api/") {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试打开文件
|
||||||
|
trimmedPath := strings.TrimPrefix(path, "/")
|
||||||
|
if trimmedPath == "" {
|
||||||
|
trimmedPath = "index.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
m.logger.Debug("Trying to open file", zap.String("trimmedPath", trimmedPath))
|
||||||
|
file, err := distFS.Open(trimmedPath)
|
||||||
|
if err == nil {
|
||||||
|
// 文件存在,直接返回
|
||||||
|
defer file.Close()
|
||||||
|
stat, _ := file.Stat()
|
||||||
|
if !stat.IsDir() {
|
||||||
|
m.logger.Debug("Serving file", zap.String("path", trimmedPath))
|
||||||
|
http.ServeContent(w, r, path, stat.ModTime(), file.(io.ReadSeeker))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m.logger.Debug("File not found", zap.String("path", trimmedPath), zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件不存在或是目录,返回 index.html (SPA 路由)
|
||||||
|
m.logger.Debug("Serving index.html for SPA")
|
||||||
|
m.serveVueIndex(w, r, distFS)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveVueIndex 提供 Vue SPA 的 index.html
|
||||||
|
func (m *Mosdns) serveVueIndex(w http.ResponseWriter, r *http.Request, distFS fs.FS) {
|
||||||
|
// 读取 index.html
|
||||||
|
indexHTML, err := fs.ReadFile(distFS, "index.html")
|
||||||
|
if err != nil {
|
||||||
|
m.logger.Error("failed to read Vue index.html", zap.Error(err))
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
w.Header().Set("Pragma", "no-cache")
|
||||||
|
w.Header().Set("Expires", "0")
|
||||||
|
|
||||||
|
// 返回 HTML 内容
|
||||||
|
w.Write(indexHTML)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerWebAPI 注册 Web 管理 API 路由(使用独立的 webMux)
|
||||||
|
func (m *Mosdns) registerWebAPI() {
|
||||||
|
// Web API 路由组(在 Web UI 服务器上提供 API 代理)
|
||||||
|
m.webMux.Route("/api", func(r chi.Router) {
|
||||||
|
// 服务器信息和状态
|
||||||
|
r.Get("/server/info", m.handleWebServerInfo)
|
||||||
|
r.Get("/server/status", m.handleWebServerStatus)
|
||||||
|
|
||||||
|
// 配置管理
|
||||||
|
r.Get("/config", m.handleWebGetConfig)
|
||||||
|
r.Put("/config", m.handleWebUpdateConfig)
|
||||||
|
r.Post("/config/reload", m.handleWebReloadConfig)
|
||||||
|
r.Post("/config/validate", m.handleWebValidateConfig)
|
||||||
|
r.Post("/config/backup", m.handleWebBackupConfig)
|
||||||
|
|
||||||
|
// 域名文件管理
|
||||||
|
r.Get("/domain-files", m.handleWebListDomainFiles)
|
||||||
|
r.Get("/domain-files/{filename}", m.handleWebGetDomainFile)
|
||||||
|
r.Put("/domain-files/{filename}", m.handleWebUpdateDomainFile)
|
||||||
|
r.Delete("/domain-files/{filename}", m.handleWebDeleteDomainFile)
|
||||||
|
|
||||||
|
// 插件管理
|
||||||
|
r.Get("/plugins", m.handleWebListPlugins)
|
||||||
|
r.Get("/plugins/{tag}/status", m.handleWebPluginStatus)
|
||||||
|
|
||||||
|
// 统计信息
|
||||||
|
r.Get("/stats/detailed", m.handleWebDetailedStats)
|
||||||
|
|
||||||
|
// 日志管理
|
||||||
|
r.Get("/logs", m.handleWebGetLogs)
|
||||||
|
r.Post("/logs/clear", m.handleWebClearLogs)
|
||||||
|
|
||||||
|
// 缓存管理
|
||||||
|
r.Post("/cache/flush", m.handleWebFlushCache)
|
||||||
|
|
||||||
|
// MikroTik 管理
|
||||||
|
r.Get("/mikrotik/list", m.handleListMikroTik)
|
||||||
|
r.Post("/mikrotik/add", m.handleAddMikroTik)
|
||||||
|
r.Delete("/mikrotik/{tag}", m.handleDeleteMikroTik)
|
||||||
|
|
||||||
|
// 系统操作
|
||||||
|
r.Post("/system/restart", m.handleSystemRestart)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web API 处理函数的简化版本,复用现有的管理 API 逻辑
|
||||||
|
func (m *Mosdns) handleWebServerInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m.handleServerInfo(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mosdns) handleWebServerStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m.handleServerStatus(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mosdns) handleWebGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m.handleGetConfig(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mosdns) handleWebUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m.handleUpdateConfig(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mosdns) handleWebReloadConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m.handleReloadConfig(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mosdns) handleWebValidateConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m.handleValidateConfig(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mosdns) handleWebBackupConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m.handleBackupConfig(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mosdns) handleWebListDomainFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m.handleListDomainFiles(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mosdns) handleWebGetDomainFile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m.handleGetDomainFile(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mosdns) handleWebUpdateDomainFile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m.handleUpdateDomainFile(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mosdns) handleWebDeleteDomainFile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m.handleDeleteDomainFile(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mosdns) handleWebListPlugins(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m.handleListPlugins(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mosdns) handleWebPluginStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m.handlePluginStatus(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mosdns) handleWebDetailedStats(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 返回详细统计信息
|
||||||
|
stats := map[string]interface{}{
|
||||||
|
"totalQueries": 0,
|
||||||
|
"successfulQueries": 0,
|
||||||
|
"failedQueries": 0,
|
||||||
|
"cacheHits": 0,
|
||||||
|
"cacheMisses": 0,
|
||||||
|
"avgResponseTime": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.writeJSONResponse(w, APIResponse{
|
||||||
|
Success: true,
|
||||||
|
Data: stats,
|
||||||
|
Message: "统计信息获取成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mosdns) handleWebGetLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 返回日志内容(这里可以根据实际需求实现)
|
||||||
|
logs := map[string]interface{}{
|
||||||
|
"content": "MosDNS 日志内容将在这里显示...\n[INFO] MosDNS 服务已启动\n[INFO] 配置文件加载成功\n[INFO] 所有插件已加载完成",
|
||||||
|
}
|
||||||
|
|
||||||
|
m.writeJSONResponse(w, APIResponse{
|
||||||
|
Success: true,
|
||||||
|
Data: logs,
|
||||||
|
Message: "日志获取成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mosdns) handleWebClearLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 清空日志(这里可以根据实际需求实现)
|
||||||
|
m.writeJSONResponse(w, APIResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "日志清空成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mosdns) handleWebFlushCache(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 清空缓存(这里可以根据实际需求实现)
|
||||||
|
m.writeJSONResponse(w, APIResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "缓存清空成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,192 +0,0 @@
|
|||||||
# MosDNS + MikroTik Amazon 域名处理部署指南(更新版)
|
|
||||||
|
|
||||||
## 功能说明
|
|
||||||
|
|
||||||
这个配置会在解析 Amazon 相关域名时,自动将解析到的 IP 地址添加到 MikroTik 路由器的 address list 中,用于防火墙规则控制。
|
|
||||||
|
|
||||||
## 部署步骤
|
|
||||||
|
|
||||||
### 1. 上传文件到 Debian 12 服务器
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 上传编译好的 mosdns 可执行文件
|
|
||||||
scp mosdns-linux-amd64 user@your-server:/usr/local/bin/mosdns
|
|
||||||
|
|
||||||
# 上传配置文件
|
|
||||||
scp config.yaml user@your-server:/opt/mosdns/
|
|
||||||
scp dns.yaml user@your-server:/opt/mosdns/
|
|
||||||
|
|
||||||
# 设置执行权限
|
|
||||||
ssh user@your-server "chmod +x /usr/local/bin/mosdns"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 创建必要的目录和文件
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 创建配置目录
|
|
||||||
sudo mkdir -p /opt/mosdns/config
|
|
||||||
|
|
||||||
# 下载 Amazon 域名列表
|
|
||||||
sudo wget -O /opt/mosdns/config/geosite_amazon.txt https://raw.githubusercontent.com/v2fly/domain-list-community/master/data/amazon
|
|
||||||
sudo wget -O /opt/mosdns/config/geosite_amazon-ads.txt https://raw.githubusercontent.com/v2fly/domain-list-community/master/data/amazon-ads
|
|
||||||
sudo wget -O /opt/mosdns/config/geosite_amazontrust.txt https://raw.githubusercontent.com/v2fly/domain-list-community/master/data/amazontrust
|
|
||||||
sudo wget -O /opt/mosdns/config/amazon.txt https://raw.githubusercontent.com/v2fly/domain-list-community/master/data/amazon
|
|
||||||
|
|
||||||
# 下载其他必要的域名和 IP 文件
|
|
||||||
sudo wget -O /opt/mosdns/config/geosite_tiktok.txt https://raw.githubusercontent.com/v2fly/domain-list-community/master/data/tiktok
|
|
||||||
sudo wget -O /opt/mosdns/config/gfwlist.out.txt https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt
|
|
||||||
sudo wget -O /opt/mosdns/config/domains.txt https://raw.githubusercontent.com/v2fly/domain-list-community/master/data/category-games
|
|
||||||
sudo wget -O /opt/mosdns/config/cn.txt https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 在 MikroTik 中创建 address list
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 通过 SSH 连接到 MikroTik 路由器
|
|
||||||
ssh admin@10.248.0.1
|
|
||||||
|
|
||||||
# 创建 IPv4 和 IPv6 address list
|
|
||||||
/ip firewall address-list add list=AmazonIP
|
|
||||||
/ip firewall address-list add list=AmazonIP6
|
|
||||||
|
|
||||||
# 创建防火墙规则(可选)
|
|
||||||
/ip firewall filter add chain=forward src-address-list=AmazonIP action=drop comment="Block Amazon IPs"
|
|
||||||
/ip firewall filter add chain=forward src-address-list=AmazonIP6 action=drop comment="Block Amazon IPv6 IPs"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 修改配置文件中的 MikroTik 连接信息
|
|
||||||
|
|
||||||
编辑 `/opt/mosdns/dns.yaml` 文件,确认 mikrotik_amazon 插件的配置:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# 当前配置(根据你的实际情况修改)
|
|
||||||
args: "10.248.0.1:9728:admin:szn0s!nw@pwd():false:10:AmazonIP:AmazonIP6:24:32:AmazonIP:86400"
|
|
||||||
```
|
|
||||||
|
|
||||||
参数说明:
|
|
||||||
- `10.248.0.1`: MikroTik 路由器 IP
|
|
||||||
- `9728`: API 端口
|
|
||||||
- `admin`: 用户名
|
|
||||||
- `szn0s!nw@pwd()`: 密码
|
|
||||||
- `false`: 不使用 TLS
|
|
||||||
- `10`: 连接超时时间
|
|
||||||
- `AmazonIP`: IPv4 address list 名称
|
|
||||||
- `AmazonIP6`: IPv6 address list 名称
|
|
||||||
- `24`: IPv4 掩码
|
|
||||||
- `32`: IPv6 掩码
|
|
||||||
- `AmazonIP`: 注释
|
|
||||||
- `86400`: 地址超时时间(24小时)
|
|
||||||
|
|
||||||
### 5. 创建 systemd 服务
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo tee /etc/systemd/system/mosdns.service > /dev/null <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=MosDNS DNS Server
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=root
|
|
||||||
ExecStart=/usr/local/bin/mosdns -c /opt/mosdns/config.yaml
|
|
||||||
Restart=always
|
|
||||||
RestartSec=3
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 重新加载 systemd 配置
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
|
|
||||||
# 启用并启动服务
|
|
||||||
sudo systemctl enable mosdns
|
|
||||||
sudo systemctl start mosdns
|
|
||||||
|
|
||||||
# 检查服务状态
|
|
||||||
sudo systemctl status mosdns
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. 配置 DNS 转发
|
|
||||||
|
|
||||||
修改 `/etc/systemd/resolved.conf`:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[Resolve]
|
|
||||||
DNS=127.0.0.1:5300
|
|
||||||
#FallbackDNS=8.8.8.8 8.8.4.4
|
|
||||||
#Domains=
|
|
||||||
#DNSSEC=no
|
|
||||||
#DNSOverTLS=no
|
|
||||||
#MulticastDNS=yes
|
|
||||||
#LLMNR=yes
|
|
||||||
#Cache=yes
|
|
||||||
#DNSStubListener=no
|
|
||||||
```
|
|
||||||
|
|
||||||
重启 systemd-resolved:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl restart systemd-resolved
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. 测试配置
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 测试 Amazon 域名解析
|
|
||||||
nslookup amazon.com 127.0.0.1:5300
|
|
||||||
nslookup aws.amazon.com 127.0.0.1:5300
|
|
||||||
|
|
||||||
# 检查 MikroTik address list 是否更新
|
|
||||||
ssh admin@10.248.0.1 "/ip firewall address-list print where list=AmazonIP"
|
|
||||||
|
|
||||||
# 查看 mosdns 日志
|
|
||||||
sudo journalctl -u mosdns -f
|
|
||||||
```
|
|
||||||
|
|
||||||
## 配置说明
|
|
||||||
|
|
||||||
### 工作流程
|
|
||||||
|
|
||||||
1. **域名匹配**:当查询 Amazon 相关域名时,匹配 `amazon_domains` 集合
|
|
||||||
2. **DNS 解析**:使用国外 DNS 服务器解析域名
|
|
||||||
3. **IP 提取**:从 DNS 响应中提取 A 和 AAAA 记录
|
|
||||||
4. **地址添加**:通过 MikroTik API 将 IP 添加到 address list
|
|
||||||
5. **超时管理**:IP 地址会在 24 小时后自动过期
|
|
||||||
|
|
||||||
### 监控和调试
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 查看实时日志
|
|
||||||
sudo journalctl -u mosdns -f
|
|
||||||
|
|
||||||
# 查看服务状态
|
|
||||||
sudo systemctl status mosdns
|
|
||||||
|
|
||||||
# 测试 MikroTik 连接
|
|
||||||
curl -k https://10.248.0.1:9729/api/rest/ip/firewall/address-list
|
|
||||||
|
|
||||||
# 查看 API 状态
|
|
||||||
curl http://localhost:5535/metrics
|
|
||||||
```
|
|
||||||
|
|
||||||
### 故障排除
|
|
||||||
|
|
||||||
1. **连接失败**:检查 MikroTik IP、端口和认证信息
|
|
||||||
2. **权限不足**:确保 MikroTik 用户具有管理 address list 的权限
|
|
||||||
3. **域名文件缺失**:确保所有域名列表文件都已下载
|
|
||||||
4. **DNS 解析失败**:检查上游 DNS 服务器配置
|
|
||||||
|
|
||||||
## 安全注意事项
|
|
||||||
|
|
||||||
1. 不要在配置文件中使用明文密码,考虑使用环境变量
|
|
||||||
2. 限制对 MikroTik API 端口的访问
|
|
||||||
3. 定期更新域名列表文件
|
|
||||||
4. 监控 address list 大小,避免过多条目影响性能
|
|
||||||
|
|
||||||
## 更新日志
|
|
||||||
|
|
||||||
- 修复了插件注册问题,现在支持 YAML 配置和快速配置
|
|
||||||
- 更新了路径配置为 `/opt/mosdns/`
|
|
||||||
- 更新了端口配置为 `5300`
|
|
||||||
- 更新了 API 端口为 `5535`
|
|
||||||
@ -1,182 +0,0 @@
|
|||||||
# MosDNS + MikroTik Amazon 域名处理部署指南
|
|
||||||
|
|
||||||
## 功能说明
|
|
||||||
|
|
||||||
这个配置会在解析 Amazon 相关域名时,自动将解析到的 IP 地址添加到 MikroTik 路由器的 address list 中,用于防火墙规则控制。
|
|
||||||
|
|
||||||
## 部署步骤
|
|
||||||
|
|
||||||
### 1. 上传文件到 Debian 12 服务器
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 上传编译好的 mosdns 可执行文件
|
|
||||||
scp mosdns-linux-amd64 user@your-server:/usr/local/bin/mosdns
|
|
||||||
|
|
||||||
# 上传配置文件
|
|
||||||
scp config.yaml user@your-server:/usr/local/mosdns/
|
|
||||||
scp dns.yaml user@your-server:/usr/local/mosdns/
|
|
||||||
|
|
||||||
# 设置执行权限
|
|
||||||
ssh user@your-server "chmod +x /usr/local/bin/mosdns"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 创建必要的目录和文件
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 创建配置目录
|
|
||||||
sudo mkdir -p /usr/local/mosdns/config
|
|
||||||
|
|
||||||
# 下载 Amazon 域名列表
|
|
||||||
sudo wget -O /usr/local/mosdns/config/geosite_amazon.txt https://raw.githubusercontent.com/v2fly/domain-list-community/master/data/amazon
|
|
||||||
sudo wget -O /usr/local/mosdns/config/geosite_amazon-ads.txt https://raw.githubusercontent.com/v2fly/domain-list-community/master/data/amazon-ads
|
|
||||||
sudo wget -O /usr/local/mosdns/config/geosite_amazontrust.txt https://raw.githubusercontent.com/v2fly/domain-list-community/master/data/amazontrust
|
|
||||||
sudo wget -O /usr/local/mosdns/config/amazon.txt https://raw.githubusercontent.com/v2fly/domain-list-community/master/data/amazon
|
|
||||||
|
|
||||||
# 下载其他必要的域名和 IP 文件
|
|
||||||
sudo wget -O /usr/local/mosdns/config/geosite_tiktok.txt https://raw.githubusercontent.com/v2fly/domain-list-community/master/data/tiktok
|
|
||||||
sudo wget -O /usr/local/mosdns/config/gfwlist.out.txt https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt
|
|
||||||
sudo wget -O /usr/local/mosdns/config/domains.txt https://raw.githubusercontent.com/v2fly/domain-list-community/master/data/category-games
|
|
||||||
sudo wget -O /usr/local/mosdns/config/cn.txt https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 在 MikroTik 中创建 address list
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 通过 SSH 连接到 MikroTik 路由器
|
|
||||||
ssh admin@192.168.1.1
|
|
||||||
|
|
||||||
# 创建 IPv4 和 IPv6 address list
|
|
||||||
/ip firewall address-list add list=amazon_ips
|
|
||||||
/ip firewall address-list add list=amazon_ips6
|
|
||||||
|
|
||||||
# 创建防火墙规则(可选)
|
|
||||||
/ip firewall filter add chain=forward src-address-list=amazon_ips action=drop comment="Block Amazon IPs"
|
|
||||||
/ip firewall filter add chain=forward src-address-list=amazon_ips6 action=drop comment="Block Amazon IPv6 IPs"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 修改配置文件中的 MikroTik 连接信息
|
|
||||||
|
|
||||||
编辑 `/usr/local/mosdns/dns.yaml` 文件,修改 mikrotik_amazon 插件的配置:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# 修改为你的 MikroTik 实际信息
|
|
||||||
args: "192.168.1.1:8728:admin:your-password:false:10:amazon_ips:amazon_ips6:24:32:amazon_domain:86400"
|
|
||||||
```
|
|
||||||
|
|
||||||
参数说明:
|
|
||||||
- `192.168.1.1`: MikroTik 路由器 IP
|
|
||||||
- `8728`: API 端口
|
|
||||||
- `admin`: 用户名
|
|
||||||
- `your-password`: 密码
|
|
||||||
- `false`: 不使用 TLS
|
|
||||||
- `10`: 连接超时时间
|
|
||||||
- `amazon_ips`: IPv4 address list 名称
|
|
||||||
- `amazon_ips6`: IPv6 address list 名称
|
|
||||||
- `24`: IPv4 掩码
|
|
||||||
- `32`: IPv6 掩码
|
|
||||||
- `amazon_domain`: 注释
|
|
||||||
- `86400`: 地址超时时间(24小时)
|
|
||||||
|
|
||||||
### 5. 创建 systemd 服务
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo tee /etc/systemd/system/mosdns.service > /dev/null <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=MosDNS DNS Server
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=root
|
|
||||||
ExecStart=/usr/local/bin/mosdns -c /usr/local/mosdns/config.yaml
|
|
||||||
Restart=always
|
|
||||||
RestartSec=3
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 重新加载 systemd 配置
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
|
|
||||||
# 启用并启动服务
|
|
||||||
sudo systemctl enable mosdns
|
|
||||||
sudo systemctl start mosdns
|
|
||||||
|
|
||||||
# 检查服务状态
|
|
||||||
sudo systemctl status mosdns
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. 配置 DNS 转发
|
|
||||||
|
|
||||||
修改 `/etc/systemd/resolved.conf`:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[Resolve]
|
|
||||||
DNS=127.0.0.1
|
|
||||||
#FallbackDNS=8.8.8.8 8.8.4.4
|
|
||||||
#Domains=
|
|
||||||
#DNSSEC=no
|
|
||||||
#DNSOverTLS=no
|
|
||||||
#MulticastDNS=yes
|
|
||||||
#LLMNR=yes
|
|
||||||
#Cache=yes
|
|
||||||
#DNSStubListener=no
|
|
||||||
```
|
|
||||||
|
|
||||||
重启 systemd-resolved:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl restart systemd-resolved
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. 测试配置
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 测试 Amazon 域名解析
|
|
||||||
nslookup amazon.com 127.0.0.1
|
|
||||||
nslookup aws.amazon.com 127.0.0.1
|
|
||||||
|
|
||||||
# 检查 MikroTik address list 是否更新
|
|
||||||
ssh admin@192.168.1.1 "/ip firewall address-list print where list=amazon_ips"
|
|
||||||
|
|
||||||
# 查看 mosdns 日志
|
|
||||||
sudo journalctl -u mosdns -f
|
|
||||||
```
|
|
||||||
|
|
||||||
## 配置说明
|
|
||||||
|
|
||||||
### 工作流程
|
|
||||||
|
|
||||||
1. **域名匹配**:当查询 Amazon 相关域名时,匹配 `amazon_domains` 集合
|
|
||||||
2. **DNS 解析**:使用国外 DNS 服务器解析域名
|
|
||||||
3. **IP 提取**:从 DNS 响应中提取 A 和 AAAA 记录
|
|
||||||
4. **地址添加**:通过 MikroTik API 将 IP 添加到 address list
|
|
||||||
5. **超时管理**:IP 地址会在 24 小时后自动过期
|
|
||||||
|
|
||||||
### 监控和调试
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 查看实时日志
|
|
||||||
sudo journalctl -u mosdns -f
|
|
||||||
|
|
||||||
# 查看服务状态
|
|
||||||
sudo systemctl status mosdns
|
|
||||||
|
|
||||||
# 测试 MikroTik 连接
|
|
||||||
curl -k https://192.168.1.1:8729/api/rest/ip/firewall/address-list
|
|
||||||
```
|
|
||||||
|
|
||||||
### 故障排除
|
|
||||||
|
|
||||||
1. **连接失败**:检查 MikroTik IP、端口和认证信息
|
|
||||||
2. **权限不足**:确保 MikroTik 用户具有管理 address list 的权限
|
|
||||||
3. **域名文件缺失**:确保所有域名列表文件都已下载
|
|
||||||
4. **DNS 解析失败**:检查上游 DNS 服务器配置
|
|
||||||
|
|
||||||
## 安全注意事项
|
|
||||||
|
|
||||||
1. 不要在配置文件中使用明文密码,考虑使用环境变量
|
|
||||||
2. 限制对 MikroTik API 端口的访问
|
|
||||||
3. 定期更新域名列表文件
|
|
||||||
4. 监控 address list 大小,避免过多条目影响性能
|
|
||||||
27
dev-vue.bat
Normal file
27
dev-vue.bat
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
echo.
|
||||||
|
echo ====================================
|
||||||
|
echo MosDNS Vue 开发模式
|
||||||
|
echo ====================================
|
||||||
|
echo.
|
||||||
|
echo 📌 提示:
|
||||||
|
echo - 终端 1: 运行此脚本(Vue 开发服务器)
|
||||||
|
echo - 终端 2: 运行 Go 后端
|
||||||
|
echo go run main.go start -c config.yaml
|
||||||
|
echo.
|
||||||
|
echo 然后访问: http://localhost:5173
|
||||||
|
echo.
|
||||||
|
echo ====================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
cd web-ui
|
||||||
|
|
||||||
|
if not exist "node_modules\" (
|
||||||
|
echo 📦 首次运行,正在安装依赖...
|
||||||
|
call npm install
|
||||||
|
)
|
||||||
|
|
||||||
|
echo 🚀 启动 Vue 开发服务器...
|
||||||
|
npm run dev
|
||||||
|
|
||||||
BIN
dist/mosdns-linux-amd64
vendored
Normal file
BIN
dist/mosdns-linux-amd64
vendored
Normal file
Binary file not shown.
@ -1,55 +0,0 @@
|
|||||||
################ DNS Plugins #################
|
|
||||||
plugins:
|
|
||||||
|
|
||||||
- tag: mikrotik-one
|
|
||||||
type: forward
|
|
||||||
args:
|
|
||||||
concurrent: 1
|
|
||||||
upstreams:
|
|
||||||
- addr: "udp://10.248.0.1"
|
|
||||||
|
|
||||||
- tag: cn-dns
|
|
||||||
type: forward
|
|
||||||
args:
|
|
||||||
concurrent: 6
|
|
||||||
upstreams:
|
|
||||||
- addr: "udp://202.96.128.86"
|
|
||||||
- addr: "udp://202.96.128.166"
|
|
||||||
- addr: "udp://119.29.29.29"
|
|
||||||
- addr: "udp://223.5.5.5"
|
|
||||||
- addr: "udp://114.114.114.114"
|
|
||||||
- addr: "udp://180.76.76.76"
|
|
||||||
|
|
||||||
- tag: jp-dns
|
|
||||||
type: forward
|
|
||||||
args:
|
|
||||||
concurrent: 4
|
|
||||||
upstreams:
|
|
||||||
- addr: "tls://1dot1dot1dot1.cloudflare-dns.com"
|
|
||||||
dial_addr: "1.1.1.1"
|
|
||||||
enable_pipeline: true
|
|
||||||
- addr: "tls://1dot1dot1dot1.cloudflare-dns.com"
|
|
||||||
dial_addr: "1.0.0.1"
|
|
||||||
enable_pipeline: true
|
|
||||||
- addr: "tls://dns.google"
|
|
||||||
dial_addr: "8.8.8.8"
|
|
||||||
enable_pipeline: true
|
|
||||||
- addr: "tls://dns.google"
|
|
||||||
dial_addr: "8.8.4.4"
|
|
||||||
enable_pipeline: true
|
|
||||||
|
|
||||||
# MikroTik Address List 插件 - 处理 Amazon 相关域名
|
|
||||||
# 示例:将地址列表改为 gfw
|
|
||||||
- tag: mikrotik_amazon
|
|
||||||
type: mikrotik_addresslist
|
|
||||||
args:
|
|
||||||
host: "10.248.0.1"
|
|
||||||
port: 9728
|
|
||||||
username: "admin"
|
|
||||||
password: "szn0s!nw@pwd()"
|
|
||||||
use_tls: false
|
|
||||||
timeout: 10
|
|
||||||
address_list4: "gfw" # 改为 gfw,插件会自动创建这个地址列表
|
|
||||||
mask4: 24
|
|
||||||
comment: "amazon_domain"
|
|
||||||
timeout_addr: 86400
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
################ DNS Plugins - 内存缓存优化版 #################
|
|
||||||
# 🚀 核心优化:
|
|
||||||
# 1. 程序启动时从MikroTik加载现有IP到内存
|
|
||||||
# 2. 完全移除验证功能
|
|
||||||
# 3. 内存判断IP存在性,避免重复写入
|
|
||||||
# 4. 使用/24网段掩码减少条目数量
|
|
||||||
|
|
||||||
plugins:
|
|
||||||
|
|
||||||
- tag: mikrotik-one
|
|
||||||
type: forward
|
|
||||||
args:
|
|
||||||
concurrent: 1
|
|
||||||
upstreams:
|
|
||||||
- addr: "udp://10.248.0.1"
|
|
||||||
|
|
||||||
- tag: cn-dns
|
|
||||||
type: forward
|
|
||||||
args:
|
|
||||||
concurrent: 6
|
|
||||||
upstreams:
|
|
||||||
- addr: "udp://202.96.128.86"
|
|
||||||
- addr: "udp://202.96.128.166"
|
|
||||||
- addr: "udp://119.29.29.29"
|
|
||||||
- addr: "udp://223.5.5.5"
|
|
||||||
- addr: "udp://114.114.114.114"
|
|
||||||
- addr: "udp://180.76.76.76"
|
|
||||||
|
|
||||||
- tag: jp-dns
|
|
||||||
type: forward
|
|
||||||
args:
|
|
||||||
concurrent: 4
|
|
||||||
upstreams:
|
|
||||||
- addr: "tls://1dot1dot1dot1.cloudflare-dns.com"
|
|
||||||
dial_addr: "1.1.1.1"
|
|
||||||
enable_pipeline: true
|
|
||||||
- addr: "tls://1dot1dot1dot1.cloudflare-dns.com"
|
|
||||||
dial_addr: "1.0.0.1"
|
|
||||||
enable_pipeline: true
|
|
||||||
- addr: "tls://dns.google"
|
|
||||||
dial_addr: "8.8.8.8"
|
|
||||||
enable_pipeline: true
|
|
||||||
- addr: "tls://dns.google"
|
|
||||||
dial_addr: "8.8.4.4"
|
|
||||||
enable_pipeline: true
|
|
||||||
|
|
||||||
# 🚀 MikroTik Address List 插件 - 内存缓存优化配置
|
|
||||||
- tag: mikrotik_amazon
|
|
||||||
type: mikrotik_addresslist
|
|
||||||
args:
|
|
||||||
host: "10.248.0.1"
|
|
||||||
port: 9728
|
|
||||||
username: "admin"
|
|
||||||
password: "szn0s!nw@pwd()"
|
|
||||||
use_tls: false
|
|
||||||
timeout: 3 # 🚀 快速连接超时
|
|
||||||
|
|
||||||
# 地址列表配置
|
|
||||||
address_list4: "gfw" # IPv4地址列表名
|
|
||||||
address_list6: "gfw6" # IPv6地址列表名(可选)
|
|
||||||
|
|
||||||
# 🚀 核心优化:网段掩码配置
|
|
||||||
mask4: 24 # 使用/24网段,减少条目数量
|
|
||||||
mask6: 64 # IPv6使用/64网段
|
|
||||||
|
|
||||||
# 超时和缓存配置
|
|
||||||
comment: "auto-amazon" # 自动添加的注释
|
|
||||||
timeout_addr: 43200 # 12小时地址超时
|
|
||||||
cache_ttl: 7200 # 2小时内存缓存TTL
|
|
||||||
|
|
||||||
# 🚀 性能优化开关
|
|
||||||
verify_add: false # 🔥 完全关闭验证功能
|
|
||||||
add_all_ips: true # 启用多IP支持
|
|
||||||
max_ips: 15 # 每个域名最多15个IP
|
|
||||||
|
|
||||||
# 🚀 新增:内存缓存优化参数
|
|
||||||
preload_existing: true # 启动时预加载现有IP
|
|
||||||
memory_cache_size: 10000 # 内存缓存最大条目数
|
|
||||||
subnet_cache_ttl: 14400 # 网段缓存4小时TTL
|
|
||||||
|
|
||||||
# 工作线程优化
|
|
||||||
worker_pool_size: 20 # 增加工作线程池
|
|
||||||
batch_size: 25 # 增加批处理大小
|
|
||||||
|
|
||||||
# 连接优化
|
|
||||||
max_retries: 2 # 最大重试次数
|
|
||||||
retry_backoff_ms: 100 # 重试退避时间(毫秒)
|
|
||||||
connection_pool_size: 3 # 连接池大小
|
|
||||||
|
|
||||||
# 🚀 启动行为配置
|
|
||||||
startup_load_timeout: 30 # 启动加载超时时间(秒)
|
|
||||||
log_cache_stats: true # 记录缓存统计信息
|
|
||||||
cleanup_interval: 3600 # 缓存清理间隔(秒)
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
################ DNS Plugins - 性能优化版 #################
|
|
||||||
plugins:
|
|
||||||
|
|
||||||
- tag: mikrotik-one
|
|
||||||
type: forward
|
|
||||||
args:
|
|
||||||
concurrent: 1
|
|
||||||
upstreams:
|
|
||||||
- addr: "udp://10.248.0.1"
|
|
||||||
|
|
||||||
- tag: cn-dns
|
|
||||||
type: forward
|
|
||||||
args:
|
|
||||||
concurrent: 6
|
|
||||||
upstreams:
|
|
||||||
- addr: "udp://202.96.128.86"
|
|
||||||
- addr: "udp://202.96.128.166"
|
|
||||||
- addr: "udp://119.29.29.29"
|
|
||||||
- addr: "udp://223.5.5.5"
|
|
||||||
- addr: "udp://114.114.114.114"
|
|
||||||
- addr: "udp://180.76.76.76"
|
|
||||||
|
|
||||||
- tag: jp-dns
|
|
||||||
type: forward
|
|
||||||
args:
|
|
||||||
concurrent: 4
|
|
||||||
upstreams:
|
|
||||||
- addr: "tls://1dot1dot1dot1.cloudflare-dns.com"
|
|
||||||
dial_addr: "1.1.1.1"
|
|
||||||
enable_pipeline: true
|
|
||||||
- addr: "tls://1dot1dot1dot1.cloudflare-dns.com"
|
|
||||||
dial_addr: "1.0.0.1"
|
|
||||||
enable_pipeline: true
|
|
||||||
- addr: "tls://dns.google"
|
|
||||||
dial_addr: "8.8.8.8"
|
|
||||||
enable_pipeline: true
|
|
||||||
- addr: "tls://dns.google"
|
|
||||||
dial_addr: "8.8.4.4"
|
|
||||||
enable_pipeline: true
|
|
||||||
|
|
||||||
# MikroTik Address List 插件 - 性能优化配置
|
|
||||||
- tag: mikrotik_amazon
|
|
||||||
type: mikrotik_addresslist
|
|
||||||
args:
|
|
||||||
host: "10.248.0.1"
|
|
||||||
port: 9728
|
|
||||||
username: "admin"
|
|
||||||
password: "szn0s!nw@pwd()"
|
|
||||||
use_tls: false
|
|
||||||
timeout: 3 # 🚀 减少连接超时到3秒
|
|
||||||
address_list4: "gfw"
|
|
||||||
mask4: 32 # 🚀 使用/32精确匹配,避免网段冲突
|
|
||||||
comment: "auto-amazon"
|
|
||||||
timeout_addr: 43200 # 🚀 减少地址超时到12小时,提高缓存命中率
|
|
||||||
cache_ttl: 7200 # 🚀 减少缓存TTL到2小时,平衡性能和准确性
|
|
||||||
verify_add: false # 🚀 关闭验证,显著提升性能
|
|
||||||
add_all_ips: true # 🚀 启用多IP支持
|
|
||||||
max_ips: 10 # 🚀 限制每个域名最多10个IP,避免过载
|
|
||||||
|
|
||||||
# 🚀 新增性能优化参数(如果支持的话)
|
|
||||||
batch_size: 20 # 批处理大小
|
|
||||||
worker_pool_size: 15 # 工作线程池大小
|
|
||||||
connection_pool_size: 5 # 连接池大小
|
|
||||||
retry_max: 2 # 最大重试次数
|
|
||||||
retry_backoff: 100 # 重试退避时间(ms)
|
|
||||||
enable_pipelining: true # 启用管道化处理
|
|
||||||
60
dns.yaml
60
dns.yaml
@ -1,60 +0,0 @@
|
|||||||
################ DNS Plugins #################
|
|
||||||
plugins:
|
|
||||||
|
|
||||||
- tag: mikrotik-one
|
|
||||||
type: forward
|
|
||||||
args:
|
|
||||||
concurrent: 1
|
|
||||||
upstreams:
|
|
||||||
- addr: "udp://10.248.0.1"
|
|
||||||
|
|
||||||
|
|
||||||
- tag: cn-dns
|
|
||||||
type: forward
|
|
||||||
args:
|
|
||||||
concurrent: 6
|
|
||||||
upstreams:
|
|
||||||
- addr: "udp://202.96.128.86"
|
|
||||||
- addr: "udp://202.96.128.166"
|
|
||||||
- addr: "udp://119.29.29.29"
|
|
||||||
- addr: "udp://223.5.5.5"
|
|
||||||
- addr: "udp://114.114.114.114"
|
|
||||||
- addr: "udp://180.76.76.76"
|
|
||||||
|
|
||||||
|
|
||||||
- tag: jp-dns
|
|
||||||
type: forward
|
|
||||||
args:
|
|
||||||
concurrent: 4 # 同步向 3 条上游并发查询
|
|
||||||
upstreams:
|
|
||||||
- addr: "tls://1dot1dot1dot1.cloudflare-dns.com"
|
|
||||||
dial_addr: "1.1.1.1"
|
|
||||||
enable_pipeline: true
|
|
||||||
- addr: "tls://1dot1dot1dot1.cloudflare-dns.com"
|
|
||||||
dial_addr: "1.0.0.1"
|
|
||||||
enable_pipeline: true
|
|
||||||
- addr: "tls://dns.google"
|
|
||||||
dial_addr: "8.8.8.8"
|
|
||||||
enable_pipeline: true
|
|
||||||
- addr: "tls://dns.google"
|
|
||||||
dial_addr: "8.8.4.4"
|
|
||||||
enable_pipeline: true
|
|
||||||
|
|
||||||
# MikroTik Address List 插件 - 性能优化配置
|
|
||||||
- tag: mikrotik_amazon
|
|
||||||
type: mikrotik_addresslist
|
|
||||||
args:
|
|
||||||
host: "10.248.0.1"
|
|
||||||
port: 9728
|
|
||||||
username: "admin"
|
|
||||||
password: "szn0s!nw@pwd()"
|
|
||||||
use_tls: false
|
|
||||||
timeout: 3 # 🚀 优化:减少连接超时到3秒
|
|
||||||
address_list4: "gfw"
|
|
||||||
mask4: 24 # 🚀 优化:使用/24网段掩码,减少地址条目数量
|
|
||||||
comment: "auto-amazon"
|
|
||||||
timeout_addr: 43200 # 🚀 优化:减少到12小时,提高缓存效率
|
|
||||||
cache_ttl: 7200 # 🚀 优化:2小时缓存,平衡性能和准确性
|
|
||||||
verify_add: false # 🚀 优化:关闭验证,显著提升性能
|
|
||||||
add_all_ips: true # 🚀 优化:启用多IP支持
|
|
||||||
max_ips: 10 # 🚀 优化:限制每域名最多10个IP
|
|
||||||
2
go.mod
2
go.mod
@ -26,6 +26,7 @@ require (
|
|||||||
golang.org/x/sys v0.24.0
|
golang.org/x/sys v0.24.0
|
||||||
golang.org/x/time v0.6.0
|
golang.org/x/time v0.6.0
|
||||||
google.golang.org/protobuf v1.34.2
|
google.golang.org/protobuf v1.34.2
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/nadoo/ipset v0.5.0 => github.com/IrineSistiana/ipset v0.5.1-0.20220703061533-6e0fc3b04c0a
|
replace github.com/nadoo/ipset v0.5.0 => github.com/IrineSistiana/ipset v0.5.1-0.20220703061533-6e0fc3b04c0a
|
||||||
@ -67,5 +68,4 @@ require (
|
|||||||
golang.org/x/text v0.17.0 // indirect
|
golang.org/x/text v0.17.0 // indirect
|
||||||
golang.org/x/tools v0.24.0 // indirect
|
golang.org/x/tools v0.24.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
7
main.go
7
main.go
@ -21,12 +21,13 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
_ "net/http/pprof"
|
||||||
|
|
||||||
"github.com/IrineSistiana/mosdns/v5/coremain"
|
"github.com/IrineSistiana/mosdns/v5/coremain"
|
||||||
"github.com/IrineSistiana/mosdns/v5/mlog"
|
"github.com/IrineSistiana/mosdns/v5/mlog"
|
||||||
_ "github.com/IrineSistiana/mosdns/v5/plugin"
|
_ "github.com/IrineSistiana/mosdns/v5/plugin"
|
||||||
_ "github.com/IrineSistiana/mosdns/v5/tools"
|
_ "github.com/IrineSistiana/mosdns/v5/tools"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
_ "net/http/pprof"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -34,6 +35,10 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
// 设置 Web UI 文件系统
|
||||||
|
coremain.SetWebUIFS(WebUIFS)
|
||||||
|
|
||||||
|
// 添加 version 子命令
|
||||||
coremain.AddSubCmd(&cobra.Command{
|
coremain.AddSubCmd(&cobra.Command{
|
||||||
Use: "version",
|
Use: "version",
|
||||||
Short: "Print out version info and exit.",
|
Short: "Print out version info and exit.",
|
||||||
|
|||||||
@ -1,243 +0,0 @@
|
|||||||
# ============================================
|
|
||||||
# MosDNS v5 最终优化配置
|
|
||||||
# 基于增强的 mikrotik_addresslist 插件
|
|
||||||
# ============================================
|
|
||||||
|
|
||||||
log:
|
|
||||||
level: info
|
|
||||||
|
|
||||||
# 管理 API
|
|
||||||
api:
|
|
||||||
http: "0.0.0.0:5535"
|
|
||||||
|
|
||||||
plugins:
|
|
||||||
# ========= 基础组件 =========
|
|
||||||
|
|
||||||
# GFW 域名列表(仅用于分流,不写入设备)
|
|
||||||
- tag: GFW_domains
|
|
||||||
type: domain_set
|
|
||||||
args:
|
|
||||||
files:
|
|
||||||
- "/usr/local/jinlingma/config/gfwlist.out.txt"
|
|
||||||
|
|
||||||
# 中国大陆 IP 列表
|
|
||||||
- tag: geoip_cn
|
|
||||||
type: ip_set
|
|
||||||
args:
|
|
||||||
files:
|
|
||||||
- "/usr/local/jinlingma/config/cn.txt"
|
|
||||||
|
|
||||||
# 缓存
|
|
||||||
- tag: cache
|
|
||||||
type: cache
|
|
||||||
args:
|
|
||||||
size: 32768
|
|
||||||
lazy_cache_ttl: 43200
|
|
||||||
|
|
||||||
# ========= 上游 DNS 定义 =========
|
|
||||||
|
|
||||||
# 国内 DNS
|
|
||||||
- tag: china-dns
|
|
||||||
type: forward
|
|
||||||
args:
|
|
||||||
concurrent: 6
|
|
||||||
upstreams:
|
|
||||||
- addr: "udp://202.96.128.86"
|
|
||||||
- addr: "udp://202.96.128.166"
|
|
||||||
- addr: "udp://119.29.29.29"
|
|
||||||
- addr: "udp://223.5.5.5"
|
|
||||||
- addr: "udp://114.114.114.114"
|
|
||||||
- addr: "udp://180.76.76.76"
|
|
||||||
|
|
||||||
# 国外 DNS(DoT)
|
|
||||||
- tag: overseas-dns
|
|
||||||
type: forward
|
|
||||||
args:
|
|
||||||
concurrent: 4
|
|
||||||
upstreams:
|
|
||||||
- addr: "tls://1dot1dot1dot1.cloudflare-dns.com"
|
|
||||||
dial_addr: "1.1.1.1"
|
|
||||||
enable_pipeline: true
|
|
||||||
- addr: "tls://1dot1dot1dot1.cloudflare-dns.com"
|
|
||||||
dial_addr: "1.0.0.1"
|
|
||||||
enable_pipeline: true
|
|
||||||
- addr: "tls://dns.google"
|
|
||||||
dial_addr: "8.8.8.8"
|
|
||||||
enable_pipeline: true
|
|
||||||
- addr: "tls://dns.google"
|
|
||||||
dial_addr: "8.8.4.4"
|
|
||||||
enable_pipeline: true
|
|
||||||
|
|
||||||
# fallback 封装
|
|
||||||
- tag: forward_local
|
|
||||||
type: fallback
|
|
||||||
args:
|
|
||||||
primary: china-dns
|
|
||||||
secondary: china-dns
|
|
||||||
threshold: 500
|
|
||||||
always_standby: true
|
|
||||||
|
|
||||||
- tag: forward_remote
|
|
||||||
type: fallback
|
|
||||||
args:
|
|
||||||
primary: overseas-dns
|
|
||||||
secondary: overseas-dns
|
|
||||||
threshold: 500
|
|
||||||
always_standby: true
|
|
||||||
|
|
||||||
# 便捷封装:国内/国外
|
|
||||||
- tag: forward_local_upstream
|
|
||||||
type: sequence
|
|
||||||
args:
|
|
||||||
- exec: prefer_ipv4
|
|
||||||
- exec: query_summary forward_local
|
|
||||||
- exec: $forward_local
|
|
||||||
|
|
||||||
- tag: forward_remote_upstream
|
|
||||||
type: sequence
|
|
||||||
args:
|
|
||||||
- exec: prefer_ipv4
|
|
||||||
- exec: query_summary forward_remote
|
|
||||||
- exec: $forward_remote
|
|
||||||
|
|
||||||
# ========= 🚀 增强的 MikroTik 插件(支持多设备多规则)=========
|
|
||||||
|
|
||||||
# 设备 A:Amazon 相关域名
|
|
||||||
- tag: mikrotik_amazon
|
|
||||||
type: mikrotik_addresslist
|
|
||||||
domain_files:
|
|
||||||
- "/usr/local/jinlingma/config/amazon.txt"
|
|
||||||
- "/usr/local/jinlingma/config/aws.txt"
|
|
||||||
args:
|
|
||||||
host: "10.96.1.22"
|
|
||||||
port: 9728
|
|
||||||
username: "admin"
|
|
||||||
password: "szn0s!nw@pwd()"
|
|
||||||
use_tls: false
|
|
||||||
timeout: 3
|
|
||||||
address_list4: "Amazon"
|
|
||||||
address_list6: "Amazon6"
|
|
||||||
mask4: 24 # 使用/24网段,减少条目数量
|
|
||||||
mask6: 64
|
|
||||||
comment: "Amazon-AutoAdd"
|
|
||||||
timeout_addr: 43200 # 12小时
|
|
||||||
cache_ttl: 3600 # 1小时缓存
|
|
||||||
verify_add: false # 关闭验证,提升性能
|
|
||||||
add_all_ips: true # 添加所有IP
|
|
||||||
max_ips: 20 # 限制每域名最多20个IP
|
|
||||||
|
|
||||||
# 设备 B:Google 相关域名
|
|
||||||
- tag: mikrotik_google
|
|
||||||
type: mikrotik_addresslist
|
|
||||||
domain_files:
|
|
||||||
- "/usr/local/jinlingma/config/google.txt"
|
|
||||||
- "/usr/local/jinlingma/config/youtube.txt"
|
|
||||||
args:
|
|
||||||
host: "10.96.1.23"
|
|
||||||
port: 9728
|
|
||||||
username: "admin"
|
|
||||||
password: "szn0s!nw@pwd()"
|
|
||||||
use_tls: false
|
|
||||||
timeout: 3
|
|
||||||
address_list4: "Google"
|
|
||||||
mask4: 32 # 精确匹配单个IP
|
|
||||||
comment: "Google-AutoAdd"
|
|
||||||
timeout_addr: 21600 # 6小时
|
|
||||||
cache_ttl: 1800 # 30分钟缓存
|
|
||||||
verify_add: false
|
|
||||||
add_all_ips: true
|
|
||||||
max_ips: 15
|
|
||||||
|
|
||||||
# 设备 C:流媒体相关域名
|
|
||||||
- tag: mikrotik_streaming
|
|
||||||
type: mikrotik_addresslist
|
|
||||||
domain_files:
|
|
||||||
- "/usr/local/jinlingma/config/netflix.txt"
|
|
||||||
- "/usr/local/jinlingma/config/disney.txt"
|
|
||||||
args:
|
|
||||||
host: "10.96.1.24"
|
|
||||||
port: 9728
|
|
||||||
username: "admin"
|
|
||||||
password: "szn0s!nw@pwd()"
|
|
||||||
use_tls: false
|
|
||||||
timeout: 5 # 流媒体可能需要更长时间
|
|
||||||
address_list4: "Streaming"
|
|
||||||
mask4: 32
|
|
||||||
comment: "Streaming-AutoAdd"
|
|
||||||
timeout_addr: 21600 # 6小时(流媒体IP变化较频繁)
|
|
||||||
cache_ttl: 1800 # 30分钟缓存
|
|
||||||
verify_add: false
|
|
||||||
add_all_ips: true
|
|
||||||
max_ips: 30 # 流媒体服务IP较多
|
|
||||||
|
|
||||||
# ========= 查询逻辑 =========
|
|
||||||
|
|
||||||
# 检查是否有响应
|
|
||||||
- tag: has_resp_sequence
|
|
||||||
type: sequence
|
|
||||||
args:
|
|
||||||
- matches: has_resp
|
|
||||||
exec: accept
|
|
||||||
|
|
||||||
# 拒绝无效查询
|
|
||||||
- tag: reject_invalid
|
|
||||||
type: sequence
|
|
||||||
args:
|
|
||||||
- matches: qtype 65
|
|
||||||
exec: reject 3
|
|
||||||
|
|
||||||
# GFW 域名分流(仅解析,不写入设备)
|
|
||||||
- tag: gfw_routing_only
|
|
||||||
type: sequence
|
|
||||||
args:
|
|
||||||
- matches: qname $GFW_domains
|
|
||||||
exec: $forward_remote_upstream
|
|
||||||
- exec: query_summary gfw_overseas_routing
|
|
||||||
|
|
||||||
# 智能 fallback 处理
|
|
||||||
- tag: smart_fallback_handler
|
|
||||||
type: sequence
|
|
||||||
args:
|
|
||||||
- exec: prefer_ipv4
|
|
||||||
- exec: $forward_local_upstream
|
|
||||||
- matches: resp_ip $geoip_cn
|
|
||||||
exec: accept
|
|
||||||
- exec: $forward_remote_upstream
|
|
||||||
- exec: query_summary fallback_to_overseas
|
|
||||||
|
|
||||||
# 🚀 主序列(极简版)
|
|
||||||
- tag: main_sequence
|
|
||||||
type: sequence
|
|
||||||
args:
|
|
||||||
# 1. 缓存检查
|
|
||||||
- exec: $cache
|
|
||||||
|
|
||||||
# 2. 拒绝无效查询
|
|
||||||
- exec: $reject_invalid
|
|
||||||
- exec: jump has_resp_sequence
|
|
||||||
|
|
||||||
# 3. GFW 域名分流(仅解析)
|
|
||||||
- exec: $gfw_routing_only
|
|
||||||
- exec: jump has_resp_sequence
|
|
||||||
|
|
||||||
# 4. 智能 fallback
|
|
||||||
- exec: $smart_fallback_handler
|
|
||||||
- exec: jump has_resp_sequence
|
|
||||||
|
|
||||||
# 5. 🚀 MikroTik 设备处理(每个插件自动匹配域名)
|
|
||||||
- exec: $mikrotik_amazon # 自动处理 Amazon 域名
|
|
||||||
- exec: $mikrotik_google # 自动处理 Google 域名
|
|
||||||
- exec: $mikrotik_streaming # 自动处理流媒体域名
|
|
||||||
|
|
||||||
# ========= 服务监听 =========
|
|
||||||
- tag: udp_server
|
|
||||||
type: udp_server
|
|
||||||
args:
|
|
||||||
entry: main_sequence
|
|
||||||
listen: ":5322"
|
|
||||||
|
|
||||||
- tag: tcp_server
|
|
||||||
type: tcp_server
|
|
||||||
args:
|
|
||||||
entry: main_sequence
|
|
||||||
listen: ":5322"
|
|
||||||
130
v2dat.sh
Normal file
130
v2dat.sh
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e # 如果任何命令失败则退出
|
||||||
|
|
||||||
|
# 获取脚本所在目录
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
TMPDIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMPDIR"' EXIT # 确保脚本退出时删除临时目录
|
||||||
|
|
||||||
|
# 创建必要的目录
|
||||||
|
mkdir -p "$SCRIPT_DIR/geo"
|
||||||
|
mkdir -p "$SCRIPT_DIR/geosite"
|
||||||
|
mkdir -p "$SCRIPT_DIR/geoip"
|
||||||
|
mkdir -p "$SCRIPT_DIR/config"
|
||||||
|
|
||||||
|
# 下载 geoip 和 geosite 数据文件到 geo 目录
|
||||||
|
download_geodata() {
|
||||||
|
echo "正在下载 geoip.dat..."
|
||||||
|
curl --connect-timeout 5 -m 60 -kfSL -o "$SCRIPT_DIR/geo/geoip.dat" "https://cdn.jsdelivr.net/gh/Loyalsoldier/geoip@release/geoip.dat"
|
||||||
|
|
||||||
|
echo "正在下载 geosite.dat..."
|
||||||
|
curl --connect-timeout 5 -m 60 -kfSL -o "$SCRIPT_DIR/geo/geosite.dat" "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat"
|
||||||
|
|
||||||
|
echo "正在下载 CN-ip-cidr.txt"
|
||||||
|
curl --connect-timeout 5 -m 60 -kfSL -o "$SCRIPT_DIR/config/CN-ip-cidr.txt" "https://raw.githubusercontent.com/Hackl0us/GeoIP2-CN/release/CN-ip-cidr.txt"
|
||||||
|
|
||||||
|
echo "下载完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 下载 v2dat 工具(如果不存在)
|
||||||
|
download_v2dat() {
|
||||||
|
if [ ! -f "$SCRIPT_DIR/v2dat" ]; then
|
||||||
|
echo "正在下载 v2dat 工具..."
|
||||||
|
curl -fSL -o "$SCRIPT_DIR/v2dat" "https://raw.githubusercontent.com/xukecheng/scripts/main/v2dat"
|
||||||
|
chmod +x "$SCRIPT_DIR/v2dat"
|
||||||
|
echo "v2dat 工具下载完成"
|
||||||
|
else
|
||||||
|
echo "v2dat 工具已存在"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 过滤 IPv4 地址(去掉 IPv6)
|
||||||
|
filter_ipv4_only() {
|
||||||
|
local input_file="$1"
|
||||||
|
local output_file="$2"
|
||||||
|
|
||||||
|
if [ ! -f "$input_file" ]; then
|
||||||
|
echo "警告: 文件 $input_file 不存在,跳过过滤"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "正在过滤 IPv4 地址(去掉 IPv6)..."
|
||||||
|
echo "输入文件: $input_file"
|
||||||
|
echo "输出文件: $output_file"
|
||||||
|
|
||||||
|
# 统计原始行数
|
||||||
|
original_count=$(wc -l < "$input_file" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
# 过滤 IPv4 地址:
|
||||||
|
# 1. 匹配 IPv4 CIDR 格式 (x.x.x.x/xx)
|
||||||
|
# 2. 排除包含冒号的 IPv6 地址
|
||||||
|
# 3. 排除空行和注释行
|
||||||
|
grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}(/[0-9]{1,2})?$' "$input_file" > "$output_file" 2>/dev/null || {
|
||||||
|
echo "错误: 过滤 IPv4 地址失败"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 统计过滤后行数
|
||||||
|
filtered_count=$(wc -l < "$output_file" 2>/dev/null || echo "0")
|
||||||
|
removed_count=$((original_count - filtered_count))
|
||||||
|
|
||||||
|
echo "过滤完成:"
|
||||||
|
echo " 原始条目: $original_count"
|
||||||
|
echo " IPv4 条目: $filtered_count"
|
||||||
|
echo " 移除条目: $removed_count (IPv6 和无效条目)"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 使用 v2dat 工具解包数据
|
||||||
|
unpack_geodata() {
|
||||||
|
echo "正在解包 geosite.dat..."
|
||||||
|
"$SCRIPT_DIR/v2dat" unpack geosite "$SCRIPT_DIR/geo/geosite.dat" -o "$SCRIPT_DIR/geosite"
|
||||||
|
|
||||||
|
echo "正在解包 geoip.dat CN 数据..."
|
||||||
|
"$SCRIPT_DIR/v2dat" unpack geoip "$SCRIPT_DIR/geo/geoip.dat" -o "$SCRIPT_DIR/geoip" -f cn
|
||||||
|
|
||||||
|
# 🆕 新增:过滤 IPv4 地址,去掉 IPv6
|
||||||
|
local geoip_cn_file="$SCRIPT_DIR/geoip/geoip_cn.txt"
|
||||||
|
local geoip_cn_ipv4_file="$SCRIPT_DIR/config/cn.txt"
|
||||||
|
|
||||||
|
if [ -f "$geoip_cn_file" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "🔄 正在处理 CN IP 数据..."
|
||||||
|
filter_ipv4_only "$geoip_cn_file" "$geoip_cn_ipv4_file"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "✅ CN IPv4 地址列表已生成: $geoip_cn_ipv4_file"
|
||||||
|
echo " 该文件可直接用于 MosDNS 配置中的 geoip_cn"
|
||||||
|
else
|
||||||
|
echo "❌ 处理 CN IP 数据失败"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⚠️ 警告: 未找到 $geoip_cn_file 文件"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "解包完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 主流程
|
||||||
|
echo "开始处理..."
|
||||||
|
download_geodata
|
||||||
|
download_v2dat
|
||||||
|
unpack_geodata
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎉 所有操作完成!"
|
||||||
|
echo ""
|
||||||
|
echo "📁 生成的文件:"
|
||||||
|
echo " ├── geo/geoip.dat (原始 geoip 数据)"
|
||||||
|
echo " ├── geo/geosite.dat (原始 geosite 数据)"
|
||||||
|
echo " ├── geoip/geoip_cn.txt (解包的 CN IP 数据,包含 IPv6)"
|
||||||
|
echo " ├── config/cn.txt (🆕 过滤后的 CN IPv4 数据)"
|
||||||
|
echo " ├── config/CN-ip-cidr.txt (备用 CN IP 数据)"
|
||||||
|
echo " └── geosite/ (解包的域名数据)"
|
||||||
|
echo ""
|
||||||
|
echo "💡 使用建议:"
|
||||||
|
echo " - MosDNS 配置中使用: config/cn.txt (仅 IPv4)"
|
||||||
|
echo " - 如需 IPv6 支持,使用: geoip/geoip_cn.txt"
|
||||||
|
echo ""
|
||||||
8
web-ui/.editorconfig
Normal file
8
web-ui/.editorconfig
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
end_of_line = lf
|
||||||
|
max_line_length = 100
|
||||||
1
web-ui/.gitattributes
vendored
Normal file
1
web-ui/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
30
web-ui/.gitignore
vendored
Normal file
30
web-ui/.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
6
web-ui/.prettierrc.json
Normal file
6
web-ui/.prettierrc.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
48
web-ui/README.md
Normal file
48
web-ui/README.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# web-ui
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||||
|
|
||||||
|
## Recommended Browser Setup
|
||||||
|
|
||||||
|
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
||||||
|
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
||||||
|
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
||||||
|
- Firefox:
|
||||||
|
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
||||||
|
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
||||||
|
|
||||||
|
## Type Support for `.vue` Imports in TS
|
||||||
|
|
||||||
|
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type-Check, Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint with [ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
1
web-ui/env.d.ts
vendored
Normal file
1
web-ui/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
22
web-ui/eslint.config.ts
Normal file
22
web-ui/eslint.config.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { globalIgnores } from 'eslint/config'
|
||||||
|
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||||
|
import pluginVue from 'eslint-plugin-vue'
|
||||||
|
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||||
|
|
||||||
|
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
||||||
|
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
||||||
|
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
||||||
|
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
||||||
|
|
||||||
|
export default defineConfigWithVueTs(
|
||||||
|
{
|
||||||
|
name: 'app/files-to-lint',
|
||||||
|
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||||
|
},
|
||||||
|
|
||||||
|
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||||
|
|
||||||
|
pluginVue.configs['flat/essential'],
|
||||||
|
vueTsConfigs.recommended,
|
||||||
|
skipFormatting,
|
||||||
|
)
|
||||||
13
web-ui/index.html
Normal file
13
web-ui/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Vite App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5736
web-ui/package-lock.json
generated
Normal file
5736
web-ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
web-ui/package.json
Normal file
43
web-ui/package.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "web-ui",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build-only": "vite build",
|
||||||
|
"type-check": "vue-tsc --build",
|
||||||
|
"lint": "eslint . --fix",
|
||||||
|
"format": "prettier --write src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
|
"axios": "^1.12.2",
|
||||||
|
"element-plus": "^2.11.4",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
|
"vue": "^3.5.22",
|
||||||
|
"vue-router": "^4.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/node22": "^22.0.2",
|
||||||
|
"@types/node": "^22.18.6",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
|
"@vue/eslint-config-typescript": "^14.6.0",
|
||||||
|
"@vue/tsconfig": "^0.8.1",
|
||||||
|
"eslint": "^9.33.0",
|
||||||
|
"eslint-plugin-vue": "~10.4.0",
|
||||||
|
"jiti": "^2.5.1",
|
||||||
|
"npm-run-all2": "^8.0.4",
|
||||||
|
"prettier": "3.6.2",
|
||||||
|
"typescript": "~5.9.0",
|
||||||
|
"vite": "^7.1.7",
|
||||||
|
"vite-plugin-vue-devtools": "^8.0.2",
|
||||||
|
"vue-tsc": "^3.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
web-ui/public/favicon.ico
Normal file
BIN
web-ui/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
145
web-ui/src/App.vue
Normal file
145
web-ui/src/App.vue
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { useServerStore } from '@/stores/server'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const serverStore = useServerStore()
|
||||||
|
|
||||||
|
const activeIndex = ref('/')
|
||||||
|
|
||||||
|
// 导航菜单项
|
||||||
|
const menuItems = [
|
||||||
|
{ path: '/', icon: '📊', title: '仪表板' },
|
||||||
|
{ path: '/mikrotik', icon: '🔧', title: 'MikroTik 管理' },
|
||||||
|
{ path: '/domains', icon: '📝', title: '域名文件' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleSelect = (key: string) => {
|
||||||
|
router.push(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听路由变化更新激活菜单
|
||||||
|
router.afterEach((to) => {
|
||||||
|
activeIndex.value = to.path
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- 头部 -->
|
||||||
|
<el-header class="app-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="logo">
|
||||||
|
<span class="logo-icon">🌐</span>
|
||||||
|
<span class="logo-text">MosDNS 管理面板</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-info">
|
||||||
|
<el-tag :type="serverStore.isOnline ? 'success' : 'danger'" effect="plain">
|
||||||
|
{{ serverStore.isOnline ? '在线' : '离线' }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag type="info" effect="plain">
|
||||||
|
{{ serverStore.serverInfo?.version || 'v5.0.0' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
|
||||||
|
<!-- 导航菜单 -->
|
||||||
|
<el-menu
|
||||||
|
:default-active="activeIndex"
|
||||||
|
mode="horizontal"
|
||||||
|
@select="handleSelect"
|
||||||
|
class="app-menu"
|
||||||
|
>
|
||||||
|
<el-menu-item v-for="item in menuItems" :key="item.path" :index="item.path">
|
||||||
|
<span>{{ item.icon }}</span>
|
||||||
|
<span style="margin-left: 8px">{{ item.title }}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
|
<el-main class="app-main">
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<component :is="Component" />
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
|
</el-main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||||
|
border-bottom: 1px solid #dcdfe6;
|
||||||
|
padding: 0;
|
||||||
|
height: 64px !important;
|
||||||
|
line-height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-menu {
|
||||||
|
border-bottom: 2px solid #e4e7ed;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-menu :deep(.el-menu-item) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-main {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 路由过渡动画 */
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
8
web-ui/src/api/cache.ts
Normal file
8
web-ui/src/api/cache.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// 缓存相关 API
|
||||||
|
import http from './http'
|
||||||
|
|
||||||
|
export const cacheApi = {
|
||||||
|
// 清空缓存
|
||||||
|
flush: () => http.post<any, { success: boolean; message: string }>('/cache/flush'),
|
||||||
|
}
|
||||||
|
|
||||||
29
web-ui/src/api/domain.ts
Normal file
29
web-ui/src/api/domain.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// 域名文件相关 API
|
||||||
|
import http from './http'
|
||||||
|
|
||||||
|
export interface DomainFile {
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
size: number
|
||||||
|
line_count: number
|
||||||
|
last_modified: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const domainApi = {
|
||||||
|
// 获取域名文件列表
|
||||||
|
list: () => http.get<any, { success: boolean; data: DomainFile[]; message?: string }>('/domain-files'),
|
||||||
|
|
||||||
|
// 获取域名文件内容
|
||||||
|
get: (filename: string) =>
|
||||||
|
http.get<any, { success: boolean; data: { content: string } }>(
|
||||||
|
`/domain-files/${encodeURIComponent(filename)}`
|
||||||
|
),
|
||||||
|
|
||||||
|
// 更新域名文件内容
|
||||||
|
update: (filename: string, content: string) =>
|
||||||
|
http.put<any, { success: boolean; message: string }>(
|
||||||
|
`/domain-files/${encodeURIComponent(filename)}`,
|
||||||
|
{ content }
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
47
web-ui/src/api/http.ts
Normal file
47
web-ui/src/api/http.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// HTTP 客户端封装
|
||||||
|
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
// 创建 axios 实例
|
||||||
|
const http: AxiosInstance = axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
http.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
http.interceptors.response.use(
|
||||||
|
(response: AxiosResponse) => {
|
||||||
|
const res = response.data
|
||||||
|
|
||||||
|
// 如果响应包含 success 字段
|
||||||
|
if (res.hasOwnProperty('success')) {
|
||||||
|
if (!res.success) {
|
||||||
|
ElMessage.error(res.message || '请求失败')
|
||||||
|
return Promise.reject(new Error(res.message || '请求失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
const message = error.response?.data?.message || error.message || '网络错误'
|
||||||
|
ElMessage.error(message)
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default http
|
||||||
|
|
||||||
41
web-ui/src/api/mikrotik.ts
Normal file
41
web-ui/src/api/mikrotik.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// MikroTik 配置相关 API
|
||||||
|
import http from './http'
|
||||||
|
|
||||||
|
export interface MikroTikConfig {
|
||||||
|
tag: string
|
||||||
|
type: string
|
||||||
|
args: {
|
||||||
|
domain_files: string[]
|
||||||
|
host: string
|
||||||
|
port: number
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
use_tls: boolean
|
||||||
|
timeout: number
|
||||||
|
address_list4: string
|
||||||
|
mask4: number
|
||||||
|
comment: string
|
||||||
|
timeout_addr: number
|
||||||
|
cache_ttl: number
|
||||||
|
verify_add: boolean
|
||||||
|
add_all_ips: boolean
|
||||||
|
max_ips: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mikrotikApi = {
|
||||||
|
// 获取所有 MikroTik 配置
|
||||||
|
list: () => http.get<any, { success: boolean; data: MikroTikConfig[] }>('/mikrotik/list'),
|
||||||
|
|
||||||
|
// 添加 MikroTik 配置
|
||||||
|
add: (data: MikroTikConfig) =>
|
||||||
|
http.post<any, { success: boolean; message: string; data: MikroTikConfig }>(
|
||||||
|
'/mikrotik/add',
|
||||||
|
data
|
||||||
|
),
|
||||||
|
|
||||||
|
// 删除 MikroTik 配置
|
||||||
|
delete: (tag: string) =>
|
||||||
|
http.delete<any, { success: boolean; message: string }>(`/mikrotik/${encodeURIComponent(tag)}`),
|
||||||
|
}
|
||||||
|
|
||||||
47
web-ui/src/api/server.ts
Normal file
47
web-ui/src/api/server.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// 服务器信息相关 API
|
||||||
|
import http from './http'
|
||||||
|
|
||||||
|
export interface ServerInfo {
|
||||||
|
name: string
|
||||||
|
version: string
|
||||||
|
start_time: string
|
||||||
|
uptime: string
|
||||||
|
uptime_seconds: number
|
||||||
|
status: string
|
||||||
|
config_file: string
|
||||||
|
working_dir: string
|
||||||
|
plugin_count: number
|
||||||
|
api_address: string
|
||||||
|
dns_ports: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerStatus {
|
||||||
|
status: string
|
||||||
|
totalQueries?: number
|
||||||
|
cacheHitRate?: number
|
||||||
|
avgResponseTime?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatsData {
|
||||||
|
totalQueries: number
|
||||||
|
successfulQueries: number
|
||||||
|
failedQueries: number
|
||||||
|
cacheHits: number
|
||||||
|
cacheMisses: number
|
||||||
|
avgResponseTime: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const serverApi = {
|
||||||
|
// 获取服务器信息
|
||||||
|
getInfo: () => http.get<any, { success: boolean; data: ServerInfo }>('/server/info'),
|
||||||
|
|
||||||
|
// 获取服务器状态
|
||||||
|
getStatus: () => http.get<any, { success: boolean; data: ServerStatus }>('/server/status'),
|
||||||
|
|
||||||
|
// 获取详细统计
|
||||||
|
getStats: () => http.get<any, { success: boolean; data: StatsData }>('/stats/detailed'),
|
||||||
|
|
||||||
|
// 重启服务
|
||||||
|
restart: () => http.post<any, { success: boolean; message: string }>('/system/restart'),
|
||||||
|
}
|
||||||
|
|
||||||
86
web-ui/src/assets/base.css
Normal file
86
web-ui/src/assets/base.css
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/* color palette from <https://github.com/vuejs/theme> */
|
||||||
|
:root {
|
||||||
|
--vt-c-white: #ffffff;
|
||||||
|
--vt-c-white-soft: #f8f8f8;
|
||||||
|
--vt-c-white-mute: #f2f2f2;
|
||||||
|
|
||||||
|
--vt-c-black: #181818;
|
||||||
|
--vt-c-black-soft: #222222;
|
||||||
|
--vt-c-black-mute: #282828;
|
||||||
|
|
||||||
|
--vt-c-indigo: #2c3e50;
|
||||||
|
|
||||||
|
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||||
|
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||||
|
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||||
|
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||||
|
|
||||||
|
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||||
|
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||||
|
--vt-c-text-dark-1: var(--vt-c-white);
|
||||||
|
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* semantic color variables for this project */
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-white);
|
||||||
|
--color-background-soft: var(--vt-c-white-soft);
|
||||||
|
--color-background-mute: var(--vt-c-white-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-light-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-light-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-light-1);
|
||||||
|
--color-text: var(--vt-c-text-light-1);
|
||||||
|
|
||||||
|
--section-gap: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-black);
|
||||||
|
--color-background-soft: var(--vt-c-black-soft);
|
||||||
|
--color-background-mute: var(--vt-c-black-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-dark-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-dark-1);
|
||||||
|
--color-text: var(--vt-c-text-dark-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-background);
|
||||||
|
transition:
|
||||||
|
color 0.5s,
|
||||||
|
background-color 0.5s;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family:
|
||||||
|
Inter,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
Oxygen,
|
||||||
|
Ubuntu,
|
||||||
|
Cantarell,
|
||||||
|
'Fira Sans',
|
||||||
|
'Droid Sans',
|
||||||
|
'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
1
web-ui/src/assets/logo.svg
Normal file
1
web-ui/src/assets/logo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||||
|
After Width: | Height: | Size: 276 B |
39
web-ui/src/assets/main.css
Normal file
39
web-ui/src/assets/main.css
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
@import './base.css';
|
||||||
|
|
||||||
|
/* 全局样式重置 - 支持全屏布局 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
.green {
|
||||||
|
text-decoration: none;
|
||||||
|
color: hsla(160, 100%, 37%, 1);
|
||||||
|
transition: 0.4s;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
a:hover {
|
||||||
|
background-color: hsla(160, 100%, 37%, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
web-ui/src/components/HelloWorld.vue
Normal file
41
web-ui/src/components/HelloWorld.vue
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
msg: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="greetings">
|
||||||
|
<h1 class="green">{{ msg }}</h1>
|
||||||
|
<h3>
|
||||||
|
You’ve successfully created a project with
|
||||||
|
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
|
||||||
|
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
h1 {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 2.6rem;
|
||||||
|
position: relative;
|
||||||
|
top: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.greetings h1,
|
||||||
|
.greetings h3 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.greetings h1,
|
||||||
|
.greetings h3 {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
94
web-ui/src/components/TheWelcome.vue
Normal file
94
web-ui/src/components/TheWelcome.vue
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import WelcomeItem from './WelcomeItem.vue'
|
||||||
|
import DocumentationIcon from './icons/IconDocumentation.vue'
|
||||||
|
import ToolingIcon from './icons/IconTooling.vue'
|
||||||
|
import EcosystemIcon from './icons/IconEcosystem.vue'
|
||||||
|
import CommunityIcon from './icons/IconCommunity.vue'
|
||||||
|
import SupportIcon from './icons/IconSupport.vue'
|
||||||
|
|
||||||
|
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<DocumentationIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Documentation</template>
|
||||||
|
|
||||||
|
Vue’s
|
||||||
|
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
||||||
|
provides you with all information you need to get started.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<ToolingIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Tooling</template>
|
||||||
|
|
||||||
|
This project is served and bundled with
|
||||||
|
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
||||||
|
recommended IDE setup is
|
||||||
|
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
|
||||||
|
+
|
||||||
|
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener">Vue - Official</a>. If
|
||||||
|
you need to test your components and web pages, check out
|
||||||
|
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
|
||||||
|
and
|
||||||
|
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
|
||||||
|
/
|
||||||
|
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
More instructions are available in
|
||||||
|
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
|
||||||
|
>.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<EcosystemIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Ecosystem</template>
|
||||||
|
|
||||||
|
Get official tools and libraries for your project:
|
||||||
|
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
||||||
|
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
||||||
|
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
||||||
|
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
||||||
|
you need more resources, we suggest paying
|
||||||
|
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
||||||
|
a visit.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<CommunityIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Community</template>
|
||||||
|
|
||||||
|
Got stuck? Ask your question on
|
||||||
|
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
|
||||||
|
(our official Discord server), or
|
||||||
|
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
||||||
|
>StackOverflow</a
|
||||||
|
>. You should also follow the official
|
||||||
|
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
|
||||||
|
Bluesky account or the
|
||||||
|
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
||||||
|
X account for latest news in the Vue world.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<SupportIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Support Vue</template>
|
||||||
|
|
||||||
|
As an independent project, Vue relies on community backing for its sustainability. You can help
|
||||||
|
us by
|
||||||
|
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
||||||
|
</WelcomeItem>
|
||||||
|
</template>
|
||||||
87
web-ui/src/components/WelcomeItem.vue
Normal file
87
web-ui/src/components/WelcomeItem.vue
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div class="item">
|
||||||
|
<i>
|
||||||
|
<slot name="icon"></slot>
|
||||||
|
</i>
|
||||||
|
<div class="details">
|
||||||
|
<h3>
|
||||||
|
<slot name="heading"></slot>
|
||||||
|
</h3>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.item {
|
||||||
|
margin-top: 2rem;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
place-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
color: var(--color-heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.item {
|
||||||
|
margin-top: 0;
|
||||||
|
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
top: calc(50% - 25px);
|
||||||
|
left: -26px;
|
||||||
|
position: absolute;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
background: var(--color-background);
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:before {
|
||||||
|
content: ' ';
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: calc(50% + 25px);
|
||||||
|
height: calc(50% - 25px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:after {
|
||||||
|
content: ' ';
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: calc(50% + 25px);
|
||||||
|
height: calc(50% - 25px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:first-of-type:before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:last-of-type:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
7
web-ui/src/components/icons/IconCommunity.vue
Normal file
7
web-ui/src/components/icons/IconCommunity.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
web-ui/src/components/icons/IconDocumentation.vue
Normal file
7
web-ui/src/components/icons/IconDocumentation.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
web-ui/src/components/icons/IconEcosystem.vue
Normal file
7
web-ui/src/components/icons/IconEcosystem.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
web-ui/src/components/icons/IconSupport.vue
Normal file
7
web-ui/src/components/icons/IconSupport.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
19
web-ui/src/components/icons/IconTooling.vue
Normal file
19
web-ui/src/components/icons/IconTooling.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
aria-hidden="true"
|
||||||
|
role="img"
|
||||||
|
class="iconify iconify--mdi"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
26
web-ui/src/main.ts
Normal file
26
web-ui/src/main.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import './assets/main.css'
|
||||||
|
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
// 注册所有 Element Plus 图标
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
app.use(ElementPlus, {
|
||||||
|
locale: zhCn,
|
||||||
|
})
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
34
web-ui/src/router/index.ts
Normal file
34
web-ui/src/router/index.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import DashboardView from '../views/DashboardView.vue'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'dashboard',
|
||||||
|
component: DashboardView,
|
||||||
|
meta: { title: '仪表板' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/mikrotik',
|
||||||
|
name: 'mikrotik',
|
||||||
|
component: () => import('../views/MikroTikView.vue'),
|
||||||
|
meta: { title: 'MikroTik 管理' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/domains',
|
||||||
|
name: 'domains',
|
||||||
|
component: () => import('../views/DomainFilesView.vue'),
|
||||||
|
meta: { title: '域名文件' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
// 路由守卫 - 设置页面标题
|
||||||
|
router.beforeEach((to, _from, next) => {
|
||||||
|
document.title = `${to.meta.title || 'MosDNS'} - MosDNS 管理面板`
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
12
web-ui/src/stores/counter.ts
Normal file
12
web-ui/src/stores/counter.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useCounterStore = defineStore('counter', () => {
|
||||||
|
const count = ref(0)
|
||||||
|
const doubleCount = computed(() => count.value * 2)
|
||||||
|
function increment() {
|
||||||
|
count.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
return { count, doubleCount, increment }
|
||||||
|
})
|
||||||
43
web-ui/src/stores/mikrotik.ts
Normal file
43
web-ui/src/stores/mikrotik.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// MikroTik 配置管理
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { mikrotikApi, type MikroTikConfig } from '@/api/mikrotik'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
export const useMikrotikStore = defineStore('mikrotik', () => {
|
||||||
|
// 状态
|
||||||
|
const configs = ref<MikroTikConfig[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
const fetchConfigs = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await mikrotikApi.list()
|
||||||
|
configs.value = res.data || []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addConfig = async (config: MikroTikConfig) => {
|
||||||
|
const res = await mikrotikApi.add(config)
|
||||||
|
ElMessage.success(res.message || 'MikroTik 配置已保存,需要重启服务生效')
|
||||||
|
await fetchConfigs()
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteConfig = async (tag: string) => {
|
||||||
|
const res = await mikrotikApi.delete(tag)
|
||||||
|
ElMessage.success(res.message || '配置已删除,需要重启服务生效')
|
||||||
|
await fetchConfigs()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
configs,
|
||||||
|
loading,
|
||||||
|
fetchConfigs,
|
||||||
|
addConfig,
|
||||||
|
deleteConfig,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
64
web-ui/src/stores/server.ts
Normal file
64
web-ui/src/stores/server.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// 服务器状态管理
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { serverApi, type ServerInfo, type StatsData } from '@/api/server'
|
||||||
|
|
||||||
|
export const useServerStore = defineStore('server', () => {
|
||||||
|
// 状态
|
||||||
|
const serverInfo = ref<ServerInfo | null>(null)
|
||||||
|
const stats = ref<StatsData | null>(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const isOnline = computed(() => serverInfo.value?.status === 'running')
|
||||||
|
const uptime = computed(() => {
|
||||||
|
if (!serverInfo.value?.uptime_seconds) return '0分钟'
|
||||||
|
const seconds = serverInfo.value.uptime_seconds
|
||||||
|
const days = Math.floor(seconds / 86400)
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600)
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60)
|
||||||
|
|
||||||
|
const parts = []
|
||||||
|
if (days > 0) parts.push(`${days}天`)
|
||||||
|
if (hours > 0) parts.push(`${hours}小时`)
|
||||||
|
if (minutes > 0 || parts.length === 0) parts.push(`${minutes}分钟`)
|
||||||
|
|
||||||
|
return parts.join(' ')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
const fetchServerInfo = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await serverApi.getInfo()
|
||||||
|
serverInfo.value = res.data
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchStats = async () => {
|
||||||
|
try {
|
||||||
|
const res = await serverApi.getStats()
|
||||||
|
stats.value = res.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch stats:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const restart = async () => {
|
||||||
|
await serverApi.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
serverInfo,
|
||||||
|
stats,
|
||||||
|
loading,
|
||||||
|
isOnline,
|
||||||
|
uptime,
|
||||||
|
fetchServerInfo,
|
||||||
|
fetchStats,
|
||||||
|
restart,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
15
web-ui/src/views/AboutView.vue
Normal file
15
web-ui/src/views/AboutView.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<div class="about">
|
||||||
|
<h1>This is an about page</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.about {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
202
web-ui/src/views/DashboardView.vue
Normal file
202
web-ui/src/views/DashboardView.vue
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { useServerStore } from '@/stores/server'
|
||||||
|
import { cacheApi } from '@/api/cache'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Refresh, Delete } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const serverStore = useServerStore()
|
||||||
|
|
||||||
|
const handleFlushCache = async () => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要清空缓存吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
await cacheApi.flush()
|
||||||
|
ElMessage.success('缓存清空成功')
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRestart = async () => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要重启服务吗?服务将在 3 秒后重启。', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
await serverStore.restart()
|
||||||
|
ElMessage.success('重启请求已发送')
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
await serverStore.fetchServerInfo()
|
||||||
|
await serverStore.fetchStats()
|
||||||
|
ElMessage.success('数据已刷新')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await serverStore.fetchServerInfo()
|
||||||
|
await serverStore.fetchStats()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="dashboard">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<!-- 服务状态卡片 -->
|
||||||
|
<el-col :xs="24" :sm="12" :lg="8">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>📊 服务状态</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">运行状态:</span>
|
||||||
|
<el-tag :type="serverStore.isOnline ? 'success' : 'danger'">
|
||||||
|
{{ serverStore.serverInfo?.status || '-' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">运行时间:</span>
|
||||||
|
<span class="stat-value">{{ serverStore.uptime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">DNS 端口:</span>
|
||||||
|
<span class="stat-value">
|
||||||
|
{{ serverStore.serverInfo?.dns_ports?.join(', ') || '未检测到' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">API 地址:</span>
|
||||||
|
<span class="stat-value">{{ serverStore.serverInfo?.api_address || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 查询统计卡片 -->
|
||||||
|
<el-col :xs="24" :sm="12" :lg="8">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>📈 查询统计</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">总查询数:</span>
|
||||||
|
<span class="stat-value">{{ serverStore.stats?.totalQueries?.toLocaleString() || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">缓存命中:</span>
|
||||||
|
<span class="stat-value">{{ serverStore.stats?.cacheHits?.toLocaleString() || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">平均响应:</span>
|
||||||
|
<span class="stat-value">
|
||||||
|
{{ serverStore.stats?.avgResponseTime ? `${serverStore.stats.avgResponseTime}ms` : '-' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 快速操作卡片 -->
|
||||||
|
<el-col :xs="24" :sm="24" :lg="8">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>⚡ 快速操作</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="button-group">
|
||||||
|
<el-button type="primary" :icon="Refresh" @click="refreshData">刷新数据</el-button>
|
||||||
|
<el-button type="warning" :icon="Delete" @click="handleFlushCache">清空缓存</el-button>
|
||||||
|
<el-button type="danger" @click="handleRestart">重启服务</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard {
|
||||||
|
width: 100%;
|
||||||
|
min-height: calc(100vh - 160px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card) {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group .el-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.dashboard {
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
165
web-ui/src/views/DomainFilesView.vue
Normal file
165
web-ui/src/views/DomainFilesView.vue
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { domainApi, type DomainFile } from '@/api/domain'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Refresh, Document } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const files = ref<DomainFile[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const currentFile = ref<{ name: string; content: string }>({ name: '', content: '' })
|
||||||
|
const editorLoading = ref(false)
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
const formatSize = (bytes: number): string => {
|
||||||
|
if (bytes === 0) return '0 B'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (dateString: string): string => {
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载文件列表
|
||||||
|
const fetchFiles = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await domainApi.list()
|
||||||
|
files.value = res.data || []
|
||||||
|
if (res.message) {
|
||||||
|
ElMessage.info(res.message)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看文件
|
||||||
|
const handleView = async (file: DomainFile) => {
|
||||||
|
editorLoading.value = true
|
||||||
|
dialogVisible.value = true
|
||||||
|
currentFile.value = { name: file.name, content: '' }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await domainApi.get(file.name)
|
||||||
|
currentFile.value.content = res.data.content
|
||||||
|
} finally {
|
||||||
|
editorLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存文件
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!currentFile.value.name) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await domainApi.update(currentFile.value.name, currentFile.value.content)
|
||||||
|
ElMessage.success('文件保存成功')
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchFiles()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchFiles()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="domain-files-view">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>📝 域名文件管理</span>
|
||||||
|
<el-button type="primary" :icon="Refresh" @click="fetchFiles">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table :data="files" v-loading="loading" stripe>
|
||||||
|
<el-table-column prop="name" label="文件名" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span style="margin-left: 8px">{{ row.name }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="大小" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatSize(row.size) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="行数" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.line_count?.toLocaleString() }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="最后修改" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatTime(row.last_modified) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="路径" min-width="250">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tooltip :content="row.path" placement="top">
|
||||||
|
<span class="path-text">{{ row.path }}</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="100" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" size="small" @click="handleView(row)">查看</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-empty v-if="!loading && files.length === 0" description="暂无域名文件" />
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 文件编辑对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="`编辑文件: ${currentFile.name}`"
|
||||||
|
width="80%"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model="currentFile.content"
|
||||||
|
type="textarea"
|
||||||
|
:rows="20"
|
||||||
|
v-loading="editorLoading"
|
||||||
|
placeholder="文件内容..."
|
||||||
|
style="font-family: monospace"
|
||||||
|
/>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.domain-files-view {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-text {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
9
web-ui/src/views/HomeView.vue
Normal file
9
web-ui/src/views/HomeView.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import TheWelcome from '../components/TheWelcome.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<TheWelcome />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
263
web-ui/src/views/MikroTikView.vue
Normal file
263
web-ui/src/views/MikroTikView.vue
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { useMikrotikStore } from '@/stores/mikrotik'
|
||||||
|
import type { MikroTikConfig } from '@/api/mikrotik'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Refresh, Plus, Delete } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const mikrotikStore = useMikrotikStore()
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const form = reactive({
|
||||||
|
tag: '',
|
||||||
|
host: '',
|
||||||
|
port: 9728,
|
||||||
|
username: 'admin',
|
||||||
|
password: '',
|
||||||
|
addresslist: '',
|
||||||
|
domainFile: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单引用
|
||||||
|
const formRef = ref()
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const rules = {
|
||||||
|
tag: [{ required: true, message: '请输入配置标签', trigger: 'blur' }],
|
||||||
|
host: [{ required: true, message: '请输入 MikroTik 地址', trigger: 'blur' }],
|
||||||
|
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||||
|
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||||||
|
addresslist: [{ required: true, message: '请输入地址列表名', trigger: 'blur' }],
|
||||||
|
domainFile: [{ required: true, message: '请输入域名文件路径', trigger: 'blur' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
await formRef.value.validate(async (valid: boolean) => {
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
const config: MikroTikConfig = {
|
||||||
|
tag: form.tag,
|
||||||
|
type: 'mikrotik_addresslist',
|
||||||
|
args: {
|
||||||
|
domain_files: [form.domainFile],
|
||||||
|
host: form.host,
|
||||||
|
port: form.port,
|
||||||
|
username: form.username,
|
||||||
|
password: form.password,
|
||||||
|
use_tls: false,
|
||||||
|
timeout: 3,
|
||||||
|
address_list4: form.addresslist,
|
||||||
|
mask4: 24,
|
||||||
|
comment: `${form.addresslist}-AutoAdd`,
|
||||||
|
timeout_addr: 43200,
|
||||||
|
cache_ttl: 3600,
|
||||||
|
verify_add: false,
|
||||||
|
add_all_ips: true,
|
||||||
|
max_ips: 50,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await mikrotikStore.addConfig(config)
|
||||||
|
handleReset()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除配置
|
||||||
|
const handleDelete = async (tag: string) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定要删除 MikroTik 配置 "${tag}" 吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
await mikrotikStore.deleteConfig(tag)
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const handleReset = () => {
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
ElMessage.info('表单已清空')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
mikrotikStore.fetchConfigs()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mikrotik-view">
|
||||||
|
<!-- 配置列表 -->
|
||||||
|
<el-card shadow="hover" class="mb-20">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>📋 已添加的 MikroTik 配置</span>
|
||||||
|
<el-button type="primary" :icon="Refresh" @click="mikrotikStore.fetchConfigs">
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table :data="mikrotikStore.configs" v-loading="mikrotikStore.loading" stripe>
|
||||||
|
<el-table-column prop="tag" label="配置标签" width="200" />
|
||||||
|
<el-table-column label="主机地址" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.args.host }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="端口" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.args.port }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="用户名" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.args.username }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="地址列表" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.args.address_list4 }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="域名文件" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tooltip :content="row.args.domain_files.join(', ')" placement="top">
|
||||||
|
<span>{{ row.args.domain_files[0] }}</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="100" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="danger" size="small" :icon="Delete" @click="handleDelete(row.tag)">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-empty v-if="!mikrotikStore.loading && mikrotikStore.configs.length === 0" description="暂无配置" />
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 添加表单 -->
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>➕ 添加 MikroTik 配置</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="140px">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="配置标签" prop="tag">
|
||||||
|
<el-input v-model="form.tag" placeholder="例如: mikrotik_openai">
|
||||||
|
<template #append>*</template>
|
||||||
|
</el-input>
|
||||||
|
<template #help>
|
||||||
|
<span class="form-hint">唯一标识,建议使用 mikrotik_ 前缀</span>
|
||||||
|
</template>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="地址列表名" prop="addresslist">
|
||||||
|
<el-input v-model="form.addresslist" placeholder="例如: OpenAI">
|
||||||
|
<template #append>*</template>
|
||||||
|
</el-input>
|
||||||
|
<template #help>
|
||||||
|
<span class="form-hint">MikroTik 中的地址列表名称</span>
|
||||||
|
</template>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="MikroTik 地址" prop="host">
|
||||||
|
<el-input v-model="form.host" placeholder="例如: 10.248.0.1">
|
||||||
|
<template #append>*</template>
|
||||||
|
</el-input>
|
||||||
|
<template #help>
|
||||||
|
<span class="form-hint">MikroTik 设备的 IP 地址</span>
|
||||||
|
</template>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="API 端口" prop="port">
|
||||||
|
<el-input-number v-model="form.port" :min="1" :max="65535" style="width: 100%" />
|
||||||
|
<template #help>
|
||||||
|
<span class="form-hint">默认 API 端口为 9728</span>
|
||||||
|
</template>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="用户名" prop="username">
|
||||||
|
<el-input v-model="form.username">
|
||||||
|
<template #append>*</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="密码" prop="password">
|
||||||
|
<el-input v-model="form.password" type="password" show-password>
|
||||||
|
<template #append>*</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-form-item label="域名文件路径" prop="domainFile">
|
||||||
|
<el-input
|
||||||
|
v-model="form.domainFile"
|
||||||
|
placeholder="例如: /usr/local/yltx-dns/config/openai.txt 或 ./mikrotik/openai.txt"
|
||||||
|
>
|
||||||
|
<template #append>*</template>
|
||||||
|
</el-input>
|
||||||
|
<template #help>
|
||||||
|
<span class="form-hint">支持绝对路径和相对路径(相对于运行目录)</span>
|
||||||
|
</template>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :icon="Plus" size="large" @click="handleSave">
|
||||||
|
保存配置
|
||||||
|
</el-button>
|
||||||
|
<el-button size="large" @click="handleReset">清空表单</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mikrotik-view {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-20 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-hint {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
13
web-ui/tsconfig.app.json
Normal file
13
web-ui/tsconfig.app.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
|
"exclude": ["src/**/__tests__/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
web-ui/tsconfig.json
Normal file
11
web-ui/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
20
web-ui/tsconfig.node.json
Normal file
20
web-ui/tsconfig.node.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"include": [
|
||||||
|
"vite.config.*",
|
||||||
|
"vitest.config.*",
|
||||||
|
"cypress.config.*",
|
||||||
|
"nightwatch.conf.*",
|
||||||
|
"playwright.config.*",
|
||||||
|
"eslint.config.*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true
|
||||||
|
}
|
||||||
|
}
|
||||||
37
web-ui/vite.config.ts
Normal file
37
web-ui/vite.config.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:5555',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
assetsDir: 'static',
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id) {
|
||||||
|
// 将所有 node_modules 的依赖打包到 vendor
|
||||||
|
if (id.includes('node_modules')) {
|
||||||
|
return 'vendor'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
6
web_embed.go
Normal file
6
web_embed.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed all:web-ui/dist
|
||||||
|
var WebUIFS embed.FS
|
||||||
Loading…
Reference in New Issue
Block a user