feat: 填充了组件
This commit is contained in:
129
scripts/regression-long-answer-parsed-value-view.mjs
Normal file
129
scripts/regression-long-answer-parsed-value-view.mjs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { dirname, resolve } from "node:path";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const componentPath = resolve(
|
||||||
|
scriptDir,
|
||||||
|
"../src/pages/ChatMain/ChatLongAnswer/ParsedValueView.vue"
|
||||||
|
);
|
||||||
|
const detailPagePath = resolve(
|
||||||
|
scriptDir,
|
||||||
|
"../src/pages/ChatMain/ChatLongAnswer/index.vue"
|
||||||
|
);
|
||||||
|
const longTextCardPath = resolve(scriptDir, "../src/utils/longTextCard.js");
|
||||||
|
|
||||||
|
const componentSource = readFileSync(componentPath, "utf8");
|
||||||
|
const detailPageSource = readFileSync(detailPagePath, "utf8");
|
||||||
|
const longTextCardSource = readFileSync(longTextCardPath, "utf8");
|
||||||
|
const { getLongTextSections } = await import(pathToFileURL(longTextCardPath));
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
longTextCardSource,
|
||||||
|
/export\s+const\s+normalizeLongTextSpotLocate\s*=/,
|
||||||
|
"longTextCard should export normalizeLongTextSpotLocate for malformed spot_locate keys"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
longTextCardSource,
|
||||||
|
/export\s+const\s+normalizeLongTextQuestionSuggest\s*=/,
|
||||||
|
"longTextCard should export normalizeLongTextQuestionSuggest for JSON-string question_suggest values"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
longTextCardSource,
|
||||||
|
/export\s+const\s+normalizeLongTextContentImage\s*=/,
|
||||||
|
"longTextCard should export normalizeLongTextContentImage for string/object content_image values"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
componentSource,
|
||||||
|
/entry\.type\s*===\s*["']content-image["']/,
|
||||||
|
"ParsedValueView should render content_image with the dedicated image style"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
componentSource,
|
||||||
|
/entry\.type\s*===\s*["']spot-locate["']/,
|
||||||
|
"ParsedValueView should render spot_locate with the dedicated POI action card"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
componentSource,
|
||||||
|
/entry\.type\s*===\s*["']question-suggest["']/,
|
||||||
|
"ParsedValueView should render question_suggest with dedicated FAQ chips"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
componentSource,
|
||||||
|
/sendReply\(question\)/,
|
||||||
|
"question_suggest chips should send the selected follow-up question"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
componentSource,
|
||||||
|
/openMap\(entry\.value\)/,
|
||||||
|
"spot_locate action card should open the normalized map location"
|
||||||
|
);
|
||||||
|
|
||||||
|
const hiddenDetailKeysMatch = detailPageSource.match(
|
||||||
|
/const\s+HIDDEN_DETAIL_SECTION_KEYS\s*=\s*\[([\s\S]*?)\];/
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
hiddenDetailKeysMatch,
|
||||||
|
"long answer detail page should define HIDDEN_DETAIL_SECTION_KEYS"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.doesNotMatch(
|
||||||
|
hiddenDetailKeysMatch[1],
|
||||||
|
/LONG_TEXT_KEYS\.contentSummary/,
|
||||||
|
"long answer detail page should not hide content_summary"
|
||||||
|
);
|
||||||
|
|
||||||
|
const ignoredFieldKeysMatch = componentSource.match(
|
||||||
|
/const\s+IGNORED_FIELD_KEYS\s*=\s*\[([\s\S]*?)\];/
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
ignoredFieldKeysMatch,
|
||||||
|
"ParsedValueView should define IGNORED_FIELD_KEYS"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.doesNotMatch(
|
||||||
|
ignoredFieldKeysMatch[1],
|
||||||
|
/["']content_summary["']/,
|
||||||
|
"ParsedValueView should not ignore content_summary"
|
||||||
|
);
|
||||||
|
|
||||||
|
const receivedKeys = [
|
||||||
|
"tag",
|
||||||
|
"title",
|
||||||
|
"content_summary",
|
||||||
|
"content_image",
|
||||||
|
"view_section_title",
|
||||||
|
"view_section_items",
|
||||||
|
"suggestion_section_title",
|
||||||
|
"suggestion_section_content",
|
||||||
|
"light_reminder_title",
|
||||||
|
"light_reminder_items",
|
||||||
|
"spot_locate",
|
||||||
|
"question_suggest",
|
||||||
|
];
|
||||||
|
|
||||||
|
const longTextData = {
|
||||||
|
values: {},
|
||||||
|
parsedValues: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
receivedKeys.forEach((key) => {
|
||||||
|
longTextData.values[key] = key;
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
getLongTextSections(longTextData).map((section) => section.contentKey),
|
||||||
|
receivedKeys,
|
||||||
|
"long text detail sections should preserve server receive order, including configured fields after extra fields"
|
||||||
|
);
|
||||||
@@ -21,7 +21,24 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- <template v-if="entry.key === LONG_TEXT_KEYS.questionSuggest">
|
<image v-else-if="entry.type === 'content-image'" class="content-body-image" :src="entry.value.url"
|
||||||
|
mode="widthFix" @click="handlePreviewClick(entry.value.url)" />
|
||||||
|
|
||||||
|
<template v-else-if="entry.type === 'spot-locate'">
|
||||||
|
<view class="detail-action-zone">
|
||||||
|
<view class="detail-action-label">查看景点详情</view>
|
||||||
|
<view class="detail-action-card is-raised">
|
||||||
|
<view v-if="entry.value.tag" class="poi-mini-tag">{{ entry.value.tag }}</view>
|
||||||
|
<view class="poi-mini-body">
|
||||||
|
<view v-if="entry.value.name" class="poi-mini-title">{{ entry.value.name }}</view>
|
||||||
|
<view v-if="entry.value.description" class="poi-mini-desc">{{ entry.value.description }}</view>
|
||||||
|
<button v-if="hasMapLocation(entry.value)" class="detail-solid-button" @click.stop="openMap(entry.value)">带我去</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="entry.type === 'question-suggest'">
|
||||||
<view class="detail-action-label">继续追问</view>
|
<view class="detail-action-label">继续追问</view>
|
||||||
<view class="detail-faq-wrap">
|
<view class="detail-faq-wrap">
|
||||||
<view v-for="question in entry.value" :key="question" class="detail-faq-chip" @click="sendReply(question)">
|
<view v-for="question in entry.value" :key="question" class="detail-faq-chip" @click="sendReply(question)">
|
||||||
@@ -30,23 +47,6 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<image v-if="entry.key === LONG_TEXT_KEYS.contentImage" class="content-body-image" :src="entry.value"
|
|
||||||
mode="widthFix" @click="handlePreviewClick(entry.value)" />
|
|
||||||
|
|
||||||
<template v-if="entry.key === LONG_TEXT_KEYS.spotLocate">
|
|
||||||
<view class="detail-action-zone">
|
|
||||||
<view class="detail-action-label">查看景点详情</view>
|
|
||||||
<view class="detail-action-card is-raised">
|
|
||||||
<view v-if="poiTag" class="poi-mini-tag">{{ entry.value.sopt_tag }}</view>
|
|
||||||
<view class="poi-mini-body">
|
|
||||||
<view class="poi-mini-title">{{ entry.value.sopt_name }}</view>
|
|
||||||
<view class="poi-mini-desc">{{ entry.value.spot_description }}</view>
|
|
||||||
<button class="detail-solid-button" @click.stop="openMap(entry.value)">带我去</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template> -->
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -55,7 +55,12 @@
|
|||||||
import { computed, defineProps, ref } from "vue";
|
import { computed, defineProps, ref } from "vue";
|
||||||
import { SEND_MESSAGE_CONTENT_TEXT } from "@/constant/constant";
|
import { SEND_MESSAGE_CONTENT_TEXT } from "@/constant/constant";
|
||||||
import { getRandomTagToneStyle } from "@/utils/tagTone";
|
import { getRandomTagToneStyle } from "@/utils/tagTone";
|
||||||
import { LONG_TEXT_KEYS } from "@/utils/longTextCard";
|
import {
|
||||||
|
LONG_TEXT_KEYS,
|
||||||
|
normalizeLongTextContentImage,
|
||||||
|
normalizeLongTextQuestionSuggest,
|
||||||
|
normalizeLongTextSpotLocate,
|
||||||
|
} from "@/utils/longTextCard";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
fieldKey: {
|
fieldKey: {
|
||||||
@@ -77,7 +82,7 @@ const contentBodyListTextStyle = computed(() => ({
|
|||||||
color: contentBodyListToneStyle.value.color,
|
color: contentBodyListToneStyle.value.color,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const IGNORED_FIELD_KEYS = ["container_type", "content", "content_summary", "components"];
|
const IGNORED_FIELD_KEYS = ["container_type", "content", "components"];
|
||||||
|
|
||||||
const isArrayValue = (value) => Array.isArray(value);
|
const isArrayValue = (value) => Array.isArray(value);
|
||||||
|
|
||||||
@@ -151,9 +156,49 @@ const createImageEntry = (key, value) => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createSpecialFieldEntry = (key, value) => {
|
||||||
|
if (key === LONG_TEXT_KEYS.contentImage) {
|
||||||
|
const image = normalizeLongTextContentImage(value);
|
||||||
|
return image.url
|
||||||
|
? [{
|
||||||
|
key,
|
||||||
|
type: "content-image",
|
||||||
|
value: image,
|
||||||
|
}]
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === LONG_TEXT_KEYS.spotLocate) {
|
||||||
|
const spot = normalizeLongTextSpotLocate(value);
|
||||||
|
return hasDisplayValue(spot)
|
||||||
|
? [{
|
||||||
|
key,
|
||||||
|
type: "spot-locate",
|
||||||
|
value: spot,
|
||||||
|
}]
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === LONG_TEXT_KEYS.questionSuggest) {
|
||||||
|
const questions = normalizeLongTextQuestionSuggest(value);
|
||||||
|
return questions.length
|
||||||
|
? [{
|
||||||
|
key,
|
||||||
|
type: "question-suggest",
|
||||||
|
value: questions,
|
||||||
|
}]
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const renderFieldEntries = computed(() => {
|
const renderFieldEntries = computed(() => {
|
||||||
if (isIgnoredField.value || !hasDisplayValue(props.value)) return [];
|
if (isIgnoredField.value || !hasDisplayValue(props.value)) return [];
|
||||||
|
|
||||||
|
const specialFieldEntry = createSpecialFieldEntry(props.fieldKey, props.value);
|
||||||
|
if (specialFieldEntry) return specialFieldEntry;
|
||||||
|
|
||||||
const value = sanitizeValue(props.value);
|
const value = sanitizeValue(props.value);
|
||||||
if (isImageValue(value)) {
|
if (isImageValue(value)) {
|
||||||
return [createImageEntry(props.fieldKey, value)];
|
return [createImageEntry(props.fieldKey, value)];
|
||||||
@@ -200,14 +245,27 @@ const renderFieldEntries = computed(() => {
|
|||||||
const isIgnoredField = computed(() => IGNORED_FIELD_KEYS.includes(props.fieldKey));
|
const isIgnoredField = computed(() => IGNORED_FIELD_KEYS.includes(props.fieldKey));
|
||||||
const shouldRenderField = computed(() => renderFieldEntries.value.length > 0);
|
const shouldRenderField = computed(() => renderFieldEntries.value.length > 0);
|
||||||
|
|
||||||
|
const hasMapLocation = (spot) => {
|
||||||
|
return Number.isFinite(spot?.latitude) && Number.isFinite(spot?.longitude);
|
||||||
|
};
|
||||||
|
|
||||||
const openMap = (spot) => {
|
const openMap = (spot) => {
|
||||||
|
if (!hasMapLocation(spot)) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "暂无位置信息",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const latitude = spot.latitude;
|
const latitude = spot.latitude;
|
||||||
const longitude = spot.longitude;
|
const longitude = spot.longitude;
|
||||||
const address = spot.name || '';
|
const name = spot.name || "";
|
||||||
|
const address = spot.description || name;
|
||||||
uni.getLocation({ type: 'gcj02', success: () => {
|
uni.getLocation({ type: 'gcj02', success: () => {
|
||||||
uni.openLocation({ latitude, longitude, address });
|
uni.openLocation({ latitude, longitude, name, address });
|
||||||
}, fail: () => {
|
}, fail: () => {
|
||||||
uni.openLocation({ latitude, longitude, address });
|
uni.openLocation({ latitude, longitude, name, address });
|
||||||
}});
|
}});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ let unsubscribe = null;
|
|||||||
|
|
||||||
const HIDDEN_DETAIL_SECTION_KEYS = [
|
const HIDDEN_DETAIL_SECTION_KEYS = [
|
||||||
LONG_TEXT_KEYS.containerType,
|
LONG_TEXT_KEYS.containerType,
|
||||||
LONG_TEXT_KEYS.contentSummary,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const shouldUseParsedValueView = (section) => {
|
const shouldUseParsedValueView = (section) => {
|
||||||
|
|||||||
@@ -127,6 +127,10 @@
|
|||||||
line-height: 42px;
|
line-height: 42px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-solid-button::after {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-faq-wrap {
|
.detail-faq-wrap {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,8 +88,6 @@ export const LONG_TEXT_PREVIEW_KEYS = [
|
|||||||
LONG_TEXT_KEYS.tag,
|
LONG_TEXT_KEYS.tag,
|
||||||
];
|
];
|
||||||
|
|
||||||
const CONFIGURED_KEYS = LONG_TEXT_FIELD_CONFIG.map((item) => item.key);
|
|
||||||
|
|
||||||
export const createLongTextData = () => ({
|
export const createLongTextData = () => ({
|
||||||
values: {},
|
values: {},
|
||||||
parsedValues: {},
|
parsedValues: {},
|
||||||
@@ -116,6 +114,121 @@ const tryParseJSON = (raw) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseMaybeJSON = (value) => {
|
||||||
|
if (typeof value !== "string") return value;
|
||||||
|
|
||||||
|
const parsed = tryParseJSON(value.trim());
|
||||||
|
return parsed.ok ? parsed.value : value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toTrimmedText = (value) => {
|
||||||
|
if (value === undefined || value === null) return "";
|
||||||
|
return String(value).trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toFiniteNumber = (value) => {
|
||||||
|
if (value === undefined || value === null || value === "") return null;
|
||||||
|
const numberValue = Number(value);
|
||||||
|
return Number.isFinite(numberValue) ? numberValue : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pickFirstValue = (...values) => {
|
||||||
|
return values.find((value) => value !== undefined && value !== null && value !== "");
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeObjectKeys = (value) => {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
||||||
|
|
||||||
|
return Object.keys(value).reduce((result, key) => {
|
||||||
|
const normalizedKey = String(key).trim().replace(/[::]+$/, "");
|
||||||
|
result[normalizedKey] = value[key];
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeLongTextContentImage = (value) => {
|
||||||
|
const parsedValue = parseMaybeJSON(value);
|
||||||
|
|
||||||
|
if (typeof parsedValue === "string") {
|
||||||
|
return {
|
||||||
|
url: toTrimmedText(parsedValue),
|
||||||
|
caption: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueObj = normalizeObjectKeys(parsedValue);
|
||||||
|
return {
|
||||||
|
url: toTrimmedText(
|
||||||
|
pickFirstValue(
|
||||||
|
valueObj.url,
|
||||||
|
valueObj.image_url,
|
||||||
|
valueObj.image,
|
||||||
|
valueObj.image_id,
|
||||||
|
valueObj.content_image
|
||||||
|
)
|
||||||
|
),
|
||||||
|
caption: toTrimmedText(pickFirstValue(valueObj.caption, valueObj.image_caption)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeLongTextQuestionSuggest = (value) => {
|
||||||
|
const parsedValue = parseMaybeJSON(value);
|
||||||
|
const rawList = Array.isArray(parsedValue)
|
||||||
|
? parsedValue
|
||||||
|
: [parsedValue?.questions, parsedValue?.items, parsedValue?.list].find(Array.isArray) || [];
|
||||||
|
|
||||||
|
return rawList
|
||||||
|
.map((item) => toTrimmedText(item))
|
||||||
|
.filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeLongTextSpotLocate = (value) => {
|
||||||
|
const valueObj = normalizeObjectKeys(parseMaybeJSON(value));
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: toTrimmedText(
|
||||||
|
pickFirstValue(
|
||||||
|
valueObj.spot_name,
|
||||||
|
valueObj.sopt_name,
|
||||||
|
valueObj.name,
|
||||||
|
valueObj.title
|
||||||
|
)
|
||||||
|
),
|
||||||
|
description: toTrimmedText(
|
||||||
|
pickFirstValue(
|
||||||
|
valueObj.spot_description,
|
||||||
|
valueObj.sopt_description,
|
||||||
|
valueObj.description,
|
||||||
|
valueObj.desc
|
||||||
|
)
|
||||||
|
),
|
||||||
|
longitude: toFiniteNumber(
|
||||||
|
pickFirstValue(
|
||||||
|
valueObj.spot_longitude,
|
||||||
|
valueObj.sopt_longitude,
|
||||||
|
valueObj.longitude,
|
||||||
|
valueObj.lng
|
||||||
|
)
|
||||||
|
),
|
||||||
|
latitude: toFiniteNumber(
|
||||||
|
pickFirstValue(
|
||||||
|
valueObj.spot_latitude,
|
||||||
|
valueObj.sopt_latitude,
|
||||||
|
valueObj.latitude,
|
||||||
|
valueObj.lat
|
||||||
|
)
|
||||||
|
),
|
||||||
|
tag: toTrimmedText(
|
||||||
|
pickFirstValue(
|
||||||
|
valueObj.spot_tag,
|
||||||
|
valueObj.sopt_tag,
|
||||||
|
valueObj.tag,
|
||||||
|
valueObj.type
|
||||||
|
)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const appendLongTextChunk = (target, chunk = {}) => {
|
export const appendLongTextChunk = (target, chunk = {}) => {
|
||||||
if (!target || !chunk.contentKey) return target;
|
if (!target || !chunk.contentKey) return target;
|
||||||
|
|
||||||
@@ -165,12 +278,7 @@ export const hasLongTextExtraSections = (data, previewKeys = LONG_TEXT_PREVIEW_K
|
|||||||
export const getLongTextSections = (data) => {
|
export const getLongTextSections = (data) => {
|
||||||
if (!data || !data.values) return [];
|
if (!data || !data.values) return [];
|
||||||
|
|
||||||
const extraKeys = Object.keys(data.values).filter(
|
return Object.keys(data.values)
|
||||||
(key) => !CONFIGURED_KEYS.includes(key),
|
|
||||||
);
|
|
||||||
|
|
||||||
return [...CONFIGURED_KEYS, ...extraKeys]
|
|
||||||
.filter((key) => Object.prototype.hasOwnProperty.call(data.values, key))
|
|
||||||
.map((key) => ({
|
.map((key) => ({
|
||||||
contentKey: key,
|
contentKey: key,
|
||||||
contentValue: getLongTextValue(data, key),
|
contentValue: getLongTextValue(data, key),
|
||||||
|
|||||||
Reference in New Issue
Block a user