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 @@
留空「订阅事件」表示三类事件全部推送。
-
+ /api/ai/chat
@@ -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