From 52cd0eeee691e010fd707271280241b423e9b176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Fri, 25 Jul 2025 10:54:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B=20?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=88=B0pc=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/.env.development | 4 +- nkebao/{public => }/favicon.ico | Bin .../{ => mobile}/component-test/index.tsx | 0 .../pages/{ => mobile}/content/form/api.ts | 0 .../pages/{ => mobile}/content/form/data.ts | 0 .../content/form/index.module.scss | 0 .../pages/{ => mobile}/content/form/index.tsx | 0 .../pages/{ => mobile}/content/list/api.ts | 0 .../pages/{ => mobile}/content/list/data.ts | 0 .../content/list/index.module.scss | 0 .../pages/{ => mobile}/content/list/index.tsx | 0 .../content/materials/form/api.ts | 0 .../content/materials/form/data.ts | 0 .../content/materials/form/index.module.scss | 0 .../content/materials/form/index.tsx | 0 .../content/materials/list/api.ts | 0 .../content/materials/list/data.ts | 0 .../content/materials/list/index.module.scss | 0 .../content/materials/list/index.tsx | 0 nkebao/src/pages/{ => mobile}/home/api.ts | 0 .../pages/{ => mobile}/home/index.module.scss | 0 nkebao/src/pages/{ => mobile}/home/index.tsx | 0 nkebao/src/pages/{ => mobile}/login/api.ts | 0 .../{ => mobile}/login/login.module.scss | 0 nkebao/src/pages/{ => mobile}/login/login.tsx | 0 .../mine/devices/DeviceDetail.tsx | 0 .../pages/{ => mobile}/mine/devices/index.tsx | 0 .../src/pages/{ => mobile}/mine/main/api.ts | 0 .../{ => mobile}/mine/main/index.module.scss | 0 .../pages/{ => mobile}/mine/main/index.tsx | 0 .../mine/recharge/index.module.scss | 0 .../{ => mobile}/mine/recharge/index.tsx | 0 .../mine/traffic-pool/detail/api.ts | 0 .../mine/traffic-pool/detail/data.ts | 0 .../traffic-pool/detail/index.module.scss | 0 .../mine/traffic-pool/detail/index.tsx | 0 .../mine/traffic-pool/list/BatchAddModal.tsx | 0 .../traffic-pool/list/DataAnalysisPanel.tsx | 0 .../mine/traffic-pool/list/FilterModal.tsx | 0 .../mine/traffic-pool/list/api.ts | 0 .../mine/traffic-pool/list/data.ts | 0 .../mine/traffic-pool/list/dataAnyx.tsx | 0 .../mine/traffic-pool/list/index.module.scss | 0 .../mine/traffic-pool/list/index.tsx | 0 .../mine/userSet/index.module.scss | 0 .../pages/{ => mobile}/mine/userSet/index.tsx | 0 .../mine/wechat-accounts/detail/api.ts | 0 .../mine/wechat-accounts/detail/data.ts | 0 .../wechat-accounts/detail/detail.module.scss | 0 .../mine/wechat-accounts/detail/index.tsx | 0 .../mine/wechat-accounts/list/api.ts | 0 .../wechat-accounts/list/index.module.scss | 0 .../mine/wechat-accounts/list/index.tsx | 0 nkebao/src/pages/mobile/scenarios/list/api.ts | 26 + .../mobile/scenarios/list/index.module.scss | 322 ++++++++ .../src/pages/mobile/scenarios/list/index.tsx | 172 ++++ .../pages/mobile/scenarios/plan/list/api.ts | 32 + .../pages/mobile/scenarios/plan/list/data.ts | 59 ++ .../scenarios/plan/list/index.module.scss | 383 +++++++++ .../mobile/scenarios/plan/list/index.tsx | 611 ++++++++++++++ .../scenarios/plan/list/planApi.module.scss | 601 ++++++++++++++ .../mobile/scenarios/plan/list/planApi.tsx | 437 ++++++++++ .../mobile/scenarios/plan/new/index.api.ts | 53 ++ .../pages/mobile/scenarios/plan/new/index.tsx | 217 +++++ .../plan/new/steps/BasicSettings.tsx | 774 ++++++++++++++++++ .../plan/new/steps/FriendRequestSettings.tsx | 244 ++++++ .../plan/new/steps/MessageSettings.tsx | 645 +++++++++++++++ .../scenarios/plan/new/steps/base.module.scss | 164 ++++ .../plan/new/steps/friend.module.scss | 48 ++ .../plan/new/steps/messages.module.scss | 103 +++ .../scenarios/plan/new/steps/step.api.ts | 363 ++++++++ .../workspace/ai-analyzer/index.module.scss | 96 +++ .../mobile/workspace/ai-analyzer/index.tsx | 141 ++++ .../ai-assistant/AIAssistant.module.scss | 139 ++++ .../workspace/ai-assistant/AIAssistant.tsx | 264 ++++++ .../mobile/workspace/auto-group/detail/api.ts | 6 + .../auto-group/detail/index.module.scss | 149 ++++ .../workspace/auto-group/detail/index.tsx | 384 +++++++++ .../mobile/workspace/auto-group/form/api.ts | 11 + .../auto-group/form/index.module.scss | 34 + .../workspace/auto-group/form/index.tsx | 253 ++++++ .../mobile/workspace/auto-group/list/api.ts | 8 + .../auto-group/list/index.module.scss | 173 ++++ .../workspace/auto-group/list/index.tsx | 312 +++++++ .../mobile/workspace/auto-like/list/api.ts | 63 ++ .../auto-like/list/index.module.scss | 278 +++++++ .../mobile/workspace/auto-like/list/index.tsx | 382 +++++++++ .../workspace/auto-like/new/NewAutoLike.tsx | 552 +++++++++++++ .../mobile/workspace/auto-like/new/api.ts | 21 + .../mobile/workspace/auto-like/new/index.tsx | 416 ++++++++++ .../workspace/auto-like/new/new.module.scss | 230 ++++++ .../auto-like/record/AutoLikeDetail.tsx | 281 +++++++ .../mobile/workspace/auto-like/record/api.ts | 63 ++ .../mobile/workspace/auto-like/record/data.ts | 119 +++ .../workspace/auto-like/record/index.tsx | 309 +++++++ .../auto-like/record/record.module.scss | 266 ++++++ .../workspace/group-push/detail/groupPush.ts | 73 ++ .../group-push/detail/index.module.scss | 100 +++ .../workspace/group-push/detail/index.tsx | 261 ++++++ .../form/components/BasicSettings.tsx | 237 ++++++ .../form/components/ContentSelector.tsx | 247 ++++++ .../form/components/GroupSelector.tsx | 245 ++++++ .../workspace/group-push/form/index.data.ts | 32 + .../workspace/group-push/form/index.tsx | 199 +++++ .../group-push/list/index.module.scss | 111 +++ .../workspace/group-push/list/index.tsx | 405 +++++++++ nkebao/src/pages/mobile/workspace/main/api.ts | 14 + .../mobile/workspace/main/index.module.scss | 119 +++ .../src/pages/mobile/workspace/main/index.tsx | 214 +++++ .../mobile/workspace/moments-sync/Detail.tsx | 198 +++++ .../workspace/moments-sync/MomentsSync.tsx | 286 +++++++ .../workspace/moments-sync/index.module.scss | 365 +++++++++ .../mobile/workspace/moments-sync/new/api.ts | 13 + .../moments-sync/new/index.module.scss | 249 ++++++ .../workspace/moments-sync/new/index.tsx | 352 ++++++++ .../traffic-distribution/detail/api.ts | 0 .../traffic-distribution/detail/data.ts | 0 .../detail/index.module.scss | 0 .../traffic-distribution/detail/index.tsx | 3 + .../traffic-distribution/form/api.ts | 0 .../traffic-distribution/form/data.ts | 0 .../form/index.module.scss | 207 +++++ .../traffic-distribution/form/index.tsx | 315 +++++++ .../traffic-distribution/list/api.ts | 28 + .../traffic-distribution/list/data.ts | 133 +++ .../list/index.module.scss | 124 +++ .../traffic-distribution/list/index.tsx | 337 ++++++++ 127 files changed, 15073 insertions(+), 2 deletions(-) rename nkebao/{public => }/favicon.ico (100%) rename nkebao/src/pages/{ => mobile}/component-test/index.tsx (100%) rename nkebao/src/pages/{ => mobile}/content/form/api.ts (100%) rename nkebao/src/pages/{ => mobile}/content/form/data.ts (100%) rename nkebao/src/pages/{ => mobile}/content/form/index.module.scss (100%) rename nkebao/src/pages/{ => mobile}/content/form/index.tsx (100%) rename nkebao/src/pages/{ => mobile}/content/list/api.ts (100%) rename nkebao/src/pages/{ => mobile}/content/list/data.ts (100%) rename nkebao/src/pages/{ => mobile}/content/list/index.module.scss (100%) rename nkebao/src/pages/{ => mobile}/content/list/index.tsx (100%) rename nkebao/src/pages/{ => mobile}/content/materials/form/api.ts (100%) rename nkebao/src/pages/{ => mobile}/content/materials/form/data.ts (100%) rename nkebao/src/pages/{ => mobile}/content/materials/form/index.module.scss (100%) rename nkebao/src/pages/{ => mobile}/content/materials/form/index.tsx (100%) rename nkebao/src/pages/{ => mobile}/content/materials/list/api.ts (100%) rename nkebao/src/pages/{ => mobile}/content/materials/list/data.ts (100%) rename nkebao/src/pages/{ => mobile}/content/materials/list/index.module.scss (100%) rename nkebao/src/pages/{ => mobile}/content/materials/list/index.tsx (100%) rename nkebao/src/pages/{ => mobile}/home/api.ts (100%) rename nkebao/src/pages/{ => mobile}/home/index.module.scss (100%) rename nkebao/src/pages/{ => mobile}/home/index.tsx (100%) rename nkebao/src/pages/{ => mobile}/login/api.ts (100%) rename nkebao/src/pages/{ => mobile}/login/login.module.scss (100%) rename nkebao/src/pages/{ => mobile}/login/login.tsx (100%) rename nkebao/src/pages/{ => mobile}/mine/devices/DeviceDetail.tsx (100%) rename nkebao/src/pages/{ => mobile}/mine/devices/index.tsx (100%) rename nkebao/src/pages/{ => mobile}/mine/main/api.ts (100%) rename nkebao/src/pages/{ => mobile}/mine/main/index.module.scss (100%) rename nkebao/src/pages/{ => mobile}/mine/main/index.tsx (100%) rename nkebao/src/pages/{ => mobile}/mine/recharge/index.module.scss (100%) rename nkebao/src/pages/{ => mobile}/mine/recharge/index.tsx (100%) rename nkebao/src/pages/{ => mobile}/mine/traffic-pool/detail/api.ts (100%) rename nkebao/src/pages/{ => mobile}/mine/traffic-pool/detail/data.ts (100%) rename nkebao/src/pages/{ => mobile}/mine/traffic-pool/detail/index.module.scss (100%) rename nkebao/src/pages/{ => mobile}/mine/traffic-pool/detail/index.tsx (100%) rename nkebao/src/pages/{ => mobile}/mine/traffic-pool/list/BatchAddModal.tsx (100%) rename nkebao/src/pages/{ => mobile}/mine/traffic-pool/list/DataAnalysisPanel.tsx (100%) rename nkebao/src/pages/{ => mobile}/mine/traffic-pool/list/FilterModal.tsx (100%) rename nkebao/src/pages/{ => mobile}/mine/traffic-pool/list/api.ts (100%) rename nkebao/src/pages/{ => mobile}/mine/traffic-pool/list/data.ts (100%) rename nkebao/src/pages/{ => mobile}/mine/traffic-pool/list/dataAnyx.tsx (100%) rename nkebao/src/pages/{ => mobile}/mine/traffic-pool/list/index.module.scss (100%) rename nkebao/src/pages/{ => mobile}/mine/traffic-pool/list/index.tsx (100%) rename nkebao/src/pages/{ => mobile}/mine/userSet/index.module.scss (100%) rename nkebao/src/pages/{ => mobile}/mine/userSet/index.tsx (100%) rename nkebao/src/pages/{ => mobile}/mine/wechat-accounts/detail/api.ts (100%) rename nkebao/src/pages/{ => mobile}/mine/wechat-accounts/detail/data.ts (100%) rename nkebao/src/pages/{ => mobile}/mine/wechat-accounts/detail/detail.module.scss (100%) rename nkebao/src/pages/{ => mobile}/mine/wechat-accounts/detail/index.tsx (100%) rename nkebao/src/pages/{ => mobile}/mine/wechat-accounts/list/api.ts (100%) rename nkebao/src/pages/{ => mobile}/mine/wechat-accounts/list/index.module.scss (100%) rename nkebao/src/pages/{ => mobile}/mine/wechat-accounts/list/index.tsx (100%) create mode 100644 nkebao/src/pages/mobile/scenarios/list/api.ts create mode 100644 nkebao/src/pages/mobile/scenarios/list/index.module.scss create mode 100644 nkebao/src/pages/mobile/scenarios/list/index.tsx create mode 100644 nkebao/src/pages/mobile/scenarios/plan/list/api.ts create mode 100644 nkebao/src/pages/mobile/scenarios/plan/list/data.ts create mode 100644 nkebao/src/pages/mobile/scenarios/plan/list/index.module.scss create mode 100644 nkebao/src/pages/mobile/scenarios/plan/list/index.tsx create mode 100644 nkebao/src/pages/mobile/scenarios/plan/list/planApi.module.scss create mode 100644 nkebao/src/pages/mobile/scenarios/plan/list/planApi.tsx create mode 100644 nkebao/src/pages/mobile/scenarios/plan/new/index.api.ts create mode 100644 nkebao/src/pages/mobile/scenarios/plan/new/index.tsx create mode 100644 nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx create mode 100644 nkebao/src/pages/mobile/scenarios/plan/new/steps/FriendRequestSettings.tsx create mode 100644 nkebao/src/pages/mobile/scenarios/plan/new/steps/MessageSettings.tsx create mode 100644 nkebao/src/pages/mobile/scenarios/plan/new/steps/base.module.scss create mode 100644 nkebao/src/pages/mobile/scenarios/plan/new/steps/friend.module.scss create mode 100644 nkebao/src/pages/mobile/scenarios/plan/new/steps/messages.module.scss create mode 100644 nkebao/src/pages/mobile/scenarios/plan/new/steps/step.api.ts create mode 100644 nkebao/src/pages/mobile/workspace/ai-analyzer/index.module.scss create mode 100644 nkebao/src/pages/mobile/workspace/ai-analyzer/index.tsx create mode 100644 nkebao/src/pages/mobile/workspace/ai-assistant/AIAssistant.module.scss create mode 100644 nkebao/src/pages/mobile/workspace/ai-assistant/AIAssistant.tsx create mode 100644 nkebao/src/pages/mobile/workspace/auto-group/detail/api.ts create mode 100644 nkebao/src/pages/mobile/workspace/auto-group/detail/index.module.scss create mode 100644 nkebao/src/pages/mobile/workspace/auto-group/detail/index.tsx create mode 100644 nkebao/src/pages/mobile/workspace/auto-group/form/api.ts create mode 100644 nkebao/src/pages/mobile/workspace/auto-group/form/index.module.scss create mode 100644 nkebao/src/pages/mobile/workspace/auto-group/form/index.tsx create mode 100644 nkebao/src/pages/mobile/workspace/auto-group/list/api.ts create mode 100644 nkebao/src/pages/mobile/workspace/auto-group/list/index.module.scss create mode 100644 nkebao/src/pages/mobile/workspace/auto-group/list/index.tsx create mode 100644 nkebao/src/pages/mobile/workspace/auto-like/list/api.ts create mode 100644 nkebao/src/pages/mobile/workspace/auto-like/list/index.module.scss create mode 100644 nkebao/src/pages/mobile/workspace/auto-like/list/index.tsx create mode 100644 nkebao/src/pages/mobile/workspace/auto-like/new/NewAutoLike.tsx create mode 100644 nkebao/src/pages/mobile/workspace/auto-like/new/api.ts create mode 100644 nkebao/src/pages/mobile/workspace/auto-like/new/index.tsx create mode 100644 nkebao/src/pages/mobile/workspace/auto-like/new/new.module.scss create mode 100644 nkebao/src/pages/mobile/workspace/auto-like/record/AutoLikeDetail.tsx create mode 100644 nkebao/src/pages/mobile/workspace/auto-like/record/api.ts create mode 100644 nkebao/src/pages/mobile/workspace/auto-like/record/data.ts create mode 100644 nkebao/src/pages/mobile/workspace/auto-like/record/index.tsx create mode 100644 nkebao/src/pages/mobile/workspace/auto-like/record/record.module.scss create mode 100644 nkebao/src/pages/mobile/workspace/group-push/detail/groupPush.ts create mode 100644 nkebao/src/pages/mobile/workspace/group-push/detail/index.module.scss create mode 100644 nkebao/src/pages/mobile/workspace/group-push/detail/index.tsx create mode 100644 nkebao/src/pages/mobile/workspace/group-push/form/components/BasicSettings.tsx create mode 100644 nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx create mode 100644 nkebao/src/pages/mobile/workspace/group-push/form/components/GroupSelector.tsx create mode 100644 nkebao/src/pages/mobile/workspace/group-push/form/index.data.ts create mode 100644 nkebao/src/pages/mobile/workspace/group-push/form/index.tsx create mode 100644 nkebao/src/pages/mobile/workspace/group-push/list/index.module.scss create mode 100644 nkebao/src/pages/mobile/workspace/group-push/list/index.tsx create mode 100644 nkebao/src/pages/mobile/workspace/main/api.ts create mode 100644 nkebao/src/pages/mobile/workspace/main/index.module.scss create mode 100644 nkebao/src/pages/mobile/workspace/main/index.tsx create mode 100644 nkebao/src/pages/mobile/workspace/moments-sync/Detail.tsx create mode 100644 nkebao/src/pages/mobile/workspace/moments-sync/MomentsSync.tsx create mode 100644 nkebao/src/pages/mobile/workspace/moments-sync/index.module.scss create mode 100644 nkebao/src/pages/mobile/workspace/moments-sync/new/api.ts create mode 100644 nkebao/src/pages/mobile/workspace/moments-sync/new/index.module.scss create mode 100644 nkebao/src/pages/mobile/workspace/moments-sync/new/index.tsx create mode 100644 nkebao/src/pages/mobile/workspace/traffic-distribution/detail/api.ts create mode 100644 nkebao/src/pages/mobile/workspace/traffic-distribution/detail/data.ts create mode 100644 nkebao/src/pages/mobile/workspace/traffic-distribution/detail/index.module.scss create mode 100644 nkebao/src/pages/mobile/workspace/traffic-distribution/detail/index.tsx create mode 100644 nkebao/src/pages/mobile/workspace/traffic-distribution/form/api.ts create mode 100644 nkebao/src/pages/mobile/workspace/traffic-distribution/form/data.ts create mode 100644 nkebao/src/pages/mobile/workspace/traffic-distribution/form/index.module.scss create mode 100644 nkebao/src/pages/mobile/workspace/traffic-distribution/form/index.tsx create mode 100644 nkebao/src/pages/mobile/workspace/traffic-distribution/list/api.ts create mode 100644 nkebao/src/pages/mobile/workspace/traffic-distribution/list/data.ts create mode 100644 nkebao/src/pages/mobile/workspace/traffic-distribution/list/index.module.scss create mode 100644 nkebao/src/pages/mobile/workspace/traffic-distribution/list/index.tsx diff --git a/nkebao/.env.development b/nkebao/.env.development index 6d0e088ca..3b39062c9 100644 --- a/nkebao/.env.development +++ b/nkebao/.env.development @@ -1,6 +1,6 @@ # 基础环境变量示例 -# VITE_API_BASE_URL=http://www.yishi.com -VITE_API_BASE_URL=https://ckbapi.quwanzhi.com +VITE_API_BASE_URL=http://www.yishi.com +# VITE_API_BASE_URL=https://ckbapi.quwanzhi.com VITE_APP_TITLE=Nkebao Base diff --git a/nkebao/public/favicon.ico b/nkebao/favicon.ico similarity index 100% rename from nkebao/public/favicon.ico rename to nkebao/favicon.ico diff --git a/nkebao/src/pages/component-test/index.tsx b/nkebao/src/pages/mobile/component-test/index.tsx similarity index 100% rename from nkebao/src/pages/component-test/index.tsx rename to nkebao/src/pages/mobile/component-test/index.tsx diff --git a/nkebao/src/pages/content/form/api.ts b/nkebao/src/pages/mobile/content/form/api.ts similarity index 100% rename from nkebao/src/pages/content/form/api.ts rename to nkebao/src/pages/mobile/content/form/api.ts diff --git a/nkebao/src/pages/content/form/data.ts b/nkebao/src/pages/mobile/content/form/data.ts similarity index 100% rename from nkebao/src/pages/content/form/data.ts rename to nkebao/src/pages/mobile/content/form/data.ts diff --git a/nkebao/src/pages/content/form/index.module.scss b/nkebao/src/pages/mobile/content/form/index.module.scss similarity index 100% rename from nkebao/src/pages/content/form/index.module.scss rename to nkebao/src/pages/mobile/content/form/index.module.scss diff --git a/nkebao/src/pages/content/form/index.tsx b/nkebao/src/pages/mobile/content/form/index.tsx similarity index 100% rename from nkebao/src/pages/content/form/index.tsx rename to nkebao/src/pages/mobile/content/form/index.tsx diff --git a/nkebao/src/pages/content/list/api.ts b/nkebao/src/pages/mobile/content/list/api.ts similarity index 100% rename from nkebao/src/pages/content/list/api.ts rename to nkebao/src/pages/mobile/content/list/api.ts diff --git a/nkebao/src/pages/content/list/data.ts b/nkebao/src/pages/mobile/content/list/data.ts similarity index 100% rename from nkebao/src/pages/content/list/data.ts rename to nkebao/src/pages/mobile/content/list/data.ts diff --git a/nkebao/src/pages/content/list/index.module.scss b/nkebao/src/pages/mobile/content/list/index.module.scss similarity index 100% rename from nkebao/src/pages/content/list/index.module.scss rename to nkebao/src/pages/mobile/content/list/index.module.scss diff --git a/nkebao/src/pages/content/list/index.tsx b/nkebao/src/pages/mobile/content/list/index.tsx similarity index 100% rename from nkebao/src/pages/content/list/index.tsx rename to nkebao/src/pages/mobile/content/list/index.tsx diff --git a/nkebao/src/pages/content/materials/form/api.ts b/nkebao/src/pages/mobile/content/materials/form/api.ts similarity index 100% rename from nkebao/src/pages/content/materials/form/api.ts rename to nkebao/src/pages/mobile/content/materials/form/api.ts diff --git a/nkebao/src/pages/content/materials/form/data.ts b/nkebao/src/pages/mobile/content/materials/form/data.ts similarity index 100% rename from nkebao/src/pages/content/materials/form/data.ts rename to nkebao/src/pages/mobile/content/materials/form/data.ts diff --git a/nkebao/src/pages/content/materials/form/index.module.scss b/nkebao/src/pages/mobile/content/materials/form/index.module.scss similarity index 100% rename from nkebao/src/pages/content/materials/form/index.module.scss rename to nkebao/src/pages/mobile/content/materials/form/index.module.scss diff --git a/nkebao/src/pages/content/materials/form/index.tsx b/nkebao/src/pages/mobile/content/materials/form/index.tsx similarity index 100% rename from nkebao/src/pages/content/materials/form/index.tsx rename to nkebao/src/pages/mobile/content/materials/form/index.tsx diff --git a/nkebao/src/pages/content/materials/list/api.ts b/nkebao/src/pages/mobile/content/materials/list/api.ts similarity index 100% rename from nkebao/src/pages/content/materials/list/api.ts rename to nkebao/src/pages/mobile/content/materials/list/api.ts diff --git a/nkebao/src/pages/content/materials/list/data.ts b/nkebao/src/pages/mobile/content/materials/list/data.ts similarity index 100% rename from nkebao/src/pages/content/materials/list/data.ts rename to nkebao/src/pages/mobile/content/materials/list/data.ts diff --git a/nkebao/src/pages/content/materials/list/index.module.scss b/nkebao/src/pages/mobile/content/materials/list/index.module.scss similarity index 100% rename from nkebao/src/pages/content/materials/list/index.module.scss rename to nkebao/src/pages/mobile/content/materials/list/index.module.scss diff --git a/nkebao/src/pages/content/materials/list/index.tsx b/nkebao/src/pages/mobile/content/materials/list/index.tsx similarity index 100% rename from nkebao/src/pages/content/materials/list/index.tsx rename to nkebao/src/pages/mobile/content/materials/list/index.tsx diff --git a/nkebao/src/pages/home/api.ts b/nkebao/src/pages/mobile/home/api.ts similarity index 100% rename from nkebao/src/pages/home/api.ts rename to nkebao/src/pages/mobile/home/api.ts diff --git a/nkebao/src/pages/home/index.module.scss b/nkebao/src/pages/mobile/home/index.module.scss similarity index 100% rename from nkebao/src/pages/home/index.module.scss rename to nkebao/src/pages/mobile/home/index.module.scss diff --git a/nkebao/src/pages/home/index.tsx b/nkebao/src/pages/mobile/home/index.tsx similarity index 100% rename from nkebao/src/pages/home/index.tsx rename to nkebao/src/pages/mobile/home/index.tsx diff --git a/nkebao/src/pages/login/api.ts b/nkebao/src/pages/mobile/login/api.ts similarity index 100% rename from nkebao/src/pages/login/api.ts rename to nkebao/src/pages/mobile/login/api.ts diff --git a/nkebao/src/pages/login/login.module.scss b/nkebao/src/pages/mobile/login/login.module.scss similarity index 100% rename from nkebao/src/pages/login/login.module.scss rename to nkebao/src/pages/mobile/login/login.module.scss diff --git a/nkebao/src/pages/login/login.tsx b/nkebao/src/pages/mobile/login/login.tsx similarity index 100% rename from nkebao/src/pages/login/login.tsx rename to nkebao/src/pages/mobile/login/login.tsx diff --git a/nkebao/src/pages/mine/devices/DeviceDetail.tsx b/nkebao/src/pages/mobile/mine/devices/DeviceDetail.tsx similarity index 100% rename from nkebao/src/pages/mine/devices/DeviceDetail.tsx rename to nkebao/src/pages/mobile/mine/devices/DeviceDetail.tsx diff --git a/nkebao/src/pages/mine/devices/index.tsx b/nkebao/src/pages/mobile/mine/devices/index.tsx similarity index 100% rename from nkebao/src/pages/mine/devices/index.tsx rename to nkebao/src/pages/mobile/mine/devices/index.tsx diff --git a/nkebao/src/pages/mine/main/api.ts b/nkebao/src/pages/mobile/mine/main/api.ts similarity index 100% rename from nkebao/src/pages/mine/main/api.ts rename to nkebao/src/pages/mobile/mine/main/api.ts diff --git a/nkebao/src/pages/mine/main/index.module.scss b/nkebao/src/pages/mobile/mine/main/index.module.scss similarity index 100% rename from nkebao/src/pages/mine/main/index.module.scss rename to nkebao/src/pages/mobile/mine/main/index.module.scss diff --git a/nkebao/src/pages/mine/main/index.tsx b/nkebao/src/pages/mobile/mine/main/index.tsx similarity index 100% rename from nkebao/src/pages/mine/main/index.tsx rename to nkebao/src/pages/mobile/mine/main/index.tsx diff --git a/nkebao/src/pages/mine/recharge/index.module.scss b/nkebao/src/pages/mobile/mine/recharge/index.module.scss similarity index 100% rename from nkebao/src/pages/mine/recharge/index.module.scss rename to nkebao/src/pages/mobile/mine/recharge/index.module.scss diff --git a/nkebao/src/pages/mine/recharge/index.tsx b/nkebao/src/pages/mobile/mine/recharge/index.tsx similarity index 100% rename from nkebao/src/pages/mine/recharge/index.tsx rename to nkebao/src/pages/mobile/mine/recharge/index.tsx diff --git a/nkebao/src/pages/mine/traffic-pool/detail/api.ts b/nkebao/src/pages/mobile/mine/traffic-pool/detail/api.ts similarity index 100% rename from nkebao/src/pages/mine/traffic-pool/detail/api.ts rename to nkebao/src/pages/mobile/mine/traffic-pool/detail/api.ts diff --git a/nkebao/src/pages/mine/traffic-pool/detail/data.ts b/nkebao/src/pages/mobile/mine/traffic-pool/detail/data.ts similarity index 100% rename from nkebao/src/pages/mine/traffic-pool/detail/data.ts rename to nkebao/src/pages/mobile/mine/traffic-pool/detail/data.ts diff --git a/nkebao/src/pages/mine/traffic-pool/detail/index.module.scss b/nkebao/src/pages/mobile/mine/traffic-pool/detail/index.module.scss similarity index 100% rename from nkebao/src/pages/mine/traffic-pool/detail/index.module.scss rename to nkebao/src/pages/mobile/mine/traffic-pool/detail/index.module.scss diff --git a/nkebao/src/pages/mine/traffic-pool/detail/index.tsx b/nkebao/src/pages/mobile/mine/traffic-pool/detail/index.tsx similarity index 100% rename from nkebao/src/pages/mine/traffic-pool/detail/index.tsx rename to nkebao/src/pages/mobile/mine/traffic-pool/detail/index.tsx diff --git a/nkebao/src/pages/mine/traffic-pool/list/BatchAddModal.tsx b/nkebao/src/pages/mobile/mine/traffic-pool/list/BatchAddModal.tsx similarity index 100% rename from nkebao/src/pages/mine/traffic-pool/list/BatchAddModal.tsx rename to nkebao/src/pages/mobile/mine/traffic-pool/list/BatchAddModal.tsx diff --git a/nkebao/src/pages/mine/traffic-pool/list/DataAnalysisPanel.tsx b/nkebao/src/pages/mobile/mine/traffic-pool/list/DataAnalysisPanel.tsx similarity index 100% rename from nkebao/src/pages/mine/traffic-pool/list/DataAnalysisPanel.tsx rename to nkebao/src/pages/mobile/mine/traffic-pool/list/DataAnalysisPanel.tsx diff --git a/nkebao/src/pages/mine/traffic-pool/list/FilterModal.tsx b/nkebao/src/pages/mobile/mine/traffic-pool/list/FilterModal.tsx similarity index 100% rename from nkebao/src/pages/mine/traffic-pool/list/FilterModal.tsx rename to nkebao/src/pages/mobile/mine/traffic-pool/list/FilterModal.tsx diff --git a/nkebao/src/pages/mine/traffic-pool/list/api.ts b/nkebao/src/pages/mobile/mine/traffic-pool/list/api.ts similarity index 100% rename from nkebao/src/pages/mine/traffic-pool/list/api.ts rename to nkebao/src/pages/mobile/mine/traffic-pool/list/api.ts diff --git a/nkebao/src/pages/mine/traffic-pool/list/data.ts b/nkebao/src/pages/mobile/mine/traffic-pool/list/data.ts similarity index 100% rename from nkebao/src/pages/mine/traffic-pool/list/data.ts rename to nkebao/src/pages/mobile/mine/traffic-pool/list/data.ts diff --git a/nkebao/src/pages/mine/traffic-pool/list/dataAnyx.tsx b/nkebao/src/pages/mobile/mine/traffic-pool/list/dataAnyx.tsx similarity index 100% rename from nkebao/src/pages/mine/traffic-pool/list/dataAnyx.tsx rename to nkebao/src/pages/mobile/mine/traffic-pool/list/dataAnyx.tsx diff --git a/nkebao/src/pages/mine/traffic-pool/list/index.module.scss b/nkebao/src/pages/mobile/mine/traffic-pool/list/index.module.scss similarity index 100% rename from nkebao/src/pages/mine/traffic-pool/list/index.module.scss rename to nkebao/src/pages/mobile/mine/traffic-pool/list/index.module.scss diff --git a/nkebao/src/pages/mine/traffic-pool/list/index.tsx b/nkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx similarity index 100% rename from nkebao/src/pages/mine/traffic-pool/list/index.tsx rename to nkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx diff --git a/nkebao/src/pages/mine/userSet/index.module.scss b/nkebao/src/pages/mobile/mine/userSet/index.module.scss similarity index 100% rename from nkebao/src/pages/mine/userSet/index.module.scss rename to nkebao/src/pages/mobile/mine/userSet/index.module.scss diff --git a/nkebao/src/pages/mine/userSet/index.tsx b/nkebao/src/pages/mobile/mine/userSet/index.tsx similarity index 100% rename from nkebao/src/pages/mine/userSet/index.tsx rename to nkebao/src/pages/mobile/mine/userSet/index.tsx diff --git a/nkebao/src/pages/mine/wechat-accounts/detail/api.ts b/nkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts similarity index 100% rename from nkebao/src/pages/mine/wechat-accounts/detail/api.ts rename to nkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts diff --git a/nkebao/src/pages/mine/wechat-accounts/detail/data.ts b/nkebao/src/pages/mobile/mine/wechat-accounts/detail/data.ts similarity index 100% rename from nkebao/src/pages/mine/wechat-accounts/detail/data.ts rename to nkebao/src/pages/mobile/mine/wechat-accounts/detail/data.ts diff --git a/nkebao/src/pages/mine/wechat-accounts/detail/detail.module.scss b/nkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss similarity index 100% rename from nkebao/src/pages/mine/wechat-accounts/detail/detail.module.scss rename to nkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss diff --git a/nkebao/src/pages/mine/wechat-accounts/detail/index.tsx b/nkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx similarity index 100% rename from nkebao/src/pages/mine/wechat-accounts/detail/index.tsx rename to nkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx diff --git a/nkebao/src/pages/mine/wechat-accounts/list/api.ts b/nkebao/src/pages/mobile/mine/wechat-accounts/list/api.ts similarity index 100% rename from nkebao/src/pages/mine/wechat-accounts/list/api.ts rename to nkebao/src/pages/mobile/mine/wechat-accounts/list/api.ts diff --git a/nkebao/src/pages/mine/wechat-accounts/list/index.module.scss b/nkebao/src/pages/mobile/mine/wechat-accounts/list/index.module.scss similarity index 100% rename from nkebao/src/pages/mine/wechat-accounts/list/index.module.scss rename to nkebao/src/pages/mobile/mine/wechat-accounts/list/index.module.scss diff --git a/nkebao/src/pages/mine/wechat-accounts/list/index.tsx b/nkebao/src/pages/mobile/mine/wechat-accounts/list/index.tsx similarity index 100% rename from nkebao/src/pages/mine/wechat-accounts/list/index.tsx rename to nkebao/src/pages/mobile/mine/wechat-accounts/list/index.tsx diff --git a/nkebao/src/pages/mobile/scenarios/list/api.ts b/nkebao/src/pages/mobile/scenarios/list/api.ts new file mode 100644 index 000000000..0fff16626 --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/list/api.ts @@ -0,0 +1,26 @@ +import request from '@/api/request'; + +// 获取场景列表 +export function getScenarios(params: any) { + return request('/v1/plan/scenes', params, 'GET'); +} + +// 获取场景详情 +export function getScenarioDetail(id: string) { + return request(`/v1/scenarios/${id}`, {}, 'GET'); +} + +// 创建场景 +export function createScenario(data: any) { + return request('/v1/scenarios', data, 'POST'); +} + +// 更新场景 +export function updateScenario(id: string, data: any) { + return request(`/v1/scenarios/${id}`, data, 'PUT'); +} + +// 删除场景 +export function deleteScenario(id: string) { + return request(`/v1/scenarios/${id}`, {}, 'DELETE'); +} diff --git a/nkebao/src/pages/mobile/scenarios/list/index.module.scss b/nkebao/src/pages/mobile/scenarios/list/index.module.scss new file mode 100644 index 000000000..e6a3ae827 --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/list/index.module.scss @@ -0,0 +1,322 @@ +// 导航栏样式 +.nav-title { + font-size: 18px; + font-weight: 600; + color: var(--primary-color); +} + +.nav-text { + color: var(--primary-color); +} + +.nav-right { + margin-left: 4px; + font-size: 12px; +} + +.new-plan-btn { + border-radius: 20px; + padding: 4px 12px; + height: 32px; + font-size: 12px; + background: var(--primary-gradient); + border: none; + box-shadow: 0 2px 8px var(--primary-shadow); + + &:active { + transform: translateY(1px); + box-shadow: 0 1px 4px var(--primary-shadow); + } +} + +// 页面容器 +.scene-page { + background: #f5f6fa; + min-height: 100vh; + padding: 0 0 60px 0; +} + +// 错误提示 +.error-notice { + margin-bottom: 12px; + padding: 8px 12px; + background: #fff2e8; + border: 1px solid #ffd591; + border-radius: 8px; + box-shadow: 0 1px 4px rgba(255, 213, 145, 0.2); +} + +.error-notice-text { + font-size: 12px; + color: #d46b08; + font-weight: 500; +} + +// 加载状态 +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 200px; + gap: 12px; +} + +.loading-text { + font-size: 14px; + color: #666; + font-weight: 500; +} + +// 错误状态 +.error-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 200px; + gap: 16px; +} + +.error-text { + font-size: 14px; + color: #ff4d4f; + text-align: center; + font-weight: 500; +} + +.retry-button { + min-width: 100px; + border-radius: 20px; + background: var(--primary-gradient); + border: none; + box-shadow: 0 2px 8px var(--primary-shadow); +} + +// 页面头部 +.scene-header { + margin-bottom: 16px; + text-align: center; +} + +.header-title { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + font-size: 18px; + font-weight: 700; + color: var(--primary-color); + margin-bottom: 6px; +} + +.header-icon { + font-size: 20px; + color: var(--primary-color); +} + +.header-subtitle { + font-size: 12px; + color: #666; + line-height: 1.4; +} + +// 场景列表 +.scenarios-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +// 场景卡片 +.scenario-item { + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + transform: translateY(-1px); + } + + &:active { + transform: translateY(0); + } +} + +.scenarios-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + padding: 16px; +} + +.scenario-card { + background: #fff; + border-radius: 16px; + box-shadow: 0 2px 8px rgba(0,0,0,0.06); + transition: box-shadow 0.2s, transform 0.2s; + cursor: pointer; + overflow: hidden; + &:hover { + box-shadow: 0 6px 16px rgba(0,0,0,0.12); + transform: translateY(-2px) scale(1.02); + } +} + +.card-inner { + display: flex; + flex-direction: column; + align-items: center; + padding: 18px 10px 14px 10px; +} + +.card-img-wrap { + margin-bottom: 8px; +} +.card-img-bg { + width: 48px; + height: 48px; + background: #f0f2f5; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} +.card-img { + width: 32px; + height: 32px; + object-fit: contain; +} + +.card-title { + font-size: 16px; + font-weight: 600; + color: #1677ff; + text-align: center; + margin-bottom: 2px; +} + +.card-desc { + font-size: 12px; + color: #888; + text-align: center; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.card-stats { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + margin-top: 4px; +} +.card-count { + font-size: 13px; + color: #666; +} +.card-growth { + font-size: 12px; + color: #52c41a; + display: flex; + align-items: center; +} + +// 空状态 +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; + text-align: center; +} + +.empty-icon { + font-size: 36px; + margin-bottom: 12px; + opacity: 0.6; +} + +.empty-text { + font-size: 14px; + color: #666; + margin-bottom: 16px; + font-weight: 500; +} + +.empty-action { + border-radius: 20px; + background: var(--primary-gradient); + border: none; + box-shadow: 0 2px 8px var(--primary-shadow); + padding: 6px 16px; + font-weight: 500; + font-size: 12px; +} + +// 响应式设计 +@media (max-width: 480px) { + + .scenario-card { + padding: 14px 16px; + min-height: 70px; + } + + .scenario-icon { + width: 46px; + height: 46px; + } + + .scenario-image { + width: 28px; + height: 28px; + } + + .scenario-name { + font-size: 15px; + } + + .stat-text { + font-size: 12px; + } + + .scenario-growth { + font-size: 15px; + } + + .growth-icon { + font-size: 13px; + } +} + +@media (max-width: 500px) { + .scenarios-grid { + gap: 10px; + padding: 10px; + } + .card-inner { + padding: 12px 4px 10px 4px; + } + .card-img-bg { + width: 60px; + height: 60px; + } + .card-img { + width: 40px; + height: 40px; + } + .card-title { + font-size: 15px; + } + .card-desc { + font-size: 11px; + } + .card-count { + font-size: 12px; + } + .card-growth { + font-size: 11px; + } +} diff --git a/nkebao/src/pages/mobile/scenarios/list/index.tsx b/nkebao/src/pages/mobile/scenarios/list/index.tsx new file mode 100644 index 000000000..24ddffe85 --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/list/index.tsx @@ -0,0 +1,172 @@ +import React, { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { NavBar, Button, Toast } from "antd-mobile"; +import { PlusOutlined, RiseOutlined } from "@ant-design/icons"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import Layout from "@/components/Layout/Layout"; +import { getScenarios } from "./api"; +import style from "./index.module.scss"; + +interface Scenario { + id: string; + name: string; + image: string; + description?: string; + count: number; + growth: string; + status: number; +} + +const scenarioDescriptions: Record = { + douyin: "通过抖音平台进行精准获客", + xiaohongshu: "利用小红书平台进行内容营销获客", + gongzhonghao: "通过微信公众号进行获客", + haibao: "通过海报分享进行获客", + phone: "通过电话营销进行获客", + weixinqun: "通过微信群进行获客", + payment: "通过付款码进行获客", + api: "通过API接口进行获客", +}; + +const Scene: React.FC = () => { + const navigate = useNavigate(); + const [scenarios, setScenarios] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + const fetchScenarios = async () => { + setLoading(true); + setError(""); + try { + const response = await getScenarios({ page: 1, limit: 20 }); + const transformedScenarios: Scenario[] = response.map((item: any) => ({ + id: item.id.toString(), + name: item.name, + image: + item.image || + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-api.png", + description: + scenarioDescriptions[item.name?.toLowerCase()] || + "通过该平台进行获客", + count: item.count, + growth: item.growth, + status: item.status, + })); + setScenarios(transformedScenarios); + } catch (error) { + setError("获取场景数据失败,请稍后重试"); + Toast.show({ + content: "获取场景数据失败,请稍后重试", + position: "top", + }); + } finally { + setLoading(false); + } + }; + fetchScenarios(); + }, []); + + const handleScenarioClick = (scenarioId: string, scenarioName: string) => { + navigate( + `/scenarios/list/${scenarioId}/${encodeURIComponent(scenarioName)}` + ); + }; + + const handleNewPlan = () => { + navigate("/scenarios/new"); + }; + + if (error && scenarios.length === 0) { + return ( + +
场景获客
+ + + } + > +
+
{error}
+ +
+
+ ); + } + + return ( + 场景获客} + right={ + + } + > + } + footer={} + > +
+
+ {scenarios.map((scenario) => ( +
handleScenarioClick(scenario.id, scenario.name)} + > +
+
+
+ {scenario.name} { + e.currentTarget.src = + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-api.png"; + }} + /> +
+
+
{scenario.name}
+ {scenario.description && ( +
+ {scenario.description} +
+ )} +
+ + 今日: {scenario.count} + + + + {scenario.growth} + +
+
+
+ ))} +
+
+
+ ); +}; + +export default Scene; diff --git a/nkebao/src/pages/mobile/scenarios/plan/list/api.ts b/nkebao/src/pages/mobile/scenarios/plan/list/api.ts new file mode 100644 index 000000000..4b5f6e4b2 --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/plan/list/api.ts @@ -0,0 +1,32 @@ +import request from "@/api/request"; +import { PlanDetail, PlanListResponse, ApiResponse } from "./data"; + +// ==================== 计划相关接口 ==================== +// 获取计划列表 +export function getPlanList(params: { + sceneId: string; + page: number; + pageSize: number; +}): Promise { + return request(`/v1/plan/list`, params, "GET"); +} + +// 获取计划详情 +export function getPlanDetail(planId: string): Promise { + return request(`/v1/plan/detail`, { planId }, "GET"); +} + +// 复制计划 +export function copyPlan(planId: string): Promise> { + return request(`/v1/plan/copy`, { planId }, "GET"); +} + +// 删除计划 +export function deletePlan(planId: string): Promise> { + return request(`/v1/plan/delete`, { planId }, "DELETE"); +} + +// 获取小程序二维码 +export function getWxMinAppCode(planId: string): Promise> { + return request(`/v1/plan/getWxMinAppCode`, { taskId: planId }, "GET"); +} diff --git a/nkebao/src/pages/mobile/scenarios/plan/list/data.ts b/nkebao/src/pages/mobile/scenarios/plan/list/data.ts new file mode 100644 index 000000000..e757f433c --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/plan/list/data.ts @@ -0,0 +1,59 @@ +export interface Task { + id: string; + name: string; + status: number; + created_at: string; + updated_at: string; + enabled: boolean; + total_customers?: number; + today_customers?: number; + lastUpdated?: string; + stats?: { + devices?: number; + acquired?: number; + added?: number; + }; + reqConf?: { + device?: string[]; + selectedDevices?: string[]; + }; + acquiredCount?: number; + addedCount?: number; + passRate?: number; +} + +export interface ApiSettings { + apiKey: string; + webhookUrl: string; + taskId: string; +} + +// API响应相关类型 +export interface TextUrl { + apiKey: string; + originalString?: string; + sign?: string; + fullUrl: string; +} + +export interface PlanDetail { + id: number; + name: string; + scenario: number; + enabled: boolean; + status: number; + apiKey: string; + textUrl: TextUrl; + [key: string]: any; +} + +export interface ApiResponse { + code: number; + msg?: string; + data: T; +} + +export interface PlanListResponse { + list: Task[]; + total: number; +} diff --git a/nkebao/src/pages/mobile/scenarios/plan/list/index.module.scss b/nkebao/src/pages/mobile/scenarios/plan/list/index.module.scss new file mode 100644 index 000000000..cfd64be74 --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/plan/list/index.module.scss @@ -0,0 +1,383 @@ +.scenario-list-page { + padding:0 16px; +} + +.loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 60vh; + gap: 16px; +} + +.loading-text { + color: #666; + font-size: 14px; +} + +.search-bar { + display: flex; + gap: 12px; + align-items: center; + padding: 16px; +} + +.search-input-wrapper { + position: relative; + flex: 1; + + .ant-input { + border-radius: 8px; + height: 40px; + } +} + + +.plan-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.plan-item { + background: white; + border-radius: 12px; + padding: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: all 0.2s ease; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } +} + +.plan-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.plan-name { + font-size: 16px; + font-weight: 600; + color: #333; + flex: 1; + margin-right: 12px; +} + +.plan-header-right { + display: flex; + align-items: center; + gap: 8px; +} + +.more-btn { + padding: 4px; + min-width: auto; + height: 28px; + width: 28px; + border-radius: 4px; + + &:hover { + background-color: #f5f5f5; + } +} + +.stats-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + margin-bottom: 16px; +} + +.stat-item { + background: #f8f9fa; + border-radius: 8px; + padding: 12px; + text-align: center; + border: 1px solid #e9ecef; +} + +.stat-label { + font-size: 12px; + color: #666; + margin-bottom: 4px; + font-weight: 500; +} + +.stat-value { + font-size: 18px; + font-weight: 600; + color: #333; + line-height: 1.2; +} + +.plan-footer { + border-top: 1px solid #f0f0f0; + padding-top: 12px; +} + +.last-execution { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: #999; + + svg { + font-size: 14px; + color: #999; + } +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + text-align: center; +} + +.empty-text { + color: #999; + font-size: 14px; + margin-bottom: 20px; +} + +.create-first-btn { + height: 40px; + padding: 0 24px; + border-radius: 20px; +} + +// 加载更多按钮样式 +.load-more-container { + display: flex; + justify-content: center; + padding: 20px 0; +} + +.load-more-btn { + height: 44px; + padding: 0 32px; + border-radius: 22px; + font-size: 16px; + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; + transition: all 0.2s ease; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } + + &:active { + transform: translateY(0); + } +} + +// 没有更多数据提示样式 +.no-more-data { + display: flex; + justify-content: center; + align-items: center; + padding: 20px 0; + color: #999; + font-size: 14px; + + span { + position: relative; + padding: 0 20px; + + &::before, + &::after { + content: ''; + position: absolute; + top: 50%; + width: 40px; + height: 1px; + background-color: #e0e0e0; + } + + &::before { + left: -50px; + } + + &::after { + right: -50px; + } + } +} + +.action-menu-dialog { + background: white; + border-radius: 16px 16px 0 0; + padding: 20px; + max-height: 60vh; + display: flex; + flex-direction: column; +} + +.action-menu-item { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s ease; + + &:hover { + background-color: #f5f5f5; + } + + &.danger { + color: #ff4d4f; + + &:hover { + background-color: #fff2f0; + } + } +} + +.action-icon { + font-size: 16px; + width: 20px; + text-align: center; +} + +.action-text { + font-size: 16px; + font-weight: 500; +} + +.dialog-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid #f0f0f0; + + h3 { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #333; + } +} + +.dialog-content { + flex: 1; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.qr-dialog { + background: white; + border-radius: 16px; + padding: 20px; + width: 100%; +} + +.qr-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; + gap: 16px; + color: #666; + font-size: 14px; +} + +.qr-image { + width: 100%; + max-width: 200px; + height: auto; + border-radius: 8px; +} + +.qr-error { + text-align: center; + color: #ff4d4f; + font-size: 14px; + padding: 40px 20px; +} + +.qr-link-section { + margin-top: 20px; + width: 100%; + padding: 0 10px; +} + +.link-label { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 8px; + text-align: left; +} + +.link-input-wrapper { + display: flex; + gap: 8px; + align-items: center; + width: 100%; + + @media (max-width: 480px) { + flex-direction: column; + gap: 12px; + } +} + +.link-input { + flex: 1; + + .ant-input { + border-radius: 8px; + font-size: 12px; + color: #666; + background-color: #f8f9fa; + border: 1px solid #e9ecef; + + &:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + } + } + + @media (max-width: 480px) { + width: 100%; + } +} + +.copy-button { + height: 32px; + padding: 0 12px; + border-radius: 8px; + font-size: 12px; + display: flex; + align-items: center; + gap: 4px; + white-space: nowrap; + flex-shrink: 0; + + .anticon { + font-size: 12px; + } + + @media (max-width: 480px) { + width: 100%; + justify-content: center; + } +} \ No newline at end of file diff --git a/nkebao/src/pages/mobile/scenarios/plan/list/index.tsx b/nkebao/src/pages/mobile/scenarios/plan/list/index.tsx new file mode 100644 index 000000000..552f73470 --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/plan/list/index.tsx @@ -0,0 +1,611 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { + NavBar, + Button, + Toast, + SpinLoading, + Dialog, + Popup, + Card, + Tag, +} from "antd-mobile"; +import { Input } from "antd"; +import { + PlusOutlined, + CopyOutlined, + DeleteOutlined, + SettingOutlined, + SearchOutlined, + ReloadOutlined, + QrcodeOutlined, + EditOutlined, + MoreOutlined, + ClockCircleOutlined, + DownOutlined, +} from "@ant-design/icons"; +import NavCommon from "@/components/NavCommon"; +import Layout from "@/components/Layout/Layout"; +import { + getPlanList, + getPlanDetail, + copyPlan, + deletePlan, + getWxMinAppCode, +} from "./api"; +import style from "./index.module.scss"; +import { Task, ApiSettings, PlanDetail } from "./data"; +import PlanApi from "./planApi"; +import { buildApiUrl } from "@/utils/apiUrl"; + +const ScenarioList: React.FC = () => { + const { scenarioId, scenarioName } = useParams<{ + scenarioId: string; + scenarioName: string; + }>(); + const navigate = useNavigate(); + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(true); + const [showApiDialog, setShowApiDialog] = useState(false); + const [currentApiSettings, setCurrentApiSettings] = useState({ + apiKey: "", + webhookUrl: "", + taskId: "", + }); + const [searchTerm, setSearchTerm] = useState(""); + const [loadingTasks, setLoadingTasks] = useState(false); + const [showQrDialog, setShowQrDialog] = useState(false); + const [qrLoading, setQrLoading] = useState(false); + const [qrImg, setQrImg] = useState(""); + const [currentTaskId, setCurrentTaskId] = useState(""); + const [showActionMenu, setShowActionMenu] = useState(null); + + // 分页相关状态 + const [currentPage, setCurrentPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [total, setTotal] = useState(0); + const pageSize = 20; + + // 获取渠道中文名称 + const getChannelName = (channel: string) => { + const channelMap: Record = { + douyin: "抖音直播获客", + kuaishou: "快手直播获客", + xiaohongshu: "小红书种草获客", + weibo: "微博话题获客", + haibao: "海报扫码获客", + phone: "电话号码获客", + gongzhonghao: "公众号引流获客", + weixinqun: "微信群裂变获客", + payment: "付款码获客", + api: "API接口获客", + }; + return channelMap[channel] || `${channel}获客`; + }; + + // 获取计划列表数据 + const fetchPlanList = async (page: number, isLoadMore: boolean = false) => { + if (!scenarioId) return; + + if (isLoadMore) { + setLoadingMore(true); + } else { + setLoadingTasks(true); + } + + try { + const response = await getPlanList({ + sceneId: scenarioId, + page: page, + pageSize: pageSize, + }); + + if (response && response.list) { + if (isLoadMore) { + // 加载更多时,追加数据 + setTasks((prev) => [...prev, ...response.list]); + } else { + // 首次加载或刷新时,替换数据 + setTasks(response.list); + } + + // 更新分页信息 + setTotal(response.total || 0); + setHasMore(response.list.length === pageSize); + setCurrentPage(page); + } + } catch (error) { + console.error("获取计划列表失败:", error); + if (!isLoadMore) { + setTasks([]); + } + Toast.show({ + content: "获取数据失败", + position: "top", + }); + } finally { + if (isLoadMore) { + setLoadingMore(false); + } else { + setLoadingTasks(false); + } + } + }; + + useEffect(() => { + const fetchScenarioData = async () => { + if (!scenarioId) return; + setLoading(true); + + try { + await fetchPlanList(1, false); + } catch (error) { + console.error("获取场景数据失败:", error); + setTasks([]); + } finally { + setLoading(false); + } + }; + + fetchScenarioData(); + }, [scenarioId]); + + // 加载下一页数据 + const handleLoadMore = async () => { + if (loadingMore || !hasMore) return; + await fetchPlanList(currentPage + 1, true); + }; + + const handleCopyPlan = async (taskId: string) => { + const taskToCopy = tasks.find((task) => task.id === taskId); + if (!taskToCopy) return; + + try { + await copyPlan(taskId); + Toast.show({ + content: `已成功复制"${taskToCopy.name}"`, + position: "top", + }); + // 刷新列表 + handleRefresh(); + } catch (error) { + Toast.show({ + content: "复制失败,请重试", + position: "top", + }); + } + }; + + const handleDeletePlan = async (taskId: string) => { + const taskToDelete = tasks.find((task) => task.id === taskId); + if (!taskToDelete) return; + + const result = await Dialog.confirm({ + content: `确定要删除"${taskToDelete.name}"吗?`, + confirmText: "删除", + cancelText: "取消", + }); + + if (result) { + try { + await deletePlan(taskId); + Toast.show({ + content: "计划已删除", + position: "top", + }); + // 刷新列表 + handleRefresh(); + } catch (error) { + Toast.show({ + content: "删除失败,请重试", + position: "top", + }); + } + } + }; + + const handleOpenApiSettings = async (taskId: string) => { + try { + const response: PlanDetail = await getPlanDetail(taskId); + if (response) { + // 处理webhook URL,使用工具函数构建完整地址 + const webhookUrl = buildApiUrl( + response.textUrl?.fullUrl || `webhook/${taskId}` + ); + + setCurrentApiSettings({ + apiKey: response.apiKey || "demo-api-key-123456", + webhookUrl: webhookUrl, + taskId: taskId, + }); + setShowApiDialog(true); + } + } catch (error) { + Toast.show({ + content: "获取计划接口失败", + position: "top", + }); + } + }; + + const handleCreateNewPlan = () => { + navigate(`/scenarios/new/${scenarioId}`); + }; + + const handleShowQrCode = async (taskId: string) => { + setQrLoading(true); + setShowQrDialog(true); + setQrImg(""); + setCurrentTaskId(taskId); // 设置当前任务ID + + try { + const response = await getWxMinAppCode(taskId); + setQrImg(response); + } catch (error) { + Toast.show({ + content: "获取二维码失败", + position: "top", + }); + } finally { + setQrLoading(false); + } + }; + + const getStatusColor = (status: number) => { + switch (status) { + case 1: + return "success"; + case 0: + return "default"; + case -1: + return "danger"; + default: + return "default"; + } + }; + + const getStatusText = (status: number) => { + switch (status) { + case 1: + return "进行中"; + case 0: + return "已暂停"; + case -1: + return "已停止"; + default: + return "未知"; + } + }; + + const handleRefresh = async () => { + // 重置分页状态 + setCurrentPage(1); + setHasMore(true); + await fetchPlanList(1, false); + }; + + const filteredTasks = tasks.filter((task) => + task.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + // 生成操作菜单 + const getActionMenu = (task: Task) => [ + { + key: "edit", + text: "编辑计划", + icon: , + onClick: () => { + setShowActionMenu(null); + navigate(`/scenarios/edit/${task.id}`); + }, + }, + { + key: "copy", + text: "复制计划", + icon: , + onClick: () => { + setShowActionMenu(null); + handleCopyPlan(task.id); + }, + }, + { + key: "settings", + text: "计划接口", + icon: , + onClick: () => { + setShowActionMenu(null); + handleOpenApiSettings(task.id); + }, + }, + { + key: "qrcode", + text: "二维码", + icon: , + onClick: () => { + setShowActionMenu(null); + handleShowQrCode(task.id); + }, + }, + { + key: "delete", + text: "删除计划", + icon: , + onClick: () => { + setShowActionMenu(null); + handleDeletePlan(task.id); + }, + danger: true, + }, + ]; + + const deviceCount = (task: Task) => { + return Array.isArray(task.reqConf?.device) + ? task.reqConf!.device.length + : Array.isArray(task.reqConf?.selectedDevices) + ? task.reqConf!.selectedDevices.length + : 0; + }; + + return ( + + navigate("/scenarios")} + title={scenarioName || ""} + right={ + + } + /> + + {/* 搜索栏 */} +
+
+ setSearchTerm(e.target.value)} + prefix={} + allowClear + size="large" + /> +
+ +
+ + } + loading={loading} + > +
+ {/* 计划列表 */} +
+ {filteredTasks.length === 0 ? ( +
+
+ {searchTerm ? "没有找到匹配的计划" : "暂无计划"} +
+ +
+ ) : ( + <> + {filteredTasks.map((task) => ( + + {/* 头部:标题、状态和操作菜单 */} +
+
{task.name}
+
+ + {getStatusText(task.status)} + + +
+
+ + {/* 统计数据网格 */} +
+
+
设备数
+
+ {deviceCount(task)} +
+
+
+
已获客
+
+ {task?.acquiredCount || 0} +
+
+
+
已添加
+
+ {task.addedCount || 0} +
+
+
+
通过率
+
+ {task.passRate}% +
+
+
+ + {/* 底部:上次执行时间 */} +
+
+ + 上次执行: {task.lastUpdated || "--"} +
+
+
+ ))} + + {/* 加载更多按钮 */} + {hasMore && ( +
+ +
+ )} + + {/* 没有更多数据提示 */} + {!hasMore && filteredTasks.length > 0 && ( +
+ 没有更多数据了 +
+ )} + + )} +
+ + {/* 计划接口弹窗 */} + setShowApiDialog(false)} + apiKey={currentApiSettings.apiKey} + webhookUrl={currentApiSettings.webhookUrl} + taskId={currentApiSettings.taskId} + /> + + {/* 操作菜单弹窗 */} + setShowActionMenu(null)} + position="bottom" + bodyStyle={{ height: "auto", maxHeight: "60vh" }} + > +
+
+

