From ce55c48c64458c99bb57e39924c90e1b376a3625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A1=E8=8B=A5?= Date: Mon, 20 Apr 2026 11:07:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=8C=E6=AD=A5=E4=BB=8A=E6=97=A5?= =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E4=B8=8E=E5=90=8E=E5=8F=B0=E8=BF=AD?= =?UTF-8?q?=E4=BB=A3=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 集中提交今日 API、管理端、小程序与部署文档调整,确保 Gitea 主分支与本地最新开发版本一致。 Made-with: Cursor --- .gitignore | 3 + admin/src/assets/admin-theme.css | 77 + admin/src/components/TopTestUsersPanel.vue | 109 + admin/src/components/UserJourneyPanel.vue | 278 ++ admin/src/components/UserRfmPanel.vue | 273 ++ admin/src/layouts/AdminLayout.vue | 10 +- admin/src/views/admin/Dashboard.vue | 662 ++--- admin/src/views/admin/Distribution.vue | 44 +- .../src/views/admin/FeishuLeadConfigPanel.vue | 2 +- admin/src/views/admin/Finance.vue | 120 +- .../views/admin/MiniprogramConfigPanel.vue | 6 +- admin/src/views/admin/OrdersHub.vue | 45 +- admin/src/views/admin/PosterEditor.vue | 2 +- admin/src/views/admin/Pricing.vue | 40 +- admin/src/views/admin/PushHookConfigPanel.vue | 3 +- admin/src/views/admin/Questions.vue | 4 +- admin/src/views/admin/Settings.vue | 176 +- admin/src/views/admin/Users.vue | 79 +- admin/src/views/admin/UsersHub.vue | 154 +- admin/src/views/superadmin/AIConfig.vue | 9 +- admin/src/views/superadmin/Database.vue | 2 +- admin/src/views/superadmin/Distribution.vue | 24 +- admin/src/views/superadmin/Enterprises.vue | 17 +- admin/src/views/superadmin/Finance.vue | 38 +- admin/src/views/superadmin/PosterEditor.vue | 2 +- admin/src/views/superadmin/Pricing.vue | 63 +- admin/src/views/superadmin/Questions.vue | 4 +- admin/src/views/superadmin/Settings.vue | 248 +- admin/src/views/superadmin/SoulArticles.vue | 136 +- admin/src/views/superadmin/Users.vue | 23 + api/.env.local.merge.example | 3 + api/app/BaseController.php | 262 +- api/app/common/AnalyticsEventLabels.php | 24 +- api/app/common/AnalyticsPagePathLabels.php | 65 + api/app/common/service/AiCallService.php | 212 +- .../service/AiChatArticleDisplayService.php | 210 +- .../service/FeishuLeadWebhookService.php | 41 + .../common/service/MiniprogramAuditMode.php | 28 + api/app/common/service/MpTabbarService.php | 65 + .../service/OutboundPushHookService.php | 236 +- .../service/ResumeFileExtractService.php | 99 + api/app/common/service/UserJourneyService.php | 50 +- .../common/service/WechatAuditSyncService.php | 126 + api/app/common/service/WechatService.php | 24 + api/app/controller/admin/AppUser.php | 27 +- api/app/controller/admin/Settings.php | 4 +- api/app/controller/api/AiChat.php | 777 ++++-- api/app/controller/api/AiReport.php | 60 +- api/app/controller/api/Analyze.php | 145 +- api/app/controller/api/AppConfig.php | 192 +- api/app/controller/api/Distribution.php | 12 +- api/app/controller/api/InternalPushHook.php | 9 + api/app/controller/api/MpConfig.php | 43 +- api/app/controller/api/Payment.php | 5 + api/app/controller/api/Test.php | 52 +- .../controller/superadmin/Distribution.php | 8 +- api/app/controller/superadmin/Settings.php | 66 +- api/app/model/AiConversation.php | 7 +- api/app/model/AiMessage.php | 7 +- api/database/add_deep_pricing_categories.sql | 14 +- api/database/migrations/add_ai_chat_jobs.sql | 17 + .../patch_ai_messages_isdegraded.sql | 5 + .../update_deep_personal_vip_20260417.sql | 8 + ...ate_distribution_min_withdraw_20260417.sql | 8 + api/mbti_data.sql | 2476 ++++++++--------- api/route/api.php | 12 +- api/scripts/check_ai_chat_ready.php | 75 + api/scripts/mint_jwt_for_smoke.js | 30 + api/scripts/run_sql_file_pdo.php | 64 + api/scripts/smoke_ai_provider.php | 62 + douyin-miniprogram/app.js | 16 +- douyin-miniprogram/pages/index/index.js | 8 +- douyin-miniprogram/pages/promo/index.js | 563 ++-- douyin-miniprogram/pages/promo/index.ttml | 438 +-- douyin-miniprogram/pages/purchase/index.js | 132 +- douyin-miniprogram/pages/purchase/index.ttml | 5 +- douyin-miniprogram/pages/purchase/index.ttss | 22 +- douyin-miniprogram/utils/request.js | 225 +- miniprogram/app.js | 165 +- miniprogram/app.json | 15 +- miniprogram/app.wxss | 7 +- miniprogram/custom-tab-bar/index.js | 15 +- miniprogram/pages/ai-chat/history.js | 5 +- miniprogram/pages/ai-chat/index.js | 1027 +++++-- miniprogram/pages/ai-chat/index.json | 2 +- miniprogram/pages/ai-chat/index.wxml | 182 +- miniprogram/pages/ai-chat/index.wxss | 634 ++--- miniprogram/pages/ai-chat/report.js | 135 +- miniprogram/pages/ai-chat/report.wxml | 4 +- miniprogram/pages/ai-chat/report.wxss | 14 +- miniprogram/pages/ai-test/index.js | 35 +- miniprogram/pages/ai-test/index.wxml | 9 +- miniprogram/pages/ai-test/index.wxss | 4 +- miniprogram/pages/index/camera.json | 1 + miniprogram/pages/index/camera.wxss | 94 +- miniprogram/pages/index/index.js | 23 +- miniprogram/pages/index/index.wxml | 2 +- miniprogram/pages/index/index.wxss | 18 +- miniprogram/pages/index/result.js | 53 +- miniprogram/pages/index/result.wxml | 86 +- miniprogram/pages/index/result.wxss | 4 +- miniprogram/pages/match-job/index.js | 163 +- miniprogram/pages/match-job/index.wxml | 98 +- miniprogram/pages/match-job/index.wxss | 218 +- miniprogram/pages/profile/index.js | 199 +- miniprogram/pages/profile/index.wxml | 15 +- miniprogram/pages/profile/index.wxss | 5 +- miniprogram/pages/promo/index.js | 23 +- miniprogram/pages/promo/index.wxml | 438 +-- miniprogram/pages/promo/index.wxss | 1268 ++++----- miniprogram/pages/purchase/index.js | 215 +- miniprogram/pages/purchase/index.wxml | 21 +- miniprogram/pages/purchase/index.wxss | 74 +- miniprogram/pages/result/disc.js | 59 +- miniprogram/pages/result/disc.json | 7 +- miniprogram/pages/result/disc.wxml | 26 +- miniprogram/pages/result/disc.wxss | 3 +- miniprogram/pages/result/mbti.js | 47 +- miniprogram/pages/result/mbti.json | 7 +- miniprogram/pages/result/mbti.wxml | 26 +- miniprogram/pages/result/mbti.wxss | 3 +- miniprogram/pages/result/pdp.js | 65 +- miniprogram/pages/result/pdp.json | 7 +- miniprogram/pages/result/pdp.wxml | 26 +- miniprogram/pages/result/pdp.wxss | 3 +- miniprogram/pages/result/sbti.js | 49 +- miniprogram/pages/result/sbti.json | 7 +- miniprogram/pages/result/sbti.wxml | 26 +- miniprogram/pages/result/sbti.wxss | 3 +- miniprogram/pages/test-select/index.js | 20 +- miniprogram/pages/test-select/index.wxss | 262 +- miniprogram/pages/test/disc.js | 10 +- miniprogram/pages/test/mbti.js | 8 +- miniprogram/pages/test/pdp.js | 10 +- miniprogram/pages/test/sbti.js | 8 +- miniprogram/pages/user-profile/index.js | 469 ++-- miniprogram/project.config.json | 2 +- miniprogram/project.private.config.json | 88 +- miniprogram/styles/result-page-dashboard.wxss | 11 +- miniprogram/utils/aiChatIntent.js | 105 + miniprogram/utils/deepPricingFilter.js | 101 + miniprogram/utils/miniprogramAuditGate.js | 46 + miniprogram/utils/payment.js | 7 + miniprogram/utils/phoneAuth.js | 97 + miniprogram/utils/recoMiniProgram.js | 27 + miniprogram/utils/request.js | 113 +- miniprogram/utils/resultJourneyState.js | 4 +- miniprogram/utils/resultProfileGate.js | 7 +- scripts/deploy_mbti_bt_api.py | 67 + scripts/deploy_mbti_ssh.sh | 39 + scripts/env.bt.example | 20 + scripts/path1_api.sh | 9 + scripts/requirements_bt_tencent.txt | 2 + scripts/sync_env_bt_from_tencent_ssm.py | 198 ++ scripts/tencent_cos_tat_deploy_mbti.py | 330 +++ 开发文档/.obsidian/workspace.json | 72 +- .../1、需求/修改/ai功能202600417 -7.plan.md | 1 + .../{ => images}/超级管理后台20260417-plan-1 1.md | 0 .../1、需求/修改/小程序202600418 -plan 1.md | 50 + 开发文档/1、需求/修改/用户运营参考Soul规则.md | 123 + .../1、需求/修改/管理后台202600418 -plan 2.md | 27 + .../1、需求/修改/管理后台20260417-plan-1.md | 1 + .../{修改 => 已完成}/MBTI闭环流程图-迭代优化.png | Bin .../{修改 => 已完成}/ai功能202600417 -6.md | 0 .../{修改 => 已完成}/ai功能202600417 -6.plan.md | 0 .../已完成/全站全链路测试与缺陷_20260327.md | 17 +- .../{修改 => 已完成}/超级管理后台20260417-exec.md | 0 开发文档/6、后端/神仙AI异步对话_排障手册.md | 57 + 开发文档/Pasted image 20260418100549.png | Bin 0 -> 285466 bytes 开发文档/Pasted image 20260418100757.png | Bin 0 -> 171111 bytes 开发文档/Pasted image 20260418100914.png | Bin 0 -> 391057 bytes 开发文档/Pasted image 20260418183810.png | Bin 0 -> 481681 bytes 开发文档/Pasted image 20260418184818.png | Bin 0 -> 400650 bytes 开发文档/Pasted image 20260418184929.png | Bin 0 -> 684241 bytes 开发文档/Pasted image 20260418185001.png | Bin 0 -> 476243 bytes 开发文档/Pasted image 20260418185026.png | Bin 0 -> 476243 bytes 开发文档/Pasted image 20260418185217.png | Bin 0 -> 348847 bytes 开发文档/Pasted image 20260418185309.png | Bin 0 -> 693018 bytes 开发文档/Pasted image 20260418185357.png | Bin 0 -> 465978 bytes 开发文档/需求/修复 20260327.md | 2 +- 180 files changed, 11659 insertions(+), 6078 deletions(-) create mode 100644 admin/src/components/TopTestUsersPanel.vue create mode 100644 admin/src/components/UserJourneyPanel.vue create mode 100644 admin/src/components/UserRfmPanel.vue create mode 100644 api/app/common/AnalyticsPagePathLabels.php create mode 100644 api/app/common/service/MiniprogramAuditMode.php create mode 100644 api/app/common/service/MpTabbarService.php create mode 100644 api/app/common/service/ResumeFileExtractService.php create mode 100644 api/app/common/service/WechatAuditSyncService.php create mode 100644 api/database/migrations/add_ai_chat_jobs.sql create mode 100644 api/database/migrations/patch_ai_messages_isdegraded.sql create mode 100644 api/database/update_deep_personal_vip_20260417.sql create mode 100644 api/database/update_distribution_min_withdraw_20260417.sql create mode 100644 api/scripts/check_ai_chat_ready.php create mode 100644 api/scripts/mint_jwt_for_smoke.js create mode 100644 api/scripts/run_sql_file_pdo.php create mode 100644 api/scripts/smoke_ai_provider.php create mode 100644 miniprogram/utils/aiChatIntent.js create mode 100644 miniprogram/utils/deepPricingFilter.js create mode 100644 miniprogram/utils/miniprogramAuditGate.js create mode 100644 miniprogram/utils/recoMiniProgram.js create mode 100644 scripts/env.bt.example create mode 100755 scripts/path1_api.sh create mode 100644 scripts/requirements_bt_tencent.txt create mode 100644 scripts/sync_env_bt_from_tencent_ssm.py create mode 100644 scripts/tencent_cos_tat_deploy_mbti.py rename 开发文档/1、需求/修改/{ => images}/超级管理后台20260417-plan-1 1.md (100%) create mode 100644 开发文档/1、需求/修改/小程序202600418 -plan 1.md create mode 100644 开发文档/1、需求/修改/用户运营参考Soul规则.md create mode 100644 开发文档/1、需求/修改/管理后台202600418 -plan 2.md rename 开发文档/1、需求/{修改 => 已完成}/MBTI闭环流程图-迭代优化.png (100%) rename 开发文档/1、需求/{修改 => 已完成}/ai功能202600417 -6.md (100%) rename 开发文档/1、需求/{修改 => 已完成}/ai功能202600417 -6.plan.md (100%) rename 开发文档/1、需求/{修改 => 已完成}/超级管理后台20260417-exec.md (100%) create mode 100644 开发文档/6、后端/神仙AI异步对话_排障手册.md create mode 100644 开发文档/Pasted image 20260418100549.png create mode 100644 开发文档/Pasted image 20260418100757.png create mode 100644 开发文档/Pasted image 20260418100914.png create mode 100644 开发文档/Pasted image 20260418183810.png create mode 100644 开发文档/Pasted image 20260418184818.png create mode 100644 开发文档/Pasted image 20260418184929.png create mode 100644 开发文档/Pasted image 20260418185001.png create mode 100644 开发文档/Pasted image 20260418185026.png create mode 100644 开发文档/Pasted image 20260418185217.png create mode 100644 开发文档/Pasted image 20260418185309.png create mode 100644 开发文档/Pasted image 20260418185357.png diff --git a/.gitignore b/.gitignore index 3e2a535..3482632 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ build/ .env.*.local .env.local.merge .env.bak.* +# 宝塔 API 本地密钥(deploy_mbti_bt_api.py 可选读取) +scripts/.env.bt +.venv-bt/ # 日志与运行时 *.log diff --git a/admin/src/assets/admin-theme.css b/admin/src/assets/admin-theme.css index f4e4812..71f348f 100644 --- a/admin/src/assets/admin-theme.css +++ b/admin/src/assets/admin-theme.css @@ -1205,3 +1205,80 @@ color: #92400E !important; } +/* ========================================================================== + 统一 tabs · pill-tabs 外观 + 覆盖 Distribution / Pricing / OrdersHub / Finance(superadmin) 等页面里 + 历史遗留的 .custom-tabs-container / .custom-tabs / .tab-item scoped 样式 + 使 admin / superadmin 所有内部子标签视觉一致 + ========================================================================== */ +.admin-layout .custom-tabs-container, +.superadmin-layout .custom-tabs-container { + background-color: #f1f5f9 !important; + padding: 4px !important; + border-radius: 10px !important; + display: inline-flex !important; + max-width: 100% !important; + overflow-x: auto !important; + -webkit-overflow-scrolling: touch !important; + margin-bottom: 20px !important; + width: auto !important; + box-shadow: none !important; + border: 0 !important; +} +.admin-layout .custom-tabs-container .custom-tabs, +.superadmin-layout .custom-tabs-container .custom-tabs { + display: inline-flex !important; + gap: 2px !important; + width: auto !important; + min-width: min-content !important; +} +.admin-layout .custom-tabs-container .tab-item, +.superadmin-layout .custom-tabs-container .tab-item { + flex: 0 0 auto !important; + border: 0 !important; + background: transparent !important; + color: #64748b !important; + font-size: 13px !important; + font-weight: 500 !important; + padding: 7px 18px !important; + border-radius: 6px !important; + cursor: pointer !important; + white-space: nowrap !important; + transition: all 0.18s !important; + line-height: 1.4 !important; + text-align: center !important; + box-shadow: none !important; +} +.admin-layout .custom-tabs-container .tab-item:hover, +.superadmin-layout .custom-tabs-container .tab-item:hover { + color: #0f172a !important; + background: transparent !important; +} +.admin-layout .custom-tabs-container .tab-item.active, +.superadmin-layout .custom-tabs-container .tab-item.active { + background: #ffffff !important; + color: #0f172a !important; + font-weight: 600 !important; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08) !important; +} + +/* ========================================================================== + 统一 page-header · admin/superadmin 页面标题区 + 把 Finance / Settings / Users / Orders / Distribution 等 h2 + .subtitle + 的视觉尺度收敛到 token(不改源文件结构) + ========================================================================== */ +.admin-layout .page-header h2, +.admin-layout .page-header .title { + font-size: 20px !important; + font-weight: 700 !important; + color: var(--admin-text) !important; + letter-spacing: -0.01em !important; + margin: 0 0 4px !important; +} +.admin-layout .page-header .subtitle { + font-size: 13px !important; + color: var(--admin-text-sub) !important; + margin: 0 !important; + line-height: 1.55 !important; +} + diff --git a/admin/src/components/TopTestUsersPanel.vue b/admin/src/components/TopTestUsersPanel.vue new file mode 100644 index 0000000..d363122 --- /dev/null +++ b/admin/src/components/TopTestUsersPanel.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/admin/src/components/UserJourneyPanel.vue b/admin/src/components/UserJourneyPanel.vue new file mode 100644 index 0000000..5075577 --- /dev/null +++ b/admin/src/components/UserJourneyPanel.vue @@ -0,0 +1,278 @@ + + + + + diff --git a/admin/src/components/UserRfmPanel.vue b/admin/src/components/UserRfmPanel.vue new file mode 100644 index 0000000..e429adb --- /dev/null +++ b/admin/src/components/UserRfmPanel.vue @@ -0,0 +1,273 @@ + + + + + diff --git a/admin/src/layouts/AdminLayout.vue b/admin/src/layouts/AdminLayout.vue index 6f5f43c..b6de7c6 100644 --- a/admin/src/layouts/AdminLayout.vue +++ b/admin/src/layouts/AdminLayout.vue @@ -174,7 +174,7 @@ const handleLogout = async () => { width: 34px; height: 34px; border-radius: 8px; - background-color: #7c3aed; + background-color: var(--admin-primary, #4F46E5); display: flex; align-items: center; justify-content: center; @@ -273,8 +273,8 @@ const handleLogout = async () => { } &.active { - background-color: #f5f3ff; - color: #7c3aed; + background-color: var(--admin-primary-soft, #EEF2FF); + color: var(--admin-primary, #4F46E5); font-weight: 500; &::before { @@ -284,11 +284,11 @@ const handleLogout = async () => { top: 0; bottom: 0; width: 3px; - background-color: #7c3aed; + background-color: var(--admin-primary, #4F46E5); } .nav-icon { - color: #7c3aed; + color: var(--admin-primary, #4F46E5); } } diff --git a/admin/src/views/admin/Dashboard.vue b/admin/src/views/admin/Dashboard.vue index 15a3fda..3ce9a13 100644 --- a/admin/src/views/admin/Dashboard.vue +++ b/admin/src/views/admin/Dashboard.vue @@ -1,49 +1,16 @@ @@ -490,50 +517,46 @@ onMounted(() => { color: #374151; } -.custom-tabs-container { - background-color: #f3f4f6; +.pill-tabs { + display: inline-flex; + background: #f1f5f9; padding: 4px; - border-radius: 8px; - display: flex; + border-radius: 10px; + gap: 2px; margin-bottom: 20px; - width: 100%; + max-width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} - .custom-tabs { - display: flex; - gap: 4px; - width: 100%; +.pill-tab { + border: 0; + background: transparent; + color: #64748b; + font-size: 13px; + font-weight: 500; + padding: 7px 18px; + border-radius: 6px; + cursor: pointer; + white-space: nowrap; + transition: all 0.18s; - .tab-item { - flex: 1; - padding: 6px 20px; - font-size: 13px; - color: #6b7280; - cursor: pointer; - border-radius: 6px; - transition: all 0.2s; - white-space: nowrap; - text-align: center; + &:hover { color: #0f172a; } - &:hover { - color: #111827; - } - - &.active { - background-color: #fff; - color: #111827; - font-weight: 600; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - } - } + &.is-active { + background: #ffffff; + color: #0f172a; + font-weight: 600; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08); } } .tab-content-card { background: #fff; - border-radius: 10px; - border: 1px solid #f3f4f6; - padding: 32px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + border-radius: 12px; + border: 1px solid #e2e8f0; + padding: 28px; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 4px 12px rgba(15, 23, 42, 0.03); &.flat-embed { background: transparent; @@ -547,6 +570,39 @@ onMounted(() => { width: 100%; } +.terminal-wrap { + display: flex; + flex-direction: column; + gap: 28px; +} +.terminal-section { + background: #fafafa; + border-radius: 12px; + padding: 20px; + border: 1px solid #f3f4f6; +} +.terminal-section--poster { + padding-bottom: 12px; +} +.terminal-h3 { + margin: 0 0 6px; + font-size: 17px; + font-weight: 700; + color: #111827; +} +.terminal-desc { + margin: 0 0 16px; + font-size: 13px; + color: #6b7280; + line-height: 1.55; +} +.poster-embed { + background: #fff; + border-radius: 10px; + padding: 12px; + border: 1px solid #eef2f7; +} + .tab-content { .content-header { margin-bottom: 32px; diff --git a/admin/src/views/admin/Users.vue b/admin/src/views/admin/Users.vue index 067261c..5bcaea0 100644 --- a/admin/src/views/admin/Users.vue +++ b/admin/src/views/admin/Users.vue @@ -3,7 +3,7 @@ - -
+
👥
-
总用户数
-
{{ total }}
+
用户数量
+
{{ pageUserCount }}
+
符合条件的用户共 {{ total }}
🪞
-
已面容分析
+
已面容 / 面相
{{ profileStats.faceCount }} - / 本页 {{ users.length }} + / {{ pageUserCount }}
@@ -37,7 +37,17 @@
已完成 MBTI
{{ profileStats.mbtiCount }} - / 本页 {{ users.length }} + / {{ pageUserCount }} +
+
+
+
+
🃏
+
+
已完成 SBTI
+
+ {{ profileStats.sbtiCount }} + / {{ pageUserCount }}
@@ -47,7 +57,7 @@
至少一项测试
{{ profileStats.anyTestCount }} - / 本页 {{ users.length }} + / {{ pageUserCount }}
@@ -128,12 +138,13 @@ @@ -172,7 +183,7 @@ v-model:current-page="currentPage" :page-size="pageSize" :total="total" - layout="prev, pager, next" + layout="total, prev, pager, next" @current-change="handlePageChange" /> @@ -731,22 +742,33 @@ const currentTestType = computed(() => (currentTest.value?.testType || '').toLow const users = ref([]) -/** 页内画像汇总 */ +const pageUserCount = computed(() => (users.value || []).length) + +/** 页内画像汇总(严格只遍历当前表格 users,与分页接口返回的本页 list 一致) */ const profileStats = computed(() => { const list = users.value || [] let faceCount = 0 let mbtiCount = 0 + let sbtiCount = 0 let anyTestCount = 0 for (const u of list) { - const hasFace = !!(u.faceMbtiType || u.faceDiscType || u.facePdpType) + const hasFace = !!( + u.faceMbtiType || + u.faceDiscType || + u.facePdpType || + u.faceType || + u.coldFaceLevel + ) const hasMbti = !!u.mbtiType + const hasSbti = !!u.sbtiType const hasDisc = !!u.discType const hasPdp = !!u.pdpType if (hasFace) faceCount++ if (hasMbti) mbtiCount++ - if (hasFace || hasMbti || hasDisc || hasPdp) anyTestCount++ + if (hasSbti) sbtiCount++ + if (hasFace || hasMbti || hasSbti || hasDisc || hasPdp) anyTestCount++ } - return { faceCount, mbtiCount, anyTestCount } + return { faceCount, mbtiCount, sbtiCount, anyTestCount } }) async function loadUsers() { @@ -761,12 +783,13 @@ async function loadUsers() { params.coldFaceLevel = coldFaceFilter.value.join(',') } const res: any = await request.get('/admin/app-users', { params }) - const list = res.data?.list ?? res?.list ?? [] + const payload = res.data ?? res + const list = Array.isArray(payload?.list) ? payload.list : Array.isArray(payload) ? payload : [] users.value = list.map((row: any) => ({ ...row, username: row.username ?? row.nickname ?? ('用户' + row.id) })) - total.value = res.data?.total ?? res?.total ?? 0 + total.value = Number(payload?.total ?? 0) || 0 } catch { users.value = [] total.value = 0 @@ -1212,6 +1235,13 @@ onMounted(() => { gap: 12px; margin-bottom: 16px; + &.profile-summary--five { + grid-template-columns: repeat(5, minmax(0, 1fr)); + @media (max-width: 1100px) { + grid-template-columns: repeat(2, 1fr); + } + } + @media (max-width: 900px) { grid-template-columns: repeat(2, 1fr); } @@ -1246,6 +1276,7 @@ onMounted(() => { &.ic-blue { background: #eff6ff; } &.ic-teal { background: #f0fdfa; } &.ic-indigo { background: #eef2ff; } + &.ic-violet { background: #f5f3ff; } &.ic-amber { background: #fffbeb; } } @@ -1273,6 +1304,14 @@ onMounted(() => { margin-left: 6px; } } + + .summary-foot { + margin-top: 8px; + font-size: 12px; + color: #6b7280; + line-height: 1.4; + font-weight: 400; + } } .search-section { @@ -1408,6 +1447,12 @@ onMounted(() => { cursor: pointer; } + :deep(.tag-sbti.el-tag) { + --el-tag-bg-color: #f5f3ff; + --el-tag-border-color: #ddd6fe; + --el-tag-text-color: #5b21b6; + } + .no-test { font-size: 13px; color: #9ca3af; diff --git a/admin/src/views/admin/UsersHub.vue b/admin/src/views/admin/UsersHub.vue index 59f159c..b3d0926 100644 --- a/admin/src/views/admin/UsersHub.vue +++ b/admin/src/views/admin/UsersHub.vue @@ -2,31 +2,29 @@

用户运营

-

测试用户数据、小程序展示文案与分销海报一站式维护

-
-
-
- {{ t.label }} -
-
+
+
- -
- -
+ + +
@@ -35,10 +33,11 @@ import { ref, watch, onMounted } from 'vue' import { useRoute, useRouter } from 'vue-router' import Users from './Users.vue' -import PosterEditor from './PosterEditor.vue' -import MiniprogramConfigPanel from './MiniprogramConfigPanel.vue' +import TopTestUsersPanel from '@/components/TopTestUsersPanel.vue' +import UserJourneyPanel from '@/components/UserJourneyPanel.vue' +import UserRfmPanel from '@/components/UserRfmPanel.vue' -const TAB_IDS = ['users', 'miniprogram', 'poster'] as const +const TAB_IDS = ['users', 'journey', 'rfm', 'top20'] as const type TabId = (typeof TAB_IDS)[number] function isTabId(s: string): s is TabId { @@ -48,16 +47,21 @@ function isTabId(s: string): s is TabId { const route = useRoute() const router = useRouter() const activeTab = ref('users') -const miniRef = ref | null>(null) const innerTabs: { label: string; value: TabId }[] = [ { label: '用户列表', value: 'users' }, - { label: '小程序配置', value: 'miniprogram' }, - { label: '海报配置', value: 'poster' } + { label: '旅程漏斗', value: 'journey' }, + { label: 'RFM 价值分层', value: 'rfm' }, + { label: '测评 Top 20', value: 'top20' } ] const applyRouteTab = () => { const t = route.query.tab + if (t === 'miniprogram' || t === 'poster') { + router.replace({ path: '/admin/settings', query: { tab: 'terminal' } }) + activeTab.value = 'users' + return + } if (typeof t === 'string' && isTabId(t)) { activeTab.value = t } else { @@ -83,23 +87,11 @@ watch( () => route.query.tab, () => { applyRouteTab() - if (activeTab.value === 'miniprogram') { - miniRef.value?.loadMiniprogramConfig?.() - } } ) -watch(activeTab, (tab) => { - if (tab === 'miniprogram') { - miniRef.value?.loadMiniprogramConfig?.() - } -}) - onMounted(() => { applyRouteTab() - if (activeTab.value === 'miniprogram') { - miniRef.value?.loadMiniprogramConfig?.() - } }) @@ -114,77 +106,55 @@ onMounted(() => { margin-bottom: 20px; h2 { - margin: 0 0 6px; + margin: 0; font-size: 22px; font-weight: 700; color: #111827; } - - .hub-subtitle { - margin: 0; - font-size: 14px; - color: #6b7280; - } } -.custom-tabs-container { - background-color: #f3f4f6; +.pill-tabs { + display: inline-flex; + background: #f1f5f9; padding: 4px; - border-radius: 8px; + border-radius: 10px; + gap: 2px; margin-bottom: 20px; - width: 100%; + max-width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} - &.tabs-scroll { - overflow-x: auto; - -webkit-overflow-scrolling: touch; +.pill-tab { + border: 0; + background: transparent; + color: #64748b; + font-size: 13px; + font-weight: 500; + padding: 7px 18px; + border-radius: 6px; + cursor: pointer; + white-space: nowrap; + transition: all 0.18s; + + &:hover { + color: #0f172a; } - .custom-tabs { - display: flex; - gap: 4px; - min-width: min-content; - - &.tabs-many .tab-item { - flex: 0 0 auto; - padding: 8px 16px; - font-size: 13px; - } - - .tab-item { - flex: 1; - padding: 8px 18px; - font-size: 14px; - color: #6b7280; - cursor: pointer; - border-radius: 6px; - white-space: nowrap; - text-align: center; - transition: all 0.2s; - - &:hover { - color: #111827; - } - - &.active { - background: #fff; - color: #111827; - font-weight: 600; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); - } - } + &.is-active { + background: #ffffff; + color: #0f172a; + font-weight: 600; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08); } } .hub-card { background: #fff; - border-radius: 10px; - border: 1px solid #f3f4f6; - padding: 28px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); - - &.no-pad { - padding: 16px; - } + border-radius: 12px; + border: 1px solid #e2e8f0; + padding: 24px; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 4px 12px rgba(15, 23, 42, 0.03); &.flat-embed { background: transparent; @@ -193,8 +163,4 @@ onMounted(() => { padding: 0; } } - -.poster-wrap { - min-height: 720px; -} diff --git a/admin/src/views/superadmin/AIConfig.vue b/admin/src/views/superadmin/AIConfig.vue index 9d9e356..29144a2 100644 --- a/admin/src/views/superadmin/AIConfig.vue +++ b/admin/src/views/superadmin/AIConfig.vue @@ -21,6 +21,13 @@
+ + + 小程序「神仙 AI」不设每日对话条数上限,由本页启用的服务商直接回复。若用户端仍提示「今日对话次数已用完」,请将服务器上的 + api/app/controller/api/AiChat.phpapi/app/controller/api/AppConfig.php + 更新为仓库最新版并重启 PHP。 + + 管理数据库连接、备份和恢复

- + 备份数据库
diff --git a/admin/src/views/superadmin/Distribution.vue b/admin/src/views/superadmin/Distribution.vue index 0f40fb6..8ed9e20 100644 --- a/admin/src/views/superadmin/Distribution.vue +++ b/admin/src/views/superadmin/Distribution.vue @@ -257,7 +257,7 @@
- +
@@ -315,7 +315,7 @@
- 保存配置 + 保存配置
@@ -386,7 +386,7 @@ const loadingTopAgents = ref(false) const distEnabled = ref(true) const promoCenterTitle = ref('推广中心') -const minWithdraw = ref(1) +const minWithdraw = ref(0.01) const maxWithdraw = ref(0) const requireAudit = ref(true) const withdrawFee = ref(0) @@ -540,7 +540,7 @@ const loadSettings = async () => { const d = res.data distEnabled.value = d.enabled ?? true promoCenterTitle.value = d.promoCenterTitle ?? '推广中心' - minWithdraw.value = d.minWithdraw ?? 1 + minWithdraw.value = d.minWithdraw ?? 0.01 maxWithdraw.value = d.maxWithdraw ?? 0 requireAudit.value = d.requireAudit !== false withdrawFee.value = d.withdrawFee ?? 0 @@ -617,21 +617,7 @@ onMounted(() => { loadOverview(); loadTopAgents() }) } } -.custom-tabs-container { - background-color: #f3f4f6; padding: 4px; - border-radius: 8px; display: flex; margin-bottom: 24px; width: 100%; - .custom-tabs { - display: flex; gap: 4px; width: 100%; - .tab-item { - flex: 1; display: flex; align-items: center; justify-content: center; - padding: 8px 0; font-size: 13px; color: #6b7280; - cursor: pointer; border-radius: 6px; transition: all 0.2s; - white-space: nowrap; - &:hover { color: #111827; } - &.active { background: #fff; color: #111827; font-weight: 600; box-shadow: 0 1px 2px rgba(0,0,0,.05); } - } - } -} +/* .custom-tabs-container 视觉已统一在 admin-theme.css */ .overview-section { .stats-grid { diff --git a/admin/src/views/superadmin/Enterprises.vue b/admin/src/views/superadmin/Enterprises.vue index 39a9ceb..3aebb32 100644 --- a/admin/src/views/superadmin/Enterprises.vue +++ b/admin/src/views/superadmin/Enterprises.vue @@ -9,7 +9,7 @@ 刷新 - + 新建企业 @@ -46,7 +46,7 @@ 刷新 - + 新建企业 @@ -254,7 +254,7 @@