Parcourir la source

1. 扩充了端口冲突管理功能,端口类变量新增复数形式,变量值支持以逗号分隔的多个端口号,检测时对每个端口分别查询占用并支持单独终止;
2. 调整了服务卡片的点击逻辑,去除“服务必须由本平台启动并处于运行中状态”的限制,只要配置了 Web URL 即可点击打开,适配以其他方式(外部)启动服务的场景;
3. 更新了 README.md,补充了分类管理、全局交互反馈等已上线功能的描述,并同步了项目结构与本次变更;
4. 优化了端口检测对话框的空态提示文案,明确列出了 PORTS、SERVER_PORTS 等复数变量名。

weisijie il y a 1 mois
Parent
commit
d53f62e2a5

+ 5 - 2
README.md

@@ -14,14 +14,16 @@
 ## 功能特性
 
 - **服务管理** — 创建、编辑、删除服务配置
+- **分类管理** — 无限层级的分类树,支持创建、重命名、删除、按分类筛选
 - **多终端支持** — CMD、PowerShell、Git Bash、SSH 远程执行
 - **启动命令引擎** — 支持多行命令、`[可选片段]` 语法、`{{变量}}` 替换
 - **进程管理** — 后台启动、PID 追踪、优雅停止(10s 超时后强制终止)
 - **PID 持久化** — 进程 PID 写入磁盘,应用重启后自动恢复运行状态
 - **实时日志** — 在线查看服务输出,支持多种字符编码(UTF-8、GB18030 等)
-- **Web 界面跳转** — 配置 Web URL 后,点击运行中的服务卡片可直接在浏览器中打开
-- **端口冲突管理** — 自动检测端口占用,查看占用进程详情,确认后一键终止
+- **Web 界面跳转** — 配置 Web URL 后,点击服务卡片可在浏览器中打开(不要求服务由本平台启动,外部启动亦可)
+- **端口冲突管理** — 自动检测端口占用,支持单/多端口变量(`PORT`、`PORTS`、`SERVER_PORT`、`SERVER_PORTS` 等,多端口值以逗号分隔),查看占用进程详情,确认后一键终止
 - **一键启停** — 支持一键启动/停止所有服务
+- **全局交互反馈** — 自定义 Toast 通知与确认对话框,替代浏览器原生 `alert`/`confirm`,统一交互体验
 - **统一部署** — 前端编译后嵌入 Spring Boot 静态资源,单一 JAR 部署
 
 ## 快速开始
@@ -79,6 +81,7 @@ service-management/
 │   └── src/
 │       ├── api/                # Axios API 层
 │       ├── components/         # Vue 组件
+│       ├── composables/        # 全局可复用组合式函数(useDialog 等)
 │       └── main.ts
 ├── data/                       # 运行时数据(自动生成,不入版本库)
 │   ├── services.json           # 服务配置持久化

+ 25 - 10
backend/src/main/java/com/svcman/service/PortService.java

