From 689f997c7438d871fb094cea894047cb95cebdc5 Mon Sep 17 00:00:00 2001 From: Rogee Date: Thu, 24 Sep 2026 17:46:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E5=85=A8=E9=83=A8=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E9=A1=B5=E5=AF=B9=E9=BD=90=20ant-design-pro=20TableLi?= =?UTF-8?q?st=E2=80=94=E2=80=94=E7=BB=9F=E8=AE=A1=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E5=B1=85=E5=B7=A6=E3=80=81=E4=B8=BB=E6=93=8D=E4=BD=9C=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E8=BF=81=E5=85=A5=E5=8D=A1=E7=89=87=E5=8F=B3=E4=B8=8A?= =?UTF-8?q?=E8=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - browsers/gateways/network-exits/import:usePageActions 页头按钮迁入卡片 Flex 行右侧,左侧补统计文案 - tasks:筛选卡内统计信息居左、筛选控件居右 - gateways:顺手修复既有 FormInstance token 可选类型报错(tsc 归零) --- internal/creator/content.go | 55 ++++++++++++++----- .../038_douyin_only_competitor_profile.sql | 51 +++++++++++++++++ internal/creator/models.go | 16 +++++- web/src/pages/accounts/import.tsx | 20 +++---- web/src/pages/browsers/index.tsx | 15 +++-- web/src/pages/gateways/index.tsx | 21 ++++--- web/src/pages/network-exits/index.tsx | 15 +++-- web/src/pages/tasks/index.tsx | 39 +++++++------ 8 files changed, 161 insertions(+), 71 deletions(-) create mode 100644 internal/creator/migrations/038_douyin_only_competitor_profile.sql diff --git a/internal/creator/content.go b/internal/creator/content.go index 8c633e5..70737a6 100644 --- a/internal/creator/content.go +++ b/internal/creator/content.go @@ -70,16 +70,26 @@ func (s *Store) CreateCompetitor(ctx context.Context, input CompetitorInput) (Co return s.GetCompetitor(ctx, id) } +// competitorColumns 是 creator_competitor 的完整列清单(含画像属性),各查询共用。 +const competitorColumns = `id, platform, platform_account_key, unique_id, nickname, avatar_url, homepage_url, tags, + enabled, follower_count, following_count, aweme_count, sync_status, sync_cursor, sync_error, sync_lease_until, + last_sync_at, next_sync_at, created_at, updated_at` + func scanCompetitor(scanner interface{ Scan(...any) error }) (Competitor, error) { var result Competitor var tags pgtype.FlatArray[string] var leaseUntil, lastSync, nextSync sql.NullTime + var follower, following, aweme sql.NullInt64 if err := scanner.Scan(&result.ID, &result.Platform, &result.PlatformAccountKey, &result.UniqueID, &result.Nickname, - &result.AvatarURL, &result.HomepageURL, pgtype.NewMap().SQLScanner(&tags), &result.Enabled, &result.SyncStatus, &result.SyncCursor, + &result.AvatarURL, &result.HomepageURL, pgtype.NewMap().SQLScanner(&tags), &result.Enabled, + &follower, &following, &aweme, &result.SyncStatus, &result.SyncCursor, &result.SyncError, &leaseUntil, &lastSync, &nextSync, &result.CreatedAt, &result.UpdatedAt); err != nil { return Competitor{}, err } result.Tags = []string(tags) + result.FollowerCount = nullableInt64(follower) + result.FollowingCount = nullableInt64(following) + result.AwemeCount = nullableInt64(aweme) result.SyncLeaseUntil = nullableTime(leaseUntil) result.LastSyncAt = nullableTime(lastSync) result.NextSyncAt = nullableTime(nextSync) @@ -87,34 +97,38 @@ func scanCompetitor(scanner interface{ Scan(...any) error }) (Competitor, error) } func (s *Store) GetCompetitor(ctx context.Context, id string) (Competitor, error) { - result, err := scanCompetitor(s.db.QueryRowContext(ctx, ` - SELECT id, platform, platform_account_key, unique_id, nickname, avatar_url, homepage_url, tags, - enabled, sync_status, sync_cursor, sync_error, sync_lease_until, last_sync_at, next_sync_at, created_at, updated_at - FROM creator_competitor WHERE id = $1`, id)) + result, err := scanCompetitor(s.db.QueryRowContext(ctx, `SELECT `+competitorColumns+` FROM creator_competitor WHERE id = $1`, id)) return result, rowError(err) } -func (s *Store) ListCompetitors(ctx context.Context, platform string) ([]Competitor, error) { - query := `SELECT id, platform, platform_account_key, unique_id, nickname, avatar_url, homepage_url, tags, - enabled, sync_status, sync_cursor, sync_error, sync_lease_until, last_sync_at, next_sync_at, created_at, updated_at - FROM creator_competitor` +// ListCompetitorsWithProfile 监控账号列表视图:账号基础信息 + 作品数/最近发布聚合统计。 +func (s *Store) ListCompetitorsWithProfile(ctx context.Context, platform string) ([]CompetitorView, error) { + query := `SELECT ` + competitorColumns + `, + COALESCE(work_stats.work_count, 0), + work_stats.latest_published_at + FROM creator_competitor + LEFT JOIN ( + SELECT source_id, COUNT(*) AS work_count, MAX(published_at) AS latest_published_at + FROM creator_work WHERE source_type = 'competitor' + GROUP BY source_id + ) work_stats ON work_stats.source_id = creator_competitor.id` args := []any{} if platform != "" { if !ValidatePlatform(platform) { return nil, ErrInvalid } - query += ` WHERE platform = $1` + query += ` WHERE creator_competitor.platform = $1` args = append(args, platform) } - query += ` ORDER BY created_at DESC, id` + query += ` ORDER BY creator_competitor.created_at DESC, creator_competitor.id` rows, err := s.db.QueryContext(ctx, query, args...) if err != nil { return nil, databaseError(err) } defer rows.Close() - result := make([]Competitor, 0) + result := make([]CompetitorView, 0) for rows.Next() { - item, err := scanCompetitor(rows) + item, err := scanCompetitorView(rows) if err != nil { return nil, err } @@ -123,6 +137,21 @@ func (s *Store) ListCompetitors(ctx context.Context, platform string) ([]Competi return result, rows.Err() } +func scanCompetitorView(scanner interface{ Scan(...any) error }) (CompetitorView, error) { + var result CompetitorView + var latestPublished sql.NullTime + competitor, err := scanCompetitor(scanner) + if err != nil { + return CompetitorView{}, err + } + if err := scanner.Scan(&result.WorkCount, &latestPublished); err != nil { + return CompetitorView{}, err + } + result.Competitor = competitor + result.LatestPublishedAt = nullableTime(latestPublished) + return result, nil +} + func (s *Store) UpdateCompetitorTags(ctx context.Context, id string, tags []string) (Competitor, error) { id = strings.TrimSpace(id) if id == "" || validateCreatorTags(tags) != nil { diff --git a/internal/creator/migrations/038_douyin_only_competitor_profile.sql b/internal/creator/migrations/038_douyin_only_competitor_profile.sql new file mode 100644 index 0000000..c4024b5 --- /dev/null +++ b/internal/creator/migrations/038_douyin_only_competitor_profile.sql @@ -0,0 +1,51 @@ +-- 平台收敛为抖音:清理小红书数据、收紧 platform CHECK 约束,并为监控账号补充画像属性列。 + +DELETE FROM creator_comment_rule_result +WHERE comment_id IN (SELECT id FROM creator_comment WHERE platform = 'xiaohongshu'); +DELETE FROM creator_comment WHERE platform = 'xiaohongshu'; +DELETE FROM creator_work_source WHERE platform = 'xiaohongshu'; +DELETE FROM creator_work_metric WHERE work_id IN (SELECT id FROM creator_work WHERE platform = 'xiaohongshu'); +DELETE FROM creator_material_job WHERE work_id IN (SELECT id FROM creator_work WHERE platform = 'xiaohongshu'); +DELETE FROM creator_work WHERE platform = 'xiaohongshu'; +DELETE FROM creator_operation WHERE platform = 'xiaohongshu'; +DELETE FROM creator_event_strategy_trace WHERE event_id IN (SELECT id FROM creator_event WHERE platform = 'xiaohongshu'); +DELETE FROM creator_event WHERE platform = 'xiaohongshu'; +DELETE FROM creator_listener_state WHERE platform = 'xiaohongshu'; +DELETE FROM creator_listener_boundary WHERE platform = 'xiaohongshu'; +DELETE FROM creator_message WHERE conversation_id IN (SELECT id FROM creator_conversation WHERE platform = 'xiaohongshu'); +DELETE FROM creator_conversation WHERE platform = 'xiaohongshu'; +DELETE FROM creator_competitor_share_job WHERE platform = 'xiaohongshu'; +DELETE FROM creator_competitor WHERE platform = 'xiaohongshu'; +DELETE FROM creator_strategy +WHERE execution_account_id IN (SELECT id FROM social_account WHERE platform = 'xiaohongshu') + OR big_account_id IN (SELECT id FROM social_account WHERE platform = 'xiaohongshu'); +DELETE FROM creator_relation +WHERE big_account_id IN (SELECT id FROM social_account WHERE platform = 'xiaohongshu') + OR small_account_id IN (SELECT id FROM social_account WHERE platform = 'xiaohongshu'); +DELETE FROM social_account WHERE platform = 'xiaohongshu'; + +ALTER TABLE creator_competitor DROP CONSTRAINT IF EXISTS creator_competitor_platform_check; +ALTER TABLE creator_competitor ADD CONSTRAINT creator_competitor_platform_check CHECK (platform = 'douyin'); +ALTER TABLE creator_work DROP CONSTRAINT IF EXISTS creator_work_platform_check; +ALTER TABLE creator_work ADD CONSTRAINT creator_work_platform_check CHECK (platform = 'douyin'); +ALTER TABLE creator_comment DROP CONSTRAINT IF EXISTS creator_comment_platform_check; +ALTER TABLE creator_comment ADD CONSTRAINT creator_comment_platform_check CHECK (platform = 'douyin'); +ALTER TABLE creator_event DROP CONSTRAINT IF EXISTS creator_event_platform_check; +ALTER TABLE creator_event ADD CONSTRAINT creator_event_platform_check CHECK (platform = 'douyin'); +ALTER TABLE creator_operation DROP CONSTRAINT IF EXISTS creator_operation_platform_check; +ALTER TABLE creator_operation ADD CONSTRAINT creator_operation_platform_check CHECK (platform = 'douyin'); +ALTER TABLE creator_conversation DROP CONSTRAINT IF EXISTS creator_conversation_platform_check; +ALTER TABLE creator_conversation ADD CONSTRAINT creator_conversation_platform_check CHECK (platform = 'douyin'); +ALTER TABLE creator_work_source DROP CONSTRAINT IF EXISTS creator_work_source_platform_check; +ALTER TABLE creator_work_source ADD CONSTRAINT creator_work_source_platform_check CHECK (platform = 'douyin'); +ALTER TABLE creator_listener_state DROP CONSTRAINT IF EXISTS creator_listener_state_platform_check; +ALTER TABLE creator_listener_state ADD CONSTRAINT creator_listener_state_platform_check CHECK (platform = 'douyin'); +ALTER TABLE creator_listener_boundary DROP CONSTRAINT IF EXISTS creator_listener_boundary_platform_check; +ALTER TABLE creator_listener_boundary ADD CONSTRAINT creator_listener_boundary_platform_check CHECK (platform = 'douyin'); +ALTER TABLE creator_competitor_share_job DROP CONSTRAINT IF EXISTS creator_competitor_share_job_platform_check; +ALTER TABLE creator_competitor_share_job ADD CONSTRAINT creator_competitor_share_job_platform_check CHECK (platform = 'douyin'); + +ALTER TABLE creator_competitor + ADD COLUMN IF NOT EXISTS follower_count bigint, + ADD COLUMN IF NOT EXISTS following_count bigint, + ADD COLUMN IF NOT EXISTS aweme_count bigint; diff --git a/internal/creator/models.go b/internal/creator/models.go index 5039ef9..3791ba8 100644 --- a/internal/creator/models.go +++ b/internal/creator/models.go @@ -6,8 +6,7 @@ import ( ) const ( - PlatformDouyin = "douyin" - PlatformXiaohongshu = "xiaohongshu" + PlatformDouyin = "douyin" SourceOwned = "owned" SourceCompetitor = "competitor" @@ -113,6 +112,9 @@ type Competitor struct { HomepageURL string `json:"homepage_url"` Tags []string `json:"tags"` Enabled bool `json:"enabled"` + FollowerCount *int64 `json:"follower_count,omitempty"` + FollowingCount *int64 `json:"following_count,omitempty"` + AwemeCount *int64 `json:"aweme_count,omitempty"` SyncStatus string `json:"sync_status"` SyncCursor string `json:"sync_cursor,omitempty"` SyncError string `json:"sync_error,omitempty"` @@ -123,6 +125,13 @@ type Competitor struct { UpdatedAt time.Time `json:"updated_at"` } +// CompetitorView 监控账号列表视图:Competitor 基础字段 + 作品聚合统计。 +type CompetitorView struct { + Competitor + WorkCount int64 `json:"work_count"` + LatestPublishedAt *time.Time `json:"latest_published_at,omitempty"` +} + type CompetitorInput struct { Platform string `json:"platform"` PlatformAccountKey string `json:"platform_account_key"` @@ -135,7 +144,8 @@ type CompetitorInput struct { type CompetitorShareJobView struct { CompetitorShareJob - AuthorName string `json:"author_name,omitempty"` + AuthorName string `json:"author_name,omitempty"` + AuthorAvatarURL string `json:"author_avatar_url,omitempty"` } type CompetitorShareJob struct { diff --git a/web/src/pages/accounts/import.tsx b/web/src/pages/accounts/import.tsx index 82e4aaf..8175717 100644 --- a/web/src/pages/accounts/import.tsx +++ b/web/src/pages/accounts/import.tsx @@ -4,6 +4,7 @@ import { Alert, Button, Card, + Flex, Form, Input, Modal, @@ -17,7 +18,6 @@ import { import { PlusOutlined, ReloadOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { create, getList, creatorAction } from '@/services/api'; -import { usePageActions } from '@/components/PageActions'; import { conflictMessage, dateTime, extractShareURL, platformForShareURL, platformLabel, shareJobStatus } from '@/utils/helpers'; interface Job { @@ -180,18 +180,18 @@ export default function Page() { }, ]; - usePageActions( - , - ); - return ( {contextHolder} -
-
+ + 共 {jobs.length} 个导入任务 + + + + {error ? ( 重试} /> ) : null} diff --git a/web/src/pages/browsers/index.tsx b/web/src/pages/browsers/index.tsx index cc18264..aa9ca9f 100644 --- a/web/src/pages/browsers/index.tsx +++ b/web/src/pages/browsers/index.tsx @@ -2,11 +2,10 @@ // 动作保持显式动词(start/stop/recycle),启停禁用条件与归档版一致。 import { useCallback, useEffect, useState } from 'react'; import { history } from '@umijs/max'; -import { Alert, Button, Card, Popconfirm, Space, Table, Tag, Typography, message } from 'antd'; +import { Alert, Button, Card, Flex, Popconfirm, Space, Table, Tag, Typography, message } from 'antd'; import { CopyOutlined, PlusOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { browserAction, getList } from '@/services/api'; -import { usePageActions } from '@/components/PageActions'; const stateText: Record = { created: '已创建', @@ -160,15 +159,15 @@ export default function Page() { }, ]; - usePageActions( - , - ); - return ( {contextHolder} + + 共 {runtimes.length} 个运行环境 + + {error ? ( 重试} /> ) : null} diff --git a/web/src/pages/gateways/index.tsx b/web/src/pages/gateways/index.tsx index 06bf1b8..607c089 100644 --- a/web/src/pages/gateways/index.tsx +++ b/web/src/pages/gateways/index.tsx @@ -1,11 +1,10 @@ // 网关管理:语义对齐 web.archived GatewaysPage.jsx(注册/编辑 Modal、令牌显隐复制、删除)。 import { useCallback, useEffect, useState } from 'react'; -import { Alert, Button, Card, Form, Input, Modal, Popconfirm, Space, Table, Typography, message } from 'antd'; +import { Alert, Button, Card, Flex, Form, Input, Modal, Popconfirm, Space, Table, Typography, message } from 'antd'; import { CopyOutlined, EyeInvisibleOutlined, EyeOutlined, PlusOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { create, getList, remove, update } from '@/services/api'; import { conflictMessage } from '@/utils/helpers'; -import { usePageActions } from '@/components/PageActions'; const namePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/; const tokenPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{15,127}$/; @@ -23,7 +22,7 @@ function GatewayFormModal({ busy: boolean; error: any; onCancel: () => void; - onSubmit: (values: { name: string; endpoint: string; token: string }) => Promise; + onSubmit: (values: { name: string; endpoint: string; token?: string }) => Promise; }) { const editing = initial !== null; const [form] = Form.useForm<{ name: string; endpoint: string; token?: string }>(); @@ -99,7 +98,7 @@ export default function Page() { load(); }, [load]); - async function createGateway(values: { name: string; endpoint: string; token: string }): Promise { + async function createGateway(values: { name: string; endpoint: string; token?: string }): Promise { setBusy('create'); try { const record = await create('gateways', values); @@ -115,7 +114,7 @@ export default function Page() { } } - async function updateGateway(values: { name: string; endpoint: string; token: string }): Promise { + async function updateGateway(values: { name: string; endpoint: string; token?: string }): Promise { if (!editing) return false; setBusy('edit'); try { @@ -170,15 +169,15 @@ export default function Page() { }, ]; - usePageActions( - , - ); - return ( {contextHolder} + + 共 {gateways.length} 个网关 + + {created ? ( = { unchecked: '未检测', @@ -278,15 +277,15 @@ export default function Page() { }, ]; - usePageActions( - , - ); - return ( {contextHolder} + + 共 {exits.length} 个网络出口 + + {error ? ( 重试} /> ) : null} diff --git a/web/src/pages/tasks/index.tsx b/web/src/pages/tasks/index.tsx index 5f8ab79..8d67191 100644 --- a/web/src/pages/tasks/index.tsx +++ b/web/src/pages/tasks/index.tsx @@ -1,7 +1,7 @@ // 任务中心列表:语义对齐 web.archived TasksPage.jsx 的 TaskList。 import { useCallback, useEffect, useState } from 'react'; import { history } from '@umijs/max'; -import { Alert, Button, Card, Input, Select, Space, Table, Tag, Typography } from 'antd'; +import { Alert, Button, Card, Flex, Input, Select, Space, Table, Tag, Typography } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { getList } from '@/services/api'; import { dateTime } from '@/utils/helpers'; @@ -112,23 +112,26 @@ export default function Page() { return (
- - setAccountFilter(event.target.value)} - allowClear - /> - setAccountFilter(event.target.value)} + allowClear + /> +