diff --git a/admin/.env.production b/admin/.env.production index 100804b..51f1b09 100644 --- a/admin/.env.production +++ b/admin/.env.production @@ -1,7 +1,10 @@ -# 生产环境配置 -# 正式环境API地址 -VITE_API_BASE_URL=https://mbtiapi.quwanzhi.com - -# 环境标识 -VITE_APP_ENV=production -VITE_APP_TITLE=MBTI管理后台 \ No newline at end of file +# 生产环境配置 +# 正式环境API地址 +VITE_API_BASE_URL=https://mbtiapi.quwanzhi.com + +# 环境标识 +VITE_APP_ENV=production +VITE_APP_TITLE=MBTI管理后台 + +# 出站推送「测试与调试」:不设或 false = 超管后台不显示;仅必要时改为 true 后重新构建 +# VITE_SHOW_OUTBOUND_PUSH_DEBUG=true \ No newline at end of file diff --git a/admin/src/utils/outboundPushDebug.ts b/admin/src/utils/outboundPushDebug.ts new file mode 100644 index 0000000..45cd380 --- /dev/null +++ b/admin/src/utils/outboundPushDebug.ts @@ -0,0 +1,15 @@ +/** + * 出站推送「测试与调试」区块是否展示(超管后台)。 + * - 生产构建默认不展示; + * - 本地 `npm run dev`(import.meta.env.DEV)默认展示; + * - 生产若需临时调试,在构建前于 .env 设置 VITE_SHOW_OUTBOUND_PUSH_DEBUG=true。 + */ +export function isOutboundPushDebugVisible(): boolean { + if (import.meta.env.VITE_SHOW_OUTBOUND_PUSH_DEBUG === 'false') { + return false + } + return ( + Boolean(import.meta.env.DEV) || + import.meta.env.VITE_SHOW_OUTBOUND_PUSH_DEBUG === 'true' + ) +} diff --git a/admin/src/views/admin/PushHookConfigPanel.vue b/admin/src/views/admin/PushHookConfigPanel.vue index cc54ed1..2f52d72 100644 --- a/admin/src/views/admin/PushHookConfigPanel.vue +++ b/admin/src/views/admin/PushHookConfigPanel.vue @@ -17,37 +17,7 @@ 留空「订阅事件」表示三类事件全部推送。

-
-

测试与调试

-

- 发送连接测试:只验证 URL 是否可达(hook.ping),不写去重表。手动测试测评完成:按库表 - test_results.id 重放真实 test.result_completed(人脸、问卷、简历等均可)。 -

-
- 发送连接测试 -
-
- 记录 ID - - 强制 - - - 手动测试测评完成 - -
-
+ /api/ai/chat
@@ -81,7 +51,11 @@ {{ opt.label }} -

与文档一致:lead.order_paidlead.phone_boundtest.result_completed。全部勾选保存后将以「空列表」存库,表示订阅全部。上方「手动测试测评完成」与真实落库推送使用同一套订阅校验。

+

+ 与文档一致:lead.order_paidlead.phone_boundtest.result_completed。全部勾选保存后将以「空列表」存库,表示订阅全部。 + + +

