feat(official): 实现企业官网分包及内容展示
- 新增官方分包目录及页面(首页、资讯列表、详情、关于我们、留言、我的) - 自定义实现底部导航组件 OfficialTabBar,解决微信 tabBar 不支持分包问题 - 统一接口调用走 shop-api 代理,无新增 CMS 域名,减少合法域名负担 - 新增 CMS 站点信息获取接口及留言咨询提交与查询接口 - 设计并完善官网富文本样式,品牌色统一为 #185FA5,并更新 tailwind 配置 - 首页新增企业官网入口卡片,方便用户访问官网内容 - 完成官网分包构建验证,解决 Taro 构建批量删除保护问题 - 规范 CMS 字段使用,适配字段名称及状态含义 - 后续计划对历史页面 NutUI 组件进行空壳化整改,及更新 UI 设计文档
This commit is contained in:
Generated
+18
-15
@@ -4,10 +4,13 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="2de94e4f-0587-4c9b-8bfd-e87e0ae95b81" name="更改" comment="feat(checkout): 接入微信支付功能并优化支付交互体验 - 支付方式新增微信支付(payType=1)并设为默认,保留线下付款(payType=9) - 提交订单时若选择微信支付,调用 Taro.requestPayment 调起支付流程 - 支付取消或失败时给出明确提示,订单已创建可重新支付 - 线下付款保持原有“订单已提交”提示 - 支付选项选中时微信显示绿色提示,线下显示橙色提示 - 提交按钮文案区分微信支付(提交并支付)和线下付款(提交订单) - 后端支付配置错误时统一提示“微信支付暂不可用:支付配置缺失” - 确保编译构建通过,无新引入类型错误">
|
||||
<list default="true" id="2de94e4f-0587-4c9b-8bfd-e87e0ae95b81" name="更改" comment="feat(checkout): 优化微信支付和线下付款流程 - 支付方式新增微信支付(payType=1),并设为默认,保留线下付款(payType=9) - 微信支付时调用 Taro.requestPayment 发起支付流程,取消或失败给出明确提示 - 支付选项选中状态下微信支付显示绿色提示,线下付款显示橙色提示 - 提交按钮文案区分微信支付(提交并支付)与线下付款(提交订单) - 后端支付配置缺失时提示“微信支付暂不可用:支付配置缺失” - 确保编译构建通过,未引入新类型错误">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/config/app.ts" beforeDir="false" afterPath="$PROJECT_DIR$/config/app.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/project.config.json" beforeDir="false" afterPath="$PROJECT_DIR$/project.config.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/api/cms/cmsWebsite/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/api/cms/cmsWebsite/index.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/app.config.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/app.config.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/app.scss" beforeDir="false" afterPath="$PROJECT_DIR$/src/app.scss" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/index/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/index/index.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/tailwind.config.js" beforeDir="false" afterPath="$PROJECT_DIR$/tailwind.config.js" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -122,15 +125,7 @@
|
||||
<workItem from="1784638429525" duration="16147000" />
|
||||
<workItem from="1784798575885" duration="4896000" />
|
||||
<workItem from="1784873955217" duration="3822000" />
|
||||
<workItem from="1786371937331" duration="7602000" />
|
||||
</task>
|
||||
<task id="LOCAL-00028" summary="feat(userVerify): 添加阿里云身份证二要素核验功能 - 在接口层新增 verifyIdCard 接口,实现姓名与身份证号核验 - userVerify 页面提交时增加身份核验逻辑,核验失败阻止提交 - 调用核验接口时显示加载状态,并根据结果提示用户 - 后端依赖 aliyun cloudauth,提供实人认证相关服务接口 - 完成基本的错误捕获与用户提示,提升实名认证流程安全性">
|
||||
<option name="closed" value="true" />
|
||||
<created>1776021764419</created>
|
||||
<option name="number" value="00028" />
|
||||
<option name="presentableId" value="LOCAL-00028" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1776021764419</updated>
|
||||
<workItem from="1786371937331" duration="8819000" />
|
||||
</task>
|
||||
<task id="LOCAL-00029" summary="refactor(api): 规范前端API接口地址并统一路径前缀 - 修正开发者、小程序和企业相关API的基础URL,避免重复添加/api前缀 - 统一调整developer、enterprise、invite等模块接口路径,保持与后端Controller一致 - 新增权限申请及消息通知相关API,并完善相关函数实现 - 规范请求参数传递,移除多余包装,改用直接传递对象方式 - 新增API Key重置、版本发布/回滚等接口支持 feat(invite): 重构邀请登录流程支持未注册快速加入 - 完整重写invite页,分离已注册用户和未注册用户的按钮显示和交互 - 实现未注册用户微信手机号授权后自动注册登录及自动加入应用 - 已注册用户直接确认加入应用,无需手机号授权弹窗 - 统一使用getPhoneNumber按钮处理两种状态并修复“授权码不能为空”报错 - 前端配合后端改造,支持已登录用户可直接通过Authorization头加入应用 fix(developer): 修复开发者中心应用加载问题 - 修复只查询创建的应用导致邀请加入的应用不显示的问题 - 同时请求创建应用和参与应用接口,合并并去重应用列表 - 新增pageJoinedApp接口调用及合并逻辑,提高应用列表完整性和体验">
|
||||
<option name="closed" value="true" />
|
||||
@@ -516,7 +511,15 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1786372231925</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="77" />
|
||||
<task id="LOCAL-00077" summary="feat(checkout): 优化微信支付和线下付款流程 - 支付方式新增微信支付(payType=1),并设为默认,保留线下付款(payType=9) - 微信支付时调用 Taro.requestPayment 发起支付流程,取消或失败给出明确提示 - 支付选项选中状态下微信支付显示绿色提示,线下付款显示橙色提示 - 提交按钮文案区分微信支付(提交并支付)与线下付款(提交订单) - 后端支付配置缺失时提示“微信支付暂不可用:支付配置缺失” - 确保编译构建通过,未引入新类型错误">
|
||||
<option name="closed" value="true" />
|
||||
<created>1786407521622</created>
|
||||
<option name="number" value="00077" />
|
||||
<option name="presentableId" value="LOCAL-00077" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1786407521622</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="78" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -536,7 +539,6 @@
|
||||
<component name="VcsManagerConfiguration">
|
||||
<option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="false" />
|
||||
<option name="CHECK_NEW_TODO" value="false" />
|
||||
<MESSAGE value="fix(index): 优化外链提示及删除复制咨询模板功能 - 替换小程序内外链打开方式提示为弹窗提醒用户手动复制 - 删除复制咨询模板功能及相关按钮 - 修改联系我们文案,突出直接电话沟通或需求反馈 - 移除无用导入的复制文本工具函数copyText" />
|
||||
<MESSAGE value="feat(utils): 优化请求函数URL拼接逻辑 - 在request函数中将请求拦截改为先拼接完整URL - 支持相对路径自动补全baseUrl - 删除原有请求拦截中url未处理的问题" />
|
||||
<MESSAGE value="docs(schedule): 添加2026年7月1日日程文件 - 创建新的日程文档文件 2026-07-01.md - 文件内容待后续补充和更新" />
|
||||
<MESSAGE value="feat(pay): 优化小程序支付流程并增强日志调试 - 更新 wx-login 接口以返回完整 LoginResult,包括 access_token 和 user 对象 - 支付页面新增大量日志,详尽打印参数、请求和响应信息 - 优化获取订阅详情流程,支持 returnRaw 返回完整响应用于调试 - 支付流程新增步骤,先通过 code 获取 openid 并缓存,再尝试用 openid 自动登录 - 增加自动登录失败交互,提示未注册用户先登录 - 支付相关请求均使用 returnRaw 方便错误排查 - request 请求模块添加统一请求和响应日志,增强调试能力 - request 认证错误和业务错误时加入详细日志输出" />
|
||||
@@ -561,7 +563,8 @@
|
||||
<MESSAGE value="feat(user): 合并切换开发者菜单项状态并更新开发者中心UI - 用户菜单中根据 isDeveloper 字段条件展示“申请成为开发者”或“进入开发者中心” - 已审核通过用户菜单显示“进入开发者中心”,并设置高亮样式 - 去除冗余的独立“开发者中心”菜单条目,简化菜单结构 - 重新设计开发者中心页面UI,统一为门店中心风格 - 顶部展示开发者头像及昵称手机号信息 - 功能卡片展示“我的应用”、“API Key”和“我的工单”,异步加载数量角标 - 底部添加“开发者帮助与支持”入口,替代门店订阅消息卡片 - 未登录或非开发者状态显示相应空态提示及操作按钮 - 优化登录状态与权限校验逻辑,防止页面闪烁和无权限内容显示" />
|
||||
<MESSAGE value="feat(user): 合并切换开发者菜单并优化首页轮播图显示 - 用户菜单根据 isDeveloper 字段切换显示申请或进入开发者中心 - 已审核用户菜单高亮显示“进入开发者中心” - 去除冗余开发者中心菜单条目,简化结构 - 重新设计开发者中心页面UI,统一门店中心风格 - 顶部展示开发者头像及昵称手机号信息 - 功能卡片展示“我的应用”、“API Key”和“我的工单”,异步加载数量角标 - 底部添加“开发者帮助与支持”入口,替代门店订阅消息卡片 - 未登录或非开发者状态显示相应空态及操作按钮 - 首页轮播图为空时不显示容器,功能入口自动上移,优化空状态体验 - 优化登录状态与权限校验逻辑,防止页面闪烁和无权限展示" />
|
||||
<MESSAGE value="feat(checkout): 优化微信支付和线下付款流程 - 将默认支付方式调整为微信支付(payType=1),增加支付方式选择项 - 微信支付成功后直接调用微信支付接口并处理支付结果反馈 - 支付取消或失败时显示对应提示信息 - 线下付款(payType=9)保留并显示相应提示,不触发在线支付 - 提交订单时根据支付方式显示不同的按钮文本 (‘提交并支付’ 或 ‘提交订单’) - 针对微信支付配置缺失的错误,统一显示详细提示,便于定位问题 - 优化支付方式选中时的提示样式与说明,提升用户体验" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="feat(checkout): 优化微信支付和线下付款流程 - 将默认支付方式调整为微信支付(payType=1),增加支付方式选择项 - 微信支付成功后直接调用微信支付接口并处理支付结果反馈 - 支付取消或失败时显示对应提示信息 - 线下付款(payType=9)保留并显示相应提示,不触发在线支付 - 提交订单时根据支付方式显示不同的按钮文本 (‘提交并支付’ 或 ‘提交订单’) - 针对微信支付配置缺失的错误,统一显示详细提示,便于定位问题 - 优化支付方式选中时的提示样式与说明,提升用户体验" />
|
||||
<MESSAGE value="feat(checkout): 优化微信支付和线下付款流程 - 支付方式新增微信支付(payType=1),并设为默认,保留线下付款(payType=9) - 微信支付时调用 Taro.requestPayment 发起支付流程,取消或失败给出明确提示 - 支付选项选中状态下微信支付显示绿色提示,线下付款显示橙色提示 - 提交按钮文案区分微信支付(提交并支付)与线下付款(提交订单) - 后端支付配置缺失时提示“微信支付暂不可用:支付配置缺失” - 确保编译构建通过,未引入新类型错误" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="feat(checkout): 优化微信支付和线下付款流程 - 支付方式新增微信支付(payType=1),并设为默认,保留线下付款(payType=9) - 微信支付时调用 Taro.requestPayment 发起支付流程,取消或失败给出明确提示 - 支付选项选中状态下微信支付显示绿色提示,线下付款显示橙色提示 - 提交按钮文案区分微信支付(提交并支付)与线下付款(提交订单) - 后端支付配置缺失时提示“微信支付暂不可用:支付配置缺失” - 确保编译构建通过,未引入新类型错误" />
|
||||
<option name="NON_MODAL_COMMIT_POSTPONE_SLOW_CHECKS" value="false" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -8,3 +8,34 @@
|
||||
- B端移动(商家版):暂缓,验证需求后再建独立 AppID 小程序,不并入 C 端。
|
||||
- 已产出:规划文档 `.workbuddy/miniprogram-ui-plan.md`、官网展示型小程序原型(4屏 mockup)、产品矩阵分层架构图。
|
||||
- 待确认:落地路径(独立官网小程序 / websopy-taro 加分包 / 仅 CMS 展示层)及上述产品矩阵方向。
|
||||
|
||||
## 官网分包(official)落地实现 —— 已完成并构建通过
|
||||
用户确认方向后,在 websopy-taro 内新增 `official` 分包实现企业官网展示端,并对接真实 CMS 内容。
|
||||
|
||||
### 目录与文件(新增 12 个)
|
||||
- 页面 `src/official/`:`index`(首页:品牌栏+Banner轮播+栏目宫格+资讯动态+留言CTA)、`article`(资讯列表,滚动分页+categoryId筛选)、`article-detail`(RichText 详情)、`about`(关于我们)、`message`(留言表单)、`mine`(我的)。
|
||||
- 组件 `src/official/components/`:`OfficialTabBar`(自实现底部导航,redirectTo 切换)、`ArticleCard`、`SectionTitle`。
|
||||
- 数据层:`src/api/official/index.ts`(统一出口,re-export `src/api/cms/*`)、`src/api/cms/cmsContactLead/{index.ts,model/index.ts}`。
|
||||
|
||||
### 修改(5 处)
|
||||
- `src/api/cms/cmsWebsite/index.ts`:新增 `getCmsWebsiteSiteInfo()` → GET `/cms/cms-website/getSiteInfo`。
|
||||
- `src/app.config.ts`:subpackages 增加 `{root:'official', pages:[index,article,article-detail,about,message,mine]}`。
|
||||
- `tailwind.config.js`:新增 `brand` 色阶(600=#185FA5)。
|
||||
- `src/app.scss`:`:root` 增 `--brand-color/--brand-light`;新增 `.official-rich` 富文本样式(img/p/h1-h4/ul/blockquote/table 等,px 自动转 rpx,已验证打进 app-origin.wxss)。
|
||||
- `src/pages/index/index.tsx`:搜索栏下方新增「企业官网」入口卡片 → `/official/index`。
|
||||
|
||||
### 关键技术决策
|
||||
- **请求链路复用默认 shop-api**:原本打算给官网单配 `CMS_API_BASE` 直连 `cms-api.websoft.top`,但发现现有 `listCmsAd`/`listCmsArticle` 走默认 `https://shop-api.websoft.top/api` 且线上可用 → 说明 shop-api 已代理 `/cms`,**不新增域名**(避免小程序 request 合法域名白名单问题)。租户靠 `TenantId` 请求头(config/app.ts 当前 TenantId='5')。
|
||||
- **微信 tabBar 不支持分包页** → 用 `OfficialTabBar` 自实现(redirectTo)。
|
||||
- **规避 NutUI weapp 空壳化**:官网 6 页全部用 Taro 原生组件 + 自定义组件,构建产物 grep `nut-cell|nut-price|nut-divider` = 0。
|
||||
- CMS 字段陷阱(已按此实现):文章封面 `image` 非 cover、摘要 `overview` 非 summary;`ARTICLE_STATUS.PUBLISHED = 0`;留言字段是 `need` 非 content,提交带 `source:'miniapp'`。
|
||||
|
||||
### 构建验证
|
||||
- `npm run build:weapp` → `✔ Webpack Compiled successfully in 20.06s`,Exit 0。
|
||||
- 产物 `dist/official/` 6 页齐全,分包体积 80K。
|
||||
- ⚠️ 坑:Taro 构建前清空 `dist/`(641 文件)会触发批量删除保护而中断。解决:先 `mv dist /tmp/xxx` 再构建。
|
||||
|
||||
### 后续待办
|
||||
- 历史页面(order/booking 等 28 文件)仍引用 NutUI,需按同样模式去空壳化。
|
||||
- `.workbuddy/miniprogram-ui-plan.md` 需更新为"多分包品牌小程序"版本。
|
||||
- 真机联调需确认后端 TenantId 与 `/cms/cms-website/getSiteInfo`、`/cms/cms-contact-lead/submit` 两个接口在 shop-api 侧确实已代理。
|
||||
|
||||
@@ -22,6 +22,17 @@
|
||||
- 用户变更后如需同步 websopy,经 `syncMessageProducer.sendUserSyncMessage("websopy","UPDATE",user)`(在 `UserServiceImpl` 内,`@Autowired(required=false)` 注入)。
|
||||
- 用户相关代码都在 `com.gxwebsoft.common.system`(entity/service/controller),业务应用数据在 `com.gxwebsoft.websopy`(无 Controller)。
|
||||
|
||||
## 官网分包 official(2026-08-11 落地)
|
||||
- 位置 `src/official/`(6 页:index/article/article-detail/about/message/mine)+ `src/official/components/`(OfficialTabBar/ArticleCard/SectionTitle),在 `src/app.config.ts` 的 `subpackages` 中 root=`official`。入口在 `src/pages/index/index.tsx` 搜索栏下的「企业官网」卡片。
|
||||
- 数据层统一从 `@/api/official` 引入(re-export `src/api/cms/*`)。**内容请求走默认 shop-api(`https://shop-api.websoft.top/api`),shop-api 已代理 `/cms` 前缀,不要另配 cms-api 域名**(避免小程序合法域名白名单成本)。租户靠 `TenantId` 请求头。
|
||||
- CMS 字段陷阱:文章封面 `image`(非 cover)、摘要 `overview`(非 summary)、`ARTICLE_STATUS.PUBLISHED = 0`(0 才是已发布);留言内容字段是 `need`(非 content),小程序提交带 `source:'miniapp'`。
|
||||
- 微信原生 tabBar **不支持分包页面**,官网底部导航是自实现的 `OfficialTabBar`(用 `redirectTo`)。
|
||||
- 品牌色统一 token:tailwind `brand` 色阶(600=`#185FA5`)+ `app.scss` 的 `--brand-color/--brand-light`。富文本用 `.official-rich`(定义在 `app.scss`,px 自动转 rpx)。
|
||||
|
||||
## ⚠️ Taro 构建陷阱:dist 清空触发批量删除保护(2026-08-11)
|
||||
`npm run build:weapp` 启动时 Taro 会 `emptyOutputDir` 清空 `dist/`,文件数 >50 会被安全删除守卫中断并抛 `SAFE_DELETE_BULK_CONFIRM_REQUIRED`。
|
||||
**解决**:构建前先 `mv dist /tmp/taro-dist-bak/dist-$(date +%s)`,再执行构建,完成后 `rm -rf /tmp/taro-dist-bak`。
|
||||
|
||||
## ⚠️ Taro 小程序 nutui-react-taro 兼容性陷阱(2026-07-23 确认)
|
||||
|
||||
**问题**:`@nutui/nutui-react-taro@2.7.4` 的 **Cell / Price / Divider / Button** 等展示类组件,内部用 `React.createElement("div", ...)` 构建布局。Taro 4 的 babel/swc 没有把这些 `div` 转成 `view`,而 `dist/base.wxml` 里完全没有 `<div>` 模板,导致这些组件在 weapp 中"空壳化"——外层元素不存在,内层内容(`<div>`)被丢弃。
|
||||
|
||||
@@ -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'>
|
||||
|
||||
+14
-1
@@ -15,7 +15,20 @@ module.exports = {
|
||||
700: '#0369a1',
|
||||
800: '#075985',
|
||||
900: '#0c4a6e',
|
||||
}
|
||||
},
|
||||
// 官网(企业官网展示端)品牌色,统一设计 token
|
||||
brand: {
|
||||
50: '#EAF2FB',
|
||||
100: '#CFE2F6',
|
||||
200: '#A6C8EC',
|
||||
300: '#6FA8DE',
|
||||
400: '#3D86CE',
|
||||
500: '#1E6BB8',
|
||||
600: '#185FA5',
|
||||
700: '#12518C',
|
||||
800: '#0E4070',
|
||||
900: '#0A2E50',
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user