refactor(official): 使用演示数据兜底官网内容展示

- 在关于我们页补充企业优势和联系方式演示数据展示
- 资讯列表页增加分类标签,实现按分类筛选资讯
- 文章详情页新增相关推荐文章展示逻辑
- 主页使用演示数据兜底导航、Banner、核心业务、公司简介和文章列表
- 各页面均优化站点信息使用,优先展示真实数据,缺失时使用演示数据
- 留言页增加公司联系信息展示,提升体验
- 个人中心页支持分享官网,展示演示数据防止空白
- 主页Banner支持无图时使用渐变背景和文字占位
- 统一使用演示数据,保证页面无空白,提升整体稳定性和用户体验
This commit is contained in:
2026-08-12 19:17:47 +08:00
parent aa8d038e75
commit d83a6bdd69
10 changed files with 463 additions and 78 deletions
+21 -13
View File
@@ -4,8 +4,15 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="2de94e4f-0587-4c9b-8bfd-e87e0ae95b81" name="更改" comment="refactor(common): 移除公共组件中的 NutUI Button改用 Taro 原生 Button&#10;&#10;- `EmptyState` 和 `FixedButton` 两个组件全部替换 NutUI Button 为 Taro 原生 Button&#10;- 解决空状态按钮在微信小程序端不可见问题,确保按钮正常显示&#10;- `FixedButton` 组件虽然未被调用,仍保留但调整实现,icon 由 prop 改为 children 渲染&#10;- 统一主色使用 tailwind 的 `bg-red-500`,弃用 NutUI primary 颜色&#10;- 全局修复微信小程序原生 button 的 `::after` 灰色描边问题,在全局样式中设置 `button::after { border: none; }`&#10;- NutUI Button 引用文件数由 27 减至 25`src/components/common/` 完全无 NutUI 依赖&#10;- CMS 请求链路调整,所有 CMS 接口改用独立 `cmsRequest` 实现">
<list default="true" id="2de94e4f-0587-4c9b-8bfd-e87e0ae95b81" name="更改" comment="refactor(common): 移除 NutUI Button改用 Taro 原生 Button&#10;&#10;- `EmptyState` 和 `FixedButton` 组件替换 NutUI Button 为 Taro 原生 Button&#10;- 解决空状态按钮在微信小程序端不可见问题,确保按钮正常显示&#10;- `FixedButton` 组件调整实现,icon 由 prop 改为 children 渲染&#10;- 统一主色用 tailwind 的 `bg-red-500`,弃用 NutUI primary 颜色&#10;- 全局样式修复微信小程序原生 button 的 `::after` 灰色描边问题&#10;- NutUI Button 引用文件数由 27 减至 25`src/components/common/` 无 NutUI 依赖&#10;- CMS 请求链路调整,所有 CMS 接口改用独立 `cmsRequest` 实现">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/official/about/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/official/about/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/official/article-detail/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/official/article-detail/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/official/article/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/official/article/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/official/index/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/official/index/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/official/message/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/official/message/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/official/mine/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/official/mine/index.tsx" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -120,15 +127,8 @@
<workItem from="1784638429525" duration="16147000" />
<workItem from="1784798575885" duration="4896000" />
<workItem from="1784873955217" duration="3822000" />
<workItem from="1786371937331" duration="12399000" />
</task>
<task id="LOCAL-00034" summary="feat(app): 添加应用入口文件和配置文件&#10;&#10;- 新增 index.tsx 作为应用的主入口文件&#10;- 新增 index.config.ts 用于应用的配置管理">
<option name="closed" value="true" />
<created>1782804461646</created>
<option name="number" value="00034" />
<option name="presentableId" value="LOCAL-00034" />
<option name="project" value="LOCAL" />
<updated>1782804461646</updated>
<workItem from="1786371937331" duration="13654000" />
<workItem from="1786532924218" duration="478000" />
</task>
<task id="LOCAL-00035" summary="fix(tailwind): 更新配置以兼容微信小程序&#10;&#10;- 启用 autoprefixer 并新增 postcss-nested 插件支持嵌套语法&#10;- 扩展主题颜色,添加 primary 色系&#10;- 更新 content 路径包含 public/index.html&#10;- 禁用 preflight 以移除默认样式重置&#10;- 移除多数 corePlugins 禁用项,保留 preflight 禁用,简化兼容性配置">
<option name="closed" value="true" />
@@ -514,7 +514,15 @@
<option name="project" value="LOCAL" />
<updated>1786417944432</updated>
</task>
<option name="localTasksCounter" value="83" />
<task id="LOCAL-00083" summary="refactor(common): 移除 NutUI Button 并改用 Taro 原生 Button&#10;&#10;- `EmptyState` 和 `FixedButton` 组件替换 NutUI Button 为 Taro 原生 Button&#10;- 解决空状态按钮在微信小程序端不可见问题,确保按钮正常显示&#10;- `FixedButton` 组件调整实现,icon 由 prop 改为 children 渲染&#10;- 统一主色改用 tailwind 的 `bg-red-500`,弃用 NutUI primary 颜色&#10;- 全局样式修复微信小程序原生 button 的 `::after` 灰色描边问题&#10;- NutUI Button 引用文件数由 27 减至 25`src/components/common/` 无 NutUI 依赖&#10;- CMS 请求链路调整,所有 CMS 接口改用独立 `cmsRequest` 实现">
<option name="closed" value="true" />
<created>1786434456515</created>
<option name="number" value="00083" />
<option name="presentableId" value="LOCAL-00083" />
<option name="project" value="LOCAL" />
<updated>1786434456515</updated>
</task>
<option name="localTasksCounter" value="84" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -534,7 +542,6 @@
<component name="VcsManagerConfiguration">
<option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="false" />
<option name="CHECK_NEW_TODO" value="false" />
<MESSAGE value="feat(pay): 支付成功后自动跳转到已购产品页面&#10;&#10;- 支付成功后显示成功提示&#10;- 延迟1.5秒自动跳转到已购产品页面&#10;- 处理跳转失败时回退到用户首页&#10;- 优化用户支付完成后的使用体验" />
<MESSAGE value="feat(pay-bak): 新增小程序支付页面及相关配置&#10;&#10;- 新增确认支付页面配置,设置导航栏标题和样式&#10;- 实现小程序支付页完整组件逻辑,包括加载订单、发起JSAPI支付等&#10;- 支持通过 scene 或 subscriptionNo 获取订单参数&#10;- 完善支付流程接口请求及错误处理,支持自动登录和支付确认&#10;- 增加支付成功、支付中、支付失败、取消等状态页面展示&#10;- 支持支付成功后跳转已购产品页或用户页&#10;- 提供取消支付返回逻辑和界面展示&#10;- 细化支付金额显示和提示用户支付后可使用应用功能" />
<MESSAGE value="feat(address): 新增收货地址与售后申请功能&#10;&#10;- 新增地址编辑页面,支持地址智能识别、地图选点、地区选择和默认地址设置&#10;- 实现地址列表页面,支持地址查看、删除、设为默认及选择返回结算页&#10;- 新增售后申请页面,支持退款类型选择、商品选择、原因填写、图片上传和提交审核&#10;- 修复 passport 分包配置,移除不存在分包并补充缺失声明,避免 Taro 编译报错&#10;- 新增地址类型定义,增强前端地址数据结构类型安全&#10;- 优化页面交互体验,完善表单校验及错误提示逻辑&#10;- 统一代码格式与命名规范,保持代码风格一致性" />
<MESSAGE value="feat(passport): 迁移并修复小程序支付页面访问 websopy API 问题&#10;&#10;- 迁移 src/passport/pay 相关代码至当前项目,无需替换文件&#10;- 新增环境变量 WEBSOPY_API_BASE_URL,提供 websopy API 基础地址&#10;- 在 config/app.ts 导出 WebsopyBaseUrl,供代码引用&#10;- 配置 env.d.ts 类型定义,消除 TypeScript 隐式 any 错误&#10;- request 请求方法支持自定义 baseUrl 以调用不同接口地址&#10;- passport/pay 页面调用订阅接口统一传入 WebsopyBaseUrl&#10;- 修正 tenantId 类型为 number,调整登录用户类型断言&#10;- 确认编译成功,dist 目录生成对应文件及正确请求地址&#10;- 说明 type-check 依赖库问题不影响正常构建和运行" />
@@ -559,7 +566,8 @@
<MESSAGE value="feat(official): 实现企业官网分包及内容展示&#10;&#10;- 新增官方分包目录及页面(首页、资讯列表、详情、关于我们、留言、我的)&#10;- 自定义实现底部导航组件 OfficialTabBar,解决微信 tabBar 不支持分包问题&#10;- 统一接口调用走 shop-api 代理,无新增 CMS 域名,减少合法域名负担&#10;- 新增 CMS 站点信息获取接口及留言咨询提交与查询接口&#10;- 设计并完善官网富文本样式,品牌色统一为 #185FA5,并更新 tailwind 配置&#10;- 首页新增企业官网入口卡片,方便用户访问官网内容&#10;- 完成官网分包构建验证,解决 Taro 构建批量删除保护问题&#10;- 规范 CMS 字段使用,适配字段名称及状态含义&#10;- 后续计划对历史页面 NutUI 组件进行空壳化整改,及更新 UI 设计文档" />
<MESSAGE value="refactor(cms): 迁移CMS模块接口到独立cms-api服务&#10;&#10;- 新增cmsRequest工具,基于request封装,默认baseURL为cms-api&#10;- 改用cmsRequest替换所有cms相关请求,确保请求走独立域名&#10;- 配置三环境新增CMS_API_BASE_URL及导出CmsBaseUrl常量&#10;- 解决CMS接口走shop-api时返回404的问题,避免小程序域名白名单冲突&#10;- 统一修改34个cms API模块的261处调用,保持接口行为一致&#10;- 修复部分拼接全URL方式改为相对路径调用cmsRequest&#10;- 更新注释说明并调整相关接口调用代码&#10;- 兼容macOS sed替换问题,采用perl命令完成批量替换" />
<MESSAGE value="refactor(common): 移除公共组件中的 NutUI Button,改用 Taro 原生 Button&#10;&#10;- `EmptyState` 和 `FixedButton` 两个组件全部替换 NutUI Button 为 Taro 原生 Button&#10;- 解决空状态按钮在微信小程序端不可见的问题,确保按钮正常显示&#10;- `FixedButton` 组件虽然未被调用,仍保留但调整实现,icon 由 prop 改为 children 渲染&#10;- 统一主色使用 tailwind 的 `bg-red-500`,弃用原 NutUI primary 颜色&#10;- 全局修复微信小程序原生 button 的 `::after` 灰色描边问题,在全局样式中设置 `button::after { border: none; }`&#10;- NutUI Button 的引用文件数由 27 减至 25`src/components/common/` 完全无 NutUI 依赖&#10;- CMS 请求链路调整,所有 CMS 接口改用独立 `cmsRequest` 实现" />
<option name="LAST_COMMIT_MESSAGE" value="refactor(common): 移除公共组件中的 NutUI Button改用 Taro 原生 Button&#10;&#10;- `EmptyState` 和 `FixedButton` 两个组件全部替换 NutUI Button 为 Taro 原生 Button&#10;- 解决空状态按钮在微信小程序端不可见问题,确保按钮正常显示&#10;- `FixedButton` 组件虽然未被调用,仍保留但调整实现,icon 由 prop 改为 children 渲染&#10;- 统一主色使用 tailwind 的 `bg-red-500`,弃用 NutUI primary 颜色&#10;- 全局修复微信小程序原生 button 的 `::after` 灰色描边问题,在全局样式中设置 `button::after { border: none; }`&#10;- NutUI Button 引用文件数由 27 减至 25`src/components/common/` 完全无 NutUI 依赖&#10;- CMS 请求链路调整,所有 CMS 接口改用独立 `cmsRequest` 实现" />
<MESSAGE value="refactor(common): 移除 NutUI Button改用 Taro 原生 Button&#10;&#10;- `EmptyState` 和 `FixedButton` 组件替换 NutUI Button 为 Taro 原生 Button&#10;- 解决空状态按钮在微信小程序端不可见问题,确保按钮正常显示&#10;- `FixedButton` 组件调整实现,icon 由 prop 改为 children 渲染&#10;- 统一主色用 tailwind 的 `bg-red-500`,弃用 NutUI primary 颜色&#10;- 全局样式修复微信小程序原生 button 的 `::after` 灰色描边问题&#10;- NutUI Button 引用文件数由 27 减至 25`src/components/common/` 无 NutUI 依赖&#10;- CMS 请求链路调整,所有 CMS 接口改用独立 `cmsRequest` 实现" />
<option name="LAST_COMMIT_MESSAGE" value="refactor(common): 移除 NutUI Button 并改用 Taro 原生 Button&#10;&#10;- `EmptyState` 和 `FixedButton` 组件替换 NutUI Button 为 Taro 原生 Button&#10;- 解决空状态按钮在微信小程序端不可见问题,确保按钮正常显示&#10;- `FixedButton` 组件调整实现,icon 由 prop 改为 children 渲染&#10;- 统一主色改用 tailwind 的 `bg-red-500`,弃用 NutUI primary 颜色&#10;- 全局样式修复微信小程序原生 button 的 `::after` 灰色描边问题&#10;- NutUI Button 引用文件数由 27 减至 25`src/components/common/` 无 NutUI 依赖&#10;- CMS 请求链路调整,所有 CMS 接口改用独立 `cmsRequest` 实现" />
<option name="NON_MODAL_COMMIT_POSTPONE_SLOW_CHECKS" value="false" />
</component>
</project>
+1
View File
@@ -10,6 +10,7 @@
],
"scripts": {
"build:weapp": "taro build --type weapp",
"build:weapp:safe": "bash scripts/safe-build-weapp.sh",
"build:swan": "taro build --type swan",
"build:alipay": "taro build --type alipay",
"build:tt": "taro build --type tt",
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
# 安全构建小程序:规避 Taro emptyOutputDir 触发的 SAFE_DELETE 批量删除守卫
# dist 文件数 > 50 时守卫会中断构建,导致 official/index 等页面 .js 缺失而白屏)
set -e
BAK_ROOT="/tmp/taro-dist-bak"
if [ -d dist ]; then
mkdir -p "$BAK_ROOT"
BAK="$BAK_ROOT/dist-$(date +%s)"
mv dist "$BAK"
echo "[safe-build] 已备份旧 dist -> $BAK"
fi
echo "[safe-build] 开始构建 weapp ..."
./node_modules/.bin/taro build --type weapp
echo "[safe-build] 构建完成"
+41 -7
View File
@@ -6,6 +6,7 @@ import type { CmsWebsite } from '@/api/official'
import { getCompressedImageUrl } from '@/utils/image'
import OfficialTabBar from '../components/OfficialTabBar'
import NavBar from '@/components/NavBar'
import { DEMO_SITE, DEMO_ADVANTAGES } from '../data/demo'
definePageConfig({
navigationBarTitleText: '关于我们',
@@ -22,8 +23,15 @@ const About: React.FC = () => {
.finally(() => setLoading(false))
}, [])
const effectiveSite: CmsWebsite = site ?? (DEMO_SITE as CmsWebsite)
const aboutHtml =
(site as any)?.config?.about || (site as any)?.description || ''
(site as any)?.config?.about || (site as any)?.description || DEMO_SITE.config?.about || ''
const contact = {
phone: effectiveSite.phone || DEMO_SITE.phone,
email: effectiveSite.email || DEMO_SITE.email,
address: effectiveSite.address || DEMO_SITE.address,
}
return (
<View className='flex flex-col h-screen bg-gray-50'>
@@ -31,15 +39,21 @@ const About: React.FC = () => {
<ScrollView scrollY className='flex-1'>
<View className='p-4'>
<View className='flex items-center mb-4'>
{site?.websiteLogo && (
{effectiveSite.websiteLogo ? (
<Image
src={getCompressedImageUrl(site.websiteLogo, { width: 120 })}
src={getCompressedImageUrl(effectiveSite.websiteLogo, { width: 120 })}
className='w-16 h-16 rounded mr-3'
mode='aspectFill'
/>
) : (
<View className='w-16 h-16 rounded bg-brand-600 flex items-center justify-center mr-3'>
<Text className='text-white text-2xl font-bold'>
{(effectiveSite.websiteName || '官').slice(0, 1)}
</Text>
</View>
)}
<Text className='text-lg font-semibold text-gray-800'>
{site?.websiteName || '企业官网'}
{effectiveSite.websiteName || '企业官网'}
</Text>
</View>
@@ -51,10 +65,30 @@ const About: React.FC = () => {
</Text>
)}
{/* 企业优势 */}
<View className='mt-5'>
<View className='flex items-center mb-3'>
<View className='w-1 h-4 bg-brand-600 rounded mr-2' />
<Text className='text-base font-semibold text-gray-800'></Text>
</View>
<View className='flex flex-wrap -mx-1'>
{DEMO_ADVANTAGES.map((a) => (
<View key={a.title} className='w-1/2 px-1 mb-2'>
<View className='bg-white rounded-lg p-3 h-full'>
<Text className='text-xl'>{a.icon}</Text>
<Text className='text-sm font-medium text-gray-800 block mt-1'>{a.title}</Text>
<Text className='text-xs text-gray-400 mt-0.5 leading-snug block'>{a.desc}</Text>
</View>
</View>
))}
</View>
</View>
{/* 联系方式 */}
<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>
<Text className='text-sm text-gray-500 block'>{contact.phone || '—'}</Text>
<Text className='text-sm text-gray-500 block mt-2'>{contact.email || '—'}</Text>
<Text className='text-sm text-gray-500 block mt-2'>{contact.address || '—'}</Text>
</View>
</View>
</ScrollView>
+25 -4
View File
@@ -1,8 +1,9 @@
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 { getCmsArticle, pageCmsArticle } from '@/api/official'
import type { CmsArticle } from '@/api/official'
import ArticleCard from '../components/ArticleCard'
import NavBar from '@/components/NavBar'
definePageConfig({
@@ -13,6 +14,7 @@ const ArticleDetail: React.FC = () => {
const router = useRouter()
const id = router.params.id
const [article, setArticle] = useState<CmsArticle>()
const [related, setRelated] = useState<CmsArticle[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
@@ -21,6 +23,13 @@ const ArticleDetail: React.FC = () => {
.then((a) => setArticle(a))
.catch(() => {})
.finally(() => setLoading(false))
// 相关推荐:取最新几条,排除当前
pageCmsArticle({ status: 0, deleted: 0, page: 1, limit: 4 })
.then((r) => {
const list = (r?.list || []).filter((x) => x.articleId !== Number(id)).slice(0, 3)
setRelated(list)
})
.catch(() => {})
}, [id])
return (
@@ -43,10 +52,22 @@ const ArticleDetail: React.FC = () => {
</Text>
)}
</View>
{/* 相关推荐 */}
{related.length > 0 && (
<View className='px-3 pb-4'>
<View className='flex items-center mb-2'>
<View className='w-1 h-4 bg-brand-600 rounded mr-2' />
<Text className='text-base font-semibold text-gray-800'></Text>
</View>
{related.map((a) => (
<ArticleCard key={a.articleId} article={a} />
))}
</View>
)}
</ScrollView>
<View
className='px-4 py-3 bg-white border-t border-gray-200 safe-area-bottom'
>
<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' })}
+69 -27
View File
@@ -1,8 +1,8 @@
import React, { useState, useEffect } from 'react'
import React, { useState, useEffect, useCallback } 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 { pageCmsArticle, treeNavigation } from '@/api/official'
import type { CmsArticle, CmsNavigation } from '@/api/official'
import ArticleCard from '../components/ArticleCard'
import OfficialTabBar from '../components/OfficialTabBar'
import NavBar from '@/components/NavBar'
@@ -13,42 +13,84 @@ definePageConfig({
const ArticleList: React.FC = () => {
const router = useRouter()
const categoryId = router.params.categoryId
const initCategory = router.params.categoryId ? Number(router.params.categoryId) : undefined
const [cats, setCats] = useState<{ id?: number; title: string }[]>([{ id: undefined, title: '全部' }])
const [activeCat, setActiveCat] = useState<number | undefined>(initCategory)
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,
useEffect(() => {
treeNavigation({ top: 1 })
.then((res) => {
const list = (res || []).map((n: CmsNavigation) => ({ id: n.navigationId, title: n.title }))
if (list.length) setCats([{ id: undefined, title: '全部' }, ...list])
})
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)
}
}
.catch(() => {})
}, [])
const load = useCallback(
async (p: number, cat?: number) => {
if (loading || finished) return
setLoading(true)
try {
const res = await pageCmsArticle({
status: 0,
deleted: 0,
page: p,
limit: 10,
categoryId: cat,
})
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)
}
},
[loading, finished]
)
useEffect(() => {
load(1)
}, [])
setArticles([])
setPage(1)
setFinished(false)
load(1, activeCat)
}, [activeCat])
const onTab = (id?: number) => {
if (id === activeCat) return
setActiveCat(id)
}
return (
<View className='flex flex-col h-screen bg-gray-50'>
<NavBar title='资讯' />
<ScrollView scrollY className='flex-1' onScrollToLower={() => load(page + 1)}>
{/* 分类 tab */}
<ScrollView scrollX className='bg-white border-b border-gray-100' style={{ whiteSpace: 'nowrap' }}>
<View className='inline-flex px-2'>
{cats.map((c) => {
const on = c.id === activeCat
return (
<View
key={String(c.id)}
className={`inline-block px-3 py-3 ${on ? 'text-brand-600' : 'text-gray-500'}`}
onClick={() => onTab(c.id)}
>
<Text className={`text-sm ${on ? 'font-semibold' : ''}`}>{c.title}</Text>
{on ? <View className='mx-auto mt-1 h-0.5 w-6 bg-brand-600 rounded' /> : null}
</View>
)
})}
</View>
</ScrollView>
<ScrollView scrollY className='flex-1' onScrollToLower={() => load(page + 1, activeCat)}>
<View className='p-3'>
{articles.length > 0 ? (
articles.map((a) => <ArticleCard key={a.articleId} article={a} />)
+118
View File
@@ -0,0 +1,118 @@
/**
* 官网演示兜底数据
* 当 CMS 后台未配置对应内容(站点信息 / 导航 / Banner / 文章等)时,
* 前端使用下方数据渲染成型页面,保证「打开即见官网」而非空白。
* 一旦 CMS 返回真实数据,页面自动以真实数据覆盖(见各页 merge 逻辑)。
*
* 注意:图片类字段统一留空,由组件用 CSS 渐变 / 首字母占位渲染,
* 避免演示环境外链裂图。
*/
import type { CmsWebsite, CmsNavigation } from '@/api/cms/cmsWebsite/model'
import type { CmsArticle } from '@/api/cms/cmsArticle/model'
/** 站点信息兜底(对应 getCmsWebsiteSiteInfo 返回结构,按需取字段) */
export const DEMO_SITE: Partial<CmsWebsite> = {
websiteName: '星辰科技企业',
websiteLogo: '',
description: '专注于为企业提供数字化转型与智能营销一体化解决方案,助力品牌增长。',
phone: '400-888-6666',
email: 'contact@example.com',
address: '广东省深圳市南山区科技园',
icpNo: '粤ICP备2024000000号',
config: {
about:
'<p>星辰科技成立于 2015 年,是一家以技术与服务驱动的数字化服务商,致力于帮助企业在移动互联网时代实现增长。</p>' +
'<p>我们已服务超过 <b>2000+</b> 家企业客户,覆盖零售、制造、教育、本地生活等多个行业,提供从战略咨询到系统落地的一站式服务。</p>',
},
}
/** 顶部快捷导航宫格兜底 */
export const DEMO_NAVS: CmsNavigation[] = [
{ navigationId: 1, title: '公司简介', icon: '🏢', color: '#EAF2FB', path: '/official/about' },
{ navigationId: 2, title: '产品中心', icon: '📦', color: '#EAF2FB' },
{ navigationId: 3, title: '解决方案', icon: '💡', color: '#EAF2FB' },
{ navigationId: 4, title: '新闻资讯', icon: '📰', color: '#EAF2FB', path: '/official/article' },
{ navigationId: 5, title: '客户案例', icon: '🤝', color: '#EAF2FB' },
{ navigationId: 6, title: '留言咨询', icon: '💬', color: '#EAF2FB', path: '/official/message' },
{ navigationId: 7, title: '关于我们', icon: '️', color: '#EAF2FB', path: '/official/about' },
{ navigationId: 8, title: '联系我们', icon: '📞', color: '#EAF2FB', path: '/official/message' },
]
/** Banner 兜底(渐变 + 标题,无外链图片) */
export interface DemoBanner {
title: string
subtitle: string
gradient: string
}
export const DEMO_BANNERS: DemoBanner[] = [
{ title: '数字化转型专家', subtitle: '让增长更简单', gradient: 'linear-gradient(135deg,#185FA5,#3D86CE)' },
{ title: '智能营销一体化', subtitle: '全渠道触达你的客户', gradient: 'linear-gradient(135deg,#12518C,#1E6BB8)' },
{ title: '2000+ 企业信赖', subtitle: '专业服务值得托付', gradient: 'linear-gradient(135deg,#0E4070,#185FA5)' },
]
/** 首页「核心业务」横向卡片兜底 */
export interface DemoBusiness {
id: number
title: string
desc: string
icon: string
}
export const DEMO_BUSINESS: DemoBusiness[] = [
{ id: 1, title: '数字化转型', desc: '从战略到落地的全链路咨询与实施', icon: '🚀' },
{ id: 2, title: '智能营销', desc: '私域运营 + 全渠道增长引擎', icon: '📈' },
{ id: 3, title: '客户管理', desc: '一体化 CRM 与会员体系', icon: '👥' },
{ id: 4, title: '数据洞察', desc: '可视化看板辅助经营决策', icon: '📊' },
]
/** 资讯列表兜底 */
export const DEMO_ARTICLES: CmsArticle[] = [
{
articleId: 101,
title: '星辰科技发布全新一代智能营销平台',
overview: '集成私域运营、内容分发与数据洞察,帮助企业以更低成本获取并留住客户。',
image: '',
source: '公司新闻',
author: '星辰科技',
createTime: '2026-08-01',
},
{
articleId: 102,
title: '零售行业数字化转型的 5 个关键步骤',
overview: '从会员体系到门店数字化,拆解一套可落地的转型路线图。',
image: '',
source: '行业洞察',
author: '研究院',
createTime: '2026-07-20',
},
{
articleId: 103,
title: '客户案例:某连锁品牌会员复购提升 38%',
overview: '通过一体化客户管理与精准触达,实现显著增长。',
image: '',
source: '客户案例',
author: '星辰科技',
createTime: '2026-07-08',
},
{
articleId: 104,
title: '小程序官网能力上新:留言咨询与内容管理',
overview: '企业可自助维护站点内容,客户一键发起咨询。',
image: '',
source: '产品动态',
author: '产品团队',
createTime: '2026-06-25',
},
]
/** 关于页「企业优势」兜底 */
export interface DemoAdvantage {
title: string
desc: string
icon: string
}
export const DEMO_ADVANTAGES: DemoAdvantage[] = [
{ title: '技术领先', desc: '自研增长引擎,稳定支撑高并发业务', icon: '⚙️' },
{ title: '服务专业', desc: '一对一顾问,从咨询到落地全程陪伴', icon: '🎯' },
{ title: '数据安全', desc: '金融级加密与权限管控,合规可靠', icon: '🔒' },
{ title: '持续迭代', desc: '产品按月更新,紧跟行业趋势', icon: '🔄' },
]
+110 -20
View File
@@ -12,12 +12,27 @@ import type { CmsAd, CmsWebsite, CmsArticle, CmsNavigation } from '@/api/officia
import OfficialTabBar from '../components/OfficialTabBar'
import ArticleCard from '../components/ArticleCard'
import SectionTitle from '../components/SectionTitle'
import {
DEMO_SITE,
DEMO_NAVS,
DEMO_BANNERS,
DEMO_BUSINESS,
DEMO_ARTICLES,
} from '../data/demo'
definePageConfig({
navigationBarTitleText: '企业官网',
enableShareAppMessage: true,
})
interface Slide {
key: string
src?: string
title?: string
subtitle?: string
gradient?: string
}
const OfficialIndex: React.FC = () => {
const [site, setSite] = useState<CmsWebsite>()
const [banners, setBanners] = useState<CmsAd[]>([])
@@ -42,10 +57,14 @@ const OfficialIndex: React.FC = () => {
siteRes?.topNavs && siteRes.topNavs.length
? siteRes.topNavs
: (navRes || [])
setNavs(topNavs)
setArticles(artRes?.list || [])
setNavs(topNavs.length ? topNavs : DEMO_NAVS)
const realArticles = artRes?.list || []
setArticles(realArticles.length ? realArticles : DEMO_ARTICLES)
} catch (e) {
console.error('官网首页加载失败', e)
// 任意异常都回退到演示内容,保证页面成型
setNavs(DEMO_NAVS)
setArticles(DEMO_ARTICLES)
} finally {
if (mounted) setLoading(false)
}
@@ -55,54 +74,85 @@ const OfficialIndex: React.FC = () => {
}
}, [])
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 effectiveSite: CmsWebsite = site ?? (DEMO_SITE as CmsWebsite)
const slides: Slide[] = (() => {
const real = 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 as string }]
: []
)
if (real.length) return real
return DEMO_BANNERS.map((b, i) => ({ key: `demo-${i}`, ...b }))
})()
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}` })
} else {
Taro.navigateTo({ url: '/official/article' })
}
}
const contact = {
phone: effectiveSite.phone || DEMO_SITE.phone,
email: effectiveSite.email || DEMO_SITE.email,
address: effectiveSite.address || DEMO_SITE.address,
icpNo: effectiveSite.icpNo || DEMO_SITE.icpNo,
}
return (
<View className='flex flex-col h-screen bg-gray-50'>
{/* 顶部品牌栏 */}
<View className='flex items-center px-4 py-3 bg-white'>
{site?.websiteLogo ? (
{effectiveSite.websiteLogo ? (
<Image
src={getCompressedImageUrl(site.websiteLogo, { width: 80 })}
src={getCompressedImageUrl(effectiveSite.websiteLogo, { width: 80 })}
className='w-8 h-8 rounded mr-2'
mode='aspectFill'
/>
) : null}
) : (
<View className='w-8 h-8 rounded bg-brand-600 flex items-center justify-center mr-2'>
<Text className='text-white text-sm font-bold'>
{(effectiveSite.websiteName || '官').slice(0, 1)}
</Text>
</View>
)}
<Text className='text-lg font-semibold text-gray-800'>
{site?.websiteName || '企业官网'}
{effectiveSite.websiteName || '企业官网'}
</Text>
</View>
<ScrollView scrollY className='flex-1'>
{/* Banner 轮播 */}
{bannerSlides.length > 0 && (
{slides.length > 0 && (
<Swiper
autoplay
interval={3000}
className='mx-3 mt-3 rounded-lg overflow-hidden'
style={{ height: '150px' }}
>
{bannerSlides.map((s) => (
{slides.map((s) => (
<SwiperItem key={s.key}>
<Image
src={getCompressedImageUrl(s.src, { width: 750 })}
className='w-full h-full'
mode='aspectFill'
/>
{s.src ? (
<Image
src={getCompressedImageUrl(s.src, { width: 750 })}
className='w-full h-full'
mode='aspectFill'
/>
) : (
<View
className='w-full h-full flex flex-col items-center justify-center'
style={{ background: s.gradient }}
>
<Text className='text-white text-lg font-semibold'>{s.title}</Text>
<Text className='text-white text-xs mt-1 opacity-80'>{s.subtitle}</Text>
</View>
)}
</SwiperItem>
))}
</Swiper>
@@ -131,6 +181,36 @@ const OfficialIndex: React.FC = () => {
</View>
)}
{/* 公司简介 */}
<View className='mx-3 mt-3 bg-white rounded-lg p-4'>
<View className='flex items-center mb-2'>
<View className='w-1 h-4 bg-brand-600 rounded mr-2' />
<Text className='text-base font-semibold text-gray-800'></Text>
</View>
<Text className='text-sm text-gray-500 leading-relaxed'>
{effectiveSite.description || DEMO_SITE.description}
</Text>
</View>
{/* 核心业务 */}
<View className='mt-1'>
<SectionTitle title='核心业务' moreUrl='/official/article' />
<ScrollView scrollX className='whitespace-nowrap px-3' style={{ overflow: 'hidden' }}>
{DEMO_BUSINESS.map((b) => (
<View
key={b.id}
className='inline-flex flex-col w-36 mr-3 bg-white rounded-lg p-3 align-top'
style={{ height: '104px' }}
onClick={() => Taro.navigateTo({ url: '/official/article' })}
>
<Text className='text-2xl'>{b.icon}</Text>
<Text className='text-sm font-medium text-gray-800 mt-1'>{b.title}</Text>
<Text className='text-xs text-gray-400 mt-1 leading-snug'>{b.desc}</Text>
</View>
))}
</ScrollView>
</View>
{/* 资讯动态 */}
<View className='mt-1'>
<SectionTitle title='资讯动态' moreUrl='/official/article' />
@@ -156,6 +236,16 @@ const OfficialIndex: React.FC = () => {
<Text className='text-white text-sm font-medium'>📩 </Text>
</View>
</View>
{/* 底部联系方式 */}
<View className='mx-3 mb-4 bg-white rounded-lg p-4'>
<Text className='text-sm text-gray-500 block'>{contact.phone || '—'}</Text>
<Text className='text-sm text-gray-500 block mt-2'>{contact.email || '—'}</Text>
<Text className='text-sm text-gray-500 block mt-2'>{contact.address || '—'}</Text>
{contact.icpNo ? (
<Text className='text-xs text-gray-300 block mt-3'>{contact.icpNo}</Text>
) : null}
</View>
</ScrollView>
<OfficialTabBar active='home' />
+31 -2
View File
@@ -1,8 +1,10 @@
import React, { useState } from 'react'
import React, { useState, useEffect } from 'react'
import { View, Text, Input, Textarea } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { submitCmsContactLead } from '@/api/official'
import { submitCmsContactLead, getCmsWebsiteSiteInfo } from '@/api/official'
import type { CmsWebsite } from '@/api/official'
import NavBar from '@/components/NavBar'
import { DEMO_SITE } from '../data/demo'
definePageConfig({
navigationBarTitleText: '留言咨询',
@@ -18,6 +20,20 @@ interface FormState {
const Message: React.FC = () => {
const [form, setForm] = useState<FormState>({ name: '', phone: '', company: '', need: '' })
const [submitting, setSubmitting] = useState(false)
const [site, setSite] = useState<CmsWebsite>()
useEffect(() => {
getCmsWebsiteSiteInfo()
.then((s) => setSite(s))
.catch(() => {})
}, [])
const effectiveSite: CmsWebsite = site ?? (DEMO_SITE as CmsWebsite)
const contact = {
phone: effectiveSite.phone || DEMO_SITE.phone,
email: effectiveSite.email || DEMO_SITE.email,
address: effectiveSite.address || DEMO_SITE.address,
}
const set = (k: keyof FormState, v: string) =>
setForm((prev) => ({ ...prev, [k]: v }))
@@ -47,6 +63,19 @@ const Message: React.FC = () => {
<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 mb-3'>
<Text className='text-sm text-gray-800 font-medium block'>
{effectiveSite.websiteName || '企业官网'}
</Text>
<Text className='text-xs text-gray-400 block mt-1'></Text>
<View className='mt-3 pt-3 border-t border-gray-100'>
<Text className='text-xs text-gray-500 block'>{contact.phone || '—'}</Text>
<Text className='text-xs text-gray-500 block mt-1'>{contact.email || '—'}</Text>
<Text className='text-xs text-gray-500 block mt-1'>{contact.address || '—'}</Text>
</View>
</View>
<View className='bg-white rounded-lg p-4'>
<Field label='称呼' placeholder='请输入您的称呼' value={form.name} onInput={(v) => set('name', v)} />
<Field
+31 -5
View File
@@ -1,13 +1,15 @@
import React, { useState, useEffect } from 'react'
import { View, Text, ScrollView } from '@tarojs/components'
import { View, Text, ScrollView, Button } 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'
import { DEMO_SITE } from '../data/demo'
definePageConfig({
navigationBarTitleText: '我的',
enableShareAppMessage: true,
})
const Mine: React.FC = () => {
@@ -23,6 +25,14 @@ const Mine: React.FC = () => {
.catch(() => {})
}, [])
const effectiveSite: CmsWebsite = site ?? (DEMO_SITE as CmsWebsite)
const siteName = effectiveSite.websiteName || '企业官网'
Taro.useShareAppMessage(() => ({
title: `${siteName} · 企业官网`,
path: '/official/index',
}))
const menus = [
{ label: '我的留言', value: leadCount ? `${leadCount}` : '暂无', url: '/official/message' },
{ label: '关于我们', url: '/official/about' },
@@ -35,12 +45,10 @@ const Mine: React.FC = () => {
<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>
<Text className='text-white text-xl'>{(siteName || '官').slice(0, 1)}</Text>
</View>
<View className='ml-3'>
<Text className='text-base font-semibold text-gray-800'>
{site?.websiteName || '企业官网'}
</Text>
<Text className='text-base font-semibold text-gray-800'>{siteName}</Text>
<Text className='text-xs text-gray-400 mt-1'></Text>
</View>
</View>
@@ -56,6 +64,24 @@ const Mine: React.FC = () => {
<Text className='text-xs text-gray-400'>{m.value || ''}</Text>
</View>
))}
{/* 分享官网 */}
<Button
openType='share'
className='flex items-center justify-between px-4 py-4 w-full text-left bg-white'
>
<Text className='text-sm text-gray-700'></Text>
<Text className='text-xs text-gray-400'></Text>
</Button>
</View>
<View className='mt-4 mx-4'>
<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>
</ScrollView>
<OfficialTabBar active='mine' />