diff --git a/scripts/regression-long-answer-parsed-value-view.mjs b/scripts/regression-long-answer-parsed-value-view.mjs
index 7833900..d7d0510 100644
--- a/scripts/regression-long-answer-parsed-value-view.mjs
+++ b/scripts/regression-long-answer-parsed-value-view.mjs
@@ -18,69 +18,148 @@ 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));
+const {
+ appendLongTextChunk,
+ createLongTextData,
+ getLongTextSections,
+} = await import(pathToFileURL(longTextCardPath));
-assert.match(
+assert.doesNotMatch(
longTextCardSource,
- /export\s+const\s+normalizeLongTextSpotLocate\s*=/,
- "longTextCard should export normalizeLongTextSpotLocate for malformed spot_locate keys"
+ /normalizeLongText/,
+ "longTextCard should not contain field-specific normalize helpers"
);
-assert.match(
+assert.doesNotMatch(
longTextCardSource,
- /export\s+const\s+normalizeLongTextQuestionSuggest\s*=/,
- "longTextCard should export normalizeLongTextQuestionSuggest for JSON-string question_suggest values"
+ /spot_longitude|spot_latitude|spot_tag/,
+ "longTextCard should not know spot_locate child field names"
);
-assert.match(
- longTextCardSource,
- /export\s+const\s+normalizeLongTextContentImage\s*=/,
- "longTextCard should export normalizeLongTextContentImage for string/object content_image values"
+assert.doesNotMatch(
+ componentSource,
+ /isGenericObjectSectionField/,
+ "ParsedValueView should use renderFieldEntries instead of a separate generic object branch"
);
assert.match(
componentSource,
- /entry\.type\s*===\s*["']content-image["']/,
- "ParsedValueView should render content_image with the dedicated image style"
+ /getRenderEntriesForObject/,
+ "ParsedValueView should derive object rows through the existing entry renderer"
+);
+
+assert.doesNotMatch(
+ componentSource,
+ /STRUCTURED_SECTION_CONFIG/,
+ "ParsedValueView should not require per-field structured section config"
+);
+
+assert.doesNotMatch(
+ componentSource,
+ /LONG_TEXT_KEYS\.(preparationSection|sectionSuggestion|pitfallSection|viewSection|suggestionSection|lightReminder|photoSpotSection|phoneSection|decisionSection|routeWarning|tourRoutes|facilitiesAlongTheWay)/,
+ "ParsedValueView should not reference ordinary section keys individually"
+);
+
+assert.doesNotMatch(
+ componentSource,
+ /content-body-section-dot|content-body-section-list/,
+ "generic object arrays should keep the original list-card rendering style"
+);
+
+assert.doesNotMatch(
+ componentSource,
+ /entry\.type\s*===\s*'(question-suggest|photo-list|aigc-componet|commodity-list|spot-locate)'/,
+ "renderFieldEntries should only render generic text/image/list entries"
+);
+
+assert.doesNotMatch(
+ componentSource,
+ /createSpecialFieldEntry/,
+ "dedicated long text components should be selected by top-level computed branches only"
);
assert.match(
componentSource,
- /entry\.type\s*===\s*["']spot-locate["']/,
+ /shouldRenderQuestionSuggest/,
+ "top-level question_suggest fields should render through the dedicated chips"
+);
+
+assert.match(
+ componentSource,
+ /shouldRenderCommodityList/,
+ "top-level commodity_list fields should render through the dedicated product list"
+);
+
+assert.match(
+ componentSource,
+ /shouldRenderPhotoList/,
+ "top-level photo_list fields should render through the dedicated photo swiper"
+);
+
+assert.match(
+ componentSource,
+ /shouldRenderAigcComponet/,
+ "top-level aigc_componet fields should render through the dedicated AIGC card"
+);
+
+assert.match(
+ componentSource,
+ /shouldRenderSpotLocate/,
+ "top-level spot_locate fields should render through the dedicated POI card"
+);
+
+assert.doesNotMatch(
+ componentSource,
+ /createRenderEntry\(key,\s*entryKey,\s*value\[key\]\)/,
+ "nested object keys should not trigger dedicated long text components"
+);
+
+assert.match(
+ componentSource,
+ /openMap\(spotLocateValue\)/,
"ParsedValueView should render spot_locate with the dedicated POI action card"
);
assert.match(
componentSource,
- /entry\.type\s*===\s*["']question-suggest["']/,
+ /getSpotLocateValue/,
+ "ParsedValueView should own spot_locate value shaping"
+);
+
+assert.doesNotMatch(
+ componentSource,
+ /spot_longitude:|spot_latitude:|spot_tag:/,
+ "ParsedValueView should not read malformed spot_locate keys"
+);
+
+assert.match(
+ componentSource,
+ /shouldRenderQuestionSuggest/,
"ParsedValueView should render question_suggest with dedicated FAQ chips"
);
+assert.match(
+ componentSource,
+ /getQuestionSuggestItems/,
+ "ParsedValueView should own question_suggest value shaping"
+);
+
assert.match(
componentSource,
/sendReply\(question\)/,
"question_suggest chips should send the selected follow-up question"
);
+assert.doesNotMatch(
+ detailPageSource,
+ /LONG_TEXT_KEYS\.content\b/,
+ "long answer detail page should not reference removed content key"
+);
+
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"
+ /LONG_TEXT_KEYS\.contentImage\b/,
+ "ParsedValueView should render content_image with the dedicated image style"
);
const ignoredFieldKeysMatch = componentSource.match(
@@ -102,28 +181,27 @@ const receivedKeys = [
"tag",
"title",
"content_summary",
+ "scene_image",
"content_image",
- "view_section_title",
- "view_section_items",
- "suggestion_section_title",
- "suggestion_section_content",
- "light_reminder_title",
- "light_reminder_items",
+ "view_section",
+ "suggestion_section",
+ "light_reminder",
"spot_locate",
"question_suggest",
];
-const longTextData = {
- values: {},
- parsedValues: {},
-};
+const longTextData = createLongTextData();
receivedKeys.forEach((key) => {
- longTextData.values[key] = key;
+ appendLongTextChunk(longTextData, { contentKey: key, contentValue: key });
+});
+appendLongTextChunk(longTextData, {
+ contentKey: "view_section_items",
+ contentValue: "old flat field",
});
assert.deepEqual(
getLongTextSections(longTextData).map((section) => section.contentKey),
- receivedKeys,
- "long text detail sections should preserve server receive order, including configured fields after extra fields"
+ [...receivedKeys, "view_section_items"],
+ "long text detail sections should keep configured fields first and preserve unknown contentKey fields"
);
diff --git a/scripts/test-ParsedValueView-strict.mjs b/scripts/test-ParsedValueView-strict.mjs
index ed69a32..140c5f8 100644
--- a/scripts/test-ParsedValueView-strict.mjs
+++ b/scripts/test-ParsedValueView-strict.mjs
@@ -42,9 +42,16 @@ for (const snippet of forbiddenCompatibilitySnippets) {
const requiredStrictRenderingSnippets = [
"LONG_TEXT_KEYS.sceneImage",
+ "LONG_TEXT_KEYS.contentImage",
"LONG_TEXT_KEYS.commodityList",
"LONG_TEXT_KEYS.photoList",
"LONG_TEXT_KEYS.aigcComponet",
+ "shouldRenderSpotLocate",
+ "shouldRenderQuestionSuggest",
+ "shouldRenderCommodityList",
+ "shouldRenderPhotoList",
+ "shouldRenderAigcComponet",
+ "getRenderEntriesForObject",
"commodity.commodity_id",
"commodity.commodity_name",
"commodity.commodity_price",
@@ -62,9 +69,7 @@ const requiredStrictRenderingSnippets = [
"jumpAigcClick",
"getAccessToken",
"navigateTo",
- "content-body-list-marker",
- "entry.value.length > 1",
- "formatListMarker(index)",
+ "content-body-list-card",
];
for (const snippet of requiredStrictRenderingSnippets) {
@@ -75,4 +80,71 @@ for (const snippet of requiredStrictRenderingSnippets) {
);
}
+const removedInnerSpecialEntrySnippets = [
+ "createSpecialFieldEntry",
+ "entry.type === 'question-suggest'",
+ "entry.type === 'photo-list'",
+ "entry.type === 'aigc-componet'",
+ "entry.type === 'commodity-list'",
+ "entry.type === 'spot-locate'",
+];
+
+for (const snippet of removedInnerSpecialEntrySnippets) {
+ assert.equal(
+ source.includes(snippet),
+ false,
+ `ParsedValueView should keep special fields at the top level only: ${snippet}`
+ );
+}
+
+const removedFlatFieldSnippets = [
+ "preparationSectionTitle",
+ "preparationSectionItems",
+ "sectionSuggestionTitle",
+ "sectionSuggestionContent",
+ "pitfallSectionTitle",
+ "pitfallSectionItems",
+ "viewSectionTitle",
+ "viewSectionItems",
+ "suggestionSectionTitle",
+ "suggestionSectionContent",
+ "lightReminderTitle",
+ "lightReminderItems",
+];
+
+for (const snippet of removedFlatFieldSnippets) {
+ assert.equal(
+ source.includes(`LONG_TEXT_KEYS.${snippet}`),
+ false,
+ `ParsedValueView should not reference removed flat field: ${snippet}`
+ );
+}
+
+const removedStructuredConfigSnippets = [
+ "STRUCTURED_SECTION_CONFIG",
+ "isGenericObjectSectionField",
+ "content-body-section-dot",
+ "content-body-section-list",
+ "LONG_TEXT_KEYS.preparationSection",
+ "LONG_TEXT_KEYS.sectionSuggestion",
+ "LONG_TEXT_KEYS.pitfallSection",
+ "LONG_TEXT_KEYS.viewSection",
+ "LONG_TEXT_KEYS.suggestionSection",
+ "LONG_TEXT_KEYS.lightReminder",
+ "LONG_TEXT_KEYS.photoSpotSection",
+ "LONG_TEXT_KEYS.phoneSection",
+ "LONG_TEXT_KEYS.decisionSection",
+ "LONG_TEXT_KEYS.routeWarning",
+ "LONG_TEXT_KEYS.tourRoutes",
+ "LONG_TEXT_KEYS.facilitiesAlongTheWay",
+];
+
+for (const snippet of removedStructuredConfigSnippets) {
+ assert.equal(
+ source.includes(snippet),
+ false,
+ `ParsedValueView should use generic section rendering instead of configured field: ${snippet}`
+ );
+}
+
console.log("ParsedValueView strict field checks passed");
diff --git a/scripts/test-longTextCard.mjs b/scripts/test-longTextCard.mjs
index 7a10ea8..f1273da 100644
--- a/scripts/test-longTextCard.mjs
+++ b/scripts/test-longTextCard.mjs
@@ -6,15 +6,45 @@ const source = await readFile(resolve("src/utils/longTextCard.js"), "utf8");
const moduleUrl = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
const longTextCard = await import(moduleUrl);
+assert.doesNotMatch(
+ source,
+ /normalizeLongText/,
+ "longTextCard should not contain field-specific normalize helpers"
+);
+
+assert.doesNotMatch(
+ source,
+ /spot_longitude|spot_latitude|spot_tag/,
+ "longTextCard should not know spot_locate child field names"
+);
+
const {
LONG_TEXT_FIELD_CONFIG,
LONG_TEXT_KEYS,
+ appendLongTextChunk,
+ createLongTextData,
+ getLongTextParsedValue,
+ getLongTextSections,
+ getLongTextValue,
+ hasLongTextExtraSections,
parseLongTextDisplayValue,
sanitizeLongTextDisplayValue,
hasLongTextDisplayValue,
formatLongTextDisplayValue,
} = longTextCard;
+assert.equal(
+ source.includes("REMOVED_LONG_TEXT_FIELD_KEYS"),
+ false,
+ "longTextCard should not carry a removed field blacklist"
+);
+
+assert.equal(
+ source.includes("hasLongTextChunkPayload"),
+ false,
+ "ChatMainList should not ask longTextCard to inspect plain data.content payloads"
+);
+
assert.deepEqual(
parseLongTextDisplayValue('[ "see bridge", "hear water" ]'),
["see bridge", "hear water"],
@@ -22,25 +52,29 @@ assert.deepEqual(
);
assert.deepEqual(
- parseLongTextDisplayValue('{"spot_name":"bridge","spot_longitude:":107.712345}'),
- { spot_name: "bridge", "spot_longitude:": 107.712345 },
+ parseLongTextDisplayValue('{"spot_name":"bridge","spot_longitude":107.712345}'),
+ { spot_name: "bridge", spot_longitude: 107.712345 },
"serialized objects should be parsed for display"
);
assert.deepEqual(
sanitizeLongTextDisplayValue(
{
- content: "hidden",
- components: ["hidden"],
- view_section_items: '[ "check piers", "count arches" ]',
+ preparation_section: {
+ preparation_section_title: "keep",
+ preparation_section_items: '[ "check piers", "count arches" ]',
+ },
nested: {
title: "keep",
},
},
- ["content", "components"]
+ []
),
{
- view_section_items: ["check piers", "count arches"],
+ preparation_section: {
+ preparation_section_title: "keep",
+ preparation_section_items: ["check piers", "count arches"],
+ },
nested: {
title: "keep",
},
@@ -54,20 +88,28 @@ assert.equal(formatLongTextDisplayValue(true), "\u662f");
assert.equal(formatLongTextDisplayValue({ title: "bridge" }), '{"title":"bridge"}');
const expectedNewKeys = {
- preparationSectionTitle: "preparation_section_title",
- preparationSectionItems: "preparation_section_items",
- sectionSuggestionTitle: "section_suggestion_title",
- sectionSuggestionContent: "section_suggestion_content",
- pitfallSectionTitle: "pitfall_section_title",
- pitfallSectionItems: "pitfall_section_items",
+ tag: "tag",
+ title: "title",
+ contentSummary: "content_summary",
+ sceneImage: "scene_image",
+ preparationSection: "preparation_section",
+ sectionSuggestion: "section_suggestion",
+ pitfallSection: "pitfall_section",
commodityList: "commodity_list",
- photoSpotSectionTitle: "photo_spot_section_title",
- photoSpotSectionItems: "photo_spot_section_items",
- bestTimeSuggestion: "best_time_suggestion",
- phoneSectionTitle: "phone_section_title",
- phoneSectionItems: "phone_section_items",
+ contentImage: "content_image",
+ viewSection: "view_section",
+ suggestionSection: "suggestion_section",
+ lightReminder: "light_reminder",
+ spotLocate: "spot_locate",
+ photoSpotSection: "photo_spot_section",
+ phoneSection: "phone_section",
photoList: "photo_list",
aigcComponet: "aigc_componet",
+ decisionSection: "decision_section",
+ routeWarning: "route_warning",
+ tourRoutes: "tour_routes",
+ facilitiesAlongTheWay: "facilities_along_the_way",
+ questionSuggest: "question_suggest",
};
for (const [keyName, keyValue] of Object.entries(expectedNewKeys)) {
@@ -79,4 +121,153 @@ for (const [keyName, keyValue] of Object.entries(expectedNewKeys)) {
);
}
+const removedKeys = [
+ "content",
+ "guideConclusion",
+ "keyFacts",
+ "preparationSectionTitle",
+ "preparationSectionItems",
+ "sectionSuggestionTitle",
+ "sectionSuggestionContent",
+ "pitfallSectionTitle",
+ "pitfallSectionItems",
+ "viewSectionTitle",
+ "viewSectionItems",
+ "suggestionSectionTitle",
+ "suggestionSectionContent",
+ "lightReminderTitle",
+ "lightReminderItems",
+ "components",
+ "actionZone",
+];
+
+for (const keyName of removedKeys) {
+ assert.equal(
+ Object.prototype.hasOwnProperty.call(LONG_TEXT_KEYS, keyName),
+ false,
+ `${keyName} should be removed from LONG_TEXT_KEYS`
+ );
+}
+
+const oldFlatFieldValues = [
+ "preparation_section_title",
+ "preparation_section_items",
+ "view_section_title",
+ "view_section_items",
+];
+
+for (const keyValue of oldFlatFieldValues) {
+ assert.equal(
+ LONG_TEXT_FIELD_CONFIG.some((item) => item.key === keyValue),
+ false,
+ `${keyValue} should be removed from LONG_TEXT_FIELD_CONFIG`
+ );
+}
+
+const longTextData = createLongTextData();
+appendLongTextChunk(longTextData, { contentKey: "title", contentValue: "漂流攻略" });
+appendLongTextChunk(longTextData, {
+ contentKey: "preparation_section",
+ contentValue: {
+ preparation_section_title: "下水前",
+ preparation_section_items: ["带手机防水袋", "穿涉水鞋"],
+ },
+});
+appendLongTextChunk(longTextData, {
+ contentKey: "future_section",
+ contentValue: {
+ future_section_title: "未来新增字段",
+ future_section_items: ["不改 LONG_TEXT_KEYS 也展示"],
+ },
+});
+appendLongTextChunk(longTextData, {
+ contentKey: "content_image",
+ contentValue: "https://oss.nianxx.cn/XiaoQiKong/GQ00001.jpg",
+});
+appendLongTextChunk(longTextData, {
+ contentKey: "spot_locate",
+ contentValue: '{"spot_name":"卧龙潭","spot_longitude":"107.712345","spot_latitude":"25.251234","spot_tag":"景点"}',
+});
+
+assert.equal(getLongTextValue(longTextData, "title"), "漂流攻略");
+assert.deepEqual(
+ getLongTextParsedValue(longTextData, "preparation_section"),
+ {
+ preparation_section_title: "下水前",
+ preparation_section_items: ["带手机防水袋", "穿涉水鞋"],
+ },
+ "object contentValue should be serialized and parsed"
+);
+assert.deepEqual(
+ getLongTextSections(longTextData).map((section) => section.contentKey),
+ ["title", "preparation_section", "future_section", "content_image", "spot_locate"],
+ "sections should place unknown fields by receive order between configured fields"
+);
+assert.equal(hasLongTextExtraSections(longTextData), true);
+
+assert.deepEqual(
+ getLongTextParsedValue(longTextData, "future_section"),
+ {
+ future_section_title: "未来新增字段",
+ future_section_items: ["不改 LONG_TEXT_KEYS 也展示"],
+ },
+ "future top-level fields should keep parsed values"
+);
+
+const mergedPayload = {
+ tag: "攻略",
+ title: "合并结构标题",
+ content_summary: "合并结构摘要",
+ preparation_section: {
+ preparation_section_title: "下水前",
+ preparation_section_items: ["带手机防水袋", "穿涉水鞋"],
+ },
+ spot_locate: {
+ spot_name: "卧龙潭",
+ spot_longitude: "107.712345",
+ spot_latitude: "25.251234",
+ spot_tag: "景点",
+ },
+ photo_list: [
+ {
+ photo_name: "桥边机位",
+ photo_description: "站在桥头拍",
+ photo_url: "https://example.com/photo.png",
+ },
+ ],
+ photo_spot_section: "{\n\"photo_spot_section_title\":\"四个不会错的机位\",\n\"photo_spot_items\":\"- 涵碧潭半清半浊\\n- 断桥飞瀑错位玩\",\n\"best_time_suggestion\":\"下午 2 点后人少\"\n}",
+ phone_section: "{\n\"phone_section_title\":\"手机党看这里\",\n\"phone_section_items\":\"- 手机倒过来贴近水面\\n- 瀑布用长曝光\"\n}",
+ aigc_componet: {
+ background: "https://example.com/aigc.png",
+ title: "AIGC合影",
+ description: "生成合影",
+ jumpUrl: "https://example.com/aigc",
+ },
+ question_suggest: ["还要准备什么?"],
+ future_section: {
+ future_section_title: "后续新增模块",
+ future_section_items: ["仍然展示"],
+ },
+};
+
+const mergedLongTextData = createLongTextData();
+appendLongTextChunk(mergedLongTextData, {
+ contentKey: "photo_card_payload",
+ contentValue: mergedPayload,
+});
+
+assert.deepEqual(
+ getLongTextSections(mergedLongTextData).map((section) => section.contentKey),
+ [
+ "photo_card_payload",
+ ],
+ "unknown contentKey fields should be kept as a single generic section"
+);
+
+assert.deepEqual(
+ getLongTextParsedValue(mergedLongTextData, "photo_card_payload"),
+ mergedPayload,
+ "unknown contentKey object values should keep parsed values"
+);
+
console.log("longTextCard display helpers passed");
diff --git a/src/pages/ChatMain/ChatLongAnswer/ParsedValueView.vue b/src/pages/ChatMain/ChatLongAnswer/ParsedValueView.vue
index c8f65c0..97018e0 100644
--- a/src/pages/ChatMain/ChatLongAnswer/ParsedValueView.vue
+++ b/src/pages/ChatMain/ChatLongAnswer/ParsedValueView.vue
@@ -194,7 +194,7 @@ const contentBodyListCardStyle = computed(() => ({
}));
const contentBodyMarkdownColor = computed(() => contentBodyListToneStyle.value.color);
-const IGNORED_FIELD_KEYS = ["container_type", "content", "components"];
+const IGNORED_FIELD_KEYS = [];
/// ======== Value Processing Helpers ========
const isArrayValue = (value) => Array.isArray(value);
@@ -239,12 +239,11 @@ const createImageEntry = (key, value) => ({
},
});
-
/// ======== Render Logic ========
const isIgnoredField = computed(() => IGNORED_FIELD_KEYS.includes(props.fieldKey));
const isContentImageField = computed(() =>
- props.fieldKey === LONG_TEXT_KEYS.contentImage ||
- props.fieldKey === LONG_TEXT_KEYS.sceneImage
+ props.fieldKey === LONG_TEXT_KEYS.sceneImage ||
+ props.fieldKey === LONG_TEXT_KEYS.contentImage
);
const isSpotLocateField = computed(() => props.fieldKey === LONG_TEXT_KEYS.spotLocate);
const isQuestionSuggestField = computed(() => props.fieldKey === LONG_TEXT_KEYS.questionSuggest);
@@ -260,28 +259,34 @@ const contentImageUrl = computed(() => {
return typeof displayValue.value === "string" ? toTrimmedText(displayValue.value) : "";
});
-const spotLocateValue = computed(() => {
- const value = isObjectValue(displayValue.value) ? displayValue.value : {};
+const getSpotLocateValue = (value) => {
+ const spot = isObjectValue(value) ? value : {};
return {
- name: toTrimmedText(value.spot_name),
- description: toTrimmedText(value.spot_description),
- longitude: toFiniteNumber(value.spot_longitude),
- latitude: toFiniteNumber(value.spot_latitude),
- tag: toTrimmedText(value.spot_tag),
+ name: toTrimmedText(spot.spot_name),
+ description: toTrimmedText(spot.spot_description),
+ longitude: toFiniteNumber(spot.spot_longitude),
+ latitude: toFiniteNumber(spot.spot_latitude),
+ tag: toTrimmedText(spot.spot_tag),
};
+};
+
+const spotLocateValue = computed(() => {
+ return getSpotLocateValue(displayValue.value);
});
-const questionSuggestItems = computed(() => {
- return isArrayValue(displayValue.value)
- ? displayValue.value.map((item) => toTrimmedText(item)).filter(Boolean)
+const getQuestionSuggestItems = (value) => {
+ return isArrayValue(value)
+ ? value.map((item) => toTrimmedText(item)).filter(Boolean)
: [];
-});
+};
-const commodityItems = computed(() => {
- if (!isArrayValue(displayValue.value)) return [];
+const questionSuggestItems = computed(() => getQuestionSuggestItems(displayValue.value));
- return displayValue.value
+const getCommodityItems = (value) => {
+ if (!isArrayValue(value)) return [];
+
+ return value
.filter((item) => isObjectValue(item))
.map((commodity) => ({
commodity_id: toTrimmedText(commodity.commodity_id),
@@ -291,12 +296,14 @@ const commodityItems = computed(() => {
commodity_photo: toTrimmedText(commodity.commodity_photo),
}))
.filter((commodity) => hasDisplayValue(commodity));
-});
+};
-const photoItems = computed(() => {
- if (!isArrayValue(displayValue.value)) return [];
+const commodityItems = computed(() => getCommodityItems(displayValue.value));
- return displayValue.value
+const getPhotoItems = (value) => {
+ if (!isArrayValue(value)) return [];
+
+ return value
.filter((item) => isObjectValue(item))
.map((photo) => ({
photo_name: toTrimmedText(photo.photo_name),
@@ -304,10 +311,12 @@ const photoItems = computed(() => {
photo_url: toTrimmedText(photo.photo_url),
}))
.filter((photo) => !!photo.photo_url);
-});
+};
-const aigcComponetValue = computed(() => {
- const aigc = isObjectValue(displayValue.value) ? displayValue.value : {};
+const photoItems = computed(() => getPhotoItems(displayValue.value));
+
+const getAigcComponetValue = (value) => {
+ const aigc = isObjectValue(value) ? value : {};
return {
background: toTrimmedText(aigc.background),
@@ -315,7 +324,44 @@ const aigcComponetValue = computed(() => {
description: toTrimmedText(aigc.description),
jumpUrl: toTrimmedText(aigc.jumpUrl),
};
-});
+};
+
+const aigcComponetValue = computed(() => getAigcComponetValue(displayValue.value));
+
+const createRenderEntry = (key, value) => {
+ if (isImageValue(value)) {
+ return [createImageEntry(key, value)];
+ }
+
+ if (isArrayValue(value)) {
+ return [{
+ key,
+ type: "list",
+ value: value.filter((item) => hasDisplayValue(item)),
+ }];
+ }
+
+ if (isObjectValue(value)) {
+ return getRenderEntriesForObject(value, key);
+ }
+
+ return [{
+ key,
+ type: "text",
+ value: formatLeafValue(value),
+ }];
+};
+
+const getRenderEntriesForObject = (value = {}, parentKey = "") => {
+ if (!isObjectValue(value)) return [];
+
+ return Object.keys(value)
+ .filter((key) => hasDisplayValue(value[key]))
+ .reduce((entries, key) => {
+ const entryKey = parentKey ? `${parentKey}-${key}` : key;
+ return entries.concat(createRenderEntry(entryKey, value[key]));
+ }, []);
+};
const shouldRenderContentImage = computed(() => {
return isContentImageField.value && !!contentImageUrl.value;
@@ -345,6 +391,7 @@ const shouldRenderAigcComponet = computed(() => {
const renderFieldEntries = computed(() => {
if (isIgnoredField.value || !hasDisplayValue(props.value)) return [];
const value = displayValue.value;
+
if (isImageValue(value)) {
return [createImageEntry(props.fieldKey, value)];
}
@@ -358,26 +405,7 @@ const renderFieldEntries = computed(() => {
}
if (isObjectValue(value)) {
- return Object.keys(value)
- .filter((key) => hasDisplayValue(value[key]))
- .map((key) => {
- const entryValue = value[key];
- if (isImageValue(entryValue)) {
- return createImageEntry(key, entryValue);
- }
- if (isArrayValue(entryValue)) {
- return {
- key,
- type: "list",
- value: entryValue.filter((item) => hasDisplayValue(item)),
- };
- }
- return {
- key,
- type: "text",
- value: formatLeafValue(entryValue),
- };
- });
+ return getRenderEntriesForObject(value);
}
return [{
diff --git a/src/pages/ChatMain/ChatLongAnswer/index.vue b/src/pages/ChatMain/ChatLongAnswer/index.vue
index deab239..dc4783e 100644
--- a/src/pages/ChatMain/ChatLongAnswer/index.vue
+++ b/src/pages/ChatMain/ChatLongAnswer/index.vue
@@ -24,8 +24,6 @@
:value="section.parsedValue !== null ? section.parsedValue : section.contentValue"
/>
-
-
@@ -65,16 +63,13 @@ const longAnswerTagStyle = computed(() => buildTagToneStyle(longAnswerTagColor.v
let unsubscribe = null;
-const HIDDEN_DETAIL_SECTION_KEYS = [
- LONG_TEXT_KEYS.containerType,
-];
+const HIDDEN_DETAIL_SECTION_KEYS = [];
const shouldUseParsedValueView = (section) => {
return (
section.fromLongTextData &&
section.contentKey !== LONG_TEXT_KEYS.tag &&
section.contentKey !== LONG_TEXT_KEYS.title &&
- section.contentKey !== LONG_TEXT_KEYS.content &&
!HIDDEN_DETAIL_SECTION_KEYS.includes(section.contentKey)
);
};
@@ -227,7 +222,7 @@ onLoad(({ message = "", streamId = "", finished = "0", tagToneColor = "" }) => {
answerText.value = text || "";
longTextData.value = payload || null;
title.value = computeTitle(
- getLongTextValue(longTextData.value, "title") || answerText.value
+ getLongTextValue(longTextData.value, LONG_TEXT_KEYS.title) || answerText.value
);
nextTick(() => {
diff --git a/src/pages/ChatModule/AnswerComponent/index.vue b/src/pages/ChatModule/AnswerComponent/index.vue
index c775993..e49340f 100644
--- a/src/pages/ChatModule/AnswerComponent/index.vue
+++ b/src/pages/ChatModule/AnswerComponent/index.vue
@@ -33,6 +33,7 @@ import ChatMarkdown from "../../ChatMain/ChatMarkdown/index.vue";
import ChatLoading from "../../ChatMain/ChatLoading/index.vue";
import StreamManager from '@/utils/StreamManager.js';
import {
+ LONG_TEXT_KEYS,
getLongTextPreviewText,
getLongTextValue,
hasLongTextExtraSections,
@@ -55,12 +56,12 @@ const props = defineProps({
},
});
-const tag = computed(() => getLongTextValue(props.longTextData, "tag"));
-const title = computed(() => getLongTextValue(props.longTextData, "title"));
+const tag = computed(() => getLongTextValue(props.longTextData, LONG_TEXT_KEYS.tag));
+const title = computed(() => getLongTextValue(props.longTextData, LONG_TEXT_KEYS.title));
const longAnswerTagColor = ref(pickRandomTagToneColor());
const longAnswerTagStyle = computed(() => buildTagToneStyle(longAnswerTagColor.value));
const previewContent = computed(() => {
- return getLongTextPreviewText(props.longTextData, ["content_summary"]);
+ return getLongTextPreviewText(props.longTextData, [LONG_TEXT_KEYS.contentSummary]);
});
// 处理文本内容:按行截断以保证预览最多显示两行(更贴近视觉行数)
diff --git a/src/utils/longTextCard.js b/src/utils/longTextCard.js
index 2c846e7..a76f3f4 100644
--- a/src/utils/longTextCard.js
+++ b/src/utils/longTextCard.js
@@ -1,127 +1,56 @@
export const LONG_TEXT_KEYS = {
- containerType: "container_type",
tag: "tag",
title: "title",
- content: "content",
contentSummary: "content_summary",
- guideConclusion: "guide_conclusion",
- keyFacts: "key_facts",
sceneImage: "scene_image",
- photoSpotSectionTitle: "photo_spot_section_title",
- photoSpotSectionItems: "photo_spot_section_items",
- bestTimeSuggestion: "best_time_suggestion",
- phoneSectionTitle: "phone_section_title",
- phoneSectionItems: "phone_section_items",
- photoList: "photo_list",
- aigcComponet: "aigc_componet",
- preparationSectionTitle: "preparation_section_title",
- preparationSectionItems: "preparation_section_items",
- sectionSuggestionTitle: "section_suggestion_title",
- sectionSuggestionContent: "section_suggestion_content",
- pitfallSectionTitle: "pitfall_section_title",
- pitfallSectionItems: "pitfall_section_items",
+ preparationSection: "preparation_section",
+ sectionSuggestion: "section_suggestion",
+ pitfallSection: "pitfall_section",
commodityList: "commodity_list",
contentImage: "content_image",
- viewSectionTitle: "view_section_title",
- viewSectionItems: "view_section_items",
- suggestionSectionTitle: "suggestion_section_title",
- suggestionSectionContent: "suggestion_section_content",
- lightReminderTitle: "light_reminder_title",
- lightReminderItems: "light_reminder_items",
- checklistOrSteps: "checklist_or_steps",
- bestTimeOrPeople: "best_time_or_people",
- avoidPitfalls: "avoid_pitfalls",
- nextSuggestion: "next_suggestion",
- poiDefinition: "poi_definition",
- keyHighlights: "key_highlights",
- heroImage: "hero_image",
- backgroundStory: "background_story",
- bestTime: "best_time",
- visitSuggestion: "visit_suggestion",
- routeSummary: "route_summary",
- routeMeta: "route_meta",
- routeSteps: "route_steps",
- onsiteClues: "onsite_clues",
- realtimeNotice: "realtime_notice",
- routeWarning: "route_warning",
- arrivalNextStep: "arrival_next_step",
- photoConclusion: "photo_conclusion",
- photoSpots: "photo_spots",
- compositionTips: "composition_tips",
- phoneSettings: "phone_settings",
+ viewSection: "view_section",
+ suggestionSection: "suggestion_section",
lightReminder: "light_reminder",
- sampleImage: "sample_image",
- components: "components",
- checklist: "checklist",
- suggest: "suggest",
- commodity: "commodity",
- actionZone: "action_zone",
spotLocate: "spot_locate",
+ photoSpotSection: "photo_spot_section",
+ phoneSection: "phone_section",
+ photoList: "photo_list",
+ aigcComponet: "aigc_componet",
+ decisionSection: "decision_section",
+ routeWarning: "route_warning",
+ tourRoutes: "tour_routes",
+ facilitiesAlongTheWay: "facilities_along_the_way",
questionSuggest: "question_suggest",
};
export const LONG_TEXT_FIELD_CONFIG = [
- { key: LONG_TEXT_KEYS.containerType },
{ key: LONG_TEXT_KEYS.tag },
{ key: LONG_TEXT_KEYS.title },
- { key: LONG_TEXT_KEYS.content },
{ key: LONG_TEXT_KEYS.contentSummary },
- { key: LONG_TEXT_KEYS.guideConclusion },
- { key: LONG_TEXT_KEYS.keyFacts },
{ key: LONG_TEXT_KEYS.sceneImage },
- { key: LONG_TEXT_KEYS.photoSpotSectionTitle },
- { key: LONG_TEXT_KEYS.photoSpotSectionItems },
- { key: LONG_TEXT_KEYS.bestTimeSuggestion },
- { key: LONG_TEXT_KEYS.phoneSectionTitle },
- { key: LONG_TEXT_KEYS.phoneSectionItems },
- { key: LONG_TEXT_KEYS.photoList },
- { key: LONG_TEXT_KEYS.aigcComponet },
- { key: LONG_TEXT_KEYS.preparationSectionTitle },
- { key: LONG_TEXT_KEYS.preparationSectionItems },
- { key: LONG_TEXT_KEYS.sectionSuggestionTitle },
- { key: LONG_TEXT_KEYS.sectionSuggestionContent },
- { key: LONG_TEXT_KEYS.pitfallSectionTitle },
- { key: LONG_TEXT_KEYS.pitfallSectionItems },
+ { key: LONG_TEXT_KEYS.preparationSection },
+ { key: LONG_TEXT_KEYS.sectionSuggestion },
+ { key: LONG_TEXT_KEYS.pitfallSection },
{ key: LONG_TEXT_KEYS.commodityList },
{ key: LONG_TEXT_KEYS.contentImage },
- { key: LONG_TEXT_KEYS.viewSectionTitle },
- { key: LONG_TEXT_KEYS.viewSectionItems },
- { key: LONG_TEXT_KEYS.suggestionSectionTitle },
- { key: LONG_TEXT_KEYS.suggestionSectionContent },
- { key: LONG_TEXT_KEYS.lightReminderTitle },
- { key: LONG_TEXT_KEYS.lightReminderItems },
- { key: LONG_TEXT_KEYS.checklistOrSteps },
- { key: LONG_TEXT_KEYS.bestTimeOrPeople },
- { key: LONG_TEXT_KEYS.avoidPitfalls },
- { key: LONG_TEXT_KEYS.nextSuggestion },
- { key: LONG_TEXT_KEYS.poiDefinition },
- { key: LONG_TEXT_KEYS.keyHighlights },
- { key: LONG_TEXT_KEYS.heroImage },
- { key: LONG_TEXT_KEYS.backgroundStory },
- { key: LONG_TEXT_KEYS.bestTime },
- { key: LONG_TEXT_KEYS.visitSuggestion },
- { key: LONG_TEXT_KEYS.routeSummary },
- { key: LONG_TEXT_KEYS.routeMeta },
- { key: LONG_TEXT_KEYS.routeSteps },
- { key: LONG_TEXT_KEYS.onsiteClues },
- { key: LONG_TEXT_KEYS.realtimeNotice },
- { key: LONG_TEXT_KEYS.routeWarning },
- { key: LONG_TEXT_KEYS.arrivalNextStep },
- { key: LONG_TEXT_KEYS.photoConclusion },
- { key: LONG_TEXT_KEYS.photoSpots },
- { key: LONG_TEXT_KEYS.compositionTips },
- { key: LONG_TEXT_KEYS.phoneSettings },
+ { key: LONG_TEXT_KEYS.viewSection },
+ { key: LONG_TEXT_KEYS.suggestionSection },
{ key: LONG_TEXT_KEYS.lightReminder },
- { key: LONG_TEXT_KEYS.sampleImage },
- { key: LONG_TEXT_KEYS.components },
- { key: LONG_TEXT_KEYS.checklist },
- { key: LONG_TEXT_KEYS.suggest },
- { key: LONG_TEXT_KEYS.commodity },
- { key: LONG_TEXT_KEYS.actionZone },
{ key: LONG_TEXT_KEYS.spotLocate },
+ { key: LONG_TEXT_KEYS.photoSpotSection },
+ { key: LONG_TEXT_KEYS.phoneSection },
+ { key: LONG_TEXT_KEYS.photoList },
+ { key: LONG_TEXT_KEYS.aigcComponet },
+ { key: LONG_TEXT_KEYS.decisionSection },
+ { key: LONG_TEXT_KEYS.routeWarning },
+ { key: LONG_TEXT_KEYS.tourRoutes },
+ { key: LONG_TEXT_KEYS.facilitiesAlongTheWay },
{ key: LONG_TEXT_KEYS.questionSuggest },
];
+const LONG_TEXT_FIELD_KEYS = LONG_TEXT_FIELD_CONFIG.map((item) => item.key);
+const LONG_TEXT_FIELD_KEY_SET = new Set(LONG_TEXT_FIELD_KEYS);
+
export const LONG_TEXT_PREVIEW_KEYS = [
LONG_TEXT_KEYS.contentSummary,
LONG_TEXT_KEYS.title,
@@ -133,9 +62,21 @@ export const createLongTextData = () => ({
parsedValues: {},
});
+export const isKnownLongTextKey = (key) => {
+ return LONG_TEXT_FIELD_KEY_SET.has(String(key));
+};
+
const toText = (value) => {
if (value === undefined || value === null) return "";
- return typeof value === "string" ? value : String(value);
+ if (typeof value === "string") return value;
+ if (typeof value === "object") {
+ try {
+ return JSON.stringify(value);
+ } catch (e) {
+ return String(value);
+ }
+ }
+ return String(value);
};
const shouldParseJSON = (raw) => {
@@ -167,6 +108,10 @@ const isLongTextObjectValue = (value) => {
return value !== null && typeof value === "object" && !Array.isArray(value);
};
+const hasOwnField = (value, key) => {
+ return Object.prototype.hasOwnProperty.call(value, key);
+};
+
export const parseLongTextDisplayValue = (value) => {
return parseMaybeJSON(value);
};
@@ -218,15 +163,18 @@ export const formatLongTextDisplayValue = (value, ignoredKeys = []) => {
return String(value);
};
-export const appendLongTextChunk = (target, chunk = {}) => {
- if (!target || !chunk.contentKey) return target;
-
- const key = String(chunk.contentKey);
- const value = toText(chunk.contentValue);
+const getChunkContentValue = (chunk = {}) => {
+ if (!isLongTextObjectValue(chunk)) return chunk;
+ if (hasOwnField(chunk, "contentValue")) return chunk.contentValue;
+ if (hasOwnField(chunk, "content")) return chunk.content;
+ return chunk;
+};
+const appendLongTextField = (target, key, rawValue) => {
if (!target.values) target.values = {};
if (!target.parsedValues) target.parsedValues = {};
+ const value = toText(rawValue);
target.values[key] = (target.values[key] || "") + value;
const parsed = tryParseJSON(target.values[key]);
@@ -235,7 +183,12 @@ export const appendLongTextChunk = (target, chunk = {}) => {
} else {
delete target.parsedValues[key];
}
+};
+export const appendLongTextChunk = (target, chunk = {}) => {
+ if (!target || !chunk || !chunk.contentKey) return target;
+
+ appendLongTextField(target, String(chunk.contentKey), getChunkContentValue(chunk));
return target;
};
@@ -260,14 +213,40 @@ export const getLongTextPreviewText = (data, keys = LONG_TEXT_PREVIEW_KEYS) => {
};
export const hasLongTextExtraSections = (data, previewKeys = LONG_TEXT_PREVIEW_KEYS) => {
- if (!data || !data.values) return false;
- return Object.keys(data.values).some((key) => !previewKeys.includes(key));
+ return getLongTextSections(data).some((section) => !previewKeys.includes(section.contentKey));
};
export const getLongTextSections = (data) => {
if (!data || !data.values) return [];
- return Object.keys(data.values)
+ const receivedKeys = Object.keys(data.values);
+ const extraKeysByAnchor = receivedKeys
+ .filter((key) => !LONG_TEXT_FIELD_KEY_SET.has(key))
+ .reduce((result, key) => {
+ const keyIndex = receivedKeys.indexOf(key);
+ const anchorKey = receivedKeys
+ .slice(0, keyIndex)
+ .reverse()
+ .find((item) => LONG_TEXT_FIELD_KEY_SET.has(item)) || "";
+
+ if (!result[anchorKey]) result[anchorKey] = [];
+ result[anchorKey].push(key);
+ return result;
+ }, {});
+
+ const orderedKeys = LONG_TEXT_FIELD_KEYS
+ .filter((key) =>
+ Object.prototype.hasOwnProperty.call(data.values, key)
+ )
+ .reduce((result, key) => {
+ result.push(key);
+ if (extraKeysByAnchor[key]) {
+ result.push(...extraKeysByAnchor[key]);
+ }
+ return result;
+ }, extraKeysByAnchor[""] || []);
+
+ return orderedKeys
.map((key) => ({
contentKey: key,
contentValue: getLongTextValue(data, key),