feat: 小程序 AI 底栏与聊天页、出站推送调试与 tabbar 迁移

- 自定义 tabBar:AI 入口图标与样式,相关页面与审核门控调整

- API:MpTabbar、出站推送 Hook、后台设置与迁移 SQL(含 FAB 居中排序)

- 管理端:推送 Hook 面板、出站推送调试工具与生产环境注释说明

Made-with: Cursor
This commit is contained in:
Ghost
2026-04-20 15:29:10 +08:00
parent ce55c48c64
commit d07235b64c
26 changed files with 486 additions and 317 deletions

View File

@@ -1,7 +1,10 @@
# 生产环境配置
# 正式环境API地址
VITE_API_BASE_URL=https://mbtiapi.quwanzhi.com
# 环境标识
VITE_APP_ENV=production
VITE_APP_TITLE=MBTI管理后台
# 生产环境配置
# 正式环境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

View File

@@ -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'
)
}

View File

@@ -17,37 +17,7 @@
留空订阅事件表示三类事件全部推送
</p>
<div class="form-section test-tools">
<h4 class="test-tools-title">测试与调试</h4>
<p class="hint test-tools-hint">
<strong>发送连接测试</strong>只验证 URL 是否可达hook.ping不写去重表<strong>手动测试测评完成</strong>按库表
<code>test_results.id</code> 重放真实 <code>test.result_completed</code>人脸问卷简历等均可
</p>
<div class="test-tools-actions">
<el-button plain :loading="testLoading" @click="sendTest">发送连接测试</el-button>
</div>
<div class="manual-test-row">
<span class="manual-test-label">记录 ID</span>
<el-input
v-model.number="replayTestResultId"
type="number"
:min="1"
step="1"
placeholder="test_results 主键"
class="manual-test-input"
/>
<span class="manual-test-label force-label">强制</span>
<el-switch v-model="replayForce" />
<el-button
type="primary"
class="manual-test-btn"
:loading="replayLoading"
@click="replayTestResult"
>
手动测试测评完成
</el-button>
</div>
</div>
/api/ai/chat
</div>
<div class="form-section">
<div class="form-item row-line">
@@ -81,7 +51,11 @@
{{ opt.label }}
</el-checkbox>
</el-checkbox-group>
<p class="hint">与文档一致<code>lead.order_paid</code><code>lead.phone_bound</code><code>test.result_completed</code>全部勾选保存后将以空列表存库表示订阅全部上方手动测试测评完成与真实落库推送使用同一套订阅校验</p>
<p class="hint">
与文档一致<code>lead.order_paid</code><code>lead.phone_bound</code><code>test.result_completed</code>全部勾选保存后将以空列表存库表示订阅全部
<template v-if="showTestTools">上方手动测试测评完成与真实落库推送使用同一套订阅校验</template>
<template v-else>保存后与真实落库推送使用同一套订阅校验</template>
</p>
</div>
</div>
<div class="save-actions save-actions-row">
@@ -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 = [

View File

@@ -153,7 +153,7 @@
</div>
<div v-if="activeTab === 'pushhook'" class="tab-content">
<PushHookConfigPanel api-prefix="/admin" />
<PushHookConfigPanel api-prefix="/admin" :show-test-tools="false" />
</div>
<div v-if="activeTab === 'finance'" class="embed-wrap">

View File

@@ -385,7 +385,7 @@
</div>
<div v-if="activeTab === 'pushhook'" class="tab-content">
<PushHookConfigPanel api-prefix="/superadmin" />
<PushHookConfigPanel api-prefix="/superadmin" :show-test-tools="showOutboundPushDebug" />
</div>
<!-- 账户安全 -->
@@ -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<TabId>('review')
const saveSuccess = ref<string | null>(null)

View File

@@ -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],
];
}

View File