操作菜单

+ +
+
+ {showActionMenu && + getActionMenu(tasks.find((t) => t.id === showActionMenu)!).map( + (item) => ( +
+ {item.icon} + {item.text} +
+ ) + )} +
+
+
+ + {/* 二维码弹窗 */} + setShowQrDialog(false)} + position="bottom" + > +
+
+

小程序二维码

+ +
+
+ {qrLoading ? ( +
+ +
生成二维码中...
+
+ ) : qrImg ? ( + <> + 小程序二维码 + {/* 链接复制区域 */} +
+
小程序链接
+
+ + +
+
+ + ) : ( +
二维码生成失败
+ )} +
+
+
+
+
+ ); +}; + +export default ScenarioList; diff --git a/nkebao/src/pages/mobile/scenarios/plan/list/planApi.module.scss b/nkebao/src/pages/mobile/scenarios/plan/list/planApi.module.scss new file mode 100644 index 000000000..da57e0d62 --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/plan/list/planApi.module.scss @@ -0,0 +1,601 @@ +// 移动端样式 +.plan-api-dialog { + background: white; + border-radius: 16px 16px 0 0; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.dialog-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 20px; + border-bottom: 1px solid #f0f0f0; + background: #fafafa; + + .header-left { + display: flex; + align-items: flex-start; + gap: 12px; + flex: 1; + } + + .header-icon { + font-size: 24px; + color: #1890ff; + margin-top: 4px; + } + + .header-content { + flex: 1; + + h3 { + margin: 0 0 8px 0; + font-size: 18px; + font-weight: 600; + color: #333; + } + + p { + margin: 0; + font-size: 14px; + color: #666; + line-height: 1.5; + } + } + + .close-btn { + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + color: #999; + background: transparent; + border: none; + cursor: pointer; + + &:hover { + background: #f5f5f5; + } + } +} + +.nav-tabs { + display: flex; + background: white; + border-bottom: 1px solid #f0f0f0; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + + .nav-tab { + flex: 1; + min-width: 80px; + padding: 12px 8px; + border: none; + background: transparent; + color: #666; + font-size: 14px; + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + transition: all 0.2s ease; + white-space: nowrap; + + svg { + font-size: 16px; + } + + &:hover { + color: #1890ff; + } + + &.active { + color: #1890ff; + border-bottom: 2px solid #1890ff; + } + } +} + +.dialog-content { + flex: 1; + overflow-y: auto; + padding: 20px; +} + +.dialog-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-top: 1px solid #f0f0f0; + background: #fafafa; + + .security-note { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: #666; + + svg { + color: #52c41a; + } + } + + .complete-btn { + height: 36px; + padding: 0 24px; + border-radius: 18px; + } +} + +// 配置内容样式 +.config-content { + .config-section { + margin-bottom: 24px; + + &:last-child { + margin-bottom: 0; + } + } + + .section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + + .section-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 16px; + font-weight: 600; + color: #333; + + .section-icon { + color: #1890ff; + } + } + } + + .input-group { + display: flex; + gap: 8px; + margin-bottom: 12px; + + .api-input { + flex: 1; + border-radius: 8px; + } + + .copy-btn { + height: 40px; + padding: 0 16px; + border-radius: 8px; + display: flex; + align-items: center; + gap: 4px; + } + } + + .security-tip { + padding: 12px; + background: #fff7e6; + border: 1px solid #ffd591; + border-radius: 8px; + font-size: 12px; + color: #d46b08; + line-height: 1.5; + } + + .params-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + margin-top: 16px; + } + + .param-section { + background: #f8f9fa; + border-radius: 8px; + padding: 12px; + border: 1px solid #e9ecef; + + h4 { + margin: 0 0 8px 0; + font-size: 14px; + font-weight: 600; + color: #333; + } + + .param-list { + font-size: 12px; + color: #666; + line-height: 1.6; + + div { + margin-bottom: 4px; + } + + code { + background: #e9ecef; + padding: 2px 4px; + border-radius: 4px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 11px; + } + } + } +} + +// 测试内容样式 +.test-content { + .test-section { + h3 { + margin: 0 0 16px 0; + font-size: 16px; + font-weight: 600; + color: #333; + } + + .test-input { + margin-bottom: 16px; + border-radius: 8px; + } + + .test-buttons { + display: flex; + gap: 12px; + + .test-btn { + flex: 1; + height: 40px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + } + } + } +} + +// 文档内容样式 +.docs-content { + .docs-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + } + + .doc-card { + text-align: center; + padding: 24px 16px; + border-radius: 12px; + border: 1px solid #f0f0f0; + transition: all 0.2s ease; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + } + + .doc-icon { + width: 48px; + height: 48px; + border-radius: 50%; + background: #f0f8ff; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 16px; + font-size: 24px; + color: #1890ff; + } + + h4 { + margin: 0 0 8px 0; + font-size: 16px; + font-weight: 600; + color: #333; + } + + p { + margin: 0; + font-size: 14px; + color: #666; + line-height: 1.5; + } + } +} + +// 代码内容样式 +.code-content { + .language-tabs { + display: flex; + gap: 8px; + margin-bottom: 16px; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + + .lang-tab { + padding: 8px 16px; + border: 1px solid #d9d9d9; + background: white; + border-radius: 6px; + font-size: 14px; + color: #666; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + + &:hover { + border-color: #1890ff; + color: #1890ff; + } + + &.active { + background: #1890ff; + border-color: #1890ff; + color: white; + } + } + } + + .code-block { + position: relative; + background: #f6f8fa; + border-radius: 8px; + border: 1px solid #e1e4e8; + overflow: hidden; + + .code { + margin: 0; + padding: 16px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 13px; + line-height: 1.5; + color: #24292e; + overflow-x: auto; + white-space: pre-wrap; + word-break: break-all; + } + + .copy-code-btn { + position: absolute; + top: 8px; + right: 8px; + height: 32px; + padding: 0 12px; + border-radius: 6px; + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + } + } +} + +// PC端样式覆盖 +.plan-api-modal { + .plan-api-dialog { + border-radius: 12px; + height: auto; + max-height: 80vh; + } + + .nav-tabs { + .nav-tab { + min-width: 100px; + padding: 16px 12px; + font-size: 15px; + + svg { + font-size: 18px; + } + } + } + + .dialog-content { + padding: 24px; + } + + .params-grid { + grid-template-columns: 1fr 1fr; + } + + .docs-grid { + grid-template-columns: 1fr 1fr; + } + + .test-buttons { + flex-direction: row; + } +} + +// 响应式设计 +@media (max-width: 768px) { + .plan-api-dialog { + .header-content { + h3 { + font-size: 16px; + } + + p { + font-size: 13px; + } + } + } + + .nav-tabs { + .nav-tab { + font-size: 13px; + padding: 10px 6px; + + svg { + font-size: 14px; + } + } + } + + .dialog-content { + padding: 16px; + } + + .config-content { + .params-grid { + grid-template-columns: 1fr; + gap: 8px; + } + } + + .docs-content { + .docs-grid { + grid-template-columns: 1fr; + gap: 12px; + } + } + + .test-content { + .test-buttons { + flex-direction: column; + gap: 8px; + } + } + + .code-content { + .language-tabs { + .lang-tab { + padding: 6px 12px; + font-size: 13px; + } + } + + .code-block { + .code { + font-size: 12px; + padding: 12px; + } + } + } +} + +// 暗色主题支持 +@media (prefers-color-scheme: dark) { + .plan-api-dialog { + background: #1f1f1f; + color: #fff; + + .dialog-header { + background: #262626; + border-bottom-color: #434343; + + .header-content { + h3 { + color: #fff; + } + + p { + color: #a6a6a6; + } + } + } + + .nav-tabs { + background: #262626; + border-bottom-color: #434343; + + .nav-tab { + color: #a6a6a6; + + &:hover { + color: #1890ff; + } + + &.active { + color: #1890ff; + border-bottom-color: #1890ff; + } + } + } + + .dialog-footer { + background: #262626; + border-top-color: #434343; + + .security-note { + color: #a6a6a6; + } + } + } + + .config-content { + .section-title { + color: #fff; + } + + .security-tip { + background: #2a1f00; + border-color: #d48806; + color: #ffc53d; + } + + .param-section { + background: #262626; + border-color: #434343; + + h4 { + color: #fff; + } + + .param-list { + color: #a6a6a6; + + code { + background: #434343; + } + } + } + } + + .test-content { + h3 { + color: #fff; + } + } + + .docs-content { + .doc-card { + background: #262626; + border-color: #434343; + + h4 { + color: #fff; + } + + p { + color: #a6a6a6; + } + } + } + + .code-content { + .code-block { + background: #0d1117; + border-color: #30363d; + + .code { + color: #c9d1d9; + } + } + } +} \ No newline at end of file diff --git a/nkebao/src/pages/mobile/scenarios/plan/list/planApi.tsx b/nkebao/src/pages/mobile/scenarios/plan/list/planApi.tsx new file mode 100644 index 000000000..7fc94b1ae --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/plan/list/planApi.tsx @@ -0,0 +1,437 @@ +import React, { useState, useMemo } from "react"; +import { Popup, Button, Toast, SpinLoading } from "antd-mobile"; +import { Modal, Input, Tabs, Card, Tag, Space } from "antd"; +import { + CopyOutlined, + CodeOutlined, + BookOutlined, + ThunderboltOutlined, + SettingOutlined, + LinkOutlined, + SafetyOutlined, + CheckCircleOutlined, +} from "@ant-design/icons"; +import style from "./planApi.module.scss"; +import { buildApiUrl } from "@/utils/apiUrl"; + +/** + * 计划接口配置弹窗组件 + * + * 使用示例: + * ```tsx + * const [showApiDialog, setShowApiDialog] = useState(false); + * const [apiSettings, setApiSettings] = useState({ + * apiKey: "your-api-key", + * webhookUrl: "https://api.example.com/webhook", + * taskId: "task-123" + * }); + * + * setShowApiDialog(false)} + * apiKey={apiSettings.apiKey} + * webhookUrl={apiSettings.webhookUrl} + * taskId={apiSettings.taskId} + * /> + * ``` + * + * 特性: + * - 移动端使用 Popup,PC端使用 Modal + * - 支持四个标签页:接口配置、快速测试、开发文档、代码示例 + * - 支持多种编程语言的代码示例 + * - 响应式设计,自适应不同屏幕尺寸 + * - 支持暗色主题 + * - 自动拼接API地址前缀 + */ + +interface PlanApiProps { + visible: boolean; + onClose: () => void; + apiKey: string; + webhookUrl: string; + taskId: string; +} + +interface ApiSettings { + apiKey: string; + webhookUrl: string; + taskId: string; +} + +const PlanApi: React.FC = ({ + visible, + onClose, + apiKey, + webhookUrl, + taskId, +}) => { + const [activeTab, setActiveTab] = useState("config"); + const [activeLanguage, setActiveLanguage] = useState("javascript"); + + // 处理webhook URL,确保包含完整的API地址 + const fullWebhookUrl = useMemo(() => { + return buildApiUrl(webhookUrl); + }, [webhookUrl]); + + // 生成测试URL + const testUrl = useMemo(() => { + if (!fullWebhookUrl) return ""; + return `${fullWebhookUrl}?name=测试客户&phone=13800138000&source=API测试`; + }, [fullWebhookUrl]); + + // 检测是否为移动端 + const isMobile = window.innerWidth <= 768; + + const handleCopy = (text: string, type: string) => { + navigator.clipboard.writeText(text); + Toast.show({ + content: `${type}已复制到剪贴板`, + position: "top", + }); + }; + + const handleTestInBrowser = () => { + window.open(testUrl, "_blank"); + }; + + const renderConfigTab = () => ( +
+ {/* API密钥配置 */} +
+
+
+ + API密钥 +
+ 安全认证 +
+
+ + +
+
+ 安全提示: + 请妥善保管API密钥,不要在客户端代码中暴露。建议在服务器端使用该密钥。 +
+
+ + {/* 接口地址配置 */} +
+
+
+ + 接口地址 +
+ POST请求 +
+
+ + +
+ + {/* 参数说明 */} +
+
+

