diff --git a/backend/internal/handler/admin/admin_service_stub_test.go b/backend/internal/handler/admin/admin_service_stub_test.go index c1256081..a5376e4a 100644 --- a/backend/internal/handler/admin/admin_service_stub_test.go +++ b/backend/internal/handler/admin/admin_service_stub_test.go @@ -259,6 +259,12 @@ func (s *stubAdminService) GetAllProxiesWithAccountCount(ctx context.Context) ([ } func (s *stubAdminService) GetProxy(ctx context.Context, id int64) (*service.Proxy, error) { + for i := range s.proxies { + proxy := s.proxies[i] + if proxy.ID == id { + return &proxy, nil + } + } proxy := service.Proxy{ID: id, Name: "proxy", Status: service.StatusActive} return &proxy, nil } diff --git a/backend/internal/handler/admin/proxy_data.go b/backend/internal/handler/admin/proxy_data.go index 0bcab027..bc2a76ab 100644 --- a/backend/internal/handler/admin/proxy_data.go +++ b/backend/internal/handler/admin/proxy_data.go @@ -2,6 +2,8 @@ package admin import ( "context" + "fmt" + "strconv" "strings" "time" @@ -14,17 +16,32 @@ import ( func (h *ProxyHandler) ExportData(c *gin.Context) { ctx := c.Request.Context() - protocol := c.Query("protocol") - status := c.Query("status") - search := strings.TrimSpace(c.Query("search")) - if len(search) > 100 { - search = search[:100] + selectedIDs, err := parseProxyIDs(c) + if err != nil { + response.BadRequest(c, err.Error()) + return } - proxies, err := h.listProxiesFiltered(ctx, protocol, status, search) - if err != nil { - response.ErrorFrom(c, err) - return + var proxies []service.Proxy + if len(selectedIDs) > 0 { + proxies, err = h.getProxiesByIDs(ctx, selectedIDs) + if err != nil { + response.ErrorFrom(c, err) + return + } + } else { + protocol := c.Query("protocol") + status := c.Query("status") + search := strings.TrimSpace(c.Query("search")) + if len(search) > 100 { + search = search[:100] + } + + proxies, err = h.listProxiesFiltered(ctx, protocol, status, search) + if err != nil { + response.ErrorFrom(c, err) + return + } } dataProxies := make([]DataProxy, 0, len(proxies)) @@ -168,6 +185,50 @@ func (h *ProxyHandler) ImportData(c *gin.Context) { response.Success(c, result) } +func (h *ProxyHandler) getProxiesByIDs(ctx context.Context, ids []int64) ([]service.Proxy, error) { + out := make([]service.Proxy, 0, len(ids)) + for _, id := range ids { + proxy, err := h.adminService.GetProxy(ctx, id) + if err != nil { + return nil, err + } + if proxy == nil { + continue + } + out = append(out, *proxy) + } + return out, nil +} + +func parseProxyIDs(c *gin.Context) ([]int64, error) { + values := c.QueryArray("ids") + if len(values) == 0 { + raw := strings.TrimSpace(c.Query("ids")) + if raw != "" { + values = []string{raw} + } + } + if len(values) == 0 { + return nil, nil + } + + ids := make([]int64, 0, len(values)) + for _, item := range values { + for _, part := range strings.Split(item, ",") { + part = strings.TrimSpace(part) + if part == "" { + continue + } + id, err := strconv.ParseInt(part, 10, 64) + if err != nil || id <= 0 { + return nil, fmt.Errorf("invalid proxy id: %s", part) + } + ids = append(ids, id) + } + } + return ids, nil +} + func (h *ProxyHandler) listProxiesFiltered(ctx context.Context, protocol, status, search string) ([]service.Proxy, error) { page := 1 pageSize := dataPageCap diff --git a/backend/internal/handler/admin/proxy_data_handler_test.go b/backend/internal/handler/admin/proxy_data_handler_test.go index f7609097..545b24a3 100644 --- a/backend/internal/handler/admin/proxy_data_handler_test.go +++ b/backend/internal/handler/admin/proxy_data_handler_test.go @@ -75,6 +75,45 @@ func TestProxyExportDataRespectsFilters(t *testing.T) { require.Equal(t, "https", resp.Data.Proxies[0].Protocol) } +func TestProxyExportDataWithSelectedIDs(t *testing.T) { + router, adminSvc := setupProxyDataRouter() + + adminSvc.proxies = []service.Proxy{ + { + ID: 1, + Name: "proxy-a", + Protocol: "http", + Host: "127.0.0.1", + Port: 8080, + Username: "user", + Password: "pass", + Status: service.StatusActive, + }, + { + ID: 2, + Name: "proxy-b", + Protocol: "https", + Host: "10.0.0.2", + Port: 443, + Username: "u", + Password: "p", + Status: service.StatusDisabled, + }, + } + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/proxies/data?ids=2", nil) + router.ServeHTTP(rec, req) + require.Equal(t, http.StatusOK, rec.Code) + + var resp proxyDataResponse + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) + require.Equal(t, 0, resp.Code) + require.Len(t, resp.Data.Proxies, 1) + require.Equal(t, "https", resp.Data.Proxies[0].Protocol) + require.Equal(t, "10.0.0.2", resp.Data.Proxies[0].Host) +} + func TestProxyImportDataReusesAndTriggersLatencyProbe(t *testing.T) { router, adminSvc := setupProxyDataRouter() diff --git a/frontend/src/api/admin/proxies.ts b/frontend/src/api/admin/proxies.ts index 76c96c7d..b6aaf595 100644 --- a/frontend/src/api/admin/proxies.ts +++ b/frontend/src/api/admin/proxies.ts @@ -210,14 +210,24 @@ export async function batchDelete(ids: number[]): Promise<{ return data } -export async function exportData(filters?: { - protocol?: string - status?: 'active' | 'inactive' - search?: string +export async function exportData(options?: { + ids?: number[] + filters?: { + protocol?: string + status?: 'active' | 'inactive' + search?: string + } }): Promise { - const { data } = await apiClient.get('/admin/proxies/data', { - params: filters - }) + const params: Record = {} + if (options?.ids && options.ids.length > 0) { + params.ids = options.ids.join(',') + } else if (options?.filters) { + const { protocol, status, search } = options.filters + if (protocol) params.protocol = protocol + if (status) params.status = status + if (search) params.search = search + } + const { data } = await apiClient.get('/admin/proxies/data', { params }) return data } diff --git a/frontend/src/components/admin/account/AccountTableActions.vue b/frontend/src/components/admin/account/AccountTableActions.vue index a449f866..ee521f83 100644 --- a/frontend/src/components/admin/account/AccountTableActions.vue +++ b/frontend/src/components/admin/account/AccountTableActions.vue @@ -6,6 +6,7 @@ + diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 3c407080..0a267f06 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -1903,6 +1903,7 @@ export default { editProxy: 'Edit Proxy', deleteProxy: 'Delete Proxy', dataImport: 'Import', + dataExportSelected: 'Export Selected', dataImportTitle: 'Import Proxies', dataImportHint: 'Upload the exported proxy JSON file to import proxies in bulk.', dataImportWarning: 'Import will create or reuse proxies, keep their status, and trigger latency checks after completion.', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 9ff89dfc..e1a70054 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -2012,6 +2012,7 @@ export default { deleteConfirmMessage: "确定要删除代理 '{name}' 吗?", testProxy: '测试代理', dataImport: '导入', + dataExportSelected: '导出选中', dataImportTitle: '导入代理', dataImportHint: '上传代理导出的 JSON 文件以批量导入代理。', dataImportWarning: '导入将创建或复用代理,保留状态并在完成后自动触发延迟检测。', diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue index e6fe25c8..d19e010e 100644 --- a/frontend/src/views/admin/AccountsView.vue +++ b/frontend/src/views/admin/AccountsView.vue @@ -96,7 +96,7 @@ -