feat: implement miniapp C-group pages with real API calls and design tokens

This commit is contained in:
Codex
2026-09-14 10:02:22 +08:00
parent bb380d40f6
commit 63cbbff2de
20 changed files with 300 additions and 23 deletions
+76 -1
View File
@@ -1 +1,76 @@
Page({data:{categories:['推荐','认识自己','沟通成长','关系经营','婚恋反诈'],active:0,articles:[{id:'1',title:'第一次见面,怎样自然地开始交流',category:'沟通成长',read:'5分钟'},{id:'2',title:'建立健康关系,需要怎样的边界',category:'关系经营',read:'6分钟'},{id:'3',title:'警惕以投资为名的婚恋骗局',category:'婚恋反诈',read:'4分钟'}]},select(e){this.setData({active:e.currentTarget.dataset.index})},open(e){wx.navigateTo({url:'/pages/content-detail/content-detail?id='+e.currentTarget.dataset.id})}})
const api = require('../../utils/api')
const formatDate = (value) => {
if (!value) return ''
const date = new Date(value)
if (Number.isNaN(date.getTime())) return ''
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${date.getFullYear()}-${month}-${day}`
}
Page({
data: {
categories: [{ id: '', name: '推荐' }],
activeCategoryId: '',
articles: [],
loading: true,
loadError: false,
},
onLoad() {
this.loadPage()
},
async loadPage() {
this.setData({ loading: true, loadError: false })
try {
const [categories, articles] = await Promise.all([
api.get('/content/categories'),
api.get('/content/articles'),
])
this.setData({
categories: [{ id: '', name: '推荐' }, ...(categories || [])],
articles: this.normalizeArticles(articles || [], categories || []),
loading: false,
})
} catch (error) {
console.error('加载婚恋课堂失败', error)
this.setData({ loading: false, loadError: true })
}
},
normalizeArticles(articles, categories) {
const categoryNames = new Map(categories.map((item) => [item.id, item.name]))
return articles.map((item) => ({
...item,
categoryName: categoryNames.get(item.categoryId) || '婚恋课堂',
publishAtText: formatDate(item.publishAt || item.createdAt),
}))
},
async select(e) {
const categoryId = e.currentTarget.dataset.id || ''
if (categoryId === this.data.activeCategoryId) return
this.setData({ activeCategoryId: categoryId, loading: true, loadError: false })
try {
const articles = await api.get('/content/articles', categoryId ? { categoryId } : undefined)
this.setData({
articles: this.normalizeArticles(articles || [], this.data.categories),
loading: false,
})
} catch (error) {
console.error('加载课堂分类失败', error)
this.setData({ loading: false, loadError: true })
}
},
open(e) {
const id = e.currentTarget.dataset.id
if (id) wx.navigateTo({ url: `/pages/content-detail/content-detail?id=${id}` })
},
onRetry() {
this.loadPage()
},
})
+10 -1
View File
@@ -1 +1,10 @@
{"navigationBarTitleText":"婚恋课堂","backgroundColor":"#FAFAF8"}
{
"navigationBarTitleText": "婚恋课堂",
"backgroundColor": "#F4F5FA",
"usingComponents": {
"ui-service-hero": "/components/service-hero/index",
"ui-loading": "/components/loading/index",
"ui-empty": "/components/empty/index",
"ui-error-state": "/components/error-state/index"
}
}
+19 -1
View File
@@ -1 +1,19 @@
<view class="service-page reference-page"><view class="hero"><text class="eyebrow">婚恋课堂</text><text class="hero-title">认真学习,更好地相遇</text><text class="hero-desc">提供真实、克制、可实践的关系成长与安全知识。</text></view><scroll-view scroll-x style="white-space:nowrap;padding:24rpx 32rpx;box-sizing:border-box"><text wx:for="{{categories}}" wx:key="*this" data-index="{{index}}" bindtap="select" class="tag {{active===index ? '' : 'warning'}}">{{item}}</text></scroll-view><view class="content"><view wx:for="{{articles}}" wx:key="id" class="article-card" data-id="{{item.id}}" bindtap="open"><view class="article-cover"><text>{{item.category}}</text></view><view class="article-body"><text class="article-title">{{item.title}}</text><text class="article-meta">{{item.category}} · 阅读约 {{item.read}}</text></view></view></view></view>
<view class="content-page app-page reference-page">
<ui-service-hero eyebrow="学习成长 · 认真相遇" title="婚恋课堂" subtitle="学习成长,收获更好的自己" image-src="/assets/covers/classroom-default.jpg" tone="rose" />
<scroll-view scroll-x class="content-categories">
<view class="content-category-row"><view wx:for="{{categories}}" wx:key="id" data-id="{{item.id}}" bindtap="select" class="content-category {{activeCategoryId === item.id ? 'active' : ''}}">{{item.name}}</view></view>
</scroll-view>
<view class="content-list">
<ui-loading wx:if="{{loading}}" mode="skeleton" rows="4" />
<ui-error-state wx:elif="{{loadError}}" title="暂时无法加载课堂内容" description="请检查网络后重试" bind:retry="onRetry" />
<view wx:elif="{{articles.length}}">
<view wx:for="{{articles}}" wx:key="id" class="content-card" data-id="{{item.id}}" bindtap="open">
<image class="content-cover" src="{{item.coverFileId || '/assets/covers/classroom-default.jpg'}}" mode="aspectFill" />
<view class="content-card__body"><text class="content-card__category">{{item.categoryName}}</text><text class="content-card__title">{{item.title}}</text><text wx:if="{{item.summary}}" class="content-card__summary">{{item.summary}}</text><view class="content-card__meta"><text>浏览 {{item.viewCount || 0}}</text><text>{{item.publishAtText}}</text></view></view>
<image class="content-card__arrow" src="/assets/icons/ui/chevron-right.png" mode="aspectFit" />
</view>
</view>
<ui-empty wx:else illustration="default" title="暂无课堂内容" description="公益内容正在准备中,请稍后再来" />
</view>
</view>
+2 -1
View File
@@ -1 +1,2 @@
@import "../../styles/service-page.wxss";
@import "../../styles/service-page.wxss";
.content-page{min-height:100vh;padding-bottom:40rpx;background:#F4F5FA}.content-categories{padding:24rpx 0;white-space:nowrap}.content-category-row{display:inline-flex;gap:12rpx;padding:0 24rpx}.content-category{padding:11rpx 22rpx;border-radius:24rpx;background:#fff;color:#767D96;font-size:21rpx;box-shadow:0 6rpx 18rpx rgba(35,35,95,.05)}.content-category.active{background:linear-gradient(135deg,var(--mini-accent-coral),var(--mini-accent-pink));color:#fff;font-weight:650}.content-list{padding:0 24rpx}.content-card{display:flex;align-items:center;overflow:hidden;min-height:150rpx;margin-bottom:16rpx;border-radius:24rpx;background:#fff;box-shadow:0 8rpx 26rpx rgba(35,35,95,.06)}.content-cover{width:176rpx;height:150rpx;flex:none}.content-card__body{display:flex;flex:1;min-width:0;flex-direction:column;padding:16rpx}.content-card__category{align-self:flex-start;padding:4rpx 10rpx;border-radius:12rpx;background:#FFF0F5;color:var(--mini-accent-pink);font-size:17rpx}.content-card__title{overflow:hidden;margin-top:8rpx;color:#283056;font-size:24rpx;font-weight:650;white-space:nowrap;text-overflow:ellipsis}.content-card__summary{overflow:hidden;margin-top:5rpx;color:#83899F;font-size:18rpx;white-space:nowrap;text-overflow:ellipsis}.content-card__meta{display:flex;gap:18rpx;margin-top:9rpx;color:#9A9EB0;font-size:17rpx}.content-card__arrow{width:24rpx;height:24rpx;margin-right:16rpx;flex:none}