feat: 双端题库统一与 PDP/DISC 结果文案、测评 API 与企测上下文
1、修复了测评/结果页与个人中心展示不一致问题;废弃独立 questions.js,统一使用 questionBank;结果格式与详情解析相关问题。 2、新增了 PdpDiscResultText(PDP+DISC 双类型摘要文案)、抖音 questionBank 与 enterpriseContext;扩展 api/Test 与路由、管理端 Order 与 ExtractsTestResults。 3、优化了 resultFormat 与 questionBank 组织方式、UserDetailDialog;移除不再使用的 mbti_test_results 恢复 SQL/脚本。 Made-with: Cursor
This commit is contained in:
@@ -40,10 +40,10 @@
|
||||
<div class="ud-quick-stats__tile"><el-icon><Aim /></el-icon>{{ shortOrDash(user.mbtiType) }}</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="PDP" placement="top">
|
||||
<div class="ud-quick-stats__tile"><el-icon><TrendCharts /></el-icon>{{ shortOrDash(user.pdpType, 4) }}</div>
|
||||
<div class="ud-quick-stats__tile"><el-icon><TrendCharts /></el-icon>{{ shortOrDash(user.pdpType, 16) }}</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="DISC" placement="top">
|
||||
<div class="ud-quick-stats__tile"><el-icon><PieChart /></el-icon>{{ shortOrDash(user.discType, 3) }}</div>
|
||||
<div class="ud-quick-stats__tile"><el-icon><PieChart /></el-icon>{{ shortOrDash(user.discType, 16) }}</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="ud-dimension-tags" v-if="profileTags.length">
|
||||
|
||||
250
api/app/common/PdpDiscResultText.php
Normal file
250
api/app/common/PdpDiscResultText.php
Normal file
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
namespace app\common;
|
||||
|
||||
/**
|
||||
* PDP / DISC 结果摘要:权重最高的 2 项。
|
||||
* 展示格式:DISC 为「D+I型」(仅最后一项带「型」);PDP 为「孔雀+老虎型」。
|
||||
*/
|
||||
class PdpDiscResultText
|
||||
{
|
||||
private const PDP_EN_TO_CN = [
|
||||
'Tiger' => '老虎型',
|
||||
'Peacock' => '孔雀型',
|
||||
'Koala' => '考拉型',
|
||||
'Owl' => '猫头鹰型',
|
||||
'Chameleon' => '变色龙型',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $data
|
||||
*/
|
||||
public static function discTopTwo(array $data): string
|
||||
{
|
||||
[$fL, $sL] = self::discResolveTwoLetters($data);
|
||||
if ($fL !== '' || $sL !== '') {
|
||||
if ($fL === '') {
|
||||
return $sL !== '' ? ($sL . '型') : '';
|
||||
}
|
||||
if ($sL === '' || $sL === $fL) {
|
||||
return $fL . '型';
|
||||
}
|
||||
|
||||
return $fL . '+' . $sL . '型';
|
||||
}
|
||||
|
||||
return self::discNormalizeLegacyDualDescription($data['description']['type'] ?? null) ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $data
|
||||
*/
|
||||
public static function pdpTopTwo(array $data): string
|
||||
{
|
||||
[$firstFull, $secondFull] = self::pdpResolveTwoFull($data);
|
||||
if ($firstFull === '') {
|
||||
return $secondFull;
|
||||
}
|
||||
if ($secondFull === '' || $secondFull === $firstFull) {
|
||||
return $firstFull;
|
||||
}
|
||||
$firstShort = preg_replace('/型$/u', '', $firstFull);
|
||||
|
||||
return $firstShort . '+' . $secondFull;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:string,1:string} DISC 字母 D/I/S/C
|
||||
*/
|
||||
private static function discResolveTwoLetters(array $data): array
|
||||
{
|
||||
$f = self::discPrimaryLetter($data);
|
||||
$s = '';
|
||||
$sk = $data['secondaryType'] ?? null;
|
||||
if (is_string($sk) && $sk !== '') {
|
||||
$u = strtoupper(substr(trim($sk), 0, 1));
|
||||
if (in_array($u, ['D', 'I', 'S', 'C'], true)) {
|
||||
$s = $u;
|
||||
}
|
||||
}
|
||||
$a = '';
|
||||
$b = '';
|
||||
if (isset($data['scores']) && is_array($data['scores'])) {
|
||||
[$a, $b] = self::discOrderedLetters($data['scores']);
|
||||
}
|
||||
if ($a === '' && $b === '' && isset($data['percentages']) && is_array($data['percentages'])) {
|
||||
[$a, $b] = self::discOrderedLetters($data['percentages']);
|
||||
}
|
||||
if ($f === '' && $a !== '') {
|
||||
$f = $a;
|
||||
}
|
||||
if ($s === '' || $s === $f) {
|
||||
if ($b !== '' && $b !== $f) {
|
||||
$s = $b;
|
||||
} else {
|
||||
$s = '';
|
||||
}
|
||||
}
|
||||
|
||||
return [$f, $s];
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧数据:「S型 + I型」等 → 「S+I型」
|
||||
*/
|
||||
private static function discNormalizeLegacyDualDescription(?string $desc): ?string
|
||||
{
|
||||
if ($desc === null || trim($desc) === '') {
|
||||
return null;
|
||||
}
|
||||
$t = preg_replace('/\s+/u', '', trim($desc));
|
||||
$t = str_replace('+', '+', $t);
|
||||
if (preg_match('/^([DISC])型\+([DISC])型$/iu', $t, $m)) {
|
||||
return strtoupper($m[1]) . '+' . strtoupper($m[2]) . '型';
|
||||
}
|
||||
if (preg_match('/^([DISC])型$/iu', $t, $m)) {
|
||||
return strtoupper($m[1]) . '型';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:string,1:string} 完整 PDP 类型文案(含「型」)
|
||||
*/
|
||||
private static function pdpResolveTwoFull(array $data): array
|
||||
{
|
||||
$f = self::pdpPrimaryFull($data);
|
||||
$s = '';
|
||||
$sk = $data['secondaryType'] ?? null;
|
||||
if (is_string($sk) && $sk !== '') {
|
||||
$s = self::PDP_EN_TO_CN[$sk] ?? $sk;
|
||||
}
|
||||
$aEn = '';
|
||||
$bEn = '';
|
||||
if (isset($data['scores']) && is_array($data['scores'])) {
|
||||
[$aEn, $bEn] = self::pdpOrderedKeys($data['scores']);
|
||||
}
|
||||
if ($aEn === '' && $bEn === '' && isset($data['percentages']) && is_array($data['percentages'])) {
|
||||
[$aEn, $bEn] = self::pdpOrderedKeys($data['percentages']);
|
||||
}
|
||||
if ($f === '' && $aEn !== '') {
|
||||
$f = self::PDP_EN_TO_CN[$aEn] ?? $aEn;
|
||||
}
|
||||
if ($s === '' || $s === $f) {
|
||||
if ($bEn !== '') {
|
||||
$cand = self::PDP_EN_TO_CN[$bEn] ?? $bEn;
|
||||
if ($cand !== $f) {
|
||||
$s = $cand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [$f, $s];
|
||||
}
|
||||
|
||||
private static function discPrimaryLetter(array $data): string
|
||||
{
|
||||
$desc = $data['description']['type'] ?? null;
|
||||
if (is_string($desc) && $desc !== '') {
|
||||
$t = trim($desc);
|
||||
$noXing = preg_replace('/型$/u', '', $t);
|
||||
if (mb_strlen($noXing) === 1) {
|
||||
$u = strtoupper($noXing);
|
||||
if (in_array($u, ['D', 'I', 'S', 'C'], true)) {
|
||||
return $u;
|
||||
}
|
||||
}
|
||||
}
|
||||
$dom = $data['dominantType'] ?? null;
|
||||
if (is_string($dom) && $dom !== '') {
|
||||
$u = strtoupper(substr(trim($dom), 0, 1));
|
||||
if (in_array($u, ['D', 'I', 'S', 'C'], true)) {
|
||||
return $u;
|
||||
}
|
||||
}
|
||||
$disc = $data['disc'] ?? null;
|
||||
if (is_string($disc) && $disc !== '') {
|
||||
$t = trim($disc);
|
||||
$noXing = preg_replace('/型$/u', '', $t);
|
||||
if (mb_strlen($noXing) === 1) {
|
||||
$u = strtoupper($noXing);
|
||||
if (in_array($u, ['D', 'I', 'S', 'C'], true)) {
|
||||
return $u;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private static function pdpPrimaryFull(array $data): string
|
||||
{
|
||||
$desc = $data['description']['type'] ?? null;
|
||||
if (is_string($desc) && $desc !== '') {
|
||||
return trim($desc);
|
||||
}
|
||||
$dom = $data['dominantType'] ?? null;
|
||||
if (is_string($dom) && $dom !== '') {
|
||||
return self::PDP_EN_TO_CN[$dom] ?? trim($dom);
|
||||
}
|
||||
$pdp = $data['pdp'] ?? null;
|
||||
if (is_string($pdp) && $pdp !== '') {
|
||||
return trim($pdp);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,int|float> $scores
|
||||
* @return array{0:string,1:string}
|
||||
*/
|
||||
private static function discOrderedLetters(array $scores): array
|
||||
{
|
||||
$allowed = ['D' => true, 'I' => true, 'S' => true, 'C' => true];
|
||||
$pairs = [];
|
||||
foreach ($scores as $k => $v) {
|
||||
if (!is_string($k) && !is_numeric($k)) {
|
||||
continue;
|
||||
}
|
||||
$ku = strtoupper(substr(trim((string) $k), 0, 1));
|
||||
if (!isset($allowed[$ku])) {
|
||||
continue;
|
||||
}
|
||||
$pairs[] = [$ku, (int) $v];
|
||||
}
|
||||
usort($pairs, static function ($a, $b) {
|
||||
return ($b[1] <=> $a[1]) ?: strcmp($a[0], $b[0]);
|
||||
});
|
||||
$f = $pairs[0][0] ?? '';
|
||||
$s = $pairs[1][0] ?? '';
|
||||
|
||||
return [$f, $s];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,int|float> $scores
|
||||
* @return array{0:string,1:string}
|
||||
*/
|
||||
private static function pdpOrderedKeys(array $scores): array
|
||||
{
|
||||
$pairs = [];
|
||||
foreach ($scores as $k => $v) {
|
||||
if (!is_string($k)) {
|
||||
continue;
|
||||
}
|
||||
$key = trim($k);
|
||||
if ($key === '' || !isset(self::PDP_EN_TO_CN[$key])) {
|
||||
continue;
|
||||
}
|
||||
$pairs[] = [$key, (int) $v];
|
||||
}
|
||||
usort($pairs, static function ($a, $b) {
|
||||
return ($b[1] <=> $a[1]) ?: strcmp($a[0], $b[0]);
|
||||
});
|
||||
$f = $pairs[0][0] ?? '';
|
||||
$s = $pairs[1][0] ?? '';
|
||||
|
||||
return [$f, $s];
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\common\PdpDiscResultText;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
@@ -156,6 +157,10 @@ class Order extends BaseController
|
||||
return (string) ($data['mbtiType'] ?? $data['type'] ?? $data['result'] ?? '');
|
||||
}
|
||||
if ($type === 'disc') {
|
||||
$two = PdpDiscResultText::discTopTwo($data);
|
||||
if ($two !== '') {
|
||||
return $two;
|
||||
}
|
||||
$desc = $data['description']['type'] ?? null;
|
||||
if (is_string($desc) && $desc !== '') {
|
||||
return $desc;
|
||||
@@ -166,6 +171,10 @@ class Order extends BaseController
|
||||
return (string) ($data['disc'] ?? '');
|
||||
}
|
||||
if ($type === 'pdp') {
|
||||
$two = PdpDiscResultText::pdpTopTwo($data);
|
||||
if ($two !== '') {
|
||||
return $two;
|
||||
}
|
||||
$desc = $data['description']['type'] ?? null;
|
||||
if (is_string($desc) && $desc !== '') {
|
||||
return $desc;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
namespace app\controller\admin\concern;
|
||||
|
||||
use app\common\PdpDiscResultText;
|
||||
|
||||
/**
|
||||
* 从测试记录数组中解析 MBTI / DISC / PDP / 人脸子类型(与 AppUser 逻辑一致)
|
||||
*/
|
||||
@@ -69,6 +71,10 @@ trait ExtractsTestResults
|
||||
}
|
||||
|
||||
if ($targetType === 'disc') {
|
||||
$two = PdpDiscResultText::discTopTwo($dec);
|
||||
if ($two !== '') {
|
||||
return $two;
|
||||
}
|
||||
$desc = $dec['description']['type'] ?? null;
|
||||
if (is_string($desc) && $desc !== '') {
|
||||
return $desc;
|
||||
@@ -81,6 +87,10 @@ trait ExtractsTestResults
|
||||
}
|
||||
|
||||
if ($targetType === 'pdp') {
|
||||
$two = PdpDiscResultText::pdpTopTwo($dec);
|
||||
if ($two !== '') {
|
||||
return $two;
|
||||
}
|
||||
$desc = $dec['description']['type'] ?? null;
|
||||
if (is_string($desc) && $desc !== '') {
|
||||
return $desc;
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
namespace app\controller\api;
|
||||
|
||||
use app\BaseController;
|
||||
use app\common\PdpDiscResultText;
|
||||
use app\model\Enterprise as EnterpriseModel;
|
||||
use app\model\PricingConfig as PricingConfigModel;
|
||||
use app\model\Question as QuestionModel;
|
||||
use app\model\UserProfile as UserProfileModel;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
@@ -144,20 +146,33 @@ class Test extends BaseController
|
||||
], $paymentFields);
|
||||
break;
|
||||
case 'disc':
|
||||
$discType = is_array($data) ? ($data['dominantType'] ?? $data['disc'] ?? '未知') : '未知';
|
||||
$discTxt = '未知';
|
||||
if (is_array($data)) {
|
||||
$discTxt = PdpDiscResultText::discTopTwo($data);
|
||||
if ($discTxt === '') {
|
||||
$fallback = $data['dominantType'] ?? $data['disc'] ?? '未知';
|
||||
$discTxt = (is_string($fallback) || is_numeric($fallback) ? (string) $fallback : '未知') . '型';
|
||||
}
|
||||
}
|
||||
$list[] = array_merge([
|
||||
'id' => $id,
|
||||
'type' => 'disc',
|
||||
'key' => 'disc_' . $id,
|
||||
'emoji' => '📊',
|
||||
'typeName' => 'DISC性格测试',
|
||||
'resultText'=> (is_string($discType) || is_numeric($discType) ? (string) $discType : '未知') . '型',
|
||||
'resultText'=> $discTxt,
|
||||
'testTime' => $timeLabel,
|
||||
'data' => null,
|
||||
], $paymentFields);
|
||||
break;
|
||||
case 'pdp':
|
||||
$primary = is_array($data) ? ($data['description']['type'] ?? $data['pdp'] ?? '未知') : '未知';
|
||||
$primary = '未知';
|
||||
if (is_array($data)) {
|
||||
$primary = PdpDiscResultText::pdpTopTwo($data);
|
||||
if ($primary === '') {
|
||||
$primary = (string) ($data['description']['type'] ?? $data['pdp'] ?? '未知');
|
||||
}
|
||||
}
|
||||
$emoji = (is_array($data) && isset($data['description']['emoji'])) ? $data['description']['emoji'] : '🦁';
|
||||
$list[] = array_merge([
|
||||
'id' => $id,
|
||||
@@ -337,6 +352,36 @@ class Test extends BaseController
|
||||
return array_values(array_unique($allowed));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 test_results.resultData:支持 JSON 字符串、已解码的 array、包在 result 键里的结构
|
||||
*
|
||||
* @param mixed $raw
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
protected function decodeResultDataPayload($raw): array
|
||||
{
|
||||
if ($raw === null || $raw === '') {
|
||||
return [];
|
||||
}
|
||||
if (is_array($raw)) {
|
||||
$data = $raw;
|
||||
} elseif (is_string($raw)) {
|
||||
$decoded = json_decode(trim($raw), true);
|
||||
$data = is_array($decoded) ? $decoded : [];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
if (isset($data['result']) && is_array($data['result'])) {
|
||||
$inner = $data['result'];
|
||||
if (isset($inner['percentages']) || isset($inner['scores']) || isset($inner['dominantType'])
|
||||
|| isset($inner['description']) || isset($inner['mbtiType'])) {
|
||||
$data = array_merge($data, $inner);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化单条记录为 recent 接口返回结构
|
||||
*/
|
||||
@@ -345,11 +390,7 @@ class Test extends BaseController
|
||||
$testType = $row['testType'] ?? '';
|
||||
$createdAt = $row['createdAt'] ?? null;
|
||||
$raw = $row['resultData'] ?? ($row['result'] ?? null);
|
||||
$data = [];
|
||||
if ($raw !== null && $raw !== '') {
|
||||
$decoded = json_decode($raw, true);
|
||||
$data = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
$data = $this->decodeResultDataPayload($raw);
|
||||
|
||||
$resultText = '';
|
||||
$emoji = '';
|
||||
@@ -363,13 +404,19 @@ class Test extends BaseController
|
||||
$typeName = 'MBTI性格';
|
||||
break;
|
||||
case 'disc':
|
||||
$dominantType = $data['dominantType'] ?? $data['disc'] ?? '未知';
|
||||
$resultText = $dominantType . '型';
|
||||
$emoji = '📊';
|
||||
$typeName = 'DISC测评';
|
||||
$resultText = PdpDiscResultText::discTopTwo($data);
|
||||
if ($resultText === '') {
|
||||
$dominantType = $data['dominantType'] ?? $data['disc'] ?? '未知';
|
||||
$resultText = (is_string($dominantType) || is_numeric($dominantType) ? (string) $dominantType : '未知') . '型';
|
||||
}
|
||||
$emoji = '📊';
|
||||
$typeName = 'DISC测评';
|
||||
break;
|
||||
case 'pdp':
|
||||
$resultText = $data['description']['type'] ?? $data['pdp'] ?? '未知';
|
||||
$resultText = PdpDiscResultText::pdpTopTwo($data);
|
||||
if ($resultText === '') {
|
||||
$resultText = $data['description']['type'] ?? $data['pdp'] ?? '未知';
|
||||
}
|
||||
$emoji = $data['description']['emoji'] ?? '🦁';
|
||||
$typeName = 'PDP行为';
|
||||
break;
|
||||
@@ -397,6 +444,28 @@ class Test extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
// 小程序「最新测试」:始终附带 resultMeta(便于前端 getTypeOnly;避免仅 resultText 旧格式)
|
||||
$resultMeta = null;
|
||||
if ($testType === 'disc') {
|
||||
$resultMeta = [
|
||||
'scores' => $data['scores'] ?? null,
|
||||
'percentages' => $data['percentages'] ?? null,
|
||||
'dominantType' => $data['dominantType'] ?? null,
|
||||
'secondaryType' => $data['secondaryType'] ?? null,
|
||||
'description' => $data['description'] ?? null,
|
||||
'disc' => $data['disc'] ?? null,
|
||||
];
|
||||
} elseif ($testType === 'pdp') {
|
||||
$resultMeta = [
|
||||
'scores' => $data['scores'] ?? null,
|
||||
'percentages' => $data['percentages'] ?? null,
|
||||
'dominantType' => $data['dominantType'] ?? null,
|
||||
'secondaryType' => $data['secondaryType'] ?? null,
|
||||
'description' => $data['description'] ?? null,
|
||||
'pdp' => $data['pdp'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
$out = [
|
||||
'id' => (int) $row['id'],
|
||||
'testType' => ($testType === 'face') ? 'ai' : $testType,
|
||||
@@ -410,6 +479,9 @@ class Test extends BaseController
|
||||
if ($gallupPreview !== '') {
|
||||
$out['gallupPreview'] = $gallupPreview;
|
||||
}
|
||||
if ($resultMeta !== null) {
|
||||
$out['resultMeta'] = $resultMeta;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
@@ -802,5 +874,90 @@ class Test extends BaseController
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序拉取做题题库(仅启用题):企业本题库有题则用企业,否则用超管 enterpriseId 为空
|
||||
* GET /api/test/questions?type=mbti|disc|pdp&enterpriseId=可选
|
||||
*/
|
||||
public function questions()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['source'] ?? '') !== 'wechat') {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
$type = (string) Request::param('type', '');
|
||||
if (!in_array($type, ['mbti', 'disc', 'pdp'], true)) {
|
||||
return error('type 须为 mbti、disc 或 pdp', 400);
|
||||
}
|
||||
|
||||
$rawEid = Request::param('enterpriseId', null);
|
||||
$enterpriseId = null;
|
||||
if ($rawEid !== null && $rawEid !== '') {
|
||||
$enterpriseId = (int) $rawEid;
|
||||
if ($enterpriseId <= 0) {
|
||||
$enterpriseId = null;
|
||||
}
|
||||
}
|
||||
|
||||
$resolvedEnterpriseId = null;
|
||||
if ($enterpriseId !== null) {
|
||||
$enterpriseQuestionCount = QuestionModel::where('enterpriseId', $enterpriseId)
|
||||
->where('type', $type)
|
||||
->where('status', 1)
|
||||
->count();
|
||||
if ($enterpriseQuestionCount > 0) {
|
||||
$resolvedEnterpriseId = $enterpriseId;
|
||||
}
|
||||
}
|
||||
|
||||
$query = QuestionModel::where('type', $type)->where('status', 1);
|
||||
if ($resolvedEnterpriseId !== null) {
|
||||
$query->where('enterpriseId', $resolvedEnterpriseId);
|
||||
} else {
|
||||
$query->whereNull('enterpriseId');
|
||||
}
|
||||
|
||||
$list = $query->order('sort', 'asc')
|
||||
->order('id', 'asc')
|
||||
->field('id,question,options,dimension')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
foreach ($list as &$item) {
|
||||
if (isset($item['options']) && is_object($item['options'])) {
|
||||
$item['options'] = json_decode(json_encode($item['options']), true);
|
||||
}
|
||||
if (isset($item['options']) && is_array($item['options']) && !empty($item['options']) && !isset($item['options'][0])) {
|
||||
$item['options'] = array_values($item['options']);
|
||||
}
|
||||
if (!isset($item['options']) || !is_array($item['options'])) {
|
||||
$item['options'] = [];
|
||||
}
|
||||
$item['id'] = (int) ($item['id'] ?? 0);
|
||||
foreach ($item['options'] as &$opt) {
|
||||
if (!is_array($opt)) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($opt['value']) && isset($opt['label'])) {
|
||||
$opt['value'] = $opt['label'];
|
||||
}
|
||||
if (!isset($opt['text']) && isset($opt['label'])) {
|
||||
$opt['text'] = $opt['label'];
|
||||
}
|
||||
}
|
||||
unset($opt);
|
||||
if ($type !== 'mbti') {
|
||||
unset($item['dimension']);
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
|
||||
return success([
|
||||
'list' => $list,
|
||||
'resolvedEnterpriseId' => $resolvedEnterpriseId,
|
||||
'usingSuperAdminBank' => $resolvedEnterpriseId === null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Generate restore_mbti_test_results_1_145.sql from api/mbti_data.sql."""
|
||||
import pathlib
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
src = ROOT / "mbti_data.sql"
|
||||
out = pathlib.Path(__file__).resolve().parent / "restore_mbti_test_results_1_145.sql"
|
||||
|
||||
lines = src.read_text(encoding="utf-8").splitlines()
|
||||
starts = None
|
||||
ends = None
|
||||
for i, L in enumerate(lines):
|
||||
if starts is None and L.startswith("INSERT INTO `mbti_test_results`") and "VALUES (1," in L:
|
||||
starts = i
|
||||
if L.startswith("INSERT INTO `mbti_test_results`") and "VALUES (145," in L:
|
||||
ends = i + 1
|
||||
break
|
||||
|
||||
if starts is None or ends is None:
|
||||
raise SystemExit(f"range not found: starts={starts} ends={ends}")
|
||||
|
||||
chunk = lines[starts:ends]
|
||||
head = """-- 恢复 mbti_test_results 表 id 1~145(与 api/mbti_data.sql 中导出一致)
|
||||
-- 用法:mysql -u... -p... 数据库名 < restore_mbti_test_results_1_145.sql
|
||||
-- REPLACE 会按主键删除旧行再插入,执行前请备份。
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
START TRANSACTION;
|
||||
|
||||
"""
|
||||
body = "\n".join(L.replace("INSERT INTO", "REPLACE INTO", 1) for L in chunk)
|
||||
tail = """
|
||||
|
||||
COMMIT;
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
"""
|
||||
out.write_text(head + body + tail, encoding="utf-8")
|
||||
print("wrote", out, "statements", len(chunk))
|
||||
File diff suppressed because one or more lines are too long
@@ -49,6 +49,8 @@ Route::group('api', function () {
|
||||
Route::get('test/recent', 'api.Test/recent');
|
||||
// 单条测试详情
|
||||
Route::get('test/detail', 'api.Test/detail');
|
||||
// 启用题库(企业有则用企业,否则超管)
|
||||
Route::get('test/questions', 'api.Test/questions');
|
||||
// 提交测试结果(MBTI/DISC/PDP 等)
|
||||
Route::post('test/submit', 'api.Test/submit');
|
||||
// 简历综合分析(基于人脸/MBTI/PDP/DISC 最近一次结果)
|
||||
|
||||
@@ -3,6 +3,16 @@ const app = getApp()
|
||||
const { getTypeOnly } = require('../../utils/resultFormat')
|
||||
const { request } = require('../../utils/request')
|
||||
|
||||
function summaryFromRecentRecord(rec, testType) {
|
||||
if (!rec) return ''
|
||||
const meta = rec.resultMeta
|
||||
if (meta && typeof meta === 'object') {
|
||||
const t = getTypeOnly(meta, testType)
|
||||
if (t) return t
|
||||
}
|
||||
return String(rec.resultText || '').trim()
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
hasLogin: false,
|
||||
@@ -45,17 +55,16 @@ Page({
|
||||
permPdp: true,
|
||||
permDisc: true,
|
||||
permDistribution: true,
|
||||
showLatestTestCards: false,
|
||||
showLatestTestRow: false,
|
||||
showEmptyPersonalityTags: true
|
||||
},
|
||||
|
||||
_computeShowLatestTestCards(d) {
|
||||
_computeShowLatestTestRow(d) {
|
||||
const rm = !!(d.reviewMode)
|
||||
if (d.permMbti && d.mbtiType) return true
|
||||
if (d.permPdp && d.pdpType) return true
|
||||
if (d.permDisc && d.discType) return true
|
||||
if (!rm && d.permFace && (d.gallupPreview || d.aiType)) return true
|
||||
return false
|
||||
if (rm) {
|
||||
return !!(d.permMbti || d.permPdp || d.permDisc)
|
||||
}
|
||||
return !!(d.permMbti || d.permPdp || d.permDisc || d.permFace)
|
||||
},
|
||||
|
||||
_computeShowEmptyPersonalityTags(d) {
|
||||
@@ -79,7 +88,7 @@ Page({
|
||||
const d = { ...this.data, ...next }
|
||||
this.setData({
|
||||
...next,
|
||||
showLatestTestCards: this._computeShowLatestTestCards(d),
|
||||
showLatestTestRow: this._computeShowLatestTestRow(d),
|
||||
showEmptyPersonalityTags: this._computeShowEmptyPersonalityTags(d)
|
||||
})
|
||||
},
|
||||
@@ -197,8 +206,7 @@ Page({
|
||||
const { records = {}, totalCount = 0 } = payload.data
|
||||
const r = records
|
||||
|
||||
// DISC resultText 后端已含「型」,type badge 只显示字母,去掉「型」
|
||||
const discType = r.disc ? r.disc.resultText.replace(/型$/, '') : ''
|
||||
const discType = summaryFromRecentRecord(r.disc, 'disc')
|
||||
|
||||
const gallupPreview = (r.ai && r.ai.gallupPreview) ? String(r.ai.gallupPreview) : ''
|
||||
const patch = {
|
||||
@@ -206,7 +214,7 @@ Page({
|
||||
hasResults: !!(r.mbti || r.disc || r.pdp || r.ai),
|
||||
mbtiType: r.mbti ? r.mbti.resultText : '',
|
||||
discType,
|
||||
pdpType: r.pdp ? r.pdp.resultText : '',
|
||||
pdpType: summaryFromRecentRecord(r.pdp, 'pdp'),
|
||||
aiType: r.ai ? r.ai.resultText : '',
|
||||
gallupPreview,
|
||||
mbtiTime: r.mbti ? r.mbti.testTime : '',
|
||||
@@ -221,7 +229,7 @@ Page({
|
||||
const d = { ...this.data, ...patch }
|
||||
this.setData({
|
||||
...patch,
|
||||
showLatestTestCards: this._computeShowLatestTestCards(d),
|
||||
showLatestTestRow: this._computeShowLatestTestRow(d),
|
||||
showEmptyPersonalityTags: this._computeShowEmptyPersonalityTags(d)
|
||||
})
|
||||
},
|
||||
@@ -297,7 +305,7 @@ Page({
|
||||
const d = { ...this.data, ...patch }
|
||||
this.setData({
|
||||
...patch,
|
||||
showLatestTestCards: this._computeShowLatestTestCards(d),
|
||||
showLatestTestRow: this._computeShowLatestTestRow(d),
|
||||
showEmptyPersonalityTags: this._computeShowEmptyPersonalityTags(d)
|
||||
})
|
||||
},
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<text class="tag-text">{{mbtiType}}</text>
|
||||
</view>
|
||||
<view class="tag tag-blue" tt:if="{{discType && permDisc}}">
|
||||
<text class="tag-text">{{discType}}型</text>
|
||||
<text class="tag-text">{{discType}}</text>
|
||||
</view>
|
||||
<view class="tag tag-orange" tt:if="{{pdpType && permPdp}}">
|
||||
<text class="tag-text">{{pdpType}}</text>
|
||||
@@ -78,56 +78,56 @@
|
||||
<text class="depth-header-chevron">›</text>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view tt:if="{{showLatestTestCards}}" scroll-x class="cards-scroll cards-scroll--in-card" enhanced show-scrollbar="{{false}}">
|
||||
<scroll-view tt:if="{{showLatestTestRow}}" scroll-x class="cards-scroll cards-scroll--in-card" enhanced show-scrollbar="{{false}}">
|
||||
<view class="cards-row cards-row--in-card">
|
||||
<view class="result-card card-purple" bindtap="viewMBTI" tt:if="{{permMbti && mbtiType}}">
|
||||
<view class="result-card card-purple {{mbtiType ? '' : 'result-card--placeholder'}}" bindtap="viewMBTI" tt:if="{{permMbti}}">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-purple">
|
||||
<text class="card-icon">🧠</text>
|
||||
</view>
|
||||
<text class="card-label">MBTI性格</text>
|
||||
<text class="card-value">{{mbtiType}}</text>
|
||||
<text class="card-time">{{mbtiTime}}</text>
|
||||
<text class="card-value">{{mbtiType || '未测评'}}</text>
|
||||
<text class="card-time">{{mbtiTime || '—'}}</text>
|
||||
</view>
|
||||
<view class="result-card card-orange" bindtap="viewPDP" tt:if="{{permPdp && pdpType}}">
|
||||
<view class="result-card card-orange {{pdpType ? '' : 'result-card--placeholder'}}" bindtap="viewPDP" tt:if="{{permPdp}}">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-orange">
|
||||
<text class="card-icon">🦁</text>
|
||||
</view>
|
||||
<text class="card-label">PDP行为</text>
|
||||
<text class="card-value">{{pdpType}}</text>
|
||||
<text class="card-time">{{pdpTime}}</text>
|
||||
<text class="card-value">{{pdpType || '未测评'}}</text>
|
||||
<text class="card-time">{{pdpTime || '—'}}</text>
|
||||
</view>
|
||||
<view class="result-card card-blue" bindtap="viewDISC" tt:if="{{permDisc && discType}}">
|
||||
<view class="result-card card-blue {{discType ? '' : 'result-card--placeholder'}}" bindtap="viewDISC" tt:if="{{permDisc}}">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-blue">
|
||||
<text class="card-icon">📊</text>
|
||||
</view>
|
||||
<text class="card-label">DISC测评</text>
|
||||
<text class="card-value">{{discType + '型'}}</text>
|
||||
<text class="card-time">{{discTime}}</text>
|
||||
<text class="card-value">{{discType || '未测评'}}</text>
|
||||
<text class="card-time">{{discTime || '—'}}</text>
|
||||
</view>
|
||||
<view class="result-card card-teal" bindtap="viewGallup" tt:if="{{!reviewMode && permFace && (gallupPreview || aiType)}}">
|
||||
<view class="result-card card-teal {{(gallupPreview || aiType) ? '' : 'result-card--placeholder'}}" bindtap="viewGallup" tt:if="{{!reviewMode && permFace}}">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-teal">
|
||||
<text class="card-icon">⭐</text>
|
||||
</view>
|
||||
<text class="card-label">盖洛普优势</text>
|
||||
<text class="card-value card-value--small">{{gallupPreview || '见面相报告'}}</text>
|
||||
<text class="card-time">{{aiTime}}</text>
|
||||
<text class="card-value card-value--small">{{gallupPreview || (aiType ? '见面相报告' : '未测评')}}</text>
|
||||
<text class="card-time">{{aiTime || '—'}}</text>
|
||||
</view>
|
||||
<view class="result-card card-rose" bindtap="viewAI" tt:if="{{!reviewMode && permFace && aiType}}">
|
||||
<view class="result-card card-rose {{aiType ? '' : 'result-card--placeholder'}}" bindtap="viewAI" tt:if="{{!reviewMode && permFace}}">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-rose">
|
||||
<text class="card-icon">👁️</text>
|
||||
</view>
|
||||
<text class="card-label">面相分析</text>
|
||||
<text class="card-value">{{aiType}}</text>
|
||||
<text class="card-time">{{aiTime}}</text>
|
||||
<text class="card-value">{{aiType || '未测评'}}</text>
|
||||
<text class="card-time">{{aiTime || '—'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view tt:if="{{showLatestTestCards}}" class="depth-empty-hint depth-empty-hint--compact">
|
||||
<view tt:if="{{showLatestTestRow}}" class="depth-empty-hint depth-empty-hint--compact">
|
||||
<text>点击卡片查看详情;右上方可查看全部测试记录。</text>
|
||||
</view>
|
||||
<view class="depth-inner-divider"></view>
|
||||
|
||||
@@ -458,6 +458,14 @@ page {
|
||||
letter-spacing: -1rpx;
|
||||
}
|
||||
|
||||
.result-card .card-value {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.card-purple .card-value { color: #7C3AED; }
|
||||
.card-blue .card-value { color: #2563EB; }
|
||||
.card-orange .card-value { color: #D97706; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// pages/result/disc.js - DISC结果页(支持付费墙 + 历史详情拉取)
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
const { getTypeOnly } = require('../../utils/resultFormat')
|
||||
|
||||
function toIntPercent(v) {
|
||||
if (v == null) return 0
|
||||
@@ -24,6 +25,7 @@ function withPercentagesInt(data) {
|
||||
Page({
|
||||
data: {
|
||||
result: null,
|
||||
typeSummaryLine: '',
|
||||
typeList: [
|
||||
{ type: 'D', label: 'D型 - 支配型', colorClass: 'fill-d' },
|
||||
{ type: 'I', label: 'I型 - 影响型', colorClass: 'fill-i' },
|
||||
@@ -45,7 +47,10 @@ Page({
|
||||
}
|
||||
const result = tt.getStorageSync('discResult')
|
||||
if (result) {
|
||||
this.setData({ result: withPercentagesInt(result) })
|
||||
this.setData({
|
||||
result: withPercentagesInt(result),
|
||||
typeSummaryLine: getTypeOnly(result, 'disc')
|
||||
})
|
||||
this.initPayInfoFromRuntime('disc')
|
||||
} else {
|
||||
tt.showToast({ title: '暂无测试结果', icon: 'none' })
|
||||
@@ -71,7 +76,10 @@ Page({
|
||||
const paidAmount = payload.paidAmount != null ? Number(payload.paidAmount) : 0
|
||||
const amountYuan = payload.amountYuan != null ? Number(payload.amountYuan) : (paidAmount > 0 ? paidAmount / 100 : 0)
|
||||
const needPaymentToUnlock = payload.needPaymentToUnlock === true || (!!payload.requiresPayment && !isPaid && paidAmount > 0)
|
||||
this.setData({ result: withPercentagesInt(data) })
|
||||
this.setData({
|
||||
result: withPercentagesInt(data),
|
||||
typeSummaryLine: getTypeOnly(data, 'disc')
|
||||
})
|
||||
const payInfo = {
|
||||
requiresPayment: needPaymentToUnlock,
|
||||
isPaid,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<view class="type-card">
|
||||
<view class="type-header">
|
||||
<text class="type-label">您的DISC性格类型</text>
|
||||
<text class="type-value">{{result.dominantType}}{{result.secondaryType ? ' + ' + result.secondaryType : ''}}</text>
|
||||
<text class="type-value">{{typeSummaryLine}}</text>
|
||||
<text class="type-description" tt:if="{{result.description && result.description.description}}">{{result.description.description}}</text>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// pages/result/pdp.js - PDP结果页(支持付费墙 + 历史详情拉取)
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
const { getTypeOnly } = require('../../utils/resultFormat')
|
||||
|
||||
const PDP_KEYS = ['Tiger', 'Peacock', 'Koala', 'Owl', 'Chameleon']
|
||||
|
||||
@@ -23,6 +24,7 @@ function withPercentagesInt(data) {
|
||||
Page({
|
||||
data: {
|
||||
result: null,
|
||||
typeSummaryLine: '',
|
||||
typeList: [
|
||||
{ type: 'Tiger', emoji: '🐅', label: '老虎型', colorClass: 'fill-tiger' },
|
||||
{ type: 'Peacock', emoji: '🦚', label: '孔雀型', colorClass: 'fill-peacock' },
|
||||
@@ -45,7 +47,10 @@ Page({
|
||||
}
|
||||
const result = tt.getStorageSync('pdpResult')
|
||||
if (result) {
|
||||
this.setData({ result: withPercentagesInt(result) })
|
||||
this.setData({
|
||||
result: withPercentagesInt(result),
|
||||
typeSummaryLine: getTypeOnly(result, 'pdp')
|
||||
})
|
||||
this.initPayInfoFromRuntime('pdp')
|
||||
} else {
|
||||
tt.showToast({ title: '暂无测试结果', icon: 'none' })
|
||||
@@ -71,7 +76,10 @@ Page({
|
||||
const paidAmount = payload.paidAmount != null ? Number(payload.paidAmount) : 0
|
||||
const amountYuan = payload.amountYuan != null ? Number(payload.amountYuan) : (paidAmount > 0 ? paidAmount / 100 : 0)
|
||||
const needPaymentToUnlock = payload.needPaymentToUnlock === true || (!!payload.requiresPayment && !isPaid && paidAmount > 0)
|
||||
this.setData({ result: withPercentagesInt(data) })
|
||||
this.setData({
|
||||
result: withPercentagesInt(data),
|
||||
typeSummaryLine: getTypeOnly(data, 'pdp')
|
||||
})
|
||||
const payInfo = {
|
||||
requiresPayment: needPaymentToUnlock,
|
||||
isPaid,
|
||||
@@ -138,19 +146,19 @@ Page({
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const result = this.data.result
|
||||
const line = this.data.typeSummaryLine || this.data.result?.description?.type || ''
|
||||
const { getSharePathByScope } = require('../../utils/share')
|
||||
return {
|
||||
title: `我的PDP类型是${result?.description?.type}${result?.description?.emoji},来测测你的吧!`,
|
||||
title: `我的PDP类型是${line},来测测你的吧!`,
|
||||
path: getSharePathByScope('/pages/index/index')
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const result = this.data.result
|
||||
const line = this.data.typeSummaryLine || this.data.result?.description?.type || ''
|
||||
const { buildShareQuery } = require('../../utils/share')
|
||||
return {
|
||||
title: `我的PDP类型是${result?.description?.type}${result?.description?.emoji},来测测你的吧!`,
|
||||
title: `我的PDP类型是${line},来测测你的吧!`,
|
||||
query: buildShareQuery()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<view class="type-card">
|
||||
<view class="type-header">
|
||||
<text class="type-label">PDP性格类型</text>
|
||||
<text class="type-value">{{result.description.type || result.dominantType}}{{result.description.emoji || ''}}</text>
|
||||
<text class="type-value">{{typeSummaryLine}}</text>
|
||||
<text class="type-title" tt:if="{{result.description.title}}">{{result.description.title}}</text>
|
||||
<text class="type-description" tt:if="{{result.description.description}}">{{result.description.description}}</text>
|
||||
</view>
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
// pages/test/disc.js
|
||||
const { discQuestions, shuffleQuestions } = require('../../utils/questions')
|
||||
const { loadQuestions } = require('../../utils/questionBank')
|
||||
const { discDescriptions } = require('../../utils/descriptions')
|
||||
const app = getApp()
|
||||
|
||||
const DISC_TIME_SEC = 15 * 60
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: true,
|
||||
questions: [],
|
||||
currentIndex: 0,
|
||||
currentQuestion: null,
|
||||
answers: {},
|
||||
selectedAnswer: null,
|
||||
total: discQuestions.length,
|
||||
total: 0,
|
||||
answeredCount: 0,
|
||||
progress: 0,
|
||||
timeRemaining: 15 * 60,
|
||||
timeRemaining: DISC_TIME_SEC,
|
||||
_initialSeconds: DISC_TIME_SEC,
|
||||
formatTime: '15:00',
|
||||
isSubmitting: false
|
||||
},
|
||||
@@ -21,10 +25,31 @@ Page({
|
||||
timer: null,
|
||||
|
||||
onLoad() {
|
||||
const questions = shuffleQuestions(discQuestions)
|
||||
this.setData({ questions, currentQuestion: questions[0] })
|
||||
try { require('../../utils/analytics').track('test_start', { type: 'disc', total: questions.length }) } catch (e) {}
|
||||
this.startTimer()
|
||||
loadQuestions('disc', {})
|
||||
.then((questions) => {
|
||||
if (!questions.length) {
|
||||
tt.showToast({ title: '暂无题目', icon: 'none' })
|
||||
this.setData({ loading: false })
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
loading: false,
|
||||
questions,
|
||||
currentQuestion: questions[0],
|
||||
total: questions.length,
|
||||
timeRemaining: DISC_TIME_SEC,
|
||||
_initialSeconds: DISC_TIME_SEC,
|
||||
formatTime: '15:00'
|
||||
})
|
||||
try {
|
||||
require('../../utils/analytics').track('test_start', { type: 'disc', total: questions.length })
|
||||
} catch (e) {}
|
||||
this.startTimer()
|
||||
})
|
||||
.catch((err) => {
|
||||
this.setData({ loading: false })
|
||||
tt.showToast({ title: (err && err.message) || '加载失败', icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
@@ -144,7 +169,7 @@ Page({
|
||||
dominantType,
|
||||
secondaryType,
|
||||
description: discDescriptions[dominantType],
|
||||
testDuration: 15 * 60 - this.data.timeRemaining,
|
||||
testDuration: (this.data._initialSeconds || DISC_TIME_SEC) - this.data.timeRemaining,
|
||||
completedAt: new Date().toISOString(),
|
||||
// 便于后端留存完整答题过程
|
||||
answers: this.data.answers
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<!--pages/test/disc.wxml - DISC测试页面(按旧版模板重构)-->
|
||||
<view class="test-page">
|
||||
<view tt:if="{{loading}}" class="test-loading">
|
||||
<text class="test-loading-text">加载题目…</text>
|
||||
</view>
|
||||
<block tt:elif="{{currentQuestion}}">
|
||||
<view class="progress-section">
|
||||
<view class="progress-info">
|
||||
<text class="question-count">问题 {{currentIndex + 1}}/{{total}}</text>
|
||||
@@ -45,4 +49,5 @@
|
||||
<text class="submit-text">{{isSubmitting ? '计算中...' : '完成测试,查看结果'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -7,6 +7,19 @@
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.test-loading {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx;
|
||||
}
|
||||
|
||||
.test-loading-text {
|
||||
font-size: 30rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
// pages/test/mbti.js - MBTI测试页面逻辑(与微信端对齐:最后一题必提交结果)
|
||||
const { mbtiQuestions, shuffleQuestions } = require('../../utils/questions')
|
||||
const { loadQuestions } = require('../../utils/questionBank')
|
||||
const { mbtiDescriptions } = require('../../utils/descriptions')
|
||||
const payment = require('../../utils/payment')
|
||||
const app = getApp()
|
||||
|
||||
const MBTI_TIME_SEC = 30 * 60 // 30 分钟
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: true,
|
||||
questions: [],
|
||||
currentIndex: 0,
|
||||
currentQuestion: null,
|
||||
answers: {},
|
||||
selectedAnswer: null,
|
||||
total: mbtiQuestions.length,
|
||||
total: 0,
|
||||
answeredCount: 0,
|
||||
progress: 0,
|
||||
timeRemaining: 30 * 60,
|
||||
timeRemaining: MBTI_TIME_SEC,
|
||||
_initialSeconds: MBTI_TIME_SEC,
|
||||
formatTime: '30:00',
|
||||
isSubmitting: false,
|
||||
canAccess: false
|
||||
@@ -23,17 +27,34 @@ Page({
|
||||
timer: null,
|
||||
|
||||
onLoad() {
|
||||
const questions = shuffleQuestions(mbtiQuestions)
|
||||
const total = questions.length
|
||||
this.setData({
|
||||
questions,
|
||||
currentQuestion: questions[0],
|
||||
canAccess: true,
|
||||
total,
|
||||
progress: total ? Math.round((1 / total) * 100) : 0
|
||||
})
|
||||
try { require('../../utils/analytics').track('test_start', { type: 'mbti', total }) } catch (e) {}
|
||||
this.startTimer()
|
||||
loadQuestions('mbti', {})
|
||||
.then((questions) => {
|
||||
const total = questions.length
|
||||
if (!total) {
|
||||
tt.showToast({ title: '暂无题目', icon: 'none' })
|
||||
this.setData({ loading: false })
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
loading: false,
|
||||
questions,
|
||||
currentQuestion: questions[0],
|
||||
canAccess: true,
|
||||
total,
|
||||
progress: Math.round((1 / total) * 100),
|
||||
timeRemaining: MBTI_TIME_SEC,
|
||||
_initialSeconds: MBTI_TIME_SEC,
|
||||
formatTime: '30:00'
|
||||
})
|
||||
try {
|
||||
require('../../utils/analytics').track('test_start', { type: 'mbti', total })
|
||||
} catch (e) {}
|
||||
this.startTimer()
|
||||
})
|
||||
.catch((err) => {
|
||||
this.setData({ loading: false })
|
||||
tt.showToast({ title: (err && err.message) || '加载失败', icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
checkAccess() {
|
||||
@@ -186,7 +207,7 @@ Page({
|
||||
const resultData = {
|
||||
...result,
|
||||
answers: this.data.answers,
|
||||
testDuration: 30 * 60 - this.data.timeRemaining,
|
||||
testDuration: (this.data._initialSeconds || MBTI_TIME_SEC) - this.data.timeRemaining,
|
||||
completedAt: new Date().toISOString(),
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<!--pages/test/mbti.wxml - MBTI测试页面(按旧版模板重构)-->
|
||||
<view class="test-page">
|
||||
<view tt:if="{{loading}}" class="test-loading">
|
||||
<text class="test-loading-text">加载题目…</text>
|
||||
</view>
|
||||
<block tt:elif="{{currentQuestion}}">
|
||||
<view class="progress-section">
|
||||
<view class="progress-info">
|
||||
<text class="question-count">问题 {{currentIndex + 1}}/{{total}}</text>
|
||||
@@ -45,4 +49,5 @@
|
||||
<text class="button-text button-text-on-primary">{{isSubmitting ? '正在生成…' : '查看结果'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -7,6 +7,19 @@
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.test-loading {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx;
|
||||
}
|
||||
|
||||
.test-loading-text {
|
||||
font-size: 30rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
// pages/test/pdp.js
|
||||
const { pdpQuestions, shuffleQuestions } = require('../../utils/questions')
|
||||
const { loadQuestions } = require('../../utils/questionBank')
|
||||
const { pdpDescriptions } = require('../../utils/descriptions')
|
||||
const app = getApp()
|
||||
|
||||
const PDP_TIME_SEC = 15 * 60
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: true,
|
||||
questions: [],
|
||||
currentIndex: 0,
|
||||
currentQuestion: null,
|
||||
answers: {},
|
||||
selectedAnswer: null,
|
||||
total: pdpQuestions.length,
|
||||
total: 0,
|
||||
answeredCount: 0,
|
||||
progress: 0,
|
||||
timeRemaining: 15 * 60,
|
||||
timeRemaining: PDP_TIME_SEC,
|
||||
_initialSeconds: PDP_TIME_SEC,
|
||||
formatTime: '15:00',
|
||||
isSubmitting: false
|
||||
},
|
||||
@@ -21,10 +25,31 @@ Page({
|
||||
timer: null,
|
||||
|
||||
onLoad() {
|
||||
const questions = shuffleQuestions(pdpQuestions)
|
||||
this.setData({ questions, currentQuestion: questions[0] })
|
||||
try { require('../../utils/analytics').track('test_start', { type: 'pdp', total: questions.length }) } catch (e) {}
|
||||
this.startTimer()
|
||||
loadQuestions('pdp', {})
|
||||
.then((questions) => {
|
||||
if (!questions.length) {
|
||||
tt.showToast({ title: '暂无题目', icon: 'none' })
|
||||
this.setData({ loading: false })
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
loading: false,
|
||||
questions,
|
||||
currentQuestion: questions[0],
|
||||
total: questions.length,
|
||||
timeRemaining: PDP_TIME_SEC,
|
||||
_initialSeconds: PDP_TIME_SEC,
|
||||
formatTime: '15:00'
|
||||
})
|
||||
try {
|
||||
require('../../utils/analytics').track('test_start', { type: 'pdp', total: questions.length })
|
||||
} catch (e) {}
|
||||
this.startTimer()
|
||||
})
|
||||
.catch((err) => {
|
||||
this.setData({ loading: false })
|
||||
tt.showToast({ title: (err && err.message) || '加载失败', icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
@@ -147,7 +172,7 @@ Page({
|
||||
dominantType,
|
||||
secondaryType,
|
||||
description: pdpDescriptions[dominantType],
|
||||
testDuration: 15 * 60 - this.data.timeRemaining,
|
||||
testDuration: (this.data._initialSeconds || PDP_TIME_SEC) - this.data.timeRemaining,
|
||||
completedAt: new Date().toISOString(),
|
||||
// 便于后端留存完整答题过程
|
||||
answers: this.data.answers
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<!--pages/test/pdp.wxml - PDP测试页面(按旧版模板重构)-->
|
||||
<view class="test-page">
|
||||
<view tt:if="{{loading}}" class="test-loading">
|
||||
<text class="test-loading-text">加载题目…</text>
|
||||
</view>
|
||||
<block tt:elif="{{currentQuestion}}">
|
||||
<view class="progress-section">
|
||||
<view class="progress-info">
|
||||
<text class="question-count">问题 {{currentIndex + 1}}/{{total}}</text>
|
||||
@@ -45,4 +49,5 @@
|
||||
<text class="submit-text">{{isSubmitting ? '计算中...' : '完成测试,查看结果'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -7,6 +7,19 @@
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.test-loading {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx;
|
||||
}
|
||||
|
||||
.test-loading-text {
|
||||
font-size: 30rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
|
||||
44
douyin-miniprogram/utils/enterpriseContext.js
Normal file
44
douyin-miniprogram/utils/enterpriseContext.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 抖音小程序企业上下文(与微信 miniprogram 逻辑对齐,storage 用 tt)
|
||||
*/
|
||||
|
||||
function getEffectiveEnterpriseId() {
|
||||
const app = getApp()
|
||||
const gd = app.globalData || {}
|
||||
const fromScene = gd.enterpriseIdFromScene
|
||||
if (fromScene != null && Number(fromScene) > 0) {
|
||||
return Number(fromScene)
|
||||
}
|
||||
const u = gd.userInfo || tt.getStorageSync('userInfo') || {}
|
||||
const bound = u.enterpriseId
|
||||
if (bound != null && Number(bound) > 0) {
|
||||
return Number(bound)
|
||||
}
|
||||
const def = gd.defaultEnterpriseId
|
||||
if (def != null && Number(def) > 0) {
|
||||
return Number(def)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 与微信 miniprogram/utils/enterpriseContext.getEnterpriseIdForApiPayload 一致
|
||||
*/
|
||||
function getEnterpriseIdForApiPayload() {
|
||||
const app = getApp()
|
||||
const gd = app.globalData || {}
|
||||
const scope = gd.appScope || 'personal'
|
||||
if (scope === 'personal') {
|
||||
const fromScene = gd.enterpriseIdFromScene
|
||||
if (fromScene != null && Number(fromScene) > 0) {
|
||||
return Number(fromScene)
|
||||
}
|
||||
return null
|
||||
}
|
||||
return getEffectiveEnterpriseId()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getEffectiveEnterpriseId,
|
||||
getEnterpriseIdForApiPayload
|
||||
}
|
||||
98
douyin-miniprogram/utils/questionBank.js
Normal file
98
douyin-miniprogram/utils/questionBank.js
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 仅从服务端拉取启用题库,无本地题目保底。
|
||||
*/
|
||||
const { requestPromise } = require('./request')
|
||||
|
||||
function getAppSafe() {
|
||||
try {
|
||||
return getApp()
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function shuffleQuestions(questions) {
|
||||
const arr = (questions || []).map(q => ({
|
||||
...q,
|
||||
options: (q.options || []).slice().sort(() => Math.random() - 0.5)
|
||||
}))
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1))
|
||||
;[arr[i], arr[j]] = [arr[j], arr[i]]
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
function resolveEnterpriseIdForQuestionBank(opts) {
|
||||
const o = opts || {}
|
||||
if (Object.prototype.hasOwnProperty.call(o, 'enterpriseId')) {
|
||||
const v = o.enterpriseId
|
||||
if (v == null || v === '') {
|
||||
return null
|
||||
}
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) && n > 0 ? n : null
|
||||
}
|
||||
try {
|
||||
const { getEnterpriseIdForApiPayload } = require('./enterpriseContext')
|
||||
return getEnterpriseIdForApiPayload()
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getTestQuestionDrawCount() {
|
||||
const app = getAppSafe()
|
||||
const n = app && app.globalData && app.globalData.testQuestionDrawCount
|
||||
const num = parseInt(String(n == null || n === '' ? '0' : n), 10)
|
||||
if (!Number.isFinite(num) || num <= 0) {
|
||||
return 0
|
||||
}
|
||||
return Math.min(500, num)
|
||||
}
|
||||
|
||||
function applyDrawCountAfterShuffle(questions) {
|
||||
const n = getTestQuestionDrawCount()
|
||||
if (!n || questions.length <= n) {
|
||||
return questions
|
||||
}
|
||||
return questions.slice(0, n)
|
||||
}
|
||||
|
||||
function fetchQuestionBank(type, enterpriseId) {
|
||||
const q = [`type=${encodeURIComponent(type)}`]
|
||||
if (enterpriseId != null && Number(enterpriseId) > 0) {
|
||||
q.push(`enterpriseId=${Number(enterpriseId)}`)
|
||||
}
|
||||
return requestPromise({
|
||||
url: `/api/test/questions?${q.join('&')}`,
|
||||
method: 'GET',
|
||||
needAuth: true
|
||||
}).then((res) => {
|
||||
const body = res.data || {}
|
||||
if (body.code !== 200 || body.data == null) {
|
||||
throw new Error(body.message || '拉取题库失败')
|
||||
}
|
||||
const list = body.data.list
|
||||
if (!Array.isArray(list)) {
|
||||
throw new Error('题库格式错误')
|
||||
}
|
||||
return list
|
||||
})
|
||||
}
|
||||
|
||||
function loadQuestions(type, opts = {}) {
|
||||
const enterpriseId = resolveEnterpriseIdForQuestionBank(opts)
|
||||
return fetchQuestionBank(type, enterpriseId).then((list) => {
|
||||
if (!list.length) {
|
||||
throw new Error('暂无启用题目')
|
||||
}
|
||||
return applyDrawCountAfterShuffle(shuffleQuestions(list))
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchQuestionBank,
|
||||
loadQuestions,
|
||||
shuffleQuestions
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
// utils/questions.js - 测试题库
|
||||
|
||||
// MBTI测试题(精简版30题,每个维度约7-8题)
|
||||
const mbtiQuestions = [
|
||||
// E vs I 维度 (8题)
|
||||
{ id: 1, question: "在社交场合中,您通常会:", dimension: "EI", options: [{ value: "E", text: "认识新朋友,扩大社交圈" }, { value: "I", text: "与已认识的朋友交流" }] },
|
||||
{ id: 2, question: "您更喜欢:", dimension: "EI", options: [{ value: "E", text: "在团队中工作" }, { value: "I", text: "独立工作" }] },
|
||||
{ id: 3, question: "当您需要充电时,您会选择:", dimension: "EI", options: [{ value: "E", text: "与朋友聚会" }, { value: "I", text: "独处休息" }] },
|
||||
{ id: 4, question: "在会议中,您通常:", dimension: "EI", options: [{ value: "E", text: "积极发言" }, { value: "I", text: "先思考再发表" }] },
|
||||
{ id: 5, question: "您更倾向于:", dimension: "EI", options: [{ value: "E", text: "认识很多人" }, { value: "I", text: "与少数人深交" }] },
|
||||
{ id: 6, question: "面对问题时,您更倾向于:", dimension: "EI", options: [{ value: "E", text: "与他人讨论" }, { value: "I", text: "独自思考" }] },
|
||||
{ id: 7, question: "您更喜欢的休闲活动是:", dimension: "EI", options: [{ value: "E", text: "团体活动" }, { value: "I", text: "个人爱好" }] },
|
||||
{ id: 8, question: "在陌生环境中,您通常会:", dimension: "EI", options: [{ value: "E", text: "主动与人交谈" }, { value: "I", text: "观察周围环境" }] },
|
||||
|
||||
// S vs N 维度 (7题)
|
||||
{ id: 9, question: "您更关注:", dimension: "SN", options: [{ value: "S", text: "具体的细节和事实" }, { value: "N", text: "整体概念和可能性" }] },
|
||||
{ id: 10, question: "您更信任:", dimension: "SN", options: [{ value: "S", text: "实际经验" }, { value: "N", text: "直觉和灵感" }] },
|
||||
{ id: 11, question: "您更喜欢:", dimension: "SN", options: [{ value: "S", text: "按步骤执行" }, { value: "N", text: "创造性解决" }] },
|
||||
{ id: 12, question: "学习新事物时,您更喜欢:", dimension: "SN", options: [{ value: "S", text: "实际操作" }, { value: "N", text: "理论学习" }] },
|
||||
{ id: 13, question: "您更喜欢的工作类型是:", dimension: "SN", options: [{ value: "S", text: "明确的任务" }, { value: "N", text: "创新的项目" }] },
|
||||
{ id: 14, question: "您描述事物时更倾向于:", dimension: "SN", options: [{ value: "S", text: "具体描述" }, { value: "N", text: "使用比喻" }] },
|
||||
{ id: 15, question: "您更关心:", dimension: "SN", options: [{ value: "S", text: "当下的现实" }, { value: "N", text: "未来的可能" }] },
|
||||
|
||||
// T vs F 维度 (8题)
|
||||
{ id: 16, question: "做决定时,您更依赖:", dimension: "TF", options: [{ value: "T", text: "逻辑分析" }, { value: "F", text: "个人价值" }] },
|
||||
{ id: 17, question: "在争论中,您更看重:", dimension: "TF", options: [{ value: "T", text: "事实真相" }, { value: "F", text: "和谐关系" }] },
|
||||
{ id: 18, question: "给予反馈时,您更注重:", dimension: "TF", options: [{ value: "T", text: "直接指出问题" }, { value: "F", text: "考虑对方感受" }] },
|
||||
{ id: 19, question: "评估方案时,您更关注:", dimension: "TF", options: [{ value: "T", text: "效率和结果" }, { value: "F", text: "对人的影响" }] },
|
||||
{ id: 20, question: "当朋友遇到困难时,您更倾向于:", dimension: "TF", options: [{ value: "T", text: "分析问题提供建议" }, { value: "F", text: "倾听并给予支持" }] },
|
||||
{ id: 21, question: "您更欣赏的品质是:", dimension: "TF", options: [{ value: "T", text: "理性客观" }, { value: "F", text: "善解人意" }] },
|
||||
{ id: 22, question: "您认为好的决定应该:", dimension: "TF", options: [{ value: "T", text: "基于客观分析" }, { value: "F", text: "考虑各方感受" }] },
|
||||
{ id: 23, question: "当与他人意见不同时,您更倾向于:", dimension: "TF", options: [{ value: "T", text: "坚持正确观点" }, { value: "F", text: "寻求共识" }] },
|
||||
|
||||
// J vs P 维度 (7题)
|
||||
{ id: 24, question: "您更喜欢的工作方式是:", dimension: "JP", options: [{ value: "J", text: "有计划地进行" }, { value: "P", text: "随机应变" }] },
|
||||
{ id: 25, question: "对于截止日期,您通常:", dimension: "JP", options: [{ value: "J", text: "提前完成" }, { value: "P", text: "在最后完成" }] },
|
||||
{ id: 26, question: "您的生活方式更倾向于:", dimension: "JP", options: [{ value: "J", text: "有条理有规律" }, { value: "P", text: "灵活随意" }] },
|
||||
{ id: 27, question: "面对选择时,您更倾向于:", dimension: "JP", options: [{ value: "J", text: "快速做出决定" }, { value: "P", text: "保持开放选项" }] },
|
||||
{ id: 28, question: "您的桌面通常是:", dimension: "JP", options: [{ value: "J", text: "整洁有序" }, { value: "P", text: "创意性混乱" }] },
|
||||
{ id: 29, question: "计划改变时,您的反应是:", dimension: "JP", options: [{ value: "J", text: "感到不安" }, { value: "P", text: "觉得有趣" }] },
|
||||
{ id: 30, question: "您更喜欢:", dimension: "JP", options: [{ value: "J", text: "事先规划" }, { value: "P", text: "即兴发挥" }] }
|
||||
]
|
||||
|
||||
// DISC测试题 (20题)
|
||||
const discQuestions = [
|
||||
{ id: 1, question: "在团队中,您更倾向于:", options: [{ value: "D", text: "主导决策,带领团队" }, { value: "I", text: "活跃气氛,激励成员" }, { value: "S", text: "支持他人,确保和谐" }, { value: "C", text: "分析数据,确保质量" }] },
|
||||
{ id: 2, question: "面对挑战时,您的第一反应是:", options: [{ value: "D", text: "立即行动" }, { value: "I", text: "寻找支持" }, { value: "S", text: "冷静思考" }, { value: "C", text: "收集信息" }] },
|
||||
{ id: 3, question: "您在工作中最看重的是:", options: [{ value: "D", text: "成果和效率" }, { value: "I", text: "认可和赞赏" }, { value: "S", text: "稳定和安全" }, { value: "C", text: "准确和质量" }] },
|
||||
{ id: 4, question: "与他人沟通时,您通常:", options: [{ value: "D", text: "直接了当" }, { value: "I", text: "热情友好" }, { value: "S", text: "耐心倾听" }, { value: "C", text: "逻辑清晰" }] },
|
||||
{ id: 5, question: "压力之下,您会:", options: [{ value: "D", text: "更加强势" }, { value: "I", text: "寻求鼓励" }, { value: "S", text: "保持冷静" }, { value: "C", text: "更加谨慎" }] },
|
||||
{ id: 6, question: "您认为自己的优势是:", options: [{ value: "D", text: "决断力强" }, { value: "I", text: "人际关系好" }, { value: "S", text: "可靠稳定" }, { value: "C", text: "分析能力强" }] },
|
||||
{ id: 7, question: "在会议中,您通常扮演:", options: [{ value: "D", text: "主导者" }, { value: "I", text: "激励者" }, { value: "S", text: "调和者" }, { value: "C", text: "分析者" }] },
|
||||
{ id: 8, question: "您最不喜欢的工作环境是:", options: [{ value: "D", text: "进展缓慢" }, { value: "I", text: "被孤立" }, { value: "S", text: "变化太快" }, { value: "C", text: "混乱无序" }] },
|
||||
{ id: 9, question: "做决定时,您更依赖:", options: [{ value: "D", text: "直觉经验" }, { value: "I", text: "他人意见" }, { value: "S", text: "过去经验" }, { value: "C", text: "数据事实" }] },
|
||||
{ id: 10, question: "您的工作风格是:", options: [{ value: "D", text: "快速高效" }, { value: "I", text: "灵活多变" }, { value: "S", text: "稳定持续" }, { value: "C", text: "严谨细致" }] },
|
||||
{ id: 11, question: "遇到冲突时,您会:", options: [{ value: "D", text: "直面解决" }, { value: "I", text: "调解双方" }, { value: "S", text: "避免冲突" }, { value: "C", text: "分析原因" }] },
|
||||
{ id: 12, question: "您期望的领导风格是:", options: [{ value: "D", text: "给予挑战" }, { value: "I", text: "认可表扬" }, { value: "S", text: "稳定支持" }, { value: "C", text: "明确指导" }] },
|
||||
{ id: 13, question: "处理任务时,您更注重:", options: [{ value: "D", text: "速度效率" }, { value: "I", text: "创意新颖" }, { value: "S", text: "过程协作" }, { value: "C", text: "质量准确" }] },
|
||||
{ id: 14, question: "您的社交方式是:", options: [{ value: "D", text: "目的明确" }, { value: "I", text: "广泛社交" }, { value: "S", text: "深度交往" }, { value: "C", text: "选择性社交" }] },
|
||||
{ id: 15, question: "您理想的工作节奏是:", options: [{ value: "D", text: "快节奏" }, { value: "I", text: "灵活多变" }, { value: "S", text: "稳定有序" }, { value: "C", text: "有条理" }] },
|
||||
{ id: 16, question: "面对变化,您的态度是:", options: [{ value: "D", text: "主动拥抱" }, { value: "I", text: "积极适应" }, { value: "S", text: "需要时间" }, { value: "C", text: "谨慎评估" }] },
|
||||
{ id: 17, question: "您的时间管理风格是:", options: [{ value: "D", text: "高效利用" }, { value: "I", text: "灵活安排" }, { value: "S", text: "按部就班" }, { value: "C", text: "精确规划" }] },
|
||||
{ id: 18, question: "激励您的是:", options: [{ value: "D", text: "成就控制" }, { value: "I", text: "认可社交" }, { value: "S", text: "稳定归属" }, { value: "C", text: "正确标准" }] },
|
||||
{ id: 19, question: "您处理细节的方式是:", options: [{ value: "D", text: "关注大局" }, { value: "I", text: "可能忽略" }, { value: "S", text: "认真对待" }, { value: "C", text: "极度重视" }] },
|
||||
{ id: 20, question: "您对规则的态度是:", options: [{ value: "D", text: "灵活打破" }, { value: "I", text: "灵活运用" }, { value: "S", text: "遵守维护" }, { value: "C", text: "严格遵守" }] }
|
||||
]
|
||||
|
||||
// PDP测试题 (20题)
|
||||
const pdpQuestions = [
|
||||
{ id: 1, question: "面对紧急任务,您的第一反应是:", options: [{ value: "Tiger", text: "立即行动" }, { value: "Peacock", text: "召集团队" }, { value: "Koala", text: "冷静分析" }, { value: "Owl", text: "仔细规划" }, { value: "Chameleon", text: "灵活应对" }] },
|
||||
{ id: 2, question: "在社交场合,您通常会:", options: [{ value: "Tiger", text: "主导话题" }, { value: "Peacock", text: "活跃气氛" }, { value: "Koala", text: "安静倾听" }, { value: "Owl", text: "观察分析" }, { value: "Chameleon", text: "根据对象调整" }] },
|
||||
{ id: 3, question: "您最看重工作中的:", options: [{ value: "Tiger", text: "权力和成就" }, { value: "Peacock", text: "认可和赞赏" }, { value: "Koala", text: "稳定和和谐" }, { value: "Owl", text: "准确和质量" }, { value: "Chameleon", text: "平衡和适应" }] },
|
||||
{ id: 4, question: "处理冲突时,您倾向于:", options: [{ value: "Tiger", text: "直接解决" }, { value: "Peacock", text: "调解双方" }, { value: "Koala", text: "避免冲突" }, { value: "Owl", text: "理性处理" }, { value: "Chameleon", text: "视情况而定" }] },
|
||||
{ id: 5, question: "您的决策风格是:", options: [{ value: "Tiger", text: "果断迅速" }, { value: "Peacock", text: "直觉判断" }, { value: "Koala", text: "深思熟虑" }, { value: "Owl", text: "数据分析" }, { value: "Chameleon", text: "灵活决策" }] },
|
||||
{ id: 6, question: "面对压力,您会:", options: [{ value: "Tiger", text: "更加强势" }, { value: "Peacock", text: "寻求支持" }, { value: "Koala", text: "保持冷静" }, { value: "Owl", text: "更加谨慎" }, { value: "Chameleon", text: "调整策略" }] },
|
||||
{ id: 7, question: "您的领导风格是:", options: [{ value: "Tiger", text: "指挥型" }, { value: "Peacock", text: "激励型" }, { value: "Koala", text: "支持型" }, { value: "Owl", text: "专家型" }, { value: "Chameleon", text: "教练型" }] },
|
||||
{ id: 8, question: "您处理细节的方式是:", options: [{ value: "Tiger", text: "关注大局" }, { value: "Peacock", text: "可能忽略" }, { value: "Koala", text: "认真对待" }, { value: "Owl", text: "极度重视" }, { value: "Chameleon", text: "视情况决定" }] },
|
||||
{ id: 9, question: "您的沟通方式是:", options: [{ value: "Tiger", text: "直接简短" }, { value: "Peacock", text: "热情生动" }, { value: "Koala", text: "温和耐心" }, { value: "Owl", text: "逻辑清晰" }, { value: "Chameleon", text: "根据对象调整" }] },
|
||||
{ id: 10, question: "您对变化的态度是:", options: [{ value: "Tiger", text: "主动推动" }, { value: "Peacock", text: "积极拥抱" }, { value: "Koala", text: "需要适应" }, { value: "Owl", text: "谨慎评估" }, { value: "Chameleon", text: "随机应变" }] },
|
||||
{ id: 11, question: "您的时间管理风格是:", options: [{ value: "Tiger", text: "追求速度" }, { value: "Peacock", text: "灵活安排" }, { value: "Koala", text: "稳定执行" }, { value: "Owl", text: "精确规划" }, { value: "Chameleon", text: "根据情况调整" }] },
|
||||
{ id: 12, question: "您被什么激励:", options: [{ value: "Tiger", text: "成就权力" }, { value: "Peacock", text: "认可赞赏" }, { value: "Koala", text: "安全归属" }, { value: "Owl", text: "正确标准" }, { value: "Chameleon", text: "多样平衡" }] },
|
||||
{ id: 13, question: "您的学习方式是:", options: [{ value: "Tiger", text: "边做边学" }, { value: "Peacock", text: "互动讨论" }, { value: "Koala", text: "循序渐进" }, { value: "Owl", text: "深入研究" }, { value: "Chameleon", text: "多种结合" }] },
|
||||
{ id: 14, question: "您对规则的态度是:", options: [{ value: "Tiger", text: "灵活打破" }, { value: "Peacock", text: "不拘一格" }, { value: "Koala", text: "遵守维护" }, { value: "Owl", text: "严格遵守" }, { value: "Chameleon", text: "灵活处理" }] },
|
||||
{ id: 15, question: "您在团队中的角色是:", options: [{ value: "Tiger", text: "领导者" }, { value: "Peacock", text: "激励者" }, { value: "Koala", text: "协调者" }, { value: "Owl", text: "专家" }, { value: "Chameleon", text: "多面手" }] },
|
||||
{ id: 16, question: "您的工作节奏是:", options: [{ value: "Tiger", text: "快节奏" }, { value: "Peacock", text: "充满活力" }, { value: "Koala", text: "稳定有序" }, { value: "Owl", text: "有条理" }, { value: "Chameleon", text: "灵活调整" }] },
|
||||
{ id: 17, question: "您最大的优势是:", options: [{ value: "Tiger", text: "执行力" }, { value: "Peacock", text: "影响力" }, { value: "Koala", text: "可靠性" }, { value: "Owl", text: "准确性" }, { value: "Chameleon", text: "适应力" }] },
|
||||
{ id: 18, question: "您的人际关系特点是:", options: [{ value: "Tiger", text: "目标导向" }, { value: "Peacock", text: "朋友众多" }, { value: "Koala", text: "关系稳定" }, { value: "Owl", text: "志同道合" }, { value: "Chameleon", text: "灵活建立" }] },
|
||||
{ id: 19, question: "面对批评,您的反应是:", options: [{ value: "Tiger", text: "可能反驳" }, { value: "Peacock", text: "可能受伤" }, { value: "Koala", text: "接受思考" }, { value: "Owl", text: "分析合理性" }, { value: "Chameleon", text: "灵活调整" }] },
|
||||
{ id: 20, question: "您的理想工作环境是:", options: [{ value: "Tiger", text: "充满挑战" }, { value: "Peacock", text: "互动频繁" }, { value: "Koala", text: "稳定和谐" }, { value: "Owl", text: "有序规范" }, { value: "Chameleon", text: "灵活多变" }] }
|
||||
]
|
||||
|
||||
/**
|
||||
* Fisher-Yates 洗牌:随机打乱题目顺序,同时随机打乱每题的选项顺序
|
||||
* 不修改原数组,返回深拷贝后的新数组
|
||||
* @param {Array} questions - 原题目数组
|
||||
* @returns {Array} 打乱后的题目数组
|
||||
*/
|
||||
function shuffleQuestions(questions) {
|
||||
// 深拷贝,避免污染原始数组
|
||||
const arr = questions.map(q => ({
|
||||
...q,
|
||||
options: q.options.slice().sort(() => Math.random() - 0.5)
|
||||
}))
|
||||
// Fisher-Yates 打乱题目顺序
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]]
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mbtiQuestions,
|
||||
discQuestions,
|
||||
pdpQuestions,
|
||||
shuffleQuestions
|
||||
}
|
||||
@@ -8,12 +8,164 @@ function toIntPercent(value) {
|
||||
return Number.isFinite(n) ? Math.round(n) : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据测试类型和原始结果,生成带整数百分比的摘要文案
|
||||
* @param {object} data - 单条测试结果(mbtiResult / discResult / pdpResult)
|
||||
* @param {string} testType - 'mbti' | 'disc' | 'pdp'
|
||||
* @returns {string}
|
||||
*/
|
||||
const PDP_EN_TO_CN = {
|
||||
Tiger: '老虎型',
|
||||
Peacock: '孔雀型',
|
||||
Koala: '考拉型',
|
||||
Owl: '猫头鹰型',
|
||||
Chameleon: '变色龙型'
|
||||
}
|
||||
|
||||
function pdpOrderedFromScores(scores) {
|
||||
if (!scores || typeof scores !== 'object') return ['', '']
|
||||
const pairs = []
|
||||
for (const k of Object.keys(scores)) {
|
||||
if (!PDP_EN_TO_CN[k]) continue
|
||||
pairs.push([k, Number(scores[k]) || 0])
|
||||
}
|
||||
pairs.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||
return [pairs[0] ? pairs[0][0] : '', pairs[1] ? pairs[1][0] : '']
|
||||
}
|
||||
|
||||
function discOrderedFromScores(scores) {
|
||||
if (!scores || typeof scores !== 'object') return ['', '']
|
||||
const allow = { D: 1, I: 1, S: 1, C: 1 }
|
||||
const pairs = []
|
||||
for (const k of Object.keys(scores)) {
|
||||
const u = String(k).trim().toUpperCase().charAt(0)
|
||||
if (!allow[u]) continue
|
||||
pairs.push([u, Number(scores[k]) || 0])
|
||||
}
|
||||
pairs.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||
return [pairs[0] ? pairs[0][0] : '', pairs[1] ? pairs[1][0] : '']
|
||||
}
|
||||
|
||||
function discOrderedFromPercentages(pct) {
|
||||
return discOrderedFromScores(pct)
|
||||
}
|
||||
|
||||
function discPrimaryLetter(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const dType = data.description && data.description.type
|
||||
if (typeof dType === 'string' && dType) {
|
||||
const noXing = String(dType).trim().replace(/型$/, '')
|
||||
if (noXing.length === 1) {
|
||||
const u = noXing.toUpperCase()
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) return u
|
||||
}
|
||||
}
|
||||
if (data.dominantType) {
|
||||
const u = String(data.dominantType).trim().toUpperCase().charAt(0)
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) return u
|
||||
}
|
||||
if (data.disc) {
|
||||
const noXing = String(data.disc).trim().replace(/型$/, '')
|
||||
if (noXing.length === 1) {
|
||||
const u = noXing.toUpperCase()
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) return u
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function discResolveTwoLetters(data) {
|
||||
let f = discPrimaryLetter(data)
|
||||
let s = ''
|
||||
if (data.secondaryType) {
|
||||
const u = String(data.secondaryType).trim().toUpperCase().charAt(0)
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) s = u
|
||||
}
|
||||
let a0 = ''
|
||||
let b = ''
|
||||
if (data.scores && typeof data.scores === 'object') {
|
||||
const ord = discOrderedFromScores(data.scores)
|
||||
a0 = ord[0] || ''
|
||||
b = ord[1] || ''
|
||||
}
|
||||
if (!a0 && !b && data.percentages && typeof data.percentages === 'object') {
|
||||
const ord = discOrderedFromPercentages(data.percentages)
|
||||
a0 = ord[0] || ''
|
||||
b = ord[1] || ''
|
||||
}
|
||||
if (!f && a0) f = a0
|
||||
if (!s || s === f) {
|
||||
if (b && b !== f) s = b
|
||||
else s = ''
|
||||
}
|
||||
return [f, s]
|
||||
}
|
||||
|
||||
function discNormalizeLegacyDualType(desc) {
|
||||
if (typeof desc !== 'string' || !desc) return ''
|
||||
const t = desc.replace(/\s+/g, '').replace(/\uFF0B/g, '+')
|
||||
let m = t.match(/^([DISC])型\+([DISC])型$/i)
|
||||
if (m) return m[1].toUpperCase() + '+' + m[2].toUpperCase() + '型'
|
||||
m = t.match(/^([DISC])型$/i)
|
||||
if (m) return m[1].toUpperCase() + '型'
|
||||
return ''
|
||||
}
|
||||
|
||||
function discTopTwoLabel(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const [fL, sL] = discResolveTwoLetters(data)
|
||||
if (fL || sL) {
|
||||
if (!fL) return sL ? sL + '型' : ''
|
||||
if (!sL || sL === fL) return fL + '型'
|
||||
return fL + '+' + sL + '型'
|
||||
}
|
||||
return discNormalizeLegacyDualType(data.description && data.description.type)
|
||||
}
|
||||
|
||||
function pdpPrimaryFull(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const desc = data.description && data.description.type
|
||||
if (typeof desc === 'string' && desc) return desc.trim()
|
||||
if (data.dominantType) {
|
||||
const key = String(data.dominantType).trim()
|
||||
return PDP_EN_TO_CN[key] || key
|
||||
}
|
||||
if (data.pdp) return String(data.pdp).trim()
|
||||
return ''
|
||||
}
|
||||
|
||||
function pdpResolveTwoFull(data) {
|
||||
let f = pdpPrimaryFull(data)
|
||||
let s = ''
|
||||
if (data.secondaryType) {
|
||||
const k = String(data.secondaryType).trim()
|
||||
s = PDP_EN_TO_CN[k] || k
|
||||
}
|
||||
let aEn = ''
|
||||
let bEn = ''
|
||||
if (data.scores && typeof data.scores === 'object') {
|
||||
const ord = pdpOrderedFromScores(data.scores)
|
||||
aEn = ord[0] || ''
|
||||
bEn = ord[1] || ''
|
||||
}
|
||||
if (!aEn && !bEn && data.percentages && typeof data.percentages === 'object') {
|
||||
const ord = pdpOrderedFromScores(data.percentages)
|
||||
aEn = ord[0] || ''
|
||||
bEn = ord[1] || ''
|
||||
}
|
||||
if (!f && aEn) f = PDP_EN_TO_CN[aEn] || aEn
|
||||
if (!s || s === f) {
|
||||
if (bEn) {
|
||||
const cand = PDP_EN_TO_CN[bEn] || bEn
|
||||
if (cand !== f) s = cand
|
||||
}
|
||||
}
|
||||
return [f, s]
|
||||
}
|
||||
|
||||
function pdpTopTwoLabel(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const [ff, sf] = pdpResolveTwoFull(data)
|
||||
if (!ff) return sf || ''
|
||||
if (!sf || sf === ff) return ff
|
||||
const short = String(ff).replace(/型$/, '')
|
||||
return short + '+' + sf
|
||||
}
|
||||
|
||||
function formatTestSummary(data, testType) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const t = (testType || '').toLowerCase()
|
||||
@@ -34,8 +186,9 @@ function formatTestSummary(data, testType) {
|
||||
}
|
||||
|
||||
if (t === 'disc') {
|
||||
const two = discTopTwoLabel(data)
|
||||
const desc = data.description && data.description.type
|
||||
const label = (typeof desc === 'string' && desc) ? desc : ((data.dominantType ? data.dominantType + '型' : '') || (data.disc || ''))
|
||||
const label = two || ((typeof desc === 'string' && desc) ? desc : ((data.dominantType ? data.dominantType + '型' : '') || (data.disc || '')))
|
||||
const pct = data.percentages
|
||||
if (pct && typeof pct === 'object') {
|
||||
const d = toIntPercent(pct.D != null ? pct.D : pct.d)
|
||||
@@ -48,8 +201,9 @@ function formatTestSummary(data, testType) {
|
||||
}
|
||||
|
||||
if (t === 'pdp') {
|
||||
const two = pdpTopTwoLabel(data)
|
||||
const desc = data.description && data.description.type
|
||||
const label = (typeof desc === 'string' && desc) ? desc : (data.dominantType || data.pdp || '')
|
||||
const label = two || ((typeof desc === 'string' && desc) ? desc : (data.dominantType || data.pdp || ''))
|
||||
const pct = data.percentages
|
||||
if (pct && typeof pct === 'object') {
|
||||
const names = { Tiger: '老虎', Peacock: '孔雀', Owl: '猫头鹰', Koala: '考拉', Chameleon: '变色龙' }
|
||||
@@ -66,24 +220,28 @@ function formatTestSummary(data, testType) {
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅返回类型标签(无百分比),用于列表、个人中心等
|
||||
*/
|
||||
function getTypeOnly(data, testType) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const t = (testType || '').toLowerCase()
|
||||
if (t === 'mbti') return String(data.mbtiType ?? data.type ?? data.result ?? '')
|
||||
if (t === 'mbti') return String(data.mbtiType != null ? data.mbtiType : (data.type != null ? data.type : (data.result != null ? data.result : '')))
|
||||
if (t === 'disc') {
|
||||
const desc = data.description?.type
|
||||
const two = discTopTwoLabel(data)
|
||||
if (two) return two
|
||||
const desc = data.description && data.description.type
|
||||
if (typeof desc === 'string' && desc) return desc
|
||||
if (data.dominantType) return String(data.dominantType) + '型'
|
||||
return String(data.disc ?? '')
|
||||
return String(data.disc != null ? data.disc : '')
|
||||
}
|
||||
if (t === 'pdp') {
|
||||
const desc = data.description?.type
|
||||
const two = pdpTopTwoLabel(data)
|
||||
if (two) return two
|
||||
const desc = data.description && data.description.type
|
||||
if (typeof desc === 'string' && desc) return desc
|
||||
if (data.dominantType) return String(data.dominantType)
|
||||
return String(data.pdp ?? '')
|
||||
if (data.dominantType) {
|
||||
const k = String(data.dominantType).trim()
|
||||
return PDP_EN_TO_CN[k] || k
|
||||
}
|
||||
return String(data.pdp != null ? data.pdp : '')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
@@ -91,5 +249,7 @@ function getTypeOnly(data, testType) {
|
||||
module.exports = {
|
||||
toIntPercent,
|
||||
formatTestSummary,
|
||||
getTypeOnly
|
||||
getTypeOnly,
|
||||
discTopTwoLabel,
|
||||
pdpTopTwoLabel
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ App({
|
||||
// 超管配置的默认企业 ID(无 scene/eid 等入口参数时回落)
|
||||
defaultEnterpriseId: null,
|
||||
// API基础地址(开发时用本地,生产环境替换为实际域名)
|
||||
apiBase: 'https://mbtiapi.quwanzhi.com',
|
||||
//apiBase: 'http://mbti.com',
|
||||
//apiBase: 'https://mbtiapi.quwanzhi.com',
|
||||
apiBase: 'http://mbti.com',
|
||||
// VIP信息
|
||||
vipInfo: null,
|
||||
// 测试次数
|
||||
|
||||
@@ -3,6 +3,17 @@ const app = getApp()
|
||||
const { getTypeOnly } = require('../../utils/resultFormat')
|
||||
const { request } = require('../../utils/request')
|
||||
|
||||
/** recent 单条:优先用 resultMeta 与结果页一致的「双项」文案 */
|
||||
function summaryFromRecentRecord(rec, testType) {
|
||||
if (!rec) return ''
|
||||
const meta = rec.resultMeta
|
||||
if (meta && typeof meta === 'object') {
|
||||
const t = getTypeOnly(meta, testType)
|
||||
if (t) return t
|
||||
}
|
||||
return String(rec.resultText || '').trim()
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
hasLogin: false,
|
||||
@@ -47,19 +58,18 @@ Page({
|
||||
permPdp: true,
|
||||
permDisc: true,
|
||||
permDistribution: true,
|
||||
/** 最新测试横滑区:仅有已出结果的卡片时才显示,避免禁权或未测评占位 */
|
||||
showLatestTestCards: false,
|
||||
/** 最新测试横滑区:有问卷/面相权限即显示;无记录时卡片灰阶占位,不隐藏 */
|
||||
showLatestTestRow: false,
|
||||
/** 用户卡片下性格标签:在「当前权限下无任何问卷结果」时显示灰色提示 */
|
||||
showEmptyPersonalityTags: true
|
||||
},
|
||||
|
||||
_computeShowLatestTestCards(d) {
|
||||
_computeShowLatestTestRow(d) {
|
||||
const rm = !!(d.reviewMode)
|
||||
if (d.permMbti && d.mbtiType) return true
|
||||
if (d.permPdp && d.pdpType) return true
|
||||
if (d.permDisc && d.discType) return true
|
||||
if (!rm && d.permFace && (d.gallupPreview || d.aiType)) return true
|
||||
return false
|
||||
if (rm) {
|
||||
return !!(d.permMbti || d.permPdp || d.permDisc)
|
||||
}
|
||||
return !!(d.permMbti || d.permPdp || d.permDisc || d.permFace)
|
||||
},
|
||||
|
||||
_computeShowEmptyPersonalityTags(d) {
|
||||
@@ -83,7 +93,7 @@ Page({
|
||||
const d = { ...this.data, ...next }
|
||||
this.setData({
|
||||
...next,
|
||||
showLatestTestCards: this._computeShowLatestTestCards(d),
|
||||
showLatestTestRow: this._computeShowLatestTestRow(d),
|
||||
showEmptyPersonalityTags: this._computeShowEmptyPersonalityTags(d)
|
||||
})
|
||||
},
|
||||
@@ -203,8 +213,7 @@ Page({
|
||||
const { records = {}, totalCount = 0 } = payload.data
|
||||
const r = records
|
||||
|
||||
// DISC resultText 后端已含「型」,type badge 只显示字母,去掉「型」
|
||||
const discType = r.disc ? r.disc.resultText.replace(/型$/, '') : ''
|
||||
const discType = summaryFromRecentRecord(r.disc, 'disc')
|
||||
|
||||
const gallupPreview = (r.ai && r.ai.gallupPreview) ? String(r.ai.gallupPreview) : ''
|
||||
const patch = {
|
||||
@@ -212,7 +221,7 @@ Page({
|
||||
hasResults: !!(r.mbti || r.disc || r.pdp || r.ai),
|
||||
mbtiType: r.mbti ? r.mbti.resultText : '',
|
||||
discType,
|
||||
pdpType: r.pdp ? r.pdp.resultText : '',
|
||||
pdpType: summaryFromRecentRecord(r.pdp, 'pdp'),
|
||||
aiType: r.ai ? r.ai.resultText : '',
|
||||
gallupPreview,
|
||||
mbtiTime: r.mbti ? r.mbti.testTime : '',
|
||||
@@ -227,7 +236,7 @@ Page({
|
||||
const d = { ...this.data, ...patch }
|
||||
this.setData({
|
||||
...patch,
|
||||
showLatestTestCards: this._computeShowLatestTestCards(d),
|
||||
showLatestTestRow: this._computeShowLatestTestRow(d),
|
||||
showEmptyPersonalityTags: this._computeShowEmptyPersonalityTags(d)
|
||||
})
|
||||
},
|
||||
@@ -303,7 +312,7 @@ Page({
|
||||
const d = { ...this.data, ...patch }
|
||||
this.setData({
|
||||
...patch,
|
||||
showLatestTestCards: this._computeShowLatestTestCards(d),
|
||||
showLatestTestRow: this._computeShowLatestTestRow(d),
|
||||
showEmptyPersonalityTags: this._computeShowEmptyPersonalityTags(d)
|
||||
})
|
||||
},
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<text class="tag-text">{{mbtiType}}</text>
|
||||
</view>
|
||||
<view class="tag tag-blue" wx:if="{{discType && permDisc}}">
|
||||
<text class="tag-text">{{discType}}型</text>
|
||||
<text class="tag-text">{{discType}}</text>
|
||||
</view>
|
||||
<view class="tag tag-orange" wx:if="{{pdpType && permPdp}}">
|
||||
<text class="tag-text">{{pdpType}}</text>
|
||||
@@ -79,56 +79,56 @@
|
||||
<text class="depth-header-chevron">›</text>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view wx:if="{{showLatestTestCards}}" scroll-x class="cards-scroll cards-scroll--in-card" enhanced show-scrollbar="{{false}}">
|
||||
<scroll-view wx:if="{{showLatestTestRow}}" scroll-x class="cards-scroll cards-scroll--in-card" enhanced show-scrollbar="{{false}}">
|
||||
<view class="cards-row cards-row--in-card">
|
||||
<view class="result-card card-purple" bindtap="viewMBTI" wx:if="{{permMbti && mbtiType}}">
|
||||
<view class="result-card card-purple {{mbtiType ? '' : 'result-card--placeholder'}}" bindtap="viewMBTI" wx:if="{{permMbti}}">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-purple">
|
||||
<text class="card-icon">🧠</text>
|
||||
</view>
|
||||
<text class="card-label">MBTI性格</text>
|
||||
<text class="card-value">{{mbtiType}}</text>
|
||||
<text class="card-time">{{mbtiTime}}</text>
|
||||
<text class="card-value">{{mbtiType || '未测评'}}</text>
|
||||
<text class="card-time">{{mbtiTime || '—'}}</text>
|
||||
</view>
|
||||
<view class="result-card card-orange" bindtap="viewPDP" wx:if="{{permPdp && pdpType}}">
|
||||
<view class="result-card card-orange {{pdpType ? '' : 'result-card--placeholder'}}" bindtap="viewPDP" wx:if="{{permPdp}}">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-orange">
|
||||
<text class="card-icon">🦁</text>
|
||||
</view>
|
||||
<text class="card-label">PDP行为</text>
|
||||
<text class="card-value">{{pdpType}}</text>
|
||||
<text class="card-time">{{pdpTime}}</text>
|
||||
<text class="card-value">{{pdpType || '未测评'}}</text>
|
||||
<text class="card-time">{{pdpTime || '—'}}</text>
|
||||
</view>
|
||||
<view class="result-card card-blue" bindtap="viewDISC" wx:if="{{permDisc && discType}}">
|
||||
<view class="result-card card-blue {{discType ? '' : 'result-card--placeholder'}}" bindtap="viewDISC" wx:if="{{permDisc}}">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-blue">
|
||||
<text class="card-icon">📊</text>
|
||||
</view>
|
||||
<text class="card-label">DISC测评</text>
|
||||
<text class="card-value">{{discType + '型'}}</text>
|
||||
<text class="card-time">{{discTime}}</text>
|
||||
<text class="card-value">{{discType || '未测评'}}</text>
|
||||
<text class="card-time">{{discTime || '—'}}</text>
|
||||
</view>
|
||||
<view class="result-card card-teal" bindtap="viewGallup" wx:if="{{!reviewMode && permFace && (gallupPreview || aiType)}}">
|
||||
<view class="result-card card-teal {{(gallupPreview || aiType) ? '' : 'result-card--placeholder'}}" bindtap="viewGallup" wx:if="{{!reviewMode && permFace}}">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-teal">
|
||||
<text class="card-icon">⭐</text>
|
||||
</view>
|
||||
<text class="card-label">盖洛普优势</text>
|
||||
<text class="card-value card-value--small">{{gallupPreview || '见面相报告'}}</text>
|
||||
<text class="card-time">{{aiTime}}</text>
|
||||
<text class="card-value card-value--small">{{gallupPreview || (aiType ? '见面相报告' : '未测评')}}</text>
|
||||
<text class="card-time">{{aiTime || '—'}}</text>
|
||||
</view>
|
||||
<view class="result-card card-rose" bindtap="viewAI" wx:if="{{!reviewMode && permFace && aiType}}">
|
||||
<view class="result-card card-rose {{aiType ? '' : 'result-card--placeholder'}}" bindtap="viewAI" wx:if="{{!reviewMode && permFace}}">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-rose">
|
||||
<text class="card-icon">👁️</text>
|
||||
</view>
|
||||
<text class="card-label">面相分析</text>
|
||||
<text class="card-value">{{aiType}}</text>
|
||||
<text class="card-time">{{aiTime}}</text>
|
||||
<text class="card-value">{{aiType || '未测评'}}</text>
|
||||
<text class="card-time">{{aiTime || '—'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view wx:if="{{showLatestTestCards}}" class="depth-empty-hint depth-empty-hint--compact">
|
||||
<view wx:if="{{showLatestTestRow}}" class="depth-empty-hint depth-empty-hint--compact">
|
||||
<text>点击卡片查看详情;右上方可查看全部测试记录。</text>
|
||||
</view>
|
||||
<view class="depth-inner-divider"></view>
|
||||
|
||||
@@ -467,6 +467,15 @@ custom-tab-bar {
|
||||
letter-spacing: -1rpx;
|
||||
}
|
||||
|
||||
/* 与 MBTI 同字号;长文案(如 PDP/DISC 双项)最多两行 */
|
||||
.result-card .card-value {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.card-purple .card-value { color: #7C3AED; }
|
||||
.card-blue .card-value { color: #2563EB; }
|
||||
.card-orange .card-value { color: #D97706; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// pages/result/disc.js - DISC结果页(支持付费墙 + 历史详情拉取)
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
const { getTypeOnly } = require('../../utils/resultFormat')
|
||||
|
||||
function toIntPercent(v) {
|
||||
if (v == null) return 0
|
||||
@@ -24,6 +25,8 @@ function withPercentagesInt(data) {
|
||||
Page({
|
||||
data: {
|
||||
result: null,
|
||||
/** 主+次高权重展示(如 S+I型),与接口摘要一致 */
|
||||
typeSummaryLine: '',
|
||||
typeList: [
|
||||
{ type: 'D', label: 'D型 - 支配型', colorClass: 'fill-d' },
|
||||
{ type: 'I', label: 'I型 - 影响型', colorClass: 'fill-i' },
|
||||
@@ -45,7 +48,8 @@ Page({
|
||||
}
|
||||
const result = wx.getStorageSync('discResult')
|
||||
if (result) {
|
||||
this.setData({ result: withPercentagesInt(result) })
|
||||
const r = withPercentagesInt(result)
|
||||
this.setData({ result: r, typeSummaryLine: getTypeOnly(result, 'disc') })
|
||||
this.initPayInfoFromRuntime('disc')
|
||||
} else {
|
||||
wx.showToast({ title: '暂无测试结果', icon: 'none' })
|
||||
@@ -71,7 +75,11 @@ Page({
|
||||
const paidAmount = payload.paidAmount != null ? Number(payload.paidAmount) : 0
|
||||
const amountYuan = payload.amountYuan != null ? Number(payload.amountYuan) : (paidAmount > 0 ? paidAmount / 100 : 0)
|
||||
const needPaymentToUnlock = payload.needPaymentToUnlock === true || (!!payload.requiresPayment && !isPaid && paidAmount > 0)
|
||||
this.setData({ result: withPercentagesInt(data) })
|
||||
const r = withPercentagesInt(data)
|
||||
this.setData({
|
||||
result: r,
|
||||
typeSummaryLine: getTypeOnly(data, 'disc')
|
||||
})
|
||||
const payInfo = {
|
||||
requiresPayment: needPaymentToUnlock,
|
||||
isPaid,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<view class="type-card">
|
||||
<view class="type-header">
|
||||
<text class="type-label">您的DISC性格类型</text>
|
||||
<text class="type-value">{{result.dominantType}}{{result.secondaryType ? ' + ' + result.secondaryType : ''}}</text>
|
||||
<text class="type-value">{{typeSummaryLine}}</text>
|
||||
<text class="type-description" wx:if="{{result.description && result.description.description}}">{{result.description.description}}</text>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// pages/result/pdp.js - PDP结果页(支持付费墙 + 历史详情拉取)
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
const { getTypeOnly } = require('../../utils/resultFormat')
|
||||
|
||||
const PDP_KEYS = ['Tiger', 'Peacock', 'Koala', 'Owl', 'Chameleon']
|
||||
|
||||
@@ -23,6 +24,7 @@ function withPercentagesInt(data) {
|
||||
Page({
|
||||
data: {
|
||||
result: null,
|
||||
typeSummaryLine: '',
|
||||
typeList: [
|
||||
{ type: 'Tiger', emoji: '🐅', label: '老虎型', colorClass: 'fill-tiger' },
|
||||
{ type: 'Peacock', emoji: '🦚', label: '孔雀型', colorClass: 'fill-peacock' },
|
||||
@@ -45,7 +47,10 @@ Page({
|
||||
}
|
||||
const result = wx.getStorageSync('pdpResult')
|
||||
if (result) {
|
||||
this.setData({ result: withPercentagesInt(result) })
|
||||
this.setData({
|
||||
result: withPercentagesInt(result),
|
||||
typeSummaryLine: getTypeOnly(result, 'pdp')
|
||||
})
|
||||
this.initPayInfoFromRuntime('pdp')
|
||||
} else {
|
||||
wx.showToast({ title: '暂无测试结果', icon: 'none' })
|
||||
@@ -71,7 +76,10 @@ Page({
|
||||
const paidAmount = payload.paidAmount != null ? Number(payload.paidAmount) : 0
|
||||
const amountYuan = payload.amountYuan != null ? Number(payload.amountYuan) : (paidAmount > 0 ? paidAmount / 100 : 0)
|
||||
const needPaymentToUnlock = payload.needPaymentToUnlock === true || (!!payload.requiresPayment && !isPaid && paidAmount > 0)
|
||||
this.setData({ result: withPercentagesInt(data) })
|
||||
this.setData({
|
||||
result: withPercentagesInt(data),
|
||||
typeSummaryLine: getTypeOnly(data, 'pdp')
|
||||
})
|
||||
const payInfo = {
|
||||
requiresPayment: needPaymentToUnlock,
|
||||
isPaid,
|
||||
@@ -138,19 +146,19 @@ Page({
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const result = this.data.result
|
||||
const line = this.data.typeSummaryLine || this.data.result?.description?.type || ''
|
||||
const { getSharePathByScope } = require('../../utils/share')
|
||||
return {
|
||||
title: `我的PDP类型是${result?.description?.type}${result?.description?.emoji},来测测你的吧!`,
|
||||
title: `我的PDP类型是${line},来测测你的吧!`,
|
||||
path: getSharePathByScope('/pages/index/index')
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const result = this.data.result
|
||||
const line = this.data.typeSummaryLine || this.data.result?.description?.type || ''
|
||||
const { buildShareQuery } = require('../../utils/share')
|
||||
return {
|
||||
title: `我的PDP类型是${result?.description?.type}${result?.description?.emoji},来测测你的吧!`,
|
||||
title: `我的PDP类型是${line},来测测你的吧!`,
|
||||
query: buildShareQuery()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<view class="type-card">
|
||||
<view class="type-header">
|
||||
<text class="type-label">PDP性格类型</text>
|
||||
<text class="type-value">{{result.description.type || result.dominantType}}{{result.description.emoji || ''}}</text>
|
||||
<text class="type-value">{{typeSummaryLine}}</text>
|
||||
<text class="type-title" wx:if="{{result.description.title}}">{{result.description.title}}</text>
|
||||
<text class="type-description" wx:if="{{result.description.description}}">{{result.description.description}}</text>
|
||||
</view>
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
// pages/test/disc.js
|
||||
const { discQuestions, shuffleQuestions } = require('../../utils/questions')
|
||||
const { loadQuestions } = require('../../utils/questionBank')
|
||||
const { discDescriptions } = require('../../utils/descriptions')
|
||||
const app = getApp()
|
||||
|
||||
const DISC_TIME_SEC = 15 * 60
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: true,
|
||||
questions: [],
|
||||
currentIndex: 0,
|
||||
currentQuestion: null,
|
||||
answers: {},
|
||||
selectedAnswer: null,
|
||||
total: discQuestions.length,
|
||||
total: 0,
|
||||
answeredCount: 0,
|
||||
progress: 0,
|
||||
timeRemaining: 15 * 60,
|
||||
timeRemaining: DISC_TIME_SEC,
|
||||
_initialSeconds: DISC_TIME_SEC,
|
||||
formatTime: '15:00',
|
||||
isSubmitting: false
|
||||
},
|
||||
@@ -21,10 +25,31 @@ Page({
|
||||
timer: null,
|
||||
|
||||
onLoad() {
|
||||
const questions = shuffleQuestions(discQuestions)
|
||||
this.setData({ questions, currentQuestion: questions[0] })
|
||||
try { require('../../utils/analytics').track('test_start', { type: 'disc', total: questions.length }) } catch (e) {}
|
||||
this.startTimer()
|
||||
loadQuestions('disc', {})
|
||||
.then((questions) => {
|
||||
if (!questions.length) {
|
||||
wx.showToast({ title: '暂无题目', icon: 'none' })
|
||||
this.setData({ loading: false })
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
loading: false,
|
||||
questions,
|
||||
currentQuestion: questions[0],
|
||||
total: questions.length,
|
||||
timeRemaining: DISC_TIME_SEC,
|
||||
_initialSeconds: DISC_TIME_SEC,
|
||||
formatTime: '15:00'
|
||||
})
|
||||
try {
|
||||
require('../../utils/analytics').track('test_start', { type: 'disc', total: questions.length })
|
||||
} catch (e) {}
|
||||
this.startTimer()
|
||||
})
|
||||
.catch((err) => {
|
||||
this.setData({ loading: false })
|
||||
wx.showToast({ title: (err && err.message) || '加载失败', icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
@@ -148,7 +173,7 @@ Page({
|
||||
dominantType,
|
||||
secondaryType,
|
||||
description: discDescriptions[dominantType],
|
||||
testDuration: 15 * 60 - this.data.timeRemaining,
|
||||
testDuration: (this.data._initialSeconds || DISC_TIME_SEC) - this.data.timeRemaining,
|
||||
completedAt: new Date().toISOString(),
|
||||
// 便于后端留存完整答题过程
|
||||
answers: this.data.answers
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<!--pages/test/disc.wxml - DISC测试页面(按旧版模板重构)-->
|
||||
<view class="test-page">
|
||||
<view wx:if="{{loading}}" class="test-loading">
|
||||
<text class="test-loading-text">加载题目…</text>
|
||||
</view>
|
||||
<block wx:elif="{{currentQuestion}}">
|
||||
<view class="progress-section">
|
||||
<view class="progress-info">
|
||||
<text class="question-count">问题 {{currentIndex + 1}}/{{total}}</text>
|
||||
@@ -45,4 +49,5 @@
|
||||
<text class="submit-text">{{isSubmitting ? '计算中...' : '完成测试,查看结果'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -7,6 +7,19 @@
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.test-loading {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx;
|
||||
}
|
||||
|
||||
.test-loading-text {
|
||||
font-size: 30rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
// pages/test/mbti.js - MBTI测试页面逻辑
|
||||
const { mbtiQuestions, shuffleQuestions } = require('../../utils/questions')
|
||||
const { loadQuestions } = require('../../utils/questionBank')
|
||||
const { mbtiDescriptions } = require('../../utils/descriptions')
|
||||
const payment = require('../../utils/payment')
|
||||
const app = getApp()
|
||||
|
||||
const MBTI_TIME_SEC = 30 * 60 // 30 分钟
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: true,
|
||||
questions: [],
|
||||
currentIndex: 0,
|
||||
currentQuestion: null,
|
||||
answers: {},
|
||||
selectedAnswer: null,
|
||||
total: mbtiQuestions.length,
|
||||
total: 0,
|
||||
answeredCount: 0,
|
||||
progress: 0,
|
||||
timeRemaining: 30 * 60, // 30分钟
|
||||
timeRemaining: MBTI_TIME_SEC,
|
||||
_initialSeconds: MBTI_TIME_SEC,
|
||||
formatTime: '30:00',
|
||||
isSubmitting: false,
|
||||
canAccess: false
|
||||
@@ -23,17 +27,34 @@ Page({
|
||||
timer: null,
|
||||
|
||||
onLoad() {
|
||||
const questions = shuffleQuestions(mbtiQuestions)
|
||||
const total = questions.length
|
||||
this.setData({
|
||||
questions,
|
||||
currentQuestion: questions[0],
|
||||
canAccess: true,
|
||||
total,
|
||||
progress: total ? Math.round((1 / total) * 100) : 0
|
||||
})
|
||||
try { require('../../utils/analytics').track('test_start', { type: 'mbti', total }) } catch (e) {}
|
||||
this.startTimer()
|
||||
loadQuestions('mbti', {})
|
||||
.then((questions) => {
|
||||
const total = questions.length
|
||||
if (!total) {
|
||||
wx.showToast({ title: '暂无题目', icon: 'none' })
|
||||
this.setData({ loading: false })
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
loading: false,
|
||||
questions,
|
||||
currentQuestion: questions[0],
|
||||
canAccess: true,
|
||||
total,
|
||||
progress: Math.round((1 / total) * 100),
|
||||
timeRemaining: MBTI_TIME_SEC,
|
||||
_initialSeconds: MBTI_TIME_SEC,
|
||||
formatTime: '30:00'
|
||||
})
|
||||
try {
|
||||
require('../../utils/analytics').track('test_start', { type: 'mbti', total })
|
||||
} catch (e) {}
|
||||
this.startTimer()
|
||||
})
|
||||
.catch((err) => {
|
||||
this.setData({ loading: false })
|
||||
wx.showToast({ title: (err && err.message) || '加载失败', icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
// 检查访问权限
|
||||
@@ -197,7 +218,7 @@ Page({
|
||||
const resultData = {
|
||||
...result,
|
||||
answers: this.data.answers,
|
||||
testDuration: 30 * 60 - this.data.timeRemaining,
|
||||
testDuration: (this.data._initialSeconds || MBTI_TIME_SEC) - this.data.timeRemaining,
|
||||
completedAt: new Date().toISOString(),
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<!--pages/test/mbti.wxml - MBTI测试页面(按旧版模板重构)-->
|
||||
<view class="test-page">
|
||||
<view wx:if="{{loading}}" class="test-loading">
|
||||
<text class="test-loading-text">加载题目…</text>
|
||||
</view>
|
||||
<block wx:elif="{{currentQuestion}}">
|
||||
<view class="progress-section">
|
||||
<view class="progress-info">
|
||||
<text class="question-count">问题 {{currentIndex + 1}}/{{total}}</text>
|
||||
@@ -45,4 +49,5 @@
|
||||
<text class="button-text button-text-on-primary">{{isSubmitting ? '正在生成…' : '查看结果'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -7,6 +7,19 @@
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.test-loading {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx;
|
||||
}
|
||||
|
||||
.test-loading-text {
|
||||
font-size: 30rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
// pages/test/pdp.js
|
||||
const { pdpQuestions, shuffleQuestions } = require('../../utils/questions')
|
||||
const { loadQuestions } = require('../../utils/questionBank')
|
||||
const { pdpDescriptions } = require('../../utils/descriptions')
|
||||
const app = getApp()
|
||||
|
||||
const PDP_TIME_SEC = 15 * 60
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: true,
|
||||
questions: [],
|
||||
currentIndex: 0,
|
||||
currentQuestion: null,
|
||||
answers: {},
|
||||
selectedAnswer: null,
|
||||
total: pdpQuestions.length,
|
||||
total: 0,
|
||||
answeredCount: 0,
|
||||
progress: 0,
|
||||
timeRemaining: 15 * 60,
|
||||
timeRemaining: PDP_TIME_SEC,
|
||||
_initialSeconds: PDP_TIME_SEC,
|
||||
formatTime: '15:00',
|
||||
isSubmitting: false
|
||||
},
|
||||
@@ -21,10 +25,31 @@ Page({
|
||||
timer: null,
|
||||
|
||||
onLoad() {
|
||||
const questions = shuffleQuestions(pdpQuestions)
|
||||
this.setData({ questions, currentQuestion: questions[0] })
|
||||
try { require('../../utils/analytics').track('test_start', { type: 'pdp', total: questions.length }) } catch (e) {}
|
||||
this.startTimer()
|
||||
loadQuestions('pdp', {})
|
||||
.then((questions) => {
|
||||
if (!questions.length) {
|
||||
wx.showToast({ title: '暂无题目', icon: 'none' })
|
||||
this.setData({ loading: false })
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
loading: false,
|
||||
questions,
|
||||
currentQuestion: questions[0],
|
||||
total: questions.length,
|
||||
timeRemaining: PDP_TIME_SEC,
|
||||
_initialSeconds: PDP_TIME_SEC,
|
||||
formatTime: '15:00'
|
||||
})
|
||||
try {
|
||||
require('../../utils/analytics').track('test_start', { type: 'pdp', total: questions.length })
|
||||
} catch (e) {}
|
||||
this.startTimer()
|
||||
})
|
||||
.catch((err) => {
|
||||
this.setData({ loading: false })
|
||||
wx.showToast({ title: (err && err.message) || '加载失败', icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
@@ -151,7 +176,7 @@ Page({
|
||||
dominantType,
|
||||
secondaryType,
|
||||
description: pdpDescriptions[dominantType],
|
||||
testDuration: 15 * 60 - this.data.timeRemaining,
|
||||
testDuration: (this.data._initialSeconds || PDP_TIME_SEC) - this.data.timeRemaining,
|
||||
completedAt: new Date().toISOString(),
|
||||
// 便于后端留存完整答题过程
|
||||
answers: this.data.answers
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<!--pages/test/pdp.wxml - PDP测试页面(按旧版模板重构)-->
|
||||
<view class="test-page">
|
||||
<view wx:if="{{loading}}" class="test-loading">
|
||||
<text class="test-loading-text">加载题目…</text>
|
||||
</view>
|
||||
<block wx:elif="{{currentQuestion}}">
|
||||
<view class="progress-section">
|
||||
<view class="progress-info">
|
||||
<text class="question-count">问题 {{currentIndex + 1}}/{{total}}</text>
|
||||
@@ -45,4 +49,5 @@
|
||||
<text class="submit-text">{{isSubmitting ? '计算中...' : '完成测试,查看结果'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -7,6 +7,19 @@
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.test-loading {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx;
|
||||
}
|
||||
|
||||
.test-loading-text {
|
||||
font-size: 30rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* 从服务端拉取启用题库,失败或为空时回落本地 questions.js;顺序由 shuffleQuestions 随机。
|
||||
* 仅从服务端拉取启用题库(/api/test/questions),无本地题目保底。
|
||||
* enterpriseId:未传时与 test/submit 一致,见 enterpriseContext.getEnterpriseIdForApiPayload()
|
||||
*/
|
||||
const { requestPromise } = require('./request')
|
||||
const { shuffleQuestions } = require('./questions')
|
||||
|
||||
function getAppSafe() {
|
||||
try {
|
||||
@@ -12,7 +12,45 @@ function getAppSafe() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 与 runtime 一致:>0 时在乱序后截取前 N 题 */
|
||||
/**
|
||||
* Fisher-Yates:打乱题目顺序,并随机每题选项顺序(深拷贝)
|
||||
* @param {Array} questions
|
||||
* @returns {Array}
|
||||
*/
|
||||
function shuffleQuestions(questions) {
|
||||
const arr = (questions || []).map(q => ({
|
||||
...q,
|
||||
options: (q.options || []).slice().sort(() => Math.random() - 0.5)
|
||||
}))
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1))
|
||||
;[arr[i], arr[j]] = [arr[j], arr[i]]
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ enterpriseId?: number|null }} opts
|
||||
* @returns {number|null}
|
||||
*/
|
||||
function resolveEnterpriseIdForQuestionBank(opts) {
|
||||
const o = opts || {}
|
||||
if (Object.prototype.hasOwnProperty.call(o, 'enterpriseId')) {
|
||||
const v = o.enterpriseId
|
||||
if (v == null || v === '') {
|
||||
return null
|
||||
}
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) && n > 0 ? n : null
|
||||
}
|
||||
try {
|
||||
const { getEnterpriseIdForApiPayload } = require('./enterpriseContext')
|
||||
return getEnterpriseIdForApiPayload()
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getTestQuestionDrawCount() {
|
||||
const app = getAppSafe()
|
||||
const n = app && app.globalData && app.globalData.testQuestionDrawCount
|
||||
@@ -60,23 +98,21 @@ function fetchQuestionBank(type, enterpriseId) {
|
||||
|
||||
/**
|
||||
* @param {'mbti'|'disc'|'pdp'} type
|
||||
* @param {Array} localQuestions
|
||||
* @param {{ enterpriseId?: number|null }} opts
|
||||
* @returns {Promise<Array>}
|
||||
* @returns {Promise<Array>} 乱序后的题目;接口无题或失败则 reject
|
||||
*/
|
||||
function loadQuestionsWithFallback(type, localQuestions, opts = {}) {
|
||||
const { enterpriseId } = opts
|
||||
return fetchQuestionBank(type, enterpriseId)
|
||||
.then((list) => {
|
||||
if (!list.length) {
|
||||
return applyDrawCountAfterShuffle(shuffleQuestions(localQuestions))
|
||||
}
|
||||
return shuffleQuestions(list)
|
||||
})
|
||||
.catch(() => applyDrawCountAfterShuffle(shuffleQuestions(localQuestions)))
|
||||
function loadQuestions(type, opts = {}) {
|
||||
const enterpriseId = resolveEnterpriseIdForQuestionBank(opts)
|
||||
return fetchQuestionBank(type, enterpriseId).then((list) => {
|
||||
if (!list.length) {
|
||||
throw new Error('暂无启用题目')
|
||||
}
|
||||
return applyDrawCountAfterShuffle(shuffleQuestions(list))
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchQuestionBank,
|
||||
loadQuestionsWithFallback
|
||||
loadQuestions,
|
||||
shuffleQuestions
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
// utils/questions.js - 测试题库
|
||||
|
||||
// MBTI测试题(精简版30题,每个维度约7-8题)
|
||||
const mbtiQuestions = [
|
||||
// E vs I 维度 (8题)
|
||||
{ id: 1, question: "在社交场合中,您通常会:", dimension: "EI", options: [{ value: "E", text: "认识新朋友,扩大社交圈" }, { value: "I", text: "与已认识的朋友交流" }] },
|
||||
{ id: 2, question: "您更喜欢:", dimension: "EI", options: [{ value: "E", text: "在团队中工作" }, { value: "I", text: "独立工作" }] },
|
||||
{ id: 3, question: "当您需要充电时,您会选择:", dimension: "EI", options: [{ value: "E", text: "与朋友聚会" }, { value: "I", text: "独处休息" }] },
|
||||
{ id: 4, question: "在会议中,您通常:", dimension: "EI", options: [{ value: "E", text: "积极发言" }, { value: "I", text: "先思考再发表" }] },
|
||||
{ id: 5, question: "您更倾向于:", dimension: "EI", options: [{ value: "E", text: "认识很多人" }, { value: "I", text: "与少数人深交" }] },
|
||||
{ id: 6, question: "面对问题时,您更倾向于:", dimension: "EI", options: [{ value: "E", text: "与他人讨论" }, { value: "I", text: "独自思考" }] },
|
||||
{ id: 7, question: "您更喜欢的休闲活动是:", dimension: "EI", options: [{ value: "E", text: "团体活动" }, { value: "I", text: "个人爱好" }] },
|
||||
{ id: 8, question: "在陌生环境中,您通常会:", dimension: "EI", options: [{ value: "E", text: "主动与人交谈" }, { value: "I", text: "观察周围环境" }] },
|
||||
|
||||
// S vs N 维度 (7题)
|
||||
{ id: 9, question: "您更关注:", dimension: "SN", options: [{ value: "S", text: "具体的细节和事实" }, { value: "N", text: "整体概念和可能性" }] },
|
||||
{ id: 10, question: "您更信任:", dimension: "SN", options: [{ value: "S", text: "实际经验" }, { value: "N", text: "直觉和灵感" }] },
|
||||
{ id: 11, question: "您更喜欢:", dimension: "SN", options: [{ value: "S", text: "按步骤执行" }, { value: "N", text: "创造性解决" }] },
|
||||
{ id: 12, question: "学习新事物时,您更喜欢:", dimension: "SN", options: [{ value: "S", text: "实际操作" }, { value: "N", text: "理论学习" }] },
|
||||
{ id: 13, question: "您更喜欢的工作类型是:", dimension: "SN", options: [{ value: "S", text: "明确的任务" }, { value: "N", text: "创新的项目" }] },
|
||||
{ id: 14, question: "您描述事物时更倾向于:", dimension: "SN", options: [{ value: "S", text: "具体描述" }, { value: "N", text: "使用比喻" }] },
|
||||
{ id: 15, question: "您更关心:", dimension: "SN", options: [{ value: "S", text: "当下的现实" }, { value: "N", text: "未来的可能" }] },
|
||||
|
||||
// T vs F 维度 (8题)
|
||||
{ id: 16, question: "做决定时,您更依赖:", dimension: "TF", options: [{ value: "T", text: "逻辑分析" }, { value: "F", text: "个人价值" }] },
|
||||
{ id: 17, question: "在争论中,您更看重:", dimension: "TF", options: [{ value: "T", text: "事实真相" }, { value: "F", text: "和谐关系" }] },
|
||||
{ id: 18, question: "给予反馈时,您更注重:", dimension: "TF", options: [{ value: "T", text: "直接指出问题" }, { value: "F", text: "考虑对方感受" }] },
|
||||
{ id: 19, question: "评估方案时,您更关注:", dimension: "TF", options: [{ value: "T", text: "效率和结果" }, { value: "F", text: "对人的影响" }] },
|
||||
{ id: 20, question: "当朋友遇到困难时,您更倾向于:", dimension: "TF", options: [{ value: "T", text: "分析问题提供建议" }, { value: "F", text: "倾听并给予支持" }] },
|
||||
{ id: 21, question: "您更欣赏的品质是:", dimension: "TF", options: [{ value: "T", text: "理性客观" }, { value: "F", text: "善解人意" }] },
|
||||
{ id: 22, question: "您认为好的决定应该:", dimension: "TF", options: [{ value: "T", text: "基于客观分析" }, { value: "F", text: "考虑各方感受" }] },
|
||||
{ id: 23, question: "当与他人意见不同时,您更倾向于:", dimension: "TF", options: [{ value: "T", text: "坚持正确观点" }, { value: "F", text: "寻求共识" }] },
|
||||
|
||||
// J vs P 维度 (7题)
|
||||
{ id: 24, question: "您更喜欢的工作方式是:", dimension: "JP", options: [{ value: "J", text: "有计划地进行" }, { value: "P", text: "随机应变" }] },
|
||||
{ id: 25, question: "对于截止日期,您通常:", dimension: "JP", options: [{ value: "J", text: "提前完成" }, { value: "P", text: "在最后完成" }] },
|
||||
{ id: 26, question: "您的生活方式更倾向于:", dimension: "JP", options: [{ value: "J", text: "有条理有规律" }, { value: "P", text: "灵活随意" }] },
|
||||
{ id: 27, question: "面对选择时,您更倾向于:", dimension: "JP", options: [{ value: "J", text: "快速做出决定" }, { value: "P", text: "保持开放选项" }] },
|
||||
{ id: 28, question: "您的桌面通常是:", dimension: "JP", options: [{ value: "J", text: "整洁有序" }, { value: "P", text: "创意性混乱" }] },
|
||||
{ id: 29, question: "计划改变时,您的反应是:", dimension: "JP", options: [{ value: "J", text: "感到不安" }, { value: "P", text: "觉得有趣" }] },
|
||||
{ id: 30, question: "您更喜欢:", dimension: "JP", options: [{ value: "J", text: "事先规划" }, { value: "P", text: "即兴发挥" }] }
|
||||
]
|
||||
|
||||
// DISC测试题 (20题)
|
||||
const discQuestions = [
|
||||
{ id: 1, question: "在团队中,您更倾向于:", options: [{ value: "D", text: "主导决策,带领团队" }, { value: "I", text: "活跃气氛,激励成员" }, { value: "S", text: "支持他人,确保和谐" }, { value: "C", text: "分析数据,确保质量" }] },
|
||||
{ id: 2, question: "面对挑战时,您的第一反应是:", options: [{ value: "D", text: "立即行动" }, { value: "I", text: "寻找支持" }, { value: "S", text: "冷静思考" }, { value: "C", text: "收集信息" }] },
|
||||
{ id: 3, question: "您在工作中最看重的是:", options: [{ value: "D", text: "成果和效率" }, { value: "I", text: "认可和赞赏" }, { value: "S", text: "稳定和安全" }, { value: "C", text: "准确和质量" }] },
|
||||
{ id: 4, question: "与他人沟通时,您通常:", options: [{ value: "D", text: "直接了当" }, { value: "I", text: "热情友好" }, { value: "S", text: "耐心倾听" }, { value: "C", text: "逻辑清晰" }] },
|
||||
{ id: 5, question: "压力之下,您会:", options: [{ value: "D", text: "更加强势" }, { value: "I", text: "寻求鼓励" }, { value: "S", text: "保持冷静" }, { value: "C", text: "更加谨慎" }] },
|
||||
{ id: 6, question: "您认为自己的优势是:", options: [{ value: "D", text: "决断力强" }, { value: "I", text: "人际关系好" }, { value: "S", text: "可靠稳定" }, { value: "C", text: "分析能力强" }] },
|
||||
{ id: 7, question: "在会议中,您通常扮演:", options: [{ value: "D", text: "主导者" }, { value: "I", text: "激励者" }, { value: "S", text: "调和者" }, { value: "C", text: "分析者" }] },
|
||||
{ id: 8, question: "您最不喜欢的工作环境是:", options: [{ value: "D", text: "进展缓慢" }, { value: "I", text: "被孤立" }, { value: "S", text: "变化太快" }, { value: "C", text: "混乱无序" }] },
|
||||
{ id: 9, question: "做决定时,您更依赖:", options: [{ value: "D", text: "直觉经验" }, { value: "I", text: "他人意见" }, { value: "S", text: "过去经验" }, { value: "C", text: "数据事实" }] },
|
||||
{ id: 10, question: "您的工作风格是:", options: [{ value: "D", text: "快速高效" }, { value: "I", text: "灵活多变" }, { value: "S", text: "稳定持续" }, { value: "C", text: "严谨细致" }] },
|
||||
{ id: 11, question: "遇到冲突时,您会:", options: [{ value: "D", text: "直面解决" }, { value: "I", text: "调解双方" }, { value: "S", text: "避免冲突" }, { value: "C", text: "分析原因" }] },
|
||||
{ id: 12, question: "您期望的领导风格是:", options: [{ value: "D", text: "给予挑战" }, { value: "I", text: "认可表扬" }, { value: "S", text: "稳定支持" }, { value: "C", text: "明确指导" }] },
|
||||
{ id: 13, question: "处理任务时,您更注重:", options: [{ value: "D", text: "速度效率" }, { value: "I", text: "创意新颖" }, { value: "S", text: "过程协作" }, { value: "C", text: "质量准确" }] },
|
||||
{ id: 14, question: "您的社交方式是:", options: [{ value: "D", text: "目的明确" }, { value: "I", text: "广泛社交" }, { value: "S", text: "深度交往" }, { value: "C", text: "选择性社交" }] },
|
||||
{ id: 15, question: "您理想的工作节奏是:", options: [{ value: "D", text: "快节奏" }, { value: "I", text: "灵活多变" }, { value: "S", text: "稳定有序" }, { value: "C", text: "有条理" }] },
|
||||
{ id: 16, question: "面对变化,您的态度是:", options: [{ value: "D", text: "主动拥抱" }, { value: "I", text: "积极适应" }, { value: "S", text: "需要时间" }, { value: "C", text: "谨慎评估" }] },
|
||||
{ id: 17, question: "您的时间管理风格是:", options: [{ value: "D", text: "高效利用" }, { value: "I", text: "灵活安排" }, { value: "S", text: "按部就班" }, { value: "C", text: "精确规划" }] },
|
||||
{ id: 18, question: "激励您的是:", options: [{ value: "D", text: "成就控制" }, { value: "I", text: "认可社交" }, { value: "S", text: "稳定归属" }, { value: "C", text: "正确标准" }] },
|
||||
{ id: 19, question: "您处理细节的方式是:", options: [{ value: "D", text: "关注大局" }, { value: "I", text: "可能忽略" }, { value: "S", text: "认真对待" }, { value: "C", text: "极度重视" }] },
|
||||
{ id: 20, question: "您对规则的态度是:", options: [{ value: "D", text: "灵活打破" }, { value: "I", text: "灵活运用" }, { value: "S", text: "遵守维护" }, { value: "C", text: "严格遵守" }] }
|
||||
]
|
||||
|
||||
// PDP测试题 (20题)
|
||||
const pdpQuestions = [
|
||||
{ id: 1, question: "面对紧急任务,您的第一反应是:", options: [{ value: "Tiger", text: "立即行动" }, { value: "Peacock", text: "召集团队" }, { value: "Koala", text: "冷静分析" }, { value: "Owl", text: "仔细规划" }, { value: "Chameleon", text: "灵活应对" }] },
|
||||
{ id: 2, question: "在社交场合,您通常会:", options: [{ value: "Tiger", text: "主导话题" }, { value: "Peacock", text: "活跃气氛" }, { value: "Koala", text: "安静倾听" }, { value: "Owl", text: "观察分析" }, { value: "Chameleon", text: "根据对象调整" }] },
|
||||
{ id: 3, question: "您最看重工作中的:", options: [{ value: "Tiger", text: "权力和成就" }, { value: "Peacock", text: "认可和赞赏" }, { value: "Koala", text: "稳定和和谐" }, { value: "Owl", text: "准确和质量" }, { value: "Chameleon", text: "平衡和适应" }] },
|
||||
{ id: 4, question: "处理冲突时,您倾向于:", options: [{ value: "Tiger", text: "直接解决" }, { value: "Peacock", text: "调解双方" }, { value: "Koala", text: "避免冲突" }, { value: "Owl", text: "理性处理" }, { value: "Chameleon", text: "视情况而定" }] },
|
||||
{ id: 5, question: "您的决策风格是:", options: [{ value: "Tiger", text: "果断迅速" }, { value: "Peacock", text: "直觉判断" }, { value: "Koala", text: "深思熟虑" }, { value: "Owl", text: "数据分析" }, { value: "Chameleon", text: "灵活决策" }] },
|
||||
{ id: 6, question: "面对压力,您会:", options: [{ value: "Tiger", text: "更加强势" }, { value: "Peacock", text: "寻求支持" }, { value: "Koala", text: "保持冷静" }, { value: "Owl", text: "更加谨慎" }, { value: "Chameleon", text: "调整策略" }] },
|
||||
{ id: 7, question: "您的领导风格是:", options: [{ value: "Tiger", text: "指挥型" }, { value: "Peacock", text: "激励型" }, { value: "Koala", text: "支持型" }, { value: "Owl", text: "专家型" }, { value: "Chameleon", text: "教练型" }] },
|
||||
{ id: 8, question: "您处理细节的方式是:", options: [{ value: "Tiger", text: "关注大局" }, { value: "Peacock", text: "可能忽略" }, { value: "Koala", text: "认真对待" }, { value: "Owl", text: "极度重视" }, { value: "Chameleon", text: "视情况决定" }] },
|
||||
{ id: 9, question: "您的沟通方式是:", options: [{ value: "Tiger", text: "直接简短" }, { value: "Peacock", text: "热情生动" }, { value: "Koala", text: "温和耐心" }, { value: "Owl", text: "逻辑清晰" }, { value: "Chameleon", text: "根据对象调整" }] },
|
||||
{ id: 10, question: "您对变化的态度是:", options: [{ value: "Tiger", text: "主动推动" }, { value: "Peacock", text: "积极拥抱" }, { value: "Koala", text: "需要适应" }, { value: "Owl", text: "谨慎评估" }, { value: "Chameleon", text: "随机应变" }] },
|
||||
{ id: 11, question: "您的时间管理风格是:", options: [{ value: "Tiger", text: "追求速度" }, { value: "Peacock", text: "灵活安排" }, { value: "Koala", text: "稳定执行" }, { value: "Owl", text: "精确规划" }, { value: "Chameleon", text: "根据情况调整" }] },
|
||||
{ id: 12, question: "您被什么激励:", options: [{ value: "Tiger", text: "成就权力" }, { value: "Peacock", text: "认可赞赏" }, { value: "Koala", text: "安全归属" }, { value: "Owl", text: "正确标准" }, { value: "Chameleon", text: "多样平衡" }] },
|
||||
{ id: 13, question: "您的学习方式是:", options: [{ value: "Tiger", text: "边做边学" }, { value: "Peacock", text: "互动讨论" }, { value: "Koala", text: "循序渐进" }, { value: "Owl", text: "深入研究" }, { value: "Chameleon", text: "多种结合" }] },
|
||||
{ id: 14, question: "您对规则的态度是:", options: [{ value: "Tiger", text: "灵活打破" }, { value: "Peacock", text: "不拘一格" }, { value: "Koala", text: "遵守维护" }, { value: "Owl", text: "严格遵守" }, { value: "Chameleon", text: "灵活处理" }] },
|
||||
{ id: 15, question: "您在团队中的角色是:", options: [{ value: "Tiger", text: "领导者" }, { value: "Peacock", text: "激励者" }, { value: "Koala", text: "协调者" }, { value: "Owl", text: "专家" }, { value: "Chameleon", text: "多面手" }] },
|
||||
{ id: 16, question: "您的工作节奏是:", options: [{ value: "Tiger", text: "快节奏" }, { value: "Peacock", text: "充满活力" }, { value: "Koala", text: "稳定有序" }, { value: "Owl", text: "有条理" }, { value: "Chameleon", text: "灵活调整" }] },
|
||||
{ id: 17, question: "您最大的优势是:", options: [{ value: "Tiger", text: "执行力" }, { value: "Peacock", text: "影响力" }, { value: "Koala", text: "可靠性" }, { value: "Owl", text: "准确性" }, { value: "Chameleon", text: "适应力" }] },
|
||||
{ id: 18, question: "您的人际关系特点是:", options: [{ value: "Tiger", text: "目标导向" }, { value: "Peacock", text: "朋友众多" }, { value: "Koala", text: "关系稳定" }, { value: "Owl", text: "志同道合" }, { value: "Chameleon", text: "灵活建立" }] },
|
||||
{ id: 19, question: "面对批评,您的反应是:", options: [{ value: "Tiger", text: "可能反驳" }, { value: "Peacock", text: "可能受伤" }, { value: "Koala", text: "接受思考" }, { value: "Owl", text: "分析合理性" }, { value: "Chameleon", text: "灵活调整" }] },
|
||||
{ id: 20, question: "您的理想工作环境是:", options: [{ value: "Tiger", text: "充满挑战" }, { value: "Peacock", text: "互动频繁" }, { value: "Koala", text: "稳定和谐" }, { value: "Owl", text: "有序规范" }, { value: "Chameleon", text: "灵活多变" }] }
|
||||
]
|
||||
|
||||
/**
|
||||
* Fisher-Yates 洗牌:随机打乱题目顺序,同时随机打乱每题的选项顺序
|
||||
* 不修改原数组,返回深拷贝后的新数组
|
||||
* @param {Array} questions - 原题目数组
|
||||
* @returns {Array} 打乱后的题目数组
|
||||
*/
|
||||
function shuffleQuestions(questions) {
|
||||
// 深拷贝,避免污染原始数组
|
||||
const arr = questions.map(q => ({
|
||||
...q,
|
||||
options: q.options.slice().sort(() => Math.random() - 0.5)
|
||||
}))
|
||||
// Fisher-Yates 打乱题目顺序
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]]
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mbtiQuestions,
|
||||
discQuestions,
|
||||
pdpQuestions,
|
||||
shuffleQuestions
|
||||
}
|
||||
@@ -8,6 +8,168 @@ function toIntPercent(value) {
|
||||
return Number.isFinite(n) ? Math.round(n) : 0
|
||||
}
|
||||
|
||||
const PDP_EN_TO_CN = {
|
||||
Tiger: '老虎型',
|
||||
Peacock: '孔雀型',
|
||||
Koala: '考拉型',
|
||||
Owl: '猫头鹰型',
|
||||
Chameleon: '变色龙型'
|
||||
}
|
||||
|
||||
function pdpOrderedFromScores(scores) {
|
||||
if (!scores || typeof scores !== 'object') return ['', '']
|
||||
const pairs = []
|
||||
for (const k of Object.keys(scores)) {
|
||||
if (!PDP_EN_TO_CN[k]) continue
|
||||
pairs.push([k, Number(scores[k]) || 0])
|
||||
}
|
||||
pairs.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||
return [pairs[0] ? pairs[0][0] : '', pairs[1] ? pairs[1][0] : '']
|
||||
}
|
||||
|
||||
function discOrderedFromScores(scores) {
|
||||
if (!scores || typeof scores !== 'object') return ['', '']
|
||||
const allow = { D: 1, I: 1, S: 1, C: 1 }
|
||||
const pairs = []
|
||||
for (const k of Object.keys(scores)) {
|
||||
const u = String(k).trim().toUpperCase().charAt(0)
|
||||
if (!allow[u]) continue
|
||||
pairs.push([u, Number(scores[k]) || 0])
|
||||
}
|
||||
pairs.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||
return [pairs[0] ? pairs[0][0] : '', pairs[1] ? pairs[1][0] : '']
|
||||
}
|
||||
|
||||
/** 与 scores 相同维度;DISC 接口常以 percentages 存四维而无 scores */
|
||||
function discOrderedFromPercentages(pct) {
|
||||
return discOrderedFromScores(pct)
|
||||
}
|
||||
|
||||
/** DISC:「S+I型」,仅最后一项带「型」 */
|
||||
function discPrimaryLetter(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const dType = data.description && data.description.type
|
||||
if (typeof dType === 'string' && dType) {
|
||||
const noXing = String(dType).trim().replace(/型$/, '')
|
||||
if (noXing.length === 1) {
|
||||
const u = noXing.toUpperCase()
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) return u
|
||||
}
|
||||
}
|
||||
if (data.dominantType) {
|
||||
const u = String(data.dominantType).trim().toUpperCase().charAt(0)
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) return u
|
||||
}
|
||||
if (data.disc) {
|
||||
const noXing = String(data.disc).trim().replace(/型$/, '')
|
||||
if (noXing.length === 1) {
|
||||
const u = noXing.toUpperCase()
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) return u
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function discResolveTwoLetters(data) {
|
||||
let f = discPrimaryLetter(data)
|
||||
let s = ''
|
||||
if (data.secondaryType) {
|
||||
const u = String(data.secondaryType).trim().toUpperCase().charAt(0)
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) s = u
|
||||
}
|
||||
let a0 = ''
|
||||
let b = ''
|
||||
if (data.scores && typeof data.scores === 'object') {
|
||||
const ord = discOrderedFromScores(data.scores)
|
||||
a0 = ord[0] || ''
|
||||
b = ord[1] || ''
|
||||
}
|
||||
if (!a0 && !b && data.percentages && typeof data.percentages === 'object') {
|
||||
const ord = discOrderedFromPercentages(data.percentages)
|
||||
a0 = ord[0] || ''
|
||||
b = ord[1] || ''
|
||||
}
|
||||
if (!f && a0) f = a0
|
||||
if (!s || s === f) {
|
||||
if (b && b !== f) s = b
|
||||
else s = ''
|
||||
}
|
||||
return [f, s]
|
||||
}
|
||||
|
||||
/** 旧接口/库存:「S型 + I型」「S型+I型」→ S+I型 */
|
||||
function discNormalizeLegacyDualType(desc) {
|
||||
if (typeof desc !== 'string' || !desc) return ''
|
||||
const t = desc.replace(/\s+/g, '').replace(/\uFF0B/g, '+')
|
||||
let m = t.match(/^([DISC])型\+([DISC])型$/i)
|
||||
if (m) return m[1].toUpperCase() + '+' + m[2].toUpperCase() + '型'
|
||||
m = t.match(/^([DISC])型$/i)
|
||||
if (m) return m[1].toUpperCase() + '型'
|
||||
return ''
|
||||
}
|
||||
|
||||
function discTopTwoLabel(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const [fL, sL] = discResolveTwoLetters(data)
|
||||
if (fL || sL) {
|
||||
if (!fL) return sL ? sL + '型' : ''
|
||||
if (!sL || sL === fL) return fL + '型'
|
||||
return fL + '+' + sL + '型'
|
||||
}
|
||||
return discNormalizeLegacyDualType(data.description && data.description.type)
|
||||
}
|
||||
|
||||
function pdpPrimaryFull(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const desc = data.description && data.description.type
|
||||
if (typeof desc === 'string' && desc) return desc.trim()
|
||||
if (data.dominantType) {
|
||||
const key = String(data.dominantType).trim()
|
||||
return PDP_EN_TO_CN[key] || key
|
||||
}
|
||||
if (data.pdp) return String(data.pdp).trim()
|
||||
return ''
|
||||
}
|
||||
|
||||
function pdpResolveTwoFull(data) {
|
||||
let f = pdpPrimaryFull(data)
|
||||
let s = ''
|
||||
if (data.secondaryType) {
|
||||
const k = String(data.secondaryType).trim()
|
||||
s = PDP_EN_TO_CN[k] || k
|
||||
}
|
||||
let aEn = ''
|
||||
let bEn = ''
|
||||
if (data.scores && typeof data.scores === 'object') {
|
||||
const ord = pdpOrderedFromScores(data.scores)
|
||||
aEn = ord[0] || ''
|
||||
bEn = ord[1] || ''
|
||||
}
|
||||
if (!aEn && !bEn && data.percentages && typeof data.percentages === 'object') {
|
||||
const ord = pdpOrderedFromScores(data.percentages)
|
||||
aEn = ord[0] || ''
|
||||
bEn = ord[1] || ''
|
||||
}
|
||||
if (!f && aEn) f = PDP_EN_TO_CN[aEn] || aEn
|
||||
if (!s || s === f) {
|
||||
if (bEn) {
|
||||
const cand = PDP_EN_TO_CN[bEn] || bEn
|
||||
if (cand !== f) s = cand
|
||||
}
|
||||
}
|
||||
return [f, s]
|
||||
}
|
||||
|
||||
/** PDP:「孔雀+老虎型」,仅最后一项带「型」 */
|
||||
function pdpTopTwoLabel(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const [ff, sf] = pdpResolveTwoFull(data)
|
||||
if (!ff) return sf || ''
|
||||
if (!sf || sf === ff) return ff
|
||||
const short = String(ff).replace(/型$/, '')
|
||||
return short + '+' + sf
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据测试类型和原始结果,生成带整数百分比的摘要文案
|
||||
* @param {object} data - 单条测试结果(mbtiResult / discResult / pdpResult)
|
||||
@@ -34,8 +196,9 @@ function formatTestSummary(data, testType) {
|
||||
}
|
||||
|
||||
if (t === 'disc') {
|
||||
const two = discTopTwoLabel(data)
|
||||
const desc = data.description && data.description.type
|
||||
const label = (typeof desc === 'string' && desc) ? desc : ((data.dominantType ? data.dominantType + '型' : '') || (data.disc || ''))
|
||||
const label = two || ((typeof desc === 'string' && desc) ? desc : ((data.dominantType ? data.dominantType + '型' : '') || (data.disc || '')))
|
||||
const pct = data.percentages
|
||||
if (pct && typeof pct === 'object') {
|
||||
const d = toIntPercent(pct.D != null ? pct.D : pct.d)
|
||||
@@ -48,8 +211,9 @@ function formatTestSummary(data, testType) {
|
||||
}
|
||||
|
||||
if (t === 'pdp') {
|
||||
const two = pdpTopTwoLabel(data)
|
||||
const desc = data.description && data.description.type
|
||||
const label = (typeof desc === 'string' && desc) ? desc : (data.dominantType || data.pdp || '')
|
||||
const label = two || ((typeof desc === 'string' && desc) ? desc : (data.dominantType || data.pdp || ''))
|
||||
const pct = data.percentages
|
||||
if (pct && typeof pct === 'object') {
|
||||
const names = { Tiger: '老虎', Peacock: '孔雀', Owl: '猫头鹰', Koala: '考拉', Chameleon: '变色龙' }
|
||||
@@ -74,15 +238,22 @@ function getTypeOnly(data, testType) {
|
||||
const t = (testType || '').toLowerCase()
|
||||
if (t === 'mbti') return String(data.mbtiType ?? data.type ?? data.result ?? '')
|
||||
if (t === 'disc') {
|
||||
const two = discTopTwoLabel(data)
|
||||
if (two) return two
|
||||
const desc = data.description?.type
|
||||
if (typeof desc === 'string' && desc) return desc
|
||||
if (data.dominantType) return String(data.dominantType) + '型'
|
||||
return String(data.disc ?? '')
|
||||
}
|
||||
if (t === 'pdp') {
|
||||
const two = pdpTopTwoLabel(data)
|
||||
if (two) return two
|
||||
const desc = data.description?.type
|
||||
if (typeof desc === 'string' && desc) return desc
|
||||
if (data.dominantType) return String(data.dominantType)
|
||||
if (data.dominantType) {
|
||||
const k = String(data.dominantType).trim()
|
||||
return PDP_EN_TO_CN[k] || k
|
||||
}
|
||||
return String(data.pdp ?? '')
|
||||
}
|
||||
return ''
|
||||
@@ -91,5 +262,7 @@ function getTypeOnly(data, testType) {
|
||||
module.exports = {
|
||||
toIntPercent,
|
||||
formatTestSummary,
|
||||
getTypeOnly
|
||||
getTypeOnly,
|
||||
discTopTwoLabel,
|
||||
pdpTopTwoLabel
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user