@@ -103,8 +77,10 @@ const props = withDefaults( defineProps<{ /** 如 /admin 或 /superadmin,对应 request 基路径下的 settings */ apiPrefix: string + /** 是否显示「测试与调试」(默认 false;超管由父组件按环境传入) */ + showTestTools?: boolean }>(), - { apiPrefix: '/admin' } + { apiPrefix: '/admin', showTestTools: false } ) const eventOptions = [ diff --git a/admin/src/views/admin/Settings.vue b/admin/src/views/admin/Settings.vue index ab75542..91d5405 100644 --- a/admin/src/views/admin/Settings.vue +++ b/admin/src/views/admin/Settings.vue @@ -153,7 +153,7 @@
- +
diff --git a/admin/src/views/superadmin/Settings.vue b/admin/src/views/superadmin/Settings.vue index aad07dc..69ebc78 100644 --- a/admin/src/views/superadmin/Settings.vue +++ b/admin/src/views/superadmin/Settings.vue @@ -385,7 +385,7 @@
- +
@@ -474,6 +474,7 @@ import { } from '@element-plus/icons-vue' import { ElMessage } from 'element-plus' import { request } from '@/utils/request' +import { isOutboundPushDebugVisible } from '@/utils/outboundPushDebug' import PosterEditor from './PosterEditor.vue' import Questions from './Questions.vue' import PushHookConfigPanel from '../admin/PushHookConfigPanel.vue' @@ -496,6 +497,9 @@ function isTabId(s: string): s is TabId { const route = useRoute() const router = useRouter() +/** 出站推送测试区:仅开发或 VITE_SHOW_OUTBOUND_PUSH_DEBUG=true,生产包默认隐藏 */ +const showOutboundPushDebug = isOutboundPushDebugVisible() + const activeTab = ref('review') const saveSuccess = ref(null) diff --git a/api/app/common/service/MpTabbarService.php b/api/app/common/service/MpTabbarService.php index 9137eeb..61619c6 100644 --- a/api/app/common/service/MpTabbarService.php +++ b/api/app/common/service/MpTabbarService.php @@ -25,10 +25,20 @@ class MpTabbarService foreach ($items as $row) { $iconKey = $row['iconKey'] ?? 'home'; $iconUrl = $row['iconUrl'] ?? null; + $iconUrlActive = null; if ($iconKey === 'ai') { - $iconUrl = '/images/shenxian-oldman-circle.png'; + $u = is_string($iconUrl) ? trim($iconUrl) : ''; + if ($u !== '') { + $iconUrl = $u; + $iconUrlActive = isset($row['iconUrlActive']) && $row['iconUrlActive'] !== '' + ? $row['iconUrlActive'] + : '/images/tab-ai-active.png'; + } else { + $iconUrl = null; + $iconUrlActive = null; + } } - $list[] = [ + $item = [ 'id' => (int) $row['id'], 'pagePath' => $row['pagePath'], 'text' => $row['text'], @@ -37,16 +47,21 @@ class MpTabbarService 'highlight' => (int) ($row['highlight'] ?? 0) === 1, 'badgeKey' => $row['badgeKey'] ?? null, ]; + if ($iconUrlActive !== null) { + $item['iconUrlActive'] = $iconUrlActive; + } + $list[] = $item; } } catch (\Throwable $e) { $list = []; } if (empty($list)) { + // 顺序:首页 · 第2项凸起拍摄 · 神仙AI · 我(与小程序 app.json tabBar.list 一致) $list = [ ['id' => 0, 'pagePath' => 'pages/index/index', 'text' => '首页', 'iconKey' => 'home', 'iconUrl' => null, 'highlight' => false, 'badgeKey' => null], ['id' => 0, 'pagePath' => 'pages/index/camera', 'text' => '拍摄', 'iconKey' => 'camera', 'iconUrl' => null, 'highlight' => true, 'badgeKey' => null], - ['id' => 0, 'pagePath' => 'pages/ai-chat/index', 'text' => '神仙AI', 'iconKey' => 'ai', 'iconUrl' => '/images/shenxian-oldman-circle.png', 'highlight' => false, 'badgeKey' => null], + ['id' => 0, 'pagePath' => 'pages/ai-chat/index', 'text' => '神仙AI', 'iconKey' => 'ai', 'iconUrl' => null, 'highlight' => false, 'badgeKey' => null], ['id' => 0, 'pagePath' => 'pages/profile/index', 'text' => '我', 'iconKey' => 'profile', 'iconUrl' => null, 'highlight' => false, 'badgeKey' => null], ]; } diff --git a/api/app/common/service/OutboundPushHookService.php b/api/app/common/service/OutboundPushHookService.php index a558e8e..ef01399 100644 --- a/api/app/common/service/OutboundPushHookService.php +++ b/api/app/common/service/OutboundPushHookService.php @@ -511,10 +511,12 @@ class OutboundPushHookService 'X-MBTI-Internal-Signature: ' . self::signAsyncInternalDispatch($body, $timestamp), ]; - if (!self::postJsonAsyncNoWait($url, $body, $headers)) { + $verifyHttp2xx = (($payload['job'] ?? '') === 'ai.chat_turn'); + + if (!self::postJsonAsyncNoWait($url, $body, $headers, $verifyHttp2xx)) { // 偶发 TLS 握手/链路抖动:短间隔重试一次再回落 shutdown usleep(150000); - if (!self::postJsonAsyncNoWait($url, $body, $headers)) { + if (!self::postJsonAsyncNoWait($url, $body, $headers, $verifyHttp2xx)) { Log::warning('OutboundPushHook async enqueue failed', [ 'url' => self::maskUrl($url), 'payload' => $payload, @@ -566,11 +568,13 @@ class OutboundPushHookService } /** - * fire-and-forget 异步 POST:只负责把请求投出去,不等待接口处理完成。 + * fire-and-forget 异步 POST:把请求写出;可选读取 HTTP 首行状态码。 + * - verifyHttp2xx=false:与旧版一致,仅判断 fwrite(其它 job 可能同步较慢,不宜等响应头)。 + * - verifyHttp2xx=true:用于 ai.chat_turn(dispatch 须先快速返回 2xx,否则会误判失败且无法触发 AiChat shutdown 兜底)。 * * @param array $headers */ - private static function postJsonAsyncNoWait(string $url, string $body, array $headers): bool + private static function postJsonAsyncNoWait(string $url, string $body, array $headers, bool $verifyHttp2xx = false): bool { $parts = parse_url($url); if (!is_array($parts) || empty($parts['host'])) { @@ -633,9 +637,44 @@ class OutboundPushHookService $rawRequest = implode("\r\n", $requestLines) . "\r\n\r\n" . $body; $written = @fwrite($socket, $rawRequest); - @fclose($socket); + if ($written === false) { + @fclose($socket); - return $written !== false; + return false; + } + + if (!$verifyHttp2xx) { + @fclose($socket); + + return true; + } + + $statusLine = @fgets($socket); + @fclose($socket); + if ($statusLine === false || $statusLine === '') { + Log::warning('OutboundPushHook async: no HTTP status line', ['url' => self::maskUrl($url)]); + + return false; + } + if (!preg_match('#HTTP/\d\.\d\s+(\d{3})#', $statusLine, $m)) { + Log::warning('OutboundPushHook async: bad status line', [ + 'url' => self::maskUrl($url), + 'prefix' => mb_substr($statusLine, 0, 80), + ]); + + return false; + } + $code = (int) $m[1]; + if ($code < 200 || $code >= 300) { + Log::warning('OutboundPushHook async: non-2xx from internal dispatch', [ + 'url' => self::maskUrl($url), + 'http' => $code, + ]); + + return false; + } + + return true; } /** diff --git a/api/app/controller/admin/Settings.php b/api/app/controller/admin/Settings.php index ca0ab7d..dbcd2b3 100644 --- a/api/app/controller/admin/Settings.php +++ b/api/app/controller/admin/Settings.php @@ -554,17 +554,11 @@ class Settings extends BaseController /** * POST /api/v1/admin/settings/push-hook/test * 向当前解析到的 URL 发送 hook.ping(不写去重表) + * 企业后台不提供:请在超管后台调试,避免误触对外 URL / 重放业务数据。 */ public function testPushHookConfig() { - $user = $this->request->user ?? null; - if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { - return error('无权限访问', 403); - } - $ctx = $this->resolveAdminPushHookEnterpriseId(); - $r = OutboundPushHookService::sendTestPing($ctx); - - return success($r, $r['ok'] ? '测试推送已发出' : ($r['message'] ?? '测试失败')); + return error('连接测试与重放调试仅在超管后台开放', 403); } /** @@ -573,16 +567,7 @@ class Settings extends BaseController */ public function testPushHookTestResult() { - $user = $this->request->user ?? null; - if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { - return error('无权限访问', 403); - } - - $testResultId = (int) Request::param('testResultId', 0); - $force = (int) Request::param('force', 0) === 1; - $r = OutboundPushHookService::replayTestResultForDebug($testResultId, $force); - - return success($r, $r['message'] ?? ($r['ok'] ? '已重放推送' : '重放失败')); + return error('连接测试与重放调试仅在超管后台开放', 403); } /** diff --git a/api/app/controller/api/InternalPushHook.php b/api/app/controller/api/InternalPushHook.php index 64f6457..3250bc9 100644 --- a/api/app/controller/api/InternalPushHook.php +++ b/api/app/controller/api/InternalPushHook.php @@ -55,7 +55,14 @@ class InternalPushHook extends BaseController if ($uid <= 0 || $cid <= 0 || $jid === '' || strlen($jid) > 64 || !preg_match('/^[a-f0-9]+$/', $jid)) { return error('invalid ai chat job', 400); } - \app\controller\api\AiChat::runDeferredChatJob($uid, $cid, $jid); + // 大模型耗时长:须先结束本 HTTP 响应,再在 shutdown 中跑任务;否则主请求里 postJsonAsyncNoWait 无法读到 2xx,且易误判投递失败 + register_shutdown_function(static function () use ($uid, $cid, $jid) { + try { + \app\controller\api\AiChat::runDeferredChatJob($uid, $cid, $jid); + } catch (\Throwable $e) { + Log::error('InternalPushHook ai.chat_turn deferred: ' . $e->getMessage()); + } + }); break; default: return error('unsupported job', 400); diff --git a/api/database/migrations/add_tabbar_and_ai_profit_sharing.sql b/api/database/migrations/add_tabbar_and_ai_profit_sharing.sql index 341529b..bd5a579 100644 --- a/api/database/migrations/add_tabbar_and_ai_profit_sharing.sql +++ b/api/database/migrations/add_tabbar_and_ai_profit_sharing.sql @@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS `mbti_mp_tabbar_items` ( KEY `idx_sort` (`sortOrder`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='小程序 TabBar 后台可配'; --- 初始 4 项(默认顺序:首页·拍摄·神仙AI·我) +-- 初始 4 项(默认顺序:首页·拍摄(第2项凸起)·神仙AI·我,与 app.json tabBar 一致) INSERT INTO `mbti_mp_tabbar_items` (sortOrder, pagePath, text, iconKey, highlight, visible, createdAt, updatedAt) VALUES (10, 'pages/index/index', '首页', 'home', 0, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), (20, 'pages/index/camera', '拍摄', 'camera', 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), diff --git a/api/database/migrations/reorder_mp_tabbar_fab_center.sql b/api/database/migrations/reorder_mp_tabbar_fab_center.sql new file mode 100644 index 0000000..3879344 --- /dev/null +++ b/api/database/migrations/reorder_mp_tabbar_fab_center.sql @@ -0,0 +1,7 @@ +-- 已有库迁移:底部 Tab 顺序改为「首页 · 拍摄(第2项凸起) · 神仙AI · 我」 +-- 在已存在 mbti_mp_tabbar_items 数据时执行;按 pagePath 更新,不依赖自增 id + +UPDATE `mbti_mp_tabbar_items` SET `sortOrder` = 10, `highlight` = 0 WHERE `pagePath` = 'pages/index/index'; +UPDATE `mbti_mp_tabbar_items` SET `sortOrder` = 20, `highlight` = 1 WHERE `pagePath` = 'pages/index/camera'; +UPDATE `mbti_mp_tabbar_items` SET `sortOrder` = 30, `highlight` = 0 WHERE `pagePath` = 'pages/ai-chat/index'; +UPDATE `mbti_mp_tabbar_items` SET `sortOrder` = 40, `highlight` = 0 WHERE `pagePath` = 'pages/profile/index'; diff --git a/miniprogram/app.js b/miniprogram/app.js index d39a195..77e1cb4 100644 --- a/miniprogram/app.js +++ b/miniprogram/app.js @@ -59,6 +59,7 @@ App({ // API 基础地址:默认走线上;本机/内网调试可在开发者工具执行 // wx.setStorageSync('apiBaseOverride', 'https://你的调试域名') 后重启小程序 apiBase: 'https://mbtiapi.quwanzhi.com', + //apiBase: 'http://mbti.com', // VIP信息 vipInfo: null, // 测试次数 @@ -687,6 +688,10 @@ App({ wx.setStorageSync('tabBarConfig', data.tabBar) } catch (e) {} } + try { + const { applyAuditUiOverride } = require('./utils/miniprogramAuditGate.js') + applyAuditUiOverride(this) + } catch (e) {} this.refreshCustomTabBar() if (data.aiQuickQuestions && typeof data.aiQuickQuestions === 'object') { this.globalData.runtimeAiQuickQuestions = data.aiQuickQuestions diff --git a/miniprogram/custom-tab-bar/index.js b/miniprogram/custom-tab-bar/index.js index 3809991..7ed7b40 100644 --- a/miniprogram/custom-tab-bar/index.js +++ b/miniprogram/custom-tab-bar/index.js @@ -1,13 +1,13 @@ // 自定义 TabBar:完全由后台配置驱动 // 数据来源:app.globalData.tabBar.items(GET /api/mp/tabbar) -// 兜底:若配置缺失,使用默认 4 项(首页·拍摄·神仙AI·我) +// 兜底:首页 · 拍摄(第2项凸起) · 神仙AI · 我 -const { isAuditHideAiMode } = require('../utils/miniprogramAuditGate.js') +const { isAuditHideAiMode, shouldHideTabBarHighlightFab } = require('../utils/miniprogramAuditGate.js') const DEFAULT_ITEMS = [ { pagePath: 'pages/index/index', text: '首页', iconKey: 'home', iconUrl: '', highlight: false }, { pagePath: 'pages/index/camera', text: '拍摄', iconKey: 'camera', iconUrl: '', highlight: true }, - { pagePath: 'pages/ai-chat/index', text: '神仙AI', iconKey: 'ai', iconUrl: '/images/shenxian-ai-logo.png', highlight: false }, + { pagePath: 'pages/ai-chat/index', text: '神仙AI', iconKey: 'ai', iconUrl: '', iconUrlActive: '', highlight: false }, { pagePath: 'pages/profile/index', text: '我', iconKey: 'profile', iconUrl: '', highlight: false } ] @@ -38,17 +38,25 @@ Component({ const gd = app.globalData || {} const cfg = gd.tabBar && Array.isArray(gd.tabBar.items) ? gd.tabBar.items : null const rawItems = (cfg && cfg.length >= 2) ? cfg.slice() : DEFAULT_ITEMS.slice() - const items = rawItems.map((it) => ({ - ...it, - // 仅神仙AI使用指定 logo;其他图标保持后台配置 - iconUrl: it.iconKey === 'ai' ? '/images/shenxian-ai-logo.png' : (it.iconUrl || '') - })) + const items = rawItems.map((it) => { + if (it.iconKey === 'ai') { + const u = (it.iconUrl && String(it.iconUrl).trim()) || '' + const raster = /\.(png|jpg|jpeg|webp)$/i.test(u) + if (raster) { + return { + ...it, + iconUrl: u, + iconUrlActive: (it.iconUrlActive && String(it.iconUrlActive).trim()) || '/images/tab-ai-active.png' + } + } + return { ...it, iconUrl: '', iconUrlActive: '' } + } + return { ...it, iconUrl: it.iconUrl || '' } + }) const reviewMode = !!(gd.reviewMode || gd.maintenanceMode) const hideAiTab = isAuditHideAiMode(gd) - const ep = gd.enterprisePermissions - const faceOff = !!(ep && ep.face === false) - const hideMiddleFab = reviewMode || faceOff + const hideMiddleFab = shouldHideTabBarHighlightFab(gd) // 提审或面相审核:去掉神仙 AI Tab(与 miniprogramAuditGate 一致) // 审核模式 / face 关闭时:隐藏所有 highlight=true 的 Tab diff --git a/miniprogram/custom-tab-bar/index.wxml b/miniprogram/custom-tab-bar/index.wxml index 7ec3b1c..84235a4 100644 --- a/miniprogram/custom-tab-bar/index.wxml +++ b/miniprogram/custom-tab-bar/index.wxml @@ -1,4 +1,4 @@ - + @@ -27,8 +27,8 @@ > "); } -/* ===== 图标:ai (sparkle) ===== */ +/* ===== 图标:ai(神仙AI · 未选中 #999 / 选中 #7c3aed) ===== */ .tab-icon--ai { - background-image: url("data:image/svg+xml;utf8,"); + background-image: url("data:image/svg+xml;utf8,"); } .tab-icon--ai.tab-icon--active, .tab-item.active .tab-icon--ai { - background-image: url("data:image/svg+xml;utf8,"); + background-image: url("data:image/svg+xml;utf8,"); } /* ===== 图标:profile ===== */ @@ -117,16 +128,16 @@ background-image: url("data:image/svg+xml;utf8,"); } -/* ===== 中间浮钮 ===== */ +/* ===== 中间浮钮(第 2 项:拍摄) ===== */ .tab-slot-middle { flex: 1; min-width: 0; position: relative; - z-index: 4; + z-index: 6; display: flex; flex-direction: column; align-items: center; - justify-content: center; + justify-content: flex-end; padding-bottom: 0; } @@ -134,40 +145,46 @@ display: flex; flex-direction: column; align-items: center; - justify-content: center; + justify-content: flex-start; width: 100%; - transform: translateY(-26rpx); - margin-bottom: -10rpx; + transform: translateY(-40rpx); + margin-bottom: -36rpx; } .center-circle { - width: 84rpx; - height: 84rpx; - min-width: 84rpx; - min-height: 84rpx; + width: 96rpx; + height: 96rpx; + min-width: 96rpx; + min-height: 96rpx; border-radius: 50%; - background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%); + background: linear-gradient(145deg, #7c3aed 0%, #a78bfa 55%, #8b5cf6 100%); display: flex; align-items: center; justify-content: center; - margin-bottom: 6rpx; - box-shadow: 0 4rpx 20rpx rgba(124, 58, 237, 0.4); - border: 4rpx solid #ffffff; + margin-bottom: 8rpx; + border: 5rpx solid #ffffff; flex-shrink: 0; - overflow: hidden; + overflow: visible; + box-shadow: + 0 0 0 1rpx rgba(167, 139, 250, 0.35), + 0 12rpx 36rpx rgba(124, 58, 237, 0.48), + 0 0 40rpx rgba(124, 58, 237, 0.28); } .middle-fab.active .center-circle { - background: linear-gradient(135deg, #6d28d9 0%, #7c3aed 100%); - box-shadow: 0 4rpx 20rpx rgba(124, 58, 237, 0.6); + background: linear-gradient(145deg, #6d28d9 0%, #7c3aed 50%, #a78bfa 100%); + box-shadow: + 0 0 0 1rpx rgba(196, 181, 253, 0.45), + 0 14rpx 40rpx rgba(124, 58, 237, 0.58), + 0 0 48rpx rgba(124, 58, 237, 0.35); } .center-svg { - width: 44rpx; - height: 44rpx; + width: 46rpx; + height: 46rpx; background-repeat: no-repeat; background-position: center; - background-size: 42rpx 42rpx; + background-size: 44rpx 44rpx; } .center-svg--camera { @@ -178,11 +195,12 @@ } .tab-text-fab { - font-size: 18rpx; + font-size: 20rpx; color: #999999; line-height: 1.25; text-align: center; max-width: 168rpx; + margin-top: 2rpx; } .middle-fab.active .tab-text-fab { diff --git a/miniprogram/images/tab-ai-active.png b/miniprogram/images/tab-ai-active.png new file mode 100644 index 0000000..959be94 Binary files /dev/null and b/miniprogram/images/tab-ai-active.png differ diff --git a/miniprogram/images/tab-ai.png b/miniprogram/images/tab-ai.png new file mode 100644 index 0000000..2f0c3d4 Binary files /dev/null and b/miniprogram/images/tab-ai.png differ diff --git a/miniprogram/pages/ai-chat/index.json b/miniprogram/pages/ai-chat/index.json index a012767..da55d1a 100644 --- a/miniprogram/pages/ai-chat/index.json +++ b/miniprogram/pages/ai-chat/index.json @@ -1,7 +1,7 @@ { "navigationBarTitleText": "神仙 AI", - "navigationBarBackgroundColor": "#ffffff", + "navigationBarBackgroundColor": "#faf9ff", "navigationBarTextStyle": "black", - "backgroundColor": "#ffffff", + "backgroundColor": "#f4f3f9", "usingComponents": {} } diff --git a/miniprogram/pages/ai-chat/index.wxml b/miniprogram/pages/ai-chat/index.wxml index 791e746..d0fc1f2 100644 --- a/miniprogram/pages/ai-chat/index.wxml +++ b/miniprogram/pages/ai-chat/index.wxml @@ -82,7 +82,7 @@ - + - 📎 + 简历 "); } .resume-entry-txt { font-size: 20rpx; - color: #6b7280; - margin-top: 2rpx; + color: #6d28d9; + font-weight: 600; + margin-top: 4rpx; line-height: 1.2; } .input { flex: 1; min-width: 0; - background: #F3F4F6; + background: #f3f4f6; border-radius: 100rpx; - padding: 16rpx 28rpx; + padding: 18rpx 30rpx; font-size: 28rpx; - min-height: 72rpx; - color: #1F1B4D; + min-height: 76rpx; + color: #1f2937; + border: 1rpx solid rgba(229, 231, 235, 0.95); } .input-ph { - color: #9CA3AF; + color: #9ca3af; font-size: 26rpx; } @@ -490,10 +525,7 @@ page { .send-icon-btn--active { background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%) !important; -} - -.send-icon-btn--active .send-icon-plane { - filter: brightness(0) invert(1); + box-shadow: 0 6rpx 18rpx rgba(124, 58, 237, 0.35); } .send-icon-btn--hover { @@ -501,11 +533,15 @@ page { } .send-icon-plane { - width: 36rpx; - height: 36rpx; + width: 34rpx; + height: 34rpx; background-repeat: no-repeat; background-position: center; - background-size: 36rpx 36rpx; - background-image: url("data:image/svg+xml;utf8,"); + background-size: 34rpx 34rpx; + background-image: url("data:image/svg+xml;utf8,"); +} + +.send-icon-btn--active .send-icon-plane { + background-image: url("data:image/svg+xml;utf8,"); } diff --git a/miniprogram/pages/enterprise/index.js b/miniprogram/pages/enterprise/index.js index e217c33..94b81f8 100644 --- a/miniprogram/pages/enterprise/index.js +++ b/miniprogram/pages/enterprise/index.js @@ -97,18 +97,21 @@ Page({ app.globalData.siteTitle = cfg.siteTitle this.setData({ siteTitle: cfg.siteTitle }) } - const maintenanceMode = !!(cfg.maintenanceMode || cfg.reviewMode) if (cfg.maintenanceMode !== undefined) app.globalData.maintenanceMode = !!cfg.maintenanceMode if (cfg.reviewMode !== undefined || cfg.maintenanceMode !== undefined) { app.globalData.reviewMode = !!(cfg.reviewMode || cfg.maintenanceMode) } + try { + require('../../utils/miniprogramAuditGate.js').applyAuditUiOverride(app) + } catch (e) {} + const auditEff = !!(app.globalData.reviewMode || app.globalData.maintenanceMode) const ep2 = app.globalData.enterprisePermissions const pf2 = !ep2 || ep2.face !== false this.setData({ - startButtonEnterprise: (maintenanceMode || !pf2) ? '开始性格测试' : (cfg.textConfig && cfg.textConfig.startButtonEnterprise || '开始面部测试'), + startButtonEnterprise: (auditEff || !pf2) ? '开始性格测试' : (cfg.textConfig && cfg.textConfig.startButtonEnterprise || '开始面部测试'), aiAnalysisText: (cfg.textConfig && cfg.textConfig.aiAnalysisText) || '智能分析', - maintenanceMode, - reviewMode: maintenanceMode, + maintenanceMode: auditEff, + reviewMode: auditEff, permFace: pf2 }) } @@ -129,18 +132,21 @@ Page({ this.setData({ siteTitle: cfg.siteTitle }) } if (cfg.textConfig) app.globalData.textConfig = cfg.textConfig - const maintenanceMode = !!(cfg && (cfg.maintenanceMode || cfg.reviewMode)) if (cfg.maintenanceMode !== undefined) app.globalData.maintenanceMode = !!cfg.maintenanceMode if (cfg.reviewMode !== undefined || cfg.maintenanceMode !== undefined) { app.globalData.reviewMode = !!(cfg.reviewMode || cfg.maintenanceMode) } + try { + require('../../utils/miniprogramAuditGate.js').applyAuditUiOverride(app) + } catch (e) {} + const auditEff2 = !!(app.globalData.reviewMode || app.globalData.maintenanceMode) const ep3 = app.globalData.enterprisePermissions const pf3 = !ep3 || ep3.face !== false this.setData({ - startButtonEnterprise: (maintenanceMode || !pf3) ? '开始性格测试' : (cfg.textConfig && cfg.textConfig.startButtonEnterprise || '开始面部测试'), + startButtonEnterprise: (auditEff2 || !pf3) ? '开始性格测试' : (cfg.textConfig && cfg.textConfig.startButtonEnterprise || '开始面部测试'), aiAnalysisText: (cfg.textConfig && cfg.textConfig.aiAnalysisText) || '智能分析', - maintenanceMode, - reviewMode: maintenanceMode, + maintenanceMode: auditEff2, + reviewMode: auditEff2, permFace: pf3 }) } diff --git a/miniprogram/pages/index/camera.js b/miniprogram/pages/index/camera.js index 3d993e8..05bb581 100644 --- a/miniprogram/pages/index/camera.js +++ b/miniprogram/pages/index/camera.js @@ -31,6 +31,9 @@ Page({ if (cfg.reviewMode !== undefined) app.globalData.reviewMode = !!cfg.reviewMode else if (cfg.maintenanceMode !== undefined) app.globalData.reviewMode = !!cfg.maintenanceMode if (cfg.maintenanceMode !== undefined) app.globalData.maintenanceMode = !!cfg.maintenanceMode + try { + require('../../utils/miniprogramAuditGate.js').applyAuditUiOverride(app) + } catch (e) {} if (cfg.textConfig) { app.globalData.textConfig = cfg.textConfig this.setData({ diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js index a15332d..b4fd21a 100644 --- a/miniprogram/pages/index/index.js +++ b/miniprogram/pages/index/index.js @@ -46,19 +46,23 @@ Page({ if (cfg.maintenanceMode !== undefined) { getApp().globalData.maintenanceMode = !!cfg.maintenanceMode } + try { + require('../../utils/miniprogramAuditGate.js').applyAuditUiOverride(getApp()) + } catch (e) {} + const rmEff = !!(getApp().globalData.reviewMode || getApp().globalData.maintenanceMode) if (cfg.siteTitle) { getApp().globalData.siteTitle = cfg.siteTitle - this.setData({ siteTitle: rm ? cfg.siteTitle.replace(/AI/gi, '') : cfg.siteTitle }) + this.setData({ siteTitle: rmEff ? cfg.siteTitle.replace(/AI/gi, '') : cfg.siteTitle }) } if (cfg.textConfig) { getApp().globalData.textConfig = cfg.textConfig this.setData({ - startButtonText: rm ? '开始性格测试' : (cfg.textConfig.startButtonText || '30秒测出你的性格'), - aiAnalysisText: rm ? '分析' : (cfg.textConfig.aiAnalysisText || '分析') + startButtonText: rmEff ? '开始性格测试' : (cfg.textConfig.startButtonText || '30秒测出你的性格'), + aiAnalysisText: rmEff ? '分析' : (cfg.textConfig.aiAnalysisText || '分析') }) } const ep2 = getApp().globalData.enterprisePermissions - this.setData({ reviewMode: rm, permFace: !ep2 || ep2.face !== false }) + this.setData({ reviewMode: rmEff, permFace: !ep2 || ep2.face !== false }) try { const tb = typeof this.getTabBar === 'function' ? this.getTabBar() : null if (tb && typeof tb.updateSelected === 'function') tb.updateSelected() diff --git a/miniprogram/pages/phone-auth/index.js b/miniprogram/pages/phone-auth/index.js index 1c72b08..64c2db7 100644 --- a/miniprogram/pages/phone-auth/index.js +++ b/miniprogram/pages/phone-auth/index.js @@ -1,74 +1,74 @@ -// 手机号授权页:用户点击按钮授权后,用 code 换手机号并写回 userInfo,再返回或跳转 next -const app = getApp() - -Page({ - data: { - next: '', - }, - - onLoad(options) { - this.setData({ - next: options.next ? decodeURIComponent(options.next) : '', - }) - }, - - onGetPhoneNumber(e) { - const { code, errMsg } = e.detail || {} - if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) { - wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' }) - return - } - if (!code) { - wx.showToast({ title: '获取手机号失败', icon: 'none' }) - return - } - const token = app.globalData.token || wx.getStorageSync('token') - if (!token) { - wx.showToast({ title: '请先登录', icon: 'none' }) - return - } - wx.showLoading({ title: '处理中...', mask: true }) - wx.request({ - url: `${app.globalData.apiBase.replace(/\/$/, '')}/api/auth/wechat/phone`, - method: 'POST', - header: { - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - data: { code }, - success: (res) => { - wx.hideLoading() - if (res.statusCode === 200 && res.data && res.data.code === 200) { - const data = res.data.data || {} - const user = data.user || app.globalData.userInfo || {} - const phone = data.phone || user.phone || '' - const newUser = { ...user, phone } - app.globalData.userInfo = newUser - wx.setStorageSync('userInfo', newUser) - wx.showToast({ title: '授权成功', icon: 'success' }) - const nextPath = this.data.next && this.data.next.startsWith('/') ? this.data.next : '' - const tabBarPaths = ['/pages/index/index', '/pages/index/camera', '/pages/profile/index'] - const isTabBar = tabBarPaths.some(p => nextPath === p || nextPath.startsWith(p + '?')) - if (nextPath) { - setTimeout(() => { - if (isTabBar) { - const pathOnly = nextPath.split('?')[0] - wx.switchTab({ url: pathOnly, fail: () => wx.navigateBack() }) - } else { - wx.redirectTo({ url: nextPath, fail: () => wx.navigateBack() }) - } - }, 500) - } else { - setTimeout(() => wx.navigateBack(), 500) - } - } else { - wx.showToast({ title: res.data && res.data.message ? res.data.message : '获取手机号失败', icon: 'none' }) - } - }, - fail: () => { - wx.hideLoading() - wx.showToast({ title: '网络请求失败', icon: 'none' }) - }, - }) - }, -}) +// 手机号授权页:用户点击按钮授权后,用 code 换手机号并写回 userInfo,再返回或跳转 next +const app = getApp() + +Page({ + data: { + next: '', + }, + + onLoad(options) { + this.setData({ + next: options.next ? decodeURIComponent(options.next) : '', + }) + }, + + onGetPhoneNumber(e) { + const { code, errMsg } = e.detail || {} + if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) { + wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' }) + return + } + if (!code) { + wx.showToast({ title: '获取手机号失败', icon: 'none' }) + return + } + const token = app.globalData.token || wx.getStorageSync('token') + if (!token) { + wx.showToast({ title: '请先登录', icon: 'none' }) + return + } + wx.showLoading({ title: '处理中...', mask: true }) + wx.request({ + url: `${app.globalData.apiBase.replace(/\/$/, '')}/api/auth/wechat/phone`, + method: 'POST', + header: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + }, + data: { code }, + success: (res) => { + wx.hideLoading() + if (res.statusCode === 200 && res.data && res.data.code === 200) { + const data = res.data.data || {} + const user = data.user || app.globalData.userInfo || {} + const phone = data.phone || user.phone || '' + const newUser = { ...user, phone } + app.globalData.userInfo = newUser + wx.setStorageSync('userInfo', newUser) + wx.showToast({ title: '授权成功', icon: 'success' }) + const nextPath = this.data.next && this.data.next.startsWith('/') ? this.data.next : '' + const tabBarPaths = ['/pages/index/index', '/pages/index/camera', '/pages/ai-chat/index', '/pages/profile/index'] + const isTabBar = tabBarPaths.some(p => nextPath === p || nextPath.startsWith(p + '?')) + if (nextPath) { + setTimeout(() => { + if (isTabBar) { + const pathOnly = nextPath.split('?')[0] + wx.switchTab({ url: pathOnly, fail: () => wx.navigateBack() }) + } else { + wx.redirectTo({ url: nextPath, fail: () => wx.navigateBack() }) + } + }, 500) + } else { + setTimeout(() => wx.navigateBack(), 500) + } + } else { + wx.showToast({ title: res.data && res.data.message ? res.data.message : '获取手机号失败', icon: 'none' }) + } + }, + fail: () => { + wx.hideLoading() + wx.showToast({ title: '网络请求失败', icon: 'none' }) + }, + }) + }, +}) diff --git a/miniprogram/pages/profile/index.wxss b/miniprogram/pages/profile/index.wxss index bac7fff..6ae6e2f 100644 --- a/miniprogram/pages/profile/index.wxss +++ b/miniprogram/pages/profile/index.wxss @@ -251,7 +251,7 @@ custom-tab-bar { /* ===== Section ===== */ .section { - margin-bottom: 28rpx; + margin-bottom: 24rpx; } .px-section { @@ -707,19 +707,19 @@ custom-tab-bar { } .bottom-safe { - height: 40rpx; + height: 28rpx; } -/* ===== 用户数据条(Soul 风,营销数据融入身份卡下方)===== */ +/* ===== 用户数据条(四宫格:收紧留白、金额列紫色层级统一)===== */ .user-stats { display: flex; align-items: stretch; - margin: 16rpx 32rpx 0; - padding: 28rpx 8rpx; + margin: 12rpx 32rpx 8rpx; + padding: 20rpx 4rpx; background: #fff; - border-radius: 24rpx; - box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.05); - border: 1rpx solid #f1f5f9; + border-radius: 20rpx; + box-shadow: 0 2rpx 16rpx rgba(15, 23, 42, 0.04); + border: 1rpx solid #eef2f7; } .user-stat { flex: 1; @@ -727,75 +727,87 @@ custom-tab-bar { flex-direction: column; align-items: center; justify-content: center; - gap: 6rpx; + gap: 4rpx; position: relative; + min-width: 0; } .user-stat + .user-stat::before { content: ''; position: absolute; left: 0; - top: 14rpx; - bottom: 14rpx; + top: 8rpx; + bottom: 8rpx; width: 1rpx; - background: #f1f5f9; + background: #e2e8f0; } .user-stat__v { - font-size: 36rpx; + font-size: 34rpx; font-weight: 700; - color: #111827; - line-height: 1.1; - letter-spacing: -0.01em; + color: #0f172a; + line-height: 1.15; + letter-spacing: -0.02em; + font-variant-numeric: tabular-nums; } .user-stat__l { - font-size: 22rpx; - color: #94a3b8; + font-size: 21rpx; + color: #64748b; + line-height: 1.2; + letter-spacing: 0.02em; } +/* 可提现:主色强调 */ .user-stat--highlight .user-stat__v { color: #7c3aed; } - -.user-stat:active { - background: rgba(124, 58, 237, 0.04); - border-radius: 12rpx; +.user-stat--highlight .user-stat__l { + color: #64748b; + font-weight: 500; } -/* "累计"第四格:金额字号略小,紫色辅文字呼应高亮格 */ +.user-stat:active { + background: rgba(124, 58, 237, 0.05); + border-radius: 14rpx; +} + +/* 累计:同色系略浅一档,与可提现区分层级 */ .user-stat__v--sm { - font-size: 30rpx; + font-size: 28rpx; + font-weight: 700; } .user-stat--sub .user-stat__v { - color: #a78bfa; + color: #8b5cf6; } .user-stat--sub .user-stat__l { - color: #7c3aed; - font-weight: 600; + color: #64748b; + font-weight: 400; } -/* ===== 小节标题(Soul 风:短促、带副箭头链接)===== */ +/* ===== 小节标题(与数据条间距收紧)===== */ .sec-head { display: flex; align-items: center; justify-content: space-between; - padding: 0 8rpx 16rpx; + padding: 0 4rpx 12rpx; } .sec-head__title { font-size: 30rpx; font-weight: 700; - color: #111827; - letter-spacing: -0.01em; + color: #0f172a; + letter-spacing: -0.02em; } .sec-head__link { display: flex; align-items: center; - gap: 4rpx; - font-size: 24rpx; - color: #94a3b8; + gap: 2rpx; + font-size: 23rpx; + color: #64748b; + font-weight: 500; } .sec-head__chev { - font-size: 26rpx; - color: #cbd5e1; + font-size: 24rpx; + color: #94a3b8; + margin-left: 2rpx; } /* ===== 快捷入口图标网格 2×3 / 2×4 ===== */ diff --git a/miniprogram/utils/miniprogramAuditGate.js b/miniprogram/utils/miniprogramAuditGate.js index a21f65f..22ab0d7 100644 --- a/miniprogram/utils/miniprogramAuditGate.js +++ b/miniprogram/utils/miniprogramAuditGate.js @@ -4,7 +4,30 @@ * - maintenanceMode / reviewMode:面相审核(用户口语「审核模式」常指其一) */ +/** 临时:为 true 时在客户端忽略审核/提审隐藏逻辑。平时必须为 false,由 runtime 与后端 miniprogramAuditMode 等决定展示 */ +const TEMP_FORCE_SHOW_ALL_HIDDEN_UI = false + +/** + * 在 getRuntimeConfig 写入 globalData 之后调用:把审核相关开关置为关闭,供各页与 TabBar 使用。 + */ +function applyAuditUiOverride(app) { + if (!TEMP_FORCE_SHOW_ALL_HIDDEN_UI || !app || !app.globalData) return + app.globalData.reviewMode = false + app.globalData.maintenanceMode = false + app.globalData.miniprogramAuditMode = false +} + +/** 是否因审核/面相权限隐藏中间凸起 Tab(拍摄) */ +function shouldHideTabBarHighlightFab(gd) { + if (TEMP_FORCE_SHOW_ALL_HIDDEN_UI) return false + if (!gd) return false + const ep = gd.enterprisePermissions + const faceOff = !!(ep && ep.face === false) + return !!(gd.reviewMode || gd.maintenanceMode) || faceOff +} + function isAuditHideAiMode(gd) { + if (TEMP_FORCE_SHOW_ALL_HIDDEN_UI) return false if (!gd) return false return !!(gd.miniprogramAuditMode || gd.maintenanceMode || gd.reviewMode) } @@ -40,6 +63,9 @@ function ensureRuntimeThenGate(callback) { } module.exports = { + TEMP_FORCE_SHOW_ALL_HIDDEN_UI, + applyAuditUiOverride, + shouldHideTabBarHighlightFab, isAuditHideAiMode, redirectIfMiniprogramAudit, ensureRuntimeThenGate