Bladeren bron

feat(节点): 统一使用bindRole获取节点图标

refactor(节点抽屉): 重构节点数据初始化和表单处理逻辑

feat(nginx): 新增403、404、50x错误页面和nginx配置

feat(lua): 新增课程查询和文件上传接口

style(节点): 调整节点角色列表顺序和图标获取逻辑

chore: 新增index.html首页文件
zhanghaijun 11 maanden geleden
bovenliggende
commit
c44eb35a3c

+ 46 - 0
develop/lua/course_query.lua

@@ -0,0 +1,46 @@
+-- =========================================
+-- json查询接口
+-- =========================================
+
+local cjson = require "cjson.safe"
+
+ngx.header.content_type = "application/json; charset=utf-8"
+
+-- 获取请求路径
+local uri = ngx.var.uri
+local prefix = "/api/json/"
+local relative_path = uri:sub(#prefix + 1)
+
+-- 默认 all.json
+if relative_path == "" then
+    relative_path = "all.json"
+elseif not relative_path:match("%.json$") then
+    relative_path = relative_path .. ".json"
+end
+
+local filepath = "/opt/server_json/" .. relative_path
+
+-- 打开文件
+local file, err = io.open(filepath, "r")
+if not file then
+    ngx.status = 404
+    ngx.say(cjson.encode({ code = 404, msg = "未找到字典文件: " .. relative_path }))
+    return
+end
+
+-- 读取文件内容
+local content = file:read("*a")
+file:close()
+
+-- 校验 JSON 格式
+local json_data = cjson.decode(content)
+if not json_data then
+    ngx.status = 500
+    ngx.say(cjson.encode({ code = 500, msg = "JSON 格式错误: " .. relative_path }))
+    return
+end
+
+-- 组装响应
+local result = cjson.encode({code = 200, msg = "success", data = json_data})
+
+ngx.say(result)

+ 98 - 0
develop/lua/course_upload.lua

@@ -0,0 +1,98 @@
+-- =========================================
+-- 文件上传接口
+-- =========================================
+local upload = require "resty.upload"
+local cjson = require "cjson.safe"
+
+ngx.header.content_type = "application/json; charset=utf-8"
+
+local upload_dir = "/opt/oss/upload/"
+local max_size = 20 * 1024 * 1024  -- 20MB
+
+-- 工具函数
+local function now_ms()
+    return tostring(math.floor(ngx.now() * 1000))
+end
+
+local function get_ym()
+    return os.date("%Y%m")
+end
+
+local function safe_suffix(name)
+    -- 获取文件后缀,例如 .png
+    return name:match("^.+(%..+)$") or ""
+end
+
+local form, err = upload:new(4096)
+if not form then
+    ngx.say(cjson.encode({ code = 500, msg = "创建上传对象失败: " .. (err or ""), data = {} }))
+    return
+end
+form:set_timeout(10000)
+
+local file, filename, filesize = nil, nil, 0
+
+while true do
+    local typ, res, err = form:read()
+    if not typ then
+        ngx.say(cjson.encode({ code = 500, msg = "读取上传内容失败: " .. (err or ""), data = {} }))
+        return
+    end
+
+    if typ == "header" then
+        local name, value = res[1], res[2]
+        if name == "Content-Disposition" then
+            local orig_name = value:match('filename="(.-)"')
+            if orig_name then
+                local suffix = safe_suffix(orig_name)
+                local newname = get_ym() .. "_" .. now_ms() .. suffix
+                local fullpath = upload_dir .. newname
+
+                -- 判断文件是否存在
+                local f = io.open(fullpath, "r")
+                if f then
+                    f:close()
+                    ngx.say(cjson.encode({code = 429, msg = "当前访问人数较多,请稍后重试", data = {}}))
+                    return
+                end
+
+                -- 打开文件写入
+                file, err = io.open(fullpath, "w+")
+                if not file then
+                    ngx.say(cjson.encode({ code = 500, msg = "无法创建文件: " .. (err or ""), data = {} }))
+                    return
+                end
+
+                filename = newname
+            end
+        end
+    elseif typ == "body" then
+        if file then
+            filesize = filesize + #res
+            if filesize > max_size then
+                file:close()
+                os.remove(upload_dir .. filename)
+                ngx.say(cjson.encode({ code = 413, msg = "文件过大(限制20MB)", data = {} }))
+                return
+            end
+            file:write(res)
+        end
+    elseif typ == "part_end" then
+        if file then
+            file:close()
+            file = nil
+        end
+    elseif typ == "eof" then
+        break
+    end
+end
+
+ngx.say(cjson.encode({
+    code = 200,
+    msg = "上传成功",
+    data = {
+        filename = filename,
+        url = "/oss/upload/" .. filename,
+        size = filesize
+    }
+}))

+ 195 - 0
develop/nginx/443.conf

@@ -0,0 +1,195 @@
+server {
+    listen 443 ssl;
+    http2 on;
+    server_name _;
+
+    # SSL证书路径
+    ssl_certificate /usr/local/openresty/nginx/ssl/ali.haijunit.icu.pem;
+    ssl_certificate_key /usr/local/openresty/nginx/ssl/ali.haijunit.icu.key;
+
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_ciphers 'EECDH+AESGCM:EECDH+CHACHA20:EECDH+AES256:!aNULL:!MD5:!RC4';
+    ssl_prefer_server_ciphers on;
+    ssl_session_cache shared:SSL:50m;
+    ssl_session_timeout 10m;
+
+    ssl_stapling on;
+    ssl_stapling_verify on;
+
+    # 可选:显式指定 TLSv1.3 cipher(仅 OpenSSL 1.1.1+ 支持)
+    ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
+
+     # 安全 Header
+    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+    add_header X-Frame-Options SAMEORIGIN;
+    add_header X-Content-Type-Options nosniff;
+    add_header X-XSS-Protection "1; mode=block";
+    add_header Content-Security-Policy "default-src 'self' data: blob: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: https:; font-src 'self' data: https:;" always;
+
+    root html;
+    try_files $uri $uri/ /index.html;
+
+    #-------------------------------------------------
+    # 通用业务
+    #-------------------------------------------------
+    location = / {
+        rewrite ^/ /course break;
+    }
+    # lua 文件上传接口
+    location = /api/upload {
+        content_by_lua_file /usr/local/openresty/lua/file_upload.lua;
+    }
+    # lua 读取json文件
+    location /api/json/ {
+        content_by_lua_file /usr/local/openresty/lua/query_json.lua;
+    }
+
+    #-------------------------------------------------
+    # 前端应用
+    #-------------------------------------------------
+    location / {
+        index index.html;
+        try_files $uri $uri/ /index.html;
+        expires 7d;
+        add_header Cache-Control "public, max-age=604800, immutable";
+    }
+
+    #-------------------------------------------------
+    # 静态资源缓存 + 防盗链
+    #-------------------------------------------------
+    location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2?|ttf|ico)$ {
+        expires 30d;
+        access_log off;
+        add_header Cache-Control "public, max-age=2592000, immutable";
+    }
+
+    #-------------------------------------------------
+    # OSS 静态目录(带目录索引)
+    #-------------------------------------------------
+    location ^~ /oss/ {
+        alias /opt/oss/;
+        index _;                       # 去掉默认的界面
+        autoindex on;                  # 开启目录索引
+        autoindex_exact_size off;      # 文件大小人性化显示
+        autoindex_localtime on;        # 显示本地时间
+        charset utf-8;
+        expires 30d;
+        add_header Cache-Control "public, max-age=2592000, immutable";
+        # 如果不做这个配置,点击目录下的txt文件,大部分浏览器默认是直接浏览的。这里通过添加响应头来控制。
+        if ($request_filename ~* ^.*?\.(html|txt|doc|pdf|rar|gz|zip|docx|exe|xlsx|ppt|pptx|conf)$){
+            add_header Content-Disposition 'attachment;';
+        }
+    }
+
+    #-------------------------------------------------
+    # 课程管理
+    #-------------------------------------------------
+    location ^~ /course/ {
+        alias /opt/course/;
+        index index.html;
+
+         # 安全头部配置
+        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
+        add_header X-Content-Type-Options nosniff;
+        add_header X-XSS-Protection "1; mode=block";
+        add_header Referrer-Policy "same-origin";
+
+        add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()";
+        add_header Content-Security-Policy "default-src 'self'; worker-src 'self' blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: https:; connect-src 'self' https:; font-src 'self' data: https:; frame-ancestors 'self'";
+
+        try_files $uri $uri/ /course/index.html;
+    }
+    location = /course/api/device/status {
+
+        # 允许所有域名跨域
+        add_header 'Access-Control-Allow-Origin' '*';
+        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
+        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
+        add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
+
+        default_type application/json;
+        content_by_lua_file /usr/local/openresty/lua/course_device_status.lua;
+    }
+    location ^~ /course/api/ {
+        default_type application/json;
+
+        add_header 'Access-Control-Allow-Origin' '*' always;
+        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE' always;
+        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
+        add_header 'Access-Control-Allow-Credentials' 'true' always;
+
+        # 移除 rewrite,改用 proxy_pass 直接处理
+        proxy_pass http://124.222.28.5:7213/;
+
+        proxy_set_header Host m1.apifoxmock.com;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+
+        # 处理代理失败的情况(如404)
+        proxy_intercept_errors on;
+        error_page 404 = @course_api_fallback;
+    }
+
+    location @course_api_fallback {
+        default_type application/json;
+        content_by_lua_block {
+            local uri = ngx.var.uri
+
+            if uri:find("^/course/api/system/dict") then
+                -- 字典相关接口
+                local dict = require "course_dict"
+                dict.handle(uri)
+            else
+                -- 其他接口走 mock
+                local mock_router = require "course_mock"
+                mock_router("/course/api")
+            end
+        }
+    }
+    #-------------------------------------------------
+    # 健康检查
+    #-------------------------------------------------
+    location /health {
+        access_log off;
+        return 200 'healthy';
+        add_header Content-Type text/plain;
+    }
+
+    #-------------------------------------------------
+    # 错误页
+    #-------------------------------------------------
+    error_page 403 /403.html;
+    error_page 404 /404.html;
+    error_page 500 502 503 504 /50x.html;
+
+    location = /403.html {
+    }
+    location = /404.html {
+    }
+    location = /50x.html {
+    }
+    #-------------------------------------------------
+    # 安全配置
+    #-------------------------------------------------
+    # 安全配置:禁止访问隐藏文件
+    location ~ /\. {
+        deny all;
+        access_log off;
+        log_not_found off;
+    }
+    # 安全配置:禁止访问常见敏感文件
+    location ~* (\.log|\.sql|\.env|\.git) {
+        deny all;
+        access_log off;
+        log_not_found off;
+    }
+}
+#=====================================================
+# HTTP -> HTTPS 自动跳转
+#=====================================================
+server {
+    listen 80;
+    server_name _;
+    return 301 https://$host$request_uri;
+}

