恢复修改的数据
This commit is contained in:
@@ -1,46 +1,262 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { BusinessType } from '@/types/api'
|
||||
import type { Case } from '@/types/business/cases'
|
||||
import type { BusinessLine, HomeStats } from '@/types/business/home'
|
||||
import { getBusinessDetail, getBusinessLines, getFeaturedCases, getHomeStats } from '@/api/home'
|
||||
import type {
|
||||
BannerVo,
|
||||
BusinessLine,
|
||||
HomeStats,
|
||||
ProjectStatusCount,
|
||||
ProjectSubmissionFlat,
|
||||
ProjectSubmissionInfo,
|
||||
ProjectSubmissionVo,
|
||||
ServiceContentBlock,
|
||||
ServiceType,
|
||||
} from '@/types/business/home'
|
||||
import type { PortalEntryGroup, PortalEntryVo } from '@/types/business/home'
|
||||
import {
|
||||
getCarouselList,
|
||||
getPortalEntryList,
|
||||
getProjectStatusCount,
|
||||
getServiceTypeDetail,
|
||||
getServiceTypeList,
|
||||
switchPortalEntryGroup as switchPortalEntryGroupApi,
|
||||
} from '@/api/home'
|
||||
import { getFeaturedCases as getFeaturedCasesApi } from '@/api/cases'
|
||||
import { toCase } from '@/utils/caseVo'
|
||||
import { formatBusinessType, toBusinessType } from '@/utils/format'
|
||||
import { useCasesStore } from '@/stores/modules/cases'
|
||||
|
||||
/**
|
||||
* 首页数据(数据层:取数 + 状态 + 后端 VO → 页面类型映射,页面只调本 store)
|
||||
*
|
||||
* 后端接口:
|
||||
* - UserSideWebHomePageController(前缀 /web/home-page):看板、业务服务线、业务详情
|
||||
* - ProjectCaseUserController 的 /web/projectCase/mostViewed:案例库精选
|
||||
*
|
||||
* 页面/组件消费的类型(BusinessLine / Case / HomeStats)保持不变,
|
||||
* 后端 VO 与页面字段的差异(字段名、业务类型取值、富文本、数值单位)全部在本层对齐。
|
||||
*/
|
||||
|
||||
// 业务类型归一化(toBusinessType)已抽到 `@/utils/format`,首页与案例库共用。
|
||||
|
||||
/** 富文本转纯文本(服务内容区以 <pre> 展示,需去掉 HTML 标签,避免渲染出标签本身) */
|
||||
function stripHtml(html: string): string {
|
||||
return html
|
||||
.replace(/<br\s*\/?>/gi, '\n')
|
||||
.replace(/<\/(p|div|li|h[1-6]|tr)>/gi, '\n')
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.replace(/ /gi, ' ')
|
||||
.replace(/&/gi, '&')
|
||||
.replace(/</gi, '<')
|
||||
.replace(/>/gi, '>')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
}
|
||||
|
||||
/** 后端「项目提报」→ 业务线卡片 / 业务详情 */
|
||||
function toBusinessLine(vo: ProjectSubmissionVo): BusinessLine {
|
||||
const serviceTypes: ServiceType[] = (vo.serviceTypes ?? []).map((item) => ({
|
||||
label: item.serviceName ?? '',
|
||||
desc: '',
|
||||
icon: item.iconUrl ?? '',
|
||||
}))
|
||||
const serviceContent = (vo.backgrounds ?? [])
|
||||
.map((item) => stripHtml(item.content ?? ''))
|
||||
.filter((text) => text.length > 0)
|
||||
.join('\n\n')
|
||||
// 列表接口的「项目背景」富文本按条映射为服务内容区块(保留 HTML 原文,详情页逐块渲染)
|
||||
const contents: ServiceContentBlock[] = (vo.backgrounds ?? [])
|
||||
.map((item) => ({ title: '', content: item.content ?? '' }))
|
||||
.filter((block) => block.content.trim().length > 0)
|
||||
const subtitle = vo.subtitle ?? ''
|
||||
return {
|
||||
id: toBusinessType(vo.businessType),
|
||||
name: vo.title ?? '',
|
||||
desc: subtitle,
|
||||
slogan: subtitle,
|
||||
icon: vo.logoUrl ?? '',
|
||||
serviceContent,
|
||||
contents,
|
||||
serviceTypes,
|
||||
video: '',
|
||||
}
|
||||
}
|
||||
|
||||
/** 后端「业务详情聚合 VO」→ 业务线(主表单对象;服务类型 / 附件 / 富文本目录均为一对多 List,需按数组下钻读取) */
|
||||
function toBusinessLineFromInfo(info: ProjectSubmissionInfo): BusinessLine {
|
||||
const ps = info.projectSubmission ?? ({} as ProjectSubmissionFlat)
|
||||
const serviceTypes: ServiceType[] = (info.projectServiceType ?? []).map((item) => ({
|
||||
label: item.serviceName ?? '',
|
||||
desc: '',
|
||||
icon: item.iconUrl ?? '',
|
||||
}))
|
||||
const serviceContent = (info.projectSubmissionCatalog ?? [])
|
||||
.map((item) => stripHtml(item.content ?? ''))
|
||||
.filter((text) => text.length > 0)
|
||||
.join('\n\n')
|
||||
// 一、二、三…每条富文本目录 → 一个「服务内容」区块(保留 HTML,含内嵌图片/视频)
|
||||
const contents: ServiceContentBlock[] = (info.projectSubmissionCatalog ?? [])
|
||||
.map((item) => ({ title: item.title ?? '', content: item.content ?? '' }))
|
||||
.filter((block) => block.content.trim().length > 0)
|
||||
const subtitle = ps.subtitle ?? ''
|
||||
return {
|
||||
id: toBusinessType(ps.businessType),
|
||||
name: ps.title ?? '',
|
||||
desc: subtitle,
|
||||
slogan: subtitle,
|
||||
icon: ps.logoUrl ?? '',
|
||||
serviceContent,
|
||||
contents,
|
||||
serviceTypes,
|
||||
video: '',
|
||||
}
|
||||
}
|
||||
|
||||
// 后端案例 VO → 页面 Case 的映射已抽到 `@/utils/caseVo`(首页与案例库共用)。
|
||||
|
||||
/** 未登录时后端看板返回 null,这里兜底为零值 */
|
||||
const EMPTY_STATUS: ProjectStatusCount = {
|
||||
projectCount: 0,
|
||||
ongoingProjectCount: 0,
|
||||
pendingProjectCount: 0,
|
||||
}
|
||||
|
||||
/** 首页数据(数据层:取数 + 状态,页面只调本 store) */
|
||||
export const useHomeStore = defineStore('home', () => {
|
||||
/**
|
||||
* 案例类型字典(提报 id → 类型名)由案例库 store 提供。
|
||||
*
|
||||
* <p>案例表 business_type 列存的是提报主键 id,精选案例标签要翻译成名称就得查这份字典,
|
||||
* 故首页与案例库共用同一份(见 casesStore.ensureCategories)。</p>
|
||||
*/
|
||||
const casesStore = useCasesStore()
|
||||
|
||||
const stats = ref<HomeStats>({ pending: 0, inProgress: 0, total: 0 })
|
||||
const banners = ref<BannerVo[]>([])
|
||||
const businessLines = ref<BusinessLine[]>([])
|
||||
const featuredCases = ref<Case[]>([])
|
||||
const currentBusiness = ref<BusinessLine | null>(null)
|
||||
|
||||
/** 首页初始化:看板 + 业务线 + 案例精选 */
|
||||
/** 提报原始列表(内部缓存):业务线卡片与详情共用,详情需按提报主键 id 取数 */
|
||||
const submissions = ref<ProjectSubmissionVo[]>([])
|
||||
|
||||
/** 门户入口原始列表(含快捷 / 其他两组,由 groupType 区分) */
|
||||
const portalEntries = ref<PortalEntryVo[]>([])
|
||||
|
||||
/** 门户入口是否已成功取到(失败时组件回退到内置默认入口) */
|
||||
const portalEntriesLoaded = ref(false)
|
||||
|
||||
/** 快捷入口组(groupType = 1) */
|
||||
const quickEntries = computed(() =>
|
||||
portalEntries.value.filter((item) => item.groupType === '1'),
|
||||
)
|
||||
|
||||
/** 其他入口组(groupType ≠ 1) */
|
||||
const otherEntries = computed(() =>
|
||||
portalEntries.value.filter((item) => item.groupType !== '1'),
|
||||
)
|
||||
|
||||
/**
|
||||
* 拉取当前用户的门户入口。
|
||||
*
|
||||
* <p>该接口对游客(无登录态)与无权限用户不可用,失败时保持入口为空,
|
||||
* 由调用方回退到默认入口,不影响首页其余数据。</p>
|
||||
*/
|
||||
async function fetchPortalEntries(): Promise<void> {
|
||||
try {
|
||||
const list = await getPortalEntryList()
|
||||
portalEntries.value = list ?? []
|
||||
portalEntriesLoaded.value = true
|
||||
} catch {
|
||||
portalEntries.value = []
|
||||
portalEntriesLoaded.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 快捷 / 其他入口互转,成功后重新拉取以同步后端排序 */
|
||||
async function switchEntryGroup(id: number, type: PortalEntryGroup): Promise<void> {
|
||||
await switchPortalEntryGroupApi(id, type)
|
||||
await fetchPortalEntries()
|
||||
}
|
||||
|
||||
/** 确保提报列表已加载(业务详情页可能被直接打开,此时列表尚未取过) */
|
||||
async function ensureSubmissions(): Promise<void> {
|
||||
if (submissions.value.length === 0) {
|
||||
submissions.value = await getServiceTypeList()
|
||||
}
|
||||
}
|
||||
|
||||
/** 首页初始化:看板 + 轮播图 + 业务服务线 + 案例精选 */
|
||||
async function fetchHomeData(): Promise<void> {
|
||||
const [homeStats, lines, cases] = await Promise.all([
|
||||
getHomeStats(),
|
||||
getBusinessLines(),
|
||||
getFeaturedCases(),
|
||||
const [statusCount, submissionList, cases, carouselList] = await Promise.all([
|
||||
getProjectStatusCount(),
|
||||
getServiceTypeList(),
|
||||
getFeaturedCasesApi(),
|
||||
getCarouselList(),
|
||||
// 案例标签需要「提报 id → 类型名」字典,与首页其余数据并行取(幂等,已加载则不发请求)
|
||||
casesStore.ensureCategories(),
|
||||
])
|
||||
stats.value = homeStats
|
||||
businessLines.value = lines
|
||||
featuredCases.value = cases
|
||||
const status = statusCount ?? EMPTY_STATUS
|
||||
stats.value = {
|
||||
pending: status.pendingProjectCount ?? 0,
|
||||
inProgress: status.ongoingProjectCount ?? 0,
|
||||
total: status.projectCount ?? 0,
|
||||
}
|
||||
submissions.value = submissionList
|
||||
businessLines.value = submissionList.map(toBusinessLine)
|
||||
featuredCases.value = cases.map((vo) => toCase(vo, casesStore.typeNameById))
|
||||
banners.value = carouselList ?? []
|
||||
// 入口个性化接口失败已被内部吞掉,不会影响上面的首页数据
|
||||
await fetchPortalEntries()
|
||||
}
|
||||
|
||||
/** 按业务类型切换案例精选 */
|
||||
/**
|
||||
* 按业务类型切换案例精选。
|
||||
*
|
||||
* <p>后端 selectByType 按案例表 business_type 列的**原值**精确匹配,而该列实测存的是
|
||||
* **提报主键 id**,故这里用 BusinessType 名称经 /categories 字典反查提报 id 再下发;
|
||||
* 字典里查不到(提报表未登记该类型)时直接置空,不发无效请求。</p>
|
||||
*/
|
||||
async function fetchFeaturedCases(category: BusinessType | 'all' = 'all'): Promise<void> {
|
||||
featuredCases.value = await getFeaturedCases(category)
|
||||
if (category === 'all') {
|
||||
featuredCases.value = (await getFeaturedCasesApi()).map((vo) => toCase(vo, casesStore.typeNameById))
|
||||
return
|
||||
}
|
||||
await casesStore.ensureCategories()
|
||||
const name = formatBusinessType(category)
|
||||
const typeId = Object.entries(casesStore.typeNameById).find(([, value]) => value === name)?.[0]
|
||||
if (!typeId) {
|
||||
featuredCases.value = []
|
||||
return
|
||||
}
|
||||
featuredCases.value = (await getFeaturedCasesApi(typeId)).map((vo) => toCase(vo, casesStore.typeNameById))
|
||||
}
|
||||
|
||||
/** 业务详情 */
|
||||
/** 业务详情:前端 BusinessType 反查提报主键后取详情 */
|
||||
async function fetchBusinessDetail(type: BusinessType): Promise<void> {
|
||||
currentBusiness.value = await getBusinessDetail(type)
|
||||
await ensureSubmissions()
|
||||
const target = submissions.value.find((item) => toBusinessType(item.businessType) === type)
|
||||
if (!target || target.id == null) {
|
||||
currentBusiness.value = null
|
||||
return
|
||||
}
|
||||
currentBusiness.value = toBusinessLineFromInfo(await getServiceTypeDetail(target.id))
|
||||
}
|
||||
|
||||
return {
|
||||
stats,
|
||||
banners,
|
||||
businessLines,
|
||||
featuredCases,
|
||||
currentBusiness,
|
||||
portalEntries,
|
||||
portalEntriesLoaded,
|
||||
quickEntries,
|
||||
otherEntries,
|
||||
fetchHomeData,
|
||||
fetchFeaturedCases,
|
||||
fetchBusinessDetail,
|
||||
fetchPortalEntries,
|
||||
switchEntryGroup,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user