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:
PairZhu
2024-01-22 20:34:46 +08:00
committed by GitHub
parent fe9f437d9b
commit 7befe6e7da
13 changed files with 1096 additions and 958 deletions

View File

@ -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]

View File

@ -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)

View File

@ -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
View 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
View 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();});

View File

@ -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)
}
}

View File

@ -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")

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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 ""
}

File diff suppressed because it is too large Load Diff