feat: 增加商品组件
This commit is contained in:
@@ -40,4 +40,25 @@ for (const snippet of forbiddenCompatibilitySnippets) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requiredStrictRenderingSnippets = [
|
||||||
|
"LONG_TEXT_KEYS.sceneImage",
|
||||||
|
"LONG_TEXT_KEYS.commodityList",
|
||||||
|
"commodity.commodity_id",
|
||||||
|
"commodity.commodity_name",
|
||||||
|
"commodity.commodity_price",
|
||||||
|
"commodity.commodity_tag",
|
||||||
|
"commodity.commodity_photo",
|
||||||
|
"content-body-list-marker",
|
||||||
|
"entry.value.length > 1",
|
||||||
|
"formatListMarker(index)",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const snippet of requiredStrictRenderingSnippets) {
|
||||||
|
assert.equal(
|
||||||
|
source.includes(snippet),
|
||||||
|
true,
|
||||||
|
`ParsedValueView should render exact strict field: ${snippet}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("ParsedValueView strict field checks passed");
|
console.log("ParsedValueView strict field checks passed");
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ const moduleUrl = `data:text/javascript;base64,${Buffer.from(source).toString("b
|
|||||||
const longTextCard = await import(moduleUrl);
|
const longTextCard = await import(moduleUrl);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
LONG_TEXT_FIELD_CONFIG,
|
||||||
|
LONG_TEXT_KEYS,
|
||||||
parseLongTextDisplayValue,
|
parseLongTextDisplayValue,
|
||||||
sanitizeLongTextDisplayValue,
|
sanitizeLongTextDisplayValue,
|
||||||
hasLongTextDisplayValue,
|
hasLongTextDisplayValue,
|
||||||
@@ -51,4 +53,23 @@ assert.equal(hasLongTextDisplayValue({ a: "", b: [" ", null] }), false);
|
|||||||
assert.equal(formatLongTextDisplayValue(true), "\u662f");
|
assert.equal(formatLongTextDisplayValue(true), "\u662f");
|
||||||
assert.equal(formatLongTextDisplayValue({ title: "bridge" }), '{"title":"bridge"}');
|
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",
|
||||||
|
commodityList: "commodity_list",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [keyName, keyValue] of Object.entries(expectedNewKeys)) {
|
||||||
|
assert.equal(LONG_TEXT_KEYS[keyName], keyValue, `${keyName} should be registered`);
|
||||||
|
assert.equal(
|
||||||
|
LONG_TEXT_FIELD_CONFIG.some((item) => item.key === keyValue),
|
||||||
|
true,
|
||||||
|
`${keyValue} should be in LONG_TEXT_FIELD_CONFIG`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("longTextCard display helpers passed");
|
console.log("longTextCard display helpers passed");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<view v-if="shouldRenderField" class="parsed-value">
|
<view v-if="shouldRenderField" class="parsed-value">
|
||||||
<image v-if="shouldRenderContentImage" class="content-body-image" :src="contentImageUrl"
|
<image v-if="shouldRenderContentImage" class="content-body-image content-body-image-bottom" :src="contentImageUrl"
|
||||||
mode="widthFix" @click="handlePreviewClick(contentImageUrl)" />
|
mode="widthFix" @click="handlePreviewClick(contentImageUrl)" />
|
||||||
|
|
||||||
<template v-else-if="shouldRenderSpotLocate">
|
<template v-else-if="shouldRenderSpotLocate">
|
||||||
@@ -26,6 +26,47 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="shouldRenderCommodityList">
|
||||||
|
<view class="detail-action-zone">
|
||||||
|
<view class="detail-action-label">相关票务</view>
|
||||||
|
<scroll-view class="detail-product-scroll" scroll-x>
|
||||||
|
<view class="detail-product-row">
|
||||||
|
<view
|
||||||
|
v-for="commodity in commodityItems"
|
||||||
|
:key="commodity.commodity_id || commodity.commodity_name"
|
||||||
|
class="detail-product-card"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
v-if="commodity.commodity_photo"
|
||||||
|
class="detail-product-image"
|
||||||
|
:src="commodity.commodity_photo"
|
||||||
|
mode="aspectFill"
|
||||||
|
@click="handlePreviewClick(commodity.commodity_photo)"
|
||||||
|
/>
|
||||||
|
<view class="detail-product-body">
|
||||||
|
<view v-if="commodity.commodity_tag" class="detail-product-tag">
|
||||||
|
{{ commodity.commodity_tag }}
|
||||||
|
</view>
|
||||||
|
<view v-if="commodity.commodity_name" class="detail-product-name">
|
||||||
|
{{ commodity.commodity_name }}
|
||||||
|
</view>
|
||||||
|
<view v-if="commodity.commodity_price" class="detail-product-price">
|
||||||
|
<text class="detail-product-currency">¥</text>{{ commodity.commodity_price }}
|
||||||
|
</view>
|
||||||
|
<button
|
||||||
|
v-if="commodity.commodity_id"
|
||||||
|
class="detail-buy-button"
|
||||||
|
@click.stop="openCommodityDetail(commodity)"
|
||||||
|
>
|
||||||
|
立即购买
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<template v-for="entry in renderFieldEntries" :key="entry.key">
|
<template v-for="entry in renderFieldEntries" :key="entry.key">
|
||||||
<view v-if="entry.type === 'text'" class="content-body-text">
|
<view v-if="entry.type === 'text'" class="content-body-text">
|
||||||
@@ -42,6 +83,9 @@
|
|||||||
|
|
||||||
<view v-else-if="entry.type === 'list'" class="content-body-list-card" :style="contentBodyListCardStyle">
|
<view v-else-if="entry.type === 'list'" class="content-body-list-card" :style="contentBodyListCardStyle">
|
||||||
<view v-for="(item, index) in entry.value" :key="index" class="content-body-list-item">
|
<view v-for="(item, index) in entry.value" :key="index" class="content-body-list-item">
|
||||||
|
<view v-if="entry.value.length > 1" class="content-body-list-marker" :style="contentBodyListTextStyle">
|
||||||
|
{{ formatListMarker(index) }}
|
||||||
|
</view>
|
||||||
<view class="content-body-list-text" :style="contentBodyListTextStyle">
|
<view class="content-body-list-text" :style="contentBodyListTextStyle">
|
||||||
{{ formatLeafValue(item) }}
|
{{ formatLeafValue(item) }}
|
||||||
</view>
|
</view>
|
||||||
@@ -104,6 +148,17 @@ const formatLeafValue = (value) => {
|
|||||||
return formatLongTextDisplayValue(value, IGNORED_FIELD_KEYS);
|
return formatLongTextDisplayValue(value, IGNORED_FIELD_KEYS);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const LIST_MARKERS = [
|
||||||
|
"①", "②", "③", "④", "⑤",
|
||||||
|
"⑥", "⑦", "⑧", "⑨", "⑩",
|
||||||
|
"⑪", "⑫", "⑬", "⑭", "⑮",
|
||||||
|
"⑯", "⑰", "⑱", "⑲", "⑳",
|
||||||
|
];
|
||||||
|
|
||||||
|
const formatListMarker = (index) => {
|
||||||
|
return LIST_MARKERS[index] || `${index + 1}.`;
|
||||||
|
};
|
||||||
|
|
||||||
const toTrimmedText = (value) => {
|
const toTrimmedText = (value) => {
|
||||||
if (value === undefined || value === null) return "";
|
if (value === undefined || value === null) return "";
|
||||||
return String(value).trim();
|
return String(value).trim();
|
||||||
@@ -131,9 +186,13 @@ 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(() => props.fieldKey === LONG_TEXT_KEYS.contentImage);
|
const isContentImageField = computed(() =>
|
||||||
|
props.fieldKey === LONG_TEXT_KEYS.contentImage ||
|
||||||
|
props.fieldKey === LONG_TEXT_KEYS.sceneImage
|
||||||
|
);
|
||||||
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);
|
||||||
|
const isCommodityListField = computed(() => props.fieldKey === LONG_TEXT_KEYS.commodityList);
|
||||||
|
|
||||||
const displayValue = computed(() => sanitizeValue(props.value));
|
const displayValue = computed(() => sanitizeValue(props.value));
|
||||||
|
|
||||||
@@ -161,6 +220,21 @@ const questionSuggestItems = computed(() => {
|
|||||||
: [];
|
: [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const commodityItems = computed(() => {
|
||||||
|
if (!isArrayValue(displayValue.value)) return [];
|
||||||
|
|
||||||
|
return displayValue.value
|
||||||
|
.filter((item) => isObjectValue(item))
|
||||||
|
.map((commodity) => ({
|
||||||
|
commodity_id: toTrimmedText(commodity.commodity_id),
|
||||||
|
commodity_name: toTrimmedText(commodity.commodity_name),
|
||||||
|
commodity_price: toTrimmedText(commodity.commodity_price),
|
||||||
|
commodity_tag: toTrimmedText(commodity.commodity_tag),
|
||||||
|
commodity_photo: toTrimmedText(commodity.commodity_photo),
|
||||||
|
}))
|
||||||
|
.filter((commodity) => hasDisplayValue(commodity));
|
||||||
|
});
|
||||||
|
|
||||||
const shouldRenderContentImage = computed(() => {
|
const shouldRenderContentImage = computed(() => {
|
||||||
return isContentImageField.value && !!contentImageUrl.value;
|
return isContentImageField.value && !!contentImageUrl.value;
|
||||||
});
|
});
|
||||||
@@ -173,6 +247,10 @@ const shouldRenderQuestionSuggest = computed(() => {
|
|||||||
return isQuestionSuggestField.value && questionSuggestItems.value.length > 0;
|
return isQuestionSuggestField.value && questionSuggestItems.value.length > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const shouldRenderCommodityList = computed(() => {
|
||||||
|
return isCommodityListField.value && commodityItems.value.length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
/// 其他字段走通用渲染逻辑
|
/// 其他字段走通用渲染逻辑
|
||||||
const renderFieldEntries = computed(() => {
|
const renderFieldEntries = computed(() => {
|
||||||
if (isIgnoredField.value || !hasDisplayValue(props.value)) return [];
|
if (isIgnoredField.value || !hasDisplayValue(props.value)) return [];
|
||||||
@@ -226,7 +304,8 @@ const shouldRenderField = computed(() => {
|
|||||||
if (
|
if (
|
||||||
shouldRenderContentImage.value ||
|
shouldRenderContentImage.value ||
|
||||||
shouldRenderSpotLocate.value ||
|
shouldRenderSpotLocate.value ||
|
||||||
shouldRenderQuestionSuggest.value
|
shouldRenderQuestionSuggest.value ||
|
||||||
|
shouldRenderCommodityList.value
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -265,6 +344,13 @@ const sendReply = (item) => {
|
|||||||
uni.$emit(SEND_MESSAGE_CONTENT_TEXT, item);
|
uni.$emit(SEND_MESSAGE_CONTENT_TEXT, item);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openCommodityDetail = (commodity) => {
|
||||||
|
if (!commodity.commodity_id) return;
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/goods/index?commodityId=${commodity.commodity_id}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handlePreviewClick = (imageUrl) => {
|
const handlePreviewClick = (imageUrl) => {
|
||||||
uni.previewImage({
|
uni.previewImage({
|
||||||
current: imageUrl,
|
current: imageUrl,
|
||||||
|
|||||||
@@ -14,6 +14,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-body-image-bottom {
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-body-image {
|
.content-body-image {
|
||||||
@@ -36,6 +41,7 @@
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-body-list-item {
|
.content-body-list-item {
|
||||||
@@ -43,6 +49,14 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-body-list-marker {
|
||||||
|
width: 22px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.content-body-list-text {
|
.content-body-list-text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
@@ -56,7 +70,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.detail-action-label {
|
.detail-action-label {
|
||||||
padding: 12px 0 12px;
|
padding: 0 0 10px;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
@@ -131,6 +145,87 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-product-scroll {
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-product-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-product-card {
|
||||||
|
width: 220px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #f1f5f9;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 12px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-product-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 110px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-product-body {
|
||||||
|
padding: 10px 12px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-product-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
width: fit-content;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #f43f5e;
|
||||||
|
background: #fef2f2;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-product-name {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #1e293b;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 20px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-product-price {
|
||||||
|
margin: 4px 0 8px;
|
||||||
|
color: #f43f5e;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-product-currency {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-buy-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: #451a03;
|
||||||
|
background: #fbbf24;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-buy-button::after {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-faq-wrap {
|
.detail-faq-wrap {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ export const LONG_TEXT_KEYS = {
|
|||||||
guideConclusion: "guide_conclusion",
|
guideConclusion: "guide_conclusion",
|
||||||
keyFacts: "key_facts",
|
keyFacts: "key_facts",
|
||||||
sceneImage: "scene_image",
|
sceneImage: "scene_image",
|
||||||
|
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",
|
||||||
contentImage: "content_image",
|
contentImage: "content_image",
|
||||||
viewSectionTitle: "view_section_title",
|
viewSectionTitle: "view_section_title",
|
||||||
viewSectionItems: "view_section_items",
|
viewSectionItems: "view_section_items",
|
||||||
@@ -55,6 +62,13 @@ export const LONG_TEXT_FIELD_CONFIG = [
|
|||||||
{ key: LONG_TEXT_KEYS.guideConclusion },
|
{ key: LONG_TEXT_KEYS.guideConclusion },
|
||||||
{ key: LONG_TEXT_KEYS.keyFacts },
|
{ key: LONG_TEXT_KEYS.keyFacts },
|
||||||
{ key: LONG_TEXT_KEYS.sceneImage },
|
{ key: LONG_TEXT_KEYS.sceneImage },
|
||||||
|
{ 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.contentImage },
|
{ key: LONG_TEXT_KEYS.contentImage },
|
||||||
{ key: LONG_TEXT_KEYS.viewSectionTitle },
|
{ key: LONG_TEXT_KEYS.viewSectionTitle },
|
||||||
{ key: LONG_TEXT_KEYS.viewSectionItems },
|
{ key: LONG_TEXT_KEYS.viewSectionItems },
|
||||||
|
|||||||
Reference in New Issue
Block a user