From 01d1d44a15bf820ac09a3a1010c7c11e2c01d137 Mon Sep 17 00:00:00 2001 From: Rogee Date: Sun, 27 Sep 2026 19:44:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(creator):=20=E7=AB=9E=E5=93=81=E4=BD=9C?= =?UTF-8?q?=E5=93=81=E6=8C=87=E6=A0=87=E9=87=87=E9=9B=86=E6=94=B9=E7=94=A8?= =?UTF-8?q?=E5=8C=BF=E5=90=8D=E6=B5=8F=E8=A7=88=E5=99=A8=E5=B9=B6=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=94=B6=E8=97=8F=E6=95=B0=E4=B8=8E=E7=88=86=E5=93=81?= =?UTF-8?q?=E5=A2=9E=E9=80=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 指标采集不再依赖已登录自有账号:竞品作品到期后经匿名浏览器拉取 抖音作品详情接口(detail 返回完整点赞/收藏/转发/评论快照) - 补采收藏数:creator_work 与 creator_work_metric 新增 collect_count (迁移 039),works 列表与 detail 解析均映射该字段 - 新增点赞增速:LoadWorksGrowth 按时间窗计算基线增量(full/partial 覆盖标注),works API 支持 growth_hours/min_likes_growth 筛选排序 - 前端监控详情页:作品卡片展示 24h 点赞增量、支持按增量筛选爆品; 作品 Drawer 新增增长曲线折线图(@ant-design/charts)与收藏列 --- internal/controlplane/api/creator.go | 60 +- internal/creator/content.go | 40 +- internal/creator/growth.go | 162 ++ internal/creator/growth_test.go | 82 + internal/creator/integration_test.go | 33 +- internal/creator/metrics.go | 12 +- .../migrations/039_work_collect_count.sql | 4 + internal/creator/models.go | 7 + internal/creator/store.go | 4 + internal/platform/douyin/connector.go | 2 + internal/platform/douyin/creator_collector.go | 67 +- .../platform/douyin/creator_collector_test.go | 32 +- web/package-lock.json | 1588 ++++++++++++----- web/package.json | 1 + .../pages/accounts/monitoring/$id/index.tsx | 79 +- web/src/services/api.ts | 2 +- 16 files changed, 1685 insertions(+), 490 deletions(-) create mode 100644 internal/creator/growth.go create mode 100644 internal/creator/growth_test.go create mode 100644 internal/creator/migrations/039_work_collect_count.sql diff --git a/internal/controlplane/api/creator.go b/internal/controlplane/api/creator.go index 8581b73..1953da0 100644 --- a/internal/controlplane/api/creator.go +++ b/internal/controlplane/api/creator.go @@ -1017,6 +1017,21 @@ func workFilter(c fiber.Ctx) (creator.WorkFilter, error) { } *field.target = &parsed } + // 爆品筛选:growth_hours 开启点赞增量计算(默认 24h),min_likes_growth 设增量下限。 + if value := c.Query("growth_hours"); value != "" { + parsed, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return creator.WorkFilter{}, creator.ErrInvalid + } + filter.Growth.Hours = parsed + } + if value := c.Query("min_likes_growth"); value != "" { + parsed, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return creator.WorkFilter{}, creator.ErrInvalid + } + filter.Growth.MinLikes = &parsed + } return filter, nil } @@ -1962,21 +1977,50 @@ func runCreatorMetricScheduleOnce(ctx context.Context, store *creator.Store, pha return err } for _, work := range works { - accountID := work.SourceID + var refreshErr error if work.SourceType == creator.SourceCompetitor { - accountID, err = creatorCollectionAccount(ctx, store, phaseAStore, hubStore, work.Platform) - if err != nil { - logrus.WithError(err).WithField("work_id", work.ID).Warn("creator metric refresh account unavailable") - continue - } + // 竞品作品指标是公开数据:匿名浏览器拉详情接口快照,不依赖已登录自有账号。 + refreshErr = refreshCreatorCompetitorMetricAnonymous(ctx, store, hubStore, work, settings, now) + } else { + accountID := work.SourceID + refreshErr = refreshCreatorMetricWork(ctx, store, phaseAStore, hubStore, work, accountID, settings, now) } - if err := refreshCreatorMetricWork(ctx, store, phaseAStore, hubStore, work, accountID, settings, now); err != nil { - logrus.WithError(err).WithField("work_id", work.ID).Warn("creator metric refresh failed") + if refreshErr != nil { + logrus.WithError(refreshErr).WithField("work_id", work.ID).Warn("creator metric refresh failed") } } return nil } +func refreshCreatorCompetitorMetricAnonymous(ctx context.Context, store *creator.Store, hubStore *hub.Store, work creator.Work, settings creator.Settings, now time.Time) error { + competitor, err := store.GetCompetitor(ctx, work.SourceID) + if err != nil { + if errors.Is(err, creator.ErrNotFound) { + return store.StopMetricPlan(ctx, work.ID, "source_deleted") + } + return err + } + if !competitor.Enabled { + return store.StopMetricPlan(ctx, work.ID, "source_disabled") + } + if competitor.Platform != work.Platform || competitor.Platform != creator.PlatformDouyin { + return creator.ErrConflict + } + lease, err := newAnonymousBrowser(ctx, hubStore) + if err != nil { + return fmt.Errorf("%w: anonymous browser unavailable: %v", creator.ErrUnavailable, err) + } + stats, fetchErr := douyin.FetchWorkStatistics(ctx, creatorGatewayBrowser{gateway: lease.gateway, environment: lease.environment}, work.WorkKey) + if closeErr := lease.close(); closeErr != nil { + fetchErr = errors.Join(fetchErr, closeErr) + } + if fetchErr != nil { + return fetchErr + } + _, recordErr := store.RecordMetric(ctx, creator.MetricInput{WorkID: work.ID, CollectedAt: now, Likes: stats.Likes, CommentsCount: stats.CommentsCount, Shares: stats.Shares, CollectCount: stats.CollectCount}, settings, now) + return recordErr +} + func refreshCreatorMetricWork(ctx context.Context, store *creator.Store, phaseAStore *accountdomain.Store, hubStore *hub.Store, work creator.Work, accountID string, settings creator.Settings, now time.Time) (resultErr error) { if work.SourceType == creator.SourceCompetitor { competitor, err := store.GetCompetitor(ctx, work.SourceID) diff --git a/internal/creator/content.go b/internal/creator/content.go index 2342e41..a898a13 100644 --- a/internal/creator/content.go +++ b/internal/creator/content.go @@ -379,8 +379,8 @@ func (s *Store) UpsertWork(ctx context.Context, input WorkInput, now time.Time) var inserted bool err = tx.QueryRowContext(ctx, ` INSERT INTO creator_work (id, platform, work_key, source_type, source_id, author_name, title, body, - published_at, published_at_status, original_url, cover_url, raw_payload, likes, comments_count, shares) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) + published_at, published_at_status, original_url, cover_url, raw_payload, likes, comments_count, shares, collect_count) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) ON CONFLICT (platform, work_key) DO UPDATE SET author_name = CASE WHEN EXCLUDED.author_name = '' THEN creator_work.author_name ELSE EXCLUDED.author_name END, title = CASE WHEN EXCLUDED.title = '' THEN creator_work.title ELSE EXCLUDED.title END, @@ -390,10 +390,14 @@ func (s *Store) UpsertWork(ctx context.Context, input WorkInput, now time.Time) original_url = CASE WHEN EXCLUDED.original_url = '' THEN creator_work.original_url ELSE EXCLUDED.original_url END, cover_url = CASE WHEN EXCLUDED.cover_url = '' THEN creator_work.cover_url ELSE EXCLUDED.cover_url END, raw_payload = COALESCE(EXCLUDED.raw_payload, creator_work.raw_payload), + likes = COALESCE(EXCLUDED.likes, creator_work.likes), + comments_count = COALESCE(EXCLUDED.comments_count, creator_work.comments_count), + shares = COALESCE(EXCLUDED.shares, creator_work.shares), + collect_count = COALESCE(EXCLUDED.collect_count, creator_work.collect_count), updated_at = now() RETURNING id, (xmax = 0)`, id, input.Platform, input.WorkKey, input.SourceType, input.SourceID, input.AuthorName, input.Title, input.Body, input.PublishedAt, status, input.OriginalURL, input.CoverURL, - nullableRawPayload(input.RawPayload), input.Likes, input.CommentsCount, input.Shares).Scan(&returnedID, &inserted) + nullableRawPayload(input.RawPayload), input.Likes, input.CommentsCount, input.Shares, input.CollectCount).Scan(&returnedID, &inserted) if err != nil { return Work{}, false, databaseError(err) } @@ -428,11 +432,11 @@ func nullableRawPayload(value string) any { func scanWork(scanner interface{ Scan(...any) error }) (Work, error) { var result Work var publishedAt, latestAt, nextAt sql.NullTime - var likes, commentsCount, shares sql.NullInt64 + var likes, commentsCount, shares, collectCount sql.NullInt64 var rawPayload sql.NullString if err := scanner.Scan(&result.ID, &result.Platform, &result.WorkKey, &result.SourceType, &result.SourceID, &result.AuthorName, &result.Title, &result.Body, &publishedAt, &result.PublishedAtStatus, - &result.OriginalURL, &result.CoverURL, &rawPayload, &likes, &commentsCount, &shares, &latestAt, &nextAt, + &result.OriginalURL, &result.CoverURL, &rawPayload, &likes, &commentsCount, &shares, &collectCount, &latestAt, &nextAt, &result.MetricStopReason, &result.CreatedAt, &result.UpdatedAt); err != nil { return Work{}, err } @@ -441,12 +445,13 @@ func scanWork(scanner interface{ Scan(...any) error }) (Work, error) { result.RawPayload = rawPayload.String } result.Likes, result.CommentsCount, result.Shares = nullableInt64(likes), nullableInt64(commentsCount), nullableInt64(shares) + result.CollectCount = nullableInt64(collectCount) result.LatestMetricsAt, result.NextMetricAt = nullableTime(latestAt), nullableTime(nextAt) return result, nil } const workSelect = `SELECT id, platform, work_key, source_type, source_id, author_name, title, body, - published_at, published_at_status, original_url, cover_url, raw_payload, likes, comments_count, shares, + published_at, published_at_status, original_url, cover_url, raw_payload, likes, comments_count, shares, collect_count, latest_metrics_at, next_metric_at, metric_stop_reason, created_at, updated_at FROM creator_work` func (s *Store) loadWorkSources(ctx context.Context, work *Work) error { @@ -585,7 +590,7 @@ func (s *Store) ListWorks(ctx context.Context, filter WorkFilter) ([]Work, error } func (s *Store) RecordMetric(ctx context.Context, input MetricInput, settings Settings, now time.Time) (MetricPoint, error) { - if input.WorkID == "" || input.CollectedAt.IsZero() || input.Likes != nil && *input.Likes < 0 || input.CommentsCount != nil && *input.CommentsCount < 0 || input.Shares != nil && *input.Shares < 0 { + if input.WorkID == "" || input.CollectedAt.IsZero() || input.Likes != nil && *input.Likes < 0 || input.CommentsCount != nil && *input.CommentsCount < 0 || input.Shares != nil && *input.Shares < 0 || input.CollectCount != nil && *input.CollectCount < 0 { return MetricPoint{}, ErrInvalid } if err := ValidateSettings(SettingsUpdate{LookbackDays: settings.LookbackDays, NewWorkIntervalSeconds: settings.NewWorkIntervalSeconds, MetricInitialIntervalSeconds: settings.MetricInitialIntervalSeconds, MetricMultiplier: settings.MetricMultiplier, MetricMaxIntervalSeconds: settings.MetricMaxIntervalSeconds, MetricAgeSeconds: settings.MetricAgeSeconds}); err != nil { @@ -605,7 +610,7 @@ func nullableArg(value time.Time) any { } func (s *Store) ListMetrics(ctx context.Context, workID string) ([]MetricPoint, error) { - rows, err := s.db.QueryContext(ctx, `SELECT collected_at, likes, comments_count, shares FROM creator_work_metric WHERE work_id = $1 ORDER BY collected_at`, workID) + rows, err := s.db.QueryContext(ctx, `SELECT collected_at, likes, comments_count, shares, collect_count FROM creator_work_metric WHERE work_id = $1 ORDER BY collected_at`, workID) if err != nil { return nil, databaseError(err) } @@ -613,12 +618,13 @@ func (s *Store) ListMetrics(ctx context.Context, workID string) ([]MetricPoint, result := make([]MetricPoint, 0) for rows.Next() { var point MetricPoint - var likes, commentsCount, shares sql.NullInt64 - if err := rows.Scan(&point.CollectedAt, &likes, &commentsCount, &shares); err != nil { + var likes, commentsCount, shares, collectCount sql.NullInt64 + if err := rows.Scan(&point.CollectedAt, &likes, &commentsCount, &shares, &collectCount); err != nil { return nil, err } point.CollectedAt = point.CollectedAt.UTC() point.Likes, point.CommentsCount, point.Shares = nullableInt64(likes), nullableInt64(commentsCount), nullableInt64(shares) + point.CollectCount = nullableInt64(collectCount) result = append(result, point) } return result, rows.Err() @@ -977,6 +983,20 @@ func (s *Store) ListWorksPage(ctx context.Context, filter WorkFilter, page, page if err != nil { return Page[Work]{}, err } + if filter.Growth.Hours != 0 || filter.Growth.MinLikes != nil { + ids := make([]string, 0, len(items)) + for _, item := range items { + ids = append(ids, item.ID) + } + growth, growthErr := s.LoadWorksGrowth(ctx, ids, filter.Growth) + if growthErr != nil { + return Page[Work]{}, growthErr + } + items, err = ApplyWorkGrowthFilter(items, growth, filter.Growth) + if err != nil { + return Page[Work]{}, err + } + } return slicePage(items, page, pageSize) } diff --git a/internal/creator/growth.go b/internal/creator/growth.go new file mode 100644 index 0000000..5f553d0 --- /dev/null +++ b/internal/creator/growth.go @@ -0,0 +1,162 @@ +package creator + +import ( + "context" + "sort" + "time" +) + +// WorkGrowth 是作品在观察窗口内的点赞增量,用于爆品筛选。 +// Baseline 为窗口起点前最后一个采样点;无基线时以最早采样点近似。 +type WorkGrowth struct { + Likes *int64 `json:"likes_growth"` + Coverage string `json:"likes_growth_coverage"` // full=有窗口起点基线;partial=以首点近似 +} + +// WorkGrowthFilter 增速筛选条件。 +type WorkGrowthFilter struct { + Hours int64 // 观察窗口长度,默认 24 + MinLikes *int64 +} + +// normalizeGrowthFilter 校验并补默认值。 +func normalizeGrowthFilter(filter WorkGrowthFilter) (WorkGrowthFilter, error) { + if filter.Hours == 0 { + filter.Hours = 24 + } + if filter.Hours < 0 || filter.Hours > 24*30 { + return WorkGrowthFilter{}, ErrInvalid + } + if filter.MinLikes != nil && *filter.MinLikes < 0 { + return WorkGrowthFilter{}, ErrInvalid + } + return filter, nil +} + +// computeWorkGrowth 从按时间升序的采样点计算观察窗口内的点赞增量。 +// points 为空返回零值(无数据);不足两个点或无基线时 coverage=partial。 +func computeWorkGrowth(points []MetricPoint, now time.Time, hours int64) WorkGrowth { + // hours 为小时数(接口参数同名同义),窗口起点 = now - hours 小时。 + cutoff := now.Add(-time.Duration(hours) * time.Hour) + var baseline, latest *MetricPoint + for index := range points { + point := &points[index] + if point.Likes == nil { + continue + } + if !point.CollectedAt.Before(cutoff) { + // 窗口起点及之后的点属于窗口内;升序遍历取最晚点作为 latest。 + latest = point + continue + } + // 窗口起点前最近点作为基线(升序遍历,最后赋值即最近)。 + baseline = point + } + if latest == nil { + // 窗口内无采样点:以最后一个窗口外点近似(数据稀疏时的降级)。 + for index := range points { + point := &points[index] + if point.Likes == nil { + continue + } + latest = point + } + if latest == nil { + return WorkGrowth{} + } + } + if baseline == nil { + // 无窗口起点前基线:以窗口内最早采样点近似基线(partial)。 + for index := range points { + point := &points[index] + if point.Likes == nil { + continue + } + if !point.CollectedAt.Before(cutoff) { + baseline = point + break + } + } + if baseline == nil || baseline == latest { + // 仅有单点:无法计算增量。 + return WorkGrowth{Likes: int64Ptr(0), Coverage: "partial"} + } + return WorkGrowth{Likes: int64Ptr(*latest.Likes - *baseline.Likes), Coverage: "partial"} + } + if baseline == latest { + return WorkGrowth{Likes: int64Ptr(0), Coverage: "partial"} + } + return WorkGrowth{Likes: int64Ptr(*latest.Likes - *baseline.Likes), Coverage: "full"} +} + +// LoadWorksGrowth 批量加载作品的点赞增量。返回 map[workID]WorkGrowth;无采样点的作品不在 map 中。 +func (s *Store) LoadWorksGrowth(ctx context.Context, workIDs []string, filter WorkGrowthFilter) (map[string]WorkGrowth, error) { + filter, err := normalizeGrowthFilter(filter) + if err != nil { + return nil, err + } + if len(workIDs) == 0 { + return map[string]WorkGrowth{}, nil + } + rows, err := s.db.QueryContext(ctx, `SELECT work_id, collected_at, likes FROM creator_work_metric WHERE work_id = ANY($1) AND likes IS NOT NULL ORDER BY work_id, collected_at`, workIDs) + if err != nil { + return nil, databaseError(err) + } + defer rows.Close() + byWork := make(map[string][]MetricPoint) + for rows.Next() { + var workID string + var point MetricPoint + var likes int64 + if err := rows.Scan(&workID, &point.CollectedAt, &likes); err != nil { + return nil, databaseError(err) + } + point.CollectedAt = point.CollectedAt.UTC() + value := likes + point.Likes = &value + byWork[workID] = append(byWork[workID], point) + } + if err := rows.Err(); err != nil { + return nil, databaseError(err) + } + now := time.Now().UTC() + result := make(map[string]WorkGrowth, len(byWork)) + for workID, points := range byWork { + result[workID] = computeWorkGrowth(points, now, filter.Hours) + } + return result, nil +} + +// ApplyWorkGrowthFilter 为作品列表附加增速并按条件过滤、排序(增速降序)。 +// 只有设置了 MinLikes 才改变排序;否则保持原顺序仅附加增速。 +func ApplyWorkGrowthFilter(works []Work, growth map[string]WorkGrowth, filter WorkGrowthFilter) ([]Work, error) { + filter, err := normalizeGrowthFilter(filter) + if err != nil { + return nil, err + } + result := make([]Work, 0, len(works)) + for _, work := range works { + if stat, ok := growth[work.ID]; ok { + work.LikesGrowth = stat.Likes + work.LikesGrowthCoverage = stat.Coverage + } + if filter.MinLikes != nil { + if work.LikesGrowth == nil || *work.LikesGrowth < *filter.MinLikes { + continue + } + } + result = append(result, work) + } + if filter.MinLikes != nil { + sort.SliceStable(result, func(i, j int) bool { + gi, gj := result[i].LikesGrowth, result[j].LikesGrowth + if gi == nil || gj == nil { + return gj == nil && gi != nil + } + return *gi > *gj + }) + } + return result, nil +} + +func int64Ptr(value int64) *int64 { return &value } diff --git a/internal/creator/growth_test.go b/internal/creator/growth_test.go new file mode 100644 index 0000000..ae95a83 --- /dev/null +++ b/internal/creator/growth_test.go @@ -0,0 +1,82 @@ +package creator + +import ( + "testing" + "time" +) + +func TestComputeWorkGrowth(t *testing.T) { + now := time.Date(2026, 9, 27, 12, 0, 0, 0, time.UTC) + likes := func(at time.Time, value int64) MetricPoint { + return MetricPoint{CollectedAt: at, Likes: &value} + } + tooOld := now.Add(-72 * time.Hour) + + t.Run("full baseline inside window", func(t *testing.T) { + points := []MetricPoint{ + likes(tooOld, 100), + likes(now.Add(-25*time.Hour), 200), // 窗口起点前最近点 → 基线 + likes(now.Add(-2*time.Hour), 500), + } + growth := computeWorkGrowth(points, now, 24) + if growth.Likes == nil || *growth.Likes != 300 || growth.Coverage != "full" { + t.Fatalf("unexpected growth: %+v", growth) + } + }) + + t.Run("partial when only one point", func(t *testing.T) { + points := []MetricPoint{likes(now.Add(-2*time.Hour), 500)} + growth := computeWorkGrowth(points, now, 24) + if growth.Likes == nil || *growth.Likes != 0 || growth.Coverage != "partial" { + t.Fatalf("unexpected growth: %+v", growth) + } + }) + + t.Run("partial falls back to first point", func(t *testing.T) { + points := []MetricPoint{likes(now.Add(-20*time.Hour), 100), likes(now.Add(-2*time.Hour), 500)} + growth := computeWorkGrowth(points, now, 24) + // 两个点都在窗口内、窗口前无基线 → 以首点近似,partial。 + if growth.Likes == nil || *growth.Likes != 400 || growth.Coverage != "partial" { + t.Fatalf("unexpected growth: %+v likes=%v", growth, *growth.Likes) + } + }) + + t.Run("old pre-window baseline counts as full", func(t *testing.T) { + points := []MetricPoint{likes(tooOld, 100), likes(now.Add(-2*time.Hour), 500)} + growth := computeWorkGrowth(points, now, 24) + // 窗口前存在真实基线(72h 前)→ full;增量跨 72h 但可比。 + if growth.Likes == nil || *growth.Likes != 400 || growth.Coverage != "full" { + t.Fatalf("unexpected growth: %+v", growth) + } + }) + + t.Run("no points", func(t *testing.T) { + if growth := (WorkGrowth{}); computeWorkGrowth(nil, now, 24) != growth { + t.Fatal("empty points must yield zero growth") + } + }) +} + +func TestApplyWorkGrowthFilterSortsAndFilters(t *testing.T) { + mk := func(id string, growth int64) (Work, WorkGrowth) { + value := growth + return Work{ID: id}, WorkGrowth{Likes: &value, Coverage: "full"} + } + w1, g1 := mk("a", 100) + w2, g2 := mk("b", 900) + w3, _ := mk("c", 0) // 无采样点:不附加增速 + works := []Work{w1, w2, w3} + growth := map[string]WorkGrowth{"a": g1, "b": g2} + + min := int64(50) + filtered, err := ApplyWorkGrowthFilter(works, growth, WorkGrowthFilter{Hours: 24, MinLikes: &min}) + if err != nil { + t.Fatal(err) + } + if len(filtered) != 2 || filtered[0].ID != "b" || filtered[1].ID != "a" { + t.Fatalf("unexpected filter/sort result: %+v", filtered) + } + if filtered[0].LikesGrowth == nil || *filtered[0].LikesGrowth != 900 { + t.Fatalf("growth not attached: %+v", filtered[0]) + } +} diff --git a/internal/creator/integration_test.go b/internal/creator/integration_test.go index 4bf1434..2a00bdc 100644 --- a/internal/creator/integration_test.go +++ b/internal/creator/integration_test.go @@ -354,13 +354,44 @@ func TestCreatorPostgresContentAndWorkflow(t *testing.T) { if _, duplicate, err := store.SaveComment(ctx, CommentInput{Platform: PlatformDouyin, CommentKey: comment.CommentKey, WorkID: work.ID, Content: "咨询价格", CommentType: "top_level"}); err != nil || duplicate { t.Fatalf("comment deduplication failed: duplicate=%v err=%v", duplicate, err) } - if _, err := store.RecordMetric(ctx, MetricInput{WorkID: work.ID, CollectedAt: now, Likes: &likes, CommentsCount: &comments, Shares: &shares}, Settings{LookbackDays: 30, NewWorkIntervalSeconds: 1800, MetricInitialIntervalSeconds: 3600, MetricMultiplier: 2, MetricMaxIntervalSeconds: 86400, MetricAgeSeconds: 30 * 24 * 60 * 60}, now); err != nil { + collects := int64(4) + if _, err := store.RecordMetric(ctx, MetricInput{WorkID: work.ID, CollectedAt: now, Likes: &likes, CommentsCount: &comments, Shares: &shares, CollectCount: &collects}, Settings{LookbackDays: 30, NewWorkIntervalSeconds: 1800, MetricInitialIntervalSeconds: 3600, MetricMultiplier: 2, MetricMaxIntervalSeconds: 86400, MetricAgeSeconds: 30 * 24 * 60 * 60}, now); err != nil { t.Fatal(err) } metrics, err := store.ListMetrics(ctx, work.ID) if err != nil || len(metrics) != 1 { t.Fatalf("initial metric snapshot failed: metrics=%+v err=%v", metrics, err) } + if metrics[0].CollectCount == nil || *metrics[0].CollectCount != 4 { + t.Fatalf("collect_count not persisted: %+v", metrics[0]) + } + // 增速计算:独立作品(避免干扰上方计划推进断言),两个采样点。 + growthPublished := now.Add(-2 * time.Hour) + growthLikes0, growthLikes1 := int64(100), int64(400) + growthWork, _, err := store.UpsertWork(ctx, WorkInput{Platform: PlatformDouyin, WorkKey: "creator-it-growth-work-" + stamp, SourceType: SourceOwned, SourceID: smallID, Title: "Growth", Body: "body", PublishedAt: &growthPublished, PublishedAtStatus: "verified", Likes: &growthLikes0}, growthPublished.Add(time.Hour)) + if err != nil { + t.Fatal(err) + } + growthSettings := Settings{LookbackDays: 30, NewWorkIntervalSeconds: 1800, MetricInitialIntervalSeconds: 3600, MetricMultiplier: 2, MetricMaxIntervalSeconds: 86400, MetricAgeSeconds: 30 * 24 * 60 * 60} + growthPoint0 := now.Add(-90 * time.Minute) // 首点(晚于发布时间) + if _, err := store.recordMetricWithPlan(ctx, MetricInput{WorkID: growthWork.ID, CollectedAt: growthPoint0, Likes: &growthLikes0}, growthSettings, growthPoint0); err != nil { + t.Fatal(err) + } + growthPoint1 := growthPoint0.Add(2 * time.Hour) // 第二个计划点:next = 首点+2h + if _, err := store.recordMetricWithPlan(ctx, MetricInput{WorkID: growthWork.ID, CollectedAt: growthPoint1, Likes: &growthLikes1}, growthSettings, growthPoint1); err != nil { + t.Fatal(err) + } + growth, err := store.LoadWorksGrowth(ctx, []string{growthWork.ID, work.ID}, WorkGrowthFilter{Hours: 24}) + if err != nil { + t.Fatal(err) + } + stat, ok := growth[growthWork.ID] + if !ok || stat.Likes == nil || *stat.Likes != 300 || stat.Coverage != "partial" { + t.Fatalf("growth not computed: stat=%+v ok=%v", stat, ok) + } + if stat, ok := growth[work.ID]; !ok || stat.Likes == nil || *stat.Likes != 0 || stat.Coverage != "partial" { + t.Fatalf("single-point work growth: %+v ok=%v", stat, ok) + } if _, err := store.RecordMetric(ctx, MetricInput{WorkID: work.ID, CollectedAt: now.Add(-time.Minute), Likes: &likes, CommentsCount: &comments, Shares: &shares}, Settings{LookbackDays: 30, NewWorkIntervalSeconds: 1800, MetricInitialIntervalSeconds: 3600, MetricMultiplier: 2, MetricMaxIntervalSeconds: 86400, MetricAgeSeconds: 30 * 24 * 60 * 60}, now); !errors.Is(err, ErrConflict) { t.Fatalf("expected early metric point to be rejected, got %v", err) } diff --git a/internal/creator/metrics.go b/internal/creator/metrics.go index 78fa574..e23949a 100644 --- a/internal/creator/metrics.go +++ b/internal/creator/metrics.go @@ -141,9 +141,9 @@ func (s *Store) recordMetricWithPlan(ctx context.Context, input MetricInput, set if err := tx.QueryRowContext(ctx, `SELECT published_at, published_at_status, latest_metrics_at FROM creator_work WHERE id=$1 FOR UPDATE`, input.WorkID).Scan(&publishedAt, &publishedStatus, &latestAt); err != nil { return MetricPoint{}, rowError(err) } - point := MetricPoint{CollectedAt: collectedAt, Likes: input.Likes, CommentsCount: input.CommentsCount, Shares: input.Shares} + point := MetricPoint{CollectedAt: collectedAt, Likes: input.Likes, CommentsCount: input.CommentsCount, Shares: input.Shares, CollectCount: input.CollectCount} if !publishedAt.Valid || publishedStatus != "verified" { - if _, err := tx.ExecContext(ctx, `UPDATE creator_work SET likes=$2, comments_count=$3, shares=$4, latest_metrics_at=$5, next_metric_at=NULL, metric_stop_reason='published_at_pending_verification', updated_at=now() WHERE id=$1`, input.WorkID, input.Likes, input.CommentsCount, input.Shares, collectedAt); err != nil { + if _, err := tx.ExecContext(ctx, `UPDATE creator_work SET likes=$2, comments_count=$3, shares=$4, collect_count=$5, latest_metrics_at=$6, next_metric_at=NULL, metric_stop_reason='published_at_pending_verification', updated_at=now() WHERE id=$1`, input.WorkID, input.Likes, input.CommentsCount, input.Shares, input.CollectCount, collectedAt); err != nil { return MetricPoint{}, databaseError(err) } if err := tx.Commit(); err != nil { @@ -189,7 +189,7 @@ func (s *Store) recordMetricWithPlan(ctx context.Context, input MetricInput, set } if !latestAt.Valid { nextAt, nextReason := NextMetricAt(published, now, time.Duration(settings.MetricInitialIntervalSeconds)*time.Second, time.Duration(settings.MetricMaxIntervalSeconds)*time.Second, settings.MetricMultiplier, time.Duration(settings.MetricAgeSeconds)*time.Second) - if _, err := tx.ExecContext(ctx, `INSERT INTO creator_work_metric (work_id,collected_at,likes,comments_count,shares) VALUES ($1,$2,$3,$4,$5) ON CONFLICT (work_id,collected_at) DO UPDATE SET likes=EXCLUDED.likes,comments_count=EXCLUDED.comments_count,shares=EXCLUDED.shares`, input.WorkID, collectedAt, input.Likes, input.CommentsCount, input.Shares); err != nil { + if _, err := tx.ExecContext(ctx, `INSERT INTO creator_work_metric (work_id,collected_at,likes,comments_count,shares,collect_count) VALUES ($1,$2,$3,$4,$5,$6) ON CONFLICT (work_id,collected_at) DO UPDATE SET likes=EXCLUDED.likes,comments_count=EXCLUDED.comments_count,shares=EXCLUDED.shares,collect_count=EXCLUDED.collect_count`, input.WorkID, collectedAt, input.Likes, input.CommentsCount, input.Shares, input.CollectCount); err != nil { return MetricPoint{}, databaseError(err) } nextInterval := time.Duration(settings.MetricMaxIntervalSeconds) * time.Second @@ -210,7 +210,7 @@ func (s *Store) recordMetricWithPlan(ctx context.Context, input MetricInput, set } else { nextReason = "" } - if _, err := tx.ExecContext(ctx, `UPDATE creator_work SET likes=$2,comments_count=$3,shares=$4,latest_metrics_at=$5,next_metric_at=$6,metric_stop_reason=$7,updated_at=now() WHERE id=$1`, input.WorkID, input.Likes, input.CommentsCount, input.Shares, collectedAt, nullableArg(nextAt), nextReason); err != nil { + if _, err := tx.ExecContext(ctx, `UPDATE creator_work SET likes=$2,comments_count=$3,shares=$4,collect_count=$5,latest_metrics_at=$6,next_metric_at=$7,metric_stop_reason=$8,updated_at=now() WHERE id=$1`, input.WorkID, input.Likes, input.CommentsCount, input.Shares, input.CollectCount, collectedAt, nullableArg(nextAt), nextReason); err != nil { return MetricPoint{}, databaseError(err) } if err := tx.Commit(); err != nil { @@ -243,13 +243,13 @@ func (s *Store) recordMetricWithPlan(ctx context.Context, input MetricInput, set } else { stopReason = "" } - if _, err := tx.ExecContext(ctx, `INSERT INTO creator_work_metric (work_id,collected_at,likes,comments_count,shares) VALUES ($1,$2,$3,$4,$5) ON CONFLICT (work_id,collected_at) DO UPDATE SET likes=EXCLUDED.likes,comments_count=EXCLUDED.comments_count,shares=EXCLUDED.shares`, input.WorkID, collectedAt, input.Likes, input.CommentsCount, input.Shares); err != nil { + if _, err := tx.ExecContext(ctx, `INSERT INTO creator_work_metric (work_id,collected_at,likes,comments_count,shares,collect_count) VALUES ($1,$2,$3,$4,$5,$6) ON CONFLICT (work_id,collected_at) DO UPDATE SET likes=EXCLUDED.likes,comments_count=EXCLUDED.comments_count,shares=EXCLUDED.shares,collect_count=EXCLUDED.collect_count`, input.WorkID, collectedAt, input.Likes, input.CommentsCount, input.Shares, input.CollectCount); err != nil { return MetricPoint{}, databaseError(err) } if _, err := tx.ExecContext(ctx, `UPDATE creator_metric_plan SET next_plan_at=$2, interval_seconds=$3, point_index=point_index+$4, stopped=$5, stop_reason=$6, updated_at=now() WHERE work_id=$1`, input.WorkID, nullableArg(nextAt), nextInterval, steps, stopped, stopReason); err != nil { return MetricPoint{}, databaseError(err) } - if _, err := tx.ExecContext(ctx, `UPDATE creator_work SET likes=$2,comments_count=$3,shares=$4,latest_metrics_at=$5,next_metric_at=$6,metric_stop_reason=$7,updated_at=now() WHERE id=$1`, input.WorkID, input.Likes, input.CommentsCount, input.Shares, collectedAt, nullableArg(nextAt), stopReason); err != nil { + if _, err := tx.ExecContext(ctx, `UPDATE creator_work SET likes=$2,comments_count=$3,shares=$4,collect_count=$5,latest_metrics_at=$6,next_metric_at=$7,metric_stop_reason=$8,updated_at=now() WHERE id=$1`, input.WorkID, input.Likes, input.CommentsCount, input.Shares, input.CollectCount, collectedAt, nullableArg(nextAt), stopReason); err != nil { return MetricPoint{}, databaseError(err) } if err := tx.Commit(); err != nil { diff --git a/internal/creator/migrations/039_work_collect_count.sql b/internal/creator/migrations/039_work_collect_count.sql new file mode 100644 index 0000000..5dcab4b --- /dev/null +++ b/internal/creator/migrations/039_work_collect_count.sql @@ -0,0 +1,4 @@ +-- 作品指标补采收藏数(collect_count):抖音 works/detail 接口 statistics 均携带, +-- 此前解析与存储均未覆盖,导致增长曲线缺收藏维度。 +ALTER TABLE creator_work ADD COLUMN collect_count bigint; +ALTER TABLE creator_work_metric ADD COLUMN collect_count bigint; diff --git a/internal/creator/models.go b/internal/creator/models.go index 3791ba8..164053f 100644 --- a/internal/creator/models.go +++ b/internal/creator/models.go @@ -192,6 +192,9 @@ type Work struct { Likes *int64 `json:"likes"` CommentsCount *int64 `json:"comments_count"` Shares *int64 `json:"shares"` + CollectCount *int64 `json:"collect_count"` + LikesGrowth *int64 `json:"likes_growth,omitempty"` + LikesGrowthCoverage string `json:"likes_growth_coverage,omitempty"` // full=窗口基线完整 partial=以首点近似 LatestMetricsAt *time.Time `json:"latest_metrics_at,omitempty"` NextMetricAt *time.Time `json:"next_metric_at,omitempty"` MetricStopReason string `json:"metric_stop_reason,omitempty"` @@ -216,6 +219,7 @@ type WorkInput struct { Likes *int64 `json:"likes"` CommentsCount *int64 `json:"comments_count"` Shares *int64 `json:"shares"` + CollectCount *int64 `json:"collect_count"` RawPayload string `json:"-"` } @@ -229,6 +233,7 @@ type WorkFilter struct { MinLikes *int64 MinComments *int64 MinShares *int64 + Growth WorkGrowthFilter // 点赞增量筛选(爆品),空窗口不启用 } type MetricInput struct { @@ -237,6 +242,7 @@ type MetricInput struct { Likes *int64 CommentsCount *int64 Shares *int64 + CollectCount *int64 } type MetricPoint struct { @@ -244,6 +250,7 @@ type MetricPoint struct { Likes *int64 `json:"likes"` CommentsCount *int64 `json:"comments_count"` Shares *int64 `json:"shares"` + CollectCount *int64 `json:"collect_count"` } type MaterialJob struct { diff --git a/internal/creator/store.go b/internal/creator/store.go index 8600251..2875e0a 100644 --- a/internal/creator/store.go +++ b/internal/creator/store.go @@ -86,6 +86,9 @@ var migration037 string //go:embed migrations/038_douyin_only_competitor_profile.sql var migration038 string +//go:embed migrations/039_work_collect_count.sql +var migration039 string + type SecretReference struct { ID string Provider string @@ -185,6 +188,7 @@ func (s *Store) migrate(ctx context.Context) error { {version: 36, sql: migration036}, {version: 37, sql: migration037}, {version: 38, sql: migration038}, + {version: 39, sql: migration039}, } for _, migration := range migrations { var applied bool diff --git a/internal/platform/douyin/connector.go b/internal/platform/douyin/connector.go index 0308be7..b0e2899 100644 --- a/internal/platform/douyin/connector.go +++ b/internal/platform/douyin/connector.go @@ -89,6 +89,7 @@ type Work struct { DiggCount int64 `json:"digg_count"` CommentCount int64 `json:"comment_count"` ShareCount int64 `json:"share_count"` + CollectCount int64 `json:"collect_count"` PlayCount int64 `json:"play_count"` } @@ -355,6 +356,7 @@ type worksEnvelope struct { DiggCount *int64 `json:"digg_count"` CommentCount *int64 `json:"comment_count"` ShareCount *int64 `json:"share_count"` + CollectCount *int64 `json:"collect_count"` PlayCount *int64 `json:"play_count"` } `json:"statistics"` } `json:"aweme_list"` diff --git a/internal/platform/douyin/creator_collector.go b/internal/platform/douyin/creator_collector.go index ea20842..79d38f5 100644 --- a/internal/platform/douyin/creator_collector.go +++ b/internal/platform/douyin/creator_collector.go @@ -34,6 +34,54 @@ type TargetProfile struct { AvatarURL string } +// WorkStatistics 是作品详情接口返回的指标快照,用于增长曲线时序采集。 +type WorkStatistics struct { + Likes *int64 + CommentsCount *int64 + Shares *int64 + CollectCount *int64 +} + +// FetchWorkStatistics 经网关受限 fetch 拉取作品详情并解析指标快照。 +// 作品指标是公开数据,匿名浏览器即可访问(实测 detail 返回完整 statistics)。 +func FetchWorkStatistics(ctx context.Context, browser Browser, workKey string) (WorkStatistics, error) { + if browser == nil || !workKeyPattern.MatchString(workKey) { + return WorkStatistics{}, fmt.Errorf("%w: invalid work statistics request", ErrInvalid) + } + query := douyinAPIQuery() + query.Set("aweme_id", workKey) + response, err := browser.Get(ctx, workDetailEndpoint+"?"+query.Encode()) + if err != nil { + return WorkStatistics{}, err + } + if err := creatorResponseError(response, "work detail"); err != nil { + return WorkStatistics{}, err + } + stats, ok := parseWorkStatistics(response.Body, workKey) + if !ok { + return WorkStatistics{}, fmt.Errorf("%w: invalid douyin work detail response (status=%d body_len=%d head=%.160s)", ErrInvalid, response.Status, len(response.Body), response.Body) + } + return stats, nil +} + +func parseWorkStatistics(body []byte, expectedWorkKey string) (WorkStatistics, bool) { + var envelope workDetailEnvelope + if len(body) > 2<<20 || json.Unmarshal(body, &envelope) != nil || envelope.StatusCode == nil || *envelope.StatusCode != 0 || + envelope.AwemeDetail == nil || envelope.AwemeDetail.AwemeID != expectedWorkKey || envelope.AwemeDetail.Statistics == nil { + return WorkStatistics{}, false + } + stats := envelope.AwemeDetail.Statistics + for _, value := range []*int64{stats.DiggCount, stats.CommentCount, stats.ShareCount, stats.CollectCount} { + if value != nil && *value < 0 { + return WorkStatistics{}, false + } + } + if stats.DiggCount == nil && stats.CommentCount == nil && stats.ShareCount == nil && stats.CollectCount == nil { + return WorkStatistics{}, false + } + return WorkStatistics{Likes: stats.DiggCount, CommentsCount: stats.CommentCount, Shares: stats.ShareCount, CollectCount: stats.CollectCount}, true +} + type workDetailEnvelope struct { StatusCode *int `json:"status_code"` AwemeDetail *struct { @@ -47,6 +95,12 @@ type workDetailEnvelope struct { URLList []string `json:"url_list"` } `json:"avatar_thumb"` } `json:"author"` + Statistics *struct { + DiggCount *int64 `json:"digg_count"` + CommentCount *int64 `json:"comment_count"` + ShareCount *int64 `json:"share_count"` + CollectCount *int64 `json:"collect_count"` + } `json:"statistics"` } `json:"aweme_detail"` } @@ -193,7 +247,7 @@ func (c CreatorCollector) ListWorks(ctx context.Context, accountKey, cursor stri value := time.Unix(*work.CreatedAt, 0).UTC() published = &value } - likes, comments, shares := work.DiggCount, work.CommentCount, work.ShareCount + likes, comments, shares, collects := work.DiggCount, work.CommentCount, work.ShareCount, work.CollectCount sourceType, sourceID := c.SourceType, c.SourceID if sourceType == "" { sourceType = creator.SourceCompetitor @@ -207,7 +261,7 @@ func (c CreatorCollector) ListWorks(ctx context.Context, accountKey, cursor stri } else if published != nil { status = "verified" } - items = append(items, creator.WorkInput{Platform: creator.PlatformDouyin, WorkKey: work.ID, SourceType: sourceType, SourceID: sourceID, Body: work.Description, PublishedAt: published, PublishedAtStatus: status, OriginalURL: "https://www.douyin.com/video/" + work.ID, Likes: likes, CommentsCount: comments, Shares: shares}) + items = append(items, creator.WorkInput{Platform: creator.PlatformDouyin, WorkKey: work.ID, SourceType: sourceType, SourceID: sourceID, Body: work.Description, PublishedAt: published, PublishedAtStatus: status, OriginalURL: "https://www.douyin.com/video/" + work.ID, Likes: likes, CommentsCount: comments, Shares: shares, CollectCount: collects}) } page := creator.WorkPage{Items: items, HasMore: hasMore} if nextCursor != nil { @@ -288,6 +342,7 @@ type creatorWorkPageItem struct { DiggCount *int64 CommentCount *int64 ShareCount *int64 + CollectCount *int64 } func parseCreatorWorksPage(body []byte) ([]creatorWorkPageItem, bool, *int64, bool) { @@ -314,11 +369,11 @@ func parseCreatorWorksPage(body []byte) ([]creatorWorkPageItem, bool, *int64, bo return nil, false, nil, false } seen[item.ID] = struct{}{} - var likes, comments, shares *int64 + var likes, comments, shares, collects *int64 if item.Statistics != nil { - likes, comments, shares = item.Statistics.DiggCount, item.Statistics.CommentCount, item.Statistics.ShareCount + likes, comments, shares, collects = item.Statistics.DiggCount, item.Statistics.CommentCount, item.Statistics.ShareCount, item.Statistics.CollectCount } - for _, value := range []*int64{likes, comments, shares} { + for _, value := range []*int64{likes, comments, shares, collects} { if value != nil && *value < 0 { return nil, false, nil, false } @@ -328,7 +383,7 @@ func parseCreatorWorksPage(body []byte) ([]creatorWorkPageItem, bool, *int64, bo if createdAtInvalid { createdAt = nil } - items = append(items, creatorWorkPageItem{ID: item.ID, Description: item.Description, CreatedAt: createdAt, CreatedAtInvalid: createdAtInvalid, DiggCount: likes, CommentCount: comments, ShareCount: shares}) + items = append(items, creatorWorkPageItem{ID: item.ID, Description: item.Description, CreatedAt: createdAt, CreatedAtInvalid: createdAtInvalid, DiggCount: likes, CommentCount: comments, ShareCount: shares, CollectCount: collects}) } // 仅当响应带有效 max_cursor 时才声明翻页;无 cursor 的 has_more 不产生翻页游标。 hasMore := envelope.HasMore != nil && bool(*envelope.HasMore) && envelope.MaxCursor != nil diff --git a/internal/platform/douyin/creator_collector_test.go b/internal/platform/douyin/creator_collector_test.go index 5b86afd..215685d 100644 --- a/internal/platform/douyin/creator_collector_test.go +++ b/internal/platform/douyin/creator_collector_test.go @@ -137,11 +137,41 @@ func TestParseCreatorWorksPageAcceptsNumericHasMore(t *testing.T) { func TestParseCreatorWorksPageToleratesHasMoreWithoutCursor(t *testing.T) { // 匿名 works 响应实测:has_more=1 且不带 max_cursor 字段;第一页必须可用。 - body := []byte(`{"status_code":0,"has_more":1,"aweme_list":[{"aweme_id":"123","desc":"first page","create_time":1700000000,"statistics":{"digg_count":1,"comment_count":2,"share_count":3}}]}`) + body := []byte(`{"status_code":0,"has_more":1,"aweme_list":[{"aweme_id":"123","desc":"first page","create_time":1700000000,"statistics":{"digg_count":1,"comment_count":2,"share_count":3,"collect_count":4}}]}`) works, hasMore, cursor, ok := parseCreatorWorksPage(body) if !ok || hasMore || cursor != nil || len(works) != 1 || works[0].ID != "123" { t.Fatalf("has_more without cursor must still yield first page: ok=%v hasMore=%v cursor=%v works=%+v", ok, hasMore, cursor, works) } + if works[0].CollectCount == nil || *works[0].CollectCount != 4 { + t.Fatalf("collect_count not parsed: %+v", works[0]) + } +} + +func TestParseWorkStatisticsExtractsMetrics(t *testing.T) { + // 匿名 detail 响应实测:statistics 携带完整指标快照。 + body := []byte(`{"status_code":0,"aweme_detail":{"aweme_id":"123","statistics":{"digg_count":6155,"comment_count":235,"share_count":578,"collect_count":2292}}}`) + stats, ok := parseWorkStatistics(body, "123") + if !ok || stats.Likes == nil || *stats.Likes != 6155 || stats.CommentsCount == nil || *stats.CommentsCount != 235 || stats.Shares == nil || *stats.Shares != 578 || stats.CollectCount == nil || *stats.CollectCount != 2292 { + t.Fatalf("unexpected statistics: ok=%v %+v", ok, stats) + } + if _, ok := parseWorkStatistics(body, "999"); ok { + t.Fatal("accepted statistics for another work") + } + if _, ok := parseWorkStatistics([]byte(`{"status_code":0,"aweme_detail":{"aweme_id":"123"}}`), "123"); ok { + t.Fatal("accepted missing statistics") + } +} + +func TestFetchWorkStatisticsUsesDetailEndpoint(t *testing.T) { + browser := &collectorBrowser{response: Response{Status: 200, Body: []byte(`{"status_code":0,"aweme_detail":{"aweme_id":"123","statistics":{"digg_count":10}}}`)}} + stats, err := FetchWorkStatistics(context.Background(), browser, "123") + if err != nil || stats.Likes == nil || *stats.Likes != 10 { + t.Fatalf("fetch statistics: %+v err=%v", stats, err) + } + parsed, err := url.Parse(browser.url) + if err != nil || parsed.Path != "/aweme/v1/web/aweme/detail/" || parsed.Query().Get("aweme_id") != "123" { + t.Fatalf("unexpected request url: %s", browser.url) + } } func TestParseCreatorWorksPageTreatsBareEnvelopeAsEmptyPage(t *testing.T) { diff --git a/web/package-lock.json b/web/package-lock.json index 1524bd6..36f7e49 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "creator-hub-web", "dependencies": { + "@ant-design/charts": "^2.6.7", "@ant-design/icons": "^6.3.4", "@ant-design/pro-components": "^3.1.14-7", "@umijs/max": "^4.7.19", @@ -50,6 +51,34 @@ "integrity": "sha512-0vr5GCwM7xlAl6NxG1lPbABO+SYioNJL3HVy2FA8wTlsIMoZvQwcwsxTw6eLQCiN9V2UQ8kBtfz8DW8utVVE5w==", "license": "ISC" }, + "node_modules/@ant-design/charts": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/@ant-design/charts/-/charts-2.6.7.tgz", + "integrity": "sha512-XfmsnspUpfrMlRFGTwmHJ2TPKcosq5a5nSxAfIOpEXAvmJBT2N16oejGTZhUFTzba8W3XtBOziwRAXmDmLUqvA==", + "license": "MIT", + "dependencies": { + "@ant-design/graphs": "^2.1.1", + "@ant-design/plots": "^2.6.7", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": ">=16.8.4", + "react-dom": ">=16.8.4" + } + }, + "node_modules/@ant-design/charts-util": { + "version": "0.0.1-alpha.7", + "resolved": "https://registry.npmjs.org/@ant-design/charts-util/-/charts-util-0.0.1-alpha.7.tgz", + "integrity": "sha512-Yh0o6EdO6SvdSnStFZMbnUzjyymkVzV+TQ9ymVW9hlVgO/fUkUII3JYSdV+UVcFnYwUF0YiDKuSTLCZNAzg2bQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": ">=16.8.4", + "react-dom": ">=16.8.4" + } + }, "node_modules/@ant-design/colors": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.1.tgz", @@ -102,6 +131,24 @@ "node": ">=8.x" } }, + "node_modules/@ant-design/graphs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@ant-design/graphs/-/graphs-2.1.1.tgz", + "integrity": "sha512-qT3Oo8BWeoAmZEy9gfR6uIk+rczbNJ3sWXKonoOD5koATWv7dY0kgvS1JnhdM1QW4FkfPPJTeQVSlRRUtvWDwA==", + "license": "MIT", + "dependencies": { + "@ant-design/charts-util": "0.0.1-alpha.7", + "@antv/g6": "^5.0.44", + "@antv/g6-extension-react": "^0.2.0", + "@antv/graphin": "^3.0.4", + "lodash": "^4.17.21", + "styled-components": "^6.1.15" + }, + "peerDependencies": { + "react": ">=16.8.4", + "react-dom": ">=16.8.4" + } + }, "node_modules/@ant-design/icons": { "version": "6.3.4", "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.3.4.tgz", @@ -133,6 +180,37 @@ "integrity": "sha512-MLm1FUpg02fP615ShQnCUN9la2E4RylDxKyolkGqAWTIHO4HyGM0A5x71AMALEyP/bC+UEEWBGSQ+D4/8hQ+ww==", "license": "MIT" }, + "node_modules/@ant-design/plots": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/@ant-design/plots/-/plots-2.6.8.tgz", + "integrity": "sha512-QsunUs2d5rbq/1BwVhga/siA5H50OaG23YopMYwPD4sPsza6NQzPQ8FM3elNIsD/BIk298tihqX1cJ/MmvVJbQ==", + "license": "MIT", + "dependencies": { + "@ant-design/charts-util": "0.0.3", + "@antv/event-emitter": "^0.1.3", + "@antv/g": "^6.1.7", + "@antv/g2": "^5.2.7", + "@antv/g2-extension-plot": "^0.2.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": ">=16.8.4", + "react-dom": ">=16.8.4" + } + }, + "node_modules/@ant-design/plots/node_modules/@ant-design/charts-util": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@ant-design/charts-util/-/charts-util-0.0.3.tgz", + "integrity": "sha512-x1H7UT6t4dXAyGRoHqlOnEsEqBSTANFGTZEAMI0CWYhYUpp13n0o9grl9oPtoL6FEQMjUBTY+zGJKlHkz8smMw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": ">=16.8.4", + "react-dom": ">=16.8.4" + } + }, "node_modules/@ant-design/pro-components": { "version": "3.1.14-7", "resolved": "https://registry.npmjs.org/@ant-design/pro-components/-/pro-components-3.1.14-7.tgz", @@ -209,6 +287,366 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@antv/algorithm": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@antv/algorithm/-/algorithm-0.1.26.tgz", + "integrity": "sha512-DVhcFSQ8YQnMNW34Mk8BSsfc61iC1sAnmcfYoXTAshYHuU50p/6b7x3QYaGctDNKWGvi1ub7mPcSY0bK+aN0qg==", + "license": "MIT", + "dependencies": { + "@antv/util": "^2.0.13", + "tslib": "^2.0.0" + } + }, + "node_modules/@antv/algorithm/node_modules/@antv/util": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@antv/util/-/util-2.0.17.tgz", + "integrity": "sha512-o6I9hi5CIUvLGDhth0RxNSFDRwXeywmt6ExR4+RmVAzIi48ps6HUy+svxOCayvrPBN37uE6TAc2KDofRo0nK9Q==", + "license": "ISC", + "dependencies": { + "csstype": "^3.0.8", + "tslib": "^2.0.3" + } + }, + "node_modules/@antv/component": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@antv/component/-/component-2.1.12.tgz", + "integrity": "sha512-AFyo6uoM5LeRD5CNtJqkUSKvASl2Zp9DoVM3uYUZyi5z6gqgX7+oQYTxA9EGvHIQmzttL7b/WaiZFPz/+BsGpQ==", + "license": "MIT", + "dependencies": { + "@antv/g": "^6.1.11", + "@antv/scale": "^0.4.16", + "@antv/util": "^3.3.10", + "svg-path-parser": "^1.1.0" + } + }, + "node_modules/@antv/component/node_modules/@antv/scale": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@antv/scale/-/scale-0.4.16.tgz", + "integrity": "sha512-5wg/zB5kXHxpTV5OYwJD3ja6R8yTiqIOkjOhmpEJiowkzRlbEC/BOyMvNUq5fqFIHnMCE9woO7+c3zxEQCKPjw==", + "license": "MIT", + "dependencies": { + "@antv/util": "^3.3.7", + "color-string": "^1.5.5", + "fecha": "^4.2.1" + } + }, + "node_modules/@antv/coord": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@antv/coord/-/coord-0.4.7.tgz", + "integrity": "sha512-UTbrMLhwJUkKzqJx5KFnSRpU3BqrdLORJbwUbHK2zHSCT3q3bjcFA//ZYLVfIlwqFDXp/hzfMyRtp0c77A9ZVA==", + "license": "MIT", + "dependencies": { + "@antv/scale": "^0.4.12", + "@antv/util": "^2.0.13", + "gl-matrix": "^3.4.3" + } + }, + "node_modules/@antv/coord/node_modules/@antv/scale": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@antv/scale/-/scale-0.4.16.tgz", + "integrity": "sha512-5wg/zB5kXHxpTV5OYwJD3ja6R8yTiqIOkjOhmpEJiowkzRlbEC/BOyMvNUq5fqFIHnMCE9woO7+c3zxEQCKPjw==", + "license": "MIT", + "dependencies": { + "@antv/util": "^3.3.7", + "color-string": "^1.5.5", + "fecha": "^4.2.1" + } + }, + "node_modules/@antv/coord/node_modules/@antv/scale/node_modules/@antv/util": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@antv/util/-/util-3.3.11.tgz", + "integrity": "sha512-FII08DFM4ABh2q5rPYdr0hMtKXRgeZazvXaFYCs7J7uTcWDHUhczab2qOCJLNDugoj8jFag1djb7wS9ehaRYBg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "gl-matrix": "^3.3.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@antv/coord/node_modules/@antv/util": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@antv/util/-/util-2.0.17.tgz", + "integrity": "sha512-o6I9hi5CIUvLGDhth0RxNSFDRwXeywmt6ExR4+RmVAzIi48ps6HUy+svxOCayvrPBN37uE6TAc2KDofRo0nK9Q==", + "license": "ISC", + "dependencies": { + "csstype": "^3.0.8", + "tslib": "^2.0.3" + } + }, + "node_modules/@antv/event-emitter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@antv/event-emitter/-/event-emitter-0.1.3.tgz", + "integrity": "sha512-4ddpsiHN9Pd4UIlWuKVK1C4IiZIdbwQvy9i7DUSI3xNJ89FPUFt8lxDYj8GzzfdllV0NkJTRxnG+FvLk0llidg==", + "license": "MIT" + }, + "node_modules/@antv/expr": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@antv/expr/-/expr-1.0.2.tgz", + "integrity": "sha512-vrfdmPHkTuiS5voVutKl2l06w1ihBh9A8SFdQPEE+2KMVpkymzGOF1eWpfkbGZ7tiFE15GodVdhhHomD/hdIwg==", + "license": "MIT" + }, + "node_modules/@antv/g": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@antv/g/-/g-6.3.1.tgz", + "integrity": "sha512-WYEKqy86LHB2PzTmrZXrIsIe+3Epeds2f68zceQ+BJtRoGki7Sy4IhlC8LrUMztgfT1t3d/0L745NWZwITroKA==", + "license": "MIT", + "dependencies": { + "@antv/g-lite": "2.7.0", + "@antv/util": "^3.3.5", + "@babel/runtime": "^7.25.6", + "gl-matrix": "^3.4.3", + "html2canvas": "^1.4.1" + } + }, + "node_modules/@antv/g-canvas": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@antv/g-canvas/-/g-canvas-2.2.0.tgz", + "integrity": "sha512-h7zVBBo2aO64DuGKvq9sG+yTU3sCUb9DALCVm7nz8qGPs8hhLuFOkKPEzUDNfNYZGJUGzY8UDtJ3QRGRFcvEQg==", + "license": "MIT", + "dependencies": { + "@antv/g-lite": "2.7.0", + "@antv/g-math": "3.1.0", + "@antv/util": "^3.3.5", + "@babel/runtime": "^7.25.6", + "gl-matrix": "^3.4.3", + "tslib": "^2.5.3" + } + }, + "node_modules/@antv/g-lite": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@antv/g-lite/-/g-lite-2.7.0.tgz", + "integrity": "sha512-uSzgHYa5bwR5L2Au7/5tsOhFmXKZKLPBH90+Q9bP9teVs5VT4kOAi0isPSpDI8uhdDC2/VrfTWu5K9HhWI6FWw==", + "license": "MIT", + "dependencies": { + "@antv/g-math": "3.1.0", + "@antv/util": "^3.3.5", + "@antv/vendor": "^1.0.3", + "@babel/runtime": "^7.25.6", + "eventemitter3": "^5.0.1", + "gl-matrix": "^3.4.3", + "tslib": "^2.5.3" + } + }, + "node_modules/@antv/g-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@antv/g-math/-/g-math-3.1.0.tgz", + "integrity": "sha512-DtN1Gj/yI0UiK18nSBsZX8RK0LszGwqfb+cBYWgE+ddyTm8dZnW4tPUhV7QXePsS6/A5hHC+JFpAAK7OEGo5ZQ==", + "license": "MIT", + "dependencies": { + "@antv/util": "^3.3.5", + "@babel/runtime": "^7.25.6", + "gl-matrix": "^3.4.3", + "tslib": "^2.5.3" + } + }, + "node_modules/@antv/g-plugin-dragndrop": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@antv/g-plugin-dragndrop/-/g-plugin-dragndrop-2.1.1.tgz", + "integrity": "sha512-+aesDUJVQDs6UJ2bOBbDlaGAPCfHmU0MbrMTlQlfpwNplWueqtgVAZ3L57oZ2ZGHRWUHiRwZGPjXMBM3O2LELw==", + "license": "MIT", + "dependencies": { + "@antv/g-lite": "2.7.0", + "@antv/util": "^3.3.5", + "@babel/runtime": "^7.25.6", + "tslib": "^2.5.3" + } + }, + "node_modules/@antv/g-svg": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@antv/g-svg/-/g-svg-2.1.1.tgz", + "integrity": "sha512-gVzBkjqA8FzDTbkuIxj6L0Omz/X/hFbYLzK6alWr0sHTfywqP6czcjDUJU8DF2MRIY1Twy55uZYW4dqqLXOXXg==", + "license": "MIT", + "dependencies": { + "@antv/g-lite": "2.7.0", + "@antv/util": "^3.3.5", + "@babel/runtime": "^7.25.6", + "gl-matrix": "^3.4.3", + "tslib": "^2.5.3" + } + }, + "node_modules/@antv/g2": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/@antv/g2/-/g2-5.4.8.tgz", + "integrity": "sha512-IvgIpwmT4M5/QAd3Mn2WiHIDeBqFJ4WA2gcZhRRSZuZ2KmgCqZWZwwIT0hc+kIGxwYeDoCQqf//t6FMVu3ryBg==", + "license": "MIT", + "dependencies": { + "@antv/component": "^2.1.9", + "@antv/coord": "^0.4.7", + "@antv/event-emitter": "^0.1.3", + "@antv/expr": "^1.0.2", + "@antv/g": "^6.1.24", + "@antv/g-canvas": "^2.0.43", + "@antv/g-plugin-dragndrop": "^2.0.35", + "@antv/scale": "^0.5.1", + "@antv/util": "^3.3.10", + "@antv/vendor": "^1.0.11", + "flru": "^1.0.2", + "pdfast": "^0.2.0" + } + }, + "node_modules/@antv/g2-extension-plot": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@antv/g2-extension-plot/-/g2-extension-plot-0.2.2.tgz", + "integrity": "sha512-KJXCXO7as+h0hDqirGXf1omrNuYzQmY3VmBmp7lIvkepbQ7sz3pPwy895r1FWETGF3vTk5UeFcAF5yzzBHWgbw==", + "dependencies": { + "@antv/g2": "^5.1.8", + "@antv/util": "^3.3.5", + "@antv/vendor": "^1.0.10" + } + }, + "node_modules/@antv/g6": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@antv/g6/-/g6-5.1.1.tgz", + "integrity": "sha512-50bXxMUf4mChyOv4ePVeWZLwotih9VunKfp0a++Wofv/wCyY8fb9+CV2wouIBCOZnd5ydBRA4NNaX9yLJzqa2w==", + "license": "MIT", + "dependencies": { + "@antv/algorithm": "^0.1.26", + "@antv/component": "^2.1.7", + "@antv/event-emitter": "^0.1.3", + "@antv/g": "^6.1.28", + "@antv/g-canvas": "^2.0.48", + "@antv/g-plugin-dragndrop": "^2.0.38", + "@antv/graphlib": "^2.0.4", + "@antv/hierarchy": "^0.7.1", + "@antv/layout": "^2.0.0", + "@antv/util": "^3.3.11", + "bubblesets-js": "^2.3.4" + } + }, + "node_modules/@antv/g6-extension-react": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@antv/g6-extension-react/-/g6-extension-react-0.2.7.tgz", + "integrity": "sha512-X/zxGiL/kyJ+5xteX1+P2mI07oLw+zfvKcIHxfynL7IGCQCwQ6q91LkJaOlSDTuWhNRXwnwJ4Cf2Nt/9Dhq5Dg==", + "license": "MIT", + "dependencies": { + "@antv/g": "^6.1.24", + "@antv/g-svg": "^2.0.38" + }, + "peerDependencies": { + "@antv/g6": "^5.1.0", + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@antv/graphin": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@antv/graphin/-/graphin-3.0.5.tgz", + "integrity": "sha512-V/j8R8Ty44wUqxVIYLdpPuIO8WWCTIVq1eBJg5YRunL5t5o5qAFpC/qkQxslbBMWyKdIH0oWBnvHA74riGi7cw==", + "license": "MIT", + "dependencies": { + "@antv/g6": "^5.0.28" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.1.0", + "react-dom": "^18.0.0 || ^19.1.0" + } + }, + "node_modules/@antv/graphlib": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@antv/graphlib/-/graphlib-2.0.4.tgz", + "integrity": "sha512-zc/5oQlsdk42Z0ib1mGklwzhJ5vczLFiPa1v7DgJkTbgJ2YxRh9xdarf86zI49sKVJmgbweRpJs7Nu5bIiwv4w==", + "license": "MIT", + "dependencies": { + "@antv/event-emitter": "^0.1.3" + } + }, + "node_modules/@antv/hierarchy": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@antv/hierarchy/-/hierarchy-0.7.1.tgz", + "integrity": "sha512-7r22r+HxfcRZp79ZjGmsn97zgC1Iajrv0Mm9DIgx3lPfk+Kme2MG/+EKdZj1iEBsN0rJRzjWVPGL5YrBdVHchw==", + "license": "MIT" + }, + "node_modules/@antv/layout": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@antv/layout/-/layout-2.0.0.tgz", + "integrity": "sha512-aCZ3UdNc40SfT7meFV7QTADY2HCnc0DShVw56CJNTI6oExUIVU736grPuL5Dhb8/JrVaU4Y83QPN/P7KafBzlw==", + "license": "MIT", + "dependencies": { + "@antv/event-emitter": "^0.1.3", + "@antv/expr": "^1.0.2", + "@antv/graphlib": "^2.0.0", + "@antv/util": "^3.3.2", + "comlink": "^4.4.1", + "d3-force": "^3.0.0", + "d3-force-3d": "^3.0.5", + "d3-octree": "^1.0.2", + "d3-quadtree": "^3.0.1", + "dagre": "^0.8.5", + "ml-matrix": "^6.10.4", + "tslib": "^2.8.1" + } + }, + "node_modules/@antv/scale": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@antv/scale/-/scale-0.5.2.tgz", + "integrity": "sha512-rTHRAwvpHWC5PGZF/mJ2ZuTDqwwvVBDRph0Uu5PV9BXwzV7K8+9lsqGJ+XHVLxe8c6bKog5nlzvV/dcYb0d5Ow==", + "license": "MIT", + "dependencies": { + "@antv/util": "^3.3.7", + "color-string": "^1.5.5", + "fecha": "^4.2.1" + } + }, + "node_modules/@antv/util": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@antv/util/-/util-3.3.11.tgz", + "integrity": "sha512-FII08DFM4ABh2q5rPYdr0hMtKXRgeZazvXaFYCs7J7uTcWDHUhczab2qOCJLNDugoj8jFag1djb7wS9ehaRYBg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "gl-matrix": "^3.3.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@antv/vendor": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@antv/vendor/-/vendor-1.0.11.tgz", + "integrity": "sha512-LmhPEQ+aapk3barntaiIxJ5VHno/Tyab2JnfdcPzp5xONh/8VSfed4bo/9xKo5HcUAEydko38vYLfj6lJliLiw==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.2.1", + "@types/d3-color": "^3.1.3", + "@types/d3-dispatch": "^3.0.6", + "@types/d3-dsv": "^3.0.7", + "@types/d3-ease": "^3.0.2", + "@types/d3-fetch": "^3.0.7", + "@types/d3-force": "^3.0.10", + "@types/d3-format": "^3.0.4", + "@types/d3-geo": "^3.1.0", + "@types/d3-hierarchy": "^3.1.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-path": "^3.1.0", + "@types/d3-quadtree": "^3.0.6", + "@types/d3-random": "^3.0.3", + "@types/d3-scale": "^4.0.9", + "@types/d3-scale-chromatic": "^3.1.0", + "@types/d3-shape": "^3.1.7", + "@types/d3-time": "^3.0.4", + "@types/d3-timer": "^3.0.2", + "d3-array": "^3.2.4", + "d3-color": "^3.1.0", + "d3-dispatch": "^3.0.1", + "d3-dsv": "^3.0.1", + "d3-ease": "^3.0.1", + "d3-fetch": "^3.0.1", + "d3-force": "^3.0.0", + "d3-force-3d": "^3.0.5", + "d3-format": "^3.1.0", + "d3-geo": "^3.1.1", + "d3-geo-projection": "^4.0.0", + "d3-hierarchy": "^3.1.2", + "d3-interpolate": "^3.0.1", + "d3-path": "^3.1.0", + "d3-quadtree": "^3.0.1", + "d3-random": "^3.0.1", + "d3-regression": "^1.3.10", + "d3-scale": "^4.0.2", + "d3-scale-chromatic": "^3.1.0", + "d3-shape": "^3.2.0", + "d3-time": "^3.1.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -3108,319 +3546,6 @@ "node": ">= 8" } }, - "node_modules/@parcel/watcher": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", - "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "detect-libc": "^2.0.3", - "is-glob": "^4.0.3", - "node-addon-api": "^7.0.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.6.0", - "@parcel/watcher-darwin-arm64": "2.6.0", - "@parcel/watcher-darwin-x64": "2.6.0", - "@parcel/watcher-freebsd-x64": "2.6.0", - "@parcel/watcher-linux-arm-glibc": "2.6.0", - "@parcel/watcher-linux-arm-musl": "2.6.0", - "@parcel/watcher-linux-arm64-glibc": "2.6.0", - "@parcel/watcher-linux-arm64-musl": "2.6.0", - "@parcel/watcher-linux-x64-glibc": "2.6.0", - "@parcel/watcher-linux-x64-musl": "2.6.0", - "@parcel/watcher-win32-arm64": "2.6.0", - "@parcel/watcher-win32-x64": "2.6.0" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", - "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", - "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", - "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", - "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", - "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", - "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", - "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", - "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", - "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", - "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", - "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", - "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher/node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@parcel/watcher/node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -5426,12 +5551,147 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -5542,14 +5802,6 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@types/react": { "version": "19.3.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", @@ -6073,23 +6325,6 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/@umijs/bundler-vite/node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@umijs/bundler-vite/node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -6107,14 +6342,6 @@ } } }, - "node_modules/@umijs/bundler-vite/node_modules/immutable": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", - "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@umijs/bundler-vite/node_modules/picomatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", @@ -6164,63 +6391,6 @@ "node": ">=0.10.0" } }, - "node_modules/@umijs/bundler-vite/node_modules/readdirp": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", - "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@umijs/bundler-vite/node_modules/sass": { - "version": "1.104.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.104.1.tgz", - "integrity": "sha512-yDA+1aIG3EHgN4V/BvuhCvu61FF6hEd4e+9DxikUm9U0CAGuvdIZ/UYy7qbOxjhbbWQraoyLlMuzdRGOSV5Bmw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "chokidar": "^5.0.0", - "immutable": "^5.1.5", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=20.19.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" - } - }, - "node_modules/@umijs/bundler-vite/node_modules/tsx": { - "version": "4.23.15", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", - "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "esbuild": "~0.28.0" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, "node_modules/@umijs/bundler-vite/node_modules/vite": { "version": "7.3.6", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", @@ -6295,23 +6465,6 @@ } } }, - "node_modules/@umijs/bundler-vite/node_modules/yaml": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", - "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/@umijs/bundler-webpack": { "version": "4.7.19", "resolved": "https://registry.npmjs.org/@umijs/bundler-webpack/-/bundler-webpack-4.7.19.tgz", @@ -7966,29 +8119,6 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@umijs/plugins/node_modules/@types/react": { - "version": "18.3.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", - "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@umijs/plugins/node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "license": "MIT", - "optional": true, - "peer": true, - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, "node_modules/@umijs/plugins/node_modules/antd": { "version": "5.29.3", "resolved": "https://registry.npmjs.org/antd/-/antd-5.29.3.tgz", @@ -10542,6 +10672,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -10817,6 +10956,12 @@ "node-int64": "^0.4.0" } }, + "node_modules/bubblesets-js": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/bubblesets-js/-/bubblesets-js-2.3.4.tgz", + "integrity": "sha512-DyMjHmpkS2+xcFNtyN00apJYL3ESdp9fTrkDr5+9Qg/GPqFmcWgGsK1akZnttE1XFxJ/VMy4DNNGMGYtmFp1Sg==", + "license": "MIT" + }, "node_modules/buffer": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", @@ -11246,6 +11391,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/colord": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/colord/-/colord-2.10.0.tgz", @@ -11264,6 +11419,12 @@ "node": ">= 0.8" } }, + "node_modules/comlink": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz", + "integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==", + "license": "Apache-2.0" + }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -11651,6 +11812,15 @@ "node": ">=4" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-loader": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz", @@ -11824,6 +11994,334 @@ "node": ">=0.12" } }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-binarytree": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", + "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force-3d": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz", + "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==", + "license": "MIT", + "dependencies": { + "d3-binarytree": "1", + "d3-dispatch": "1 - 3", + "d3-octree": "1", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo-projection": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz", + "integrity": "sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==", + "license": "ISC", + "dependencies": { + "commander": "7", + "d3-array": "1 - 3", + "d3-geo": "1.12.0 - 3" + }, + "bin": { + "geo2svg": "bin/geo2svg.js", + "geograticule": "bin/geograticule.js", + "geoproject": "bin/geoproject.js", + "geoquantize": "bin/geoquantize.js", + "geostitch": "bin/geostitch.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo-projection/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-octree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz", + "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==", + "license": "MIT" + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-regression": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/d3-regression/-/d3-regression-1.3.10.tgz", + "integrity": "sha512-PF8GWEL70cHHWpx2jUQXc68r1pyPHIA+St16muk/XRokETzlegj5LriNKg7o4LR0TySug4nHYPJNNRz/W+/Niw==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "license": "MIT", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -13525,6 +14023,12 @@ "es5-ext": "~0.10.14" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -13797,6 +14301,12 @@ "bser": "2.1.1" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -13972,6 +14482,15 @@ "deprecated": "flatten is deprecated in favor of utility frameworks such as lodash.", "license": "MIT" }, + "node_modules/flru": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flru/-/flru-1.0.2.tgz", + "integrity": "sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/follow-redirects": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", @@ -14362,6 +14881,12 @@ "url": "https://github.com/fisker/git-hooks-list?sponsor=1" } }, + "node_modules/gl-matrix": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", + "license": "MIT" + }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -14581,6 +15106,15 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "license": "MIT" }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + } + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -14885,6 +15419,19 @@ "webpack": "^5.20.0" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -15137,6 +15684,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/intl": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/intl/-/intl-1.2.5.tgz", @@ -15187,6 +15743,12 @@ "node": ">= 0.10" } }, + "node_modules/is-any-array": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-any-array/-/is-any-array-3.0.0.tgz", + "integrity": "sha512-o4h+tylWykC4BD1vaejp6gDxoM13bwW8FGuNs4yIKpj8xbBJcRxJx8vZpq0dCr7ZDEfeKjmsi/euolKhX6f/ww==", + "license": "MIT" + }, "node_modules/is-arguments": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", @@ -17246,6 +17808,45 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/ml-array-max": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ml-array-max/-/ml-array-max-2.0.0.tgz", + "integrity": "sha512-QQZ4kENwpWmyNb98UXRDFXrmtIXuXtt1+bSbda/2KA85+F+rrJP8hZk6QOkCQXM2Th9mUDYdq/PNByPdT9ID4A==", + "license": "MIT", + "dependencies": { + "is-any-array": "^3.0.0" + } + }, + "node_modules/ml-array-min": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ml-array-min/-/ml-array-min-2.0.0.tgz", + "integrity": "sha512-GRj6Ky6sW9vGL6yIjgsHmXZ9YgrdmcQ8nCxPqEGeKc6dkfYg1XDYxGFxADUjNuZyoCd5PUscWAS4N+cFaX6hFg==", + "license": "MIT", + "dependencies": { + "is-any-array": "^3.0.0" + } + }, + "node_modules/ml-array-rescale": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ml-array-rescale/-/ml-array-rescale-2.0.0.tgz", + "integrity": "sha512-2GGtKfSno94/kIloWGvpp/U5Q5vLvLrza+SAaGsLeo6Xj4mEbA6Gqx+oTfZFkxnd1grT2X007HfJNs3T5BsiVg==", + "license": "MIT", + "dependencies": { + "is-any-array": "^3.0.0", + "ml-array-max": "^2.0.0", + "ml-array-min": "^2.0.0" + } + }, + "node_modules/ml-matrix": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ml-matrix/-/ml-matrix-6.15.0.tgz", + "integrity": "sha512-wFa1v6KP8bKp+fj0nYmRs1Pb5K4zRkXGKsOvLinvILENFIADncm4XlOI+S1M7yuACMGfI6cfk0IifDgd4j5xmw==", + "license": "MIT", + "dependencies": { + "is-any-array": "^3.0.0", + "ml-array-rescale": "^2.0.0" + } + }, "node_modules/moment": { "version": "2.31.0", "resolved": "https://registry.npmjs.org/moment/-/moment-2.31.0.tgz", @@ -17358,14 +17959,6 @@ "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", "license": "MIT" }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -18050,6 +18643,12 @@ "node": ">= 0.10" } }, + "node_modules/pdfast": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/pdfast/-/pdfast-0.2.0.tgz", + "integrity": "sha512-cq6TTu6qKSFUHwEahi68k/kqN2mfepjkGrG9Un70cgdRRKLKY6Rf8P8uvP2NvZktaQZNF3YE7agEkLj0vGK9bA==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -20904,6 +21503,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/safe-array-concat": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", @@ -21467,6 +22072,21 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/single-spa": { "version": "5.9.5", "resolved": "https://registry.npmjs.org/single-spa/-/single-spa-5.9.5.tgz", @@ -22014,6 +22634,48 @@ "integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==", "license": "ISC" }, + "node_modules/styled-components": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.5.3.tgz", + "integrity": "sha512-vAX79sfpmUerP9fsTTxoTrBDE0RuO4ahjInyWYoohNgqrdg63Ms4q6FJ/o2Fyity82NU3cujOT8Ewl9TThBdwg==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.4.0", + "css-to-react-native": "3.2.0", + "csstype": "3.2.3", + "stylis": "4.3.6" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "css-to-react-native": ">= 3.2.0", + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0", + "react-native": ">= 0.68.0" + }, + "peerDependenciesMeta": { + "css-to-react-native": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/styled-components/node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, "node_modules/styled-jsx": { "version": "5.1.7", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.7.tgz", @@ -22286,6 +22948,12 @@ "node": ">=8" } }, + "node_modules/svg-path-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/svg-path-parser/-/svg-path-parser-1.1.0.tgz", + "integrity": "sha512-jGCUqcQyXpfe38R7RFfhrMyfXcBmpMNJI/B+4CE9/Unkh98UporAc461GTthv+TVDuZXsBx7/WiwJb1Oh4tt4A==", + "license": "MIT" + }, "node_modules/svg-tags": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", @@ -22502,6 +23170,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -23104,6 +23781,15 @@ "node": ">= 0.4.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/v8-compile-cache": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", diff --git a/web/package.json b/web/package.json index e844fc8..93b2fc8 100644 --- a/web/package.json +++ b/web/package.json @@ -7,6 +7,7 @@ "start": "max preview" }, "dependencies": { + "@ant-design/charts": "^2.6.7", "@ant-design/icons": "^6.3.4", "@ant-design/pro-components": "^3.1.14-7", "@umijs/max": "^4.7.19", diff --git a/web/src/pages/accounts/monitoring/$id/index.tsx b/web/src/pages/accounts/monitoring/$id/index.tsx index a52fd3f..b14bb7f 100644 --- a/web/src/pages/accounts/monitoring/$id/index.tsx +++ b/web/src/pages/accounts/monitoring/$id/index.tsx @@ -1,10 +1,11 @@ // 监控账号详情:形态对齐账号详情页(页头槽 + 卡片流)—— // 页头 info 为账号标识(头像 + 昵称 + 同步状态 + 平台),actions 为返回/同步/暂停恢复; -// 主体两张 Card:账号画像(Descriptions)与作品列表(Row/Col card 网格,分页)。 -// 点击作品弹出 Drawer 展示统计信息:作品详情 + 指标采集记录表。 +// 主体两张 Card:账号画像(Descriptions)与作品列表(Row/Col card 网格,分页,支持按点赞增速筛选爆品)。 +// 点击作品弹出 Drawer 展示统计信息:作品详情 + 增长曲线折线图 + 指标采集记录表。 // 作品列表走 GET /creator/works?source_id=,定时采集由后端调度器维护。 -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { history, useParams } from '@umijs/max'; +import { Line } from '@ant-design/charts'; import { Alert, Avatar, @@ -15,6 +16,7 @@ import { Drawer, Empty, Flex, + InputNumber, Row, Space, Spin, @@ -23,7 +25,7 @@ import { Typography, message, } from 'antd'; -import { CommentOutlined, LikeOutlined, ReloadOutlined, ShareAltOutlined } from '@ant-design/icons'; +import { CommentOutlined, FireOutlined, LikeOutlined, ReloadOutlined, ShareAltOutlined } from '@ant-design/icons'; import { creatorAction, creatorGet, getOne, getList } from '@/services/api'; import { conflictMessage, dateTime, platformLabel } from '@/utils/helpers'; import { usePageActions, usePageInfo } from '@/components/PageActions'; @@ -60,6 +62,42 @@ function formatCount(value?: number | null): string { return value >= 10000 ? `${(value / 10000).toFixed(1)}w` : `${value}`; } +const metricSeriesMeta = [ + { key: 'likes', label: '点赞' }, + { key: 'collect_count', label: '收藏' }, + { key: 'comments_count', label: '评论' }, + { key: 'shares', label: '转发' }, +] as const; + +// 增长曲线:把指标时序点展开成长表(时间 × 指标 × 数值)供折线图渲染。 +function GrowthChart({ metrics }: { metrics: any[] }) { + const data = useMemo( + () => + metrics.flatMap((point) => + metricSeriesMeta + .filter((series) => point[series.key] != null) + .map((series) => ({ + time: dateTime(point.collected_at), + metric: series.label, + value: point[series.key], + })), + ), + [metrics], + ); + if (!data.length) return null; + return ( + (v >= 10000 ? `${(v / 10000).toFixed(1)}w` : `${v}`) }} + legend={{ position: 'top' }} + /> + ); +} + export default function Page() { const { id = '' } = useParams<{ id: string }>(); const [competitor, setCompetitor] = useState(null); @@ -74,6 +112,7 @@ export default function Page() { const [detailWork, setDetailWork] = useState(null); const [workMetrics, setWorkMetrics] = useState([]); const [metricsPending, setMetricsPending] = useState(false); + const [minGrowth, setMinGrowth] = useState(null); const [messageApi, contextHolder] = message.useMessage(); const loadCompetitor = useCallback(async () => { @@ -92,7 +131,13 @@ export default function Page() { setWorksPending(true); setWorksError(null); try { - const result = await getList({ resource: 'creator-works', page: workPage, pageSize: 25, filters: { source_id: id } }); + const filters: Record = { source_id: id }; + if (minGrowth !== null) { + // 爆品筛选:growth_hours 开启后端 24h 点赞增量计算,min_likes_growth 设下限。 + filters.growth_hours = 24; + filters.min_likes_growth = minGrowth; + } + const result = await getList({ resource: 'creator-works', page: workPage, pageSize: 25, filters }); setWorks(result.data); setWorkPageInfo({ total: result.total, hasNext: Boolean(result.hasNext) }); } catch (loadError) { @@ -100,7 +145,7 @@ export default function Page() { } finally { setWorksPending(false); } - }, [id, workPage]); + }, [id, workPage, minGrowth]); useEffect(() => { loadCompetitor(); @@ -229,6 +274,21 @@ export default function Page() { > 共 {workPageInfo.total} 个作品;作品由系统按采集间隔自动同步。 + + + 24h 点赞增量 ≥ + { + setMinGrowth(value ?? null); + setWorkPage(1); + }} + /> + {worksError ? ( {work.comments_count ?? '—'} {work.shares ?? '—'} + {work.likes_growth != null && work.likes_growth > 0 ? ( + + 24h +{work.likes_growth} + + ) : null} @@ -309,6 +374,7 @@ export default function Page() { 打开原文 ) : null} + {workMetrics.length >= 2 ? : null} dateTime(value) }, { title: '点赞', dataIndex: 'likes', render: (v) => v ?? '—' }, + { title: '收藏', dataIndex: 'collect_count', render: (v) => v ?? '—' }, { title: '评论', dataIndex: 'comments_count', render: (v) => v ?? '—' }, { title: '转发', dataIndex: 'shares', render: (v) => v ?? '—' }, ]} diff --git a/web/src/services/api.ts b/web/src/services/api.ts index cc4e270..f05757b 100644 --- a/web/src/services/api.ts +++ b/web/src/services/api.ts @@ -33,7 +33,7 @@ const filterKeys: Partial> = { tasks: ['account_id', 'draft_id', 'state'], 'creator-competitors': ['platform'], 'creator-competitor-share-jobs': ['platform', 'status'], - 'creator-works': ['platform', 'source_id', 'source_type', 'published_at_status', 'published_after', 'published_before', 'min_likes', 'min_comments', 'min_shares'], + 'creator-works': ['platform', 'source_id', 'source_type', 'published_at_status', 'published_after', 'published_before', 'min_likes', 'min_comments', 'min_shares', 'growth_hours', 'min_likes_growth'], 'creator-comments': ['platform', 'work_id'], 'creator-leads': ['platform'], 'creator-operations': ['account_id'],