+ 67 - 0
develop/nginx/html/403.html

@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>403 禁止访问</title>
+    <style>
+        body {
+            font-family: 'Microsoft YaHei', Arial, sans-serif;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            margin: 0;
+            padding: 0;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            min-height: 100vh;
+            color: white;
+            text-align: center;
+        }
+        .error-container {
+            background: rgba(255, 255, 255, 0.1);
+            padding: 50px;
+            border-radius: 20px;
+            backdrop-filter: blur(10px);
+            box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
+        }
+        .error-code {
+            font-size: 120px;
+            font-weight: bold;
+            margin: 0;
+            text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2);
+        }
+        .error-message {
+            font-size: 24px;
+            margin: 20px 0;
+        }
+        .error-description {
+            font-size: 16px;
+            margin: 20px 0;
+            opacity: 0.9;
+        }
+        .home-btn {
+            display: inline-block;
+            padding: 12px 30px;
+            background: white;
+            color: #667eea;
+            text-decoration: none;
+            border-radius: 25px;
+            font-weight: bold;
+            margin-top: 20px;
+            transition: transform 0.3s ease;
+        }
+        .home-btn:hover {
+            transform: translateY(-2px);
+            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
+        }
+    </style>
+</head>
+<body>
+    <div class="error-container">
+        <h1 class="error-code">403</h1>
+        <h2 class="error-message">禁止访问</h2>
+        <p class="error-description">抱歉,您没有权限访问此页面。</p>
+        <a href="/" class="home-btn">返回首页</a>
+    </div>
+</body>
+</html>

