Add application variables

This commit is contained in:
2025-10-26 00:08:55 +08:00
parent f41e3dac21
commit 8513cbce55
10 changed files with 1020 additions and 26 deletions

View File

@@ -168,7 +168,7 @@ func APIUpdateHandler(w http.ResponseWriter, r *http.Request) {
}
var req struct {
ID uint `json:"id"`
UUID string `json:"uuid"`
Status int `json:"status"`
SubmitAlgorithm int `json:"submit_algorithm"`
ReturnAlgorithm int `json:"return_algorithm"`
@@ -184,8 +184,8 @@ func APIUpdateHandler(w http.ResponseWriter, r *http.Request) {
}
// 验证必填字段
if req.ID == 0 {
http.Error(w, "接口ID不能为空", http.StatusBadRequest)
if strings.TrimSpace(req.UUID) == "" {
http.Error(w, "接口UUID不能为空", http.StatusBadRequest)
return
}
@@ -209,7 +209,7 @@ func APIUpdateHandler(w http.ResponseWriter, r *http.Request) {
// 查找并更新API记录
var api models.API
if err := db.First(&api, req.ID).Error; err != nil {
if err := db.Where("uuid = ?", strings.TrimSpace(req.UUID)).First(&api).Error; err != nil {
http.Error(w, "接口不存在", http.StatusNotFound)
return
}

View File

@@ -0,0 +1,429 @@
package admin
import (
"encoding/json"
"net/http"
"networkDev/database"
"networkDev/models"
"networkDev/utils"
"regexp"
"strconv"
"strings"
"github.com/sirupsen/logrus"
)
// VariableFragmentHandler 变量列表页面片段处理器
func VariableFragmentHandler(w http.ResponseWriter, r *http.Request) {
utils.RenderTemplate(w, "variables", map[string]interface{}{
"Title": "变量管理",
})
}
// VariableListHandler 变量列表API处理器
func VariableListHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 获取分页参数
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page <= 0 {
page = 1
}
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit <= 0 {
limit = 10
}
// 获取应用UUID参数用于按应用筛选变量
appUUID := strings.TrimSpace(r.URL.Query().Get("app_uuid"))
// 获取别名搜索参数
alias := strings.TrimSpace(r.URL.Query().Get("alias"))
// 构建查询
db, err := database.GetDB()
if err != nil {
logrus.WithError(err).Error("Failed to get database connection")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// 构建基础查询
query := db.Model(&models.Variable{})
// 如果指定了应用UUID则按应用筛选
if appUUID != "" {
query = query.Where("app_uuid = ?", appUUID)
}
// 如果指定了别名搜索,则按别名模糊搜索
if alias != "" {
query = query.Where("alias LIKE ?", "%"+alias+"%")
}
// 获取总数
var total int64
if err := query.Count(&total).Error; err != nil {
logrus.WithError(err).Error("Failed to count variables")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// 获取分页数据
var variables []models.Variable
offset := (page - 1) * limit
if err := query.Offset(offset).Limit(limit).Order("created_at DESC").Find(&variables).Error; err != nil {
logrus.WithError(err).Error("Failed to fetch variables")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// 获取关联的应用信息
var appUUIDs []string
for _, variable := range variables {
appUUIDs = append(appUUIDs, variable.AppUUID)
}
var apps []models.App
if len(appUUIDs) > 0 {
if err := db.Where("uuid IN ?", appUUIDs).Find(&apps).Error; err != nil {
logrus.WithError(err).Error("Failed to fetch related apps")
}
}
// 创建应用UUID到应用名称的映射
appMap := make(map[string]string)
for _, app := range apps {
appMap[app.UUID] = app.Name
}
// 构建响应数据
type VariableResponse struct {
ID uint `json:"id"`
UUID string `json:"uuid"`
Number string `json:"number"`
AppUUID string `json:"app_uuid"`
AppName string `json:"app_name"`
Alias string `json:"alias"`
Data string `json:"data"`
Remark string `json:"remark"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
var responseData []VariableResponse
for _, variable := range variables {
appName := appMap[variable.AppUUID]
if appName == "" {
appName = "未知应用"
}
responseData = append(responseData, VariableResponse{
ID: variable.ID,
UUID: variable.UUID,
Number: variable.Number,
AppUUID: variable.AppUUID,
AppName: appName,
Alias: variable.Alias,
Data: variable.Data,
Remark: variable.Remark,
CreatedAt: variable.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: variable.UpdatedAt.Format("2006-01-02 15:04:05"),
})
}
response := map[string]interface{}{
"code": 0,
"msg": "success",
"count": total,
"data": responseData,
}
w.Header().Set("Content-Type", "application/json")
utils.WriteJSONResponse(w, http.StatusOK, response)
}
// VariableCreateHandler 新增变量API处理器
func VariableCreateHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
AppUUID string `json:"app_uuid"`
Alias string `json:"alias"`
Data string `json:"data"`
Remark string `json:"remark"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
logrus.WithError(err).Error("Failed to decode JSON request")
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// 验证必填字段
if strings.TrimSpace(req.AppUUID) == "" {
http.Error(w, "应用UUID不能为空", http.StatusBadRequest)
return
}
if strings.TrimSpace(req.Alias) == "" {
http.Error(w, "变量别名不能为空", http.StatusBadRequest)
return
}
// 验证别名格式:必须以英文字母开头,只能包含数字和英文字母
aliasPattern := regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9]*$`)
if !aliasPattern.MatchString(req.Alias) {
http.Error(w, "别名必须以英文字母开头,只能包含数字和英文字母", http.StatusBadRequest)
return
}
db, err := database.GetDB()
if err != nil {
logrus.WithError(err).Error("Failed to get database connection")
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
return
}
// 验证应用是否存在
var app models.App
if err := db.Where("uuid = ?", req.AppUUID).First(&app).Error; err != nil {
http.Error(w, "应用不存在", http.StatusBadRequest)
return
}
// 创建变量
variable := models.Variable{
AppUUID: strings.TrimSpace(req.AppUUID),
Alias: strings.TrimSpace(req.Alias),
Data: req.Data,
Remark: strings.TrimSpace(req.Remark),
}
if err := db.Create(&variable).Error; err != nil {
logrus.WithError(err).Error("Failed to create variable")
http.Error(w, "创建变量失败", http.StatusInternalServerError)
return
}
response := map[string]interface{}{
"code": 0,
"msg": "创建成功",
"data": variable,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// VariableUpdateHandler 更新变量API处理器
func VariableUpdateHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
UUID string `json:"uuid"`
AppUUID string `json:"app_uuid"`
Alias string `json:"alias"`
Data string `json:"data"`
Remark string `json:"remark"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// 验证必填字段
if strings.TrimSpace(req.UUID) == "" {
http.Error(w, "变量UUID不能为空", http.StatusBadRequest)
return
}
if strings.TrimSpace(req.AppUUID) == "" {
http.Error(w, "应用UUID不能为空", http.StatusBadRequest)
return
}
if strings.TrimSpace(req.Alias) == "" {
http.Error(w, "变量别名不能为空", http.StatusBadRequest)
return
}
// 验证别名格式:必须以英文字母开头,只能包含数字和英文字母
aliasPattern := regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9]*$`)
if !aliasPattern.MatchString(req.Alias) {
http.Error(w, "别名必须以英文字母开头,只能包含数字和英文字母", http.StatusBadRequest)
return
}
db, err := database.GetDB()
if err != nil {
logrus.WithError(err).Error("Failed to get database connection")
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
return
}
// 验证应用是否存在
var app models.App
if err := db.Where("uuid = ?", req.AppUUID).First(&app).Error; err != nil {
http.Error(w, "应用不存在", http.StatusBadRequest)
return
}
// 通过uuid字段查找变量
var variable models.Variable
if err := db.Where("uuid = ?", strings.TrimSpace(req.UUID)).First(&variable).Error; err != nil {
http.Error(w, "变量不存在", http.StatusNotFound)
return
}
// 更新字段
variable.AppUUID = strings.TrimSpace(req.AppUUID)
variable.Alias = strings.TrimSpace(req.Alias)
variable.Data = req.Data
variable.Remark = strings.TrimSpace(req.Remark)
if err := db.Save(&variable).Error; err != nil {
logrus.WithError(err).Error("Failed to update variable")
http.Error(w, "更新变量失败", http.StatusInternalServerError)
return
}
response := map[string]interface{}{
"code": 0,
"msg": "更新成功",
"data": variable,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// VariableDeleteHandler 删除变量API处理器
func VariableDeleteHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
ID uint `json:"id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if req.ID == 0 {
http.Error(w, "变量ID不能为空", http.StatusBadRequest)
return
}
db, err := database.GetDB()
if err != nil {
logrus.WithError(err).Error("Failed to get database connection")
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
return
}
// 删除变量
if err := db.Delete(&models.Variable{}, req.ID).Error; err != nil {
logrus.WithError(err).Error("Failed to delete variable")
http.Error(w, "删除变量失败", http.StatusInternalServerError)
return
}
logrus.WithField("variable_id", req.ID).Info("Successfully deleted variable")
response := map[string]interface{}{
"code": 0,
"msg": "删除成功",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// VariablesBatchDeleteHandler 批量删除变量API处理器
func VariablesBatchDeleteHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
IDs []uint `json:"ids"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if len(req.IDs) == 0 {
http.Error(w, "请选择要删除的变量", http.StatusBadRequest)
return
}
db, err := database.GetDB()
if err != nil {
logrus.WithError(err).Error("Failed to get database connection")
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
return
}
// 批量删除变量
if err := db.Delete(&models.Variable{}, req.IDs).Error; err != nil {
logrus.WithError(err).Error("Failed to batch delete variables")
http.Error(w, "批量删除失败", http.StatusInternalServerError)
return
}
logrus.WithField("variable_ids", req.IDs).Info("Successfully batch deleted variables")
response := map[string]interface{}{
"code": 0,
"msg": "批量删除成功",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// VariableGetAppsHandler 获取应用列表(用于筛选下拉框)
func VariableGetAppsHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
db, err := database.GetDB()
if err != nil {
logrus.WithError(err).Error("Failed to get database connection")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
var apps []models.App
if err := db.Select("uuid, name").Find(&apps).Error; err != nil {
logrus.WithError(err).Error("Failed to fetch apps")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
response := map[string]interface{}{
"code": 0,
"msg": "success",
"data": apps,
}
w.Header().Set("Content-Type", "application/json")
utils.WriteJSONResponse(w, http.StatusOK, response)
}

View File

@@ -17,7 +17,7 @@ func AutoMigrate() error {
if err != nil {
return err
}
if err := db.AutoMigrate(&models.User{}, &models.Settings{}, &models.App{}, &models.API{}); err != nil {
if err := db.AutoMigrate(&models.User{}, &models.Settings{}, &models.App{}, &models.API{}, &models.Variable{}); err != nil {
logrus.WithError(err).Error("AutoMigrate 执行失败")
return err
}

View File

@@ -1,7 +1,11 @@
package models
import (
"strings"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// API 接口表模型
@@ -13,6 +17,9 @@ type API struct {
// ID主键自增
ID uint `gorm:"primaryKey;comment:API接口ID自增主键" json:"id"`
// UUIDAPI接口唯一标识符自动生成
UUID string `gorm:"uniqueIndex;size:36;not null;comment:API接口UUID唯一标识符" json:"uuid"`
// API类型int型
APIType int `gorm:"not null;comment:API类型" json:"api_type"`
@@ -47,6 +54,14 @@ type API struct {
UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"`
}
// BeforeCreate 在创建记录前自动生成UUID
func (api *API) BeforeCreate(tx *gorm.DB) error {
if api.UUID == "" {
api.UUID = strings.ToUpper(uuid.New().String())
}
return nil
}
// TableName 指定表名
func (API) TableName() string {
return "apis"

61
models/variable.go Normal file
View File

@@ -0,0 +1,61 @@
package models
import (
"strings"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// Variable 变量表模型
// 用于管理应用程序的变量数据
// UUID 为变量的唯一标识符,自动生成并转换为大写
// Alias 为变量别名,便于识别和管理
// Data 为变量数据内容
// Remark 为备注信息,用于描述变量用途
// CreatedAt/UpdatedAt 由 GORM 自动维护
type Variable struct {
// ID主键自增
ID uint `gorm:"primaryKey;comment:变量ID自增主键" json:"id"`
// UUID变量的唯一标识符36位字符串
UUID string `gorm:"uniqueIndex;size:36;not null;comment:变量的唯一标识符" json:"uuid"`
// Number变量编号时间戳+6位随机数字格式
Number string `gorm:"uniqueIndex;size:20;not null;comment:变量编号,时间戳+6位随机数字格式" json:"number"`
// AppUUID关联的应用UUID
AppUUID string `gorm:"size:36;not null;index;comment:关联的应用UUID" json:"app_uuid"`
// Alias变量别名便于识别和管理
Alias string `gorm:"size:100;not null;comment:变量别名" json:"alias"`
// Data变量数据内容
Data string `gorm:"type:text;comment:变量数据" json:"data"`
// Remark备注信息用于描述变量用途
Remark string `gorm:"type:text;comment:备注信息" json:"remark"`
// 时间字段
CreatedAt time.Time `gorm:"comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"`
}
// BeforeCreate 在创建记录前自动生成UUID和Number
func (variable *Variable) BeforeCreate(tx *gorm.DB) error {
// 生成UUID
if variable.UUID == "" {
variable.UUID = strings.ToUpper(uuid.New().String())
}
// 生成Number使用时间戳格式
variable.Number = time.Now().Format("20060102150405")
return nil
}
// TableName 指定表名
func (Variable) TableName() string {
return "variables"
}

View File

@@ -44,12 +44,17 @@ func RegisterAdminRoutes(mux *http.ServeMux) {
mux.HandleFunc("/admin/settings", adminctl.AdminAuthRequired(adminctl.SettingsFragmentHandler))
mux.HandleFunc("/admin/apps", adminctl.AdminAuthRequired(adminctl.AppsFragmentHandler))
mux.HandleFunc("/admin/apis", adminctl.AdminAuthRequired(adminctl.APIFragmentHandler))
mux.HandleFunc("/admin/variables", adminctl.AdminAuthRequired(adminctl.VariableFragmentHandler))
// 系统信息API用于仪表盘定时刷新
mux.HandleFunc("/admin/api/system/info", adminctl.AdminAuthRequired(adminctl.SystemInfoHandler))
// 个人资料API
mux.HandleFunc("/admin/api/user/profile", adminctl.AdminAuthRequired(adminctl.UserProfileQueryHandler))
mux.HandleFunc("/admin/api/user/profile/update", adminctl.AdminAuthRequired(adminctl.UserProfileUpdateHandler))
mux.HandleFunc("/admin/api/user/password", adminctl.AdminAuthRequired(adminctl.UserPasswordUpdateHandler))
// 设置API需要管理员认证
// 系统设置API
mux.HandleFunc("/admin/api/settings", adminctl.AdminAuthRequired(adminctl.SettingsQueryHandler))
mux.HandleFunc("/admin/api/settings/update", adminctl.AdminAuthRequired(adminctl.SettingsUpdateHandler))
@@ -79,6 +84,11 @@ func RegisterAdminRoutes(mux *http.ServeMux) {
mux.HandleFunc("/admin/api/apis/types", adminctl.AdminAuthRequired(adminctl.APIGetTypesHandler))
mux.HandleFunc("/admin/api/apis/generate_keys", adminctl.AdminAuthRequired(adminctl.APIGenerateKeysHandler))
// 系统信息API用于仪表盘定时刷新
mux.HandleFunc("/admin/api/system/info", adminctl.AdminAuthRequired(adminctl.SystemInfoHandler))
// 变量管理API
mux.HandleFunc("/admin/variable/list", adminctl.AdminAuthRequired(adminctl.VariableListHandler))
mux.HandleFunc("/admin/variable/apps", adminctl.AdminAuthRequired(adminctl.VariableGetAppsHandler))
mux.HandleFunc("/admin/variable/create", adminctl.AdminAuthRequired(adminctl.VariableCreateHandler))
mux.HandleFunc("/admin/variable/update", adminctl.AdminAuthRequired(adminctl.VariableUpdateHandler))
mux.HandleFunc("/admin/variable/delete", adminctl.AdminAuthRequired(adminctl.VariableDeleteHandler))
mux.HandleFunc("/admin/variable/batch_delete", adminctl.AdminAuthRequired(adminctl.VariablesBatchDeleteHandler))
}

View File

@@ -54,12 +54,13 @@ func (l *Logger) LogRequestWithHeaders(method, path, clientIP string, statusCode
// 避免Logrus的任何格式化和转义保持Apache日志格式的原始性
// logLine: 格式化后的日志行
func (l *Logger) writeHTTPLog(logLine string) {
// 输出到标准输出
fmt.Fprintln(os.Stdout, logLine)
// 同时输出到logrus配置的输出目标包括文件
// 使用logrus的输出目标但不经过格式化器
if l.Logger.Out != nil && l.Logger.Out != os.Stdout {
// 直接使用logrus的输出目标避免重复输出
// 如果logrus配置了MultiWriter会自动输出到所有目标控制台+文件)
// 如果logrus只配置了标准输出也会正确输出到控制台
if l.Logger.Out != nil {
fmt.Fprintln(l.Logger.Out, logLine)
} else {
// 如果没有配置输出目标,默认输出到标准输出
fmt.Fprintln(os.Stdout, logLine)
}
}

View File

@@ -17,7 +17,7 @@
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">接口类型</label>
<label class="layui-form-label">类型</label>
<div class="layui-input-inline">
<select name="api_type" lay-filter="apiTypeSelect">
<option value="">请选择接口类型</option>
@@ -54,7 +54,7 @@
<!-- 隐藏的表单弹层内容:编辑接口 -->
<div id="apiFormModal" style="display:none;padding:16px">
<form class="layui-form layui-form-pane" id="apiForm">
<input type="hidden" name="id" />
<input type="hidden" name="uuid" />
<!-- 关联应用与接口类型为固定项,移除编辑能力 -->
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="submit-algorithm">提交算法</label>
@@ -209,11 +209,13 @@ layui.use(['table', 'form', 'layer', 'dropdown'], function() {
limit: 20,
limits: [10, 20, 50, 100],
loading: true,
done: function(res, curr, count) {
// 表格渲染完成后的回调
},
cols: [[
{ field: 'id', title: 'ID', width: 80, sort: true },
{ field: 'app_name', title: '应用名称', mixWidth: 180 },
{ field: 'api_type_name', title: '接口类型', mixWidth: 120 },
{ field: 'app_name', title: '应用名称', minWidth: 150 },
{ field: 'api_type_name', title: '接口类型', minWidth: 120 },
{
field: 'status_name',
title: '状态',
@@ -227,27 +229,39 @@ layui.use(['table', 'form', 'layer', 'dropdown'], function() {
field: 'submit_algorithm',
title: '提交算法',
width: 120,
templet: (d) => d.algorithm_names ? d.algorithm_names.submit : '-'
templet: (d) => {
const algorithm = d.algorithm_names ? d.algorithm_names.submit : '-';
if (algorithm && algorithm.length > 10) {
return '<span title="' + algorithm + '">' + algorithm.substring(0, 10) + '...</span>';
}
return algorithm;
}
},
{
field: 'return_algorithm',
title: '返回算法',
width: 120,
templet: (d) => d.algorithm_names ? d.algorithm_names.return : '-'
templet: (d) => {
const algorithm = d.algorithm_names ? d.algorithm_names.return : '-';
if (algorithm && algorithm.length > 10) {
return '<span title="' + algorithm + '">' + algorithm.substring(0, 10) + '...</span>';
}
return algorithm;
}
},
{
field: 'created_at',
title: '创建时间',
width: 180,
width: 160,
templet: (d) => formatDateTime(d.created_at)
},
{
field: 'updated_at',
title: '修改时间',
width: 180,
width: 160,
templet: (d) => formatDateTime(d.updated_at)
},
{ fixed: 'right', title: '操作', toolbar: '#tpl-apis-ops', width: 70 }
{ fixed: 'right', title: '操作', toolbar: '#tpl-apis-ops', width: 100, align: 'center' }
]]
});
@@ -533,7 +547,7 @@ layui.use(['table', 'form', 'layer', 'dropdown'], function() {
if (obj.event === 'edit') {
// 编辑接口
$('#apiForm')[0].reset();
$('input[name="id"]').val(data.id);
$('input[name="uuid"]').val(data.uuid);
$('select[name="submit_algorithm"]').val(data.submit_algorithm);
$('select[name="return_algorithm"]').val(data.return_algorithm);
$('input[name="status"]').prop('checked', data.status === 1);
@@ -574,7 +588,6 @@ layui.use(['table', 'form', 'layer', 'dropdown'], function() {
// 转换数值类型
formData.submit_algorithm = parseInt(formData.submit_algorithm);
formData.return_algorithm = parseInt(formData.return_algorithm);
formData.id = parseInt(formData.id);
$.ajax({
url: '/admin/api/apis/update',

View File

@@ -56,6 +56,7 @@
<dl class="layui-nav-child">
<dd><a data-path="apps" href="javascript:;">应用列表</a></dd>
<dd><a data-path="apis" href="javascript:;">接口列表</a></dd>
<dd><a data-path="variables" href="javascript:;">变量列表</a></dd>
</dl>
</li>
</ul>

View File

@@ -0,0 +1,464 @@
{{ define "variables" }}
<section>
<h2>变量管理</h2>
<div class="layui-btn-container" style="margin:12px 0">
<button class="layui-btn" id="btnAddVariable"><i class="layui-icon layui-icon-add-1"></i> 新增变量</button>
<button class="layui-btn layui-btn-danger" id="btnBatchDeleteVariables"><i class="layui-icon layui-icon-delete"></i> 批量删除</button>
</div>
<div class="layui-card" style="margin-top:12px">
<div class="layui-card-header">筛选</div>
<div class="layui-card-body">
<form class="layui-form layui-form-pane" id="variableFilterForm" lay-filter="variableFilterForm">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">应用</label>
<div class="layui-input-inline">
<select name="app_uuid" lay-filter="appSelect">
<option value="">请选择应用</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">搜索</label>
<div class="layui-input-inline">
<input type="text" name="search" placeholder="变量别名/数据" autocomplete="off" class="layui-input" />
</div>
</div>
<div class="layui-inline">
<button type="button" class="layui-btn" id="btnSearchVariables">查询</button>
<button type="button" class="layui-btn layui-btn-primary" id="btnResetVariables">重置</button>
</div>
</div>
</form>
</div>
</div>
<div class="layui-card" style="margin-top:12px">
<div class="layui-card-header">变量列表</div>
<div class="layui-card-body">
<table id="variablesTable" lay-filter="variablesTableFilter"></table>
</div>
</div>
<!-- 表格操作模板 -->
<script type="text/html" id="tpl-variables-ops">
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
<!-- 隐藏的表单弹层内容:新增/编辑变量 -->
<div id="variableFormLayer" style="display:none;padding:20px">
<form class="layui-form layui-form-pane" lay-filter="variableForm" id="variableForm">
<input type="hidden" name="uuid">
<div class="layui-form-item">
<label class="layui-form-label">应用选择</label>
<div class="layui-input-block">
<select name="app_uuid" lay-verify="required" lay-filter="formAppSelect">
<option value="">请选择应用</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">变量别名</label>
<div class="layui-input-block">
<input type="text" name="alias" lay-verify="required|alias" placeholder="请输入变量别名(英文开头,只能包含数字和英文字母)" autocomplete="off" class="layui-input" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">变量数据</label>
<div class="layui-input-block">
<textarea name="data" placeholder="请输入变量数据" lay-verify="required" class="layui-textarea"></textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">备注</label>
<div class="layui-input-block">
<textarea name="remark" placeholder="请输入备注信息" class="layui-textarea"></textarea>
</div>
</div>
</form>
</div>
<script>
// 等待layui加载完成
function waitForLayui(callback) {
if (typeof layui !== 'undefined') {
callback();
} else {
setTimeout(() => waitForLayui(callback), 100);
}
}
waitForLayui(function() {
layui.use(['table', 'form', 'layer', 'element'], function() {
const table = layui.table;
const form = layui.form;
const layer = layui.layer;
const $ = layui.$;
// 自定义验证规则
form.verify({
alias: function(value) {
if (!value) return '别名不能为空';
// 检查是否以英文字母开头,且只包含数字和英文字母
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(value)) {
return '别名必须以英文字母开头,只能包含数字和英文字母';
}
}
});
// 格式化时间函数
function formatDateTime(dateStr) {
if (!dateStr) return '-';
return new Date(dateStr).toLocaleString();
}
// 加载应用列表到下拉框
function loadApps() {
$.ajax({
url: '/admin/variable/apps',
type: 'GET',
success: function(res) {
if (res.code === 0 && res.data) {
let options = '<option value="">请选择应用</option>';
res.data.forEach(function(app) {
options += '<option value="' + app.uuid + '">' + app.name + '</option>';
});
$('select[name="app_uuid"]').html(options);
form.render('select');
}
},
error: function() {
layer.msg('加载应用列表失败', {icon: 2});
}
});
}
// 初始化加载应用列表
loadApps();
// 渲染表格
const variablesTable = table.render({
elem: '#variablesTable',
id: 'variablesTable',
url: '/admin/variable/list',
parseData: function(res) {
return {
code: res.code,
msg: res.msg || '',
count: res.count || 0,
data: res.data || []
};
},
request: {
pageName: 'page',
limitName: 'page_size'
},
method: 'GET',
page: true,
limit: 20,
limits: [10, 20, 50, 100],
loading: true,
done: function(res, curr, count) {
// 表格渲染完成后的回调
},
cols: [[
{type: 'checkbox', width: 50},
{field: 'id', title: 'ID', width: 80, sort: true},
{field: 'app_name', title: '应用名称', minWidth: 120},
{field: 'number', title: '变量编号', width: 180},
{field: 'alias', title: '变量别名', minWidth: 150},
{
field: 'data',
title: '变量数据',
minWidth: 200,
templet: function(d) {
// 限制显示长度,避免内容过长影响布局
if (d.data && d.data.length > 50) {
return '<span title="' + d.data + '">' + d.data.substring(0, 50) + '...</span>';
}
return d.data || '-';
}
},
{
field: 'remark',
title: '备注',
minWidth: 150,
templet: function(d) {
// 限制显示长度,避免内容过长影响布局
if (d.remark && d.remark.length > 30) {
return '<span title="' + d.remark + '">' + d.remark.substring(0, 30) + '...</span>';
}
return d.remark || '-';
}
},
{
field: 'created_at',
title: '创建时间',
width: 180,
templet: function(d) {
return formatDateTime(d.created_at);
}
},
{title: '操作', width: 180, align: 'center', toolbar: '#tpl-variables-ops', fixed: 'right'}
]]
});
// 监听应用选择变化
form.on('select(appSelect)', function(data) {
variablesTable.reload({
where: {
app_uuid: data.value,
alias: $('input[name="search"]').val()
},
page: {
curr: 1
}
});
});
// 搜索功能
$('#btnSearchVariables').on('click', function() {
variablesTable.reload({
where: {
app_uuid: $('select[name="app_uuid"]').val(),
alias: $('input[name="search"]').val()
},
page: {
curr: 1
}
});
});
// 重置搜索
$('#btnResetVariables').on('click', function() {
$('#variableFilterForm')[0].reset();
form.render();
variablesTable.reload({
where: {},
page: {
curr: 1
}
});
});
// 新增变量
$('#btnAddVariable').on('click', function() {
console.log('新增变量按钮被点击');
$('#variableForm')[0].reset();
$('input[name="id"]').val('');
// 重新加载应用列表到表单中
loadApps();
layer.open({
type: 1,
title: '新增变量',
content: $('#variableFormLayer'),
area: ['500px', '460px'],
btn: ['创建', '取消'],
yes: function(index, layero) {
// 手动收集表单数据
var formData = {};
$('#variableForm').find('input, select, textarea').each(function() {
var $this = $(this);
var name = $this.attr('name');
if (name && name !== 'id') {
formData[name] = $this.val();
}
});
console.log('新增变量 - 收集到的表单数据:', formData);
// 验证必填字段
if (!formData.app_uuid || formData.app_uuid.trim() === '') {
layer.msg('应用UUID不能为空', {icon: 2});
return;
}
if (!formData.alias || formData.alias.trim() === '') {
layer.msg('请输入变量别名', {icon: 2});
return;
}
if (!formData.data || formData.data.trim() === '') {
layer.msg('请输入变量数据', {icon: 2});
return;
}
console.log('新增变量 - 发送的JSON数据:', JSON.stringify(formData));
$.ajax({
url: '/admin/variable/create',
type: 'POST',
data: JSON.stringify(formData),
contentType: 'application/json',
success: function(res) {
if (res.code === 0) {
layer.msg(res.msg, {icon: 1});
layer.close(index);
variablesTable.reload();
} else {
layer.msg(res.msg || '操作失败', {icon: 2});
}
},
error: function(xhr) {
layer.msg(xhr.responseText || '操作失败', {icon: 2});
}
});
},
btn2: function(index) {
layer.close(index);
},
success: function() {
form.render();
},
shadeClose: false
});
});
// 批量删除
$('#btnBatchDeleteVariables').on('click', function() {
const checkStatus = table.checkStatus('variablesTable');
const data = checkStatus.data;
if (data.length === 0) {
layer.msg('请选择要删除的变量', {icon: 2});
return;
}
layer.confirm('确定删除选中的 ' + data.length + ' 个变量吗?', {icon: 3, title: '提示'}, function(index) {
const ids = data.map(item => item.id);
$.ajax({
url: '/admin/variable/batch_delete',
type: 'POST',
data: JSON.stringify({ids: ids}),
contentType: 'application/json',
success: function(res) {
if (res.code === 0) {
layer.msg(res.msg, {icon: 1});
variablesTable.reload();
} else {
layer.msg(res.msg || '批量删除失败', {icon: 2});
}
},
error: function(xhr) {
layer.msg(xhr.responseText || '批量删除失败', {icon: 2});
}
});
layer.close(index);
});
});
// 表格工具栏事件
table.on('tool(variablesTableFilter)', function(obj) {
const data = obj.data;
if (obj.event === 'edit') {
// 编辑
console.log('编辑按钮被点击', data);
$('#variableForm')[0].reset();
$('input[name="uuid"]').val(data.uuid);
// 重新加载应用列表,然后设置选中值
loadApps();
setTimeout(function() {
$('select[name="app_uuid"]').val(data.app_uuid);
form.render('select');
}, 100);
$('input[name="alias"]').val(data.alias);
$('textarea[name="data"]').val(data.data);
$('textarea[name="remark"]').val(data.remark);
layer.open({
type: 1,
title: '编辑变量',
content: $('#variableFormLayer'),
area: ['500px', '460px'],
btn: ['保存', '取消'],
yes: function(index, layero) {
// 手动收集表单数据
var formData = {};
$('#variableForm').find('input, select, textarea').each(function() {
var $this = $(this);
var name = $this.attr('name');
if (name && name !== 'id') {
formData[name] = $this.val();
}
});
console.log('编辑变量 - 收集到的表单数据:', formData);
// 验证必填字段
if (!formData.app_uuid || formData.app_uuid.trim() === '') {
layer.msg('应用UUID不能为空', {icon: 2});
return;
}
if (!formData.alias || formData.alias.trim() === '') {
layer.msg('请输入变量别名', {icon: 2});
return;
}
if (!formData.data || formData.data.trim() === '') {
layer.msg('请输入变量数据', {icon: 2});
return;
}
console.log('编辑变量 - 发送的JSON数据:', JSON.stringify(formData));
$.ajax({
url: '/admin/variable/update',
type: 'POST',
data: JSON.stringify(formData),
contentType: 'application/json',
success: function(res) {
if (res.code === 0) {
layer.msg(res.msg, {icon: 1});
layer.close(index);
variablesTable.reload();
} else {
layer.msg(res.msg || '操作失败', {icon: 2});
}
},
error: function(xhr) {
layer.msg(xhr.responseText || '操作失败', {icon: 2});
}
});
},
btn2: function(index) {
layer.close(index);
},
success: function() {
form.render();
},
shadeClose: false
});
} else if (obj.event === 'del') {
// 删除
layer.confirm('确定删除该变量吗?', {icon: 3, title: '提示'}, function(index) {
$.ajax({
url: '/admin/variable/delete',
type: 'POST',
data: JSON.stringify({id: data.id}),
contentType: 'application/json',
success: function(res) {
if (res.code === 0) {
layer.msg(res.msg, {icon: 1});
variablesTable.reload();
} else {
layer.msg(res.msg || '删除失败', {icon: 2});
}
},
error: function(xhr) {
layer.msg(xhr.responseText || '删除失败', {icon: 2});
}
});
layer.close(index);
});
}
});
});
});
</script>
</section>
{{ end }}