@@ -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_turndispatch 须先快速返回 2xx否则会误判失败且无法触发 AiChat shutdown 兜底)。
*
* @param array<int,string> $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;
}
/**

View File

@@ -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);
}
/**

View File

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

View File

@@ -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()),

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
// 自定义 TabBar完全由后台配置驱动
// 数据来源app.globalData.tabBar.itemsGET /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

View File

@@ -1,4 +1,4 @@
<!-- 自定义 TabBar由 globalData.tabBar 动态渲染;支持 iconUrl 图片覆盖 SVGhighlight=中间浮钮 -->
<!-- 自定义 TabBar第2项为凸起拍摄主题色品牌紫 -->
<view class="tab-bar">
<view class="tab-bar-line"></view>
<view class="tab-bar-inner tab-bar-count-{{items.length}}">
@@ -27,8 +27,8 @@
>
<image
wx:if="{{it.iconUrl}}"
class="tab-icon-img {{selected === index ? 'tab-icon-img--active' : ''}}"
src="{{it.iconUrl}}"
class="tab-icon-img {{it.iconUrlActive ? 'tab-icon-img--dual' : ''}} {{selected === index ? 'tab-icon-img--active' : ''}}"
src="{{it.iconUrlActive && selected === index ? it.iconUrlActive : it.iconUrl}}"
mode="aspectFit"
/>
<view

View File

@@ -1,4 +1,4 @@
/* 自定义 TabBar白底、顶部分割线、支持 highlight 圆钮 · 动态数量 */
/* 自定义 TabBar白底 · 参考「左二 + 中间大圆钮上凸」布局 · 主题紫 #7c3aed */
.tab-bar {
position: fixed;
left: 0;
@@ -7,6 +7,7 @@
background: #ffffff;
padding-bottom: env(safe-area-inset-bottom);
z-index: 9999;
box-shadow: 0 -4rpx 24rpx rgba(15, 23, 42, 0.06);
}
.tab-bar-line {
@@ -17,10 +18,10 @@
.tab-bar-inner {
display: flex;
align-items: center;
align-items: flex-end;
justify-content: space-around;
min-height: 120rpx;
padding: 12rpx 12rpx 10rpx;
min-height: 116rpx;
padding: 8rpx 8rpx 12rpx;
position: relative;
box-sizing: border-box;
}
@@ -35,8 +36,8 @@
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-bottom: 0;
justify-content: flex-end;
padding-bottom: 4rpx;
z-index: 1;
}
@@ -58,12 +59,22 @@
filter: grayscale(60%);
opacity: 0.85;
}
/* 神仙AI未选/已选为两张独立 PNG不做灰阶与缩放避免叠加强调 */
.tab-icon-img--dual {
filter: none;
opacity: 1;
border-radius: 0;
}
.tab-icon-img--active {
filter: none;
opacity: 1;
transform: scale(1.05);
box-shadow: 0 4rpx 10rpx rgba(124, 58, 237, 0.25);
}
.tab-icon-img--dual.tab-icon-img--active {
transform: none;
box-shadow: none;
}
/* 中间浮钮的图片形态 */
.center-img {
@@ -90,13 +101,13 @@
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237c3aed' stroke='%237c3aed' stroke-width='1.6' stroke-linejoin='round'><path d='M3 12l9-9 9 9'/><path d='M5 10v10h14V10'/><path d='M10 20v-6h4v6' fill='none' stroke='%23ffffff'/></svg>");
}
/* ===== 图标ai (sparkle) ===== */
/* ===== 图标ai神仙AI · 未选中 #999 / 选中 #7c3aed ===== */
.tab-icon--ai {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23999999' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1'/><circle cx='12' cy='12' r='3'/></svg>");
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'><path d='M891.562667 550.570667l-58.026667-126.122667v-17.066667a317.269333 317.269333 0 0 0-107.861333-238.08 368.981333 368.981333 0 0 0-284.842667-90.453333C278.186667 95.573333 147.626667 218.112 130.048 370.176A316.416 316.416 0 0 0 241.152 648.533333a109.909333 109.909333 0 0 1 36.864 110.592l-34.133333 129.365334a49.834667 49.834667 0 0 0 5.290666 44.885333 35.669333 35.669333 0 0 0 28.501334 13.653333h318.634666a38.4 38.4 0 0 0 37.376-29.354666l20.992-93.866667a24.234667 24.234667 0 0 1 24.234667-17.066667h83.797333a69.290667 69.290667 0 0 0 70.826667-68.266666v-119.466667h11.605333a51.2 51.2 0 0 0 43.178667-23.210667 47.445333 47.445333 0 0 0 3.242667-45.226666z m-109.226667 17.066666v170.666667a18.261333 18.261333 0 0 1-19.626667 17.066667h-83.797333a75.605333 75.605333 0 0 0-74.24 57.344l-18.602667 83.285333h-290.133333l32.256-123.733333a160.426667 160.426667 0 0 0-52.906667-162.304A266.410667 266.410667 0 0 1 180.906667 375.466667c14.848-127.829333 126.293333-231.424 265.216-245.76a316.586667 316.586667 0 0 1 245.248 77.653333 266.24 266.24 0 0 1 90.965333 199.338667v29.013333l61.098667 132.608z' fill='%23999999'/><path d='M585.386667 166.229333a292.693333 292.693333 0 0 0-216.064 2.56 265.386667 265.386667 0 0 0-153.6 162.816 111.616 111.616 0 0 0 43.861333 125.952 42.837333 42.837333 0 0 1 19.797333 34.133334v32.085333a51.2 51.2 0 0 0 101.546667 0v-26.112a38.229333 38.229333 0 0 1 17.066667-32.768 164.522667 164.522667 0 0 0 59.221333-65.024 22.186667 22.186667 0 0 1 12.8-12.288 26.965333 26.965333 0 0 1 20.309333 0A131.413333 131.413333 0 0 0 631.466667 363.008 116.053333 116.053333 0 0 0 670.890667 256c-7.850667-39.253333-39.424-72.021333-85.504-89.770667z m11.946666 159.744a82.602667 82.602667 0 0 1-88.064 13.312 79.189333 79.189333 0 0 0-57.685333 0.853334A73.386667 73.386667 0 0 0 409.6 380.757333a117.589333 117.589333 0 0 1-39.594667 40.96 89.941333 89.941333 0 0 0-40.277333 64.682667 94.037333 94.037333 0 0 0-40.96-70.485333 59.733333 59.733333 0 0 1-25.088-68.266667 214.357333 214.357333 0 0 1 124.757333-131.242667 240.64 240.64 0 0 1 178.346667-2.218666 78.165333 78.165333 0 0 1 53.589333 52.394666A65.877333 65.877333 0 0 1 597.333333 325.973333z' fill='%23999999'/><path d='M502.784 207.018667a159.402667 159.402667 0 0 0-87.552 15.872 203.264 203.264 0 0 0-117.76 124.586666 51.2 51.2 0 0 0 3.584 39.765334 53.76 53.76 0 0 0 33.28 26.282666 57.514667 57.514667 0 0 0 15.36 2.048 54.272 54.272 0 0 0 52.053333-36.864 96.938667 96.938667 0 0 1 56.32-59.221333l5.802667-2.218667a42.666667 42.666667 0 0 1 29.013333-5.461333 54.101333 54.101333 0 0 0 59.221334-47.274667 51.2 51.2 0 0 0-11.605334-37.888 55.808 55.808 0 0 0-37.717333-19.626666z m-5.461333 53.76a90.453333 90.453333 0 0 0-53.248 9.216l-5.461334 3.072a148.138667 148.138667 0 0 0-85.333333 90.794666s-1.194667 1.536-4.778667 1.194667a3.413333 3.413333 0 0 1-1.877333-1.024 151.722667 151.722667 0 0 1 88.405333-92.842667 108.544 108.544 0 0 1 63.658667-12.288 5.802667 5.802667 0 0 1 2.901333 1.194667 5.290667 5.290667 0 0 1-4.266666 0.682667z' fill='%23999999'/></svg>");
}
.tab-icon--ai.tab-icon--active,
.tab-item.active .tab-icon--ai {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237c3aed' stroke='%237c3aed' stroke-width='1.6' stroke-linejoin='round'><path d='M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1' stroke='%237c3aed' fill='none' stroke-linecap='round'/><circle cx='12' cy='12' r='3.5' fill='%237c3aed'/></svg>");
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'><path d='M891.562667 550.570667l-58.026667-126.122667v-17.066667a317.269333 317.269333 0 0 0-107.861333-238.08 368.981333 368.981333 0 0 0-284.842667-90.453333C278.186667 95.573333 147.626667 218.112 130.048 370.176A316.416 316.416 0 0 0 241.152 648.533333a109.909333 109.909333 0 0 1 36.864 110.592l-34.133333 129.365334a49.834667 49.834667 0 0 0 5.290666 44.885333 35.669333 35.669333 0 0 0 28.501334 13.653333h318.634666a38.4 38.4 0 0 0 37.376-29.354666l20.992-93.866667a24.234667 24.234667 0 0 1 24.234667-17.066667h83.797333a69.290667 69.290667 0 0 0 70.826667-68.266666v-119.466667h11.605333a51.2 51.2 0 0 0 43.178667-23.210667 47.445333 47.445333 0 0 0 3.242667-45.226666z m-109.226667 17.066666v170.666667a18.261333 18.261333 0 0 1-19.626667 17.066667h-83.797333a75.605333 75.605333 0 0 0-74.24 57.344l-18.602667 83.285333h-290.133333l32.256-123.733333a160.426667 160.426667 0 0 0-52.906667-162.304A266.410667 266.410667 0 0 1 180.906667 375.466667c14.848-127.829333 126.293333-231.424 265.216-245.76a316.586667 316.586667 0 0 1 245.248 77.653333 266.24 266.24 0 0 1 90.965333 199.338667v29.013333l61.098667 132.608z' fill='%237c3aed'/><path d='M585.386667 166.229333a292.693333 292.693333 0 0 0-216.064 2.56 265.386667 265.386667 0 0 0-153.6 162.816 111.616 111.616 0 0 0 43.861333 125.952 42.837333 42.837333 0 0 1 19.797333 34.133334v32.085333a51.2 51.2 0 0 0 101.546667 0v-26.112a38.229333 38.229333 0 0 1 17.066667-32.768 164.522667 164.522667 0 0 0 59.221333-65.024 22.186667 22.186667 0 0 1 12.8-12.288 26.965333 26.965333 0 0 1 20.309333 0A131.413333 131.413333 0 0 0 631.466667 363.008 116.053333 116.053333 0 0 0 670.890667 256c-7.850667-39.253333-39.424-72.021333-85.504-89.770667z m11.946666 159.744a82.602667 82.602667 0 0 1-88.064 13.312 79.189333 79.189333 0 0 0-57.685333 0.853334A73.386667 73.386667 0 0 0 409.6 380.757333a117.589333 117.589333 0 0 1-39.594667 40.96 89.941333 89.941333 0 0 0-40.277333 64.682667 94.037333 94.037333 0 0 0-40.96-70.485333 59.733333 59.733333 0 0 1-25.088-68.266667 214.357333 214.357333 0 0 1 124.757333-131.242667 240.64 240.64 0 0 1 178.346667-2.218666 78.165333 78.165333 0 0 1 53.589333 52.394666A65.877333 65.877333 0 0 1 597.333333 325.973333z' fill='%237c3aed'/><path d='M502.784 207.018667a159.402667 159.402667 0 0 0-87.552 15.872 203.264 203.264 0 0 0-117.76 124.586666 51.2 51.2 0 0 0 3.584 39.765334 53.76 53.76 0 0 0 33.28 26.282666 57.514667 57.514667 0 0 0 15.36 2.048 54.272 54.272 0 0 0 52.053333-36.864 96.938667 96.938667 0 0 1 56.32-59.221333l5.802667-2.218667a42.666667 42.666667 0 0 1 29.013333-5.461333 54.101333 54.101333 0 0 0 59.221334-47.274667 51.2 51.2 0 0 0-11.605334-37.888 55.808 55.808 0 0 0-37.717333-19.626666z m-5.461333 53.76a90.453333 90.453333 0 0 0-53.248 9.216l-5.461334 3.072a148.138667 148.138667 0 0 0-85.333333 90.794666s-1.194667 1.536-4.778667 1.194667a3.413333 3.413333 0 0 1-1.877333-1.024 151.722667 151.722667 0 0 1 88.405333-92.842667 108.544 108.544 0 0 1 63.658667-12.288 5.802667 5.802667 0 0 1 2.901333 1.194667 5.290667 5.290667 0 0 1-4.266666 0.682667z' fill='%237c3aed'/></svg>");
}
/* ===== 图标profile ===== */
@@ -117,16 +128,16 @@
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237c3aed' stroke='%237c3aed' stroke-width='1.6' stroke-linejoin='round'><path d='M4 7h3l2-3h6l2 3h3v13H4z'/><circle cx='12' cy='13' r='4' fill='%23ffffff' stroke='%237c3aed'/></svg>");
}
/* ===== 中间浮钮 ===== */
/* ===== 中间浮钮(第 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 {

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,7 +1,7 @@
{
"navigationBarTitleText": "神仙 AI",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarBackgroundColor": "#faf9ff",
"navigationBarTextStyle": "black",
"backgroundColor": "#ffffff",
"backgroundColor": "#f4f3f9",
"usingComponents": {}
}

View File

@@ -82,7 +82,7 @@
<view class="ai-bottom-solid" aria-hidden="true"></view>
<!-- 底部:快捷语(横滑)+ 输入区,固定在 Tab 之上 -->
<view class="composer-wrap">
<view class="composer-wrap {{quickQuestions.length > 0 ? 'composer-wrap--has-chips' : ''}}">
<view class="quick-chips-outer" wx:if="{{quickQuestions.length > 0}}">
<scroll-view
class="quick-chips-scroll"
@@ -105,7 +105,7 @@
</view>
<view class="input-bar">
<view class="resume-entry" bindtap="onTapUploadResume" hover-class="resume-entry--press">
<text class="resume-entry-icon">📎</text>
<view class="resume-entry-icon" aria-hidden="true"></view>
<text class="resume-entry-txt">简历</text>
</view>
<input

View File

@@ -1,14 +1,14 @@
/* 神仙 AI · 对话页样式 */
page {
height: 100%;
background-color: #ffffff;
background-color: #f4f3f9;
}
.ai-page {
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: #ffffff;
background: linear-gradient(180deg, #f0edff 0%, #f8f7fc 18%, #ffffff 42%);
/* TabBar + 底部 composer快捷语 + 输入行) */
padding-bottom: calc(var(--tabbar-pad, 170rpx) + 200rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
@@ -16,23 +16,27 @@ page {
/* MBTI 引导卡 */
.mbti-remind-card {
margin: 8rpx 0 16rpx;
padding: 22rpx 24rpx;
background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%);
border: 1rpx solid #e9d5ff;
border-radius: 24rpx;
margin: 4rpx 0 20rpx;
padding: 28rpx 28rpx 26rpx;
background: linear-gradient(145deg, #faf8ff 0%, #f3e8ff 45%, #ede9fe 100%);
border: 1rpx solid rgba(167, 139, 250, 0.35);
border-radius: 28rpx;
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 18rpx;
gap: 22rpx;
box-shadow:
0 4rpx 6rpx rgba(124, 58, 237, 0.04),
0 16rpx 40rpx rgba(124, 58, 237, 0.1);
}
.mbti-remind-avatar {
width: 72rpx;
height: 72rpx;
width: 80rpx;
height: 80rpx;
border-radius: 50%;
flex-shrink: 0;
border: 2rpx solid #e9d5ff;
border: 3rpx solid rgba(255, 255, 255, 0.95);
box-shadow: 0 4rpx 14rpx rgba(124, 58, 237, 0.2);
background: #fff;
}
@@ -42,17 +46,18 @@ page {
}
.mbti-remind-title {
font-size: 30rpx;
font-size: 32rpx;
font-weight: 700;
color: #1f1b4d;
margin-bottom: 8rpx;
color: #312e81;
letter-spacing: 0.5rpx;
margin-bottom: 10rpx;
}
.mbti-remind-sub {
font-size: 26rpx;
color: #4b5563;
line-height: 1.45;
margin-bottom: 16rpx;
color: #57534e;
line-height: 1.5;
margin-bottom: 20rpx;
}
.mbti-badge {
@@ -63,43 +68,47 @@ page {
.mbti-remind-actions {
display: flex;
align-items: center;
gap: 16rpx;
gap: 14rpx;
flex-wrap: wrap;
}
.mbti-remind-tag {
font-size: 22rpx;
font-weight: 600;
color: #6d28d9;
background: #fff;
padding: 6rpx 18rpx;
background: #ffffff;
padding: 10rpx 22rpx;
border-radius: 100rpx;
border: 1rpx solid #ddd6fe;
border: 1rpx solid rgba(196, 181, 253, 0.85);
box-shadow: 0 2rpx 8rpx rgba(124, 58, 237, 0.06);
}
.mbti-remind-btn {
font-size: 26rpx;
font-weight: 600;
color: #fff;
padding: 12rpx 36rpx;
padding: 14rpx 38rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
box-shadow: 0 6rpx 20rpx rgba(124, 58, 237, 0.35);
background: linear-gradient(135deg, #9333ea 0%, #7c3aed 48%, #6d28d9 100%);
box-shadow: 0 8rpx 24rpx rgba(124, 58, 237, 0.38);
}
.mbti-remind-btn--press {
opacity: 0.88;
opacity: 0.9;
transform: scale(0.98);
}
/* 聊天区:不透明白底,避免页面渐变从气泡后透出 */
.chat-scroll {
flex: 1;
min-height: 0;
margin: 0 16rpx;
padding: 16rpx 20rpx 32rpx;
margin: 0 12rpx;
padding: 20rpx 22rpx 36rpx;
box-sizing: border-box;
background-color: #ffffff;
border-radius: 24rpx;
border: 1rpx solid rgba(229, 231, 235, 0.9);
border-radius: 28rpx;
border: 1rpx solid rgba(229, 231, 235, 0.65);
box-shadow: 0 8rpx 32rpx rgba(15, 23, 42, 0.04);
}
/* 消息气泡 */
@@ -143,10 +152,10 @@ page {
.bubble-ai {
background-color: #ffffff;
color: #1F1B4D;
border-top-left-radius: 4rpx;
box-shadow: 0 2rpx 12rpx rgba(31, 27, 77, 0.08);
border: 1rpx solid rgba(243, 244, 246, 0.98);
color: #1f2937;
border-top-left-radius: 6rpx;
box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.06);
border: 1rpx solid rgba(229, 231, 235, 0.85);
/* 避免开发者工具/部分机型上 text 与 scroll-view 叠层导致「透底」 */
isolation: isolate;
overflow: hidden;
@@ -332,27 +341,35 @@ page {
bottom: 0;
z-index: 9990;
height: calc(var(--tabbar-height, 168rpx) + env(safe-area-inset-bottom) + 260rpx);
background-color: #ffffff;
background-color: #f8f7fc;
pointer-events: none;
}
/* ==== 底部 composer快捷语 + 输入(整块贴在 Tab 上沿)==== */
/* ==== 底部 composer快捷语 + 输入(整块贴在 Tab 上沿,通栏纯白避免裁切/叠层发灰==== */
.composer-wrap {
position: fixed;
left: 0;
right: 0;
bottom: calc(var(--tabbar-height, 168rpx) + env(safe-area-inset-bottom));
width: 100%;
box-sizing: border-box;
background-color: #ffffff;
z-index: 9998;
box-shadow: 0 -6rpx 24rpx rgba(15, 23, 42, 0.08);
border-top: 1rpx solid #e5e7eb;
box-shadow: 0 -8rpx 32rpx rgba(15, 23, 42, 0.06);
/* 顶栏不用圆角,避免 overflow 与圆角把快捷语下沿「吃掉」;左右通栏占满 */
border-radius: 0;
overflow: visible;
border-top: 1rpx solid rgba(233, 213, 255, 0.65);
}
/* 横滑快捷语:右侧极淡渐变,暗示可横滑(无文字) */
.quick-chips-outer {
position: relative;
width: 100%;
box-sizing: border-box;
background-color: #ffffff;
/* 与下方输入条留出间距,避免视觉上被白条压住 */
padding-bottom: 8rpx;
}
.quick-chips-edge-fade {
@@ -361,11 +378,11 @@ page {
right: 0;
top: 0;
bottom: 0;
width: 56rpx;
width: 64rpx;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.75) 45%,
rgba(255, 255, 255, 0.88) 42%,
#ffffff 100%
);
}
@@ -373,10 +390,11 @@ page {
.quick-chips-scroll {
width: 100%;
white-space: nowrap;
height: 72rpx;
padding: 10rpx 0 6rpx;
/* 固定高度:须大于 chip 实际高度 + padding避免上边框被 scroll-view 裁切 */
height: 100rpx;
padding: 18rpx 0 14rpx;
box-sizing: border-box;
background-color: #ffffff;
background-color: transparent;
}
.quick-chips-row {
@@ -385,7 +403,8 @@ page {
align-items: center;
gap: 12rpx;
padding: 0 20rpx;
height: 56rpx;
min-height: 72rpx;
box-sizing: border-box;
}
.quick-chip {
@@ -395,73 +414,89 @@ page {
text-overflow: ellipsis;
white-space: nowrap;
font-size: 24rpx;
color: #374151;
line-height: 1.3;
padding: 12rpx 24rpx;
background: #f3f4f6;
color: #5b21b6;
line-height: 1.35;
padding: 14rpx 26rpx;
background: rgba(124, 58, 237, 0.06);
border-radius: 999rpx;
border: 1rpx solid #e5e7eb;
border: 1rpx solid rgba(167, 139, 250, 0.35);
}
.quick-chip:active {
opacity: 0.95;
background: #ffffff;
opacity: 0.92;
background: rgba(124, 58, 237, 0.1);
}
.input-bar {
position: relative;
left: auto;
right: auto;
bottom: auto;
left: 0;
right: 0;
width: 100%;
box-sizing: border-box;
background-color: #ffffff;
padding: 8rpx 20rpx 14rpx;
padding: 12rpx 22rpx 18rpx;
display: flex;
align-items: center;
gap: 12rpx;
border-top: 1rpx solid #f3f4f6;
gap: 14rpx;
border-top: none;
box-shadow: none;
}
/* 有快捷语时:输入条与快捷语之间的通栏分隔(无快捷语时仅保留 composer-wrap 顶边,避免双线) */
.composer-wrap--has-chips .input-bar {
border-top: 1rpx solid #ede9fe;
}
.resume-entry {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 64rpx;
padding: 4rpx 8rpx 0;
min-width: 72rpx;
padding: 10rpx 12rpx 6rpx;
box-sizing: border-box;
background: rgba(124, 58, 237, 0.06);
border-radius: 18rpx;
border: 1rpx solid rgba(196, 181, 253, 0.35);
}
.resume-entry--press {
opacity: 0.85;
opacity: 0.88;
background: rgba(124, 58, 237, 0.1);
}
.resume-entry-icon {
font-size: 32rpx;
line-height: 1;
width: 36rpx;
height: 36rpx;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'><path d='M586.9568 165.888H272.4864c-23.2448 0-42.1888 18.944-42.1888 42.1888v607.8464c0 23.2448 18.944 42.1888 42.1888 42.1888H751.616c23.2448 0 42.1888-18.944 42.1888-42.1888V372.736l-206.848-206.848z m-158.1056 88.1664c37.1712 0 67.2768 30.1056 67.2768 67.2768s-30.1056 67.2768-67.2768 67.2768c-37.1712 0-67.2768-30.1056-67.2768-67.2768s30.0032-67.2768 67.2768-67.2768z m0 145.3056c52.1216 0 94.5152 37.888 94.5152 84.3776 0 11.6736-9.4208 21.0944-21.0944 21.0944-11.6736 0-21.0944-9.4208-21.0944-21.0944 0-22.8352-23.9616-42.1888-52.3264-42.1888s-52.3264 19.3536-52.3264 42.1888c0 11.6736-9.4208 21.0944-21.0944 21.0944s-21.0944-9.4208-21.0944-21.0944c0-46.592 42.3936-84.3776 94.5152-84.3776z m240.9472 344.8832H354.2016c-11.5712 0-21.0944-9.5232-21.0944-21.0944s9.5232-21.0944 21.0944-21.0944H669.696c11.5712 0 21.0944 9.5232 21.0944 21.0944 0.1024 11.5712-9.4208 21.0944-20.992 21.0944z m0-106.8032H354.2016c-11.5712 0-21.0944-9.5232-21.0944-21.0944s9.5232-21.0944 21.0944-21.0944H669.696c11.5712 0 21.0944 9.5232 21.0944 21.0944s-9.4208 21.0944-20.992 21.0944z m-48.128-287.232c-3.8912 0-7.2704-3.3792-7.2704-7.2704v-89.9072l97.1776 97.1776h-89.9072z' fill='%236d28d9'/><path d='M428.8512 351.744c16.7936 0 30.4128-13.6192 30.4128-30.4128s-13.6192-30.4128-30.4128-30.4128-30.4128 13.6192-30.4128 30.4128 13.6192 30.4128 30.4128 30.4128z' fill='%236d28d9'/></svg>");
}
.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,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><line x1='22' y1='2' x2='11' y2='13'/><polygon points='22 2 15 22 11 13 2 9 22 2' fill='%236b7280' stroke='%236b7280'/></svg>");
background-size: 34rpx 34rpx;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'><path d='M915.515273 142.819385 98.213046 458.199122c-46.058539 17.772838-44.90475 43.601756 2.348455 57.622994l197.477685 58.594874 80.292024 238.91085c10.51184 31.277988 37.972822 37.873693 61.462483 14.603752l103.584447-102.611545 204.475018 149.840224c26.565749 19.467242 53.878547 9.222132 61.049613-23.090076l149.210699-672.34491C965.264096 147.505054 946.218922 130.971848 915.515273 142.819385zM791.141174 294.834331l-348.61988 310.610267c-6.268679 5.58499-11.941557 16.652774-12.812263 24.846818l-15.390659 144.697741c-1.728128 16.24808-7.330491 16.918483-12.497501 1.344894l-67.457277-203.338603c-2.638691-7.954906 0.975968-17.705389 8.022355-21.931178l442.114555-265.181253C812.67481 268.984974 815.674251 272.975713 791.141174 294.834331z' fill='%236b7280'/></svg>");
}
.send-icon-btn--active .send-icon-plane {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'><path d='M915.515273 142.819385 98.213046 458.199122c-46.058539 17.772838-44.90475 43.601756 2.348455 57.622994l197.477685 58.594874 80.292024 238.91085c10.51184 31.277988 37.972822 37.873693 61.462483 14.603752l103.584447-102.611545 204.475018 149.840224c26.565749 19.467242 53.878547 9.222132 61.049613-23.090076l149.210699-672.34491C965.264096 147.505054 946.218922 130.971848 915.515273 142.819385zM791.141174 294.834331l-348.61988 310.610267c-6.268679 5.58499-11.941557 16.652774-12.812263 24.846818l-15.390659 144.697741c-1.728128 16.24808-7.330491 16.918483-12.497501 1.344894l-67.457277-203.338603c-2.638691-7.954906 0.975968-17.705389 8.022355-21.931178l442.114555-265.181253C812.67481 268.984974 815.674251 272.975713 791.141174 294.834331z' fill='%23ffffff'/></svg>");
}

View File

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

View File

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

View File

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

View File

@@ -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' })
},
})
},
})

View File

@@ -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 ===== */

View File

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