+ 77 - 0
develop/nginx/html/404.html

@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>404 页面未找到</title>
+    <style>
+        body {
+            font-family: 'Microsoft YaHei', Arial, sans-serif;
+            background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+            margin: 0;
+            padding: 0;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            min-height: 100vh;
+            color: white;
+            text-align: center;
+        }
+        .error-container {
+            background: rgba(255, 255, 255, 0.1);
+            padding: 50px;
+            border-radius: 20px;
+            backdrop-filter: blur(10px);
+            box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
+        }
+        .error-code {
+            font-size: 120px;
+            font-weight: bold;
+            margin: 0;
+            text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2);
+        }
+        .error-message {
+            font-size: 24px;
+            margin: 20px 0;
+        }
+        .error-description {
+            font-size: 16px;
+            margin: 20px 0;
+            opacity: 0.9;
+        }
+        .home-btn {
+            display: inline-block;
+            padding: 12px 30px;
+            background: white;
+            color: #f5576c;
+            text-decoration: none;
+            border-radius: 25px;
+            font-weight: bold;
+            margin-top: 20px;
+            transition: transform 0.3s ease;
+        }
+        .home-btn:hover {
+            transform: translateY(-2px);
+            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
+        }
+        .search-box {
+            margin: 20px 0;
+        }
+        .search-input {
+            padding: 10px 15px;
+            border: none;
+            border-radius: 25px;
+            width: 250px;
+            font-size: 14px;
+        }
+    </style>
+</head>
+<body>
+    <div class="error-container">
+        <h1 class="error-code">404</h1>
+        <h2 class="error-message">页面未找到</h2>
+        <p class="error-description">抱歉,您访问的页面不存在或已被移动。</p>
+        <a href="/" class="home-btn">返回首页</a>
+    </div>
+</body>
+</html>

+ 98 - 0
develop/nginx/html/50x.html