必要参数

+
+
+ name - 客户姓名 +
+
+ phone - 手机号码 +
+
+
+
+

可选参数

+
+
+ source - 来源标识 +
+
+ remark - 备注信息 +
+
+ tags - 客户标签 +
+
+
+
+
+
+ ); + + const renderQuickTestTab = () => ( +
+
+

快速测试URL

+
+ +
+
+ + +
+
+
+ ); + + const renderDocsTab = () => ( +
+
+ +
+ +
+

完整API文档

+

详细的接口说明和参数文档

+
+ +
+ +
+

集成指南

+

第三方平台集成教程

+
+
+
+ ); + + const renderCodeTab = () => { + const codeExamples = { + javascript: `fetch('${fullWebhookUrl}', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ${apiKey}' + }, + body: JSON.stringify({ + name: '张三', + phone: '13800138000', + source: '官网表单', + }) +})`, + python: `import requests + +url = '${fullWebhookUrl}' +headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ${apiKey}' +} +data = { + 'name': '张三', + 'phone': '13800138000', + 'source': '官网表单' +} + +response = requests.post(url, json=data, headers=headers)`, + php: ` '张三', + 'phone' => '13800138000', + 'source' => '官网表单' +); + +$options = array( + 'http' => array( + 'header' => "Content-type: application/json\\r\\nAuthorization: Bearer ${apiKey}\\r\\n", + 'method' => 'POST', + 'content' => json_encode($data) + ) +); + +$context = stream_context_create($options); +$result = file_get_contents($url, false, $context);`, + java: `import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.URI; + +HttpClient client = HttpClient.newHttpClient(); +String json = "{\\"name\\":\\"张三\\",\\"phone\\":\\"13800138000\\",\\"source\\":\\"官网表单\\"}"; + +HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("${fullWebhookUrl}")) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer ${apiKey}") + .POST(HttpRequest.BodyPublishers.ofString(json)) + .build(); + +HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());`, + }; + + return ( +
+
+ {Object.keys(codeExamples).map((lang) => ( + + ))} +
+
+
+            
+              {codeExamples[activeLanguage as keyof typeof codeExamples]}
+            
+          
+ +
+
+ ); + }; + + const renderContent = () => ( +
+ {/* 头部 */} +
+
+ +
+

计划接口配置

+

+ 通过API接口直接导入客资到该获客计划,支持多种编程语言和第三方平台集成 +

+
+
+ +
+ + {/* 导航标签 */} +
+ + + + +
+ + {/* 内容区域 */} +
+ {activeTab === "config" && renderConfigTab()} + {activeTab === "test" && renderQuickTestTab()} + {activeTab === "docs" && renderDocsTab()} + {activeTab === "code" && renderCodeTab()} +
+ + {/* 底部 */} +
+
+ + 所有数据传输均采用HTTPS加密 +
+ +
+
+ ); + + // 移动端使用Popup + if (isMobile) { + return ( + + {renderContent()} + + ); + } + + // PC端使用Modal + return ( + + {renderContent()} + + ); +}; + +export default PlanApi; diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/index.api.ts b/nkebao/src/pages/mobile/scenarios/plan/new/index.api.ts new file mode 100644 index 000000000..0eb6c2319 --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/plan/new/index.api.ts @@ -0,0 +1,53 @@ +import request from "@/api/request"; +// 获取场景类型列表 +export function getScenarioTypes() { + return request("/v1/plan/scenes", undefined, "GET"); +} + +// 创建计划 +export function createPlan(data: any) { + return request("/v1/plan/create", data, "POST"); +} + +// 更新计划 +export function updatePlan(planId: string, data: any) { + return request(`/v1/scenarios/plans/${planId}`, data, "PUT"); +} + +// 获取计划详情 +export function getPlanDetail(planId: string) { + return request(`/v1/scenarios/plans/${planId}`, undefined, "GET"); +} + +// PlanDetail 类型定义(可根据实际接口返回结构补充字段) +export interface PlanDetail { + name: string; + scenario: number; + posters: any[]; + device: string[]; + remarkType: string; + greeting: string; + addInterval: number; + startTime: string; + endTime: string; + enabled: boolean; + sceneId: string | number; + remarkFormat: string; + addFriendInterval: number; + // 其它字段可扩展 + [key: string]: any; +} + +// 兼容旧代码的接口命名 +export function getPlanScenes() { + return getScenarioTypes(); +} +export function createScenarioPlan(data: any) { + return createPlan(data); +} +export function fetchPlanDetail(planId: string) { + return getPlanDetail(planId); +} +export function updateScenarioPlan(planId: string, data: any) { + return updatePlan(planId, data); +} diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/index.tsx b/nkebao/src/pages/mobile/scenarios/plan/new/index.tsx new file mode 100644 index 000000000..dc1362d03 --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/plan/new/index.tsx @@ -0,0 +1,217 @@ +import { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { message } from "antd"; +import NavCommon from "@/components/NavCommon"; +import BasicSettings from "./steps/BasicSettings"; +import FriendRequestSettings from "./steps/FriendRequestSettings"; +import MessageSettings from "./steps/MessageSettings"; +import Layout from "@/components/Layout/Layout"; +import StepIndicator from "@/components/StepIndicator"; +import { + getScenarioTypes, + createPlan, + getPlanDetail, + updatePlan, +} from "./index.api"; + +// 步骤定义 - 只保留三个步骤 +const steps = [ + { id: 1, title: "步骤一", subtitle: "基础设置" }, + { id: 2, title: "步骤二", subtitle: "好友申请设置" }, + { id: 3, title: "步骤三", subtitle: "消息设置" }, +]; + +// 类型定义 +interface FormData { + name: string; + scenario: number; + posters: any[]; // 后续可替换为具体Poster类型 + device: string[]; + remarkType: string; + greeting: string; + addInterval: number; + startTime: string; + endTime: string; + enabled: boolean; + sceneId: string | number; + remarkFormat: string; + addFriendInterval: number; +} + +export default function NewPlan() { + const router = useNavigate(); + const [currentStep, setCurrentStep] = useState(1); + const [formData, setFormData] = useState({ + name: "", + scenario: 1, + posters: [], + device: [], + remarkType: "phone", + greeting: "你好,请通过", + addInterval: 1, + startTime: "09:00", + endTime: "18:00", + enabled: true, + sceneId: "", + remarkFormat: "", + addFriendInterval: 1, + }); + const [sceneList, setSceneList] = useState([]); + const [sceneLoading, setSceneLoading] = useState(true); + const { scenarioId, planId } = useParams<{ + scenarioId: string; + planId: string; + }>(); + const [isEdit, setIsEdit] = useState(false); + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + setSceneLoading(true); + //获取场景类型 + getScenarioTypes() + .then((data) => { + setSceneList(data || []); + }) + .catch((err) => { + message.error(err.message || "获取场景类型失败"); + }) + .finally(() => setSceneLoading(false)); + if (planId) { + setIsEdit(true); + //获取计划详情 + + const detail = await getPlanDetail(planId); + setFormData((prev) => ({ + ...prev, + name: detail.name ?? "", + scenario: Number(detail.scenario) || 1, + posters: detail.posters ?? [], + device: detail.device ?? [], + remarkType: detail.remarkType ?? "phone", + greeting: detail.greeting ?? "", + addInterval: detail.addInterval ?? 1, + startTime: detail.startTime ?? "09:00", + endTime: detail.endTime ?? "18:00", + enabled: detail.enabled ?? true, + sceneId: Number(detail.scenario) || 1, + remarkFormat: detail.remarkFormat ?? "", + addFriendInterval: detail.addFriendInterval ?? 1, + tips: detail.tips ?? "", + })); + } else { + if (scenarioId) { + setFormData((prev) => ({ + ...prev, + ...{ scenario: Number(scenarioId) || 1 }, + })); + } + } + }; + + // 更新表单数据 + const onChange = (data: any) => { + setFormData((prev) => ({ ...prev, ...data })); + }; + + // 处理保存 + const handleSave = async () => { + try { + let result; + if (isEdit && planId) { + // 编辑:拼接后端需要的完整参数 + const editData = { + ...formData, + ...{ sceneId: Number(formData.scenario) }, + id: Number(planId), + planId: Number(planId), + // 兼容后端需要的字段 + // 你可以根据实际需要补充其它字段 + }; + result = await updatePlan(planId, editData); + } else { + // 新建 + formData.sceneId = Number(formData.scenario); + result = await createPlan(formData); + } + message.success(isEdit ? "计划已更新" : "获客计划已创建"); + const sceneItem = sceneList.find((v) => formData.scenario === v.id); + router(`/scenarios/list/${formData.scenario}/${sceneItem.name}`); + } catch (error) { + message.error( + error instanceof Error + ? error.message + : typeof error === "string" + ? error + : isEdit + ? "更新计划失败,请重试" + : "创建计划失败,请重试" + ); + } + }; + + // 下一步 + const handleNext = () => { + if (currentStep === steps.length) { + handleSave(); + } else { + setCurrentStep((prev) => prev + 1); + } + }; + + // 上一步 + const handlePrev = () => { + setCurrentStep((prev) => Math.max(prev - 1, 1)); + }; + + // 渲染当前步骤内容 + const renderStepContent = () => { + switch (currentStep) { + case 1: + return ( + + ); + case 2: + return ( + + ); + case 3: + return ( + + ); + default: + return null; + } + }; + + return ( + + + + + } + > + {renderStepContent()} + + ); +} diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx b/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx new file mode 100644 index 000000000..e2d0ba35f --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx @@ -0,0 +1,774 @@ +import React, { useState, useEffect, useRef } from "react"; +import { Form, Input, Button, Tag, Switch, Modal, Spin } from "antd"; +import { Button as ButtonMobile } from "antd-mobile"; +import { + PlusOutlined, + EyeOutlined, + CloseOutlined, + DownloadOutlined, + UploadOutlined, + CheckOutlined, +} from "@ant-design/icons"; +import { uploadFile } from "@/api/common"; +import styles from "./base.module.scss"; + +interface BasicSettingsProps { + isEdit: boolean; + formData: any; + onChange: (data: any) => void; + onNext?: () => void; + sceneList: any[]; + sceneLoading: boolean; +} + +interface Account { + id: string; + nickname: string; + avatar: string; +} + +interface Material { + id: string; + name: string; + type: string; + preview: string; +} + +const posterTemplates = [ + { + id: "poster-1", + name: "点击领取", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E9%A2%86%E5%8F%961-tipd1HI7da6qooY5NkhxQnXBnT5LGU.gif", + }, + { + id: "poster-2", + name: "点击合作", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%90%88%E4%BD%9C-LPlMdgxtvhqCSr4IM1bZFEFDBF3ztI.gif", + }, + { + id: "poster-3", + name: "点击咨询", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%92%A8%E8%AF%A2-FTiyAMAPop2g9LvjLOLDz0VwPg3KVu.gif", + }, + { + id: "poster-4", + name: "点击签到", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E7%AD%BE%E5%88%B0-94TZIkjLldb4P2jTVlI6MkSDg0NbXi.gif", + }, + { + id: "poster-5", + name: "点击了解", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E4%BA%86%E8%A7%A3-6GCl7mQVdO4WIiykJyweSubLsTwj71.gif", + }, + { + id: "poster-6", + name: "点击报名", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E6%8A%A5%E5%90%8D-Mj0nnva0BiASeDAIhNNaRRAbjPgjEj.gif", + }, +]; + +const generateRandomAccounts = (count: number): Account[] => { + return Array.from({ length: count }, (_, index) => ({ + id: `account-${index + 1}`, + nickname: `账号-${Math.random().toString(36).substring(2, 7)}`, + avatar: `/placeholder.svg?height=40&width=40&text=${index + 1}`, + })); +}; + +const generatePosterMaterials = (): Material[] => { + return posterTemplates.map((template) => ({ + id: template.id, + name: template.name, + type: "poster", + preview: template.preview, + })); +}; + +const BasicSettings: React.FC = ({ + isEdit, + formData, + onChange, + onNext, + sceneList, + sceneLoading, +}) => { + const [isAccountDialogOpen, setIsAccountDialogOpen] = useState(false); + const [isMaterialDialogOpen, setIsMaterialDialogOpen] = useState(false); + const [isPreviewOpen, setIsPreviewOpen] = useState(false); + const [isPhoneSettingsOpen, setIsPhoneSettingsOpen] = useState(false); + const [accounts] = useState(generateRandomAccounts(50)); + const [materials] = useState(generatePosterMaterials()); + const [selectedAccounts, setSelectedAccounts] = useState( + formData.accounts?.length > 0 ? formData.accounts : [] + ); + const [selectedMaterials, setSelectedMaterials] = useState( + formData.materials?.length > 0 ? formData.materials : [] + ); + // showAllScenarios 默认为 true + const [showAllScenarios, setShowAllScenarios] = useState(true); + const [isImportDialogOpen, setIsImportDialogOpen] = useState(false); + const [importedTags, setImportedTags] = useState< + Array<{ + phone: string; + wechat: string; + source?: string; + orderAmount?: number; + orderDate?: string; + }> + >(formData.importedTags || []); + + // 自定义标签相关状态 + const [customTagInput, setCustomTagInput] = useState(""); + const [customTags, setCustomTags] = useState(formData.customTags || []); + const [tips, setTips] = useState(formData.tips || ""); + const [selectedScenarioTags, setSelectedScenarioTags] = useState( + formData.scenarioTags || [] + ); + + // 电话获客相关状态 + const [phoneSettings, setPhoneSettings] = useState({ + autoAdd: formData.phoneSettings?.autoAdd ?? true, + speechToText: formData.phoneSettings?.speechToText ?? true, + questionExtraction: formData.phoneSettings?.questionExtraction ?? true, + }); + + // 群设置相关状态 + const [weixinqunName, setWeixinqunName] = useState( + formData.weixinqunName || "" + ); + const [weixinqunNotice, setWeixinqunNotice] = useState( + formData.weixinqunNotice || "" + ); + + // 新增:自定义海报相关状态 + const [customPosters, setCustomPosters] = useState([]); + const [previewUrl, setPreviewUrl] = useState(null); + + // 新增:用于文件选择的ref + const uploadInputRef = useRef(null); + const uploadOrderInputRef = useRef(null); + + // 更新电话获客设置 + const handlePhoneSettingsUpdate = () => { + onChange({ ...formData, phoneSettings }); + setIsPhoneSettingsOpen(false); + }; + + // 处理标签选择 + const handleTagToggle = (tagId: string) => { + const newTags = selectedScenarioTags.includes(tagId) + ? selectedScenarioTags.filter((id: string) => id !== tagId) + : [...selectedScenarioTags, tagId]; + + setSelectedScenarioTags(newTags); + onChange({ ...formData, scenarioTags: newTags }); + }; + + // 处理通话类型选择 + const handleCallTypeChange = (type: string) => { + // setPhoneCallType(type) // This line was removed as per the edit hint. + onChange({ ...formData, phoneCallType: type }); + }; + + // 初始化时,如果没有选择场景,默认选择海报获客 + useEffect(() => { + if (!formData.scenario) { + onChange({ ...formData, scenario: "haibao" }); + } + + // 检查是否已经有上传的订单文件 + if (formData.orderFileUploaded) { + setOrderUploaded(true); + } + }, [formData, onChange]); + + useEffect(() => { + const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, ""); + const sceneItem = sceneList.find((v) => formData.scenario === v.id); + onChange({ ...formData, name: `${sceneItem?.name || "海报"}${today}` }); + }, [isEdit]); + + useEffect(() => { + setTips(formData.tips || ""); + }, [formData.tips]); + + // 选中场景 + const handleScenarioSelect = (sceneId: number) => { + onChange({ ...formData, scenario: sceneId }); + }; + + // 选中/取消标签 + const handleScenarioTagToggle = (tag: string) => { + const newTags = selectedScenarioTags.includes(tag) + ? selectedScenarioTags.filter((t: string) => t !== tag) + : [...selectedScenarioTags, tag]; + setSelectedScenarioTags(newTags); + onChange({ ...formData, scenarioTags: newTags }); + }; + + // 添加自定义标签 + const handleAddCustomTag = () => { + if (!customTagInput.trim()) return; + const newTag = { + id: `custom-${Date.now()}`, + name: customTagInput.trim(), + }; + const updatedCustomTags = [...customTags, newTag]; + setCustomTags(updatedCustomTags); + setCustomTagInput(""); + onChange({ ...formData, customTags: updatedCustomTags }); + }; + + // 删除自定义标签 + const handleRemoveCustomTag = (tagId: string) => { + const updatedCustomTags = customTags.filter((tag: any) => tag.id !== tagId); + setCustomTags(updatedCustomTags); + onChange({ ...formData, customTags: updatedCustomTags }); + // 同时从选中标签中移除 + const updatedSelectedTags = selectedScenarioTags.filter( + (t: string) => t !== tagId + ); + setSelectedScenarioTags(updatedSelectedTags); + onChange({ + ...formData, + scenarioTags: updatedSelectedTags, + customTags: updatedCustomTags, + }); + }; + + // 新增:自定义上传图片 + const handleCustomPosterUpload = (urls: string[]) => { + if (urls && urls.length > 0) { + const newPoster: Material = { + id: `custom-${Date.now()}`, + name: "自定义海报", + type: "poster", + preview: urls[0], + }; + setCustomPosters((prev) => [...prev, newPoster]); + } + }; + + // 新增:删除自定义海报 + const handleRemoveCustomPoster = (id: string) => { + setCustomPosters((prev) => prev.filter((p) => p.id !== id)); + // 如果选中则取消选中 + if (selectedMaterials.some((m) => m.id === id)) { + setSelectedMaterials([]); + onChange({ ...formData, materials: [] }); + } + }; + + // 修改:选中/取消选中海报 + const handleMaterialSelect = (material: Material) => { + const isSelected = selectedMaterials.some((m) => m.id === material.id); + if (isSelected) { + setSelectedMaterials([]); + onChange({ ...formData, materials: [] }); + } else { + setSelectedMaterials([material]); + onChange({ ...formData, materials: [material] }); + } + }; + + // 移除已选素材 + const handleRemoveMaterial = (id: string) => { + setSelectedMaterials([]); + onChange({ ...formData, materials: [] }); + }; + + // 新增:全屏预览 + const handlePreviewImage = (url: string) => { + setPreviewUrl(url); + setIsPreviewOpen(true); + }; + + // 账号多选切换 + const handleAccountToggle = (account: Account) => { + const isSelected = selectedAccounts.some( + (a: Account) => a.id === account.id + ); + let newSelected; + if (isSelected) { + newSelected = selectedAccounts.filter( + (a: Account) => a.id !== account.id + ); + } else { + newSelected = [...selectedAccounts, account]; + } + setSelectedAccounts(newSelected); + onChange({ ...formData, accounts: newSelected }); + }; + + // 移除已选账号 + const handleRemoveAccount = (id: string) => { + const newSelected = selectedAccounts.filter((a: Account) => a.id !== id); + setSelectedAccounts(newSelected); + onChange({ ...formData, accounts: newSelected }); + }; + + // 处理文件导入 + const handleFileImport = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const content = e.target?.result as string; + const rows = content.split("\n").filter((row) => row.trim()); + const tags = rows.slice(1).map((row) => { + const [phone, wechat, source, orderAmount, orderDate] = + row.split(","); + return { + phone: phone?.trim(), + wechat: wechat?.trim(), + source: source?.trim(), + orderAmount: orderAmount ? Number(orderAmount) : undefined, + orderDate: orderDate?.trim(), + }; + }); + setImportedTags(tags); + onChange({ ...formData, importedTags: tags }); + } catch (error) { + // 可用 toast 提示 + } + }; + reader.readAsText(file); + } + }; + + // 下载模板 + const handleDownloadTemplate = () => { + const template = + "电话号码,微信号,来源,订单金额,下单日期\n13800138000,wxid_123,抖音,99.00,2024-03-03"; + const blob = new Blob([template], { type: "text/csv" }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "订单导入模板.csv"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }; + + // 修改订单表格上传逻辑,使用 uploadFile 公共方法 + const [orderUploaded, setOrderUploaded] = useState(false); + + const handleOrderFileUpload = async ( + event: React.ChangeEvent + ) => { + const file = event.target.files?.[0]; + if (file) { + try { + await uploadFile(file); // 默认接口即可 + setOrderUploaded(true); + onChange({ ...formData, orderFileUploaded: true }); + // 可用 toast 或其它方式提示成功 + // alert('上传成功'); + } catch (err) { + // 可用 toast 或其它方式提示失败 + // alert('上传失败'); + } + event.target.value = ""; + } + }; + + // 账号弹窗关闭时清理搜索等状态 + const handleAccountDialogClose = () => { + setIsAccountDialogOpen(false); + // 可在此清理账号搜索等临时状态 + }; + // 素材弹窗关闭时清理搜索等状态 + const handleMaterialDialogClose = () => { + setIsMaterialDialogOpen(false); + // 可在此清理素材搜索等临时状态 + }; + // 订单导入弹窗关闭时清理文件输入等状态 + const handleImportDialogClose = () => { + setIsImportDialogOpen(false); + // 可在此清理文件输入等临时状态 + }; + // 电话获客弹窗关闭 + const handlePhoneSettingsDialogClose = () => { + setIsPhoneSettingsOpen(false); + }; + // 图片预览关闭 + const handleImagePreviewClose = () => { + setIsPreviewOpen(false); + }; + + // 当前选中的场景对象 + const currentScene = sceneList.find((s) => s.id === formData.scenario); + //打开订单 + const openOrder = + formData.scenario !== 2 ? { display: "none" } : { display: "block" }; + + const openPoster = + formData.scenario !== 1 ? { display: "none" } : { display: "block" }; + + return ( +
+ {/* 场景选择区块 */} +
+ {sceneLoading ? ( +
+ +
+ ) : ( +
+ {sceneList.map((scene) => { + const selected = formData.scenario === scene.id; + return ( + + ); + })} +
+ )} +
+ {/* 计划名称输入区 */} +
计划名称
+
+ + onChange({ ...formData, name: String(e.target.value) }) + } + placeholder="请输入计划名称" + /> +
+
获客标签(可多选)
+ {/* 标签选择区块 */} + {formData.scenario && ( +
+ {(currentScene?.scenarioTags || []).map((tag: string) => ( + handleScenarioTagToggle(tag)} + className={styles["basic-tag-item"]} + > + {tag} + + ))} + {/* 自定义标签 */} + {customTags.map((tag: any) => ( + handleScenarioTagToggle(tag.id)} + closable + onClose={() => handleRemoveCustomTag(tag.id)} + className={styles["basic-tag-item"]} + > + {tag.name} + + ))} +
+ )} + {/* 自定义标签输入区 */} +
+ setCustomTagInput(e.target.value)} + placeholder="添加自定义标签" + /> + +
+ {/* 输入获客成功提示 */} +
+ { + setTips(e.target.value); + onChange({ ...formData, tips: e.target.value }); + }} + placeholder="请输入获客成功提示" + /> +
+ {/* 选素材 */} +
+
选择海报
+
+ {[...materials, ...customPosters].map((material) => { + const isSelected = selectedMaterials.some( + (m) => m.id === material.id + ); + const isCustom = material.id.startsWith("custom-"); + return ( +
handleMaterialSelect(material)} + > + {/* 预览按钮:自定义海报在左上,内置海报在右上 */} + { + e.stopPropagation(); + handlePreviewImage(material.preview); + }} + > + + + {/* 删除自定义海报按钮 */} + {isCustom && ( + + )} + {material.name} +
+ {material.name} +
+
+ ); + })} + {/* 添加海报卡片 */} +
uploadInputRef.current?.click()} + > + + + + 添加海报 + { + const file = e.target.files?.[0]; + if (file) { + // 直接上传 + try { + const url = await uploadFile(file); + const newPoster = { + id: `custom-${Date.now()}`, + name: "自定义海报", + type: "poster", + preview: url, + }; + setCustomPosters((prev) => [...prev, newPoster]); + } catch (err) { + // 可加toast提示 + } + e.target.value = ""; + } + }} + /> +
+
+ {/* 全屏图片预览 */} + { + setIsPreviewOpen(false); + setPreviewUrl(null); + }} + footer={null} + width={800} + > + {previewUrl && ( + Preview + )} + +
+ {/* 订单导入区块优化 */} +
+
订单表格上传
+
+ + +
+
+ 支持 CSV、Excel 格式,上传后将文件保存到服务器 +
+
+ {/* 电话获客设置区块,仅在选择电话获客场景时显示 */} + {formData.scenario === 5 && ( +
+
+ 电话获客设置 +
+
+
+ 自动加好友 + + setPhoneSettings((s) => ({ ...s, autoAdd: v })) + } + /> +
+
+ 语音转文字 + + setPhoneSettings((s) => ({ ...s, speechToText: v })) + } + /> +
+
+ 问题提取 + + setPhoneSettings((s) => ({ ...s, questionExtraction: v })) + } + /> +
+
+
+ )} + {/* 微信群设置区块,仅在选择微信群场景时显示 */} + {formData.scenario === 7 && ( +
+
+ onChange({ ...formData, weixinqunName })} + /> +
+
+ onChange({ ...formData, weixinqunNotice })} + /> +
+
+ )} +
+ 是否启用 + onChange({ ...formData, enabled: value })} + /> +
+ +
+ + 下一步 + +
+
+ ); +}; + +export default BasicSettings; diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/steps/FriendRequestSettings.tsx b/nkebao/src/pages/mobile/scenarios/plan/new/steps/FriendRequestSettings.tsx new file mode 100644 index 000000000..1c7b11e8a --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/plan/new/steps/FriendRequestSettings.tsx @@ -0,0 +1,244 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Input, Button, Modal, Alert, Select } from "antd"; +import { MessageOutlined } from "@ant-design/icons"; +import DeviceSelection from "@/components/DeviceSelection"; +import styles from "./friend.module.scss"; + +interface FriendRequestSettingsProps { + formData: any; + onChange: (data: any) => void; + onNext: () => void; + onPrev: () => void; +} + +// 招呼语模板 +const greetingTemplates = [ + "你好,请通过", + "你好,了解XX,请通过", + "你好,我是XX产品的客服请通过", + "你好,感谢关注我们的产品", + "你好,很高兴为您服务", +]; + +// 备注类型选项 +const remarkTypes = [ + { value: "phone", label: "手机号" }, + { value: "nickname", label: "昵称" }, + { value: "source", label: "来源" }, +]; + +const FriendRequestSettings: React.FC = ({ + formData, + onChange, + onNext, + onPrev, +}) => { + const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false); + const [hasWarnings, setHasWarnings] = useState(false); + const [selectedDevices, setSelectedDevices] = useState( + formData.device || [] + ); + const [showRemarkTip, setShowRemarkTip] = useState(false); + + // 获取场景标题 + const getScenarioTitle = () => { + switch (formData.scenario) { + case "douyin": + return "抖音直播"; + case "xiaohongshu": + return "小红书"; + case "weixinqun": + return "微信群"; + case "gongzhonghao": + return "公众号"; + default: + return formData.name || "获客计划"; + } + }; + + // 使用useEffect设置默认值 + useEffect(() => { + if (!formData.greeting) { + onChange({ + ...formData, + greeting: "你好,请通过", + remarkType: "phone", // 默认选择手机号 + remarkFormat: `手机号+${getScenarioTitle()}`, // 默认备注格式 + addFriendInterval: 1, + }); + } + }, [formData, formData.greeting, onChange]); + + // 检查是否有未完成的必填项 + useEffect(() => { + const hasIncompleteFields = !formData.greeting?.trim(); + setHasWarnings(hasIncompleteFields); + }, [formData]); + + const handleTemplateSelect = (template: string) => { + onChange({ ...formData, greeting: template }); + setIsTemplateDialogOpen(false); + }; + + const handleNext = () => { + // 即使有警告也允许进入下一步,但会显示提示 + onNext(); + }; + + return ( +
+ {/* 选择设备区块 */} +
选择设备
+
+ { + setSelectedDevices(deviceIds); + onChange({ ...formData, device: deviceIds }); + }} + placeholder="选择设备" + /> +
+ + {/* 好友备注区块 */} +
好友备注
+
+ + setShowRemarkTip(true)} + onMouseLeave={() => setShowRemarkTip(false)} + > + ? + + {showRemarkTip && ( +
+
设置添加好友时的备注格式
+
+ 备注格式预览: +
+
+ {formData.remarkType === "phone" && + `138****1234+${getScenarioTitle()}`} + {formData.remarkType === "nickname" && + `小红书用户2851+${getScenarioTitle()}`} + {formData.remarkType === "source" && + `抖音直播+${getScenarioTitle()}`} +
+
+ )} +
+ + {/* 招呼语区块 */} +
招呼语
+
+ onChange({ ...formData, greeting: e.target.value })} + placeholder="请输入招呼语" + suffix={ + + } + /> +
+ + {/* 添加间隔区块 */} +
添加间隔
+
+ + onChange({ + ...formData, + addFriendInterval: Number(e.target.value), + }) + } + style={{ width: 100 }} + /> + 分钟 +
+ + {/* 允许加人时间段区块 */} +
允许加人的时间段
+
+ + onChange({ ...formData, addFriendTimeStart: e.target.value }) + } + style={{ width: 120 }} + /> + + + onChange({ ...formData, addFriendTimeEnd: e.target.value }) + } + style={{ width: 120 }} + /> +
+ + {hasWarnings && ( + + )} + + {/* 底部按钮 */} +
+ + +
+ + {/* 招呼语模板弹窗 */} + setIsTemplateDialogOpen(false)} + footer={null} + > +
+ {greetingTemplates.map((template, index) => ( + + ))} +
+
+
+ ); +}; + +export default FriendRequestSettings; diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/steps/MessageSettings.tsx b/nkebao/src/pages/mobile/scenarios/plan/new/steps/MessageSettings.tsx new file mode 100644 index 000000000..56589ceb9 --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/plan/new/steps/MessageSettings.tsx @@ -0,0 +1,645 @@ +import React, { useState, useRef } from "react"; +import { Input, Button, Tabs, Modal, Alert, message } from "antd"; +import { + PlusOutlined, + CloseOutlined, + UploadOutlined, + ClockCircleOutlined, + MessageOutlined, + PictureOutlined, + VideoCameraOutlined, + FileOutlined, + AppstoreOutlined, + LinkOutlined, + TeamOutlined, +} from "@ant-design/icons"; +import styles from "./messages.module.scss"; +import { uploadFile } from "@/api/common"; + +interface MessageContent { + id: string; + type: "text" | "image" | "video" | "file" | "miniprogram" | "link" | "group"; + content: string; + sendInterval?: number; + intervalUnit?: "seconds" | "minutes"; + scheduledTime?: { + hour: number; + minute: number; + second: number; + }; + title?: string; + description?: string; + address?: string; + coverImage?: string; + groupId?: string; + linkUrl?: string; +} + +interface DayPlan { + day: number; + messages: MessageContent[]; +} + +interface MessageSettingsProps { + formData: any; + onChange: (data: any) => void; + onNext: () => void; + onPrev: () => void; +} + +// 消息类型配置 +const messageTypes = [ + { id: "text", icon: MessageOutlined, label: "文本" }, + { id: "image", icon: PictureOutlined, label: "图片" }, + { id: "video", icon: VideoCameraOutlined, label: "视频" }, + { id: "file", icon: FileOutlined, label: "文件" }, + { id: "miniprogram", icon: AppstoreOutlined, label: "小程序" }, + { id: "link", icon: LinkOutlined, label: "链接" }, + { id: "group", icon: TeamOutlined, label: "邀请入群" }, +]; + +// 模拟群组数据 +const mockGroups = [ + { id: "1", name: "产品交流群1", memberCount: 156 }, + { id: "2", name: "产品交流群2", memberCount: 234 }, + { id: "3", name: "产品交流群3", memberCount: 89 }, +]; + +const MessageSettings: React.FC = ({ + formData, + onChange, + onNext, + onPrev, +}) => { + const [dayPlans, setDayPlans] = useState([ + { + day: 0, + messages: [ + { + id: "1", + type: "text", + content: "", + sendInterval: 5, + intervalUnit: "seconds", + }, + ], + }, + ]); + const [isAddDayPlanOpen, setIsAddDayPlanOpen] = useState(false); + const [isGroupSelectOpen, setIsGroupSelectOpen] = useState(false); + const [selectedGroupId, setSelectedGroupId] = useState(""); + const fileInputRef = useRef(null); + const [uploadingIndex, setUploadingIndex] = useState(null); + const [uploadingType, setUploadingType] = useState< + "miniprogram" | "link" | null + >(null); + const [uploadingDay, setUploadingDay] = useState(null); + const [uploadingMsgIdx, setUploadingMsgIdx] = useState(null); + + // 添加新消息 + const handleAddMessage = (dayIndex: number, type = "text") => { + const updatedPlans = [...dayPlans]; + const newMessage: MessageContent = { + id: Date.now().toString(), + type: type as MessageContent["type"], + content: "", + }; + + if (dayPlans[dayIndex].day === 0) { + newMessage.sendInterval = 5; + newMessage.intervalUnit = "seconds"; + } else { + newMessage.scheduledTime = { + hour: 9, + minute: 0, + second: 0, + }; + } + + updatedPlans[dayIndex].messages.push(newMessage); + setDayPlans(updatedPlans); + onChange({ ...formData, messagePlans: updatedPlans }); + }; + + // 更新消息内容 + const handleUpdateMessage = ( + dayIndex: number, + messageIndex: number, + updates: Partial + ) => { + const updatedPlans = [...dayPlans]; + updatedPlans[dayIndex].messages[messageIndex] = { + ...updatedPlans[dayIndex].messages[messageIndex], + ...updates, + }; + setDayPlans(updatedPlans); + onChange({ ...formData, messagePlans: updatedPlans }); + }; + + // 删除消息 + const handleRemoveMessage = (dayIndex: number, messageIndex: number) => { + const updatedPlans = [...dayPlans]; + updatedPlans[dayIndex].messages.splice(messageIndex, 1); + setDayPlans(updatedPlans); + onChange({ ...formData, messagePlans: updatedPlans }); + }; + + // 切换时间单位 + const toggleIntervalUnit = (dayIndex: number, messageIndex: number) => { + const message = dayPlans[dayIndex].messages[messageIndex]; + const newUnit = message.intervalUnit === "minutes" ? "seconds" : "minutes"; + handleUpdateMessage(dayIndex, messageIndex, { intervalUnit: newUnit }); + }; + + // 添加新的天数计划 + const handleAddDayPlan = () => { + const newDay = dayPlans.length; + setDayPlans([ + ...dayPlans, + { + day: newDay, + messages: [ + { + id: Date.now().toString(), + type: "text", + content: "", + scheduledTime: { + hour: 9, + minute: 0, + second: 0, + }, + }, + ], + }, + ]); + setIsAddDayPlanOpen(false); + message.success(`已添加第${newDay}天的消息计划`); + }; + + // 选择群组 + const handleSelectGroup = (groupId: string) => { + setSelectedGroupId(groupId); + setIsGroupSelectOpen(false); + message.success( + `已选择群组:${mockGroups.find((g) => g.id === groupId)?.name}` + ); + }; + + // 触发文件选择 + const triggerUpload = ( + dayIdx: number, + msgIdx: number, + type: "miniprogram" | "link" + ) => { + setUploadingDay(dayIdx); + setUploadingMsgIdx(msgIdx); + setUploadingType(type); + setTimeout(() => { + fileInputRef.current?.click(); + }, 0); + }; + + // 处理文件上传 + const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if ( + !file || + uploadingDay === null || + uploadingMsgIdx === null || + !uploadingType + ) + return; + setUploadingIndex(`${uploadingDay}-${uploadingMsgIdx}`); + try { + const url = await uploadFile(file); + // 更新对应消息的coverImage + setDayPlans((prev) => { + const newPlans = [...prev]; + const msg = newPlans[uploadingDay].messages[uploadingMsgIdx]; + msg.coverImage = url; + return newPlans; + }); + message.success("上传成功"); + } catch (err) { + message.error("上传失败"); + } finally { + setUploadingIndex(null); + setUploadingType(null); + setUploadingDay(null); + setUploadingMsgIdx(null); + if (fileInputRef.current) fileInputRef.current.value = ""; + } + }; + + const items = dayPlans.map((plan, dayIndex) => ({ + key: plan.day.toString(), + label: plan.day === 0 ? "即时消息" : `第${plan.day}天`, + children: ( +
+ {plan.messages.map((message, messageIndex) => ( +
+
+ {/* 时间/间隔设置 */} +
+
+ {plan.day === 0 ? ( + <> + 间隔 + + handleUpdateMessage(dayIndex, messageIndex, { + sendInterval: Number(e.target.value), + }) + } + style={{ width: 60 }} + /> + + + ) : ( + <> + 发送时间 + + handleUpdateMessage(dayIndex, messageIndex, { + scheduledTime: { + ...(message.scheduledTime || { + hour: 9, + minute: 0, + second: 0, + }), + hour: Number(e.target.value), + }, + }) + } + style={{ width: 40 }} + /> + : + + handleUpdateMessage(dayIndex, messageIndex, { + scheduledTime: { + ...(message.scheduledTime || { + hour: 9, + minute: 0, + second: 0, + }), + minute: Number(e.target.value), + }, + }) + } + style={{ width: 40 }} + /> + : + + handleUpdateMessage(dayIndex, messageIndex, { + scheduledTime: { + ...(message.scheduledTime || { + hour: 9, + minute: 0, + second: 0, + }), + second: Number(e.target.value), + }, + }) + } + style={{ width: 40 }} + /> + + )} +
+ +
+ {/* 类型切换按钮 */} +
+ {messageTypes.map((type) => ( + + ))} +
+
+
+ {/* 文本消息 */} + {message.type === "text" && ( + + handleUpdateMessage(dayIndex, messageIndex, { + content: e.target.value, + }) + } + placeholder="请输入消息内容" + autoSize={{ minRows: 3, maxRows: 6 }} + /> + )} + {/* 小程序消息 */} + {message.type === "miniprogram" && ( + <> + + handleUpdateMessage(dayIndex, messageIndex, { + title: e.target.value, + }) + } + placeholder="请输入小程序标题" + style={{ marginBottom: 8 }} + /> + + handleUpdateMessage(dayIndex, messageIndex, { + description: e.target.value, + }) + } + placeholder="请输入小程序描述" + style={{ marginBottom: 8 }} + /> + + handleUpdateMessage(dayIndex, messageIndex, { + address: e.target.value, + }) + } + placeholder="请输入小程序路径" + style={{ marginBottom: 8 }} + /> +
+ {message.coverImage ? ( +
+ 封面 + +
+ ) : ( + + )} +
+ + )} + {/* 链接消息 */} + {message.type === "link" && ( + <> + + handleUpdateMessage(dayIndex, messageIndex, { + title: e.target.value, + }) + } + placeholder="请输入链接标题" + style={{ marginBottom: 8 }} + /> + + handleUpdateMessage(dayIndex, messageIndex, { + description: e.target.value, + }) + } + placeholder="请输入链接描述" + style={{ marginBottom: 8 }} + /> + + handleUpdateMessage(dayIndex, messageIndex, { + linkUrl: e.target.value, + }) + } + placeholder="请输入链接地址" + style={{ marginBottom: 8 }} + /> +
+ {message.coverImage ? ( +
+ 封面 + +
+ ) : ( + + )} +
+ + )} + {/* 群邀请消息 */} + {message.type === "group" && ( +
+ +
+ )} + {/* 图片/视频/文件消息 */} + {(message.type === "image" || + message.type === "video" || + message.type === "file") && ( +
+ +
+ )} +
+
+ ))} + +
+ ), + })); + + return ( +
+
+

消息设置

+ +
+ +
+ + +
+ {/* 添加天数计划弹窗 */} + setIsAddDayPlanOpen(false)} + onOk={() => { + handleAddDayPlan(); + setIsAddDayPlanOpen(false); + }} + > +

