feat(official): 实现企业官网分包及内容展示
- 新增官方分包目录及页面(首页、资讯列表、详情、关于我们、留言、我的) - 自定义实现底部导航组件 OfficialTabBar,解决微信 tabBar 不支持分包问题 - 统一接口调用走 shop-api 代理,无新增 CMS 域名,减少合法域名负担 - 新增 CMS 站点信息获取接口及留言咨询提交与查询接口 - 设计并完善官网富文本样式,品牌色统一为 #185FA5,并更新 tailwind 配置 - 首页新增企业官网入口卡片,方便用户访问官网内容 - 完成官网分包构建验证,解决 Taro 构建批量删除保护问题 - 规范 CMS 字段使用,适配字段名称及状态含义 - 后续计划对历史页面 NutUI 组件进行空壳化整改,及更新 UI 设计文档
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import request from '@/utils/request'
|
||||
import type { ApiResult, PageResult } from '@/api'
|
||||
import type { CmsContactLead, CmsContactLeadParam } from './model'
|
||||
|
||||
/**
|
||||
* 提交联系线索(留言咨询),免登录
|
||||
*/
|
||||
export async function submitCmsContactLead(data: Partial<CmsContactLead>) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/cms/cms-contact-lead/submit',
|
||||
data
|
||||
)
|
||||
if (res.code === 0) {
|
||||
return res.message
|
||||
}
|
||||
return Promise.reject(new Error(res.message))
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询我的联系线索
|
||||
*/
|
||||
export async function pageCmsContactLead(params: CmsContactLeadParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsContactLead>>>(
|
||||
'/cms/cms-contact-lead/page',
|
||||
params
|
||||
)
|
||||
if (res.code === 0) {
|
||||
return res.data
|
||||
}
|
||||
return Promise.reject(new Error(res.message))
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { PageParam } from '@/api'
|
||||
|
||||
/**
|
||||
* 联系线索(留言咨询)
|
||||
*/
|
||||
export interface CmsContactLead {
|
||||
// 线索ID
|
||||
leadId?: number
|
||||
// 称呼
|
||||
name?: string
|
||||
// 手机号
|
||||
phone?: string
|
||||
// 公司
|
||||
company?: string
|
||||
// 邮箱
|
||||
email?: string
|
||||
// 需求描述
|
||||
need?: string
|
||||
// 来源 web|miniapp|app
|
||||
source?: string
|
||||
// 状态 0待跟进 1已回复 2已完成 3已归档
|
||||
status?: number
|
||||
// 租户id
|
||||
tenantId?: number
|
||||
// 创建时间
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 联系线索搜索条件
|
||||
*/
|
||||
export interface CmsContactLeadParam extends PageParam {
|
||||
name?: string
|
||||
phone?: string
|
||||
source?: string
|
||||
}
|
||||
@@ -118,6 +118,19 @@ export async function getCmsWebsite(id: number) {
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取站点信息(含 topNavs / bottomNavs),官网首页与关于页使用
|
||||
*/
|
||||
export async function getCmsWebsiteSiteInfo() {
|
||||
const res = await request.get<ApiResult<CmsWebsite>>(
|
||||
'/cms/cms-website/getSiteInfo'
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 官网(企业官网展示端)统一 API 出口
|
||||
* 复用 src/api/cms 下已有模块,并补齐两个缺口:
|
||||
* - getCmsWebsiteSiteInfo:站点信息(含顶部/底部导航)
|
||||
* - submitCmsContactLead / pageCmsContactLead:留言咨询
|
||||
*
|
||||
* 内容读取走与现有 listCmsAd / listCmsArticle 一致的默认请求链路(shop-api 已代理 /cms)。
|
||||
*/
|
||||
export { pageCmsArticle, getCmsArticle, listCmsArticle } from '@/api/cms/cmsArticle'
|
||||
export { treeNavigation, listCmsNavigation } from '@/api/cms/cmsNavigation'
|
||||
export { listCmsMpPages } from '@/api/cms/cmsMpPages'
|
||||
export { getCmsWebsiteSiteInfo, listCmsWebsite, getCmsWebsite } from '@/api/cms/cmsWebsite'
|
||||
export { listCmsAd } from '@/api/cms/cmsAd'
|
||||
export { submitCmsContactLead, pageCmsContactLead } from '@/api/cms/cmsContactLead'
|
||||
|
||||
export type { CmsArticle } from '@/api/cms/cmsArticle/model'
|
||||
export type { CmsWebsite, CmsNavigation } from '@/api/cms/cmsWebsite/model'
|
||||
export type { CmsAd } from '@/api/cms/cmsAd/model'
|
||||
export type { CmsContactLead } from '@/api/cms/cmsContactLead/model'
|
||||
@@ -144,6 +144,17 @@ export default {
|
||||
"unified-qr/index",
|
||||
"pay/index"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "official",
|
||||
"pages": [
|
||||
"index",
|
||||
"article",
|
||||
"article-detail",
|
||||
"about",
|
||||
"message",
|
||||
"mine"
|
||||
]
|
||||
}
|
||||
],
|
||||
window: {
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
--text-color: #333333;
|
||||
--bg-color: #ffffff;
|
||||
--border-color: #e8e8e8;
|
||||
// 官网品牌色(统一 token)
|
||||
--brand-color: #185FA5;
|
||||
--brand-light: #EAF2FB;
|
||||
}
|
||||
|
||||
// 暗黑模式
|
||||
@@ -78,3 +81,72 @@ page {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// 官网分包富文本渲染(RichText nodes 内的标签样式)
|
||||
.official-rich {
|
||||
font-size: 30px;
|
||||
line-height: 1.8;
|
||||
color: #333333;
|
||||
word-break: break-all;
|
||||
|
||||
img {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
display: block;
|
||||
margin: 16px 0;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin: 28px 0 16px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
h1 { font-size: 38px; }
|
||||
h2 { font-size: 34px; }
|
||||
h3 { font-size: 32px; }
|
||||
h4 { font-size: 30px; }
|
||||
|
||||
a {
|
||||
color: var(--brand-color);
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
padding-left: 40px;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 20px 0;
|
||||
padding: 16px 20px;
|
||||
background-color: var(--brand-light);
|
||||
border-left: 6px solid var(--brand-color);
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #e8e8e8;
|
||||
padding: 12px;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView, RichText, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { getCmsWebsiteSiteInfo } from '@/api/official'
|
||||
import type { CmsWebsite } from '@/api/official'
|
||||
import { getCompressedImageUrl } from '@/utils/image'
|
||||
import OfficialTabBar from '../components/OfficialTabBar'
|
||||
import NavBar from '@/components/NavBar'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '关于我们',
|
||||
})
|
||||
|
||||
const About: React.FC = () => {
|
||||
const [site, setSite] = useState<CmsWebsite>()
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
getCmsWebsiteSiteInfo()
|
||||
.then((s) => setSite(s))
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
const aboutHtml =
|
||||
(site as any)?.config?.about || (site as any)?.description || ''
|
||||
|
||||
return (
|
||||
<View className='flex flex-col h-screen bg-gray-50'>
|
||||
<NavBar title='关于我们' />
|
||||
<ScrollView scrollY className='flex-1'>
|
||||
<View className='p-4'>
|
||||
<View className='flex items-center mb-4'>
|
||||
{site?.websiteLogo && (
|
||||
<Image
|
||||
src={getCompressedImageUrl(site.websiteLogo, { width: 120 })}
|
||||
className='w-16 h-16 rounded mr-3'
|
||||
mode='aspectFill'
|
||||
/>
|
||||
)}
|
||||
<Text className='text-lg font-semibold text-gray-800'>
|
||||
{site?.websiteName || '企业官网'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{aboutHtml ? (
|
||||
<RichText nodes={aboutHtml} className='official-rich' />
|
||||
) : (
|
||||
<Text className='text-sm text-gray-400'>
|
||||
{loading ? '加载中...' : '暂无介绍'}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<View className='mt-4 bg-white rounded-lg p-4'>
|
||||
<Text className='text-sm text-gray-500 block'>联系电话:{site?.phone || '—'}</Text>
|
||||
<Text className='text-sm text-gray-500 block mt-2'>邮箱:{site?.email || '—'}</Text>
|
||||
<Text className='text-sm text-gray-500 block mt-2'>地址:{site?.address || '—'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
<OfficialTabBar active='about' />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default About
|
||||
@@ -0,0 +1,61 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView, RichText } from '@tarojs/components'
|
||||
import Taro, { useRouter } from '@tarojs/taro'
|
||||
import { getCmsArticle } from '@/api/official'
|
||||
import type { CmsArticle } from '@/api/official'
|
||||
import NavBar from '@/components/NavBar'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '资讯详情',
|
||||
})
|
||||
|
||||
const ArticleDetail: React.FC = () => {
|
||||
const router = useRouter()
|
||||
const id = router.params.id
|
||||
const [article, setArticle] = useState<CmsArticle>()
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getCmsArticle(Number(id))
|
||||
.then((a) => setArticle(a))
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false))
|
||||
}, [id])
|
||||
|
||||
return (
|
||||
<View className='flex flex-col h-screen bg-white'>
|
||||
<NavBar title='资讯详情' />
|
||||
<ScrollView scrollY className='flex-1'>
|
||||
<View className='p-4'>
|
||||
<Text className='text-xl font-semibold text-gray-800 block'>
|
||||
{article?.title || '加载中...'}
|
||||
</Text>
|
||||
<View className='flex items-center mt-2 mb-4'>
|
||||
<Text className='text-xs text-gray-400'>{article?.source || article?.author || ''}</Text>
|
||||
<Text className='text-xs text-gray-400 ml-3'>{article?.createTime?.slice(0, 10)}</Text>
|
||||
</View>
|
||||
{article?.content ? (
|
||||
<RichText nodes={article.content} className='official-rich' />
|
||||
) : (
|
||||
<Text className='text-sm text-gray-400'>
|
||||
{loading ? '加载中...' : '暂无内容'}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
<View
|
||||
className='px-4 py-3 bg-white border-t border-gray-200 safe-area-bottom'
|
||||
>
|
||||
<View
|
||||
className='bg-brand-600 rounded-full py-3 text-center'
|
||||
onClick={() => Taro.navigateTo({ url: '/official/message' })}
|
||||
>
|
||||
<Text className='text-white text-sm font-medium'>立即咨询</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ArticleDetail
|
||||
@@ -0,0 +1,72 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro, { useRouter } from '@tarojs/taro'
|
||||
import { pageCmsArticle } from '@/api/official'
|
||||
import type { CmsArticle } from '@/api/official'
|
||||
import ArticleCard from '../components/ArticleCard'
|
||||
import OfficialTabBar from '../components/OfficialTabBar'
|
||||
import NavBar from '@/components/NavBar'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '资讯',
|
||||
})
|
||||
|
||||
const ArticleList: React.FC = () => {
|
||||
const router = useRouter()
|
||||
const categoryId = router.params.categoryId
|
||||
const [articles, setArticles] = useState<CmsArticle[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [finished, setFinished] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const load = async (p: number) => {
|
||||
if (loading || finished) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await pageCmsArticle({
|
||||
status: 0,
|
||||
deleted: 0,
|
||||
page: p,
|
||||
limit: 10,
|
||||
categoryId: categoryId ? Number(categoryId) : undefined,
|
||||
})
|
||||
const list = res?.list || []
|
||||
setArticles((prev) => (p === 1 ? list : [...prev, ...list]))
|
||||
setPage(p)
|
||||
if (list.length < 10) setFinished(true)
|
||||
} catch (e) {
|
||||
console.error('资讯列表加载失败', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load(1)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View className='flex flex-col h-screen bg-gray-50'>
|
||||
<NavBar title='资讯' />
|
||||
<ScrollView scrollY className='flex-1' onScrollToLower={() => load(page + 1)}>
|
||||
<View className='p-3'>
|
||||
{articles.length > 0 ? (
|
||||
articles.map((a) => <ArticleCard key={a.articleId} article={a} />)
|
||||
) : (
|
||||
<View className='py-10 text-center'>
|
||||
<Text className='text-sm text-gray-400'>{loading ? '加载中...' : '暂无资讯'}</Text>
|
||||
</View>
|
||||
)}
|
||||
{loading && (
|
||||
<View className='py-3 text-center'>
|
||||
<Text className='text-xs text-gray-400'>加载中...</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
<OfficialTabBar active='article' />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ArticleList
|
||||
@@ -0,0 +1,49 @@
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { getCompressedImageUrl } from '@/utils/image'
|
||||
import type { CmsArticle } from '@/api/cms/cmsArticle/model'
|
||||
|
||||
interface Props {
|
||||
article: CmsArticle
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 资讯列表卡片:左图右文,点击进入详情。
|
||||
* 复用项目图片工具 getCompressedImageUrl 保证 OSS 压缩一致。
|
||||
*/
|
||||
export default function ArticleCard({ article, onClick }: Props) {
|
||||
const handle = () => {
|
||||
if (onClick) {
|
||||
onClick()
|
||||
} else {
|
||||
Taro.navigateTo({ url: `/official/article-detail?id=${article.articleId}` })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='flex bg-white rounded-lg overflow-hidden mb-3' onClick={handle}>
|
||||
{article.image ? (
|
||||
<Image
|
||||
src={getCompressedImageUrl(article.image, { width: 240 })}
|
||||
className='w-24 h-24'
|
||||
mode='aspectFill'
|
||||
/>
|
||||
) : (
|
||||
<View className='w-24 h-24 bg-gray-100 flex items-center justify-center'>
|
||||
<Text className='text-gray-300 text-xs'>无图</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className='flex-1 p-3 flex flex-col justify-center'>
|
||||
<Text className='text-sm text-gray-800 font-medium text-ellipsis-2'>{article.title}</Text>
|
||||
{article.overview ? (
|
||||
<Text className='text-xs text-gray-400 mt-1 text-ellipsis-2'>{article.overview}</Text>
|
||||
) : null}
|
||||
<View className='flex items-center mt-2'>
|
||||
<Text className='text-xs text-gray-300'>{article.source || article.author || ''}</Text>
|
||||
<Text className='text-xs text-gray-300 ml-2'>{article.createTime?.slice(0, 10)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
const TABS = [
|
||||
{ key: 'home', text: '首页', icon: '🏠', path: '/official/index' },
|
||||
{ key: 'article', text: '资讯', icon: '📰', path: '/official/article' },
|
||||
{ key: 'about', text: '关于', icon: 'ℹ️', path: '/official/about' },
|
||||
{ key: 'mine', text: '我的', icon: '👤', path: '/official/mine' },
|
||||
]
|
||||
|
||||
/**
|
||||
* 官网分包内底部导航(微信原生 tabBar 不支持分包页,故自实现)
|
||||
* 通过 redirectTo 在四个官网页面间切换,避免页面栈无限增长。
|
||||
*/
|
||||
export default function OfficialTabBar({ active }: { active: string }) {
|
||||
const go = (path: string) => {
|
||||
if (path === active) return
|
||||
Taro.redirectTo({ url: path })
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='fixed bottom-0 left-0 right-0 flex bg-white border-t border-gray-200 safe-area-bottom'>
|
||||
{TABS.map((t) => {
|
||||
const on = active === t.key
|
||||
return (
|
||||
<View
|
||||
key={t.key}
|
||||
className='flex-1 flex flex-col items-center py-2'
|
||||
onClick={() => go(t.path)}
|
||||
>
|
||||
<Text className={`text-xl ${on ? 'text-brand-600' : 'text-gray-400'}`}>{t.icon}</Text>
|
||||
<Text className={`text-xs mt-0.5 ${on ? 'text-brand-600' : 'text-gray-400'}`}>{t.text}</Text>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
moreUrl?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 区块标题:左侧色条 + 标题,右侧「更多」跳转。
|
||||
*/
|
||||
export default function SectionTitle({ title, moreUrl }: Props) {
|
||||
return (
|
||||
<View className='flex items-center justify-between px-4 py-3'>
|
||||
<View className='flex items-center'>
|
||||
<View className='w-1 h-4 bg-brand-600 rounded mr-2' />
|
||||
<Text className='text-base font-semibold text-gray-800'>{title}</Text>
|
||||
</View>
|
||||
{moreUrl && (
|
||||
<Text
|
||||
className='text-xs text-gray-400'
|
||||
onClick={() => Taro.navigateTo({ url: moreUrl })}
|
||||
>
|
||||
更多 ›
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Swiper, SwiperItem, Image, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { getCompressedImageUrl } from '@/utils/image'
|
||||
import {
|
||||
listCmsAd,
|
||||
getCmsWebsiteSiteInfo,
|
||||
pageCmsArticle,
|
||||
treeNavigation,
|
||||
} from '@/api/official'
|
||||
import type { CmsAd, CmsWebsite, CmsArticle, CmsNavigation } from '@/api/official'
|
||||
import OfficialTabBar from '../components/OfficialTabBar'
|
||||
import ArticleCard from '../components/ArticleCard'
|
||||
import SectionTitle from '../components/SectionTitle'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '企业官网',
|
||||
enableShareAppMessage: true,
|
||||
})
|
||||
|
||||
const OfficialIndex: React.FC = () => {
|
||||
const [site, setSite] = useState<CmsWebsite>()
|
||||
const [banners, setBanners] = useState<CmsAd[]>([])
|
||||
const [navs, setNavs] = useState<CmsNavigation[]>([])
|
||||
const [articles, setArticles] = useState<CmsArticle[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
;(async () => {
|
||||
try {
|
||||
const [siteRes, adRes, navRes, artRes] = await Promise.all([
|
||||
getCmsWebsiteSiteInfo().catch(() => undefined),
|
||||
listCmsAd({ adType: 'banner', status: 0 }).catch(() => [] as CmsAd[]),
|
||||
treeNavigation({ top: 1 }).catch(() => [] as CmsNavigation[]),
|
||||
pageCmsArticle({ status: 0, deleted: 0, page: 1, limit: 5 }).catch(() => undefined),
|
||||
])
|
||||
if (!mounted) return
|
||||
if (siteRes) setSite(siteRes)
|
||||
setBanners(Array.isArray(adRes) ? adRes : ((adRes as any)?.list || []))
|
||||
const topNavs =
|
||||
siteRes?.topNavs && siteRes.topNavs.length
|
||||
? siteRes.topNavs
|
||||
: (navRes || [])
|
||||
setNavs(topNavs)
|
||||
setArticles(artRes?.list || [])
|
||||
} catch (e) {
|
||||
console.error('官网首页加载失败', e)
|
||||
} finally {
|
||||
if (mounted) setLoading(false)
|
||||
}
|
||||
})()
|
||||
return () => {
|
||||
mounted = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const bannerSlides = banners.flatMap((b) =>
|
||||
b.imageList && b.imageList.length
|
||||
? b.imageList.map((img, i) => ({ key: `${b.adId}-${i}`, src: (img as any).url || '' }))
|
||||
: b.image
|
||||
? [{ key: `${b.adId}-0`, src: b.image }]
|
||||
: []
|
||||
)
|
||||
|
||||
const onNavClick = (n: CmsNavigation) => {
|
||||
if (n.path) {
|
||||
Taro.navigateTo({ url: n.path.startsWith('/') ? n.path : `/${n.path}` })
|
||||
} else if (n.children && n.children.length) {
|
||||
Taro.navigateTo({ url: `/official/article?categoryId=${n.navigationId}` })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='flex flex-col h-screen bg-gray-50'>
|
||||
{/* 顶部品牌栏 */}
|
||||
<View className='flex items-center px-4 py-3 bg-white'>
|
||||
{site?.websiteLogo ? (
|
||||
<Image
|
||||
src={getCompressedImageUrl(site.websiteLogo, { width: 80 })}
|
||||
className='w-8 h-8 rounded mr-2'
|
||||
mode='aspectFill'
|
||||
/>
|
||||
) : null}
|
||||
<Text className='text-lg font-semibold text-gray-800'>
|
||||
{site?.websiteName || '企业官网'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView scrollY className='flex-1'>
|
||||
{/* Banner 轮播 */}
|
||||
{bannerSlides.length > 0 && (
|
||||
<Swiper
|
||||
autoplay
|
||||
interval={3000}
|
||||
className='mx-3 mt-3 rounded-lg overflow-hidden'
|
||||
style={{ height: '150px' }}
|
||||
>
|
||||
{bannerSlides.map((s) => (
|
||||
<SwiperItem key={s.key}>
|
||||
<Image
|
||||
src={getCompressedImageUrl(s.src, { width: 750 })}
|
||||
className='w-full h-full'
|
||||
mode='aspectFill'
|
||||
/>
|
||||
</SwiperItem>
|
||||
))}
|
||||
</Swiper>
|
||||
)}
|
||||
|
||||
{/* 栏目导航宫格 */}
|
||||
{navs.length > 0 && (
|
||||
<View className='mx-3 mt-3 bg-white rounded-lg p-3'>
|
||||
<View className='flex flex-wrap'>
|
||||
{navs.map((n) => (
|
||||
<View
|
||||
key={n.navigationId}
|
||||
className='w-1/4 flex flex-col items-center py-2'
|
||||
onClick={() => onNavClick(n)}
|
||||
>
|
||||
<View
|
||||
className='w-10 h-10 rounded-full flex items-center justify-center mb-1'
|
||||
style={{ background: n.color || '#EAF2FB' }}
|
||||
>
|
||||
<Text className='text-base'>{(n.icon as any) || '•'}</Text>
|
||||
</View>
|
||||
<Text className='text-xs text-gray-600'>{n.title}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 资讯动态 */}
|
||||
<View className='mt-1'>
|
||||
<SectionTitle title='资讯动态' moreUrl='/official/article' />
|
||||
<View className='px-3'>
|
||||
{articles.length > 0 ? (
|
||||
articles.map((a) => <ArticleCard key={a.articleId} article={a} />)
|
||||
) : (
|
||||
<View className='py-8 text-center'>
|
||||
<Text className='text-sm text-gray-400'>
|
||||
{loading ? '加载中...' : '暂无资讯'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 留言咨询 CTA */}
|
||||
<View className='mx-3 mt-3 mb-4'>
|
||||
<View
|
||||
className='bg-brand-600 rounded-lg py-3 flex items-center justify-center'
|
||||
onClick={() => Taro.navigateTo({ url: '/official/message' })}
|
||||
>
|
||||
<Text className='text-white text-sm font-medium'>📩 留言咨询</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<OfficialTabBar active='home' />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default OfficialIndex
|
||||
@@ -0,0 +1,111 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text, Input, Textarea } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { submitCmsContactLead } from '@/api/official'
|
||||
import NavBar from '@/components/NavBar'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '留言咨询',
|
||||
})
|
||||
|
||||
interface FormState {
|
||||
name: string
|
||||
phone: string
|
||||
company: string
|
||||
need: string
|
||||
}
|
||||
|
||||
const Message: React.FC = () => {
|
||||
const [form, setForm] = useState<FormState>({ name: '', phone: '', company: '', need: '' })
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const set = (k: keyof FormState, v: string) =>
|
||||
setForm((prev) => ({ ...prev, [k]: v }))
|
||||
|
||||
const submit = async () => {
|
||||
if (!form.name.trim()) {
|
||||
Taro.showToast({ title: '请填写称呼', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!/^1\d{10}$/.test(form.phone.trim())) {
|
||||
Taro.showToast({ title: '请填写正确的手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await submitCmsContactLead({ ...form, source: 'miniapp' })
|
||||
Taro.showToast({ title: '提交成功,我们会尽快联系您', icon: 'success' })
|
||||
setTimeout(() => Taro.navigateBack(), 1200)
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e?.message || '提交失败', icon: 'none' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='flex flex-col h-screen bg-gray-50'>
|
||||
<NavBar title='留言咨询' />
|
||||
<View className='flex-1 p-4'>
|
||||
<View className='bg-white rounded-lg p-4'>
|
||||
<Field label='称呼' placeholder='请输入您的称呼' value={form.name} onInput={(v) => set('name', v)} />
|
||||
<Field
|
||||
label='手机号'
|
||||
placeholder='请输入手机号'
|
||||
value={form.phone}
|
||||
type='number'
|
||||
onInput={(v) => set('phone', v)}
|
||||
/>
|
||||
<Field label='公司' placeholder='选填' value={form.company} onInput={(v) => set('company', v)} />
|
||||
<View className='mt-3'>
|
||||
<Text className='text-sm text-gray-600'>需求描述</Text>
|
||||
<Textarea
|
||||
className='w-full mt-2 p-2 bg-gray-50 rounded text-sm text-gray-800'
|
||||
style={{ height: '120px' }}
|
||||
placeholder='请描述您的需求'
|
||||
value={form.need}
|
||||
onInput={(e) => set('need', (e.detail as any).value)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
className='mt-4 bg-brand-600 rounded-full py-3 text-center'
|
||||
onClick={submit}
|
||||
>
|
||||
<Text className='text-white text-sm font-medium'>
|
||||
{submitting ? '提交中...' : '提交留言'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function Field({
|
||||
label,
|
||||
placeholder,
|
||||
value,
|
||||
onInput,
|
||||
type,
|
||||
}: {
|
||||
label: string
|
||||
placeholder?: string
|
||||
value: string
|
||||
onInput: (v: string) => void
|
||||
type?: 'text' | 'number'
|
||||
}) {
|
||||
return (
|
||||
<View className='flex items-center py-3 border-b border-gray-100'>
|
||||
<Text className='text-sm text-gray-600 w-16'>{label}</Text>
|
||||
<Input
|
||||
className='flex-1 text-sm text-gray-800 ml-2'
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
type={type || 'text'}
|
||||
onInput={(e) => onInput((e.detail as any).value)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default Message
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { getCmsWebsiteSiteInfo, pageCmsContactLead } from '@/api/official'
|
||||
import type { CmsWebsite } from '@/api/official'
|
||||
import OfficialTabBar from '../components/OfficialTabBar'
|
||||
import NavBar from '@/components/NavBar'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '我的',
|
||||
})
|
||||
|
||||
const Mine: React.FC = () => {
|
||||
const [site, setSite] = useState<CmsWebsite>()
|
||||
const [leadCount, setLeadCount] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
getCmsWebsiteSiteInfo()
|
||||
.then((s) => setSite(s))
|
||||
.catch(() => {})
|
||||
pageCmsContactLead({ page: 1, limit: 1 })
|
||||
.then((r) => setLeadCount(r?.count || 0))
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
const menus = [
|
||||
{ label: '我的留言', value: leadCount ? `${leadCount} 条` : '暂无', url: '/official/message' },
|
||||
{ label: '关于我们', url: '/official/about' },
|
||||
{ label: '留言咨询', url: '/official/message' },
|
||||
]
|
||||
|
||||
return (
|
||||
<View className='flex flex-col h-screen bg-gray-50'>
|
||||
<NavBar title='我的' />
|
||||
<ScrollView scrollY className='flex-1'>
|
||||
<View className='flex items-center p-4 bg-white'>
|
||||
<View className='w-14 h-14 rounded-full bg-brand-600 flex items-center justify-center'>
|
||||
<Text className='text-white text-xl'>{(site?.websiteName || '官').slice(0, 1)}</Text>
|
||||
</View>
|
||||
<View className='ml-3'>
|
||||
<Text className='text-base font-semibold text-gray-800'>
|
||||
{site?.websiteName || '企业官网'}
|
||||
</Text>
|
||||
<Text className='text-xs text-gray-400 mt-1'>企业账号</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='mt-3 bg-white rounded-lg'>
|
||||
{menus.map((m) => (
|
||||
<View
|
||||
key={m.label}
|
||||
className='flex items-center justify-between px-4 py-4 border-b border-gray-100'
|
||||
onClick={() => Taro.navigateTo({ url: m.url })}
|
||||
>
|
||||
<Text className='text-sm text-gray-700'>{m.label}</Text>
|
||||
<Text className='text-xs text-gray-400'>{m.value || '›'}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
<OfficialTabBar active='mine' />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default Mine
|
||||
@@ -280,6 +280,18 @@ const IndexPage: React.FC = () => {
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
{/* 企业官网入口 */}
|
||||
<View
|
||||
className='mx-3 mt-2 bg-white rounded-lg p-3 flex items-center justify-between'
|
||||
onClick={() => Taro.navigateTo({ url: '/official/index' })}
|
||||
>
|
||||
<View>
|
||||
<Text className='text-base font-medium text-gray-800'>企业官网</Text>
|
||||
<Text className='text-xs text-gray-400 mt-1 block'>了解我们 · 资讯动态 · 留言咨询</Text>
|
||||
</View>
|
||||
<Text className='text-brand-600 text-sm'>进入 ›</Text>
|
||||
</View>
|
||||
|
||||
{/* 轮播图:有数据才显示,为空则不渲染 */}
|
||||
{bannerSlides.length > 0 && (
|
||||
<View className='mx-3 mt-2 rounded-lg overflow-hidden index-banner-wrap'>
|
||||
|
||||
Reference in New Issue
Block a user