feat: 组件的数据结构调整

This commit is contained in:
2026-06-05 11:35:06 +08:00
parent 1be97986df
commit 3e06ae544b
7 changed files with 575 additions and 231 deletions

View File

@@ -18,69 +18,148 @@ const longTextCardPath = resolve(scriptDir, "../src/utils/longTextCard.js");
const componentSource = readFileSync(componentPath, "utf8"); const componentSource = readFileSync(componentPath, "utf8");
const detailPageSource = readFileSync(detailPagePath, "utf8"); const detailPageSource = readFileSync(detailPagePath, "utf8");
const longTextCardSource = readFileSync(longTextCardPath, "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, longTextCardSource,
/export\s+const\s+normalizeLongTextSpotLocate\s*=/, /normalizeLongText/,
"longTextCard should export normalizeLongTextSpotLocate for malformed spot_locate keys" "longTextCard should not contain field-specific normalize helpers"
); );
assert.match( assert.doesNotMatch(
longTextCardSource, longTextCardSource,
/export\s+const\s+normalizeLongTextQuestionSuggest\s*=/, /spot_longitude|spot_latitude|spot_tag/,
"longTextCard should export normalizeLongTextQuestionSuggest for JSON-string question_suggest values" "longTextCard should not know spot_locate child field names"
); );
assert.match( assert.doesNotMatch(
longTextCardSource, componentSource,
/export\s+const\s+normalizeLongTextContentImage\s*=/, /isGenericObjectSectionField/,
"longTextCard should export normalizeLongTextContentImage for string/object content_image values" "ParsedValueView should use renderFieldEntries instead of a separate generic object branch"
); );
assert.match( assert.match(
componentSource, componentSource,
/entry\.type\s*===\s*["']content-image["']/, /getRenderEntriesForObject/,
"ParsedValueView should render content_image with the dedicated image style" "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( assert.match(
componentSource, 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" "ParsedValueView should render spot_locate with the dedicated POI action card"
); );
assert.match( assert.match(
componentSource, 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" "ParsedValueView should render question_suggest with dedicated FAQ chips"
); );
assert.match(
componentSource,
/getQuestionSuggestItems/,
"ParsedValueView should own question_suggest value shaping"
);
assert.match( assert.match(
componentSource, componentSource,
/sendReply\(question\)/, /sendReply\(question\)/,
"question_suggest chips should send the selected follow-up 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( assert.match(
componentSource, componentSource,
/openMap\(entry\.value\)/, /LONG_TEXT_KEYS\.contentImage\b/,
"spot_locate action card should open the normalized map location" "ParsedValueView should render content_image with the dedicated image style"
);
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 ignoredFieldKeysMatch = componentSource.match(
@@ -102,28 +181,27 @@ const receivedKeys = [
"tag", "tag",
"title", "title",
"content_summary", "content_summary",
"scene_image",
"content_image", "content_image",
"view_section_title", "view_section",
"view_section_items", "suggestion_section",
"suggestion_section_title", "light_reminder",
"suggestion_section_content",
"light_reminder_title",
"light_reminder_items",
"spot_locate", "spot_locate",
"question_suggest", "question_suggest",
]; ];
const longTextData = { const longTextData = createLongTextData();
values: {},
parsedValues: {},
};
receivedKeys.forEach((key) => { 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( assert.deepEqual(
getLongTextSections(longTextData).map((section) => section.contentKey), getLongTextSections(longTextData).map((section) => section.contentKey),
receivedKeys, [...receivedKeys, "view_section_items"],
"long text detail sections should preserve server receive order, including configured fields after extra fields" "long text detail sections should keep configured fields first and preserve unknown contentKey fields"
); );

View File

@@ -42,9 +42,16 @@ for (const snippet of forbiddenCompatibilitySnippets) {
const requiredStrictRenderingSnippets = [ const requiredStrictRenderingSnippets = [
"LONG_TEXT_KEYS.sceneImage", "LONG_TEXT_KEYS.sceneImage",
"LONG_TEXT_KEYS.contentImage",
"LONG_TEXT_KEYS.commodityList", "LONG_TEXT_KEYS.commodityList",
"LONG_TEXT_KEYS.photoList", "LONG_TEXT_KEYS.photoList",
"LONG_TEXT_KEYS.aigcComponet", "LONG_TEXT_KEYS.aigcComponet",
"shouldRenderSpotLocate",
"shouldRenderQuestionSuggest",
"shouldRenderCommodityList",
"shouldRenderPhotoList",
"shouldRenderAigcComponet",
"getRenderEntriesForObject",
"commodity.commodity_id", "commodity.commodity_id",
"commodity.commodity_name", "commodity.commodity_name",
"commodity.commodity_price", "commodity.commodity_price",
@@ -62,9 +69,7 @@ const requiredStrictRenderingSnippets = [
"jumpAigcClick", "jumpAigcClick",
"getAccessToken", "getAccessToken",
"navigateTo", "navigateTo",
"content-body-list-marker", "content-body-list-card",
"entry.value.length > 1",
"formatListMarker(index)",
]; ];
for (const snippet of requiredStrictRenderingSnippets) { 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"); console.log("ParsedValueView strict field checks passed");

View File

@@ -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 moduleUrl = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
const longTextCard = await import(moduleUrl); 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 { const {
LONG_TEXT_FIELD_CONFIG, LONG_TEXT_FIELD_CONFIG,
LONG_TEXT_KEYS, LONG_TEXT_KEYS,
appendLongTextChunk,
createLongTextData,
getLongTextParsedValue,
getLongTextSections,
getLongTextValue,
hasLongTextExtraSections,
parseLongTextDisplayValue, parseLongTextDisplayValue,
sanitizeLongTextDisplayValue, sanitizeLongTextDisplayValue,
hasLongTextDisplayValue, hasLongTextDisplayValue,
formatLongTextDisplayValue, formatLongTextDisplayValue,
} = longTextCard; } = 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( assert.deepEqual(
parseLongTextDisplayValue('[ "see bridge", "hear water" ]'), parseLongTextDisplayValue('[ "see bridge", "hear water" ]'),
["see bridge", "hear water"], ["see bridge", "hear water"],
@@ -22,25 +52,29 @@ assert.deepEqual(
); );
assert.deepEqual( assert.deepEqual(
parseLongTextDisplayValue('{"spot_name":"bridge","spot_longitude:":107.712345}'), parseLongTextDisplayValue('{"spot_name":"bridge","spot_longitude":107.712345}'),
{ spot_name: "bridge", "spot_longitude:": 107.712345 }, { spot_name: "bridge", spot_longitude: 107.712345 },
"serialized objects should be parsed for display" "serialized objects should be parsed for display"
); );
assert.deepEqual( assert.deepEqual(
sanitizeLongTextDisplayValue( sanitizeLongTextDisplayValue(
{ {
content: "hidden", preparation_section: {
components: ["hidden"], preparation_section_title: "keep",
view_section_items: '[ "check piers", "count arches" ]', preparation_section_items: '[ "check piers", "count arches" ]',
},
nested: { nested: {
title: "keep", 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: { nested: {
title: "keep", title: "keep",
}, },
@@ -54,20 +88,28 @@ assert.equal(formatLongTextDisplayValue(true), "\u662f");
assert.equal(formatLongTextDisplayValue({ title: "bridge" }), '{"title":"bridge"}'); assert.equal(formatLongTextDisplayValue({ title: "bridge" }), '{"title":"bridge"}');
const expectedNewKeys = { const expectedNewKeys = {
preparationSectionTitle: "preparation_section_title", tag: "tag",
preparationSectionItems: "preparation_section_items", title: "title",
sectionSuggestionTitle: "section_suggestion_title", contentSummary: "content_summary",
sectionSuggestionContent: "section_suggestion_content", sceneImage: "scene_image",
pitfallSectionTitle: "pitfall_section_title", preparationSection: "preparation_section",
pitfallSectionItems: "pitfall_section_items", sectionSuggestion: "section_suggestion",
pitfallSection: "pitfall_section",
commodityList: "commodity_list", commodityList: "commodity_list",
photoSpotSectionTitle: "photo_spot_section_title", contentImage: "content_image",
photoSpotSectionItems: "photo_spot_section_items", viewSection: "view_section",
bestTimeSuggestion: "best_time_suggestion", suggestionSection: "suggestion_section",
phoneSectionTitle: "phone_section_title", lightReminder: "light_reminder",
phoneSectionItems: "phone_section_items", spotLocate: "spot_locate",
photoSpotSection: "photo_spot_section",
phoneSection: "phone_section",
photoList: "photo_list", photoList: "photo_list",
aigcComponet: "aigc_componet", 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)) { 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"); console.log("longTextCard display helpers passed");

View File

@@ -194,7 +194,7 @@ const contentBodyListCardStyle = computed(() => ({
})); }));
const contentBodyMarkdownColor = computed(() => contentBodyListToneStyle.value.color); const contentBodyMarkdownColor = computed(() => contentBodyListToneStyle.value.color);
const IGNORED_FIELD_KEYS = ["container_type", "content", "components"]; const IGNORED_FIELD_KEYS = [];
/// ======== Value Processing Helpers ======== /// ======== Value Processing Helpers ========
const isArrayValue = (value) => Array.isArray(value); const isArrayValue = (value) => Array.isArray(value);
@@ -239,12 +239,11 @@ const createImageEntry = (key, value) => ({
}, },
}); });
/// ======== Render Logic ======== /// ======== Render Logic ========
const isIgnoredField = computed(() => IGNORED_FIELD_KEYS.includes(props.fieldKey)); const isIgnoredField = computed(() => IGNORED_FIELD_KEYS.includes(props.fieldKey));
const isContentImageField = computed(() => 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 isSpotLocateField = computed(() => props.fieldKey === LONG_TEXT_KEYS.spotLocate);
const isQuestionSuggestField = computed(() => props.fieldKey === LONG_TEXT_KEYS.questionSuggest); const isQuestionSuggestField = computed(() => props.fieldKey === LONG_TEXT_KEYS.questionSuggest);
@@ -260,28 +259,34 @@ const contentImageUrl = computed(() => {
return typeof displayValue.value === "string" ? toTrimmedText(displayValue.value) : ""; return typeof displayValue.value === "string" ? toTrimmedText(displayValue.value) : "";
}); });
const spotLocateValue = computed(() => { const getSpotLocateValue = (value) => {
const value = isObjectValue(displayValue.value) ? displayValue.value : {}; const spot = isObjectValue(value) ? value : {};
return { return {
name: toTrimmedText(value.spot_name), name: toTrimmedText(spot.spot_name),
description: toTrimmedText(value.spot_description), description: toTrimmedText(spot.spot_description),
longitude: toFiniteNumber(value.spot_longitude), longitude: toFiniteNumber(spot.spot_longitude),
latitude: toFiniteNumber(value.spot_latitude), latitude: toFiniteNumber(spot.spot_latitude),
tag: toTrimmedText(value.spot_tag), tag: toTrimmedText(spot.spot_tag),
}; };
};
const spotLocateValue = computed(() => {
return getSpotLocateValue(displayValue.value);
}); });
const questionSuggestItems = computed(() => { const getQuestionSuggestItems = (value) => {
return isArrayValue(displayValue.value) return isArrayValue(value)
? displayValue.value.map((item) => toTrimmedText(item)).filter(Boolean) ? value.map((item) => toTrimmedText(item)).filter(Boolean)
: []; : [];
}); };
const commodityItems = computed(() => { const questionSuggestItems = computed(() => getQuestionSuggestItems(displayValue.value));
if (!isArrayValue(displayValue.value)) return [];
return displayValue.value const getCommodityItems = (value) => {
if (!isArrayValue(value)) return [];
return value
.filter((item) => isObjectValue(item)) .filter((item) => isObjectValue(item))
.map((commodity) => ({ .map((commodity) => ({
commodity_id: toTrimmedText(commodity.commodity_id), commodity_id: toTrimmedText(commodity.commodity_id),
@@ -291,12 +296,14 @@ const commodityItems = computed(() => {
commodity_photo: toTrimmedText(commodity.commodity_photo), commodity_photo: toTrimmedText(commodity.commodity_photo),
})) }))
.filter((commodity) => hasDisplayValue(commodity)); .filter((commodity) => hasDisplayValue(commodity));
}); };
const photoItems = computed(() => { const commodityItems = computed(() => getCommodityItems(displayValue.value));
if (!isArrayValue(displayValue.value)) return [];
return displayValue.value const getPhotoItems = (value) => {
if (!isArrayValue(value)) return [];
return value
.filter((item) => isObjectValue(item)) .filter((item) => isObjectValue(item))
.map((photo) => ({ .map((photo) => ({
photo_name: toTrimmedText(photo.photo_name), photo_name: toTrimmedText(photo.photo_name),
@@ -304,10 +311,12 @@ const photoItems = computed(() => {
photo_url: toTrimmedText(photo.photo_url), photo_url: toTrimmedText(photo.photo_url),
})) }))
.filter((photo) => !!photo.photo_url); .filter((photo) => !!photo.photo_url);
}); };
const aigcComponetValue = computed(() => { const photoItems = computed(() => getPhotoItems(displayValue.value));
const aigc = isObjectValue(displayValue.value) ? displayValue.value : {};
const getAigcComponetValue = (value) => {
const aigc = isObjectValue(value) ? value : {};
return { return {
background: toTrimmedText(aigc.background), background: toTrimmedText(aigc.background),
@@ -315,7 +324,44 @@ const aigcComponetValue = computed(() => {
description: toTrimmedText(aigc.description), description: toTrimmedText(aigc.description),
jumpUrl: toTrimmedText(aigc.jumpUrl), 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(() => { const shouldRenderContentImage = computed(() => {
return isContentImageField.value && !!contentImageUrl.value; return isContentImageField.value && !!contentImageUrl.value;
@@ -345,6 +391,7 @@ const shouldRenderAigcComponet = computed(() => {
const renderFieldEntries = computed(() => { const renderFieldEntries = computed(() => {
if (isIgnoredField.value || !hasDisplayValue(props.value)) return []; if (isIgnoredField.value || !hasDisplayValue(props.value)) return [];
const value = displayValue.value; const value = displayValue.value;
if (isImageValue(value)) { if (isImageValue(value)) {
return [createImageEntry(props.fieldKey, value)]; return [createImageEntry(props.fieldKey, value)];
} }
@@ -358,26 +405,7 @@ const renderFieldEntries = computed(() => {
} }
if (isObjectValue(value)) { if (isObjectValue(value)) {
return Object.keys(value) return getRenderEntriesForObject(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 [{ return [{

View File

@@ -24,8 +24,6 @@
:value="section.parsedValue !== null ? section.parsedValue : section.contentValue" :value="section.parsedValue !== null ? section.parsedValue : section.contentValue"
/> />
<ChatMarkdown v-else-if="section.contentKey === LONG_TEXT_KEYS.content" :text="section.contentValue" />
<ChatMarkdown v-else :text="section.contentValue" /> <ChatMarkdown v-else :text="section.contentValue" />
</template> </template>
@@ -65,16 +63,13 @@ const longAnswerTagStyle = computed(() => buildTagToneStyle(longAnswerTagColor.v
let unsubscribe = null; let unsubscribe = null;
const HIDDEN_DETAIL_SECTION_KEYS = [ const HIDDEN_DETAIL_SECTION_KEYS = [];
LONG_TEXT_KEYS.containerType,
];
const shouldUseParsedValueView = (section) => { const shouldUseParsedValueView = (section) => {
return ( return (
section.fromLongTextData && section.fromLongTextData &&
section.contentKey !== LONG_TEXT_KEYS.tag && section.contentKey !== LONG_TEXT_KEYS.tag &&
section.contentKey !== LONG_TEXT_KEYS.title && section.contentKey !== LONG_TEXT_KEYS.title &&
section.contentKey !== LONG_TEXT_KEYS.content &&
!HIDDEN_DETAIL_SECTION_KEYS.includes(section.contentKey) !HIDDEN_DETAIL_SECTION_KEYS.includes(section.contentKey)
); );
}; };
@@ -227,7 +222,7 @@ onLoad(({ message = "", streamId = "", finished = "0", tagToneColor = "" }) => {
answerText.value = text || ""; answerText.value = text || "";
longTextData.value = payload || null; longTextData.value = payload || null;
title.value = computeTitle( title.value = computeTitle(
getLongTextValue(longTextData.value, "title") || answerText.value getLongTextValue(longTextData.value, LONG_TEXT_KEYS.title) || answerText.value
); );
nextTick(() => { nextTick(() => {

View File

@@ -33,6 +33,7 @@ import ChatMarkdown from "../../ChatMain/ChatMarkdown/index.vue";
import ChatLoading from "../../ChatMain/ChatLoading/index.vue"; import ChatLoading from "../../ChatMain/ChatLoading/index.vue";
import StreamManager from '@/utils/StreamManager.js'; import StreamManager from '@/utils/StreamManager.js';
import { import {
LONG_TEXT_KEYS,
getLongTextPreviewText, getLongTextPreviewText,
getLongTextValue, getLongTextValue,
hasLongTextExtraSections, hasLongTextExtraSections,
@@ -55,12 +56,12 @@ const props = defineProps({
}, },
}); });
const tag = computed(() => getLongTextValue(props.longTextData, "tag")); const tag = computed(() => getLongTextValue(props.longTextData, LONG_TEXT_KEYS.tag));
const title = computed(() => getLongTextValue(props.longTextData, "title")); const title = computed(() => getLongTextValue(props.longTextData, LONG_TEXT_KEYS.title));
const longAnswerTagColor = ref(pickRandomTagToneColor()); const longAnswerTagColor = ref(pickRandomTagToneColor());
const longAnswerTagStyle = computed(() => buildTagToneStyle(longAnswerTagColor.value)); const longAnswerTagStyle = computed(() => buildTagToneStyle(longAnswerTagColor.value));
const previewContent = computed(() => { const previewContent = computed(() => {
return getLongTextPreviewText(props.longTextData, ["content_summary"]); return getLongTextPreviewText(props.longTextData, [LONG_TEXT_KEYS.contentSummary]);
}); });
// 处理文本内容:按行截断以保证预览最多显示两行(更贴近视觉行数) // 处理文本内容:按行截断以保证预览最多显示两行(更贴近视觉行数)

View File

@@ -1,127 +1,56 @@
export const LONG_TEXT_KEYS = { export const LONG_TEXT_KEYS = {
containerType: "container_type",
tag: "tag", tag: "tag",
title: "title", title: "title",
content: "content",
contentSummary: "content_summary", contentSummary: "content_summary",
guideConclusion: "guide_conclusion",
keyFacts: "key_facts",
sceneImage: "scene_image", sceneImage: "scene_image",
photoSpotSectionTitle: "photo_spot_section_title", preparationSection: "preparation_section",
photoSpotSectionItems: "photo_spot_section_items", sectionSuggestion: "section_suggestion",
bestTimeSuggestion: "best_time_suggestion", pitfallSection: "pitfall_section",
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",
commodityList: "commodity_list", commodityList: "commodity_list",
contentImage: "content_image", contentImage: "content_image",
viewSectionTitle: "view_section_title", viewSection: "view_section",
viewSectionItems: "view_section_items", suggestionSection: "suggestion_section",
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",
lightReminder: "light_reminder", lightReminder: "light_reminder",
sampleImage: "sample_image",
components: "components",
checklist: "checklist",
suggest: "suggest",
commodity: "commodity",
actionZone: "action_zone",
spotLocate: "spot_locate", 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", questionSuggest: "question_suggest",
}; };
export const LONG_TEXT_FIELD_CONFIG = [ export const LONG_TEXT_FIELD_CONFIG = [
{ key: LONG_TEXT_KEYS.containerType },
{ key: LONG_TEXT_KEYS.tag }, { key: LONG_TEXT_KEYS.tag },
{ key: LONG_TEXT_KEYS.title }, { key: LONG_TEXT_KEYS.title },
{ key: LONG_TEXT_KEYS.content },
{ key: LONG_TEXT_KEYS.contentSummary }, { key: LONG_TEXT_KEYS.contentSummary },
{ key: LONG_TEXT_KEYS.guideConclusion },
{ key: LONG_TEXT_KEYS.keyFacts },
{ key: LONG_TEXT_KEYS.sceneImage }, { key: LONG_TEXT_KEYS.sceneImage },
{ key: LONG_TEXT_KEYS.photoSpotSectionTitle }, { key: LONG_TEXT_KEYS.preparationSection },
{ key: LONG_TEXT_KEYS.photoSpotSectionItems }, { key: LONG_TEXT_KEYS.sectionSuggestion },
{ key: LONG_TEXT_KEYS.bestTimeSuggestion }, { key: LONG_TEXT_KEYS.pitfallSection },
{ 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.commodityList }, { key: LONG_TEXT_KEYS.commodityList },
{ key: LONG_TEXT_KEYS.contentImage }, { key: LONG_TEXT_KEYS.contentImage },
{ key: LONG_TEXT_KEYS.viewSectionTitle }, { key: LONG_TEXT_KEYS.viewSection },
{ key: LONG_TEXT_KEYS.viewSectionItems }, { key: LONG_TEXT_KEYS.suggestionSection },
{ 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.lightReminder }, { 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.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 }, { 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 = [ export const LONG_TEXT_PREVIEW_KEYS = [
LONG_TEXT_KEYS.contentSummary, LONG_TEXT_KEYS.contentSummary,
LONG_TEXT_KEYS.title, LONG_TEXT_KEYS.title,
@@ -133,9 +62,21 @@ export const createLongTextData = () => ({
parsedValues: {}, parsedValues: {},
}); });
export const isKnownLongTextKey = (key) => {
return LONG_TEXT_FIELD_KEY_SET.has(String(key));
};
const toText = (value) => { const toText = (value) => {
if (value === undefined || value === null) return ""; 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) => { const shouldParseJSON = (raw) => {
@@ -167,6 +108,10 @@ const isLongTextObjectValue = (value) => {
return value !== null && typeof value === "object" && !Array.isArray(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) => { export const parseLongTextDisplayValue = (value) => {
return parseMaybeJSON(value); return parseMaybeJSON(value);
}; };
@@ -218,15 +163,18 @@ export const formatLongTextDisplayValue = (value, ignoredKeys = []) => {
return String(value); return String(value);
}; };
export const appendLongTextChunk = (target, chunk = {}) => { const getChunkContentValue = (chunk = {}) => {
if (!target || !chunk.contentKey) return target; if (!isLongTextObjectValue(chunk)) return chunk;
if (hasOwnField(chunk, "contentValue")) return chunk.contentValue;
const key = String(chunk.contentKey); if (hasOwnField(chunk, "content")) return chunk.content;
const value = toText(chunk.contentValue); return chunk;
};
const appendLongTextField = (target, key, rawValue) => {
if (!target.values) target.values = {}; if (!target.values) target.values = {};
if (!target.parsedValues) target.parsedValues = {}; if (!target.parsedValues) target.parsedValues = {};
const value = toText(rawValue);
target.values[key] = (target.values[key] || "") + value; target.values[key] = (target.values[key] || "") + value;
const parsed = tryParseJSON(target.values[key]); const parsed = tryParseJSON(target.values[key]);
@@ -235,7 +183,12 @@ export const appendLongTextChunk = (target, chunk = {}) => {
} else { } else {
delete target.parsedValues[key]; 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; return target;
}; };
@@ -260,14 +213,40 @@ export const getLongTextPreviewText = (data, keys = LONG_TEXT_PREVIEW_KEYS) => {
}; };
export const hasLongTextExtraSections = (data, previewKeys = LONG_TEXT_PREVIEW_KEYS) => { export const hasLongTextExtraSections = (data, previewKeys = LONG_TEXT_PREVIEW_KEYS) => {
if (!data || !data.values) return false; return getLongTextSections(data).some((section) => !previewKeys.includes(section.contentKey));
return Object.keys(data.values).some((key) => !previewKeys.includes(key));
}; };
export const getLongTextSections = (data) => { export const getLongTextSections = (data) => {
if (!data || !data.values) return []; 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) => ({ .map((key) => ({
contentKey: key, contentKey: key,
contentValue: getLongTextValue(data, key), contentValue: getLongTextValue(data, key),