Просмотр исходного кода

Improve list sorting and row actions

codex 1 месяц назад
Родитель
Сommit
9a092029ee
4 измененных файлов с 130 добавлено и 27 удалено
  1. 50 1
      backend/app/main.py
  2. 66 26
      frontend/src/components/ItemTable.vue
  3. 12 0
      frontend/src/styles.css
  4. 2 0
      task.md

+ 50 - 1
backend/app/main.py

@@ -112,19 +112,60 @@ def list_items(
     page: int,
     page_size: int,
     fields: list[str],
+    sort_by: str | None = None,
+    sort_order: str | None = None,
 ) -> dict[str, Any]:
     where_sql, params = build_where(keyword, confirm_status, present, fields)
+    order_sql = build_order_by(table, sort_by, sort_order)
     offset = (page - 1) * page_size
     with get_db() as conn:
         total = conn.execute(f"SELECT COUNT(*) AS total FROM {table} {where_sql}", params).fetchone()["total"]
         rows = conn.execute(
-            f"SELECT * FROM {table} {where_sql} ORDER BY is_present_now DESC, last_seen_at DESC LIMIT ? OFFSET ?",
+            f"SELECT * FROM {table} {where_sql} {order_sql} LIMIT ? OFFSET ?",
             [*params, page_size, offset],
         ).fetchall()
         rows = attach_item_metadata(conn, table_to_item_type(table), rows)
     return {"items": rows, "total": total, "page": page, "page_size": page_size}
 
 
+def build_order_by(table: str, sort_by: str | None, sort_order: str | None) -> str:
+    allowed = {
+        "windows_services": {
+            "name",
+            "display_name",
+            "status",
+            "start_type",
+            "username",
+            "is_present_now",
+            "confirm_status",
+            "first_seen_at",
+            "last_seen_at",
+            "updated_at",
+        },
+        "windows_processes": {
+            "name",
+            "exe_path",
+            "username",
+            "status",
+            "last_pid",
+            "parent_pid",
+            "is_present_now",
+            "confirm_status",
+            "create_time",
+            "first_seen_at",
+            "last_seen_at",
+            "updated_at",
+        },
+    }
+    default_sql = "ORDER BY is_present_now DESC, last_seen_at DESC"
+    if not sort_by or sort_by not in allowed.get(table, set()):
+        return default_sql
+    direction = "ASC" if sort_order == "asc" else "DESC"
+    if sort_by == "is_present_now":
+        return f"ORDER BY {sort_by} {direction}, last_seen_at DESC"
+    return f"ORDER BY {sort_by} {direction}, is_present_now DESC, last_seen_at DESC"
+
+
 def get_item(table: str, item_id: int) -> dict[str, Any]:
     with get_db() as conn:
         item = conn.execute(f"SELECT * FROM {table} WHERE id = ?", (item_id,)).fetchone()
@@ -503,6 +544,8 @@ def services(
     keyword: str | None = None,
     confirm_status: str | None = None,
     present: bool | None = None,
+    sort_by: str | None = None,
+    sort_order: str | None = Query(default=None, pattern="^(asc|desc)$"),
     page: int = Query(default=1, ge=1),
     page_size: int = Query(default=20, ge=1, le=200),
 ) -> dict[str, Any]:
@@ -514,6 +557,8 @@ def services(
         page,
         page_size,
         ["name", "display_name", "binary_path", "description"],
+        sort_by,
+        sort_order,
     )
 
 
@@ -594,6 +639,8 @@ def processes(
     keyword: str | None = None,
     confirm_status: str | None = None,
     present: bool | None = None,
+    sort_by: str | None = None,
+    sort_order: str | None = Query(default=None, pattern="^(asc|desc)$"),
     page: int = Query(default=1, ge=1),
     page_size: int = Query(default=20, ge=1, le=200),
 ) -> dict[str, Any]:
@@ -605,6 +652,8 @@ def processes(
         page,
         page_size,
         ["name", "exe_path", "cmdline", "username"],
+        sort_by,
+        sort_order,
     )
 
 