选择要添加的消息计划类型

+ +
+ {/* 选择群聊弹窗 */} + setIsGroupSelectOpen(false)} + onOk={() => { + handleSelectGroup(selectedGroupId); + setIsGroupSelectOpen(false); + }} + > +
+ {mockGroups.map((group) => ( +
handleSelectGroup(group.id)} + > +
{group.name}
+
+ 成员数:{group.memberCount} +
+
+ ))} +
+
+ +
+ ); +}; + +export default MessageSettings; diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/steps/base.module.scss b/nkebao/src/pages/mobile/scenarios/plan/new/steps/base.module.scss new file mode 100644 index 000000000..99dde26e5 --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/plan/new/steps/base.module.scss @@ -0,0 +1,164 @@ +.basic-container { + padding: 12px; +} +.basic-scene-select { + background: #fff; + border-radius: 12px; + padding: 24px; + margin-bottom: 24px; +} +.basic-scene-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; +} +.basic-scene-btn { + height: 40px; + border: none; + border-radius: 10px; + font-weight: 500; + font-size: 16px; + outline: none; + cursor: pointer; + background: rgba(#1677ff,0.1); + color: #1677ff; + transition: all 0.2s; +} +.basic-scene-btn.selected { + background: #1677ff; + color: #fff; + box-shadow: 0 2px 8px rgba(22,119,255,0.08); +} +.basic-label { + margin-bottom: 12px; + font-weight: 500; +} +.basic-input-block { + border: 1px solid #eee; + margin-bottom: 16px; +} +.basic-tag-list { + display: flex; + flex-wrap: wrap; + gap: 5px; + padding-bottom: 16px; +} +.basic-tag-item{ + margin-bottom: 6px; +} +.basic-custom-tag-input { + display: flex; + gap: 8px; +} +.basic-success-tip { + display: flex; + padding-top: 16px; +} +.basic-materials { + margin: 16px 0; +} +.basic-materials-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; +} + + +.basic-material-preview{ + position: absolute; + top: 8px; + padding-left: 2px; + right: 8px; + background:rgba(0,0,0,0.5); + border-radius: 50%; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + z-index: 2; + cursor:pointer; +} +.basic-material-card { + border: 2px solid #eee; + border-radius: 8px; + padding: 6px; + cursor: pointer; + background: #fff; + text-align: center; + position: relative; + min-height: 192px; + transition: border 0.2s; +} +.basic-material-card.selected { + border: 2px solid #1890ff; + background: #e6f7ff; +} +.basic-material-img { + width: 100px; + height: 180px; + object-fit: cover; + border-radius: 4px; + margin-bottom: 0; + display: block; +} +.basic-material-name { + position: absolute; + left: 0; + bottom: 0; + width: 100%; + background: rgba(0,0,0,0.5); + color: #fff; + font-size: 14px; + padding: 4px 0; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + text-align: center; + z-index: 3; +} +.basic-add-material { + border: 2px dashed #bbb; + border-radius: 8px; + padding: 6px; + cursor: pointer; + background: #fafbfc; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 190px; +} +.basic-order-upload { + margin: 16px 0; +} +.basic-order-upload-label { + font-weight: 500; + margin-bottom: 8px; +} +.basic-order-upload-actions { + display: flex; + gap: 12px; + margin-bottom: 4px; +} +.basic-order-upload-tip { + color: #888; + font-size: 13px; + margin-bottom: 8px; +} +.basic-phone-settings { + margin: 16px 0; + background: #f7f8fa; + border-radius: 10px; + padding: 20px; + box-shadow: 0 2px 8px rgba(0,0,0,0.03); + margin-bottom: 12px; +} +.basic-wechat-group { + margin: 16px 0; +} +.basic-footer-switch { + display: flex; + align-items: center; + justify-content: space-between; + margin: 16px 0; +} diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/steps/friend.module.scss b/nkebao/src/pages/mobile/scenarios/plan/new/steps/friend.module.scss new file mode 100644 index 000000000..0c0f11386 --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/plan/new/steps/friend.module.scss @@ -0,0 +1,48 @@ +.friend-container { + padding: 12px; +} +.friend-label { + margin-bottom: 12px; + font-weight: 500; +} +.friend-block { + margin-bottom: 16px; +} +.friend-remark-tip { + position: absolute; + right: 0; + top: 36px; + z-index: 10; + background: #fff; + border: 1px solid #eee; + border-radius: 6px; + padding: 12px; + width: 220px; + box-shadow: 0 2px 8px rgba(0,0,0,0.08); +} +.friend-remark-q { + position: absolute; + right: 8px; + top: 8px; + cursor: pointer; + color: #888; +} +.friend-interval-row { + display: flex; + align-items: center; + gap: 8px; +} +.friend-time-row { + display: flex; + align-items: center; + gap: 8px; +} +.friend-footer { + display: flex; + justify-content: space-between; + margin-top: 32px; +} +.friend-modal-btn { + width: 100%; + margin-bottom: 8px; +} diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/steps/messages.module.scss b/nkebao/src/pages/mobile/scenarios/plan/new/steps/messages.module.scss new file mode 100644 index 000000000..5dc19e7f8 --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/plan/new/steps/messages.module.scss @@ -0,0 +1,103 @@ +.messages-container { + padding: 16px; +} +.messages-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} +.messages-title { + font-size: 18px; + font-weight: 600; +} +.messages-tab { + margin-bottom: 16px; +} +.messages-day-panel { + background: #fafbfc; + border-radius: 10px; + margin-bottom: 16px; +} +.messages-message-card { + background: #fff; + border-radius: 12px; + box-shadow: 0 4px 16px rgba(22, 119, 255, 0.06), 0 1.5px 4px rgba(0,0,0,0.04); + padding: 20px 12px 16px 12px; + margin-bottom: 20px; + border: 1.5px solid #f0f3fa; + transition: box-shadow 0.2s, border 0.2s, transform 0.2s; + position: relative; +} +.messages-message-card:hover { + box-shadow: 0 8px 24px rgba(22, 119, 255, 0.12), 0 2px 8px rgba(0,0,0,0.08); + border: 1.5px solid #1677ff; + transform: translateY(-2px) scale(1.01); +} +.messages-message-header { + +} +.messages-message-header-content{ + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} +.messages-message-type-btns { + display: flex; + gap: 5px; + margin-bottom: 8px; +} +.messages-message-type-btn{ + width: 20px; +} +.messages-message-content { + margin-bottom: 10px; + font-size: 15px; + color: #222; + line-height: 1.7; +} +.messages-message-actions { + display: flex; + align-items: center; + gap: 12px; + margin-top: 8px; +} +.messages-message-remove-btn { + color: #ff4d4f; + background: none; + border: none; + cursor: pointer; + font-size: 16px; + padding: 0 8px; + transition: color 0.2s; +} +.messages-message-remove-btn:hover { + color: #d9363e; +} +.messages-add-message-btn { + width: 100%; + margin-top: 8px; +} +.messages-footer { + display: flex; + justify-content: space-between; + margin-top: 32px; +} +.messages-modal-btn { + width: 100%; + margin-bottom: 8px; +} +.messages-group-select-item { + padding: 16px; + border-radius: 8px; + cursor: pointer; + background: #fff; + margin-bottom: 8px; + border: 1px solid #eee; + transition: border 0.2s, background 0.2s; +} +.messages-group-select-item.selected { + background: #e6f7ff; + border: 1.5px solid #1677ff; +} diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/steps/step.api.ts b/nkebao/src/pages/mobile/scenarios/plan/new/steps/step.api.ts new file mode 100644 index 000000000..0e1b2c34f --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/plan/new/steps/step.api.ts @@ -0,0 +1,363 @@ +import request from '@/api/request'; + +// ==================== 场景相关接口 ==================== + +// 获取场景列表 +export function getScenarios(params: any) { + return request('/v1/plan/scenes', params, 'GET'); +} + +// 获取场景详情 +export function getScenarioDetail(id: string) { + return request(`/v1/scenarios/${id}`, {}, 'GET'); +} + +// 创建场景 +export function createScenario(data: any) { + return request('/v1/scenarios', data, 'POST'); +} + +// 更新场景 +export function updateScenario(id: string, data: any) { + return request(`/v1/scenarios/${id}`, data, 'PUT'); +} + +// 删除场景 +export function deleteScenario(id: string) { + return request(`/v1/scenarios/${id}`, {}, 'DELETE'); +} + +// ==================== 计划相关接口 ==================== + +// 获取计划列表 +export function getPlanList(scenarioId: string, page: number = 1, limit: number = 20) { + return request(`/api/scenarios/${scenarioId}/plans`, { page, limit }, 'GET'); +} + // 复制计划 + export function copyPlan(planId: string) { + return request(`/api/scenarios/plans/${planId}/copy`, undefined, 'POST'); +} + +// 删除计划 +export function deletePlan(planId: string) { + return request(`/api/scenarios/plans/${planId}`, undefined, 'DELETE'); +} + + + +// 获取小程序二维码 +export function getWxMinAppCode(planId: string) { + return request(`/api/scenarios/plans/${planId}/qrcode`, undefined, 'GET'); +} + + +// ==================== 设备相关接口 ==================== + +// 获取设备列表 +export function getDevices() { + return request('/api/devices', undefined, 'GET'); +} + +// 获取设备详情 +export function getDeviceDetail(deviceId: string) { + return request(`/api/devices/${deviceId}`, undefined, 'GET'); +} + +// 创建设备 +export function createDevice(data: any) { + return request('/api/devices', data, 'POST'); +} + +// 更新设备 +export function updateDevice(deviceId: string, data: any) { + return request(`/api/devices/${deviceId}`, data, 'PUT'); +} + +// 删除设备 +export function deleteDevice(deviceId: string) { + return request(`/api/devices/${deviceId}`, undefined, 'DELETE'); +} + +// ==================== 微信号相关接口 ==================== + +// 获取微信号列表 +export function getWechatAccounts() { + return request('/api/wechat-accounts', undefined, 'GET'); +} + +// 获取微信号详情 +export function getWechatAccountDetail(accountId: string) { + return request(`/api/wechat-accounts/${accountId}`, undefined, 'GET'); +} + +// 创建微信号 +export function createWechatAccount(data: any) { + return request('/api/wechat-accounts', data, 'POST'); +} + +// 更新微信号 +export function updateWechatAccount(accountId: string, data: any) { + return request(`/api/wechat-accounts/${accountId}`, data, 'PUT'); +} + +// 删除微信号 +export function deleteWechatAccount(accountId: string) { + return request(`/api/wechat-accounts/${accountId}`, undefined, 'DELETE'); +} + +// ==================== 海报相关接口 ==================== + +// 获取海报列表 +export function getPosters() { + return request('/api/posters', undefined, 'GET'); +} + +// 获取海报详情 +export function getPosterDetail(posterId: string) { + return request(`/api/posters/${posterId}`, undefined, 'GET'); +} + +// 创建海报 +export function createPoster(data: any) { + return request('/api/posters', data, 'POST'); +} + +// 更新海报 +export function updatePoster(posterId: string, data: any) { + return request(`/api/posters/${posterId}`, data, 'PUT'); +} + +// 删除海报 +export function deletePoster(posterId: string) { + return request(`/api/posters/${posterId}`, undefined, 'DELETE'); +} + +// ==================== 内容相关接口 ==================== + +// 获取内容列表 +export function getContents(params: any) { + return request('/api/contents', params, 'GET'); +} + +// 获取内容详情 +export function getContentDetail(contentId: string) { + return request(`/api/contents/${contentId}`, undefined, 'GET'); +} + +// 创建内容 +export function createContent(data: any) { + return request('/api/contents', data, 'POST'); +} + +// 更新内容 +export function updateContent(contentId: string, data: any) { + return request(`/api/contents/${contentId}`, data, 'PUT'); +} + +// 删除内容 +export function deleteContent(contentId: string) { + return request(`/api/contents/${contentId}`, undefined, 'DELETE'); +} + +// ==================== 流量池相关接口 ==================== + +// 获取流量池列表 +export function getTrafficPools() { + return request('/api/traffic-pools', undefined, 'GET'); +} + +// 获取流量池详情 +export function getTrafficPoolDetail(poolId: string) { + return request(`/api/traffic-pools/${poolId}`, undefined, 'GET'); +} + +// 创建流量池 +export function createTrafficPool(data: any) { + return request('/api/traffic-pools', data, 'POST'); +} + +// 更新流量池 +export function updateTrafficPool(poolId: string, data: any) { + return request(`/api/traffic-pools/${poolId}`, data, 'PUT'); +} + +// 删除流量池 +export function deleteTrafficPool(poolId: string) { + return request(`/api/traffic-pools/${poolId}`, undefined, 'DELETE'); +} + +// ==================== 工作台相关接口 ==================== + +// 获取工作台统计数据 +export function getWorkspaceStats() { + return request('/api/workspace/stats', undefined, 'GET'); +} + +// 获取自动点赞任务列表 +export function getAutoLikeTasks() { + return request('/api/workspace/auto-like/tasks', undefined, 'GET'); +} + +// 创建自动点赞任务 +export function createAutoLikeTask(data: any) { + return request('/api/workspace/auto-like/tasks', data, 'POST'); +} + +// 更新自动点赞任务 +export function updateAutoLikeTask(taskId: string, data: any) { + return request(`/api/workspace/auto-like/tasks/${taskId}`, data, 'PUT'); +} + +// 删除自动点赞任务 +export function deleteAutoLikeTask(taskId: string) { + return request(`/api/workspace/auto-like/tasks/${taskId}`, undefined, 'DELETE'); +} + +// ==================== 群发相关接口 ==================== + +// 获取群发任务列表 +export function getGroupPushTasks() { + return request('/api/workspace/group-push/tasks', undefined, 'GET'); +} + +// 创建群发任务 +export function createGroupPushTask(data: any) { + return request('/api/workspace/group-push/tasks', data, 'POST'); +} + +// 更新群发任务 +export function updateGroupPushTask(taskId: string, data: any) { + return request(`/api/workspace/group-push/tasks/${taskId}`, data, 'PUT'); +} + +// 删除群发任务 +export function deleteGroupPushTask(taskId: string) { + return request(`/api/workspace/group-push/tasks/${taskId}`, undefined, 'DELETE'); +} + +// ==================== 自动建群相关接口 ==================== + +// 获取自动建群任务列表 +export function getAutoGroupTasks() { + return request('/api/workspace/auto-group/tasks', undefined, 'GET'); +} + +// 创建自动建群任务 +export function createAutoGroupTask(data: any) { + return request('/api/workspace/auto-group/tasks', data, 'POST'); +} + +// 更新自动建群任务 +export function updateAutoGroupTask(taskId: string, data: any) { + return request(`/api/workspace/auto-group/tasks/${taskId}`, data, 'PUT'); +} + +// 删除自动建群任务 +export function deleteAutoGroupTask(taskId: string) { + return request(`/api/workspace/auto-group/tasks/${taskId}`, undefined, 'DELETE'); +} + +// ==================== AI助手相关接口 ==================== + +// 获取AI对话历史 +export function getAIChatHistory() { + return request('/api/workspace/ai-assistant/chat-history', undefined, 'GET'); +} + +// 发送AI消息 +export function sendAIMessage(data: any) { + return request('/api/workspace/ai-assistant/send-message', data, 'POST'); +} + +// 获取AI分析报告 +export function getAIAnalysisReport() { + return request('/api/workspace/ai-assistant/analysis-report', undefined, 'GET'); +} + +// ==================== 订单相关接口 ==================== + +// 获取订单列表 +export function getOrders(params: any) { + return request('/api/orders', params, 'GET'); +} + +// 获取订单详情 +export function getOrderDetail(orderId: string) { + return request(`/api/orders/${orderId}`, undefined, 'GET'); +} + +// 创建订单 +export function createOrder(data: any) { + return request('/api/orders', data, 'POST'); +} + +// 更新订单 +export function updateOrder(orderId: string, data: any) { + return request(`/api/orders/${orderId}`, data, 'PUT'); +} + +// 删除订单 +export function deleteOrder(orderId: string) { + return request(`/api/orders/${orderId}`, undefined, 'DELETE'); +} + +// ==================== 用户相关接口 ==================== + +// 获取用户信息 +export function getUserInfo() { + return request('/api/user/info', undefined, 'GET'); +} + +// 更新用户信息 +export function updateUserInfo(data: any) { + return request('/api/user/info', data, 'PUT'); +} + +// 修改密码 +export function changePassword(data: any) { + return request('/api/user/change-password', data, 'POST'); +} + +// 上传头像 +export function uploadAvatar(data: any) { + return request('/api/user/upload-avatar', data, 'POST'); +} + +// ==================== 文件上传相关接口 ==================== + +// 上传文件 +export function uploadFile(data: any) { + return request('/api/upload/file', data, 'POST'); +} + +// 上传图片 +export function uploadImage(data: any) { + return request('/api/upload/image', data, 'POST'); +} + +// 删除文件 +export function deleteFile(fileId: string) { + return request(`/api/upload/files/${fileId}`, undefined, 'DELETE'); +} + +// ==================== 系统配置相关接口 ==================== + +// 获取系统配置 +export function getSystemConfig() { + return request('/api/system/config', undefined, 'GET'); +} + +// 更新系统配置 +export function updateSystemConfig(data: any) { + return request('/api/system/config', data, 'PUT'); +} + +// 获取系统通知 +export function getSystemNotifications() { + return request('/api/system/notifications', undefined, 'GET'); +} + +// 标记通知为已读 +export function markNotificationAsRead(notificationId: string) { + return request(`/api/system/notifications/${notificationId}/read`, undefined, 'PUT'); +} diff --git a/nkebao/src/pages/mobile/workspace/ai-analyzer/index.module.scss b/nkebao/src/pages/mobile/workspace/ai-analyzer/index.module.scss new file mode 100644 index 000000000..1fe010a2e --- /dev/null +++ b/nkebao/src/pages/mobile/workspace/ai-analyzer/index.module.scss @@ -0,0 +1,96 @@ +.analyzerPage { + + +} + +.tabs { + background: #fff; + padding: 0 12px; + border-radius: 0 0 12px 12px; + margin-bottom: 8px; +} + +.planList { + display: flex; + flex-direction: column; + gap: 16px; + padding: 0 12px 16px 12px; +} + +.planCard { + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.06); + padding: 16px 14px 12px 14px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.cardHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; +} + +.cardTitle { + font-size: 16px; + font-weight: 700; + color: #222; +} + +.statusDone { + background: #e6f9e6; + color: #22c55e; + font-size: 12px; + border-radius: 8px; + padding: 2px 10px; + font-weight: 600; +} + +.statusDoing { + background: #e0f2fe; + color: #1677ff; + font-size: 12px; + border-radius: 8px; + padding: 2px 10px; + font-weight: 600; +} + +.cardInfo { + font-size: 13px; + color: #444; + display: flex; + flex-direction: column; + gap: 4px; +} + +.label { + color: #888; + font-size: 12px; + margin-right: 2px; +} + +.keyword { + display: inline-block; + background: #f3f4f6; + color: #1677ff; + border-radius: 6px; + padding: 2px 8px; + font-size: 12px; + margin-right: 6px; + margin-bottom: 2px; +} + +.cardActions { + display: flex; + gap: 10px; + margin-top: 8px; +} + +.actionBtn { + border-radius: 6px !important; + font-size: 13px !important; + padding: 0 12px !important; +} \ No newline at end of file diff --git a/nkebao/src/pages/mobile/workspace/ai-analyzer/index.tsx b/nkebao/src/pages/mobile/workspace/ai-analyzer/index.tsx new file mode 100644 index 000000000..09224bd6b --- /dev/null +++ b/nkebao/src/pages/mobile/workspace/ai-analyzer/index.tsx @@ -0,0 +1,141 @@ +import React, { useState } from "react"; +import NavCommon from "@/components/NavCommon"; +import Layout from "@/components/Layout/Layout"; +import { Tabs } from "antd-mobile"; +import { Button } from "antd"; +import styles from "./index.module.scss"; +import { PlusOutlined } from "@ant-design/icons"; + +const mockPlans = [ + { + id: "1", + title: "美妆用户分析", + status: "done", + device: "设备1", + wechat: "wxid_abc123", + type: "综合分析", + keywords: ["美妆", "护肤", "彩妆"], + createTime: "2023/12/15 18:30:00", + finishTime: "2023/12/15 19:45:00", + }, + { + id: "2", + title: "健身爱好者分析", + status: "doing", + device: "设备2", + wechat: "wxid_fit456", + type: "好友信息分析", + keywords: ["健身", "运动", "健康"], + createTime: "2023/12/16 17:15:00", + finishTime: "", + }, +]; + +const statusMap = { + all: "全部计划", + doing: "进行中", + done: "已完成", +}; + +const statusTag = { + done: 已完成, + doing: 分析中, +}; + +const AiAnalyzer: React.FC = () => { + const [tab, setTab] = useState<"all" | "doing" | "done">("all"); + + const filteredPlans = + tab === "all" ? mockPlans : mockPlans.filter((p) => p.status === tab); + + return ( + + 新建计划 + + } + /> + } + > +
+ setTab(key as any)} + className={styles.tabs} + > + + + + +
+ {filteredPlans.map((plan) => ( +
+
+ {plan.title} + {statusTag[plan.status as "done" | "doing"]} +
+
+
+ 设备: + {plan.device} | 微信号: {plan.wechat} +
+
+ 分析类型: + {plan.type} +
+
+ 关键词: + {plan.keywords.map((k) => ( + + {k} + + ))} +
+
+ 创建时间: + {plan.createTime} +
+ {plan.status === "done" && ( +
+ 完成时间: + {plan.finishTime} +
+ )} +
+
+ {plan.status === "done" ? ( + <> + + + + ) : ( + + )} +
+
+ ))} +
+
+
+ ); +}; + +export default AiAnalyzer; diff --git a/nkebao/src/pages/mobile/workspace/ai-assistant/AIAssistant.module.scss b/nkebao/src/pages/mobile/workspace/ai-assistant/AIAssistant.module.scss new file mode 100644 index 000000000..d5a7cd33d --- /dev/null +++ b/nkebao/src/pages/mobile/workspace/ai-assistant/AIAssistant.module.scss @@ -0,0 +1,139 @@ +.chatContainer { + display: flex; + flex-direction: column; +} + +.messageList { + flex: 1; + overflow-y: auto; + padding: 16px 12px 80px 12px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.userMessage { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.aiMessage { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.bubble { + max-width: 80%; + padding: 10px 14px; + border-radius: 18px; + font-size: 15px; + line-height: 1.6; + word-break: break-word; + background: #fff; + color: #222; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); +} + +.userMessage .bubble { + background: linear-gradient(135deg, #a7e0ff 0%, #5bbcff 100%); + color: #222; + border-bottom-right-radius: 6px; +} + +.aiMessage .bubble { + background: #fff; + color: #222; + border-bottom-left-radius: 6px; +} + +.time { + font-size: 11px; + color: #aaa; + margin: 4px 8px 0 8px; + align-self: flex-end; +} + +.inputBar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + background: #fff; + padding: 10px 12px 10px 12px; + box-shadow: 0 -2px 8px rgba(0,0,0,0.04); + z-index: 10; +} + +.input { + flex: 1; + border: none; + outline: none; + background: #f3f4f6; + border-radius: 18px; + padding: 10px 14px; + font-size: 15px; + margin-right: 8px; +} + +.sendButton { + background: var(--primary-gradient, linear-gradient(135deg, #a7e0ff 0%, #5bbcff 100%)); + color: #fff; + border: none; + border-radius: 18px; + padding: 8px 18px; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} + +.sendButton:disabled { + background: #e5e7eb; + color: #aaa; + cursor: not-allowed; +} + +.iconBtn { + background: none; + border: none; + outline: none; + margin-right: 6px; + font-size: 20px; + color: #888; + cursor: pointer; + padding: 4px; + border-radius: 50%; + transition: background 0.2s, color 0.2s; +} + +.iconBtn:hover, .iconBtn:active { + background: #f3f4f6; + color: #5bbcff; +} + +.image { + max-width: 180px; + max-height: 180px; + border-radius: 10px; + display: block; +} + +.fileLink { + color: #5bbcff; + text-decoration: none; + font-size: 15px; + word-break: break-all; + display: flex; + align-items: center; +} + +.nav-title { + color: var(--primary-color); + font-weight: 700; + font-size: 18px; + text-shadow: 0 2px 4px rgba(24, 142, 238, 0.2); +} \ No newline at end of file diff --git a/nkebao/src/pages/mobile/workspace/ai-assistant/AIAssistant.tsx b/nkebao/src/pages/mobile/workspace/ai-assistant/AIAssistant.tsx new file mode 100644 index 000000000..4e60367e5 --- /dev/null +++ b/nkebao/src/pages/mobile/workspace/ai-assistant/AIAssistant.tsx @@ -0,0 +1,264 @@ +import React, { useRef, useState, useEffect } from "react"; +import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; +import { + PictureOutlined, + PaperClipOutlined, + AudioOutlined, +} from "@ant-design/icons"; +import styles from "./AIAssistant.module.scss"; + +interface Message { + id: string; + content: string; + from: "user" | "ai"; + time: string; + type?: "text" | "image" | "file" | "audio"; + fileName?: string; + fileUrl?: string; +} + +const initialMessages: Message[] = [ + { + id: "1", + content: "你好!我是你的AI助手,有什么可以帮助你的吗?", + from: "ai", + time: "15:29", + type: "text", + }, +]; + +const AIAssistant: React.FC = () => { + const [messages, setMessages] = useState(initialMessages); + const [input, setInput] = useState(""); + const [loading, setLoading] = useState(false); + const messagesEndRef = useRef(null); + const fileInputRef = useRef(null); + const imageInputRef = useRef(null); + const [recognizing, setRecognizing] = useState(false); + const recognitionRef = useRef(null); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + // 语音识别初始化 + useEffect(() => { + if (!("webkitSpeechRecognition" in window)) return; + const SpeechRecognition = (window as any).webkitSpeechRecognition; + recognitionRef.current = new SpeechRecognition(); + recognitionRef.current.continuous = false; + recognitionRef.current.interimResults = false; + recognitionRef.current.lang = "zh-CN"; + recognitionRef.current.onresult = (event: any) => { + const transcript = event.results[0][0].transcript; + setInput((prev) => prev + transcript); + setRecognizing(false); + }; + recognitionRef.current.onerror = () => setRecognizing(false); + recognitionRef.current.onend = () => setRecognizing(false); + }, []); + + const handleSend = async () => { + if (!input.trim()) return; + const userMsg: Message = { + id: Date.now().toString(), + content: input, + from: "user", + time: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }), + type: "text", + }; + setMessages((prev) => [...prev, userMsg]); + setInput(""); + setLoading(true); + setTimeout(() => { + setMessages((prev) => [ + ...prev, + { + id: Date.now().toString() + "-ai", + content: "AI正在思考...(此处可接入真实API)", + from: "ai", + time: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }), + type: "text", + }, + ]); + setLoading(false); + }, 1200); + }; + + // 图片上传 + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const url = URL.createObjectURL(file); + setMessages((prev) => [ + ...prev, + { + id: Date.now().toString(), + content: url, + from: "user", + time: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }), + type: "image", + fileName: file.name, + fileUrl: url, + }, + ]); + } + e.target.value = ""; + }; + + // 文件上传 + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const url = URL.createObjectURL(file); + setMessages((prev) => [ + ...prev, + { + id: Date.now().toString(), + content: file.name, + from: "user", + time: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }), + type: "file", + fileName: file.name, + fileUrl: url, + }, + ]); + } + e.target.value = ""; + }; + + // 语音输入 + const handleVoiceInput = () => { + if (!recognitionRef.current) return alert("当前浏览器不支持语音输入"); + if (recognizing) { + recognitionRef.current.stop(); + setRecognizing(false); + } else { + recognitionRef.current.start(); + setRecognizing(true); + } + }; + + return ( + } loading={false}> +
+
+ {messages.map((msg) => ( +
+ {msg.type === "text" && ( +
{msg.content}
+ )} + {msg.type === "image" && ( +
+ {msg.fileName} +
+ )} + {msg.type === "file" && ( + + )} + {/* 语音消息可后续扩展 */} +
{msg.time}
+
+ ))} + {loading && ( +
+
AI正在输入...
+
+ )} +
+
+
+ + + + + + setInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleSend(); + }} + disabled={loading} + /> + +
+
+ + ); +}; + +export default AIAssistant; diff --git a/nkebao/src/pages/mobile/workspace/auto-group/detail/api.ts b/nkebao/src/pages/mobile/workspace/auto-group/detail/api.ts new file mode 100644 index 000000000..ff38e95fd --- /dev/null +++ b/nkebao/src/pages/mobile/workspace/auto-group/detail/api.ts @@ -0,0 +1,6 @@ +import request from "@/api/request"; + +// 获取自动建群任务详情 +export function getAutoGroupDetail(id: string) { + return request(`/api/auto-group/detail/${id}`); +} diff --git a/nkebao/src/pages/mobile/workspace/auto-group/detail/index.module.scss b/nkebao/src/pages/mobile/workspace/auto-group/detail/index.module.scss new file mode 100644 index 000000000..ff8c8d5e9 --- /dev/null +++ b/nkebao/src/pages/mobile/workspace/auto-group/detail/index.module.scss @@ -0,0 +1,149 @@ +.autoGroupDetail { + padding: 16px 0 80px 0; + background: #f7f8fa; + min-height: 100vh; +} + +.headerBar { + display: flex; + align-items: center; + height: 48px; + background: #fff; + border-bottom: 1px solid #f0f0f0; + font-size: 18px; + font-weight: 600; + padding: 0 16px; +} + +.title { + font-size: 18px; + font-weight: 600; + color: #222; + flex: 1; + text-align: center; +} + +.infoCard { + margin-bottom: 16px; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.03); + border: none; + background: #fff; + padding: 16px; +} +.infoGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +} +.infoTitle { + font-size: 14px; + font-weight: 500; + color: #1677ff; + margin-bottom: 4px; +} +.infoItem { + font-size: 13px; + color: #444; + margin-bottom: 2px; +} + +.progressSection { + margin-top: 16px; +} +.progressCard { + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.03); + border: none; + background: #fff; + padding: 16px; + margin-bottom: 16px; +} +.progressHeader { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 15px; + font-weight: 500; + margin-bottom: 8px; +} +.groupList { + margin-top: 8px; + display: flex; + flex-direction: column; + gap: 12px; +} +.groupCard { + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.03); + border: none; + background: #fff; + padding: 12px 16px; +} +.groupHeader { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 15px; + font-weight: 500; + margin-bottom: 8px; +} +.memberGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 6px; + margin-top: 8px; +} +.memberItem { + background: #f5f7fa; + border-radius: 8px; + padding: 4px 8px; + font-size: 13px; + color: #333; + display: flex; + align-items: center; +} +.warnText { + color: #faad14; + font-size: 13px; + margin-top: 8px; + display: flex; + align-items: center; +} +.successText { + color: #389e0d; + font-size: 13px; + margin-top: 8px; + display: flex; + align-items: center; +} +.successAlert { + color: #389e0d; + background: #f6ffed; + border-radius: 8px; + padding: 8px 0; + text-align: center; + margin-top: 12px; + font-size: 14px; +} +.emptyCard { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 0; + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.03); + margin-top: 32px; +} +.emptyTitle { + font-size: 16px; + color: #888; + margin: 12px 0 4px 0; +} +.emptyDesc { + font-size: 13px; + color: #bbb; + margin-bottom: 16px; +} \ No newline at end of file diff --git a/nkebao/src/pages/mobile/workspace/auto-group/detail/index.tsx b/nkebao/src/pages/mobile/workspace/auto-group/detail/index.tsx new file mode 100644 index 000000000..058c320a5 --- /dev/null +++ b/nkebao/src/pages/mobile/workspace/auto-group/detail/index.tsx @@ -0,0 +1,384 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { + Card, + Button, + Toast, + ProgressBar, + Tag, + SpinLoading, +} from "antd-mobile"; +import { TeamOutline, LeftOutline } from "antd-mobile-icons"; +import { AlertOutlined } from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import style from "./index.module.scss"; + +interface GroupMember { + id: string; + nickname: string; + wechatId: string; + tags: string[]; +} + +interface Group { + id: string; + members: GroupMember[]; +} + +interface GroupTaskDetail { + id: string; + name: string; + status: "preparing" | "creating" | "completed" | "paused"; + totalGroups: number; + currentGroupIndex: number; + groups: Group[]; + createTime: string; + lastUpdateTime: string; + creator: string; + deviceCount: number; + targetFriends: number; + groupSize: { min: number; max: number }; + timeRange: { start: string; end: string }; + targetTags: string[]; + groupNameTemplate: string; + groupDescription: string; +} + +const mockTaskDetail: GroupTaskDetail = { + id: "1", + name: "VIP客户建群", + status: "creating", + totalGroups: 5, + currentGroupIndex: 2, + groups: Array.from({ length: 5 }).map((_, index) => ({ + id: `group-${index}`, + members: Array.from({ length: Math.floor(Math.random() * 10) + 30 }).map( + (_, mIndex) => ({ + id: `member-${index}-${mIndex}`, + nickname: `用户${mIndex + 1}`, + wechatId: `wx_${mIndex}`, + tags: [`标签${(mIndex % 3) + 1}`], + }) + ), + })), + createTime: "2024-11-20 19:04:14", + lastUpdateTime: "2025-02-06 13:12:35", + creator: "admin", + deviceCount: 2, + targetFriends: 156, + groupSize: { min: 20, max: 50 }, + timeRange: { start: "09:00", end: "21:00" }, + targetTags: ["VIP客户", "高价值"], + groupNameTemplate: "VIP客户交流群{序号}", + groupDescription: "VIP客户专属交流群,提供优质服务", +}; + +const GroupPreview: React.FC<{ + groupIndex: number; + members: GroupMember[]; + isCreating: boolean; + isCompleted: boolean; + onRetry?: () => void; +}> = ({ groupIndex, members, isCreating, isCompleted, onRetry }) => { + const [expanded, setExpanded] = useState(false); + const targetSize = 38; + return ( + +
+
+ 群 {groupIndex + 1} + + {isCompleted ? "已完成" : isCreating ? "创建中" : "等待中"} + +
+
+ + {members.length}/{targetSize} +
+
+ {isCreating && !isCompleted && ( + + )} + {expanded ? ( + <> +
+ {members.map((member) => ( +
+ {member.nickname} + {member.tags.length > 0 && ( + + {member.tags[0]} + + )} +
+ ))} +
+ + + ) : ( + + )} + {!isCompleted && members.length < targetSize && ( +
+ + 群人数不足{targetSize}人 + {onRetry && ( + + )} +
+ )} + {isCompleted &&
群创建完成
} +
+ ); +}; + +const GroupCreationProgress: React.FC<{ + taskDetail: GroupTaskDetail; + onComplete: () => void; +}> = ({ taskDetail, onComplete }) => { + const [groups, setGroups] = useState(taskDetail.groups); + const [currentGroupIndex, setCurrentGroupIndex] = useState( + taskDetail.currentGroupIndex + ); + const [status, setStatus] = useState( + taskDetail.status + ); + + useEffect(() => { + if (status === "creating" && currentGroupIndex < groups.length) { + const timer = setTimeout(() => { + if (currentGroupIndex === groups.length - 1) { + setStatus("completed"); + onComplete(); + } else { + setCurrentGroupIndex((prev) => prev + 1); + } + }, 3000); + return () => clearTimeout(timer); + } + }, [status, currentGroupIndex, groups.length, onComplete]); + + const handleRetryGroup = (groupIndex: number) => { + setGroups((prev) => + prev.map((group, index) => { + if (index === groupIndex) { + return { + ...group, + members: [ + ...group.members, + { + id: `retry-member-${Date.now()}`, + nickname: `补充用户${group.members.length + 1}`, + wechatId: `wx_retry_${Date.now()}`, + tags: ["新加入"], + }, + ], + }; + } + return group; + }) + ); + }; + + return ( +
+ +
+
+ 建群进度 + + {status === "preparing" + ? "准备中" + : status === "creating" + ? "创建中" + : "已完成"} + +
+
+ {currentGroupIndex + 1}/{groups.length}组 +
+
+ +
+
+ {groups.map((group, index) => ( + handleRetryGroup(index)} + /> + ))} +
+ {status === "completed" && ( +
所有群组已创建完成
+ )} +
+ ); +}; + +const AutoGroupDetail: React.FC = () => { + const { id } = useParams(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [taskDetail, setTaskDetail] = useState(null); + + useEffect(() => { + setLoading(true); + setTimeout(() => { + setTaskDetail(mockTaskDetail); + setLoading(false); + }, 800); + }, [id]); + + const handleComplete = () => { + Toast.show({ content: "所有群组已创建完成" }); + }; + + if (loading) { + return ( + + +
建群详情
+
+ } + footer={} + loading={true} + > +
+ + ); + } + + if (!taskDetail) { + return ( + + +
建群详情
+
+ } + footer={} + > + + +
任务不存在
+
请检查任务ID是否正确
+ +
+
+ ); + } + + return ( + + +
{taskDetail.name} - 建群详情
+ + } + footer={} + > +
+ +
+
+
基本信息
+
任务名称:{taskDetail.name}
+
+ 创建时间:{taskDetail.createTime} +
+
创建人:{taskDetail.creator}
+
+ 执行设备:{taskDetail.deviceCount} 个 +
+
+
+
建群配置
+
+ 群组规模:{taskDetail.groupSize.min}-{taskDetail.groupSize.max}{" "} + 人 +
+
+ 执行时间:{taskDetail.timeRange.start} -{" "} + {taskDetail.timeRange.end} +
+
+ 目标标签:{taskDetail.targetTags.join(", ")} +
+
+ 群名称模板:{taskDetail.groupNameTemplate} +
+
+
+
+ +
+
+ ); +}; + +export default AutoGroupDetail; diff --git a/nkebao/src/pages/mobile/workspace/auto-group/form/api.ts b/nkebao/src/pages/mobile/workspace/auto-group/form/api.ts new file mode 100644 index 000000000..4ab01f32c --- /dev/null +++ b/nkebao/src/pages/mobile/workspace/auto-group/form/api.ts @@ -0,0 +1,11 @@ +import request from "@/api/request"; + +// 新建自动建群任务 +export function createAutoGroup(data: any) { + return request("/api/auto-group/create", data, "POST"); +} + +// 编辑自动建群任务 +export function updateAutoGroup(id: string, data: any) { + return request(`/api/auto-group/update/${id}`, data, "POST"); +} diff --git a/nkebao/src/pages/mobile/workspace/auto-group/form/index.module.scss b/nkebao/src/pages/mobile/workspace/auto-group/form/index.module.scss new file mode 100644 index 000000000..20bf7f92d --- /dev/null +++ b/nkebao/src/pages/mobile/workspace/auto-group/form/index.module.scss @@ -0,0 +1,34 @@ +.autoGroupForm { + padding: 10px; + background: #f7f8fa; +} + +.headerBar { + display: flex; + align-items: center; + height: 48px; + background: #fff; + border-bottom: 1px solid #f0f0f0; + font-size: 18px; + font-weight: 600; + padding: 0 16px; +} + +.title { + font-size: 18px; + font-weight: 600; + color: #222; + flex: 1; + text-align: center; +} + +.timeRangeRow { + display: flex; + align-items: center; + gap: 8px; +} +.groupSizeRow { + display: flex; + align-items: center; + gap: 8px; +} \ No newline at end of file diff --git a/nkebao/src/pages/mobile/workspace/auto-group/form/index.tsx b/nkebao/src/pages/mobile/workspace/auto-group/form/index.tsx new file mode 100644 index 000000000..fd00746fa --- /dev/null +++ b/nkebao/src/pages/mobile/workspace/auto-group/form/index.tsx @@ -0,0 +1,253 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { + Form, + Input, + Button, + Toast, + Switch, + Selector, + TextArea, + NavBar, +} from "antd-mobile"; +import { ArrowLeftOutlined } from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import style from "./index.module.scss"; +import { createAutoGroup, updateAutoGroup } from "./api"; + +const defaultForm = { + name: "", + deviceCount: 1, + targetFriends: 0, + createInterval: 300, + maxGroupsPerDay: 10, + timeRange: { start: "09:00", end: "21:00" }, + groupSize: { min: 20, max: 50 }, + targetTags: [], + groupNameTemplate: "VIP客户交流群{序号}", + groupDescription: "", +}; + +const tagOptions = [ + { label: "VIP客户", value: "VIP客户" }, + { label: "高价值", value: "高价值" }, + { label: "潜在客户", value: "潜在客户" }, + { label: "中意向", value: "中意向" }, +]; + +const AutoGroupForm: React.FC = () => { + const navigate = useNavigate(); + const { id } = useParams(); + const isEdit = Boolean(id); + const [form, setForm] = useState(defaultForm); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (isEdit) { + // 这里应请求详情接口,回填表单,演示用mock + setForm({ + ...defaultForm, + name: "VIP客户建群", + deviceCount: 2, + targetFriends: 156, + createInterval: 300, + maxGroupsPerDay: 20, + timeRange: { start: "09:00", end: "21:00" }, + groupSize: { min: 20, max: 50 }, + targetTags: ["VIP客户", "高价值"], + groupNameTemplate: "VIP客户交流群{序号}", + groupDescription: "VIP客户专属交流群,提供优质服务", + }); + } + }, [isEdit, id]); + + const handleSubmit = async () => { + setLoading(true); + try { + if (isEdit) { + await updateAutoGroup(id as string, form); + Toast.show({ content: "编辑成功" }); + } else { + await createAutoGroup(form); + Toast.show({ content: "创建成功" }); + } + navigate("/workspace/auto-group"); + } catch (e) { + Toast.show({ content: "提交失败" }); + } finally { + setLoading(false); + } + }; + + return ( + + navigate(-1)} + /> + + } + > + + {isEdit ? "编辑建群任务" : "新建建群任务"} + + + } + > +
+
+ {isEdit ? "保存修改" : "创建任务"} + + } + > + + setForm((f: any) => ({ ...f, name: val }))} + placeholder="请输入任务名称" + /> + + + + setForm((f: any) => ({ ...f, deviceCount: Number(val) })) + } + placeholder="请输入设备数量" + /> + + + + setForm((f: any) => ({ ...f, targetFriends: Number(val) })) + } + placeholder="请输入目标好友数" + /> + + + + setForm((f: any) => ({ ...f, createInterval: Number(val) })) + } + placeholder="请输入建群间隔" + /> + + + + setForm((f: any) => ({ ...f, maxGroupsPerDay: Number(val) })) + } + placeholder="请输入最大建群数" + /> + + +
+ + setForm((f: any) => ({ + ...f, + timeRange: { ...f.timeRange, start: val }, + })) + } + placeholder="开始时间" + /> + - + + setForm((f: any) => ({ + ...f, + timeRange: { ...f.timeRange, end: val }, + })) + } + placeholder="结束时间" + /> +
+
+ +
+ + setForm((f: any) => ({ + ...f, + groupSize: { ...f.groupSize, min: Number(val) }, + })) + } + placeholder="最小人数" + /> + - + + setForm((f: any) => ({ + ...f, + groupSize: { ...f.groupSize, max: Number(val) }, + })) + } + placeholder="最大人数" + /> +
+
+ + + setForm((f: any) => ({ ...f, targetTags: val })) + } + /> + + + + setForm((f: any) => ({ ...f, groupNameTemplate: val })) + } + placeholder="请输入群名称模板" + /> + + +