fix(distribution): 修复分站跑马灯公告把配置 JSON 原文展示出来
分销站公告存在 distribution_sites.announcements_json 的是 { title, content } 对象数组
(分销后台品牌装修写入),而前台跑马灯组件按字符串数组渲染。Vue 对对象会做
JSON.stringify,配合 no-wrap 样式,前台就显示成
{"title":"天域商行专注AWM子弹回收、高比例!不排队!秒结算!","content":""}。
- 新增 shared/utils/announcements.ts:把公告条目归一化为展示文本,兼容纯文本、
{ title, content } 对象,以及被字符串化的对象(历史脏数据)与标题为空回退内容
- distributionSite store 解析 announcements_json 时归一化,跑马灯组件内部再兜底一次
- 首页配置 mergeHomeConfig 同样接受对象形态,主站/移动端公告入口口径统一
- 分销后台装修预览的跑马灯示例按同一口径取值(只有内容时不再空白)
- 补 9 条单测覆盖对象、字符串化对象、普通花括号文案、混排去重与空输入
This commit is contained in:
@@ -1149,7 +1149,7 @@ export default function SiteSettings() {
|
||||
>
|
||||
<NotificationOutlined />
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{announcements[0]?.title}
|
||||
{announcements[0]?.title || announcements[0]?.content}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { apiClient } from '@/shared/api/client'
|
||||
import type { ApiResponse } from '@/shared/types/types'
|
||||
import { normalizeAnnouncements } from '@/shared/utils/announcements'
|
||||
import { mergeListingPublishOptions, type ListingPublishOptions } from './listingOptions'
|
||||
|
||||
export interface HomeBannerSlide {
|
||||
@@ -86,10 +87,8 @@ export async function fetchMobileHomeConfig() {
|
||||
}
|
||||
|
||||
export function mergeHomeConfig(config?: Partial<MobileHomeConfig>): MobileHomeConfig {
|
||||
const announcements =
|
||||
config?.announcements
|
||||
?.map(item => (typeof item === 'string' ? item.trim() : ''))
|
||||
.filter(Boolean) || []
|
||||
// 公告可能是纯文本,也可能是 { title, content } 对象(分销站装修口径),统一归一化。
|
||||
const announcements = normalizeAnnouncements(config?.announcements)
|
||||
const banners =
|
||||
config?.banners
|
||||
?.map(item => {
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Bell } from '@element-plus/icons-vue'
|
||||
import { normalizeAnnouncements, type AnnouncementLike } from '@/shared/utils/announcements'
|
||||
|
||||
interface Props {
|
||||
announcements: string[]
|
||||
// 兼容历史形态:纯文本、{ title, content } 对象,或其中任意来源的混合数组。
|
||||
announcements: Array<string | AnnouncementLike>
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// 组件自身再兜底归一化一次:即使调用方直接传了站点公告对象,也不会渲染出 JSON 原文。
|
||||
const items = computed(() => normalizeAnnouncements(props.announcements))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -18,7 +24,7 @@ defineProps<Props>()
|
||||
:autoplay="true"
|
||||
:interval="3200"
|
||||
>
|
||||
<el-carousel-item v-for="item in announcements" :key="item">
|
||||
<el-carousel-item v-for="item in items" :key="item">
|
||||
<span>{{ item }}</span>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { normalizeAnnouncements, normalizeAnnouncementText } from '../announcements'
|
||||
|
||||
describe('normalizeAnnouncementText', () => {
|
||||
it('保留纯文本公告并去掉首尾空白', () => {
|
||||
expect(normalizeAnnouncementText(' 平台担保交易,拒绝私下转账 ')).toBe(
|
||||
'平台担保交易,拒绝私下转账'
|
||||
)
|
||||
})
|
||||
|
||||
it('对象公告取标题', () => {
|
||||
expect(
|
||||
normalizeAnnouncementText({ title: '天域商行专注AWM子弹回收、高比例!', content: '详情' })
|
||||
).toBe('天域商行专注AWM子弹回收、高比例!')
|
||||
})
|
||||
|
||||
it('对象公告标题为空时回退到内容', () => {
|
||||
expect(normalizeAnnouncementText({ title: ' ', content: ' 详情内容 ' })).toBe('详情内容')
|
||||
})
|
||||
|
||||
it('兼容被字符串化的对象(历史脏数据)', () => {
|
||||
expect(
|
||||
normalizeAnnouncementText('{"title":"天域商行专注AWM子弹回收、高比例! ","content":""}')
|
||||
).toBe('天域商行专注AWM子弹回收、高比例!')
|
||||
})
|
||||
|
||||
it('普通花括号文案不会被当成 JSON 配置吞掉', () => {
|
||||
expect(normalizeAnnouncementText('{今日特价} 全场九折')).toBe('{今日特价} 全场九折')
|
||||
expect(normalizeAnnouncementText('{不完整对象')).toBe('{不完整对象')
|
||||
})
|
||||
|
||||
it('空值与非法类型返回空串', () => {
|
||||
expect(normalizeAnnouncementText('')).toBe('')
|
||||
expect(normalizeAnnouncementText(' ')).toBe('')
|
||||
expect(normalizeAnnouncementText(null)).toBe('')
|
||||
expect(normalizeAnnouncementText(undefined)).toBe('')
|
||||
expect(normalizeAnnouncementText(123)).toBe('')
|
||||
expect(normalizeAnnouncementText({ foo: 'bar' })).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeAnnouncements', () => {
|
||||
it('归一化站点公告对象数组(真实线上数据)', () => {
|
||||
const raw = JSON.parse(
|
||||
'[{"title":"平台全新上线精选三角洲账号,安全畅玩!","content":""},{"title":"天域商行专注AWM子弹回收、高比例!不排队!秒结算!","content":""}]'
|
||||
)
|
||||
expect(normalizeAnnouncements(raw)).toEqual([
|
||||
'平台全新上线精选三角洲账号,安全畅玩!',
|
||||
'天域商行专注AWM子弹回收、高比例!不排队!秒结算!',
|
||||
])
|
||||
})
|
||||
|
||||
it('支持字符串与对象混排并去重', () => {
|
||||
expect(
|
||||
normalizeAnnouncements([
|
||||
'A',
|
||||
{ title: 'B' },
|
||||
'A',
|
||||
{ title: 'B', content: 'B' },
|
||||
' ',
|
||||
{ title: '' },
|
||||
])
|
||||
).toEqual(['A', 'B'])
|
||||
})
|
||||
|
||||
it('非数组输入返回空列表', () => {
|
||||
expect(normalizeAnnouncements(undefined)).toEqual([])
|
||||
expect(normalizeAnnouncements(null)).toEqual([])
|
||||
expect(normalizeAnnouncements('A')).toEqual([])
|
||||
expect(normalizeAnnouncements({ title: 'A' })).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 公告条目的统一归一化。
|
||||
*
|
||||
* 站点公告在系统里有两种历史形态:
|
||||
* - 主站首页配置(systemconfigs.mobile-home-config):纯文本字符串数组
|
||||
* - 分销站品牌装修(distribution_sites.announcements_json):{ title, content } 对象数组
|
||||
*
|
||||
* 跑马灯只展示标题。若把对象直接交给模板渲染,Vue 会对对象做 JSON.stringify,
|
||||
* 前台就会出现 {"title":"…","content":""} 这样的原文(跑马灯样式还会把换行压成一行)。
|
||||
* 因此所有公告入口都先经过这里收敛为可展示文本。
|
||||
*/
|
||||
export interface AnnouncementLike {
|
||||
title?: string
|
||||
content?: string
|
||||
}
|
||||
|
||||
/** 把单个公告条目归一化为展示文本,兼容纯文本、对象与「被字符串化的对象」。 */
|
||||
export function normalizeAnnouncementText(item: unknown): string {
|
||||
if (typeof item === 'string') {
|
||||
const text = item.trim()
|
||||
if (!text) return ''
|
||||
const parsed = parseStringifiedObject(text)
|
||||
return parsed ? normalizeAnnouncementText(parsed) : text
|
||||
}
|
||||
if (item && typeof item === 'object') {
|
||||
const record = item as Record<string, unknown>
|
||||
const title = typeof record.title === 'string' ? record.title.trim() : ''
|
||||
if (title) return title
|
||||
const content = typeof record.content === 'string' ? record.content.trim() : ''
|
||||
return content
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/** 把任意来源的公告列表归一化为去重后的展示文本列表。 */
|
||||
export function normalizeAnnouncements(items: unknown): string[] {
|
||||
if (!Array.isArray(items)) return []
|
||||
const seen = new Set<string>()
|
||||
const result: string[] = []
|
||||
for (const item of items) {
|
||||
const text = normalizeAnnouncementText(item)
|
||||
if (!text || seen.has(text)) continue
|
||||
seen.add(text)
|
||||
result.push(text)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** 只解析「看起来是 JSON 对象」的字符串,避免把普通文案误当 JSON 处理。 */
|
||||
function parseStringifiedObject(text: string): Record<string, unknown> | undefined {
|
||||
if (!text.startsWith('{') || !text.endsWith('}')) return undefined
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(text)
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined
|
||||
const record = parsed as Record<string, unknown>
|
||||
// 仅接受公告结构,避免把用户手写的 {xxx} 文案当成配置吞掉。
|
||||
if (typeof record.title === 'string' || typeof record.content === 'string') return record
|
||||
return undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { apiClient } from '@/shared/api/client'
|
||||
import type { ApiResponse } from '@/shared/types/types'
|
||||
import { normalizeAnnouncements } from '@/shared/utils/announcements'
|
||||
|
||||
export interface DistributionBanner {
|
||||
image_url: string
|
||||
@@ -160,7 +161,9 @@ export const useDistributionSiteStore = defineStore('distributionSite', {
|
||||
|
||||
if (info.announcements_json) {
|
||||
try {
|
||||
this.announcements = JSON.parse(info.announcements_json)
|
||||
// 站点公告存的是 { title, content } 对象数组,跑马灯只认文本,必须先归一化,
|
||||
// 否则 Vue 会把对象 stringify 成 {"title":"…","content":""} 原文展示。
|
||||
this.announcements = normalizeAnnouncements(JSON.parse(info.announcements_json))
|
||||
} catch {
|
||||
this.announcements = []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user