mirror of
https://github.com/jeessy2/ddns-go.git
synced 2025-10-20 15:33:46 +08:00
refactor: 重构前端代码 (#988)
* refactor: 重构前端代码 几乎完全重构前端js代码 小幅度重构后端代码(主要为了适配前端) * fix: 修复主题色无法自适应的bug 还原了主题色修改函数的write参数,去除该参数会导致自适应修改主题时也会写入localStorage * feat: 自动修改标签的for属性 自动修改”获取 IP 方式“标签的for属性,保证其始终指向当前可编辑的元素 * refactor: 去除JQuery * refactor: 按业务划分代码 * fix: 对空值兼容 * fix: 统一html命名 * feat: 默认选择最后一个配置 * fix: 删除冗余代码 * fix: Update writing.html
This commit is contained in:
@ -39,7 +39,7 @@ type DnsConfig struct {
|
||||
URL string
|
||||
NetInterface string
|
||||
Cmd string
|
||||
IPv6Reg string // ipv6匹配正则表达式
|
||||
Ipv6Reg string // ipv6匹配正则表达式
|
||||
Domains []string
|
||||
}
|
||||
DNS DNS
|
||||
@ -291,10 +291,10 @@ func (conf *DnsConfig) getIpv6AddrFromInterface() string {
|
||||
|
||||
for _, netInterface := range ipv6 {
|
||||
if netInterface.Name == conf.Ipv6.NetInterface && len(netInterface.Address) > 0 {
|
||||
if conf.Ipv6.IPv6Reg != "" {
|
||||
if conf.Ipv6.Ipv6Reg != "" {
|
||||
// 匹配第几个IPv6
|
||||
if match, err := regexp.MatchString("@\\d", conf.Ipv6.IPv6Reg); err == nil && match {
|
||||
num, err := strconv.Atoi(conf.Ipv6.IPv6Reg[1:])
|
||||
if match, err := regexp.MatchString("@\\d", conf.Ipv6.Ipv6Reg); err == nil && match {
|
||||
num, err := strconv.Atoi(conf.Ipv6.Ipv6Reg[1:])
|
||||
if err == nil {
|
||||
if num > 0 {
|
||||
if num <= len(netInterface.Address) {
|
||||
@ -303,14 +303,14 @@ func (conf *DnsConfig) getIpv6AddrFromInterface() string {
|
||||
util.Log("未找到第 %d 个IPv6地址! 将使用第一个IPv6地址", num)
|
||||
return netInterface.Address[0]
|
||||
}
|
||||
util.Log("IPv6匹配表达式 %s 不正确! 最小从1开始", conf.Ipv6.IPv6Reg)
|
||||
util.Log("IPv6匹配表达式 %s 不正确! 最小从1开始", conf.Ipv6.Ipv6Reg)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
// 正则表达式匹配
|
||||
util.Log("IPv6将使用正则表达式 %s 进行匹配", conf.Ipv6.IPv6Reg)
|
||||
util.Log("IPv6将使用正则表达式 %s 进行匹配", conf.Ipv6.Ipv6Reg)
|
||||
for i := 0; i < len(netInterface.Address); i++ {
|
||||
matched, err := regexp.MatchString(conf.Ipv6.IPv6Reg, netInterface.Address[i])
|
||||
matched, err := regexp.MatchString(conf.Ipv6.Ipv6Reg, netInterface.Address[i])
|
||||
if matched && err == nil {
|
||||
util.Log("匹配成功! 匹配到地址: ", netInterface.Address[i])
|
||||
return netInterface.Address[i]
|
||||
|
2
main.go
2
main.go
@ -162,8 +162,6 @@ func runWebServer() error {
|
||||
http.HandleFunc("/save", web.BasicAuth(web.Save))
|
||||
http.HandleFunc("/logs", web.BasicAuth(web.Logs))
|
||||
http.HandleFunc("/clearLog", web.BasicAuth(web.ClearLog))
|
||||
http.HandleFunc("/ipv4NetInterface", web.BasicAuth(web.Ipv4NetInterfaces))
|
||||
http.HandleFunc("/ipv6NetInterface", web.BasicAuth(web.Ipv6NetInterfaces))
|
||||
http.HandleFunc("/webhookTest", web.BasicAuth(web.WebhookTest))
|
||||
|
||||
util.Log("监听 %s", *listen)
|
||||
|
@ -146,11 +146,11 @@ main {
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .theme-button:hover {
|
||||
.theme-button:hover {
|
||||
box-shadow: 0px 0px 15px #0d0d0dab;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .theme-button:active {
|
||||
.theme-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
|
265
static/constant.js
Normal file
265
static/constant.js
Normal file
@ -0,0 +1,265 @@
|
||||
const DNS_PROVIDERS = {
|
||||
alidns: {
|
||||
name: {
|
||||
"en": "Aliyun",
|
||||
"zh-cn": "阿里云",
|
||||
},
|
||||
idLabel: "AccessKey ID",
|
||||
secretLabel: "AccessKey Secret",
|
||||
helpHtml: {
|
||||
"en": "<a target='_blank' href='https://ram.console.aliyun.com/manage/ak?spm=5176.12818093.nav-right.dak.488716d0mHaMgg'>Create AccessKey</a>",
|
||||
"zh-cn": "<a target='_blank' href='https://ram.console.aliyun.com/manage/ak?spm=5176.12818093.nav-right.dak.488716d0mHaMgg'>创建 AccessKey</a>",
|
||||
}
|
||||
},
|
||||
tencentcloud: {
|
||||
name: {
|
||||
"en": "Tencent",
|
||||
"zh-cn": "腾讯云",
|
||||
},
|
||||
idLabel: "SecretId",
|
||||
secretLabel: "SecretKey",
|
||||
helpHtml: {
|
||||
"en": "<a target='_blank' href='https://console.dnspod.cn/account/token/apikey'>Create AccessKey</a>",
|
||||
"zh-cn": "<a target='_blank' href='https://console.dnspod.cn/account/token/apikey'>创建腾讯云 API 密钥</a>",
|
||||
}
|
||||
},
|
||||
dnspod: {
|
||||
name: {
|
||||
"en": "DnsPod",
|
||||
},
|
||||
idLabel: "ID",
|
||||
secretLabel: "Token",
|
||||
helpHtml: {
|
||||
"en": "<a target='_blank' href='https://console.dnspod.cn/account/token/token'>Create Token</a>",
|
||||
"zh-cn": "<a target='_blank' href='https://console.dnspod.cn/account/token/token'>创建 DNSPod Token</a>",
|
||||
}
|
||||
},
|
||||
cloudflare: {
|
||||
name: {
|
||||
"en": "Cloudflare",
|
||||
},
|
||||
idLabel: "",
|
||||
secretLabel: "Token",
|
||||
helpHtml: {
|
||||
"en": "<a target='_blank' href='https://dash.cloudflare.com/profile/api-tokens'>Create Token -> Edit Zone DNS (Use template)</a>",
|
||||
"zh-cn": "<a target='_blank' href='https://dash.cloudflare.com/profile/api-tokens'>创建令牌 -> 编辑区域 DNS (使用模板)</a>",
|
||||
}
|
||||
},
|
||||
huaweicloud: {
|
||||
name: {
|
||||
"en": "Huawei",
|
||||
"zh-cn": "华为云",
|
||||
},
|
||||
idLabel: "Access Key Id",
|
||||
secretLabel: "Secret Access Key",
|
||||
helpHtml: {
|
||||
"en": "<a target='_blank' href='https://console.huaweicloud.com/iam/?locale=zh-cn#/mine/accessKey'>Create</a>",
|
||||
"zh-cn": "<a target='_blank' href='https://console.huaweicloud.com/iam/?locale=zh-cn#/mine/accessKey'>新增访问密钥</a>",
|
||||
}
|
||||
},
|
||||
callback: {
|
||||
name: {
|
||||
"en": "Callback",
|
||||
},
|
||||
idLabel: "URL",
|
||||
secretLabel: "RequestBody",
|
||||
helpHtml: {
|
||||
"en": "<a target='_blank' href='https://github.com/jeessy2/ddns-go/blob/master/README_EN.md#callback'>Callback</a> Support variables #{ip}, #{domain}, #{recordType}, #{ttl}",
|
||||
"zh-cn": "<a target='_blank' href='https://github.com/jeessy2/ddns-go#callback'>自定义回调</a> 支持的变量 #{ip}, #{domain}, #{recordType}, #{ttl}",
|
||||
}
|
||||
},
|
||||
baiducloud: {
|
||||
name: {
|
||||
"en": "Baidu",
|
||||
"zh-cn": "百度云",
|
||||
},
|
||||
idLabel: "AccessKey ID",
|
||||
secretLabel: "AccessKey Secret",
|
||||
helpHtml: {
|
||||
"en": "<a target='_blank' href='https://console.bce.baidu.com/iam/?_=1651763238057#/iam/accesslist'>Create AccessKey</a><br /><a target='_blank' href='https://ticket.bce.baidu.com/#/ticket/create~productId=60&questionId=393&channel=2'>Apply for a ticket</a> DDNS needs to call the API, and the related APIs of Baidu Cloud are only open to users who have applied. Please submit a ticket before using it.",
|
||||
"zh-cn": "<a target='_blank' href='https://console.bce.baidu.com/iam/?_=1651763238057#/iam/accesslist'>创建 AccessKey</a><br /><a target='_blank' href='https://ticket.bce.baidu.com/#/ticket/create~productId=60&questionId=393&channel=2'>申请工单</a> DDNS 需调用 API ,而百度云相关 API 仅对申请用户开放,使用前请先提交工单申请。",
|
||||
}
|
||||
},
|
||||
porkbun: {
|
||||
name: {
|
||||
"en": "Porkbun",
|
||||
},
|
||||
idLabel: "API Key",
|
||||
secretLabel: "Secret Key",
|
||||
helpHtml: {
|
||||
"en": "<a target='_blank' href='https://porkbun.com/account/api'>Create Access</a>",
|
||||
"zh-cn": "<a target='_blank' href='https://porkbun.com/account/api'>创建 Access</a>",
|
||||
}
|
||||
},
|
||||
godaddy: {
|
||||
name: {
|
||||
"en": "GoDaddy",
|
||||
},
|
||||
idLabel: "Key",
|
||||
secretLabel: "Secret",
|
||||
helpHtml: {
|
||||
"en": "<a target='_blank' href='https://porkbun.com/account/api'>Create API KEY</a>",
|
||||
"zh-cn": "<a target='_blank' href='https://developer.godaddy.com/keys'>创建 API KEY</a>",
|
||||
}
|
||||
},
|
||||
googledomain: {
|
||||
name: {
|
||||
"en": "Google Domain",
|
||||
},
|
||||
idLabel: "Username",
|
||||
secretLabel: "Password",
|
||||
helpHtml: {
|
||||
"en": "<a target='_blank' href='https://support.google.com/domains/answer/6147083?hl=en'>How to get started</a>",
|
||||
"zh-cn": "<a target='_blank' href='https://support.google.com/domains/answer/6147083?hl=zh-Hans'>新建动态域名解析记录</a>",
|
||||
}
|
||||
},
|
||||
namecheap: {
|
||||
name: {
|
||||
"en": "Namecheap",
|
||||
},
|
||||
idLabel: "",
|
||||
secretLabel: "Password",
|
||||
helpHtml: {
|
||||
"en": "<a target='_blank' href='https://www.namecheap.com/support/knowledgebase/article.aspx/36/11/how-do-i-start-using-dynamic-dns/'>How to get started</a> <span style='color: red'>Namecheap DDNS does not support updating IPv6</span>",
|
||||
"zh-cn": "<a target='_blank' href='https://www.namecheap.com/support/knowledgebase/article.aspx/36/11/how-do-i-start-using-dynamic-dns/'>开启namecheap动态域名解析</a> <span style='color: red'>Namecheap DDNS 不支持更新 IPv6</span>",
|
||||
}
|
||||
},
|
||||
namesilo: {
|
||||
name: {
|
||||
"en": "NameSilo",
|
||||
},
|
||||
idLabel: "",
|
||||
secretLabel: "Password",
|
||||
helpHtml: {
|
||||
"en": "<a target='_blank' href='https://www.namesilo.com/account/api-manager'>How to get started</a> <b>Please note that the TTL of namesilo is at least 1 hour</b>",
|
||||
"zh-cn": "<a target='_blank' href='https://www.namesilo.com/account/api-manager'>开启namesilo动态域名解析</a> <b>请注意namesilo的TTL最低1小时</b>",
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const SVG_CODE = {
|
||||
success: `<svg viewBox="64 64 896 896" focusable="false" data-icon="check-circle" width="1em" height="1em" fill="#52c41a" aria-hidden="true"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"></path></svg>`,
|
||||
info: `<svg viewBox="64 64 896 896" focusable="false" data-icon="info-circle" width="1em" height="1em" fill="#1677ff" aria-hidden="true"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm32 664c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V456c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272zm-32-344a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"></path></svg>`,
|
||||
warning: '<svg viewBox="64 64 896 896" focusable="false" data-icon="exclamation-circle" width="1em" height="1em" fill="#faad14" aria-hidden="true"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"></path></svg>',
|
||||
error: '<svg viewBox="64 64 896 896" focusable="false" data-icon="close-circle" width="1em" height="1em" fill="#ff4d4f" aria-hidden="true"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"></path></svg>'
|
||||
}
|
||||
|
||||
|
||||
const I18N_MAP = {
|
||||
'en': {
|
||||
'Logs': 'Logs',
|
||||
'Save': 'Save',
|
||||
'Config:': 'Config:',
|
||||
'Add': 'Add',
|
||||
'Delete': 'Delete',
|
||||
'DNS Provider': 'DNS Provider',
|
||||
'Create AccessKey': 'Create AccessKey',
|
||||
'Auto': 'Auto',
|
||||
'1s': '1s',
|
||||
'5s': '5s',
|
||||
'10s': '10s',
|
||||
'1m': '1m',
|
||||
'2m': '2m',
|
||||
'10m': '10m',
|
||||
'30m': '30m',
|
||||
'1h': '1h',
|
||||
'ttlHelp': 'You can modify it if the account supports a smaller TTL. The TTL will only be updated when the IP changes',
|
||||
'Enabled': 'Enabled',
|
||||
'Get IP method': 'Get IP method',
|
||||
'By api': 'By api',
|
||||
'By network card': 'By network card',
|
||||
'By command': 'By command',
|
||||
'domainsHelp': `
|
||||
One domain per line, you can use colon to separate the root domain
|
||||
(example.cn.eu.org) and the subdomain (www), fill in as: <b>www:example.cn.eu.org</b>
|
||||
`,
|
||||
'Regular exp.': 'Regular exp.',
|
||||
'regHelp': 'You can use @1 to specify the first IPv6 address, @2 to specify the second IPv6 address... You can also use regular expressions to match the specified IPv6 address, leave it blank to disable it',
|
||||
'Others': 'Others',
|
||||
'Deny from WAN': 'Deny from WAN',
|
||||
'NotAllowWanAccessHelp': 'Default enabled, can prohibit access to this page from the public network',
|
||||
'Username': 'Username',
|
||||
'accountHelp': 'Please enter to protect your information security',
|
||||
'Password': 'Password',
|
||||
'WebhookURLHelp': `
|
||||
<a
|
||||
target="blank"
|
||||
href="https://github.com/jeessy2/ddns-go/blob/master/README_EN.md#webhook"
|
||||
>Click to get more info</a
|
||||
><br />
|
||||
Support variables #{ipv4Addr}, #{ipv4Result},
|
||||
#{ipv4Domains}, #{ipv6Addr}, #{ipv6Result}, #{ipv6Domains}
|
||||
`,
|
||||
'WebhookRequestBodyHelp': 'If RequestBody is empty, it is a GET request, otherwise it is a POST request. Supported variables are the same as above',
|
||||
'WebhookHeadersHelp': 'One header per line, such as: Authorization: Bearer API_KEY',
|
||||
'Try it': 'Try it',
|
||||
'Clear': 'Clear',
|
||||
'OK': 'OK',
|
||||
"Ipv4UrlHelp": "https://myip4.ipip.net, https://ddns.oray.com/checkip, https://ip.3322.net",
|
||||
"Ipv6UrlHelp": "https://speed.neu6.edu.cn/getIP.php, https://v6.ident.me, https://6.ipw.cn",
|
||||
"Ipv4NetInterfaceHelp": "Get IPv4 address through network card",
|
||||
"Ipv6NetInterfaceHelp": "If you do not specify a matching regular expression, the first IPv6 address will be used by default",
|
||||
"Ipv4CmdHelp": "Get IPv4 through command, only use the first matching IPv4 address of standard output(stdout). Such as: ip -4 addr show eth1",
|
||||
"Ipv6CmdHelp": "Get IPv6 through command, only use the first matching IPv6 address of standard output(stdout). Such as: ip -6 addr show eth1",
|
||||
"NetInterfaceEmptyHelp": '<span style="color: red">No available network card found</span>',
|
||||
},
|
||||
'zh-cn': {
|
||||
'Logs': '日志',
|
||||
'Save': '保存',
|
||||
'Config:': '配置切换:',
|
||||
'Add': '添加',
|
||||
'Delete': '删除',
|
||||
'DNS Provider': 'DNS服务商',
|
||||
'Create AccessKey': '创建 AccessKey',
|
||||
'Auto': '自动',
|
||||
'1s': '1秒',
|
||||
'5s': '5秒',
|
||||
'10s': '10秒',
|
||||
'1m': '1分钟',
|
||||
'2m': '2分钟',
|
||||
'10m': '10分钟',
|
||||
'30m': '30分钟',
|
||||
'1h': '1小时',
|
||||
'ttlHelp': '如账号支持更小的 TTL, 可修改。IP 有变化时才会更新TTL',
|
||||
'Enabled': '是否启用',
|
||||
'Get IP method': '获取 IP 方式',
|
||||
'By api': '通过接口获取',
|
||||
'By network card': '通过网卡获取',
|
||||
'By command': '通过命令获取',
|
||||
'domainsHelp': `
|
||||
一行一个域名, 可使用冒号分隔根域名(example.cn.eu.org)与子域名(www), 填写为:<b>www:example.cn.eu.org</b>
|
||||
<a target="blank" href="https://github.com/jeessy2/ddns-go/wiki/传递自定义参数">支持传递自定义参数</a>
|
||||
`,
|
||||
'Regular exp.': '匹配正则表达式',
|
||||
'regHelp': '可使用 @1 指定第一个IPv6地址, @2 指定第二个IPv6地址... 也可使用正则表达式匹配指定的IPv6地址, 留空则不启用',
|
||||
'Others': '其他',
|
||||
'Deny from WAN': '禁止公网访问',
|
||||
'NotAllowWanAccessHelp': '默认启用, 可禁止从公网访问本页面',
|
||||
'Username': '用户名',
|
||||
'accountHelp': '为保护你的信息安全,建议输入',
|
||||
'Password': '密码',
|
||||
'WebhookURLHelp': `
|
||||
<a target="blank" href="https://github.com/jeessy2/ddns-go#webhook">点击参考官方 Webhook 说明</a>
|
||||
<br />
|
||||
支持的变量 #{ipv4Addr}, #{ipv4Result}, #{ipv4Domains}, #{ipv6Addr}, #{ipv6Result}, #{ipv6Domains}
|
||||
`,
|
||||
'WebhookRequestBodyHelp': '如果 RequestBody 为空, 则为 GET 请求, 否则为 POST 请求。支持的变量同上',
|
||||
'WebhookHeadersHelp': '一行一个Header, 如: Authorization: Bearer API_KEY',
|
||||
'Try it': '模拟测试Webhook',
|
||||
'Clear': '清空',
|
||||
'OK': '确定',
|
||||
"Ipv4UrlHelp": "https://myip4.ipip.net, https://ddns.oray.com/checkip, https://ip.3322.net",
|
||||
"Ipv6UrlHelp": "https://speed.neu6.edu.cn/getIP.php, https://v6.ident.me, https://6.ipw.cn",
|
||||
"Ipv4NetInterfaceHelp": "通过网卡获取IPv4",
|
||||
"Ipv6NetInterfaceHelp": "如不指定匹配正则表达式,将默认使用第一个 IPv6 地址",
|
||||
"Ipv4CmdHelp": `
|
||||
通过命令获取IPv4, 仅使用标准输出(stdout)的第一个匹配的 IPv4 地址。如: ip -4 addr show eth1
|
||||
<a target="blank" href="https://github.com/jeessy2/ddns-go/wiki/通过命令获取IP参考">点击参考更多</a>
|
||||
`,
|
||||
"Ipv6CmdHelp": `
|
||||
通过命令获取IPv6, 仅使用标准输出(stdout)的第一个匹配的 IPv6 地址。如: ip -6 addr show eth1
|
||||
<a target="blank" href="https://github.com/jeessy2/ddns-go/wiki/通过命令获取IP参考">点击参考更多</a>
|
||||
`,
|
||||
"NetInterfaceEmptyHelp": '<span style="color: red">没有找到可用的网卡</span>',
|
||||
}
|
||||
};
|
49
static/i18n.js
Normal file
49
static/i18n.js
Normal file
@ -0,0 +1,49 @@
|
||||
const LANG = localStorage.getItem('lang') || (navigator.language || navigator.browserLanguage).replaceAll('_', '-').toLowerCase();
|
||||
|
||||
// 支持两种调用方式:
|
||||
// 1. 文本的key + (可选:语言映射字典),{en: {hello: "hello", world: "world"}, zh: {hello: "你好", world: "世界"}}
|
||||
// 2. 语言字符串字典,{en: "hello", zh: "你好"}
|
||||
const i18n = (key, langMap = I18N_MAP) => {
|
||||
if (typeof key !== 'string') {
|
||||
langMap = key;
|
||||
key = null;
|
||||
}
|
||||
// 优先取地区语言,否则取表示语言,再否则取表示语言相同的地区语言,最后取英文
|
||||
let lang = 'en';
|
||||
if (LANG in langMap) {
|
||||
lang = LANG;
|
||||
} else if (LANG.split('-')[0] in langMap) {
|
||||
lang = LANG.split('-')[0];
|
||||
} else {
|
||||
for (const l in langMap) {
|
||||
if (l.split('-')[0] === LANG.split('-')[0]) {
|
||||
lang = l;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let text = '';
|
||||
if (key) {
|
||||
text = langMap[lang][key];
|
||||
} else {
|
||||
text = langMap[lang];
|
||||
}
|
||||
if (text === undefined) {
|
||||
console.warn(`i18n: No translation for ${key}`);
|
||||
return key;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
const convertDom = (dom = document, ...args) => {
|
||||
dom.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
const key = el.dataset.i18n;
|
||||
el.textContent = i18n(key, ...args);
|
||||
});
|
||||
dom.querySelectorAll('[data-i18n_html]').forEach(el => {
|
||||
const key = el.dataset.i18n_html;
|
||||
el.innerHTML = i18n(key, ...args);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {convertDom();});
|
117
static/utils.js
117
static/utils.js
@ -1,71 +1,110 @@
|
||||
// 常量资源
|
||||
const SVG_CODE = {
|
||||
success: `<svg viewBox="64 64 896 896" focusable="false" data-icon="check-circle" width="1em" height="1em" fill="#52c41a" aria-hidden="true"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"></path></svg>`,
|
||||
info: `<svg viewBox="64 64 896 896" focusable="false" data-icon="info-circle" width="1em" height="1em" fill="#1677ff" aria-hidden="true"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm32 664c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V456c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272zm-32-344a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"></path></svg>`,
|
||||
warning: '<svg viewBox="64 64 896 896" focusable="false" data-icon="exclamation-circle" width="1em" height="1em" fill="#faad14" aria-hidden="true"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"></path></svg>',
|
||||
error: '<svg viewBox="64 64 896 896" focusable="false" data-icon="close-circle" width="1em" height="1em" fill="#ff4d4f" aria-hidden="true"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"></path></svg>'
|
||||
}
|
||||
|
||||
|
||||
|
||||
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
const html2Element = (htmlString) => {
|
||||
const doc = new DOMParser().parseFromString(htmlString, 'text/html')
|
||||
return doc.body.firstElementChild
|
||||
}
|
||||
|
||||
// 在页面顶部显示一行消息,并在若干秒后自动消失
|
||||
const showMessage = async (msgObj) => {
|
||||
// 填充默认值
|
||||
msgObj = Object.assign({
|
||||
type: 'info',
|
||||
content: '',
|
||||
html: false,
|
||||
duration: 3000
|
||||
}, msgObj)
|
||||
// 当前是否有消息容器
|
||||
let $container = document.querySelector('#msg-container')
|
||||
let $container = document.getElementById('msg-container')
|
||||
if (!$container) {
|
||||
// 创建消息容器
|
||||
$container = document.createElement('div')
|
||||
$container.id = 'msg-container'
|
||||
$container = html2Element('<div id="msg-container"></div>')
|
||||
document.body.appendChild($container)
|
||||
}
|
||||
// 创建消息元素
|
||||
const $msg = document.createElement('div')
|
||||
const $msg = html2Element('<div class="msg msg-fade"></div>')
|
||||
// 创建两个span,用于显示消息的图标和内容
|
||||
const $icon = document.createElement('span')
|
||||
const $content = document.createElement('span')
|
||||
$icon.classList.add('msg-icon')
|
||||
const $content = html2Element('<span></span>')
|
||||
|
||||
// 填充内容,根据html属性决定使用text还是html
|
||||
if (msgObj.html) {
|
||||
$content.innerHTML = msgObj.content
|
||||
} else {
|
||||
$content.textContent = msgObj.content
|
||||
}
|
||||
// 根据消息类型设置图标
|
||||
$icon.innerHTML = SVG_CODE[msgObj.type] || SVG_CODE.info
|
||||
$content.innerText = msgObj.content || ''
|
||||
$msg.appendChild($icon)
|
||||
$msg.innerHTML = `<span class="msg-icon">${SVG_CODE[msgObj.type]}</span>`
|
||||
$msg.appendChild($content)
|
||||
// 增加出现动画
|
||||
$msg.classList.add('msg','msg-fade')
|
||||
$container.appendChild($msg)
|
||||
// 0延迟是为了让剩余的代码存入异步队列,稍后执行。否则浏览器会把两步操作合并,导致动画不生效
|
||||
// 确保动画生效
|
||||
await delay(0)
|
||||
$msg.classList.remove('msg-fade')
|
||||
// 等待动画结束
|
||||
await delay(200)
|
||||
// 消失函数
|
||||
const disappear = async () => {
|
||||
// 清除计时器
|
||||
clearTimeout(timer)
|
||||
// 销毁函数
|
||||
const destroy = async () => {
|
||||
// 增加消失动画
|
||||
$msg.classList.add('msg-fade')
|
||||
// 动画结束后移除元素
|
||||
await delay(200);
|
||||
$container.removeChild($msg)
|
||||
await delay(200)
|
||||
$msg.remove()
|
||||
// 如果容器中没有消息了,移除容器
|
||||
if ($container.children.length === 0) {
|
||||
document.body.removeChild($container)
|
||||
if (!$container.children.length) {
|
||||
$container.remove()
|
||||
}
|
||||
}
|
||||
// 如果duration为0,则不自动消失
|
||||
if (msgObj.duration === 0) {
|
||||
return disappear
|
||||
return destroy
|
||||
}
|
||||
// 自动消失计时器
|
||||
let timer = setTimeout(disappear, msgObj.duration || 3000)
|
||||
let timer = setTimeout(destroy, msgObj.duration)
|
||||
// 注册鼠标事件,鼠标移入时取消自动消失
|
||||
$msg.onmouseenter = () => {
|
||||
$msg.addEventListener('mouseenter', () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
})
|
||||
// 鼠标移出时重新计时
|
||||
$msg.onmouseleave = () => {
|
||||
timer = setTimeout(disappear, msgObj.duration || 3000)
|
||||
}
|
||||
return disappear
|
||||
$msg.addEventListener('mouseleave', () => {
|
||||
timer = setTimeout(destroy, msgObj.duration)
|
||||
})
|
||||
return destroy
|
||||
}
|
||||
|
||||
const request = {
|
||||
baseURL: './',
|
||||
parse: async function(resp) {
|
||||
const text = await resp.text()
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch (e) {
|
||||
return text
|
||||
}
|
||||
},
|
||||
stringify: function(dict) {
|
||||
const result = []
|
||||
for (let key in dict) {
|
||||
if (!dict.hasOwnProperty(key)) {
|
||||
continue
|
||||
}
|
||||
// 所有空值将被删除
|
||||
if (String(dict[key])) {
|
||||
result.push(`${key}=${encodeURIComponent(dict[key])}`)
|
||||
}
|
||||
}
|
||||
return result.join('&')
|
||||
},
|
||||
get: async function(path, data, parseFunc) {
|
||||
const response = await fetch(`${this.baseURL}${path}?${this.stringify(data)}`)
|
||||
return await (parseFunc||this.parse)(response)
|
||||
},
|
||||
post: async function(path, data, parseFunc) {
|
||||
if (typeof data === 'object') {
|
||||
data = JSON.stringify(data)
|
||||
}
|
||||
const response = await fetch(`${this.baseURL}${path}`, {
|
||||
method: 'POST',
|
||||
body: data
|
||||
})
|
||||
return await (parseFunc||this.parse)(response)
|
||||
}
|
||||
}
|
@ -56,6 +56,7 @@ func init() {
|
||||
message.SetString(language.English, "启用外网访问, 必须输入登录用户名/密码", "Enable external network access, you must enter the login username/password")
|
||||
message.SetString(language.English, "修改 '通过命令获取' 必须设置帐号密码,请先设置帐号密码", "Modify 'Get by command' must set username/password, please set username/password first")
|
||||
message.SetString(language.English, "密码不安全!尝试使用更长的密码", "insecure password, try using a longer password")
|
||||
message.SetString(language.English, "数据解析失败, 请刷新页面重试", "Data parsing failed, please refresh the page and try again")
|
||||
|
||||
// config
|
||||
message.SetString(language.English, "从网卡获得IPv4失败", "Get IPv4 from network card failed")
|
||||
|
@ -34,12 +34,7 @@ func init() {
|
||||
// Logs web
|
||||
func Logs(writer http.ResponseWriter, request *http.Request) {
|
||||
// mlogs.Logs数组转为json
|
||||
logs, err := json.Marshal(mlogs.Logs)
|
||||
if err != nil {
|
||||
// 返回错误代码
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
logs, _ := json.Marshal(mlogs.Logs)
|
||||
writer.Write(logs)
|
||||
}
|
||||
|
||||
|
@ -1,32 +0,0 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/jeessy2/ddns-go/v6/config"
|
||||
)
|
||||
|
||||
// Ipv4NetInterfaces 获得Ipv4网卡信息
|
||||
func Ipv4NetInterfaces(writer http.ResponseWriter, request *http.Request) {
|
||||
ipv4, _, err := config.GetNetInterface()
|
||||
if len(ipv4) > 0 && err == nil {
|
||||
byt, err := json.Marshal(ipv4)
|
||||
if err == nil {
|
||||
writer.Write(byt)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ipv6NetInterfaces 获得Ipv6网卡信息
|
||||
func Ipv6NetInterfaces(writer http.ResponseWriter, request *http.Request) {
|
||||
_, ipv6, err := config.GetNetInterface()
|
||||
if len(ipv6) > 0 && err == nil {
|
||||
byt, err := json.Marshal(ipv6)
|
||||
if err == nil {
|
||||
writer.Write(byt)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
44
web/save.go
44
web/save.go
@ -28,9 +28,26 @@ func Save(writer http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
func checkAndSave(request *http.Request) string {
|
||||
conf, err := config.GetConfigCached()
|
||||
usernameNew := strings.TrimSpace(request.FormValue("Username"))
|
||||
passwordNew := request.FormValue("Password")
|
||||
conf, _ := config.GetConfigCached()
|
||||
|
||||
// 从请求中读取 JSON 数据
|
||||
var data struct {
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password"`
|
||||
NotAllowWanAccess bool `json:"NotAllowWanAccess"`
|
||||
WebhookURL string `json:"WebhookURL"`
|
||||
WebhookRequestBody string `json:"WebhookRequestBody"`
|
||||
WebhookHeaders string `json:"WebhookHeaders"`
|
||||
DnsConf []dnsConf4JS `json:"DnsConf"`
|
||||
}
|
||||
|
||||
// 解析请求中的 JSON 数据
|
||||
err := json.NewDecoder(request.Body).Decode(&data)
|
||||
if err != nil {
|
||||
return util.LogStr("数据解析失败, 请刷新页面重试")
|
||||
}
|
||||
usernameNew := strings.TrimSpace(data.Username)
|
||||
passwordNew := data.Password
|
||||
|
||||
// 国际化
|
||||
accept := request.Header.Get("Accept-Language")
|
||||
@ -52,15 +69,14 @@ func checkAndSave(request *http.Request) string {
|
||||
(usernameNew != "" || passwordNew != "") {
|
||||
return util.LogStr("若从未设置过帐号密码, 仅允许在ddns-go启动后 5 分钟内设置, 请重启ddns-go")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
conf.NotAllowWanAccess = request.FormValue("NotAllowWanAccess") == "on"
|
||||
conf.NotAllowWanAccess = data.NotAllowWanAccess
|
||||
conf.Username = usernameNew
|
||||
conf.Password = passwordNew
|
||||
conf.WebhookURL = strings.TrimSpace(request.FormValue("WebhookURL"))
|
||||
conf.WebhookRequestBody = strings.TrimSpace(request.FormValue("WebhookRequestBody"))
|
||||
conf.WebhookHeaders = strings.TrimSpace(request.FormValue("WebhookHeaders"))
|
||||
conf.WebhookURL = strings.TrimSpace(data.WebhookURL)
|
||||
conf.WebhookRequestBody = strings.TrimSpace(data.WebhookRequestBody)
|
||||
conf.WebhookHeaders = strings.TrimSpace(data.WebhookHeaders)
|
||||
|
||||
// 如启用公网访问,帐号密码不能为空
|
||||
if !conf.NotAllowWanAccess && (conf.Username == "" || conf.Password == "") {
|
||||
@ -79,11 +95,7 @@ func checkAndSave(request *http.Request) string {
|
||||
}
|
||||
}
|
||||
|
||||
dnsConfFromJS := []dnsConf4JS{}
|
||||
err = json.Unmarshal([]byte(request.FormValue("DnsConf")), &dnsConfFromJS)
|
||||
if err != nil {
|
||||
return "Please refresh the browser and try again"
|
||||
}
|
||||
dnsConfFromJS := data.DnsConf
|
||||
dnsConfArray := []config.DnsConfig{}
|
||||
empty := dnsConf4JS{}
|
||||
for k, v := range dnsConfFromJS {
|
||||
@ -95,7 +107,7 @@ func checkAndSave(request *http.Request) string {
|
||||
dnsConf.DNS.Name = v.DnsName
|
||||
dnsConf.DNS.ID = strings.TrimSpace(v.DnsID)
|
||||
dnsConf.DNS.Secret = strings.TrimSpace(v.DnsSecret)
|
||||
dnsConf.Ipv4.Enable = v.Ipv4Enable == "on"
|
||||
dnsConf.Ipv4.Enable = v.Ipv4Enable
|
||||
dnsConf.Ipv4.GetType = v.Ipv4GetType
|
||||
dnsConf.Ipv4.URL = strings.TrimSpace(v.Ipv4Url)
|
||||
dnsConf.Ipv4.NetInterface = v.Ipv4NetInterface
|
||||
@ -105,12 +117,12 @@ func checkAndSave(request *http.Request) string {
|
||||
} else {
|
||||
dnsConf.Ipv4.Domains = strings.Split(v.Ipv4Domains, "\n")
|
||||
}
|
||||
dnsConf.Ipv6.Enable = v.Ipv6Enable == "on"
|
||||
dnsConf.Ipv6.Enable = v.Ipv6Enable
|
||||
dnsConf.Ipv6.GetType = v.Ipv6GetType
|
||||
dnsConf.Ipv6.URL = strings.TrimSpace(v.Ipv6Url)
|
||||
dnsConf.Ipv6.NetInterface = v.Ipv6NetInterface
|
||||
dnsConf.Ipv6.Cmd = strings.TrimSpace(v.Ipv6Cmd)
|
||||
dnsConf.Ipv6.IPv6Reg = strings.TrimSpace(v.IPv6Reg)
|
||||
dnsConf.Ipv6.Ipv6Reg = strings.TrimSpace(v.Ipv6Reg)
|
||||
if strings.Contains(v.Ipv6Domains, "\r\n") {
|
||||
dnsConf.Ipv6.Domains = strings.Split(v.Ipv6Domains, "\r\n")
|
||||
} else {
|
||||
|
@ -1,19 +1,33 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/jeessy2/ddns-go/v6/util"
|
||||
|
||||
"github.com/jeessy2/ddns-go/v6/config"
|
||||
"github.com/jeessy2/ddns-go/v6/util"
|
||||
)
|
||||
|
||||
// WebhookTest 测试webhook
|
||||
func WebhookTest(writer http.ResponseWriter, request *http.Request) {
|
||||
url := strings.TrimSpace(request.FormValue("URL"))
|
||||
requestBody := strings.TrimSpace(request.FormValue("RequestBody"))
|
||||
webhookHeaders := strings.TrimSpace(request.FormValue("WebhookHeaders"))
|
||||
var data struct {
|
||||
URL string `json:"URL"`
|
||||
RequestBody string `json:"RequestBody"`
|
||||
Headers string `json:"Headers"`
|
||||
}
|
||||
err := json.NewDecoder(request.Body).Decode(&data)
|
||||
if err != nil {
|
||||
util.Log("数据解析失败, 请刷新页面重试")
|
||||
return
|
||||
}
|
||||
|
||||
url := data.URL
|
||||
requestBody := data.RequestBody
|
||||
headers := data.Headers
|
||||
|
||||
if url == "" {
|
||||
util.Log("请输入Webhook的URL")
|
||||
return
|
||||
}
|
||||
|
||||
var domains = make([]*config.Domain, 1)
|
||||
domains[0] = &config.Domain{}
|
||||
@ -32,13 +46,9 @@ func WebhookTest(writer http.ResponseWriter, request *http.Request) {
|
||||
Webhook: config.Webhook{
|
||||
WebhookURL: url,
|
||||
WebhookRequestBody: requestBody,
|
||||
WebhookHeaders: webhookHeaders,
|
||||
WebhookHeaders: headers,
|
||||
},
|
||||
}
|
||||
|
||||
if url != "" {
|
||||
config.ExecWebhook(fakeDomains, fakeConfig)
|
||||
} else {
|
||||
util.Log("请输入Webhook的URL")
|
||||
}
|
||||
config.ExecWebhook(fakeDomains, fakeConfig)
|
||||
}
|
||||
|
@ -17,32 +17,24 @@ var writingEmbedFile embed.FS
|
||||
|
||||
const VersionEnv = "DDNS_GO_VERSION"
|
||||
|
||||
type writingData struct {
|
||||
DnsConf template.JS
|
||||
NotAllowWanAccess string
|
||||
config.User
|
||||
config.Webhook
|
||||
Version string
|
||||
}
|
||||
|
||||
// js中的dns配置
|
||||
type dnsConf4JS struct {
|
||||
DnsName string
|
||||
DnsID string
|
||||
DnsSecret string
|
||||
TTL string
|
||||
Ipv4Enable string
|
||||
Ipv4Enable bool
|
||||
Ipv4GetType string
|
||||
Ipv4Url string
|
||||
Ipv4NetInterface string
|
||||
Ipv4Cmd string
|
||||
Ipv4Domains string
|
||||
Ipv6Enable string
|
||||
Ipv6Enable bool
|
||||
Ipv6GetType string
|
||||
Ipv6Url string
|
||||
Ipv6NetInterface string
|
||||
Ipv6Cmd string
|
||||
IPv6Reg string
|
||||
Ipv6Reg string
|
||||
Ipv6Domains string
|
||||
}
|
||||
|
||||
@ -56,19 +48,34 @@ func Writing(writer http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
conf, err := config.GetConfigCached()
|
||||
|
||||
// 默认禁止公网访问
|
||||
if err != nil {
|
||||
conf.NotAllowWanAccess = true
|
||||
}
|
||||
|
||||
tmpl.Execute(writer, &writingData{
|
||||
ipv4, ipv6, _ := config.GetNetInterface()
|
||||
|
||||
err = tmpl.Execute(writer, struct {
|
||||
DnsConf template.JS
|
||||
NotAllowWanAccess bool
|
||||
config.User
|
||||
config.Webhook
|
||||
Version string
|
||||
Ipv4 []config.NetInterface
|
||||
Ipv6 []config.NetInterface
|
||||
}{
|
||||
DnsConf: template.JS(getDnsConfStr(conf.DnsConf)),
|
||||
NotAllowWanAccess: BooltoOn(conf.NotAllowWanAccess),
|
||||
NotAllowWanAccess: conf.NotAllowWanAccess,
|
||||
User: conf.User,
|
||||
Webhook: conf.Webhook,
|
||||
Version: os.Getenv(VersionEnv),
|
||||
Ipv4: ipv4,
|
||||
Ipv6: ipv6,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("Error happened..")
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getDnsConfStr(dnsConf []config.DnsConfig) string {
|
||||
@ -81,18 +88,18 @@ func getDnsConfStr(dnsConf []config.DnsConfig) string {
|
||||
DnsID: idHide,
|
||||
DnsSecret: secretHide,
|
||||
TTL: conf.TTL,
|
||||
Ipv4Enable: BooltoOn(conf.Ipv4.Enable),
|
||||
Ipv4Enable: conf.Ipv4.Enable,
|
||||
Ipv4GetType: conf.Ipv4.GetType,
|
||||
Ipv4Url: conf.Ipv4.URL,
|
||||
Ipv4NetInterface: conf.Ipv4.NetInterface,
|
||||
Ipv4Cmd: conf.Ipv4.Cmd,
|
||||
Ipv4Domains: strings.Join(conf.Ipv4.Domains, "\r\n"),
|
||||
Ipv6Enable: BooltoOn(conf.Ipv6.Enable),
|
||||
Ipv6Enable: conf.Ipv6.Enable,
|
||||
Ipv6GetType: conf.Ipv6.GetType,
|
||||
Ipv6Url: conf.Ipv6.URL,
|
||||
Ipv6NetInterface: conf.Ipv6.NetInterface,
|
||||
Ipv6Cmd: conf.Ipv6.Cmd,
|
||||
IPv6Reg: conf.Ipv6.IPv6Reg,
|
||||
Ipv6Reg: conf.Ipv6.Ipv6Reg,
|
||||
Ipv6Domains: strings.Join(conf.Ipv6.Domains, "\r\n"),
|
||||
})
|
||||
}
|
||||
@ -117,10 +124,3 @@ func getHideIDSecret(conf *config.DnsConfig) (idHide string, secretHide string)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func BooltoOn(b bool) string {
|
||||
if b {
|
||||
return "on"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
1437
web/writing.html
1437
web/writing.html
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user