feat(miniprogram): redesign public experience

This commit is contained in:
2026-08-13 09:52:28 +08:00
parent 591a9d4d2f
commit 9622d249be
35 changed files with 1736 additions and 243 deletions
+3 -1
View File
@@ -1,5 +1,7 @@
# 微信小程序
微信原生小程序 + TypeScript。Phase 1 已接入微信登录和短期会话;Phase 2–4 提供服务申请、协作任务和闭环;Phase 5 提供公开公益项目/活动、在线报名、志愿者认证、个人报名进度和服务时长。敏感申请草稿、志愿者资料和工单内容不写入本地持久化。
微信原生小程序 + TypeScript。公众端使用“首页 / 服务 / 公益 / 我的”四栏导航:首页突出本人/代申请与六类服务,服务页承载求助闭环,公益页展示公开项目和活动,我的页面按身份提供申请、消息、志愿服务和协作任务。Phase 1 已接入微信登录和短期会话;Phase 2–4 提供服务申请、协作任务和闭环;Phase 5 提供公开公益项目/活动、在线报名、志愿者认证、个人报名进度和服务时长。敏感申请草稿、志愿者资料和工单内容不写入本地持久化。
视觉系统集中定义在 `miniprogram/app.wxss`,包含暖橙主色、语义色、适老字号、圆角、阴影、状态卡和按钮基线;自定义底部导航位于 `miniprogram/custom-tab-bar/`。新增页面必须复用这些令牌并保持 loading、empty、error 与权限感知状态,不得加入未批准的捐赠交易或虚构业务数据。
使用微信开发者工具导入本目录;真实 AppID 只保存在本地私有配置中,不提交仓库。本地默认 API 为 `http://localhost:3000`;提审或发布前必须把 `miniprogram/app.ts` 中的地址替换为已加入微信 request 合法域名的 HTTPS 地址,不得发布本地 HTTP 配置。
+16 -1
View File
@@ -1,6 +1,9 @@
{
"pages": [
"pages/index/index",
"pages/service-hub/service-hub",
"pages/public-welfare/public-welfare",
"pages/profile/profile",
"pages/application-form/application-form",
"pages/application-success/application-success",
"pages/my-applications/my-applications",
@@ -17,11 +20,23 @@
"pages/notifications/notifications"
],
"window": {
"backgroundColor": "#F5F5F5",
"backgroundColor": "#F7F5F2",
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTextStyle": "black",
"navigationBarTitleText": "乐活公益"
},
"tabBar": {
"custom": true,
"color": "#77716B",
"selectedColor": "#F05A28",
"backgroundColor": "#FFFFFF",
"list": [
{ "pagePath": "pages/index/index", "text": "首页" },
{ "pagePath": "pages/service-hub/service-hub", "text": "服务" },
{ "pagePath": "pages/public-welfare/public-welfare", "text": "公益" },
{ "pagePath": "pages/profile/profile", "text": "我的" }
]
},
"sitemapLocation": "sitemap.json"
}
+212 -59
View File
@@ -1,15 +1,27 @@
page {
--color-primary: #ff6b35;
--color-primary-pressed: #e95a28;
--color-primary-soft: #fff1eb;
--color-success: #2f8f2f;
--color-info: #1677c8;
/* 乐活公益公众端设计令牌:暖、可信、适老。 */
--color-primary: #f05a28;
--color-primary-pressed: #d94718;
--color-primary-soft: #fff0e8;
--color-primary-pale: #fff8f4;
--color-secondary: #f6a23c;
--color-success: #2f9b58;
--color-success-soft: #eaf7ef;
--color-info: #287fd1;
--color-info-soft: #ebf4fd;
--color-warning: #b76e00;
--color-warning-soft: #fff6dd;
--color-danger: #b42318;
--color-text: #1f2329;
--color-text-secondary: #646a73;
--color-border: #e5e6eb;
--color-background: #f5f5f5;
--color-text: #24211f;
--color-text-secondary: #6e6862;
--color-text-tertiary: #918a83;
--color-border: #e9e3dd;
--color-background: #f7f5f2;
--color-surface: #ffffff;
--radius-sm: 16rpx;
--radius-md: 24rpx;
--radius-lg: 32rpx;
--shadow-card: 0 10rpx 32rpx rgba(91, 62, 45, 0.07);
min-height: 100%;
color: var(--color-text);
background: var(--color-background);
@@ -17,27 +29,63 @@ page {
-apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Microsoft YaHei',
sans-serif;
font-size: 32rpx;
line-height: 1.5;
}
button {
font: inherit;
}
button::after {
border: 0;
}
.page {
min-height: 100vh;
padding: 32rpx 32rpx calc(48rpx + env(safe-area-inset-bottom));
padding: 28rpx 28rpx calc(152rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.page-heading {
margin: 8rpx 4rpx 28rpx;
}
.page-title,
.page-subtitle,
.section-title,
.section-note {
display: block;
}
.page-title {
font-size: 44rpx;
font-weight: 700;
letter-spacing: 1rpx;
}
.page-subtitle {
margin-top: 8rpx;
color: var(--color-text-secondary);
font-size: 28rpx;
line-height: 1.65;
}
.section-heading {
display: flex;
margin: 40rpx 4rpx 20rpx;
align-items: flex-end;
justify-content: space-between;
gap: 20rpx;
}
.section-title {
font-size: 36rpx;
font-weight: 700;
}
.section-note {
color: var(--color-text-secondary);
font-size: 26rpx;
}
.eyebrow {
display: block;
color: var(--color-primary-pressed);
font-size: 28rpx;
font-weight: 600;
font-size: 26rpx;
font-weight: 700;
}
.primary-button,
@@ -48,105 +96,149 @@ button::after {
padding: 20rpx 32rpx;
align-items: center;
justify-content: center;
border-radius: 24rpx;
border-radius: 999rpx;
box-sizing: border-box;
font-size: 34rpx;
font-weight: 600;
font-weight: 700;
line-height: 1.4;
}
.primary-button {
color: #ffffff;
background: var(--color-primary);
color: #fff;
background: linear-gradient(135deg, #ff713d, var(--color-primary));
box-shadow: 0 12rpx 24rpx rgba(240, 90, 40, 0.2);
}
.primary-button:active {
background: var(--color-primary-pressed);
}
.secondary-button {
color: var(--color-primary-pressed);
background: var(--color-primary-soft);
border: 2rpx solid var(--color-primary);
background: #fff;
border: 2rpx solid #f5a083;
}
.primary-button[disabled],
.secondary-button[disabled] {
opacity: 0.5;
box-shadow: none;
}
.state-card,
.form-card,
.detail-card,
.detail-summary,
.success-card,
.action-card,
.case-card,
.application-card {
border: 2rpx solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-surface);
box-shadow: var(--shadow-card);
}
.form-card,
.detail-card,
.detail-summary,
.success-card {
padding: 32rpx;
border: 2rpx solid var(--color-border);
border-radius: 32rpx;
background: var(--color-surface);
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.05);
}
.state-card {
display: flex;
min-height: 280rpx;
padding: 36rpx 30rpx;
flex-direction: column;
align-items: stretch;
justify-content: center;
box-sizing: border-box;
text-align: center;
}
.state-card::before {
content: '♡';
margin-bottom: 12rpx;
color: #f6a07d;
font-size: 56rpx;
}
.state-title {
font-size: 38rpx;
font-weight: 600;
font-weight: 700;
}
.state-text {
display: block;
margin-top: 16rpx;
margin-top: 14rpx;
color: var(--color-text-secondary);
line-height: 1.7;
}
.skeleton-card,
.skeleton-line {
overflow: hidden;
border-radius: 20rpx;
background: linear-gradient(90deg, #f0f1f3 25%, #fafafa 50%, #f0f1f3 75%);
background-size: 200% 100%;
animation: skeleton 1.4s ease-in-out infinite;
.action-card {
display: flex;
width: 100%;
min-height: 144rpx;
margin: 0 0 20rpx;
padding: 28rpx 30rpx;
flex-direction: column;
align-items: flex-start;
justify-content: center;
box-sizing: border-box;
color: var(--color-text);
text-align: left;
}
.action-card:active,
.case-card:active,
.application-card:active {
transform: scale(0.99);
opacity: 0.84;
}
.action-title {
font-size: 34rpx;
font-weight: 700;
}
.action-description {
display: block;
margin-top: 8rpx;
color: var(--color-text-secondary);
font-size: 28rpx;
line-height: 1.55;
}
.card-meta {
display: block;
margin-top: 16rpx;
color: var(--color-text-tertiary);
font-size: 25rpx;
}
.skeleton-card {
height: 160rpx;
margin-bottom: 24rpx;
.notice-card {
margin: 20rpx 0;
padding: 26rpx 28rpx;
border: 2rpx solid #f4dfc8;
border-radius: var(--radius-md);
background: #fffaf4;
}
.skeleton-line {
width: 60%;
height: 28rpx;
margin: 12rpx auto;
.notice-title,
.notice-text {
display: block;
}
.skeleton-line--wide {
width: 84%;
height: 44rpx;
.notice-title {
font-weight: 700;
}
.notice-text {
margin-top: 8rpx;
color: #6f583f;
font-size: 28rpx;
line-height: 1.7;
}
.status-pill {
display: inline-flex;
min-height: 48rpx;
padding: 8rpx 16rpx;
padding: 7rpx 16rpx;
align-items: center;
border-radius: 12rpx;
color: #145ea8;
background: #eaf4ff;
font-size: 26rpx;
font-weight: 600;
border-radius: 999rpx;
color: #17653a;
background: var(--color-success-soft);
font-size: 25rpx;
font-weight: 700;
line-height: 1.2;
}
.inline-error {
.inline-error,
.form-message {
display: block;
margin: 20rpx 8rpx 0;
color: var(--color-danger);
@@ -154,6 +246,61 @@ button::after {
line-height: 1.6;
}
.skeleton-card,
.skeleton-line {
overflow: hidden;
border-radius: var(--radius-md);
background: linear-gradient(90deg, #eeeae6 25%, #faf8f6 50%, #eeeae6 75%);
background-size: 200% 100%;
animation: skeleton 1.4s ease-in-out infinite;
}
.skeleton-card {
height: 170rpx;
margin-bottom: 20rpx;
}
.skeleton-line {
width: 60%;
height: 28rpx;
margin: 12rpx auto;
}
.skeleton-line--wide {
width: 84%;
height: 44rpx;
}
.brand-card {
margin-bottom: 24rpx;
padding: 38rpx 32rpx;
overflow: hidden;
border-radius: var(--radius-lg);
color: #762b12;
background: linear-gradient(135deg, #fff4ea 0%, #ffd4be 100%);
box-shadow: 0 14rpx 36rpx rgba(208, 93, 41, 0.13);
}
.title {
display: block;
margin-top: 10rpx;
font-size: 46rpx;
font-weight: 700;
line-height: 1.3;
}
.message {
display: block;
margin-top: 14rpx;
color: #70432e;
font-size: 29rpx;
line-height: 1.7;
}
input,
textarea,
.picker-field,
.filter-picker {
color: var(--color-text);
background: #fff;
border-color: #dcd5ce !important;
}
@keyframes skeleton {
0% {
background-position: 200% 0;
@@ -162,3 +309,9 @@ button::after {
background-position: -200% 0;
}
}
@media (prefers-reduced-motion: reduce) {
.skeleton-card,
.skeleton-line {
animation: none;
}
}
@@ -0,0 +1 @@
{ "component": true }
@@ -0,0 +1,25 @@
Component({
data: {
selected: 0,
tabs: [
{ pagePath: '/pages/index/index', text: '首页', icon: '⌂' },
{ pagePath: '/pages/service-hub/service-hub', text: '服务', icon: '♡' },
{ pagePath: '/pages/public-welfare/public-welfare', text: '公益', icon: '◎' },
{ pagePath: '/pages/profile/profile', text: '我的', icon: '○' },
],
},
lifetimes: {
attached() {
const route = `/${getCurrentPages().at(-1)?.route ?? 'pages/index/index'}`;
const selected = this.data.tabs.findIndex((item) => item.pagePath === route);
if (selected >= 0) this.setData({ selected });
},
},
methods: {
switchTab(event: WechatMiniprogram.BaseEvent) {
const index = Number(event.currentTarget.dataset['index']);
const tab = this.data.tabs[index];
if (tab) void wx.switchTab({ url: tab.pagePath });
},
},
});
@@ -0,0 +1,8 @@
<view class="tab-bar">
<view class="tab-bar__inner">
<button wx:for="{{tabs}}" wx:key="pagePath" class="tab {{selected === index ? 'tab--active' : ''}}" data-index="{{index}}" bindtap="switchTab" aria-label="{{item.text}}">
<text class="tab__icon" aria-hidden="true">{{item.icon}}</text>
<text class="tab__text">{{item.text}}</text>
</button>
</view>
</view>
@@ -0,0 +1,45 @@
.tab-bar {
position: fixed;
z-index: 100;
right: 0;
bottom: 0;
left: 0;
padding-bottom: env(safe-area-inset-bottom);
background: rgba(255, 255, 255, 0.98);
border-top: 2rpx solid #eee8e2;
box-shadow: 0 -8rpx 28rpx rgba(74, 55, 43, 0.06);
}
.tab-bar__inner {
display: flex;
height: 112rpx;
align-items: stretch;
}
.tab {
display: flex;
min-width: 0;
margin: 0;
padding: 10rpx 4rpx 8rpx;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
color: #77716b;
background: transparent;
font-size: 23rpx;
line-height: 1;
}
.tab__icon {
height: 40rpx;
font-size: 38rpx;
font-weight: 700;
line-height: 40rpx;
}
.tab__text {
margin-top: 7rpx;
}
.tab--active {
color: #f05a28;
}
.tab--active .tab__text {
font-weight: 700;
}
@@ -1 +1 @@
<view class="page"><view class="section-heading"><text class="section-title">公益活动</text><text class="section-note">已认证志愿者可在线报名</text></view><view wx:if="{{loading}}" class="state-card">正在加载活动…</view><view wx:elif="{{message}}" class="state-card">{{message}}</view><view wx:elif="{{!items.length}}" class="state-card">暂无可查看活动</view><button wx:for="{{items}}" wx:key="publicId" class="action-card" data-id="{{item.publicId}}" bindtap="open"><text class="action-title">{{item.title}}</text><text class="action-description">{{item.summary}}</text><text class="card-meta">{{item.statusLabel}} · 已报名 {{item.activeRegistrationCount}} / {{item.capacity === null ? '不限' : item.capacity}}</text></button></view>
<view class="page"><view class="page-heading"><text class="page-title">公益活动</text><text class="page-subtitle">已认证志愿者可在线报名,报名结果请在志愿者中心查看。</text></view><block wx:if="{{loading}}"><view class="skeleton-card"></view><view class="skeleton-card"></view></block><view wx:elif="{{message}}" class="state-card"><text class="state-title">活动暂时无法加载</text><text class="state-text">{{message}}</text><button class="secondary-button" bindtap="load">重新加载</button></view><view wx:elif="{{!items.length}}" class="state-card"><text class="state-title">暂无可查看活动</text><text class="state-text">有新的志愿活动时会展示在这里。</text></view><button wx:for="{{items}}" wx:key="publicId" class="activity-card" data-id="{{item.publicId}}" bindtap="open"><view class="activity-card__top"><text class="activity-card__tag">{{item.statusLabel}}</text><text class="activity-card__count">{{item.activeRegistrationCount}} / {{item.capacity === null ? '不限' : item.capacity}} 人</text></view><text class="activity-card__title">{{item.title}}</text><text class="activity-card__copy">{{item.summary}}</text><view class="activity-card__footer"><text>查看活动详情</text><text>›</text></view></button></view>
@@ -1,7 +1,56 @@
@import '../../app.wxss';
.card-meta {
display: block;
margin-top: 12rpx;
color: #646a73;
.activity-card {
display: flex;
width: 100%;
min-height: 210rpx;
margin: 0 0 20rpx;
padding: 28rpx;
flex-direction: column;
align-items: stretch;
color: var(--color-text);
border: 2rpx solid var(--color-border);
border-radius: 28rpx;
background: #fff;
text-align: left;
box-shadow: var(--shadow-card);
}
.activity-card__top,
.activity-card__footer {
display: flex;
align-items: center;
justify-content: space-between;
}
.activity-card__tag {
padding: 6rpx 14rpx;
border-radius: 999rpx;
color: #17653a;
background: var(--color-success-soft);
font-size: 23rpx;
font-weight: 700;
}
.activity-card__count {
color: var(--color-text-tertiary);
font-size: 24rpx;
}
.activity-card__title {
margin-top: 18rpx;
font-size: 34rpx;
font-weight: 700;
}
.activity-card__copy {
display: -webkit-box;
margin-top: 8rpx;
overflow: hidden;
color: var(--color-text-secondary);
font-size: 27rpx;
line-height: 1.55;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.activity-card__footer {
margin-top: 20rpx;
padding-top: 16rpx;
color: var(--color-primary);
border-top: 2rpx solid #f0ece8;
font-size: 26rpx;
font-weight: 700;
}
@@ -1 +1 @@
<view class="page"><view wx:if="{{loading}}" class="state-card">正在加载…</view><view wx:elif="{{!activity}}" class="state-card">{{message}}</view><block wx:else><view class="brand-card"><text class="eyebrow">{{activity.statusLabel}}</text><text class="title">{{activity.title}}</text><text class="message">{{activity.summary}}</text></view><view class="notice-card"><text class="notice-title">时间与地点</text><text class="notice-text">{{activity.startAt}} 至 {{activity.endAt}}</text><text class="notice-text">{{activity.locationText}}</text><text class="notice-text">报名截止:{{activity.registrationDeadline}}</text></view><view class="notice-card"><text class="notice-title">活动介绍</text><text class="notice-text">{{activity.content}}</text></view><text class="form-message" wx:if="{{message}}">{{message}}</text><button class="primary-button" disabled="{{!activity.canRegister || busy}}" bindtap="register">{{busy ? '正在提交…' : activity.canRegister ? '立即报名' : '当前不可报名'}}</button><button class="secondary-button" bindtap="volunteerCenter">志愿者认证与我的报名</button></block></view>
<view class="page"><block wx:if="{{loading}}"><view class="skeleton-card"></view><view class="skeleton-card"></view></block><view wx:elif="{{!activity}}" class="state-card"><text class="state-title">活动暂时无法查看</text><text class="state-text">{{message}}</text></view><block wx:else><view class="activity-hero"><text class="activity-hero__status">{{activity.statusLabel}}</text><text class="activity-hero__title">{{activity.title}}</text><text class="activity-hero__copy">{{activity.summary}}</text></view><view class="info-card"><view><text class="info-icon orange">时</text><text class="info-label">活动时间</text><text class="info-value">{{activity.startAt}} 至 {{activity.endAt}}</text></view><view><text class="info-icon green">地</text><text class="info-label">活动地点</text><text class="info-value">{{activity.locationText}}</text></view><view><text class="info-icon blue">止</text><text class="info-label">报名截止</text><text class="info-value">{{activity.registrationDeadline}}</text></view></view><view class="detail-card"><text class="detail-section-title">活动介绍</text><text class="detail-copy" selectable>{{activity.content}}</text></view><view wx:if="{{message}}" class="notice-card"><text class="notice-text">{{message}}</text></view><button class="primary-button" disabled="{{!activity.canRegister || busy}}" bindtap="register">{{busy ? '正在提交…' : activity.canRegister ? '立即报名' : '当前不可报名'}}</button><button class="secondary-button" bindtap="volunteerCenter">志愿者认证与我的报名</button></block></view>
@@ -1 +1,83 @@
@import '../../app.wxss';
.activity-hero {
padding: 38rpx 32rpx;
border-radius: 34rpx;
color: #fff;
background: linear-gradient(145deg, #54b775, #29944e);
box-shadow: 0 14rpx 32rpx rgba(36, 137, 69, 0.18);
}
.activity-hero__status,
.activity-hero__title,
.activity-hero__copy {
display: block;
}
.activity-hero__status {
font-size: 24rpx;
font-weight: 700;
}
.activity-hero__title {
margin-top: 14rpx;
font-size: 42rpx;
font-weight: 700;
}
.activity-hero__copy {
margin-top: 12rpx;
font-size: 28rpx;
line-height: 1.65;
}
.info-card {
margin: 20rpx 0;
padding: 4rpx 28rpx;
border: 2rpx solid var(--color-border);
border-radius: 28rpx;
background: #fff;
box-shadow: var(--shadow-card);
}
.info-card > view {
display: grid;
min-height: 106rpx;
padding: 18rpx 0;
grid-template-columns: 54rpx 128rpx 1fr;
align-items: center;
gap: 12rpx;
border-bottom: 2rpx solid #f0ece8;
}
.info-card > view:last-child {
border-bottom: 0;
}
.info-icon {
display: flex;
width: 48rpx;
height: 48rpx;
align-items: center;
justify-content: center;
border-radius: 15rpx;
color: #fff;
background: #f16b39;
font-size: 22rpx;
}
.info-icon.green {
background: #38a45c;
}
.info-icon.blue {
background: #3b89cf;
}
.info-label {
font-weight: 700;
}
.info-value {
color: var(--color-text-secondary);
font-size: 26rpx;
line-height: 1.5;
}
.detail-section-title {
display: block;
font-size: 34rpx;
font-weight: 700;
}
.detail-copy {
display: block;
margin-top: 14rpx;
color: #544f4a;
line-height: 1.8;
white-space: pre-wrap;
}
@@ -61,6 +61,12 @@ Page({
const mode = options.mode as ApplicationApplyMode;
this.setData({ applyMode: mode, applyModeLabel: APPLICATION_APPLY_MODE_LABELS[mode] });
}
if (options.category && isServiceCategory(options.category)) {
this.setData({
serviceCategory: options.category,
serviceCategoryLabel: SERVICE_CATEGORY_LABELS[options.category],
});
}
void this.loadCategories();
},
@@ -8,6 +8,14 @@ Page({
message: '正在安全登录,请稍候…',
title: MINI_PROGRAM_TITLE,
canCollaborate: false,
serviceCategories: [
{ code: 'OLD_AGE', label: '关爱老人', icon: '人', tone: 'orange' },
{ code: 'WOMEN_CHILDREN', label: '妇女帮扶', icon: '爱', tone: 'rose' },
{ code: 'WOMEN_CHILDREN', label: '儿童关爱', icon: '苗', tone: 'green' },
{ code: 'DISABILITY', label: '残障帮扶', icon: '助', tone: 'blue' },
{ code: 'DIFFICULT_FAMILY', label: '生活困难', icon: '家', tone: 'gold' },
{ code: 'OTHER', label: '其他服务', icon: '心', tone: 'purple' },
],
},
onLoad() {
@@ -47,6 +55,13 @@ Page({
void wx.navigateTo({ url: '/pages/application-form/application-form?mode=FAMILY' });
},
applyForCategory(event: WechatMiniprogram.BaseEvent) {
const category = event.currentTarget.dataset['code'] as string;
void wx.navigateTo({
url: `/pages/application-form/application-form?mode=SELF&category=${category}`,
});
},
viewMyApplications() {
void wx.navigateTo({ url: '/pages/my-applications/my-applications' });
},
@@ -1,63 +1,37 @@
<view class="page">
<view class="brand-card">
<text class="eyebrow">乐活公益 · 暖心服务平台</text>
<text class="title">用爱连接,让爱传递</text>
<text class="message">为老人、妇女儿童、残障人士和困难家庭提供公益服务支持。</text>
<view class="page home-page">
<view class="home-hero">
<view class="hero-orb hero-orb--one"></view><view class="hero-orb hero-orb--two"></view>
<text class="hero-brand">乐活公益 ♡</text>
<text class="hero-title">用爱连接,让爱传递</text>
<text class="hero-copy">有需要,告诉我们;想帮忙,也欢迎加入。</text>
<view class="hero-trust"><text>公益服务</text><text>隐私保护</text><text>专人跟进</text></view>
</view>
<view wx:if="{{authStatus === 'loading'}}" class="state-card" aria-live="polite">
<view class="skeleton-line skeleton-line--wide"></view>
<view class="skeleton-line"></view>
<text class="state-text">正在安全登录,请稍候…</text>
</view>
<view wx:elif="{{authStatus === 'error'}}" class="state-card" aria-live="polite">
<text class="state-title">暂时无法登录</text>
<text class="state-text">{{message}}</text>
<button class="secondary-button" bindtap="retryLogin">重新登录</button>
</view>
<view wx:if="{{authStatus === 'loading'}}" class="state-card" aria-live="polite"><view class="skeleton-line skeleton-line--wide"></view><view class="skeleton-line"></view><text class="state-text">正在安全登录,请稍候…</text></view>
<view wx:elif="{{authStatus === 'error'}}" class="state-card" aria-live="polite"><text class="state-title">暂时无法登录</text><text class="state-text">{{message}}</text><button class="secondary-button" bindtap="retryLogin">重新登录</button></view>
<block wx:else>
<view class="section-heading">
<text class="section-title">需要哪种帮助?</text>
<text class="section-note">选择后只需几步即可提交</text>
<view class="primary-actions">
<button class="primary-action primary-action--warm" bindtap="applyForSelf"><view class="primary-action__icon">♡</view><view><text class="primary-action__title">我需要帮助</text><text class="primary-action__copy">为自己申请暖心服务</text></view><text class="primary-action__arrow">›</text></button>
<button class="primary-action primary-action--green" bindtap="applyForFamily"><view class="primary-action__icon">♧</view><view><text class="primary-action__title">帮家人申请</text><text class="primary-action__copy">替家人或他人说明需求</text></view><text class="primary-action__arrow">›</text></button>
</view>
<button class="action-card action-card--primary" bindtap="applyForSelf">
<text class="action-title">我需要帮助</text>
<text class="action-description">为自己提交服务需求</text>
</button>
<button class="action-card" bindtap="applyForFamily">
<text class="action-title">帮家人申请</text>
<text class="action-description">替家人或他人说明需要的帮助</text>
</button>
<button class="progress-link" bindtap="viewMyApplications">
<text>查看我的服务申请与进度</text>
<text aria-hidden="true">›</text>
</button>
<view class="section-heading">
<text class="section-title">参与公益</text>
<text class="section-note">了解项目,报名志愿活动</text>
<button class="progress-banner" bindtap="viewMyApplications"><view><text class="progress-banner__label">我的服务进度</text><text class="progress-banner__copy">查看申请受理与跟进情况</text></view><text class="progress-banner__arrow">查看 ›</text></button>
<view class="section-heading"><text class="section-title">我们能帮什么</text><text class="section-note">六类暖心服务</text></view>
<view class="service-grid">
<button wx:for="{{serviceCategories}}" wx:key="code" class="service-item service-item--{{item.tone}}" data-code="{{item.code}}" bindtap="applyForCategory"><text class="service-item__icon">{{item.icon}}</text><text class="service-item__label">{{item.label}}</text></button>
</view>
<button class="progress-link" bindtap="viewProjects">
<text>浏览公益项目</text><text aria-hidden="true">›</text>
</button>
<button class="progress-link" bindtap="viewActivities">
<text>查看公益活动</text><text aria-hidden="true">›</text>
</button>
<button class="progress-link progress-link--volunteer" bindtap="viewVolunteerCenter">
<text>志愿者认证与我的报名</text><text aria-hidden="true">›</text>
</button>
<view class="section-heading"><text class="section-title">了解我们</text><text class="section-note">经审核发布的公益内容</text></view>
<button class="progress-link" bindtap="viewContent"><text>公益动态与公告</text><text aria-hidden="true">›</text></button>
<button class="progress-link" bindtap="viewOrganization"><text>机构介绍</text><text aria-hidden="true">›</text></button>
<button class="progress-link" bindtap="viewNotifications"><text>我的消息与通知设置</text><text aria-hidden="true">›</text></button>
<button wx:if="{{canCollaborate}}" class="progress-link progress-link--volunteer" bindtap="viewAssignedCases">
<text>我的公益协作任务</text>
<text aria-hidden="true">›</text>
</button>
<view class="notice-card">
<text class="notice-title">温馨提示</text>
<text class="notice-text">提交后工作人员会根据实际服务能力与您联系。遇到危及生命安全的紧急情况,请及时联系 110 或 120。</text>
<view class="section-heading"><text class="section-title">一起参与公益</text><text class="section-note">每份行动都有温度</text></view>
<view class="participation-card">
<button class="participation-item" bindtap="viewProjects"><text class="participation-item__icon orange">◎</text><view><text class="participation-item__title">公益项目</text><text class="participation-item__copy">看见持续发生的改变</text></view><text>›</text></button>
<button class="participation-item" bindtap="viewActivities"><text class="participation-item__icon blue">◇</text><view><text class="participation-item__title">公益活动</text><text class="participation-item__copy">找到适合参与的活动</text></view><text>›</text></button>
<button class="participation-item" bindtap="viewVolunteerCenter"><text class="participation-item__icon green">♧</text><view><text class="participation-item__title">成为志愿者</text><text class="participation-item__copy">认证、报名与服务时长</text></view><text>›</text></button>
</view>
<view class="section-heading"><text class="section-title">乐活动态</text><text class="section-note">内容均经审核发布</text></view>
<view class="content-links"><button bindtap="viewContent">公益动态 <text>›</text></button><button bindtap="viewOrganization">了解乐活 <text>›</text></button></view>
<view class="safety-note"><text class="safety-note__title">紧急情况请先求助公共服务</text><text>如遇危及生命安全的情况,请及时联系 110 或 120。</text></view>
</block>
</view>
@@ -1,115 +1,293 @@
.brand-card {
margin-bottom: 32rpx;
padding: 40rpx 32rpx;
border-radius: 32rpx;
color: #7a2e12;
background: linear-gradient(135deg, #fff7f0, #ffe2d2);
.home-page {
padding-top: 20rpx;
}
.title {
.home-hero {
position: relative;
margin-bottom: 24rpx;
padding: 40rpx 34rpx 34rpx;
overflow: hidden;
border-radius: 36rpx;
color: #fff;
background: linear-gradient(135deg, #ff7845 0%, #f05225 100%);
box-shadow: 0 16rpx 36rpx rgba(224, 75, 25, 0.22);
}
.hero-orb {
position: absolute;
border: 3rpx solid rgba(255, 255, 255, 0.25);
border-radius: 50%;
}
.hero-orb--one {
width: 180rpx;
height: 180rpx;
right: -54rpx;
top: 30rpx;
}
.hero-orb--two {
width: 90rpx;
height: 90rpx;
right: 92rpx;
bottom: -32rpx;
}
.hero-brand,
.hero-title,
.hero-copy {
position: relative;
z-index: 1;
display: block;
}
.hero-brand {
font-size: 27rpx;
font-weight: 700;
}
.hero-title {
margin-top: 18rpx;
font-size: 46rpx;
font-weight: 700;
letter-spacing: 2rpx;
}
.hero-copy {
margin-top: 12rpx;
font-size: 48rpx;
font-weight: 600;
line-height: 1.3;
}
.message {
display: block;
margin-top: 16rpx;
color: #653b2c;
line-height: 1.7;
}
.section-heading {
display: flex;
margin: 40rpx 0 20rpx;
flex-direction: column;
gap: 8rpx;
}
.section-title {
font-size: 40rpx;
font-weight: 600;
}
.section-note,
.action-description {
color: var(--color-text-secondary);
font-size: 28rpx;
}
.action-card {
.hero-trust {
position: relative;
z-index: 1;
display: flex;
min-height: 152rpx;
margin: 0 0 24rpx;
padding: 28rpx 32rpx;
margin-top: 28rpx;
gap: 12rpx;
}
.hero-trust text {
padding: 7rpx 14rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.18);
font-size: 23rpx;
}
.primary-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18rpx;
}
.primary-action {
position: relative;
display: flex;
min-height: 190rpx;
margin: 0;
padding: 26rpx 22rpx;
flex-direction: column;
align-items: flex-start;
justify-content: center;
border: 2rpx solid var(--color-border);
border-radius: 28rpx;
color: var(--color-text);
background: #ffffff;
color: #5e2715;
border: 2rpx solid #ffd2bd;
border-radius: 30rpx;
text-align: left;
}
.action-card--primary {
border-color: #ffb08e;
background: var(--color-primary-soft);
.primary-action--warm {
background: #fff2ea;
}
.action-card:active,
.progress-link:active {
opacity: 0.72;
.primary-action--green {
color: #185f37;
border-color: #cdebd9;
background: #edf9f1;
}
.action-title {
font-size: 36rpx;
font-weight: 600;
}
.action-description {
margin-top: 8rpx;
}
.progress-link {
.primary-action__icon {
display: flex;
min-height: 104rpx;
margin: 8rpx 0 32rpx;
padding: 20rpx 28rpx;
width: 58rpx;
height: 58rpx;
align-items: center;
justify-content: space-between;
border-radius: 24rpx;
color: #145ea8;
background: #eaf4ff;
font-weight: 600;
justify-content: center;
border-radius: 18rpx;
color: #fff;
background: #f05a28;
font-size: 35rpx;
}
.progress-link--volunteer {
margin-top: -16rpx;
color: #237a2b;
background: #edf8ee;
.primary-action--green .primary-action__icon {
background: #35a864;
}
.notice-card {
padding: 28rpx;
border-left: 8rpx solid #faad14;
border-radius: 20rpx;
background: #fff9e8;
}
.notice-title,
.notice-text {
.primary-action__title,
.primary-action__copy {
display: block;
}
.notice-title {
.primary-action__title {
margin-top: 18rpx;
font-size: 33rpx;
font-weight: 700;
}
.primary-action__copy {
margin-top: 6rpx;
color: #755447;
font-size: 24rpx;
line-height: 1.5;
}
.primary-action--green .primary-action__copy {
color: #52705f;
}
.primary-action__arrow {
position: absolute;
right: 18rpx;
top: 20rpx;
font-size: 36rpx;
}
.progress-banner {
display: flex;
width: 100%;
min-height: 104rpx;
margin: 20rpx 0 0;
padding: 20rpx 26rpx;
align-items: center;
justify-content: space-between;
color: #235f91;
border-radius: 24rpx;
background: #edf6fd;
text-align: left;
}
.progress-banner__label,
.progress-banner__copy {
display: block;
}
.progress-banner__label {
font-weight: 700;
}
.progress-banner__copy {
margin-top: 4rpx;
color: #607b91;
font-size: 24rpx;
}
.progress-banner__arrow {
font-size: 26rpx;
font-weight: 700;
}
.service-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
}
.service-item {
display: flex;
min-height: 142rpx;
margin: 0;
padding: 20rpx 8rpx;
flex-direction: column;
align-items: center;
justify-content: center;
border: 2rpx solid #eee7e1;
border-radius: 26rpx;
background: #fff;
}
.service-item__icon {
display: flex;
width: 58rpx;
height: 58rpx;
align-items: center;
justify-content: center;
border-radius: 20rpx;
color: #fff;
background: #f47745;
font-size: 30rpx;
}
.service-item--green .service-item__icon {
background: #46a956;
}
.service-item--blue .service-item__icon {
background: #378fd6;
}
.service-item--purple .service-item__icon {
background: #8564cf;
}
.service-item--gold .service-item__icon {
background: #e69a31;
}
.service-item--rose .service-item__icon {
background: #e96878;
}
.service-item__label {
margin-top: 12rpx;
font-size: 26rpx;
font-weight: 600;
}
.notice-text {
margin-top: 8rpx;
color: #654f17;
font-size: 28rpx;
line-height: 1.7;
.participation-card {
padding: 0 26rpx;
border: 2rpx solid var(--color-border);
border-radius: 30rpx;
background: #fff;
box-shadow: var(--shadow-card);
}
.participation-item {
display: grid;
width: 100%;
min-height: 122rpx;
margin: 0;
padding: 20rpx 0;
grid-template-columns: 64rpx 1fr auto;
align-items: center;
gap: 18rpx;
color: var(--color-text);
border-bottom: 2rpx solid #f0ece8;
background: transparent;
text-align: left;
}
.participation-item:last-child {
border-bottom: 0;
}
.participation-item__icon {
display: flex;
width: 62rpx;
height: 62rpx;
align-items: center;
justify-content: center;
border-radius: 20rpx;
color: #fff;
font-size: 32rpx;
}
.orange {
background: #f47745;
}
.blue {
background: #378fd6;
}
.green {
background: #38a862;
}
.participation-item__title,
.participation-item__copy {
display: block;
}
.participation-item__title {
font-weight: 700;
}
.participation-item__copy {
margin-top: 4rpx;
color: var(--color-text-secondary);
font-size: 25rpx;
}
.content-links {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16rpx;
}
.content-links button {
display: flex;
min-height: 96rpx;
margin: 0;
padding: 20rpx 24rpx;
align-items: center;
justify-content: space-between;
color: #544d47;
border: 2rpx solid var(--color-border);
border-radius: 24rpx;
background: #fff;
font-weight: 600;
}
.safety-note {
display: flex;
margin-top: 30rpx;
padding: 24rpx;
flex-direction: column;
color: #735818;
border-radius: 22rpx;
background: #fff7df;
font-size: 26rpx;
line-height: 1.65;
}
.safety-note__title {
margin-bottom: 5rpx;
font-weight: 700;
}
@@ -0,0 +1 @@
{ "navigationBarTitleText": "我的" }
@@ -0,0 +1,49 @@
import type { AuthUser } from '@lehuo/contracts';
import { loginWithWechat, restoreCurrentSession } from '../../services/auth';
Page({
data: {
loading: true,
error: '',
user: null as AuthUser | null,
canCollaborate: false,
isVolunteer: false,
},
onShow() {
void this.load();
},
async load() {
this.setData({ loading: true, error: '' });
try {
const session = await restoreCurrentSession();
const user = session?.user ?? (await loginWithWechat()).user;
this.setData({
user,
canCollaborate: user.permissions.includes('assigned_cases:read'),
isVolunteer: user.roles.includes('VOLUNTEER'),
});
} catch (error) {
this.setData({ error: error instanceof Error ? error.message : '暂时无法读取个人信息' });
} finally {
this.setData({ loading: false });
}
},
applications() {
void wx.navigateTo({ url: '/pages/my-applications/my-applications' });
},
volunteer() {
void wx.navigateTo({ url: '/pages/volunteer-center/volunteer-center' });
},
notifications() {
void wx.navigateTo({ url: '/pages/notifications/notifications' });
},
assignedCases() {
void wx.navigateTo({ url: '/pages/assigned-cases/assigned-cases' });
},
content() {
void wx.navigateTo({ url: '/pages/content/content' });
},
organization() {
void wx.navigateTo({ url: '/pages/content/content?type=ORGANIZATION_INTRO' });
},
});
@@ -0,0 +1,13 @@
<view class="page profile-page">
<view wx:if="{{loading}}" class="profile-skeleton"><view class="skeleton-card"></view><view class="skeleton-card"></view></view>
<view wx:elif="{{error}}" class="state-card"><text class="state-title">暂时无法打开个人中心</text><text class="state-text">{{error}}</text><button class="secondary-button" bindtap="load">重新加载</button></view>
<block wx:else>
<view class="profile-hero"><view class="avatar">乐</view><view><text class="profile-name">{{user.displayName || '乐活用户'}}</text><text class="profile-id">账号已安全登录</text></view><text class="profile-badge">{{isVolunteer ? '认证志愿者' : '公益伙伴'}}</text></view>
<view class="summary-card"><button bindtap="applications"><text class="summary-card__icon orange">服</text><text class="summary-card__label">我的申请</text></button><button bindtap="volunteer"><text class="summary-card__icon green">愿</text><text class="summary-card__label">志愿服务</text></button><button bindtap="notifications"><text class="summary-card__icon blue">信</text><text class="summary-card__label">我的消息</text></button></view>
<view class="section-heading"><text class="section-title">我的服务</text></view>
<view class="menu-card"><button bindtap="applications"><text class="menu-icon orange">申</text><view><text class="menu-title">我的服务申请</text><text class="menu-copy">查看需求受理与服务进度</text></view><text class="menu-arrow">›</text></button><button bindtap="volunteer"><text class="menu-icon green">愿</text><view><text class="menu-title">志愿者中心</text><text class="menu-copy">资料认证、活动报名与服务时长</text></view><text class="menu-arrow">›</text></button><button wx:if="{{canCollaborate}}" bindtap="assignedCases"><text class="menu-icon blue">协</text><view><text class="menu-title">我的协作任务</text><text class="menu-copy">查看被指派的必要服务信息</text></view><text class="menu-arrow">›</text></button></view>
<view class="section-heading"><text class="section-title">消息与帮助</text></view>
<view class="menu-card"><button bindtap="notifications"><text class="menu-icon blue">信</text><view><text class="menu-title">我的消息</text><text class="menu-copy">服务进度、报名结果与通知设置</text></view><text class="menu-arrow">›</text></button><button bindtap="content"><text class="menu-icon purple">问</text><view><text class="menu-title">公益资讯与常见问题</text><text class="menu-copy">查看经审核发布的帮助内容</text></view><text class="menu-arrow">›</text></button><button bindtap="organization"><text class="menu-icon orange">乐</text><view><text class="menu-title">关于乐活公益</text><text class="menu-copy">了解机构与服务说明</text></view><text class="menu-arrow">›</text></button></view>
<view class="privacy-footnote">您的服务资料仅用于提供公益服务,并按最小权限原则保护。</view>
</block>
</view>
@@ -0,0 +1,165 @@
.profile-page {
padding-top: 20rpx;
}
.profile-hero {
position: relative;
display: flex;
min-height: 170rpx;
padding: 32rpx;
align-items: center;
gap: 22rpx;
border-radius: 34rpx;
color: #fff;
background: linear-gradient(135deg, #39aa61, #24934d);
box-shadow: 0 14rpx 32rpx rgba(35, 139, 70, 0.18);
}
.avatar {
display: flex;
width: 92rpx;
height: 92rpx;
flex: 0 0 92rpx;
align-items: center;
justify-content: center;
border: 4rpx solid rgba(255, 255, 255, 0.75);
border-radius: 50%;
background: #fff3e8;
color: #e45424;
font-size: 39rpx;
font-weight: 700;
}
.profile-name,
.profile-id {
display: block;
}
.profile-name {
font-size: 36rpx;
font-weight: 700;
}
.profile-id {
margin-top: 7rpx;
font-size: 25rpx;
opacity: 0.88;
}
.profile-badge {
position: absolute;
right: 24rpx;
top: 24rpx;
padding: 7rpx 13rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.2);
font-size: 22rpx;
}
.summary-card {
display: grid;
margin-top: 20rpx;
padding: 20rpx 8rpx;
grid-template-columns: repeat(3, 1fr);
border: 2rpx solid var(--color-border);
border-radius: 28rpx;
background: #fff;
box-shadow: var(--shadow-card);
}
.summary-card button {
display: flex;
min-height: 110rpx;
margin: 0;
padding: 10rpx;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--color-text);
border-right: 2rpx solid #eeeae6;
background: transparent;
font-size: 25rpx;
}
.summary-card button:last-child {
border-right: 0;
}
.summary-card__icon {
display: flex;
width: 52rpx;
height: 52rpx;
align-items: center;
justify-content: center;
border-radius: 17rpx;
color: #fff;
background: #f06b39;
font-size: 24rpx;
}
.summary-card__icon.green {
background: #37a85e;
}
.summary-card__icon.blue {
background: #3b89cf;
}
.summary-card__label {
margin-top: 8rpx;
font-weight: 600;
}
.menu-card {
padding: 0 26rpx;
border: 2rpx solid var(--color-border);
border-radius: 28rpx;
background: #fff;
box-shadow: var(--shadow-card);
}
.menu-card button {
display: grid;
width: 100%;
min-height: 118rpx;
margin: 0;
padding: 20rpx 0;
grid-template-columns: 58rpx 1fr auto;
align-items: center;
gap: 18rpx;
color: var(--color-text);
border-bottom: 2rpx solid #f0ece8;
background: transparent;
text-align: left;
}
.menu-card button:last-child {
border-bottom: 0;
}
.menu-icon {
display: flex;
width: 54rpx;
height: 54rpx;
align-items: center;
justify-content: center;
border-radius: 17rpx;
color: #fff;
background: #ef7040;
font-size: 23rpx;
}
.menu-icon.green {
background: #37a85e;
}
.menu-icon.blue {
background: #3b89cf;
}
.menu-icon.purple {
background: #8065c4;
}
.menu-title,
.menu-copy {
display: block;
}
.menu-title {
font-weight: 700;
}
.menu-copy {
margin-top: 4rpx;
color: var(--color-text-secondary);
font-size: 24rpx;
}
.menu-arrow {
color: #aaa39d;
font-size: 34rpx;
}
.privacy-footnote {
margin: 28rpx 18rpx 0;
color: var(--color-text-tertiary);
font-size: 24rpx;
line-height: 1.6;
text-align: center;
}
@@ -1 +1 @@
<view class="page"><view wx:if="{{loading}}" class="state-card">正在加载…</view><view wx:elif="{{message}}" class="state-card">{{message}}</view><block wx:elif="{{project}}"><view class="brand-card"><text class="eyebrow">{{project.statusLabel}}</text><text class="title">{{project.name}}</text><text class="message">{{project.summary}}</text></view><view class="notice-card"><text class="notice-title">项目周期</text><text class="notice-text">{{project.startDate}} 至 {{project.endDate}} · 负责人 {{project.managerDisplayName}}</text></view><view class="notice-card"><text class="notice-title">项目介绍</text><text class="notice-text">{{project.content}}</text></view><button class="primary-button" bindtap="activities">查看项目活动</button></block></view>
<view class="page"><block wx:if="{{loading}}"><view class="skeleton-card"></view><view class="skeleton-card"></view></block><view wx:elif="{{message}}" class="state-card"><text class="state-title">项目暂时无法查看</text><text class="state-text">{{message}}</text></view><block wx:elif="{{project}}"><view class="project-cover"><text class="project-cover__heart">♡</text><text class="project-cover__status">{{project.statusLabel}}</text></view><view class="project-title-card"><text class="project-title">{{project.name}}</text><text class="project-summary">{{project.summary}}</text><view class="project-period"><text>项目周期</text><text>{{project.startDate}} 至 {{project.endDate}}</text></view></view><view class="detail-card"><text class="detail-section-title">项目介绍</text><text class="detail-copy" selectable>{{project.content}}</text></view><view class="detail-card"><text class="detail-section-title">参与说明</text><text class="detail-copy">查看项目关联活动,选择适合的时间报名参与。平台暂不提供公开募捐交易。</text></view><button class="primary-button" bindtap="activities">查看项目活动</button></block></view>
@@ -1 +1,85 @@
@import '../../app.wxss';
.project-cover {
position: relative;
display: flex;
height: 300rpx;
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: 34rpx;
background: linear-gradient(145deg, #ffc09e, #f26935);
box-shadow: 0 14rpx 32rpx rgba(210, 86, 35, 0.18);
}
.project-cover::after {
position: absolute;
width: 240rpx;
height: 240rpx;
right: -60rpx;
top: -40rpx;
border: 3rpx solid rgba(255, 255, 255, 0.28);
border-radius: 50%;
content: '';
}
.project-cover__heart {
color: #fff;
font-size: 120rpx;
}
.project-cover__status {
position: absolute;
left: 24rpx;
bottom: 22rpx;
padding: 7rpx 15rpx;
border-radius: 999rpx;
color: #fff;
background: rgba(106, 35, 12, 0.3);
font-size: 24rpx;
font-weight: 700;
}
.project-title-card {
margin: -18rpx 16rpx 20rpx;
padding: 30rpx;
position: relative;
z-index: 1;
border-radius: 28rpx;
background: #fff;
box-shadow: var(--shadow-card);
}
.project-title,
.project-summary {
display: block;
}
.project-title {
font-size: 40rpx;
font-weight: 700;
}
.project-summary {
margin-top: 12rpx;
color: var(--color-text-secondary);
font-size: 28rpx;
line-height: 1.65;
}
.project-period {
display: flex;
margin-top: 22rpx;
padding-top: 20rpx;
align-items: center;
justify-content: space-between;
gap: 20rpx;
color: var(--color-text-secondary);
border-top: 2rpx solid #f0ece8;
font-size: 25rpx;
}
.detail-card {
margin-bottom: 20rpx;
}
.detail-section-title {
display: block;
font-size: 34rpx;
font-weight: 700;
}
.detail-copy {
display: block;
margin-top: 14rpx;
color: #544f4a;
line-height: 1.8;
white-space: pre-wrap;
}
@@ -1 +1 @@
<view class="page"><view class="section-heading"><text class="section-title">公益项目</text><text class="section-note">了解正在开展的公益服务</text></view><view wx:if="{{loading}}" class="state-card">正在加载项目…</view><view wx:elif="{{message}}" class="state-card"><text>{{message}}</text><button class="secondary-button" bindtap="load">重试</button></view><view wx:elif="{{!items.length}}" class="state-card">暂无公开项目</view><button wx:for="{{items}}" wx:key="publicId" class="action-card" data-id="{{item.publicId}}" bindtap="open"><text class="action-title">{{item.name}}</text><text class="action-description">{{item.summary}}</text><text class="card-meta">{{item.statusLabel}} · {{item.startDate}} 至 {{item.endDate}}</text></button></view>
<view class="page"><view class="page-heading"><text class="page-title">公益项目</text><text class="page-subtitle">了解正在开展的服务,看见每一份善意如何落地。</text></view><block wx:if="{{loading}}"><view class="skeleton-card"></view><view class="skeleton-card"></view></block><view wx:elif="{{message}}" class="state-card"><text class="state-title">项目暂时无法加载</text><text class="state-text">{{message}}</text><button class="secondary-button" bindtap="load">重新加载</button></view><view wx:elif="{{!items.length}}" class="state-card"><text class="state-title">暂无公开项目</text><text class="state-text">项目经审核发布后会展示在这里。</text></view><button wx:for="{{items}}" wx:key="publicId" class="feature-card" data-id="{{item.publicId}}" bindtap="open"><view class="feature-card__art">♡</view><view class="feature-card__body"><text class="feature-card__tag">{{item.statusLabel}}</text><text class="feature-card__title">{{item.name}}</text><text class="feature-card__copy">{{item.summary}}</text><text class="feature-card__meta">{{item.startDate}} 至 {{item.endDate}}</text></view></button></view>
@@ -1,10 +1,55 @@
@import '../../app.wxss';
.card-meta {
display: block;
margin-top: 12rpx;
color: #646a73;
font-size: 24rpx;
.feature-card {
display: grid;
width: 100%;
min-height: 196rpx;
margin: 0 0 20rpx;
padding: 0;
overflow: hidden;
grid-template-columns: 180rpx 1fr;
color: var(--color-text);
border: 2rpx solid var(--color-border);
border-radius: 28rpx;
background: #fff;
text-align: left;
box-shadow: var(--shadow-card);
}
.state-card {
margin-top: 24rpx;
.feature-card__art {
display: flex;
min-height: 196rpx;
align-items: center;
justify-content: center;
color: #fff;
background: linear-gradient(145deg, #ffb38e, #f26834);
font-size: 72rpx;
}
.feature-card__body {
display: flex;
padding: 22rpx 24rpx;
flex-direction: column;
justify-content: center;
}
.feature-card__tag {
color: var(--color-primary);
font-size: 23rpx;
font-weight: 700;
}
.feature-card__title {
margin-top: 5rpx;
font-size: 31rpx;
font-weight: 700;
}
.feature-card__copy {
display: -webkit-box;
margin-top: 6rpx;
overflow: hidden;
color: var(--color-text-secondary);
font-size: 25rpx;
line-height: 1.5;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.feature-card__meta {
margin-top: 10rpx;
color: var(--color-text-tertiary);
font-size: 23rpx;
}
@@ -0,0 +1 @@
{ "navigationBarTitleText": "公益同行" }
@@ -0,0 +1,56 @@
import type { ActivityListItem, ProjectListItem } from '@lehuo/contracts';
import { fetchPublicActivities, fetchPublicProjects } from '../../services/participation';
Page({
data: {
loading: true,
error: '',
projects: [] as ProjectListItem[],
activities: [] as ActivityListItem[],
},
onShow() {
void this.load();
},
async load() {
this.setData({ loading: true, error: '' });
try {
const [projects, activities] = await Promise.all([
fetchPublicProjects(),
fetchPublicActivities(),
]);
this.setData({
projects: projects.items.slice(0, 3),
activities: activities.items.slice(0, 3),
});
} catch {
this.setData({ error: '公益内容暂时无法加载,请稍后重试。' });
} finally {
this.setData({ loading: false });
}
},
projects() {
void wx.navigateTo({ url: '/pages/projects/projects' });
},
activities() {
void wx.navigateTo({ url: '/pages/activities/activities' });
},
volunteer() {
void wx.navigateTo({ url: '/pages/volunteer-center/volunteer-center' });
},
content() {
void wx.navigateTo({ url: '/pages/content/content' });
},
organization() {
void wx.navigateTo({ url: '/pages/content/content?type=ORGANIZATION_INTRO' });
},
openProject(event: WechatMiniprogram.BaseEvent) {
void wx.navigateTo({
url: `/pages/project-detail/project-detail?publicId=${event.currentTarget.dataset['id'] as string}`,
});
},
openActivity(event: WechatMiniprogram.BaseEvent) {
void wx.navigateTo({
url: `/pages/activity-detail/activity-detail?publicId=${event.currentTarget.dataset['id'] as string}`,
});
},
});
@@ -0,0 +1,15 @@
<view class="page">
<view class="welfare-hero"><text class="welfare-hero__eyebrow">一起让善意落地</text><text class="welfare-hero__title">公益不是围观,是共同参与</text><text class="welfare-hero__copy">了解真实项目,报名志愿活动,看见每一次服务留下的改变。</text><button bindtap="volunteer">成为志愿者 <text>›</text></button></view>
<block wx:if="{{loading}}"><view class="skeleton-card"></view><view class="skeleton-card"></view></block>
<view wx:elif="{{error}}" class="state-card"><text class="state-title">暂时无法加载</text><text class="state-text">{{error}}</text><button class="secondary-button" bindtap="load">重新加载</button></view>
<block wx:else>
<view class="section-heading"><text class="section-title">公益项目</text><button class="section-more" bindtap="projects">查看全部 ›</button></view>
<view wx:if="{{!projects.length}}" class="empty-strip">暂无公开项目</view>
<button wx:for="{{projects}}" wx:key="publicId" class="feature-card" data-id="{{item.publicId}}" bindtap="openProject"><view class="feature-card__art orange">♡</view><view class="feature-card__body"><text class="feature-card__tag">{{item.statusLabel}}</text><text class="feature-card__title">{{item.name}}</text><text class="feature-card__copy">{{item.summary}}</text><text class="feature-card__meta">{{item.startDate}} 至 {{item.endDate}}</text></view></button>
<view class="section-heading"><text class="section-title">公益活动</text><button class="section-more" bindtap="activities">查看全部 ›</button></view>
<view wx:if="{{!activities.length}}" class="empty-strip">暂无可查看活动</view>
<button wx:for="{{activities}}" wx:key="publicId" class="feature-card" data-id="{{item.publicId}}" bindtap="openActivity"><view class="feature-card__art green">♧</view><view class="feature-card__body"><text class="feature-card__tag">{{item.statusLabel}}</text><text class="feature-card__title">{{item.title}}</text><text class="feature-card__copy">{{item.summary}}</text><text class="feature-card__meta">已报名 {{item.activeRegistrationCount}} / {{item.capacity === null ? '不限' : item.capacity}}</text></view></button>
<view class="section-heading"><text class="section-title">了解乐活</text><text class="section-note">所有内容均经审核发布</text></view>
<view class="link-grid"><button bindtap="content"><text class="link-grid__icon orange">闻</text><text>公益动态</text><text>›</text></button><button bindtap="organization"><text class="link-grid__icon blue">乐</text><text>机构介绍</text><text>›</text></button></view>
</block>
</view>
@@ -0,0 +1,144 @@
.welfare-hero {
padding: 38rpx 32rpx;
border-radius: 34rpx;
color: #fff;
background: linear-gradient(135deg, #f46a34, #ed4f22);
box-shadow: 0 15rpx 34rpx rgba(219, 70, 22, 0.2);
}
.welfare-hero__eyebrow,
.welfare-hero__title,
.welfare-hero__copy {
display: block;
}
.welfare-hero__eyebrow {
font-size: 25rpx;
font-weight: 700;
}
.welfare-hero__title {
margin-top: 14rpx;
font-size: 42rpx;
font-weight: 700;
line-height: 1.35;
}
.welfare-hero__copy {
margin-top: 12rpx;
font-size: 27rpx;
line-height: 1.65;
}
.welfare-hero button {
display: flex;
min-height: 78rpx;
margin: 26rpx 0 0;
padding: 14rpx 24rpx;
align-items: center;
justify-content: space-between;
color: #d94819;
border-radius: 999rpx;
background: #fff;
font-weight: 700;
}
.section-more {
min-height: auto;
margin: 0;
padding: 8rpx;
color: var(--color-primary);
background: transparent;
font-size: 26rpx;
}
.feature-card {
display: grid;
width: 100%;
min-height: 190rpx;
margin: 0 0 18rpx;
padding: 0;
overflow: hidden;
grid-template-columns: 180rpx 1fr;
color: var(--color-text);
border: 2rpx solid var(--color-border);
border-radius: 28rpx;
background: #fff;
text-align: left;
box-shadow: var(--shadow-card);
}
.feature-card__art {
display: flex;
min-height: 190rpx;
align-items: center;
justify-content: center;
color: #fff;
background: linear-gradient(145deg, #ffb38e, #f26834);
font-size: 72rpx;
}
.feature-card__art.green {
background: linear-gradient(145deg, #7bce8f, #349e57);
}
.feature-card__body {
display: flex;
padding: 22rpx 24rpx;
flex-direction: column;
justify-content: center;
}
.feature-card__tag {
color: var(--color-primary);
font-size: 23rpx;
font-weight: 700;
}
.feature-card__title {
margin-top: 4rpx;
font-size: 31rpx;
font-weight: 700;
}
.feature-card__copy {
display: -webkit-box;
margin-top: 6rpx;
overflow: hidden;
color: var(--color-text-secondary);
font-size: 25rpx;
line-height: 1.5;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.feature-card__meta {
margin-top: 8rpx;
color: var(--color-text-tertiary);
font-size: 23rpx;
}
.empty-strip {
padding: 36rpx;
border-radius: 24rpx;
color: var(--color-text-secondary);
background: #fff;
text-align: center;
}
.link-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16rpx;
}
.link-grid button {
display: grid;
min-height: 96rpx;
margin: 0;
padding: 16rpx 20rpx;
grid-template-columns: 48rpx 1fr auto;
align-items: center;
color: var(--color-text);
border: 2rpx solid var(--color-border);
border-radius: 24rpx;
background: #fff;
text-align: left;
}
.link-grid__icon {
display: flex;
width: 44rpx;
height: 44rpx;
align-items: center;
justify-content: center;
border-radius: 14rpx;
color: #fff;
background: #ed7040;
font-size: 23rpx;
}
.link-grid__icon.blue {
background: #3b89cf;
}
@@ -0,0 +1 @@
{ "navigationBarTitleText": "暖心服务" }
@@ -0,0 +1,56 @@
Page({
data: {
categories: [
{
code: 'OLD_AGE',
label: '关爱老人',
description: '生活照料、陪伴关怀、健康支持',
icon: '人',
tone: 'orange',
},
{
code: 'WOMEN_CHILDREN',
label: '妇女儿童帮扶',
description: '困境支持、成长陪伴与权益关爱',
icon: '爱',
tone: 'rose',
},
{
code: 'DISABILITY',
label: '残障帮扶',
description: '生活协助、康复支持与无障碍关怀',
icon: '助',
tone: 'blue',
},
{
code: 'DIFFICULT_FAMILY',
label: '困难家庭服务',
description: '生活困难、资源链接与持续关爱',
icon: '家',
tone: 'green',
},
{
code: 'OTHER',
label: '其他服务',
description: '不确定类型也可以先告诉我们',
icon: '心',
tone: 'purple',
},
],
},
applyForSelf() {
void wx.navigateTo({ url: '/pages/application-form/application-form?mode=SELF' });
},
applyForFamily() {
void wx.navigateTo({ url: '/pages/application-form/application-form?mode=FAMILY' });
},
applyCategory(event: WechatMiniprogram.BaseEvent) {
const category = event.currentTarget.dataset['code'] as string;
void wx.navigateTo({
url: `/pages/application-form/application-form?mode=SELF&category=${category}`,
});
},
applications() {
void wx.navigateTo({ url: '/pages/my-applications/my-applications' });
},
});
@@ -0,0 +1,8 @@
<view class="page">
<view class="service-hero"><text class="service-hero__eyebrow">乐活暖心服务</text><text class="service-hero__title">有困难,您可以告诉我们</text><text class="service-hero__copy">填写基本情况后,工作人员会根据实际服务能力与您联系。</text></view>
<view class="quick-actions"><button bindtap="applyForSelf"><text class="quick-actions__icon">♡</text><text class="quick-actions__title">我需要帮助</text><text class="quick-actions__copy">为自己申请</text></button><button bindtap="applyForFamily"><text class="quick-actions__icon green">♧</text><text class="quick-actions__title">帮家人申请</text><text class="quick-actions__copy">为他人代办</text></button></view>
<button class="progress-entry" bindtap="applications"><view><text class="progress-entry__title">我的服务申请</text><text class="progress-entry__copy">查看受理进度和历史记录</text></view><text>›</text></button>
<view class="section-heading"><text class="section-title">选择服务类型</text><text class="section-note">不知道选什么也没关系</text></view>
<button wx:for="{{categories}}" wx:key="code" class="category-card" data-code="{{item.code}}" bindtap="applyCategory"><text class="category-card__icon {{item.tone}}">{{item.icon}}</text><view><text class="category-card__title">{{item.label}}</text><text class="category-card__copy">{{item.description}}</text></view><text class="category-card__arrow">›</text></button>
<view class="safety-note"><text class="safety-note__title">温馨提示</text><text>平台不承诺即时救援。如遇危及生命安全的紧急情况,请及时联系 110 或 120。</text></view>
</view>
@@ -0,0 +1,164 @@
.service-hero {
padding: 36rpx 32rpx;
border-radius: 32rpx;
color: #723018;
background: linear-gradient(135deg, #fff4eb, #ffd7c2);
}
.service-hero__eyebrow,
.service-hero__title,
.service-hero__copy {
display: block;
}
.service-hero__eyebrow {
font-size: 25rpx;
font-weight: 700;
}
.service-hero__title {
margin-top: 12rpx;
font-size: 42rpx;
font-weight: 700;
}
.service-hero__copy {
margin-top: 12rpx;
color: #765241;
font-size: 28rpx;
line-height: 1.65;
}
.quick-actions {
display: grid;
margin-top: 20rpx;
grid-template-columns: 1fr 1fr;
gap: 18rpx;
}
.quick-actions button {
display: flex;
min-height: 170rpx;
margin: 0;
padding: 24rpx;
flex-direction: column;
align-items: flex-start;
border: 2rpx solid #f2ded4;
border-radius: 28rpx;
background: #fff;
}
.quick-actions__icon {
display: flex;
width: 54rpx;
height: 54rpx;
align-items: center;
justify-content: center;
border-radius: 17rpx;
color: #fff;
background: #f05a28;
}
.quick-actions__icon.green {
background: #35a864;
}
.quick-actions__title,
.quick-actions__copy {
display: block;
}
.quick-actions__title {
margin-top: 14rpx;
font-weight: 700;
}
.quick-actions__copy {
margin-top: 5rpx;
color: var(--color-text-secondary);
font-size: 25rpx;
}
.progress-entry {
display: flex;
width: 100%;
min-height: 104rpx;
margin: 20rpx 0;
padding: 20rpx 26rpx;
align-items: center;
justify-content: space-between;
color: #245e8f;
border-radius: 24rpx;
background: #edf6fd;
text-align: left;
}
.progress-entry__title,
.progress-entry__copy {
display: block;
}
.progress-entry__title {
font-weight: 700;
}
.progress-entry__copy {
margin-top: 5rpx;
color: #60788b;
font-size: 25rpx;
}
.category-card {
display: grid;
width: 100%;
min-height: 126rpx;
margin: 0 0 16rpx;
padding: 22rpx 24rpx;
grid-template-columns: 66rpx 1fr auto;
align-items: center;
gap: 18rpx;
color: var(--color-text);
border: 2rpx solid var(--color-border);
border-radius: 26rpx;
background: #fff;
text-align: left;
}
.category-card__icon {
display: flex;
width: 62rpx;
height: 62rpx;
align-items: center;
justify-content: center;
border-radius: 20rpx;
color: #fff;
background: #f47745;
}
.category-card__icon.rose {
background: #df6979;
}
.category-card__icon.blue {
background: #398bd0;
}
.category-card__icon.green {
background: #43a15a;
}
.category-card__icon.purple {
background: #8065c4;
}
.category-card__title,
.category-card__copy {
display: block;
}
.category-card__title {
font-size: 31rpx;
font-weight: 700;
}
.category-card__copy {
margin-top: 5rpx;
color: var(--color-text-secondary);
font-size: 25rpx;
line-height: 1.5;
}
.category-card__arrow {
color: #aaa39c;
font-size: 36rpx;
}
.safety-note {
display: flex;
margin-top: 28rpx;
padding: 24rpx;
flex-direction: column;
color: #735818;
border-radius: 22rpx;
background: #fff7df;
font-size: 26rpx;
line-height: 1.65;
}
.safety-note__title {
margin-bottom: 5rpx;
font-weight: 700;
}
@@ -1 +1 @@
<view class="page"><view class="section-heading"><text class="section-title">志愿者中心</text><text class="section-note">认证资料、报名进度与服务时长</text></view><view wx:if="{{loading}}" class="state-card">正在加载…</view><block wx:else><view wx:if="{{profile}}" class="notice-card"><text class="notice-title">认证状态:{{profile.statusLabel}}</text><text class="notice-text">累计 {{profile.completedActivityCount}} 次活动,{{profile.totalServiceMinutes}} 分钟服务</text></view><view class="form-card"><label>姓名<input value="{{form.realName}}" data-key="realName" bindinput="field" maxlength="50" /></label><label>手机号<input value="{{form.mobile}}" data-key="mobile" bindinput="field" type="number" maxlength="11" /></label><label>所在地区<input value="{{form.regionCode}}" data-key="regionCode" bindinput="field" maxlength="100" /></label><label>可参与时间<textarea value="{{form.availabilityNote}}" data-key="availabilityNote" bindinput="field" maxlength="500"></textarea></label><text class="field-label">服务技能</text><checkbox-group bindchange="skills"><label wx:for="{{skills}}" wx:key="code" class="check-row"><checkbox value="{{item.code}}" checked="{{skillSelected[item.code]}}" />{{item.label}}</label></checkbox-group><text class="field-label">服务意向</text><checkbox-group bindchange="interests"><label wx:for="{{interests}}" wx:key="code" class="check-row"><checkbox value="{{item.code}}" checked="{{interestSelected[item.code]}}" />{{item.label}}</label></checkbox-group><checkbox-group bindchange="consent"><label class="check-row"><checkbox value="yes" checked="{{form.privacyConsent}}" />我同意按志愿服务目的使用上述最少必要资料</label></checkbox-group><text wx:if="{{message}}" class="form-message">{{message}}</text><button class="primary-button" disabled="{{busy}}" bindtap="submit">{{busy ? '提交中…' : profile ? '更新资料' : '申请志愿者认证'}}</button></view><view class="section-heading"><text class="section-title">我的活动报名</text></view><view wx:if="{{!registrations.length}}" class="state-card">暂无报名记录</view><view wx:for="{{registrations}}" wx:key="publicId" class="notice-card"><text class="notice-title">{{item.activityTitle}}</text><text class="notice-text">{{item.statusLabel}} · {{item.checkInStatusLabel}}</text><text class="notice-text">服务时长:{{item.serviceMinutes === null ? '待核定' : item.serviceMinutes + ' 分钟'}}</text></view></block></view>
<view class="page"><view class="volunteer-hero"><view class="volunteer-avatar">愿</view><view><text class="volunteer-title">志愿者中心</text><text class="volunteer-subtitle">让每一次参与都有记录</text></view></view><block wx:if="{{loading}}"><view class="skeleton-card"></view><view class="skeleton-card"></view></block><block wx:else><view wx:if="{{profile}}" class="volunteer-stats"><view><text class="volunteer-stats__value">{{profile.statusLabel}}</text><text>认证状态</text></view><view><text class="volunteer-stats__value">{{profile.completedActivityCount}}</text><text>完成活动</text></view><view><text class="volunteer-stats__value">{{profile.totalServiceMinutes}}</text><text>服务分钟</text></view></view><view class="section-heading"><text class="section-title">{{profile ? '我的志愿资料' : '申请志愿者认证'}}</text><text class="section-note">仅收集服务所需信息</text></view><view class="form-card"><label>姓名<input value="{{form.realName}}" data-key="realName" bindinput="field" maxlength="50" placeholder="请输入真实姓名" /></label><label>手机号<input value="{{form.mobile}}" data-key="mobile" bindinput="field" type="number" maxlength="11" placeholder="用于活动联系" /></label><label>所在地区<input value="{{form.regionCode}}" data-key="regionCode" bindinput="field" maxlength="100" placeholder="例如:丹阳市开发区" /></label><label>可参与时间<textarea value="{{form.availabilityNote}}" data-key="availabilityNote" bindinput="field" maxlength="500" placeholder="例如:周末上午"></textarea></label><text class="field-label">服务技能</text><checkbox-group bindchange="skills"><view class="option-grid"><label wx:for="{{skills}}" wx:key="code" class="check-chip"><checkbox value="{{item.code}}" checked="{{skillSelected[item.code]}}" color="#f05a28" />{{item.label}}</label></view></checkbox-group><text class="field-label">服务意向</text><checkbox-group bindchange="interests"><view class="option-grid"><label wx:for="{{interests}}" wx:key="code" class="check-chip"><checkbox value="{{item.code}}" checked="{{interestSelected[item.code]}}" color="#f05a28" />{{item.label}}</label></view></checkbox-group><checkbox-group bindchange="consent"><label class="consent-row"><checkbox value="yes" checked="{{form.privacyConsent}}" color="#f05a28" /><text>我同意按志愿服务目的使用上述最少必要资料</text></label></checkbox-group><text wx:if="{{message}}" class="form-message">{{message}}</text><button class="primary-button" disabled="{{busy}}" bindtap="submit">{{busy ? '提交中…' : profile ? '更新资料' : '申请志愿者认证'}}</button></view><view class="section-heading"><text class="section-title">我的活动报名</text><text class="section-note">审核、签到与服务时长</text></view><view wx:if="{{!registrations.length}}" class="state-card"><text class="state-title">暂无报名记录</text><text class="state-text">您可以从“公益”页面查看已发布活动。</text></view><view wx:for="{{registrations}}" wx:key="publicId" class="registration-card"><view><text class="registration-card__title">{{item.activityTitle}}</text><text class="registration-card__meta">{{item.statusLabel}} · {{item.checkInStatusLabel}}</text></view><text class="registration-card__minutes">{{item.serviceMinutes === null ? '待核定' : item.serviceMinutes + ' 分钟'}}</text></view></block></view>
@@ -1,29 +1,144 @@
@import '../../app.wxss';
.form-card {
padding: 28rpx;
border-radius: 24rpx;
.volunteer-hero {
display: flex;
padding: 34rpx;
align-items: center;
gap: 22rpx;
border-radius: 34rpx;
color: #fff;
background: linear-gradient(135deg, #3dac62, #248f4c);
}
.volunteer-avatar {
display: flex;
width: 84rpx;
height: 84rpx;
align-items: center;
justify-content: center;
border: 4rpx solid rgba(255, 255, 255, 0.7);
border-radius: 50%;
background: #fff;
color: #27924e;
font-size: 34rpx;
font-weight: 700;
}
.volunteer-title,
.volunteer-subtitle {
display: block;
}
.volunteer-title {
font-size: 38rpx;
font-weight: 700;
}
.volunteer-subtitle {
margin-top: 6rpx;
font-size: 26rpx;
opacity: 0.9;
}
.volunteer-stats {
display: grid;
margin-top: 20rpx;
padding: 24rpx 12rpx;
grid-template-columns: repeat(3, 1fr);
border: 2rpx solid var(--color-border);
border-radius: 28rpx;
background: #fff;
box-shadow: var(--shadow-card);
}
.volunteer-stats view {
display: flex;
flex-direction: column;
align-items: center;
color: var(--color-text-secondary);
border-right: 2rpx solid #eeeae6;
font-size: 24rpx;
}
.volunteer-stats view:last-child {
border-right: 0;
}
.volunteer-stats__value {
margin-bottom: 5rpx;
color: var(--color-text);
font-size: 30rpx;
font-weight: 700;
}
.form-card label,
.field-label {
display: block;
margin-top: 24rpx;
font-weight: 600;
margin-top: 26rpx;
font-weight: 700;
}
.form-card > label:first-child {
margin-top: 0;
}
.form-card input,
.form-card textarea {
width: 100%;
min-height: 88rpx;
min-height: 92rpx;
margin-top: 12rpx;
padding: 18rpx;
border: 2rpx solid #d9dce1;
border-radius: 16rpx;
padding: 20rpx 22rpx;
border: 2rpx solid #d9d3cd;
border-radius: 18rpx;
box-sizing: border-box;
font-size: 30rpx;
}
.check-row {
display: flex !important;
min-height: 72rpx;
align-items: center;
.form-card textarea {
min-height: 180rpx;
}
.option-grid {
display: grid;
margin-top: 12rpx;
grid-template-columns: 1fr 1fr;
gap: 12rpx;
}
.check-chip {
display: flex !important;
min-height: 76rpx !important;
margin: 0 !important;
padding: 10rpx 14rpx;
align-items: center;
gap: 8rpx;
border: 2rpx solid #e6e0da;
border-radius: 18rpx;
background: #faf9f7;
font-size: 26rpx;
font-weight: 400 !important;
}
.consent-row {
display: flex !important;
min-height: 96rpx;
margin-top: 26rpx !important;
align-items: flex-start;
gap: 12rpx;
color: var(--color-text-secondary);
font-size: 26rpx;
line-height: 1.6;
font-weight: 400 !important;
}
.registration-card {
display: flex;
min-height: 112rpx;
margin-bottom: 16rpx;
padding: 24rpx 26rpx;
align-items: center;
justify-content: space-between;
gap: 20rpx;
border: 2rpx solid var(--color-border);
border-radius: 24rpx;
background: #fff;
}
.registration-card__title,
.registration-card__meta {
display: block;
}
.registration-card__title {
font-weight: 700;
}
.registration-card__meta {
margin-top: 5rpx;
color: var(--color-text-secondary);
font-size: 25rpx;
}
.registration-card__minutes {
color: var(--color-primary);
font-size: 26rpx;
font-weight: 700;
}
+11
View File
@@ -146,3 +146,14 @@ P2:5. 公益项目 6. 公益活动 7. 我要做公益
- 志愿者中心只收集姓名、手机号、地区、可参与时间、固定技能和服务意向,并明确资料使用同意;同页展示资格状态、报名进度与核定服务分钟。
- 管理端新增志愿者审核、公益项目、活动与报名三个独立导航;危险状态推进二次确认,手机号列表脱敏,所有主要按钮保持至少 44px 点击高度。
- 沿用已批准的橙色主色、系统字体和功能型布局;采用高对比、可见焦点、文字+状态、加载/空/失败反馈与渐进披露,不引入复杂动画、密集仪表盘或新品牌方向。
---
## 11. 公众端视觉重设计落地(2026-08-13)
- 底部导航固定为“首页 / 服务 / 公益 / 我的”,分别承担首要入口、求助闭环、社会参与和个人事务;协作任务按权限在“我的”中显示,不另设同级导航。
- 首页保持“我需要帮助 / 帮家人申请”为最高视觉权重,六类服务入口次之;服务进度使用独立横幅,宣传内容不挤占求助任务。
- 暖橙 `#F05A28` 为主色,绿色表示志愿参与与正向状态,蓝色表示进度和消息;颜色始终配合文字、标签或图形,不作为唯一信息来源。
- 全局使用 16/24/32rpx 级圆角、低强度暖灰阴影、至少约 96rpx 的主按钮/主要点击高度和系统字体;不使用闪烁、强动画、玻璃拟态或科技大屏视觉。
- 项目、活动和志愿者页面只展示 API 返回的真实事实;封面缺失时使用轻量公益图形占位,不生成虚构人物、捐赠额、服务人数或活动成果。
- 参考图中的公开捐赠交易、证书、培训和团队入口不属于当前 V1 已实现能力,不在公众端显示;项目详情明确不提供公开募捐交易。
+12
View File
@@ -116,6 +116,18 @@ YYYY-MM-DD
遗留问题:
```
2026-08-13
任务:参考已批准视觉方向重设计微信小程序公众端(不含管理端)。
完成:分析用户提供的前端参考图并按现有 V1 业务边界重构公众端信息架构;建立暖橙主色、语义色、圆角、阴影、字号和间距设计令牌,新增“首页 / 服务 / 公益 / 我的”四栏自定义底部导航;重做首页品牌区、本人/代申请双主任务、六类服务入口、服务进度和公益参与分区;新增服务、公益和个人中心三个聚合页,全部连接现有真实申请、项目、活动、志愿者、协作任务、内容和消息链路;重做项目/活动列表与详情、志愿者中心的卡片层级和适老交互。保留 loading / empty / error / permission-aware 状态,不加入参考图中的公开捐赠交易、证书、培训、团队或虚构业务统计,不修改管理端、API、数据库、认证或权限。
测试:format check、lint、全仓 typecheck、27 个 unit tests、全仓 production build 全部通过;小程序独立 typecheck、3 个 unit tests 和 build 通过。真实 MySQL 集成测试未得到新的完整通过结果:migration 重试两次均被本机 Windows Prisma Schema Engine 启动异常阻止;随后在已迁移的健康 `lehuo_test` 库直接执行时进入测试但超过 5 分钟工具时限,残留测试进程已按命令行精确结束。本任务无 schema/API/后端变更,上一阶段 34 个集成测试通过基线保持不变,但本次不把超时运行记录为通过。
文档变化:更新小程序 README、UI/UX 规范和本任务状态;未产生新的架构或产品决策,因此不新增 ADR。
遗留问题:本机未定位到微信开发者工具 CLI,且真实 AppID/AppSecret 与微信环境仍按用户计划后期提供,因此未生成开发者工具/真机截图;当前完成代码级视觉对照、类型与构建验证,真机适老化与安全区验收仍需在微信环境执行。项目/活动真实封面继续依赖现有私有对象存储配置,未使用假图片冒充生产内容。
2026-08-12
任务:Phase 7 统计、安全与上线工程。