+ 66 - 26
frontend/src/components/ItemTable.vue

@@ -30,12 +30,12 @@
       </div>
     </div>
 
-    <el-table :data="rows.items" border stripe @selection-change="selected = $event">
+    <el-table :data="rows.items" border stripe @selection-change="selected = $event" @sort-change="handleSortChange">
       <el-table-column type="selection" width="46" />
-      <el-table-column prop="name" label="名称" min-width="170" />
-      <el-table-column v-if="type === 'service'" prop="display_name" label="显示名称" min-width="180" />
-      <el-table-column prop="status" label="运行状态" width="120" />
-      <el-table-column label="确认状态" width="120">
+      <el-table-column prop="name" label="名称" min-width="170" sortable="custom" />
+      <el-table-column v-if="type === 'service'" prop="display_name" label="显示名称" min-width="180" sortable="custom" />
+      <el-table-column prop="status" label="运行状态" width="120" sortable="custom" />
+      <el-table-column prop="confirm_status" label="确认状态" width="120" sortable="custom">
         <template #default="{ row }">
           <el-tag :type="statusMeta(row.confirm_status).type">{{ statusMeta(row.confirm_status).label }}</el-tag>
         </template>
@@ -53,35 +53,42 @@
           <span v-if="!row.tags?.length" class="muted">未设置</span>
         </template>
       </el-table-column>
-      <el-table-column label="出现" width="100">
+      <el-table-column prop="is_present_now" label="出现" width="100" sortable="custom">
         <template #default="{ row }">
           <el-tag :type="row.is_present_now ? 'success' : 'info'">{{ row.is_present_now ? '本次出现' : '未出现' }}</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="路径/命令行" min-width="260">
+      <el-table-column :prop="type === 'service' ? 'binary_path' : 'exe_path'" label="路径/命令行" min-width="260" sortable="custom">
         <template #default="{ row }">
           <div class="path">{{ row.binary_path || row.exe_path || row.cmdline || '-' }}</div>
         </template>
       </el-table-column>
-      <el-table-column prop="last_seen_at" label="最后出现" min-width="170" />
-      <el-table-column label="操作" width="420" fixed="right">
+      <el-table-column v-if="type === 'service'" prop="start_type" label="启动类型" width="110" sortable="custom" />
+      <el-table-column v-if="type === 'process'" prop="last_pid" label="PID" width="90" sortable="custom" />
+      <el-table-column prop="last_seen_at" label="最后出现" min-width="170" sortable="custom" />
+      <el-table-column label="操作" width="360" fixed="right">
         <template #default="{ row }">
-          <el-button size="small" @click="showDetail(row)">详情</el-button>
-          <el-button size="small" @click="openTags(row)">标签</el-button>
-          <el-button size="small" type="success" @click="singleUpdate(row, 'TRUSTED')">可信</el-button>
-          <el-button size="small" type="danger" @click="singleUpdate(row, 'SUSPICIOUS')">可疑</el-button>
-          <el-button size="small" @click="singleUpdate(row, 'IGNORED')">忽略</el-button>
-          <template v-if="row.can_control">
-            <template v-if="type === 'service' && row.is_present_now">
-              <el-button size="small" type="primary" @click="control(row, 'start')">启动</el-button>
-              <el-button size="small" type="warning" @click="control(row, 'stop')">停止</el-button>
-              <el-button size="small" type="primary" @click="control(row, 'restart')">重启</el-button>
-            </template>
-            <template v-if="type === 'process'">
-              <el-button size="small" type="primary" @click="control(row, 'start')">启动</el-button>
-              <el-button v-if="row.is_present_now" size="small" type="warning" @click="control(row, 'stop')">停止</el-button>
-            </template>
-          </template>
+          <div class="row-actions">
+            <el-button size="small" @click="showDetail(row)">详情</el-button>
+            <el-button size="small" @click="openTags(row)">标签</el-button>
+            <el-button size="small" type="success" @click="singleUpdate(row, 'TRUSTED')">可信</el-button>
+            <el-button size="small" type="danger" @click="singleUpdate(row, 'SUSPICIOUS')">可疑</el-button>
+            <el-button size="small" @click="singleUpdate(row, 'IGNORED')">忽略</el-button>
+            <el-dropdown v-if="controlActions(row).length" trigger="click" @command="control(row, $event)">
+              <el-button size="small">更多</el-button>
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <el-dropdown-item
+                    v-for="action in controlActions(row)"
+                    :key="action.command"
+                    :command="action.command"
+                  >
+                    {{ action.label }}
+                  </el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </div>
         </template>
       </el-table-column>
     </el-table>
