feat: 组件的数据结构调整
This commit is contained in:
@@ -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"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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 [{
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理文本内容:按行截断以保证预览最多显示两行(更贴近视觉行数)
|
// 处理文本内容:按行截断以保证预览最多显示两行(更贴近视觉行数)
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user