diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f14b74d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+################################################################################
+# 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。
+################################################################################
+
+/.vs
diff --git a/build.sh b/build.sh
new file mode 100644
index 0000000..44239d6
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+echo 'INSTALL SUCCESS'
\ No newline at end of file
diff --git a/target/aliyun/ai.lua b/target/aliyun/ai.lua
new file mode 100644
index 0000000..e5a0027
--- /dev/null
+++ b/target/aliyun/ai.lua
@@ -0,0 +1,36 @@
+local http = require("socket.http")
+local ltn12 = require("ltn12")
+local cjson = require("cjson")
+
+local M = {}
+local url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"
+
+M.ask = function(key,content,model)
+ -- 构造 JSON 格式的请求数据
+ local payload = {
+ model = model,
+ messages = content
+ }
+ local response_body = {}
+
+ local res, code, response_headers, status = http.request{
+ url = url,
+ method = "POST",
+ headers = {
+ ["Authorization"] = "Bearer " .. key,
+ ["Content-Type"] = "application/json",
+ },
+ source = ltn12.source.string(cjson.encode(payload)),
+ sink = ltn12.sink.table(response_body)
+ }
+ if code == 200 then
+ local data = cjson.decode(table.concat(response_body))
+ return true,{content = data.choices[1].message.content,reasoning_content = data.choices[1].reasoning_content}
+ else
+ return false, "请求失败,错误描述:" .. table.concat(response_body)
+ end
+end
+
+return M
+
+
diff --git a/target/aliyun/email.lua b/target/aliyun/email.lua
new file mode 100644
index 0000000..b81e278
--- /dev/null
+++ b/target/aliyun/email.lua
@@ -0,0 +1,191 @@
+local M = {}
+
+require("app.app")
+local http = require("fwutils.httpclient")
+
+
+
+M.get_token = function(app_id,app_secret)
+ local token = cache.get_json("aliyun_email_token")
+ if token then
+ if token.expires > os.time() + 60*60 then
+ return true,token.access_token
+ end
+ end
+
+ local url = "https://alimail-cn.aliyuncs.com/oauth2/v2.0/token"
+ local body = "grant_type=client_credentials&client_id="..app_id.."&client_secret="..app_secret
+ local result,err = http.post(url,{
+ ["Content-Type"] = "application/x-www-form-urlencoded"
+ },body)
+ if not result then
+ return false,err
+ end
+ local data = cjson.decode(err)
+ if data.error ~= nil and data.error ~= "" then
+ return false,data.error_description
+ end
+
+ cache.set_json("aliyun_email_token",{
+ access_token = data.access_token,
+ expires = os.time() + data.expires_in
+ })
+ return true,data.access_token
+end
+
+M.create_msg = function(from,from_name,to,to_name,title,content,app_id,app_secret)
+ local ok,access_token = M.get_token(app_id,app_secret)
+ if not ok then
+ return false,access_token
+ end
+ local body = {
+ message = {
+ subject = title,
+ summary = content,
+ priority = "PRY_HIGH",
+ isReadReceiptRequested = false,
+ from = {
+ email = from,
+ name = from_name
+ },
+ toRecipients = {
+ {
+ email = to,
+ name = to_name
+ }
+ },
+ body = {
+ bodyText = content,
+ bodyHtml = content
+ }
+ }
+ }
+ local url = "https://alimail-cn.aliyuncs.com/v2/users/"..from.."/messages"
+ local result,err = http.post(url,{
+ ["Content-Type"] = "application/json",
+ ["Authorization"] = "Bearer "..access_token
+ },cjson.encode(body))
+ if not result then
+ return false,err
+ end
+ local data = cjson.decode(err)
+ if data == nil or data.message == nil or data.message.id == nil then
+ return false,err
+ end
+ return true,data.message.id
+end
+M.send = function(from,from_name,to,to_name,title,content,app_id,app_secret)
+ local ok,access_token = M.get_token(app_id,app_secret)
+ if not ok then
+ return false,access_token
+ end
+ local ok,msg_id = M.create_msg(from,from_name,to,to_name,title,content,app_id,app_secret)
+ if not ok then
+ return false,msg_id
+ end
+ local body = {
+ saveToSentItems = true
+ }
+ local url = "https://alimail-cn.aliyuncs.com/v2/users/"..from.."/messages/"..msg_id.."/send"
+ local result,err = http.post(url,{
+ ["Content-Type"] = "application/json",
+ ["Authorization"] = "Bearer "..access_token
+ },cjson.encode(body))
+ if not result then
+ return false,"send error:"..err
+ end
+ if err == "{}" then
+ return true
+ end
+ return false,"send error:"..err
+ --return true
+end
+M.create_user = function(email_name,password,nickname,jobtitle,app_id,app_secret)
+ local ok,access_token = M.get_token(app_id,app_secret)
+ if not ok then
+ return false,access_token
+ end
+ local body = {
+ email = email_name,
+ password = password,
+ name = nickname,
+ jobTitle = jobtitle,
+ departmentIds = {config.email.department_id},
+ }
+ local url = "https://alimail-cn.aliyuncs.com/v2/users"
+ local result,err = http.post(url,{
+ ["Content-Type"] = "application/json",
+ ["Authorization"] = "Bearer "..access_token
+ },cjson.encode(body))
+ if not result then
+ return false,"send error:"..err
+ end
+
+ local data = cjson.decode(err)
+ if data.email == email_name then
+ return true
+ end
+ return false,"create user error:"..err
+end
+M.delete_user = function(email_name,app_id,app_secret)
+ local ok,access_token = M.get_token(app_id,app_secret)
+ if not ok then
+ return false,access_token
+ end
+ local url = "https://alimail-cn.aliyuncs.com/v2/users/"..email_name
+ local result,err = http.delete(url,{
+ ["Content-Type"] = "application/json",
+ ["Authorization"] = "Bearer "..access_token
+ })
+ if not result then
+ return false,"send error:"..err
+ end
+ return true
+end
+M.getDepartment = function(id,app_id,app_secret)
+ local ok,access_token = M.get_token(app_id,app_secret)
+ if not ok then
+ return false,access_token
+ end
+
+
+ local url = "https://alimail-cn.aliyuncs.com/v2/departments/"..id
+ local result,err = http.get(url,{
+ ["Content-Type"] = "application/json",
+ ["Authorization"] = "Bearer "..access_token
+ })
+
+ if not result then
+ return false,"send error:"..err
+ end
+ print("getDepartment")
+ print("result",result)
+ print("err",type(err))
+ print("err[data]",err)
+
+ return true
+end
+M.getDepartmentList = function(app_id,app_secret)
+ local ok,access_token = M.get_token(app_id,app_secret)
+ if not ok then
+ return false,access_token
+ end
+
+ local url = "https://alimail-cn.aliyuncs.com/v2/departments/$root/chain"
+ local result,err = http.get(url,{
+ ["Content-Type"] = "application/json",
+ ["Authorization"] = "Bearer "..access_token
+ })
+ if not result then
+ return false,"send error:"..err
+ end
+ print("getDepartmentList")
+ print("result",result)
+ print("err",type(err))
+ print("err[data]",err)
+
+ return true
+end
+
+return M
+
diff --git a/target/base64.lua b/target/base64.lua
new file mode 100644
index 0000000..af7dd2c
--- /dev/null
+++ b/target/base64.lua
@@ -0,0 +1,52 @@
+local M = {}
+
+-- Base64 字符集
+local b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
+-- 创建 Base64 反向查找表
+local b64lookup = {}
+for i = 1, #b64chars do
+ b64lookup[string.sub(b64chars, i, i)] = i - 1
+end
+
+M.encode = function(input)
+ local output = {}
+ local len = #input
+ for i = 1, len, 3 do
+ local a, b, c = string.byte(input, i, i + 2)
+ local chunk = (a or 0) << 16 | (b or 0) << 8 | (c or 0)
+
+ output[#output + 1] = b64chars:sub(((chunk >> 18) & 0x3F) + 1, ((chunk >> 18) & 0x3F) + 1)
+ output[#output + 1] = b64chars:sub(((chunk >> 12) & 0x3F) + 1, ((chunk >> 12) & 0x3F) + 1)
+ output[#output + 1] = b and b64chars:sub(((chunk >> 6) & 0x3F) + 1, ((chunk >> 6) & 0x3F) + 1) or "="
+ output[#output + 1] = c and b64chars:sub((chunk & 0x3F) + 1, (chunk & 0x3F) + 1) or "="
+ end
+ return table.concat(output)
+end
+
+-- Base64 解码函数
+M.decode = function(input)
+ input = input:gsub("%s", ""):gsub("=", "") -- 去除空白和填充符
+ local output = {}
+ for i = 1, #input, 4 do
+ local a, b, c, d = b64lookup[input:sub(i, i)], b64lookup[input:sub(i + 1, i + 1)],
+ b64lookup[input:sub(i + 2, i + 2)] or 0, b64lookup[input:sub(i + 3, i + 3)] or 0
+ local chunk = (a << 18) | (b << 12) | (c << 6) | d
+
+ output[#output + 1] = string.char((chunk >> 16) & 0xFF)
+ if input:sub(i + 2, i + 2) ~= "" then
+ output[#output + 1] = string.char((chunk >> 8) & 0xFF)
+ end
+ if input:sub(i + 3, i + 3) ~= "" then
+ output[#output + 1] = string.char(chunk & 0xFF)
+ end
+ end
+ return table.concat(output)
+end
+
+
+
+
+
+return M
+
+
diff --git a/target/cache.lua b/target/cache.lua
new file mode 100644
index 0000000..802ac6b
--- /dev/null
+++ b/target/cache.lua
@@ -0,0 +1,40 @@
+local M = {}
+
+local localstorage = require("localstorage")
+local fw = require("fastweb")
+local cjson = require("cjson")
+
+M.start = function(dirpath)
+ local storage = localstorage.new()
+ fw.set_ptr("localstorage_cache",storage:self())
+ if storage:open(dirpath) == false then
+ return false,storage:last_error()
+ end
+ return true
+end
+
+M.close = function()
+ localstorage.new(localstorage_cache):close()
+end
+M.get = function(key)
+ return localstorage.new(localstorage_cache):read(key)
+end
+M.get_json = function(key)
+ local value = M.get(key)
+ if value then
+ return cjson.decode(value)
+ end
+ return nil
+end
+M.set = function(key,value)
+ localstorage.new(localstorage_cache):write(key,value)
+end
+M.set_json = function(key,value)
+ M.set(key,cjson.encode(value))
+end
+M.del = function(key)
+ localstorage.new(localstorage_cache):del(key)
+end
+return M
+
+
diff --git a/target/fwutils/acl.lua b/target/fwutils/acl.lua
new file mode 100644
index 0000000..92c8fcf
--- /dev/null
+++ b/target/fwutils/acl.lua
@@ -0,0 +1,117 @@
+local utils = require("utils")
+local fw = require("fastweb")
+local config = require("fwutils.config")
+local M = {}
+
+
+-- 更新
+M.update = function(role_id,conn)
+ -- 查询权限表
+ local select = conn:select()
+ select:table("fw_role_permissions")
+ select:where_expression("AND delete_time IS NULL")
+ if role_id ~= nil then
+ select:where_i32("role_id", "=", role_id)
+ end
+ local result = select:query()
+
+
+ local bc = {}
+
+ while result:next() do
+ local id = result:get("id")
+ local path = result:get("path")
+ local role_id = tostring(result:get("role_id"))
+ local action = result:get("action")
+ local desc = result:get("desc")
+ local create_time = result:get("create_time")
+ local update_time = result:get("update_time")
+ local delete_time = result:get("delete_time")
+ -- local public = result:get("public")
+
+ if bc[role_id] == nil then
+ bc[role_id] = {}
+ end
+ if bc[role_id]["public"] == nil then
+ bc[role_id]["public"] = {}
+ end
+ if bc[role_id]["private"] == nil then
+ bc[role_id]["private"] = {}
+ end
+ -- 处理 action 字段,将其切分为表或空表
+ local actions_tbl = {}
+ if action and action ~= "" then
+ for act in string.gmatch(action, "([^,]+)") do
+ table.insert(actions_tbl, act)
+ end
+ end
+
+ local item = {
+ create_time = create_time,
+ update_time = update_time,
+ delete_time = delete_time,
+ action = actions_tbl,
+ desc = desc,
+ }
+ -- if public == 1 then
+ -- bc[role_id]["public"][path] = item
+ -- else
+ -- bc[role_id]["private"][path] = item
+ -- end
+ bc[role_id][path] = item
+ end
+ local code = "return " .. require("serpent").serialize(bc, {comment = false})
+ utils.save_file(fw.website_dir().."/"..(config.path.luabytecode:gsub("%.", "/")).."/acl_bc.lua",code)
+ return true
+end
+-- 匹配
+M.match = function(cfg)
+
+ local function match_path(path, patterns)
+
+ -- print("[match_path] path:",path)
+ for pattern, v in pairs(patterns) do
+
+ -- 如果是正则(以^开头),用string.match,否则精确匹配
+ if string.sub(pattern, 1, 1) == "^" then
+
+ if string.match(path, pattern) then
+ -- print("[TRUE] pattern:",pattern,",path:",path)
+ return true, v
+ -- else
+ -- print("[FALSE] pattern:",pattern,",path:",path)
+ end
+ else
+ if path == pattern then
+ return true, v
+ end
+ end
+ end
+ return false, nil
+ end
+ -- 检查action
+ local function check_action(actions,action)
+ if actions == nil or #actions == 0 then
+ return true
+ end
+
+ for _,v in pairs(actions) do
+ if v == action then
+ return true
+ end
+ end
+ return false, "action not match"
+ end
+ local role_id_str = string.format("%d",cfg.role_id())
+ local acl_bc = require(config.path.luabytecode..".acl_bc")
+ if acl_bc[role_id_str] == nil then
+ return false,"role id("..role_id_str..") acl not found"
+ end
+ local result, item = match_path(cfg.filepath(), acl_bc[role_id_str])
+ if result then
+ return check_action(item.action,cfg.action())
+ end
+ return false,"path("..cfg.filepath()..") acl not found"
+end
+
+return M
\ No newline at end of file
diff --git a/target/fwutils/config.lua b/target/fwutils/config.lua
new file mode 100644
index 0000000..2d3112b
--- /dev/null
+++ b/target/fwutils/config.lua
@@ -0,0 +1,22 @@
+return {
+ -- 服务器ID,如果REDIS或MYSQL与其它服务公用必须更改此参数,要求唯一
+ server_id = "TEACHER_1234567890",
+
+ path = {
+ luabytecode = "cache.fw.luabytecode"
+ },
+ token = {
+ -- 超时时间
+ expire = 3600 * 24 * 30,
+ -- 加密算法
+ algorithm = "aes-256",
+ -- 加密模式
+ mode = "cbc",
+ -- 加密密钥
+ key = "kangDzFLc3MweDQH",
+ },
+ db = {
+ redis_pool_name = "rdb",
+ mysql_pool_name = "db",
+ }
+}
\ No newline at end of file
diff --git a/target/fwutils/init.lua b/target/fwutils/init.lua
new file mode 100644
index 0000000..279ac3f
--- /dev/null
+++ b/target/fwutils/init.lua
@@ -0,0 +1,41 @@
+local mysql_pool = require("mysql.pool")
+local fw = require("fastweb")
+local utils = require("utils")
+
+
+local M = {}
+
+
+M.initialization = function(conn)
+
+ local result,err = M.__get_guest_role_id(conn)
+ if result == false then
+ return false,err
+ end
+
+ return true
+end
+M.__get_guest_role_id = function(mysql_conn)
+ local ppst = mysql_conn:setsql("SELECT * FROM fw_role WHERE guest = 1")
+ local result = ppst:query()
+ if result:row_count() ~= 1 then
+ return false,"role guest not found"
+ end
+
+ result:next()
+ local bc = {
+ GUEST_ROLE_ID = result:get("id"),
+ }
+ local config = require("fwutils.config")
+ local code = "return " .. require("serpent").serialize(bc, {comment = false})
+ utils.save_file(fw.website_dir().."/"..(config.path.luabytecode:gsub("%.", "/")).."/fwcache.lua",code)
+ return true
+end
+M.guest_role_id = function()
+ local config = require("fwutils.config")
+ return require(config.path.luabytecode..".fwcache").GUEST_ROLE_ID
+end
+
+
+
+return M
\ No newline at end of file
diff --git a/target/fwutils/menu.lua b/target/fwutils/menu.lua
new file mode 100644
index 0000000..ddae635
--- /dev/null
+++ b/target/fwutils/menu.lua
@@ -0,0 +1,104 @@
+local fw = require("fastweb")
+local config = require("fwutils.config")
+local M = {}
+-- CREATE TABLE `fw_menu` (
+-- `id` int NOT NULL AUTO_INCREMENT,
+-- `role_id` int DEFAULT NULL,
+-- `title` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+-- `path` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+-- `icon` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+-- `sort` int DEFAULT NULL,
+-- `parent_id` int DEFAULT NULL,
+-- PRIMARY KEY (`id`)
+-- ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- 更新
+M.update = function(role_id,conn)
+ -- 查询菜单表
+ local select = conn:select()
+ select:table("fw_menu")
+ select:where_expression("AND delete_time IS NULL")
+ if role_id ~= nil then
+ select:where_i32("role_id", "=", role_id)
+ end
+ local result = select:query()
+
+ local bc = {}
+ local items_by_id = {} -- 通过id快速查找菜单项:{id = {role_id, title, item, parent_id}}
+ local items_with_parent = {} -- 存储有父级的菜单项
+
+ -- 第一遍:读取所有菜单项并存储
+ while result:next() do
+ local id = result:get("id")
+ local role_id = tostring(result:get("role_id"))
+ local title = result:get("title")
+ local path = result:get("path")
+ local icon = result:get("icon")
+ local sort = result:get("sort")
+ local parent_id = result:get("parent_id")
+
+ if not title or title == "" then
+ goto continue
+ end
+
+ if bc[role_id] == nil then
+ bc[role_id] = {}
+ end
+
+ local item = {
+ path = path,
+ icon = icon,
+ sort = sort,
+ }
+
+ -- 存储所有菜单项信息
+ items_by_id[id] = {
+ role_id = role_id,
+ title = title,
+ item = item,
+ parent_id = parent_id
+ }
+
+ -- 如果parent_id为空,则作为顶层菜单项
+ if parent_id == nil or parent_id == 0 then
+ bc[role_id][title] = item
+ else
+ -- 有父级,记录下来稍后处理
+ table.insert(items_with_parent, {
+ id = id,
+ role_id = role_id,
+ title = title,
+ item = item,
+ parent_id = parent_id
+ })
+ end
+
+ ::continue::
+ end
+
+ -- 第二遍:处理有父级的菜单项,构建children结构
+ for _, menu_item in ipairs(items_with_parent) do
+ local parent_info = items_by_id[menu_item.parent_id]
+ if parent_info then
+ local parent_item = parent_info.item
+
+ -- 如果父项还没有children表,创建它
+ if not parent_item.children then
+ parent_item.children = {}
+ end
+
+ -- 将子项添加到父项的children中
+ parent_item.children[menu_item.title] = menu_item.item
+ end
+ end
+
+ local code = "return " .. require("serpent").serialize(bc, {comment = false})
+ utils.save_file(fw.website_dir().."/"..(config.path.luabytecode:gsub("%.", "/")).."/menu_bc.lua",code)
+ return true
+end
+M.get = function(role_id)
+ local menu_bc = require(config.path.luabytecode..".menu_bc")
+ return menu_bc[string.format("%d",role_id)]
+end
+
+return M
\ No newline at end of file
diff --git a/target/fwutils/request_config.lua b/target/fwutils/request_config.lua
new file mode 100644
index 0000000..369c5f6
--- /dev/null
+++ b/target/fwutils/request_config.lua
@@ -0,0 +1,64 @@
+local request = require("fastweb.request")
+local token_module = require("fwutils.token")
+local config = require("fwutils.config")
+local cjson = require("cjson")
+local utils = require("utils")
+local fwutils_init = require("fwutils.init")
+local M = {}
+M.__filepath = nil
+M.__method = nil
+M.__url_param = nil
+M.__action = nil
+M.__user_data = nil
+M.__role_id = nil
+M.__ext = nil
+
+M.init = function(website_config)
+ M.__filepath = request.filepath()
+ M.__method = request.method()
+ M.__url_param = request.url_param()
+ M.__action = nil
+ M.__user_data = nil
+ M.__role_id = nil
+ M.__ext = utils.ext(M.__filepath)
+ -- 修饰路径
+ if M.__filepath == "/" then
+ M.__filepath = website_config.default_filepath
+ end
+ -- 获取动作
+ if M.__url_param ~= nil then
+ M.__action = M.__url_param["action"]
+ end
+ -- 获取TOKEN
+ local token = string.match(request.header("Cookie"),"token=(%w+)")
+ if token ~= nil and token ~= "" then
+ local result,user_data = token_module.get(token)
+ -- print("TOKEN_DATA:",cjson.encode(user_data))
+ if result and user_data ~= nil and user_data["role_id"] ~= nil and user_data["role_id"] > 0 then
+ M.__user_data = user_data
+ M.__role_id = user_data["role_id"]
+ request.set("user_data",cjson.encode(user_data))
+ else
+ M.__role_id = fwutils_init.guest_role_id()
+ end
+ else
+ -- 访客
+ M.__role_id = fwutils_init.guest_role_id()
+ end
+end
+M.role_id = function()
+ return M.__role_id
+end
+M.user_data = function()
+ return M.__user_data
+end
+M.action = function()
+ return M.__action
+end
+M.filepath = function()
+ return M.__filepath
+end
+M.ext = function()
+ return M.__ext
+end
+return M
\ No newline at end of file
diff --git a/target/fwutils/stopwatch.lua b/target/fwutils/stopwatch.lua
new file mode 100644
index 0000000..1a1ff50
--- /dev/null
+++ b/target/fwutils/stopwatch.lua
@@ -0,0 +1,34 @@
+local M = {}
+
+M.tss = {}
+
+M.record = function(name)
+ if name == nil then
+ name = ""
+ end
+ table.insert(M.tss, {
+ name = name,
+ ts = fw_now_msec(),
+ })
+end
+
+M.print = function()
+
+ for i = 2, #M.tss do
+ local prev = M.tss[i - 1]
+ local curr = M.tss[i]
+ local diff = curr.ts - prev.ts
+ if diff < 1000 then
+ print(string.format("%s --> %dms", curr.name, diff))
+ else
+ print(string.format("%s --> %.2fs", curr.name, diff / 1000))
+ end
+ end
+ M.tss = {}
+
+end
+
+
+
+
+return M
\ No newline at end of file
diff --git a/target/fwutils/template_engine.lua b/target/fwutils/template_engine.lua
new file mode 100644
index 0000000..0e0e37e
--- /dev/null
+++ b/target/fwutils/template_engine.lua
@@ -0,0 +1,435 @@
+local utils = require("utils")
+local fw = require("fastweb")
+local config = require("fwutils.config")
+local request = require("fastweb.request")
+local response = require("fastweb.response")
+local cjson = require("cjson")
+-- 允许的扩展名
+local allowed_extensions = {
+ "shtml",
+ "html",
+ "js"
+}
+local template_engine_bc_this_role = nil
+local cfg = nil
+
+local M = {}
+
+function file_get_contents(filepath)
+
+ local file, errmsg = io.open(fw.website_dir()..filepath, "r")
+ if not file then
+ err.server(errmsg)
+ end
+ local content = file:read("*a")
+ if content == nil or content == "" then
+ print("file_get_contents error: ",filepath)
+ return ""
+ end
+ local replaced,c2 = M.replace(content)
+ if replaced then
+ return c2
+ end
+ return content
+end
+function menu_top()
+ -- 当前请求路径
+ local request_path = request.filepath() or ""
+ -- 取配置
+ local menuData = require("fwutils.menu").get(string.format("%d",cfg.role_id()))
+ if not menuData then
+ return ""
+ end
+
+ -- 提取并排序主菜单
+ local menuItems = {}
+ for name, item in pairs(menuData) do
+ table.insert(menuItems, {
+ name = name,
+ item = item,
+ sort = item.sort or 0
+ })
+ end
+ table.sort(menuItems, function(a, b)
+ return a.sort > b.sort
+ end)
+
+ local html = [[
+
+
+ ]]
+
+ -- 渲染主菜单
+ for _, entry in ipairs(menuItems) do
+ local name = entry.name
+ local item = entry.item
+ local icon = item.icon or ""
+ local path = item.path or "#"
+ local activeClass = ""
+
+ if item.children then
+ -- 有子菜单
+ local isMegamenu = item.megamenu and " dropdown-megamenu" or ""
+
+ -- 检查子菜单是否有激活
+ local hasActiveChild = false
+ local children = {}
+ for childName, childItem in pairs(item.children) do
+ table.insert(children, {
+ name = childName,
+ item = childItem,
+ sort = childItem.sort or 0
+ })
+ if not hasActiveChild and (request_path == (childItem.path or "")) then
+ hasActiveChild = true
+ end
+ end
+ table.sort(children, function(a, b)
+ return a.sort > b.sort
+ end)
+
+ -- 父菜单激活:当前页面是父菜单path,或是任一子菜单path
+ if request_path == path or hasActiveChild then
+ activeClass = "active-link"
+ end
+
+ html = html .. [[
+
+ ]]
+
+ -- 渲染子菜单
+ for _, child in ipairs(children) do
+ local childName = child.name
+ local childItem = child.item
+ local childPath = childItem.path or "#"
+
+ -- 子菜单不设置激活样式
+ html = html .. [[
+