@@ -178,16 +185,32 @@ const query = reactive({
   keyword: '',
   confirm_status: props.confirmStatus || '',
   present: null,
+  sort_by: '',
+  sort_order: '',
   page: 1,
   page_size: 20,
 })
 
 async function load() {
   query.confirm_status = props.confirmStatus || query.confirm_status
-  const { data } = await api.get(basePath, { params: query })
+  const params = cleanParams(query)
+  const { data } = await api.get(basePath, { params })
   rows.value = data
 }
 
+async function handleSortChange({ prop, order }) {
+  query.sort_by = order ? prop : ''
+  query.sort_order = order === 'ascending' ? 'asc' : order === 'descending' ? 'desc' : ''
+  query.page = 1
+  await load()
+}
+
+function cleanParams(source) {
+  return Object.fromEntries(
+    Object.entries(source).filter(([, value]) => value !== '' && value !== null && value !== undefined),
+  )
+}
+
 async function loadTags() {
   const { data } = await api.get('/api/tags')
   allTags.value = data.items
@@ -269,6 +292,23 @@ async function importAi() {
   await load()
 }
 
+function controlActions(row) {
+  if (!row.can_control) return []
+  if (props.type === 'service' && row.is_present_now) {
+    return [
+      { command: 'start', label: '启动服务' },
+      { command: 'stop', label: '停止服务' },
+      { command: 'restart', label: '重新启动服务' },
+    ]
+  }
+  if (props.type === 'process') {
+    const actions = [{ command: 'start', label: '启动进程' }]
+    if (row.is_present_now) actions.push({ command: 'stop', label: '停止进程' })
+    return actions
+  }
+  return []
+}
+
 async function control(row, action) {
   const actionLabel = { start: '启动', stop: '停止', restart: '重启' }[action]
   await ElMessageBox.confirm(`确认${actionLabel}“${row.name}”?`, `${actionLabel}确认`, { type: 'warning' })

+ 12 - 0
frontend/src/styles.css

@@ -138,6 +138,18 @@ body {
   word-break: break-word;
 }
 
+.row-actions {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  flex-wrap: nowrap;
+  white-space: nowrap;
+}
+
+.row-actions .el-button {
+  margin-left: 0;
+}
+
 @media (max-width: 760px) {
   .app-shell {
     display: block;

+ 2 - 0
task.md

@@ -34,6 +34,7 @@
 - [x] 增加进程启动、停止接口和操作按钮
 - [x] 根据确认状态和不可控标签隐藏控制操作
 - [x] AI 提示词增加标签说明,并包含系统已有标签信息
+- [x] Windows 服务、进程列表查询增加排序功能
 
 ## 进度日志
 
@@ -45,3 +46,4 @@
 - 2026-05-03:验证 Python 编译、前端构建、传感器接口、smartctl 扫描和普通 SMART 读取通过。
 - 2026-05-09:开始增加标签、服务/进程控制和 AI 标签上下文功能。
 - 2026-05-09:完成标签管理、服务/进程标签编辑、服务/进程控制接口和前端按钮、AI 标签上下文,验证编译、构建和核心接口;补充系统核心进程保护。
+- 2026-05-10:服务和进程列表增加远程排序,前端支持点击表头排序。