From 7befe6e7dabf7677c0a8e211c17c8556c463d8d7 Mon Sep 17 00:00:00 2001
From: PairZhu <1115306638@qq.com>
Date: Mon, 22 Jan 2024 20:34:46 +0800
Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E5=89=8D?=
=?UTF-8?q?=E7=AB=AF=E4=BB=A3=E7=A0=81=20(#988)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* refactor: 重构前端代码
几乎完全重构前端js代码
小幅度重构后端代码(主要为了适配前端)
* fix: 修复主题色无法自适应的bug
还原了主题色修改函数的write参数,去除该参数会导致自适应修改主题时也会写入localStorage
* feat: 自动修改标签的for属性
自动修改”获取 IP 方式“标签的for属性,保证其始终指向当前可编辑的元素
* refactor: 去除JQuery
* refactor: 按业务划分代码
* fix: 对空值兼容
* fix: 统一html命名
* feat: 默认选择最后一个配置
* fix: 删除冗余代码
* fix: Update writing.html
---
config/config.go | 14 +-
main.go | 2 -
static/common.css | 4 +-
static/constant.js | 265 ++++++++
static/i18n.js | 49 ++
static/utils.js | 115 ++--
util/messages.go | 1 +
web/logs.go | 7 +-
web/netInterfaces.go | 32 -
web/save.go | 44 +-
web/webhookTest.go | 36 +-
web/writing.go | 48 +-
web/writing.html | 1421 ++++++++++++++++++------------------------
13 files changed, 1088 insertions(+), 950 deletions(-)
create mode 100644 static/constant.js
create mode 100644 static/i18n.js
delete mode 100755 web/netInterfaces.go
diff --git a/config/config.go b/config/config.go
index 7e365663c..ca65c304f 100755
--- a/config/config.go
+++ b/config/config.go
@@ -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]
diff --git a/main.go b/main.go
index e69277963..412021493 100644
--- a/main.go
+++ b/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)
diff --git a/static/common.css b/static/common.css
index 99881135d..8d6e4fea0 100644
--- a/static/common.css
+++ b/static/common.css
@@ -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);
}
diff --git a/static/constant.js b/static/constant.js
new file mode 100644
index 000000000..2481f061d
--- /dev/null
+++ b/static/constant.js
@@ -0,0 +1,265 @@
+const DNS_PROVIDERS = {
+ alidns: {
+ name: {
+ "en": "Aliyun",
+ "zh-cn": "阿里云",
+ },
+ idLabel: "AccessKey ID",
+ secretLabel: "AccessKey Secret",
+ helpHtml: {
+ "en": "Create AccessKey",
+ "zh-cn": "创建 AccessKey",
+ }
+ },
+ tencentcloud: {
+ name: {
+ "en": "Tencent",
+ "zh-cn": "腾讯云",
+ },
+ idLabel: "SecretId",
+ secretLabel: "SecretKey",
+ helpHtml: {
+ "en": "Create AccessKey",
+ "zh-cn": "创建腾讯云 API 密钥",
+ }
+ },
+ dnspod: {
+ name: {
+ "en": "DnsPod",
+ },
+ idLabel: "ID",
+ secretLabel: "Token",
+ helpHtml: {
+ "en": "Create Token",
+ "zh-cn": "创建 DNSPod Token",
+ }
+ },
+ cloudflare: {
+ name: {
+ "en": "Cloudflare",
+ },
+ idLabel: "",
+ secretLabel: "Token",
+ helpHtml: {
+ "en": "Create Token -> Edit Zone DNS (Use template)",
+ "zh-cn": "创建令牌 -> 编辑区域 DNS (使用模板)",
+ }
+ },
+ huaweicloud: {
+ name: {
+ "en": "Huawei",
+ "zh-cn": "华为云",
+ },
+ idLabel: "Access Key Id",
+ secretLabel: "Secret Access Key",
+ helpHtml: {
+ "en": "Create",
+ "zh-cn": "新增访问密钥",
+ }
+ },
+ callback: {
+ name: {
+ "en": "Callback",
+ },
+ idLabel: "URL",
+ secretLabel: "RequestBody",
+ helpHtml: {
+ "en": "Callback Support variables #{ip}, #{domain}, #{recordType}, #{ttl}",
+ "zh-cn": "自定义回调 支持的变量 #{ip}, #{domain}, #{recordType}, #{ttl}",
+ }
+ },
+ baiducloud: {
+ name: {
+ "en": "Baidu",
+ "zh-cn": "百度云",
+ },
+ idLabel: "AccessKey ID",
+ secretLabel: "AccessKey Secret",
+ helpHtml: {
+ "en": "Create AccessKey
Apply for a ticket 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": "创建 AccessKey
申请工单 DDNS 需调用 API ,而百度云相关 API 仅对申请用户开放,使用前请先提交工单申请。",
+ }
+ },
+ porkbun: {
+ name: {
+ "en": "Porkbun",
+ },
+ idLabel: "API Key",
+ secretLabel: "Secret Key",
+ helpHtml: {
+ "en": "Create Access",
+ "zh-cn": "创建 Access",
+ }
+ },
+ godaddy: {
+ name: {
+ "en": "GoDaddy",
+ },
+ idLabel: "Key",
+ secretLabel: "Secret",
+ helpHtml: {
+ "en": "Create API KEY",
+ "zh-cn": "创建 API KEY",
+ }
+ },
+ googledomain: {
+ name: {
+ "en": "Google Domain",
+ },
+ idLabel: "Username",
+ secretLabel: "Password",
+ helpHtml: {
+ "en": "How to get started",
+ "zh-cn": "新建动态域名解析记录",
+ }
+ },
+ namecheap: {
+ name: {
+ "en": "Namecheap",
+ },
+ idLabel: "",
+ secretLabel: "Password",
+ helpHtml: {
+ "en": "How to get started Namecheap DDNS does not support updating IPv6",
+ "zh-cn": "开启namecheap动态域名解析 Namecheap DDNS 不支持更新 IPv6",
+ }
+ },
+ namesilo: {
+ name: {
+ "en": "NameSilo",
+ },
+ idLabel: "",
+ secretLabel: "Password",
+ helpHtml: {
+ "en": "How to get started Please note that the TTL of namesilo is at least 1 hour",
+ "zh-cn": "开启namesilo动态域名解析 请注意namesilo的TTL最低1小时",
+ }
+ },
+};
+
+const SVG_CODE = {
+ success: ``,
+ info: ``,
+ warning: '',
+ error: ''
+}
+
+
+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: www:example.cn.eu.org
+ `,
+ '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': `
+ Click to get more info
+ 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": 'No available network card found',
+ },
+ '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), 填写为:www:example.cn.eu.org
+ 支持传递自定义参数
+ `,
+ 'Regular exp.': '匹配正则表达式',
+ 'regHelp': '可使用 @1 指定第一个IPv6地址, @2 指定第二个IPv6地址... 也可使用正则表达式匹配指定的IPv6地址, 留空则不启用',
+ 'Others': '其他',
+ 'Deny from WAN': '禁止公网访问',
+ 'NotAllowWanAccessHelp': '默认启用, 可禁止从公网访问本页面',
+ 'Username': '用户名',
+ 'accountHelp': '为保护你的信息安全,建议输入',
+ 'Password': '密码',
+ 'WebhookURLHelp': `
+ 点击参考官方 Webhook 说明
+
+ 支持的变量 #{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
+ 点击参考更多
+ `,
+ "Ipv6CmdHelp": `
+ 通过命令获取IPv6, 仅使用标准输出(stdout)的第一个匹配的 IPv6 地址。如: ip -6 addr show eth1
+ 点击参考更多
+ `,
+ "NetInterfaceEmptyHelp": '没有找到可用的网卡',
+ }
+};
diff --git a/static/i18n.js b/static/i18n.js
new file mode 100644
index 000000000..786546069
--- /dev/null
+++ b/static/i18n.js
@@ -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();});
\ No newline at end of file
diff --git a/static/utils.js b/static/utils.js
index 3624193f3..ef9311056 100644
--- a/static/utils.js
+++ b/static/utils.js
@@ -1,71 +1,110 @@
-// 常量资源
-const SVG_CODE = {
- success: ``,
- info: ``,
- warning: '',
- error: ''
-}
-
-
-
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('