@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>500 服务器错误</title>
+    <style>
+        body {
+            font-family: 'Microsoft YaHei', Arial, sans-serif;
+            background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+            margin: 0;
+            padding: 0;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            min-height: 100vh;
+            color: white;
+            text-align: center;
+        }
+        .error-container {
+            background: rgba(255, 255, 255, 0.1);
+            padding: 50px;
+            border-radius: 20px;
+            backdrop-filter: blur(10px);
+            box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
+        }
+        .error-code {
+            font-size: 120px;
+            font-weight: bold;
+            margin: 0;
+            text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2);
+        }
+        .error-message {
+            font-size: 24px;
+            margin: 20px 0;
+        }
+        .error-description {
+            font-size: 16px;
+            margin: 20px 0;
+            opacity: 0.9;
+            line-height: 1.6;
+        }
+        .home-btn {
+            display: inline-block;
+            padding: 12px 30px;
+            background: white;
+            color: #4facfe;
+            text-decoration: none;
+            border-radius: 25px;
+            font-weight: bold;
+            margin-top: 20px;
+            transition: transform 0.3s ease;
+        }
+        .home-btn:hover {
+            transform: translateY(-2px);
+            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
+        }
+        .retry-btn {
+            display: inline-block;
+            padding: 12px 30px;
+            background: transparent;
+            color: white;
+            border: 2px solid white;
+            text-decoration: none;
+            border-radius: 25px;
+            font-weight: bold;
+            margin: 10px;
+            transition: all 0.3s ease;
+        }
+        .retry-btn:hover {
+            background: white;
+            color: #4facfe;
+        }
+        .contact-info {
+            margin-top: 30px;
+            font-size: 14px;
+            opacity: 0.8;
+        }
+    </style>
+</head>
+<body>
+    <div class="error-container">
+        <h1 class="error-code">500</h1>
+        <h2 class="error-message">服务器内部错误</h2>
+        <p class="error-description">
+            抱歉,服务器遇到了一个意外错误。<br>
+            我们的技术团队已经收到通知,正在努力修复问题。
+        </p>
+        <div>
+            <a href="javascript:location.reload()" class="retry-btn">重试页面</a>
+            <a href="/" class="home-btn">返回首页</a>
+        </div>
+        <div class="contact-info">
+            如果问题持续存在,请联系技术支持
+        </div>
+    </div>
+</body>
+</html>

+ 347 - 0
develop/nginx/html/index.html