@@ -27,10 +27,12 @@ public class PortService {
 
     private final ServiceConfigRepository repository;
 
-    /** 端口类变量名模式(不区分大小写) */
+    /** 端口类变量名模式(不区分大小写)。复数形式的值应为逗号分隔的端口列表 */
     private static final Set<String> PORT_VARIABLE_NAMES = Set.of(
             "SERVER_PORT", "PORT", "SERVERPORT", "SERVICE_PORT", "HTTP_PORT", "HTTPS_PORT",
-            "APP_PORT", "API_PORT", "WEB_PORT", "TOMCAT_PORT", "JETTY_PORT"
+            "APP_PORT", "API_PORT", "WEB_PORT", "TOMCAT_PORT", "JETTY_PORT",
+            "SERVER_PORTS", "PORTS", "SERVERPORTS", "SERVICE_PORTS", "HTTP_PORTS", "HTTPS_PORTS",
+            "APP_PORTS", "API_PORTS", "WEB_PORTS", "TOMCAT_PORTS", "JETTY_PORTS"
     );
 
     /** 数字端口号正则 */
@@ -97,13 +99,18 @@ public class PortService {
         // 去重
         ports = ports.stream().distinct().collect(Collectors.toList());
 
-        // 建立端口 → 变量名的映射
+        // 建立端口 → 变量名的映射(值可能为逗号分隔的多端口)
         Map<Integer, List<String>> portToVars = new LinkedHashMap<>();
         if (config.getVariables() != null) {
             for (CustomVariable v : config.getVariables()) {
-                if (isPortVariable(v.getName()) && isValidPort(v.getValue())) {
-                    int port = Integer.parseInt(v.getValue());
-                    portToVars.computeIfAbsent(port, k -> new ArrayList<>()).add(v.getName());
+                if (isPortVariable(v.getName()) && v.getValue() != null) {
+                    for (String token : v.getValue().split(",")) {
+                        String trimmed = token.trim();
+                        if (isValidPort(trimmed)) {
+                            int port = Integer.parseInt(trimmed);
+                            portToVars.computeIfAbsent(port, k -> new ArrayList<>()).add(v.getName());
+                        }
+                    }
                 }
             }
         }
@@ -173,13 +180,16 @@ public class PortService {
             return false;
         }
         return config.getVariables().stream()
-                .anyMatch(v -> isPortVariable(v.getName()) && isValidPort(v.getValue()));
+                .anyMatch(v -> isPortVariable(v.getName()) && v.getValue() != null
+                        && Arrays.stream(v.getValue().split(","))
+                                .map(String::trim)
+                                .anyMatch(this::isValidPort));
     }
 
     // ========== 私有方法 ==========
 
     /**
-     * 从服务配置中提取所有端口号
+     * 从服务配置中提取所有端口号(支持单端口变量和逗号分隔的多端口变量)
      */
     private List<Integer> extractPorts(ServiceConfig config) {
         List<Integer> ports = new ArrayList<>();
@@ -187,8 +197,13 @@ public class PortService {
             return ports;
         }
         for (CustomVariable v : config.getVariables()) {
-            if (isPortVariable(v.getName()) && isValidPort(v.getValue())) {
-                ports.add(Integer.parseInt(v.getValue()));
+            if (isPortVariable(v.getName()) && v.getValue() != null) {
+                for (String token : v.getValue().split(",")) {
+                    String trimmed = token.trim();
+                    if (isValidPort(trimmed)) {
+                        ports.add(Integer.parseInt(trimmed));
+                    }
+                }
             }
         }
         return ports;

+ 1 - 1
frontend/src/components/PortKillDialog.vue

@@ -96,7 +96,7 @@ const handleClose = () => {
 
         <div v-else-if="portResults.length === 0" class="empty-state">
           <i class="pi pi-info-circle"></i>
-          <span>未检测到端口类变量(如 SERVER_PORT、PORT 等)</span>
+          <span>未检测到端口类变量(如 SERVER_PORT、PORT、SERVER_PORTS、PORTS 等)</span>
         </div>
 
         <div v-else class="port-results">

+ 10 - 7
frontend/src/components/ServiceCard.vue

@@ -42,7 +42,7 @@ const formatDate = (d?: string | null) => {
 }
 
 const handleCardClick = async () => {
-  if (props.service.status !== 'RUNNING' || !props.service.webUrl) return
+  if (!props.service.webUrl) return
   try {
     const res = await serviceApi.getWebUrl(props.service.id!)
     if (res.url) {
@@ -53,17 +53,20 @@ const handleCardClick = async () => {
   }
 }
 
-const isCardClickable = computed(() => props.service.status === 'RUNNING' && !!props.service.webUrl)
+const isCardClickable = computed(() => !!props.service.webUrl)
 
-/** 检测变量中是否包含端口类变量 */
-const PORT_VAR_NAMES = ['SERVER_PORT', 'PORT', 'SERVERPORT', 'SERVICE_PORT', 'HTTP_PORT', 'HTTPS_PORT', 'APP_PORT', 'API_PORT', 'WEB_PORT', 'TOMCAT_PORT', 'JETTY_PORT']
+/** 端口类变量名(不区分大小写;复数形式的值应为逗号分隔的端口列表) */
+const PORT_VAR_NAMES = ['SERVER_PORT', 'PORT', 'SERVERPORT', 'SERVICE_PORT', 'HTTP_PORT', 'HTTPS_PORT', 'APP_PORT', 'API_PORT', 'WEB_PORT', 'TOMCAT_PORT', 'JETTY_PORT', 'SERVER_PORTS', 'PORTS', 'SERVERPORTS', 'SERVICE_PORTS', 'HTTP_PORTS', 'HTTPS_PORTS', 'APP_PORTS', 'API_PORTS', 'WEB_PORTS', 'TOMCAT_PORTS', 'JETTY_PORTS']
 
 const hasPortVars = computed(() => {
   if (!props.service.variables || props.service.variables.length === 0) return false
   return props.service.variables.some(v => {
     const normalized = v.name.toUpperCase().replace(/_/g, '')
-    return PORT_VAR_NAMES.some(n => n.replace(/_/g, '') === normalized)
-      && v.value && /^\d{1,5}$/.test(v.value.trim())
+    const isPortVar = PORT_VAR_NAMES.some(n => n.replace(/_/g, '') === normalized)
+    if (!isPortVar || !v.value) return false
+    // 值可能为单个端口或逗号分隔的多个端口,至少有一个有效端口号即视为端口变量
+    const tokens = v.value.split(',').map(s => s.trim()).filter(Boolean)
+    return tokens.length > 0 && tokens.every(t => /^\d{1,5}$/.test(t))
   })
 })
 </script>
@@ -85,7 +88,7 @@ const hasPortVars = computed(() => {
           <span class="status-dot" :style="{ background: statusColor[service.status || 'STOPPED'] }"></span>
           {{ statusLabel[service.status || 'STOPPED'] }}
         </span>
-        <span v-if="service.webUrl && service.status === 'RUNNING'" class="web-icon" title="点击卡片打开 Web 界面">
+        <span v-if="service.webUrl" class="web-icon" title="点击卡片打开 Web 界面">
           <i class="pi pi-external-link"></i>
         </span>
       </div>

+ 17 - 1
prompt.md

@@ -85,4 +85,20 @@ skill要包含few-shot示例。
 
 ---
 
-将弹出框由原生的修改为与当前页面风格一致的弹出框吧。
+将弹出框由原生的修改为与当前页面风格一致的弹出框吧。
+
+---
+
+前期我提过一个需求:“增加一个功能,用于杀死端口冲突的进程。如果自定义变量中包含SERVER_PORT、PORT、serverPort、port等变量,那么使用该功能,自动查询占用该端口号的服务,列出其PID,让用户二次确认后杀掉进程。”
+现在,我希望将其扩充,支持SERVER_PORTS、PORTS等变量名,端口号的值以“,”分隔,检测时对每个端口进行占用检测,并支持杀死对应进程。
+
+---
+
+之前卡片点击的逻辑是,如果某个服务是已启动状态,且包含url,那么可以点击打开该url;现在改一下,无论是否启动,点击都可以打开url吧,因为我有可能以其他方式启动服务。
+
+---
+
+更新README,然后生成提交日志
+
+---
+