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 @@