@@ -0,0 +1,347 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>欢迎访问我们的网站</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+        
+        body {
+            font-family: 'Microsoft YaHei', 'Segoe UI', Arial, sans-serif;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            min-height: 100vh;
+            color: #333;
+            line-height: 1.6;
+        }
+        
+        .container {
+            max-width: 1200px;
+            margin: 0 auto;
+            padding: 20px;
+        }
+        
+        /* 头部样式 */
+        header {
+            background: rgba(255, 255, 255, 0.95);
+            backdrop-filter: blur(10px);
+            padding: 20px 0;
+            box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
+            position: sticky;
+            top: 0;
+            z-index: 1000;
+        }
+        
+        .navbar {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+        }
+        
+        .logo {
+            font-size: 28px;
+            font-weight: bold;
+            background: linear-gradient(135deg, #667eea, #764ba2);
+            -webkit-background-clip: text;
+            -webkit-text-fill-color: transparent;
+        }
+        
+        .nav-links {
+            display: flex;
+            list-style: none;
+            gap: 30px;
+        }
+        
+        .nav-links a {
+            text-decoration: none;
+            color: #333;
+            font-weight: 500;
+            transition: color 0.3s ease;
+            position: relative;
+        }
+        
+        .nav-links a:hover {
+            color: #667eea;
+        }
+        
+        .nav-links a::after {
+            content: '';
+            position: absolute;
+            bottom: -5px;
+            left: 0;
+            width: 0;
+            height: 2px;
+            background: #667eea;
+            transition: width 0.3s ease;
+        }
+        
+        .nav-links a:hover::after {
+            width: 100%;
+        }
+        
+        /* 英雄区域样式 */
+        .hero {
+            text-align: center;
+            padding: 100px 0;
+            color: white;
+        }
+        
+        .hero h1 {
+            font-size: 3.5rem;
+            margin-bottom: 20px;
+            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
+            animation: fadeInUp 1s ease;
+        }
+        
+        .hero p {
+            font-size: 1.3rem;
+            margin-bottom: 30px;
+            opacity: 0.9;
+            max-width: 600px;
+            margin-left: auto;
+            margin-right: auto;
+            animation: fadeInUp 1s ease 0.2s both;
+        }
+        
+        .cta-buttons {
+            display: flex;
+            gap: 20px;
+            justify-content: center;
+            animation: fadeInUp 1s ease 0.4s both;
+        }
+        
+        .btn {
+            padding: 15px 35px;
+            border: none;
+            border-radius: 30px;
+            font-size: 1.1rem;
+            font-weight: 600;
+            cursor: pointer;
+            transition: all 0.3s ease;
+            text-decoration: none;
+            display: inline-block;
+        }
+        
+        .btn-primary {
+            background: white;
+            color: #667eea;
+            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
+        }
+        
+        .btn-primary:hover {
+            transform: translateY(-3px);
+            box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
+        }
+        
+        .btn-secondary {
+            background: transparent;
+            color: white;
+            border: 2px solid white;
+        }
+        
+        .btn-secondary:hover {
+            background: white;
+            color: #667eea;
+            transform: translateY(-3px);
+        }
+        
+        /* 特性区域样式 */
+        .features {
+            padding: 80px 0;
+            background: rgba(255, 255, 255, 0.95);
+            border-radius: 20px;
+            margin-top: 50px;
+            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
+        }
+        
+        .section-title {
+            text-align: center;
+            font-size: 2.5rem;
+            margin-bottom: 50px;
+            background: linear-gradient(135deg, #667eea, #764ba2);
+            -webkit-background-clip: text;
+            -webkit-text-fill-color: transparent;
+        }
+        
+        .features-grid {
+            display: grid;
+            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+            gap: 30px;
+            padding: 0 20px;
+        }
+        
+        .feature-card {
+            background: white;
+            padding: 40px 30px;
+            border-radius: 15px;
+            text-align: center;
+            box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
+            transition: transform 0.3s ease, box-shadow 0.3s ease;
+        }
+        
+        .feature-card:hover {
+            transform: translateY(-10px);
+            box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15);
+        }
+        
+        .feature-icon {
+            font-size: 3rem;
+            margin-bottom: 20px;
+            background: linear-gradient(135deg, #667eea, #764ba2);
+            -webkit-background-clip: text;
+            background-clip: text;
+            -webkit-text-fill-color: transparent;
+        }
+        
+        .feature-card h3 {
+            font-size: 1.5rem;
+            margin-bottom: 15px;
+            color: #333;
+        }
+        
+        .feature-card p {
+            color: #666;
+            line-height: 1.6;
+        }
+        
+        /* 页脚样式 */
+        footer {
+            background: rgba(0, 0, 0, 0.8);
+            color: white;
+            text-align: center;
+            padding: 30px 0;
+            margin-top: 80px;
+            border-radius: 20px 20px 0 0;
+        }
+        
+        /* 动画 */
+        @keyframes fadeInUp {
+            from {
+                opacity: 0;
+                transform: translateY(30px);
+            }
+            to {
+                opacity: 1;
+                transform: translateY(0);
+            }
+        }
+        
+        /* 响应式设计 */
+        @media (max-width: 768px) {
+            .hero h1 {
+                font-size: 2.5rem;
+            }
+            
+            .hero p {
+                font-size: 1.1rem;
+            }
+            
+            .cta-buttons {
+                flex-direction: column;
+                align-items: center;
+            }
+            
+            .btn {
+                width: 200px;
+            }
+            
+            .nav-links {
+                display: none;
+            }
+            
+            .mobile-menu {
+                display: block;
+            }
+        }
+        
+        /* 测试链接区域 */
+        .test-links {
+            background: rgba(255, 255, 255, 0.1);
+            padding: 30px;
+            border-radius: 15px;
+            margin-top: 50px;
+            text-align: center;
+            color: white;
+        }
+        
+        .test-links h3 {
+            margin-bottom: 20px;
+            font-size: 1.5rem;
+        }
+        
+        .test-buttons {
+            display: flex;
+            gap: 15px;
+            justify-content: center;
+            flex-wrap: wrap;
+        }
+        
+        .test-btn {
+            padding: 10px 20px;
+            background: rgba(255, 255, 255, 0.2);
+            color: white;
+            border: 1px solid rgba(255, 255, 255, 0.3);
+            border-radius: 20px;
+            text-decoration: none;
+            transition: all 0.3s ease;
+        }
+        
+        .test-btn:hover {
+            background: rgba(255, 255, 255, 0.3);
+            transform: translateY(-2px);
+        }
+    </style>
+</head>
+<body>
+    <!-- 头部导航 -->
+    <header>
+        <div class="container">
+            <nav class="navbar">
+                <div class="logo">教员管理系统</div>
+            </nav>
+        </div>
+    </header>
+
+    <!-- 主要内容 -->
+    <div class="container">
+        <!-- 英雄区域 -->
+        <section class="hero" id="home">
+            <h1>欢迎访问我们的教员管理系统</h1>
+            <p>探索无限可能,体验卓越服务。我们致力于为您提供最优质的用户体验和最可靠的服务保障。</p>
+        </section>
+
+        <!-- 特性介绍 -->
+        <section class="features" id="features">
+            <h2 class="section-title">我们的优势</h2>
+            <div class="features-grid">
+                <div class="feature-card">
+                    <div class="feature-icon">🚀</div>
+                    <h3>高性能</h3>
+                    <p>采用最新技术栈,确保网站快速响应,为用户提供流畅的浏览体验。</p>
+                </div>
+                <div class="feature-card">
+                    <div class="feature-icon">🔒</div>
+                    <h3>安全可靠</h3>
+                    <p>多层次安全防护,保障您的数据安全和隐私保护。</p>
+                </div>
+                <div class="feature-card">
+                    <div class="feature-icon">💡</div>
+                    <h3>创新设计</h3>
+                    <p>现代化界面设计,直观易用的操作流程,提升用户体验。</p>
+                </div>
+            </div>
+        </section>
+    </div>
+    <!-- 页脚 -->
+    <footer>
+        <div class="container">
+            <p>&copy; 2025 教员管理系统. 保留所有权利。</p>
+            <p>技术支持 | 服务条款 | 隐私政策</p>
+        </div>
+    </footer>
+</body>
+</html>

+ 76 - 0
develop/nginx/nginx.conf

@@ -0,0 +1,76 @@
+#user  nobody;
+worker_processes auto;
+
+error_log logs/error.log error; # 只记录严重错误,减少磁盘IO和日志文件大小
+#error_log  logs/error.log;
+#error_log  logs/error.log  notice;
+#error_log  logs/error.log  info;
+
+#pid        logs/nginx.pid;
+
+events {
+    worker_connections  1024;    # 单个worker进程最大连接数
+    multi_accept on;             # 一次性接受所有新连接
+    use epoll;
+}
+#=====================================================
+# HTTP 全局块
+#=====================================================
+http {
+    include       mime.types;
+    default_type  application/octet-stream;
+
+    #-------------------------------------------------
+    # 日志配置
+    #-------------------------------------------------
+    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+                    '$status $body_bytes_sent "$http_referer" '
+                    '"$http_user_agent" "$request_time" "$upstream_response_time"';
+    access_log logs/access.log main;
+    error_log  logs/error.log warn;
+
+    #-------------------------------------------------
+    # 性能优化
+    #-------------------------------------------------
+    server_tokens off;           # 隐藏NGINX版本信息
+    # I/O优化 - 减少带宽消耗和提高传输效率
+    sendfile on;                # 开启零拷贝文:件传输,减少CPU消耗和内核上下文切换
+    tcp_nopush on;              # 优化数据包传输,减少网络报文数量,配合sendfile使用
+    tcp_nodelay on;             # 禁用Nagle算法,降低小数据包传输延迟
+    # 连接优化 - 减少TCP连接建立的开销
+    keepalive_timeout 65;       # 保持客户端连接超时时间(秒),减少重复的TCP握手
+    keepalive_requests 100;     # 在一个keepalive连接上可处理的最大请求数
+
+    #-------------------------------------------------
+    # 静态文件缓存优化 - 减少重复读取文件的开销
+    #-------------------------------------------------
+    open_file_cache max=10000 inactive=20s;  # 缓存最多10000个文件描述符,20秒不使用则关闭
+    open_file_cache_valid 30s;               # 每30秒检查一次缓存中的文件描述符是否仍然有效
+    open_file_cache_min_uses 2;              # 文件被访问至少2次后才会被缓存,降低低频访问文件的缓存
+    open_file_cache_errors on;               # 同时缓存文件错误信息,避免重复访问不存在的文件
+
+    #-------------------------------------------------
+    # Gzip 压缩配置 - 显著减少传输数据量,节省带宽
+    #-------------------------------------------------
+    gzip on;                    # 启用gzip压缩,可减少传输数据量40%-70%
+    gzip_comp_level 6;          # 压缩级别(1-9),一般来说推荐你设置成 6 就比较合适
+    gzip_min_length 1k;         # 最小压缩文件大小(字节),小于此值的文件不压缩
+    gzip_buffers 16 8k;         # 压缩缓冲区大小,16个8k的缓冲区
+    gzip_proxied any;           # 对所有代理请求进行压缩,包括反向代理的响应
+    gzip_vary on;               # 添加Vary: Accept-Encoding响应头,正确处理缓存
+    gzip_static on;             # 优先使用预压缩.gz文件,减少CPU使用率
+    gzip_http_version 1.1;      # 使用HTTP 1.1版本,支持gzip压缩
+    gzip_disable "MSIE [1-6]\."; # 对IE6及以下浏览器禁用gzip,避免兼容性问题
+    gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml application/xml+rss image/svg+xml image/png font/eot font/otf font/ttf application/octet-stream;
+
+    #-------------------------------------------------
+    # 上传与超时限制
+    #-------------------------------------------------
+    client_max_body_size 50m;
+    client_body_timeout 20s;
+    client_header_timeout 20s;
+    send_timeout 30s;
+
+    include conf.d/*.conf;
+}
+

+ 17 - 14
src/components/business/EasyProcess/common/process.data.ts

@@ -46,14 +46,14 @@ export const nodeConfigData: Record<Process.NodeType, Process.NodeMeta> = {
     icon: { name: '&#xe7b7;' },
     defaultNode: () => ({ nodeId: generateId(), nodeName: '条件', nodeType: 'condition', isLastCondition: false, nodeData: { condition: '1' } })
   },
-  fault: {
-    title: '故障触发',
-    bgColor: '#D74545',
+  operation: {
+    title: '基本操作',
+    bgColor: '#476B9F',
     canAdd: true,
     canRemoved: true,
     hasDrawer: true,
-    icon: { name: '&#xe7cb;' },
-    defaultNode: () => ({ nodeId: generateId(), nodeName: '故障触发', nodeType: 'fault', nodeData: {} })
+    icon: { name: '&#xe60d;' },
+    defaultNode: () => ({ nodeId: generateId(), nodeName: '基本操作', nodeType: 'operation', nodeData: {} })
   },
   voice: {
     title: '语音交互',
@@ -64,14 +64,14 @@ export const nodeConfigData: Record<Process.NodeType, Process.NodeMeta> = {
     icon: { name: '&#xe6e4;' },
     defaultNode: () => ({ nodeId: generateId(), nodeName: '语音交互', nodeType: 'voice', nodeData: {} })
   },
-  operation: {
-    title: '基本操作',
-    bgColor: '#476B9F',
+  fault: {
+    title: '故障触发',
+    bgColor: '#D74545',
     canAdd: true,
     canRemoved: true,
     hasDrawer: true,
-    icon: { name: '&#xe60d;' },
-    defaultNode: () => ({ nodeId: generateId(), nodeName: '基本操作', nodeType: 'operation', nodeData: {} })
+    icon: { name: '&#xe7cb;' },
+    defaultNode: () => ({ nodeId: generateId(), nodeName: '故障触发', nodeType: 'fault', nodeData: {} })
   }
 }
 
@@ -79,11 +79,14 @@ export const blankProcessData = () => nodeConfigData['start'].defaultNode()
 
 export const nodeRoleList = [
   { value: 'role_default', label: '默认角色', id: '100' },
-  { value: 'role_substation', label: '变电检修员', id: '103' },
-  { value: 'role_catenary', label: '触网检修员', id: '105' },
   { value: 'role_dispatch', label: '电力调度员', id: '101' },
+  { value: 'role_supply', label: '供电值班员', id: '102' },
+  { value: 'role_substation', label: '变电检修员', id: '103' },
   { value: 'role_isolator', label: '隔开检修员', id: '104' },
-  { value: 'role_supply', label: '供电值班员', id: '102' }
+  { value: 'role_catenary', label: '触网检修员', id: '105' }
 ]
 
-export const getNodeRole = (role: string) => nodeRoleList.find((item) => item.label === role)?.value || 'role_default'
+export const getNodeRole = (role: string) => {
+  console.log(role)
+  return nodeRoleList.find((item) => item.id === role)?.value || 'role_default'
+}

+ 37 - 6
src/components/business/EasyProcess/node/fault_drawer.vue

@@ -138,18 +138,49 @@
     }
   }
 
-  onMounted(async () => {
-    const { data } = await ProcessServer.faultList()
-    templateList.value = data || []
-    nextTick(() => {
-      formApi.setValues(props.node.nodeData || {})
+  onMounted(() => {
+    nextTick(async () => {
+      const { data } = await ProcessServer.faultList()
+      templateList.value = data || []
+      const faultNode = props.node.nodeData?.faultNode as Record<string, any> | undefined
+      handleTemplateChange(String(faultNode?.templateID || 0))
+      formApi.setValues({
+        templateID: String(faultNode?.templateID || 0),
+        categoryName: faultNode?.categoryName || '',
+        subCategoryName: faultNode?.subCategoryName || '',
+        param1Type: String(faultNode?.param1Type || '2'),
+        param2Type: String(faultNode?.param2Type || '2'),
+        param3Type: String(faultNode?.param3Type || '2'),
+        param4Type: String(faultNode?.param4Type || '2'),
+        param5Type: String(faultNode?.param5Type || '2'),
+        param1Desc: faultNode?.param1Desc || '',
+        param2Desc: faultNode?.param2Desc || '',
+        param3Desc: faultNode?.param3Desc || '',
+        param4Desc: faultNode?.param4Desc || '',
+        param5Desc: faultNode?.param5Desc || ''
+      })
     })
   })
 
   defineExpose({
     getValues: async () => {
       await formApi.validate()
-      return formApi.getValues()
+      const values = formApi.getValues() as Record<string, any> | undefined
+      return {
+        templateID: Number(values?.templateID ?? '0'),
+        categoryName: values?.categoryName ?? '',
+        subCategoryName: values?.subCategoryName ?? '',
+        param1Type: Number(values?.param1Type ?? '2'),
+        param2Type: Number(values?.param2Type ?? '2'),
+        param3Type: Number(values?.param3Type ?? '2'),
+        param4Type: Number(values?.param4Type ?? '2'),
+        param5Type: Number(values?.param5Type ?? '2'),
+        param1Desc: values?.param1Desc ?? '',
+        param2Desc: values?.param2Desc ?? '',
+        param3Desc: values?.param3Desc ?? '',
+        param4Desc: values?.param4Desc ?? '',
+        param5Desc: values?.param5Desc ?? ''
+      }
     }
   })
 </script>

+ 1 - 1
src/components/business/EasyProcess/node/fault_node.vue

@@ -18,7 +18,7 @@
     }
   )
 
-  const getIcon = computed(() => `svg:${getNodeRole(String(nodeData.value?.role) || '')}`)
+  const getIcon = computed(() => `svg:${getNodeRole(String(nodeData.value?.bindRole) || '')}`)
 
   const validate = inject<ProcessValidator>(ProcessConstant.KEY_PROCESS_VALIDATOR)
 

+ 41 - 5
src/components/business/EasyProcess/node/operation_drawer.vue

@@ -155,16 +155,52 @@
     }
   }
 
-  onMounted(async () => {
-    const { data } = await ProcessServer.operateList()
-    templateList.value = data || []
-    formApi.setValues(props.node.nodeData || {})
+  onMounted(() => {
+    nextTick(async () => {
+      const { data } = await ProcessServer.operateList()
+      templateList.value = data || []
+      const operationNode = props.node.nodeData?.operationNode as Record<string, any> | undefined
+      formApi.setValues({
+        templateID: String(operationNode?.templateID || ''),
+        categoryName: operationNode?.categoryName || '',
+        subCategoryName: operationNode?.subCategoryName || '',
+        param1Type: String(operationNode?.param1Type || '2'),
+        param2Type: String(operationNode?.param2Type || '2'),
+        param3Type: String(operationNode?.param3Type || '2'),
+        param4Type: String(operationNode?.param4Type || '2'),
+        param5Type: String(operationNode?.param5Type || '2'),
+        param1Desc: operationNode?.param1Desc || '',
+        param2Desc: operationNode?.param2Desc || '',
+        param3Desc: operationNode?.param3Desc || '',
+        param4Desc: operationNode?.param4Desc || '',
+        param5Desc: operationNode?.param5Desc || '',
+        score: Number(operationNode?.score || 0),
+        deduction: Number(operationNode?.deduction || 0)
+      })
+    })
   })
 
   defineExpose({
     getValues: async () => {
       await formApi.validate()
-      return formApi.getValues()
+      const values = formApi.getValues() as Record<string, any> | undefined
+      return {
+        templateID: Number(values?.templateID ?? '0'),
+        categoryName: values?.categoryName ?? '',
+        subCategoryName: values?.subCategoryName ?? '',
+        param1Type: Number(values?.param1Type ?? '2'),
+        param2Type: Number(values?.param2Type ?? '2'),
+        param3Type: Number(values?.param3Type ?? '2'),
+        param4Type: Number(values?.param4Type ?? '2'),
+        param5Type: Number(values?.param5Type ?? '2'),
+        param1Desc: values?.param1Desc ?? '',
+        param2Desc: values?.param2Desc ?? '',
+        param3Desc: values?.param3Desc ?? '',
+        param4Desc: values?.param4Desc ?? '',
+        param5Desc: values?.param5Desc ?? '',
+        score: Number(values?.score ?? '0'),
+        deduction: Number(values?.deduction ?? '0')
+      }
     }
   })
 </script>

+ 1 - 1
src/components/business/EasyProcess/node/operation_node.vue

@@ -17,7 +17,7 @@
       nodeData.value = newVal || {}
     }
   )
-  const getIcon = computed(() => `svg:${getNodeRole(String(nodeData.value?.role) || '')}`)
+  const getIcon = computed(() => `svg:${getNodeRole(String(nodeData.value?.bindRole) || '')}`)
 
   const validate = inject<ProcessValidator>(ProcessConstant.KEY_PROCESS_VALIDATOR)
 

+ 24 - 6
src/components/business/EasyProcess/node/voice_drawer.vue

@@ -62,18 +62,36 @@
     }
   }
 
-  onMounted(async () => {
-    const { data } = await ProcessServer.voiceList()
-    templateList.value = data || []
-    nextTick(() => {
-      formApi.setValues(props.node.nodeData || {})
+  onMounted(() => {
+    nextTick(async () => {
+      const { data } = await ProcessServer.voiceList()
+      templateList.value = data || []
+      const voiceNode = props.node.nodeData?.voiceNode as Record<string, any> | undefined
+      nextTick(() => {
+        formApi.setValues({
+          templateID: String(voiceNode?.templateID || ''),
+          categoryName: voiceNode?.categoryName || '',
+          subCategoryName: voiceNode?.subCategoryName || '',
+          bindRole: String(voiceNode?.bindRole || ''),
+          voiceContent: voiceNode?.voiceContent || '',
+          evalContent: voiceNode?.evalContent || ''
+        })
+      })
     })
   })
 
   defineExpose({
     getValues: async () => {
       await formApi.validate()
-      return formApi.getValues()
+      const values = formApi.getValues() as Record<string, any> | undefined
+      return {
+        templateID: Number(values?.templateID ?? '0'),
+        categoryName: values?.categoryName ?? '',
+        subCategoryName: values?.subCategoryName ?? '',
+        bindRole: Number(values?.bindRole ?? '0'),
+        voiceContent: values?.voiceContent ?? '',
+        evalContent: values?.evalContent ?? ''
+      }
     }
   })
 </script>

+ 3 - 3
src/components/business/EasyProcess/node/voice_node.vue

@@ -18,17 +18,17 @@
       console.log('nodeData.value', nodeData.value)
     }
   )
-  const getIcon = computed(() => `svg:${getNodeRole(String(nodeData.value?.role) || '')}`)
+  const getIcon = computed(() => `svg:${getNodeRole(String(nodeData.value?.bindRole) || '')}`)
 
   const validate = inject<ProcessValidator>(ProcessConstant.KEY_PROCESS_VALIDATOR)
 
   validate?.register(props.node.nodeId, () => {
     const messages: string[] = []
     if (!nodeData.value?.name) {
-      messages.push('请输入语言名称')
+      messages.push('请输入语音节点名称')
     }
     if (!nodeData.value?.bindRole) {
-      messages.push('请选择语言角色')
+      messages.push('请选择语音节点角色')
     }
     return { valid: messages.length === 0, message: messages.join(',') }
   })