🎈 Sync 2026-09-29 22:47:56

This commit is contained in:
github-actions[bot] committed 2026-09-29 22:47:57 +08:00
1 parent 97287119b0
commit 4c053b8bdb
29 files changed
+1015 -157

No files matched your search

@@ -2,9 +2,15 @@
module("luci.controller.admin.substore", package.seeall)
local function back_to_list()
-- 返回列表页。err 非空时把错误带到列表页显示(§18:失败必须让用户看见,
-- 不能「失败了却看起来像成功」)。沿用 LuCI 既有的 query + 模板渲染,不引入新 framework。
local function back_to_list(err)
local http = require("luci.http")
http.redirect(luci.dispatcher.build_url("admin", "services", "substore", "list"))
local url = luci.dispatcher.build_url("admin", "services", "substore", "list")
if err ~= nil and tostring(err) ~= "" then
url = url .. "?err=" .. luci.util.urlencode(tostring(err))
end
http.redirect(url)
end
function index()
@@ -84,18 +90,20 @@ function action_create()
local proxy_enable = http.formvalue("proxy_enable") or "0"
if proxy_enable ~= "1" then proxy_enable = "0" end
local proxy = (http.formvalue("proxy") or ""):gsub("^%s+", ""):gsub("%s+$", "")
if name ~= "" and url ~= "" then
local cron_enable, cron_time = read_cron_fields()
local rules = read_rules_fields()
core.add(name, url, {
proxy_enable = proxy_enable, proxy = proxy,
cron_enable = cron_enable, cron_time = cron_time,
rules_enable = rules.rules_enable, proto_filter = rules.proto_filter,
keyword_include = rules.keyword_include, keyword_exclude = rules.keyword_exclude,
dedup = rules.dedup, rename_map = rules.rename_map,
})
core.write_cron()
if name == "" or url == "" then
return back_to_list("名称和 URL 不能为空")
end
local cron_enable, cron_time = read_cron_fields()
local rules = read_rules_fields()
local id, err = core.add(name, url, {
proxy_enable = proxy_enable, proxy = proxy,
cron_enable = cron_enable, cron_time = cron_time,
rules_enable = rules.rules_enable, proto_filter = rules.proto_filter,
keyword_include = rules.keyword_include, keyword_exclude = rules.keyword_exclude,
dedup = rules.dedup, rename_map = rules.rename_map,
})
if not id then return back_to_list(err or "创建订阅失败") end
core.write_cron()
end
back_to_list()
end
@@ -110,18 +118,21 @@ function action_save()
local proxy_enable = http.formvalue("proxy_enable") or "0"
if proxy_enable ~= "1" then proxy_enable = "0" end
local proxy = (http.formvalue("proxy") or ""):gsub("^%s+", ""):gsub("%s+$", "")
if name ~= "" and url ~= "" then
local cron_enable, cron_time = read_cron_fields()
local rules = read_rules_fields()
core.save_meta(id, {
name = name, url = url, proxy_enable = proxy_enable, proxy = proxy,
cron_enable = cron_enable, cron_time = cron_time,
rules_enable = rules.rules_enable, proto_filter = rules.proto_filter,
keyword_include = rules.keyword_include, keyword_exclude = rules.keyword_exclude,
dedup = rules.dedup, rename_map = rules.rename_map,
})
core.write_cron()
if id == "" then return back_to_list("缺少订阅 ID") end
if name == "" or url == "" then
return back_to_list("名称和 URL 不能为空")
end
local cron_enable, cron_time = read_cron_fields()
local rules = read_rules_fields()
local ok, err = core.save_meta(id, {
name = name, url = url, proxy_enable = proxy_enable, proxy = proxy,
cron_enable = cron_enable, cron_time = cron_time,
rules_enable = rules.rules_enable, proto_filter = rules.proto_filter,
keyword_include = rules.keyword_include, keyword_exclude = rules.keyword_exclude,
dedup = rules.dedup, rename_map = rules.rename_map,
})
if not ok then return back_to_list(err or "保存失败") end
core.write_cron()
end
back_to_list()
end
@@ -134,15 +145,17 @@ function action_local_create()
local content = http.formvalue("content") or ""
local local_mode = http.formvalue("local_mode") or "text"
local rules = read_rules_fields()
if name ~= "" and content ~= "" then
core.add_local(name, content, local_mode, {
rules_enable = rules.rules_enable,
proto_filter = rules.proto_filter,
keyword_include = rules.keyword_include,
keyword_exclude = rules.keyword_exclude,
dedup = rules.dedup,
})
end
-- §19:空名称 / 空内容必须明确报错,不能无声创建无效订阅
if name == "" then return back_to_list("名称不能为空") end
if content == "" then return back_to_list("订阅内容不能为空") end
local id, err = core.add_local(name, content, local_mode, {
rules_enable = rules.rules_enable,
proto_filter = rules.proto_filter,
keyword_include = rules.keyword_include,
keyword_exclude = rules.keyword_exclude,
dedup = rules.dedup,
})
if not id then return back_to_list(err or "创建本地订阅失败") end
end
back_to_list()
end
@@ -156,19 +169,23 @@ function action_local_save()
local content = http.formvalue("content") or ""
local local_mode = http.formvalue("local_mode") or "text"
local rules = read_rules_fields()
if id ~= "" and name ~= "" and content ~= "" then
core.save_meta(id, {
name = name,
raw_content = content,
local_mode = local_mode,
rules_enable = rules.rules_enable,
proto_filter = rules.proto_filter,
keyword_include = rules.keyword_include,
keyword_exclude = rules.keyword_exclude,
dedup = rules.dedup,
})
core.sync(id)
end
if id == "" then return back_to_list("缺少订阅 ID") end
if name == "" then return back_to_list("名称不能为空") end
if content == "" then return back_to_list("订阅内容不能为空") end
local ok, err = core.save_meta(id, {
name = name,
raw_content = content,
local_mode = local_mode,
rules_enable = rules.rules_enable,
proto_filter = rules.proto_filter,
keyword_include = rules.keyword_include,
keyword_exclude = rules.keyword_exclude,
dedup = rules.dedup,
})
if not ok then return back_to_list(err or "保存失败") end
-- 解析失败会写进 meta.error(列表页 Status 列可见),此处再明确提示一次
local sok, serr = core.sync(id)
if not sok then return back_to_list(serr or "解析订阅内容失败") end
end
back_to_list()
end
@@ -315,12 +332,15 @@ function action_update()
-- local subscription does not support auto update
return
end
-- pcall fallback: parse or write error will not 500 the page, error saved to list status
local ok, err = pcall(function()
return core.sync(id)
end)
-- pcall 只保证「不抛异常」;core.sync 的失败是「返回 nil, err」而非抛错,
-- 因此必须同时检查两层结果,否则失败永远不会写回列表状态。
local ok, res, err = pcall(core.sync, id)
if not ok then
core.save_meta(id, { error = tostring(err), last_update = os.time() })
-- 抛异常:第二个返回值是错误信息
core.save_meta(id, { error = tostring(res), last_update = os.time() })
elseif not res then
-- 正常返回但失败:第三个返回值是错误信息
core.save_meta(id, { error = tostring(err or "更新失败"), last_update = os.time() })
end
end
back_to_list()
@@ -27,7 +27,7 @@
<div class="cbi-section">
<div class="cbi-section-descr" style="margin:0.5em 0">
<label for="name" style="display:inline-block;min-width:10em"><%:Name%></label>
<input type="text" id="name" name="name" value="<%=is_edit and it.name or ''%>"/>
<input type="text" id="name" name="name" value="<%=luci.util.pcdata(is_edit and it.name or "")%>"/>
</div>
<div class="cbi-section-descr" style="margin:0.5em 0">
<label style="display:inline-block;min-width:10em;vertical-align:top"><%:Source subscriptions%></label>
@@ -52,11 +52,11 @@
<div id="rules_row" style="margin:0.5em 0;display:<%=rules_enabled and "block" or "none"%>">
<div class="cbi-section-descr" style="margin:0.25em 0">
<label style="display:inline-block;min-width:10em">关键词包含</label>
<input type="text" name="keyword_include" value="<%=ki%>" style="width:60%"/>
<input type="text" name="keyword_include" value="<%=luci.util.pcdata(ki)%>" style="width:60%"/>
</div>
<div class="cbi-section-descr" style="margin:0.25em 0">
<label style="display:inline-block;min-width:10em">关键词排除</label>
<input type="text" name="keyword_exclude" value="<%=ke%>" style="width:60%"/>
<input type="text" name="keyword_exclude" value="<%=luci.util.pcdata(ke)%>" style="width:60%"/>
</div>
<div class="cbi-section-descr" style="margin:0.25em 0">
<label style="display:inline-block;min-width:10em">去重</label>
@@ -38,11 +38,11 @@
<div class="cbi-section">
<div class="cbi-section-descr" style="margin:0.5em 0">
<label for="name" style="display:inline-block;min-width:10em"><%:Name%></label>
<input type="text" id="name" name="name" value="<%=is_edit and it.name or ''%>"/>
<input type="text" id="name" name="name" value="<%=luci.util.pcdata(is_edit and it.name or "")%>"/>
</div>
<div class="cbi-section-descr" style="margin:0.5em 0">
<label for="url" style="display:inline-block;min-width:10em"><%:Subscription URL%></label>
<input type="text" id="url" name="url" value="<%=is_edit and it.url or ''%>" style="width:60%"/>
<input type="text" id="url" name="url" value="<%=luci.util.pcdata(is_edit and it.url or "")%>" style="width:60%"/>
</div>
<div class="cbi-section-descr" style="margin:0.5em 0">
<label style="display:inline-block;min-width:10em">订阅代理</label>
@@ -51,7 +51,7 @@
</div>
<div id="proxy_row" class="cbi-section-descr" style="margin:0.5em 0;display:<%=proxy_enabled and "block" or "none"%>">
<label for="proxy" style="display:inline-block;min-width:10em">代理地址</label>
<input type="text" id="proxy" name="proxy" value="<%=is_edit and it.proxy or ''%>" style="width:60%"/>
<input type="text" id="proxy" name="proxy" value="<%=luci.util.pcdata(is_edit and it.proxy or "")%>" style="width:60%"/>
<div style="color:#808080;margin-top:0.25em">通过代理下载订阅(留空则不使用),支持 http:// 或 socks5:// 地址,如 socks5://192.168.5.1:1080</div>
</div>
<div class="cbi-section-descr" style="margin:0.5em 0">
@@ -78,11 +78,11 @@
<div id="rules_row" style="margin:0.5em 0;display:<%=rules_enabled and "block" or "none"%>">
<div class="cbi-section-descr" style="margin:0.25em 0">
<label style="display:inline-block;min-width:10em">关键词包含</label>
<input type="text" name="keyword_include" value="<%=ki%>" style="width:60%"/>
<input type="text" name="keyword_include" value="<%=luci.util.pcdata(ki)%>" style="width:60%"/>
</div>
<div class="cbi-section-descr" style="margin:0.25em 0">
<label style="display:inline-block;min-width:10em">关键词排除</label>
<input type="text" name="keyword_exclude" value="<%=ke%>" style="width:60%"/>
<input type="text" name="keyword_exclude" value="<%=luci.util.pcdata(ke)%>" style="width:60%"/>
</div>
<div class="cbi-section-descr" style="margin:0.25em 0">
<label style="display:inline-block;min-width:10em">去重</label>
@@ -99,7 +99,7 @@
<% if traffic_str then %> | <%:Remaining traffic:%> <%=traffic_str%><% end %>
<% if time_str then %> | <%:Remaining time:%> <%=time_str%><% end %>
| <%:Last updated:%> <%=os.date("%Y-%m-%d %H:%M", it.last_update)%>
<% if it.error and it.error ~= "" then %> | <span style="color:#cc0000"><%=it.error%></span><% end %>
<% if it.error and it.error ~= "" then %> | <span style="color:#cc0000"><%=luci.util.pcdata(it.error)%></span><% end %>
</span>
</div>
<% end %>
@@ -14,6 +14,10 @@
-- 注入 JS 的初始内容:JSON 字符串字面量,转义 "</" 防止提前闭合 script
local util = require("substore.util")
local js_content = (util.json_encode(content) or '""'):gsub("</", "<\\/")
-- local_mode 是用户可提交的字段,直接拼进 JS 字符串会被闭合引号注入;
-- 归一为白名单取值后用 json_encode 输出,保证是合法的 JS 字符串字面量
if local_mode ~= "form" then local_mode = "text" end
local js_mode = util.json_encode(local_mode) or '"text"'
%>
<h2><%=is_edit and luci.i18n.translate("Edit local subscription") or luci.i18n.translate("Add local subscription")%></h2>
@@ -25,7 +29,7 @@
<div class="cbi-section">
<div class="cbi-section-descr" style="margin:0.5em 0">
<label for="name" style="display:inline-block;min-width:10em"><%:Name%></label>
<input type="text" id="name" name="name" value="<%=is_edit and it.name or ''%>"/>
<input type="text" id="name" name="name" value="<%=luci.util.pcdata(is_edit and it.name or "")%>"/>
</div>
<div class="cbi-section-descr" style="margin:0.5em 0">
<label style="display:inline-block;min-width:10em;vertical-align:top"><%:Subscription info%></label>
@@ -59,11 +63,11 @@
<div id="rules_row" style="margin:0.5em 0;display:<%=rules_enabled and "block" or "none"%>">
<div class="cbi-section-descr" style="margin:0.25em 0">
<label style="display:inline-block;min-width:10em"><%:Keyword include%></label>
<input type="text" name="keyword_include" value="<%=ki%>" style="width:60%"/>
<input type="text" name="keyword_include" value="<%=luci.util.pcdata(ki)%>" style="width:60%"/>
</div>
<div class="cbi-section-descr" style="margin:0.25em 0">
<label style="display:inline-block;min-width:10em"><%:Keyword exclude%></label>
<input type="text" name="keyword_exclude" value="<%=ke%>" style="width:60%"/>
<input type="text" name="keyword_exclude" value="<%=luci.util.pcdata(ke)%>" style="width:60%"/>
</div>
<div class="cbi-section-descr" style="margin:0.25em 0">
<label style="display:inline-block;min-width:10em"><%:Dedup%></label>
@@ -75,7 +79,7 @@
<span style="color:#808080">
<%:Nodes:%> <%=it.node_count or 0%>
| <%:Last updated:%> <%=os.date("%Y-%m-%d %H:%M", it.last_update)%>
<% if it.error and it.error ~= "" then %> | <span style="color:#cc0000"><%=it.error%></span><% end %>
<% if it.error and it.error ~= "" then %> | <span style="color:#cc0000"><%=luci.util.pcdata(it.error)%></span><% end %>
</span>
</div>
<% end %>
@@ -89,7 +93,7 @@
<script>
// 编辑时的初始内容(文本或 JSON),由服务端注入
var SUBSTORE_INIT_CONTENT = <%=js_content%>;
var SUBSTORE_INIT_MODE = "<%=local_mode%>";
var SUBSTORE_INIT_MODE = <%=js_mode%>;
// 字段标签与界面文本(nodeform.js 读取;翻译由服务端按系统语言渲染)
var FIELD_LABELS = {
@@ -9,10 +9,10 @@
local nodes = id ~= "" and core.read_nodes(id) or {}
local n = nodes[idx]
-- 注入 JS 的节点对象:转义 "</" 防止提前闭合 script
local js_node = n and (util.json_encode(n):gsub("</", "<\\/")) or "null"
local js_node = n and ((util.json_encode(n) or "null"):gsub("</", "<\\/")) or "null"
local back_url = luci.dispatcher.build_url('admin/services/substore/nodes') .. "?id=" .. luci.util.urlencode(id)
%>
<h2><%:Edit node%> — <%=meta and meta.name or ""%></h2>
<h2><%:Edit node%> — <%=luci.util.pcdata(meta and meta.name or "")%></h2>
<% if not n then %>
<div class="cbi-section">
@@ -23,8 +23,8 @@
<form method="post" action="<%=luci.dispatcher.build_url('admin/services/substore/node_save')%>" class="cbi-map"
onsubmit="return node_edit_prepare()">
<input type="hidden" name="token" value="<%=token%>"/>
<input type="hidden" name="id" value="<%=id%>"/>
<input type="hidden" name="idx" value="<%=idx%>"/>
<input type="hidden" name="id" value="<%=luci.util.pcdata(id)%>"/>
<input type="hidden" name="idx" value="<%=luci.util.pcdata(tostring(idx or ""))%>"/>
<div class="cbi-section">
<div id="nodes_list"></div>
<input type="hidden" name="content" id="form_content" value=""/>
@@ -3,6 +3,7 @@
local http = require("luci.http")
local core = require("substore.core")
local node = require("substore.node")
local util = require("substore.util")
local ctx = luci.dispatcher.context or {}
local q = ctx.query or {}
local id = http.formvalue("id") or q.id or ""
@@ -13,6 +14,8 @@
local group = http.formvalue("group") or q.group or ""
local sort_by = http.formvalue("sort") or q.sort or "name"
local sort_desc = http.formvalue("desc") == "1" or q.desc == "1"
-- id 会进入 <script> 里的 JS 字符串字面量,用 JSON 编码而不是 HTML 转义
local js_id = util.json_encode(id) or '""'
-- 标注原始数组下标(filter 保留表引用、sort 原地排序,下标可穿透筛选排序)
for i, nn in ipairs(nodes) do nn.__idx = i end
@@ -33,13 +36,13 @@
nodes = node.sort(nodes, sort_by, sort_desc)
local node_edit_base = luci.dispatcher.build_url('admin/services/substore/node_edit')
%>
<h2><%:Nodes%> — <%=meta and meta.name or ""%></h2>
<h2><%:Nodes%> — <%=luci.util.pcdata(meta and meta.name or "")%></h2>
<div style="margin:0.5em 0">
<a class="cbi-button cbi-button-reset" href="<%=luci.dispatcher.build_url('admin/services/substore/list')%>"><%:Back%></a>
</div>
<form method="get" action="<%=luci.dispatcher.build_url('admin/services/substore/nodes')%>" class="cbi-map">
<input type="hidden" name="id" value="<%=id%>"/>
<input type="hidden" name="id" value="<%=luci.util.pcdata(id)%>"/>
<div style="display:flex;gap:0.5em;align-items:center;flex-wrap:wrap;margin:0.5em 0">
<label><%:Type%>:
<select name="proto">
@@ -57,7 +60,7 @@
<% end %>
</select>
</label>
<label><%:Keyword%>: <input type="text" name="keyword" size="12" value="<%=keyword%>"/></label>
<label><%:Keyword%>: <input type="text" name="keyword" size="12" value="<%=luci.util.pcdata(keyword)%>"/></label>
<label><%:Sort by%>:
<select name="sort">
<option value="name" <%=sort_by=="name" and "selected" or ""%>>name</option>
@@ -80,12 +83,12 @@
<!-- 勾选批量删除用的隐藏表单:JS 收集选中下标填入 idx(逗号分隔)后提交 -->
<form id="batch_delete_form" method="post" action="<%=luci.dispatcher.build_url('admin/services/substore/node_delete')%>" style="display:none">
<input type="hidden" name="token" value="<%=token%>"/>
<input type="hidden" name="id" value="<%=id%>"/>
<input type="hidden" name="id" value="<%=luci.util.pcdata(id)%>"/>
<input type="hidden" name="idx" value=""/>
<input type="hidden" name="proto" value="<%=proto%>"/>
<input type="hidden" name="keyword" value="<%=keyword%>"/>
<input type="hidden" name="group" value="<%=group%>"/>
<input type="hidden" name="sort" value="<%=sort_by%>"/>
<input type="hidden" name="proto" value="<%=luci.util.pcdata(proto)%>"/>
<input type="hidden" name="keyword" value="<%=luci.util.pcdata(keyword)%>"/>
<input type="hidden" name="group" value="<%=luci.util.pcdata(group)%>"/>
<input type="hidden" name="sort" value="<%=luci.util.pcdata(sort_by)%>"/>
<% if sort_desc then %><input type="hidden" name="desc" value="1"/><% end %>
</form>
@@ -101,28 +104,28 @@
<th><%:Actions%></th>
</tr>
<% for _, n in ipairs(nodes) do %>
<tr class="cbi-section-table-row" data-server="<%=n.server or ""%>" data-port="<%=n.port or ""%>">
<tr class="cbi-section-table-row" data-server="<%=luci.util.pcdata(n.server or "")%>" data-port="<%=luci.util.pcdata(tostring(n.port or ""))%>">
<td><input type="checkbox" class="node-check" value="<%=n.__idx%>"/></td>
<td><%=n.name or ""%></td>
<td><%=luci.util.pcdata(n.name or "")%></td>
<td>
<input type="text" class="group-input" style="width:8em" value="<%=luci.util.pcdata(n.group or "")%>"
data-idx="<%=n.__idx%>" onchange="substore_set_group(this)"/>
</td>
<td><%=n.proto or ""%></td>
<td><%=n.server or ""%></td>
<td><%=n.port or ""%></td>
<td><%=luci.util.pcdata(n.proto or "")%></td>
<td><%=luci.util.pcdata(n.server or "")%></td>
<td><%=luci.util.pcdata(tostring(n.port or ""))%></td>
<td class="node-latency">-</td>
<td style="white-space:nowrap">
<a class="cbi-button cbi-button-apply" href="<%=node_edit_base%>?id=<%=luci.util.urlencode(id)%>&idx=<%=n.__idx%>"><%:Edit%></a>
<form method="post" action="<%=luci.dispatcher.build_url('admin/services/substore/node_delete')%>" style="display:inline"
onsubmit="return confirm('<%=luci.util.pcdata(luci.i18n.translate('Delete this node?'))%>');">
<input type="hidden" name="token" value="<%=token%>"/>
<input type="hidden" name="id" value="<%=id%>"/>
<input type="hidden" name="id" value="<%=luci.util.pcdata(id)%>"/>
<input type="hidden" name="idx" value="<%=n.__idx%>"/>
<input type="hidden" name="proto" value="<%=proto%>"/>
<input type="hidden" name="keyword" value="<%=keyword%>"/>
<input type="hidden" name="group" value="<%=group%>"/>
<input type="hidden" name="sort" value="<%=sort_by%>"/>
<input type="hidden" name="proto" value="<%=luci.util.pcdata(proto)%>"/>
<input type="hidden" name="keyword" value="<%=luci.util.pcdata(keyword)%>"/>
<input type="hidden" name="group" value="<%=luci.util.pcdata(group)%>"/>
<input type="hidden" name="sort" value="<%=luci.util.pcdata(sort_by)%>"/>
<% if sort_desc then %><input type="hidden" name="desc" value="1"/><% end %>
<input type="submit" class="cbi-button cbi-button-reset" value="<%:Delete%>"/>
</form>
@@ -160,7 +163,7 @@ function substore_set_group(el) {
alert('<%:Failed to set group%>');
}
};
xhr.send('id=' + encodeURIComponent('<%=id%>')
xhr.send('id=' + encodeURIComponent(<%=js_id%>)
+ '&idx=' + encodeURIComponent(el.getAttribute('data-idx'))
+ '&group=' + encodeURIComponent(el.value)
+ '&token=' + encodeURIComponent('<%=token%>'));
@@ -23,13 +23,13 @@
end
local out, err = output.generate(nodes, fmt)
%>
<h2><%:Output%> — <%=meta and meta.name or luci.i18n.translate("Merged")%></h2>
<h2><%:Output%> — <%=luci.util.pcdata(meta and meta.name or luci.i18n.translate("Merged"))%></h2>
<div style="margin:0.5em 0">
<a class="cbi-button cbi-button-reset" href="<%=luci.dispatcher.build_url('admin/services/substore/list')%>"><%:Back%></a>
</div>
<form method="get" action="<%=luci.dispatcher.build_url('admin/services/substore/output')%>" class="cbi-map">
<input type="hidden" name="id" value="<%=id%>"/>
<input type="hidden" name="id" value="<%=luci.util.pcdata(id)%>"/>
<div style="display:flex;gap:0.5em;align-items:center;margin:0.5em 0">
<label><%:Format%>:
<select name="fmt">
@@ -41,7 +41,7 @@
</form>
<% if err then %>
<div style="color:#cc0000"><%=err%></div>
<div style="color:#cc0000"><%=luci.util.pcdata(err)%></div>
<% else %>
<textarea id="out_text" readonly style="width:100%;height:300px;font-family:monospace"><%=luci.util.pcdata(out)%></textarea>
<div style="margin-top:0.5em">
@@ -1,6 +1,17 @@
<%+header%>
<h2><%:Subscriptions%></h2>
<%
-- §18:控制器把失败原因放在 ?err= 上带回来,这里渲染成可见的提示。
-- pcdata 转义:err 可能包含用户提交的内容,不能直接当 HTML 输出。
local flash_err = require("luci.http").formvalue("err")
if flash_err and flash_err ~= "" then
%>
<div class="cbi-section" style="border-left:4px solid #cc0000;padding:0.5em 0.75em;background:#fdf0f0;color:#cc0000">
<strong><%:操作失败%>:</strong> <%=luci.util.pcdata(flash_err)%>
</div>
<% end %>
<div class="cbi-section">
<a class="cbi-button cbi-button-add" href="<%=luci.dispatcher.build_url('admin/services/substore/form')%>">
<%:Add subscription link%>
@@ -53,9 +64,9 @@
<tr class="cbi-section-table-row">
<td>
<% if it.combo then %>
<span class="cbi-value-description" style="color:#0066aa">[<%:Combination%>]</span> <%=it.name%>
<span class="cbi-value-description" style="color:#0066aa">[<%:Combination%>]</span> <%=luci.util.pcdata(it.name or "")%>
<% else %>
<%=it.name%>
<%=luci.util.pcdata(it.name or "")%>
<% end %>
<% if it.cron_enable == true or it.cron_enable == "1" then %>
<div class="cbi-value-description">定时: <%=luci.util.pcdata(it.cron_time or "")%></div>
@@ -67,16 +78,16 @@
if type(it.sources) == "table" then
for _, sid in ipairs(it.sources) do names[#names + 1] = name_by_id[sid] or sid end
end
%><span style="color:#808080"><%=table.concat(names, " + ")%></span><%
%><span style="color:#808080"><%=luci.util.pcdata(table.concat(names, " + "))%></span><%
elseif it["local"] then
%><span style="color:#0066aa">[<%:Local%>]</span><%
else
%><%=it.url%><%
%><%=luci.util.pcdata(it.url or "")%><%
end
%></td>
<td><%=it.node_count or 0%></td>
<td><%=it.last_update and os.date("%Y-%m-%d %H:%M", it.last_update) or "-"%></td>
<td style="color:<%=status_color%>"><%=status_text%></td>
<td style="color:<%=status_color%>"><%=luci.util.pcdata(status_text or "")%></td>
<td>
<div style="display:flex;gap:0.25em;align-items:center">
<select id="fmt_<%=it.id%>" data-base="<%=luci.util.pcdata(sub_link_base)%>"