feat(old-items): complete reuse workflow and refine miniapp home

This commit is contained in:
2026-08-15 16:38:30 +08:00
parent 65d23eab4a
commit f3737c80be
94 changed files with 7880 additions and 215 deletions
+9 -1
View File
@@ -19,7 +19,15 @@
"pages/content-detail/content-detail",
"pages/notifications/notifications",
"pages/financial-disclosures/financial-disclosures",
"pages/financial-disclosure-detail/financial-disclosure-detail"
"pages/financial-disclosure-detail/financial-disclosure-detail",
"pages/old-item-home/old-item-home",
"pages/old-item-donate/old-item-donate",
"pages/old-item-records/old-item-records",
"pages/old-item-detail/old-item-detail",
"pages/old-item-rewards/old-item-rewards",
"pages/old-item-certificate/old-item-certificate",
"pages/old-item-needs/old-item-needs",
"pages/old-item-guide/old-item-guide"
],
"window": {
"backgroundColor": "#F7F5F2",
Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

+208
View File
@@ -0,0 +1,208 @@
.old-page {
min-height: 100vh;
padding: 24rpx;
background: #f7f5f2;
box-sizing: border-box;
}
.old-hero {
padding: 38rpx 32rpx;
border-radius: 28rpx;
color: #fff;
background: linear-gradient(135deg, #ff8c4a, #f05a28);
box-shadow: 0 16rpx 36rpx rgba(240, 90, 40, 0.2);
}
.old-hero__title {
display: block;
font-size: 40rpx;
font-weight: 800;
}
.old-hero__copy {
display: block;
margin-top: 10rpx;
font-size: 25rpx;
opacity: 0.92;
}
.old-card {
margin-top: 22rpx;
padding: 24rpx;
border: 2rpx solid #eee7df;
border-radius: 24rpx;
background: #fff;
box-shadow: 0 10rpx 24rpx rgba(67, 50, 36, 0.05);
}
.old-title {
display: block;
font-size: 31rpx;
font-weight: 800;
}
.old-muted {
display: block;
margin-top: 8rpx;
color: #77716b;
font-size: 24rpx;
line-height: 1.6;
}
.old-primary,
.old-secondary {
width: 100%;
min-height: 88rpx;
margin-top: 18rpx;
border-radius: 999rpx;
font-size: 28rpx;
font-weight: 700;
}
.old-primary {
color: #fff;
background: linear-gradient(90deg, #ff702f, #f05a28);
}
.old-secondary {
color: #e95220;
background: #fff2ec;
border: 2rpx solid #ffd3c2;
}
.old-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12rpx;
}
.old-stat {
padding: 22rpx 8rpx;
text-align: center;
border-radius: 20rpx;
background: #fff;
}
.old-stat strong,
.old-stat text {
display: block;
}
.old-stat strong {
color: #2eaa4f;
font-size: 34rpx;
}
.old-stat text {
color: #77716b;
font-size: 21rpx;
}
.old-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
margin-top: 20rpx;
}
.old-grid button {
min-height: 140rpx;
margin: 0;
padding: 22rpx;
border-radius: 22rpx;
background: #fff;
text-align: left;
}
.old-grid strong,
.old-grid text {
display: block;
}
.old-grid strong {
font-size: 28rpx;
}
.old-grid text {
margin-top: 8rpx;
color: #77716b;
font-size: 22rpx;
}
.old-field {
display: block;
margin-top: 22rpx;
font-size: 25rpx;
font-weight: 700;
}
.old-field input,
.old-field textarea,
.old-field picker {
display: block;
margin-top: 10rpx;
padding: 20rpx;
border: 2rpx solid #e8e1da;
border-radius: 16rpx;
background: #fff;
font-weight: 400;
}
.old-options {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 12rpx;
}
.old-options button {
margin: 0;
padding: 12rpx 20rpx;
border: 2rpx solid #e6dfd8;
border-radius: 16rpx;
background: #fff;
font-size: 24rpx;
}
.old-options button.active {
color: #f05a28;
border-color: #f05a28;
background: #fff4ef;
}
.old-record {
display: block;
width: 100%;
margin-top: 18rpx;
padding: 24rpx;
border-radius: 22rpx;
background: #fff;
text-align: left;
}
.old-record__head {
display: flex;
justify-content: space-between;
}
.old-record__status {
color: #f05a28;
}
.old-trace {
margin-top: 22rpx;
padding-left: 28rpx;
border-left: 4rpx solid #ffd4c3;
}
.old-trace view {
position: relative;
margin: 0 0 26rpx;
}
.old-trace view::before {
position: absolute;
left: -38rpx;
top: 8rpx;
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background: #f05a28;
content: '';
}
.old-empty {
padding: 80rpx 20rpx;
text-align: center;
color: #77716b;
}
.old-photo-grid {
display: flex;
gap: 12rpx;
flex-wrap: wrap;
margin-top: 12rpx;
}
.old-photo {
width: 150rpx;
height: 150rpx;
border-radius: 16rpx;
}
.old-remove {
margin: 0;
color: #b33;
background: #fff;
}
.old-guide text {
display: block;
margin: 14rpx 0;
line-height: 1.7;
}
@@ -23,7 +23,7 @@ Page({
async loadApplication() {
if (!this.data.publicId) {
this.setData({ status: 'error', errorMessage: '申请信息不完整,请返回重试' });
this.setData({ status: 'error', errorMessage: '帮扶信息不完整,请返回重试' });
return;
}
this.setData({ status: 'loading', errorMessage: '' });
@@ -3,12 +3,12 @@
<view wx:elif="{{status === 'error'}}" class="state-card"><text class="state-title">暂时无法读取服务进度</text><text class="state-text">{{errorMessage}}</text><button class="secondary-button" bindtap="retryLoad">重新加载</button></view>
<block wx:else>
<view class="detail-summary">
<view><text class="eyebrow">{{application.serviceCategoryLabel}}</text><text class="detail-title">{{application.beneficiaryName}}的服务申请</text></view>
<view><text class="eyebrow">{{application.serviceCategoryLabel}}</text><text class="detail-title">{{application.beneficiaryName}}的帮扶服务</text></view>
<text class="status-pill">{{application.statusLabel}}</text>
<text class="application-meta">服务编号:{{application.applicationNo}}</text>
</view>
<view class="detail-card">
<text class="form-title">申请信息</text>
<text class="form-title">帮扶信息</text>
<view class="summary-row"><text>申请关系</text><text>{{application.applyModeLabel}}</text></view>
<view class="summary-row"><text>联系电话</text><text>{{application.contactMobileMasked}}</text></view>
<view class="summary-row"><text>所在地区</text><text>{{application.regionCode}}</text></view>
@@ -1,3 +1,3 @@
{
"navigationBarTitleText": "服务申请"
"navigationBarTitleText": "帮扶服务"
}
@@ -20,14 +20,29 @@ import { createClientRequestId } from '../../utils/request-id';
const TOTAL_STEPS = 5;
const LOCAL_SERVICE_CATEGORIES: ServiceCategoryOption[] = [
const CATEGORY_IMAGES: Record<ServiceCategory, string> = {
OLD_AGE: '/assets/images/brand-elder-care.jpg',
WOMEN: '/assets/images/service-women-support-v2.jpg',
CHILD: '/assets/images/service-child-care-v2.jpg',
DISABILITY: '/assets/images/brand-disability-support.jpg',
LIFE_DIFFICULTY: '/assets/images/service-life-support-v2.jpg',
OTHER: '/assets/images/service-other-support-v2.jpg',
};
type ServiceCategoryView = ServiceCategoryOption & { image: string };
const LOCAL_SERVICE_CATEGORY_OPTIONS = [
{ code: 'OLD_AGE', label: '关爱老人', description: '生活照料、陪伴关怀与健康支持' },
{ code: 'WOMEN', label: '困境妇女', description: '困境支持、权益关爱与资源链接' },
{ code: 'CHILD', label: '儿童关爱', description: '成长陪伴、家庭支持与儿童关爱' },
{ code: 'DISABILITY', label: '残障帮扶', description: '生活协助、康复支持与无障碍关怀' },
{ code: 'LIFE_DIFFICULTY', label: '生活困难', description: '生活支持、资源链接与持续关爱' },
{ code: 'OTHER', label: '其他服务', description: '由工作人员进一步了解并协助判断' },
];
] satisfies ServiceCategoryOption[];
const LOCAL_SERVICE_CATEGORIES: ServiceCategoryView[] = LOCAL_SERVICE_CATEGORY_OPTIONS.map(
(item) => ({ ...item, image: CATEGORY_IMAGES[item.code] }),
);
function isServiceCategory(value: string): value is ServiceCategory {
return (SERVICE_CATEGORIES as readonly string[]).includes(value);
@@ -83,7 +98,13 @@ Page({
this.setData({ loading: true, errorMessage: '' });
try {
const result = await fetchServiceCategories();
this.setData({ categories: result.items, loading: false });
this.setData({
categories: result.items.map((item) => ({
...item,
image: CATEGORY_IMAGES[item.code],
})),
loading: false,
});
} catch (error) {
this.setData({
loading: false,
@@ -29,10 +29,7 @@
bindtap="selectCategory"
aria-label="{{item.label}}:{{item.description}}"
>
<image wx:if="{{item.code === 'OLD_AGE'}}" class="selection-photo" src="/assets/images/brand-elder-care.jpg" mode="aspectFill" />
<image wx:elif="{{item.code === 'WOMEN_CHILDREN'}}" class="selection-photo" src="/assets/images/brand-women-support.jpg" mode="aspectFill" />
<image wx:elif="{{item.code === 'DISABILITY'}}" class="selection-photo" src="/assets/images/brand-disability-support.jpg" mode="aspectFill" />
<view wx:else class="selection-photo selection-photo--fallback">公益服务</view>
<image class="selection-photo" src="{{item.image}}" mode="aspectFill" />
<view class="selection-content">
<text class="selection-title">{{item.label}}</text>
<text class="selection-description">{{item.description}}</text>
@@ -86,7 +83,7 @@
</view>
<view wx:else class="form-card">
<text class="form-title">确认申请信息</text>
<text class="form-title">确认帮扶信息</text>
<text class="form-hint">提交前请确认,联系方式仅供授权工作人员提供服务使用。</text>
<view class="summary-list">
<view class="summary-row"><text>服务类型</text><text>{{serviceCategoryLabel}}</text></view>
@@ -99,7 +96,7 @@
<checkbox-group bindchange="updatePrivacyConsent">
<label class="consent-row">
<checkbox value="accepted" checked="{{privacyConsent}}" color="#FF6B35" />
<text>我已阅读并同意本次服务申请的隐私授权,确认所填信息用于公益服务联系、评估和必要留痕。</text>
<text>我已阅读并同意本次帮扶服务的隐私授权,确认所填信息用于公益服务联系、评估和必要留痕。</text>
</label>
</checkbox-group>
</view>
@@ -108,7 +105,7 @@
<view class="form-actions">
<button wx:if="{{step > 1}}" class="secondary-button action-button" disabled="{{submitting}}" bindtap="previousStep">上一步</button>
<button wx:if="{{step < totalSteps}}" class="primary-button action-button" bindtap="nextStep">下一步</button>
<button wx:else class="primary-button action-button" loading="{{submitting}}" disabled="{{submitting}}" bindtap="submitApplication">{{submitting ? '正在提交…' : '提交申请'}}</button>
<button wx:else class="primary-button action-button" loading="{{submitting}}" disabled="{{submitting}}" bindtap="submitApplication">{{submitting ? '正在提交…' : '提交帮扶需求'}}</button>
</view>
</block>
</view>
@@ -21,7 +21,7 @@ Page({
async loadApplication() {
if (!this.data.publicId) {
this.setData({ loading: false, errorMessage: '申请信息不完整,请前往“我的申请”查看' });
this.setData({ loading: false, errorMessage: '帮扶信息不完整,请前往“我的帮扶服务”查看' });
return;
}
this.setData({ loading: true, errorMessage: '' });
@@ -40,7 +40,7 @@ Page({
? `${error.message}(编号:${error.requestId})`
: error instanceof Error
? error.message
: '暂时无法读取申请信息';
: '暂时无法读取帮扶信息';
this.setData({ loading: false, errorMessage: message });
}
},
@@ -1,12 +1,12 @@
<view class="page success-page">
<view wx:if="{{loading}}" class="state-card"><view class="skeleton-card"></view><text class="state-text">正在确认提交结果…</text></view>
<view wx:elif="{{errorMessage}}" class="state-card">
<text class="state-title">暂时无法读取申请结果</text><text class="state-text">{{errorMessage}}</text>
<text class="state-title">暂时无法读取帮扶服务结果</text><text class="state-text">{{errorMessage}}</text>
<button class="secondary-button" bindtap="retryLoad">重新加载</button>
</view>
<view wx:else class="success-card">
<view class="success-mark" aria-hidden="true">✓</view>
<text class="success-title">申请已提交</text>
<text class="success-title">帮扶需求已提交</text>
<text class="success-message">乐活公益工作人员将根据实际情况与您联系,请留意来电。</text>
<view class="result-list">
<view><text>服务编号</text><text class="result-value">{{application.applicationNo}}</text></view>
@@ -8,12 +8,6 @@ Page({
message: '正在安全登录,请稍候…',
title: MINI_PROGRAM_TITLE,
canCollaborate: false,
serviceCategories: [
{ code: 'OLD_AGE', label: '关爱老人', symbol: '老', tone: 'orange' },
{ code: 'WOMEN_CHILDREN', label: '困境儿童', symbol: '童', tone: 'green' },
{ code: 'WOMEN_CHILDREN', label: '妇女帮扶', symbol: '女', tone: 'rose' },
{ code: 'DISABILITY', label: '残障救助', symbol: '助', tone: 'blue' },
],
},
onLoad() {
@@ -53,13 +47,6 @@ 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' });
},
@@ -82,6 +69,12 @@ Page({
viewContent() {
void wx.navigateTo({ url: '/pages/content/content' });
},
viewFinancialDisclosures() {
void wx.navigateTo({ url: '/pages/financial-disclosures/financial-disclosures' });
},
viewOldItems() {
void wx.navigateTo({ url: '/pages/old-item-home/old-item-home' });
},
viewOrganization() {
void wx.navigateTo({ url: '/pages/content/content?type=ORGANIZATION_INTRO' });
},
@@ -8,19 +8,23 @@
<view class="hero-glow hero-glow--one"></view><view class="hero-glow hero-glow--two"></view>
<text class="hero-title">用爱连接 让爱传递</text>
<text class="hero-copy">乐于助人 · 助人为乐 · 服务社会</text>
<button class="hero-action" bindtap="apply">立即申请服务 <text>›</text></button>
<button class="hero-action" bindtap="apply">申请帮扶服务 <text>›</text></button>
</view>
<view wx:if="{{authStatus === 'loading'}}" class="state-card"><view class="skeleton-line skeleton-line--wide"></view><text class="state-text">正在安全登录,请稍候…</text></view>
<view wx:elif="{{authStatus === 'error'}}" class="state-card"><text class="state-title">暂时无法登录</text><text class="state-text">{{message}}</text><button class="secondary-button" bindtap="retryLogin">重新登录</button></view>
<block wx:else>
<view class="quick-grid">
<button wx:for="{{serviceCategories}}" wx:key="label" class="quick-item" data-code="{{item.code}}" bindtap="applyForCategory"><view class="quick-icon quick-icon--{{item.tone}}"><text>{{item.symbol}}</text></view><text>{{item.label}}</text></button>
<button class="quick-item" bindtap="apply"><view class="quick-icon quick-icon--blue"><text>申</text></view><text>服务申请</text></button>
<button class="quick-item" bindtap="viewVolunteerCenter"><view class="quick-icon quick-icon--gold"><text>愿</text></view><text>志愿服务</text></button>
<button class="quick-item" bindtap="viewProjects"><view class="quick-icon quick-icon--green"><text>益</text></view><text>公益项目</text></button>
<button class="quick-item" bindtap="viewContent"><view class="quick-icon quick-icon--rose"><text>讯</text></view><text>公益动态</text></button>
<view class="home-services">
<button class="help-entry" bindtap="apply"><view class="help-entry__icon"><text>帮</text></view><view class="help-entry__content"><text class="help-entry__title">帮扶服务</text><text class="help-entry__copy">为自己或家人提交需求,工作人员将与您联系</text></view><text class="help-entry__arrow">›</text></button>
<view class="quick-grid">
<button class="quick-item" bindtap="viewVolunteerCenter"><view class="quick-icon quick-icon--gold"><text>愿</text></view><text>志愿服务</text></button>
<button class="quick-item" bindtap="viewProjects"><view class="quick-icon quick-icon--green"><text>益</text></view><text>公益项目</text></button>
<button class="quick-item" bindtap="viewContent"><view class="quick-icon quick-icon--rose"><text>讯</text></view><text>公益动态</text></button>
<button class="quick-item" bindtap="viewOldItems"><view class="quick-icon quick-icon--gold"><text>物</text></view><text>旧物善用</text></button>
<button class="quick-item" bindtap="viewFinancialDisclosures"><view class="quick-icon quick-icon--green"><text>财</text></view><text>财务公开</text></button>
<button class="quick-item" bindtap="viewMyApplications"><view class="quick-icon quick-icon--blue"><text>进</text></view><text>服务进度</text></button>
</view>
</view>
<button class="notice-strip" bindtap="viewContent"><text class="notice-badge">公告</text><text class="notice-text">查看乐活公益最新服务动态与温馨提示</text><text>›</text></button>
@@ -33,7 +37,6 @@
<view class="section-heading"><text class="section-title">便捷入口</text></view>
<view class="service-list">
<button bindtap="viewMyApplications"><view class="list-mark list-mark--orange">进</view><view><text class="list-title">服务进度</text><text class="list-copy">查看申请受理与跟进情况</text></view><text>›</text></button>
<button wx:if="{{canCollaborate}}" bindtap="viewAssignedCases"><view class="list-mark list-mark--blue">协</view><view><text class="list-title">协作任务</text><text class="list-copy">查看已授权的服务工单</text></view><text>›</text></button>
<button bindtap="viewActivities"><view class="list-mark list-mark--green">活</view><view><text class="list-title">公益活动</text><text class="list-copy">浏览活动并提交报名</text></view><text>›</text></button>
</view>
@@ -101,24 +101,74 @@
font-size: 25rpx;
font-weight: 700;
}
.quick-grid {
display: grid;
.home-services {
margin-top: 24rpx;
padding: 26rpx 8rpx 18rpx;
grid-template-columns: repeat(4, 1fr);
gap: 28rpx 10rpx;
padding: 20rpx;
border: 2rpx solid var(--color-border);
border-radius: 28rpx;
background: #fff;
box-shadow: var(--shadow-card);
}
.help-entry {
display: grid;
width: 100%;
min-height: 128rpx;
margin: 0;
padding: 22rpx 24rpx;
grid-template-columns: 76rpx 1fr auto;
align-items: center;
gap: 20rpx;
border-radius: 24rpx;
color: #7a3219;
background: linear-gradient(120deg, #fff0e7, #fff8f3);
text-align: left;
}
.help-entry__icon {
display: flex;
width: 72rpx;
height: 72rpx;
align-items: center;
justify-content: center;
border-radius: 24rpx;
color: #fff;
background: linear-gradient(145deg, #ff7c43, #f05a28);
font-weight: 800;
}
.help-entry__content,
.help-entry__title,
.help-entry__copy {
display: block;
}
.help-entry__title {
font-size: 32rpx;
font-weight: 800;
}
.help-entry__copy {
margin-top: 5rpx;
color: #795d50;
font-size: 24rpx;
line-height: 1.5;
}
.help-entry__arrow {
color: var(--color-primary);
font-size: 42rpx;
}
.quick-grid {
display: grid;
margin-top: 24rpx;
padding: 0 4rpx 4rpx;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx 16rpx;
}
.quick-item {
display: flex;
min-height: 112rpx;
min-width: 0;
margin: 0;
padding: 0;
padding: 8rpx 0;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12rpx;
color: var(--color-text);
background: transparent;
@@ -1,3 +1,3 @@
{
"navigationBarTitleText": "我的申请"
"navigationBarTitleText": "我的帮扶服务"
}
@@ -1,8 +1,8 @@
<view class="page list-page">
<view class="page-heading"><text class="page-title">我的服务申请</text><text class="page-subtitle">查看已提交的需求和工作人员处理进度</text></view>
<view class="page-heading"><text class="page-title">我的帮扶服务</text><text class="page-subtitle">查看已提交的帮扶需求和工作人员处理进度</text></view>
<view wx:if="{{status === 'loading'}}" class="application-list"><view wx:for="{{[1, 2, 3]}}" wx:key="*this" class="skeleton-card"></view></view>
<view wx:elif="{{status === 'empty'}}" class="state-card">
<text class="state-title">您还没有提交过服务申请</text><text class="state-text">如果您或家人需要帮助,可以从这里开始。</text>
<text class="state-title">您还没有提交过帮扶需求</text><text class="state-text">如果您或家人需要帮助,可以从这里开始。</text>
<button class="primary-button" bindtap="startApplication">开始申请</button>
</view>
<view wx:elif="{{status === 'error'}}" class="state-card">
@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "电子荣誉证书"
}
@@ -0,0 +1,33 @@
import type { OldItemCertificateItem } from '@lehuo/contracts';
import { getMyOldItemCertificate } from '../../services/old-items';
Page({
data: {
loading: true,
error: '',
certificate: null as (OldItemCertificateItem & { issuedDate: string }) | null,
},
onLoad(options) {
const id = options['id'];
if (!id) {
this.setData({ loading: false, error: '缺少证书参数' });
return;
}
void this.load(id);
},
async load(id: string) {
try {
const certificate = (await getMyOldItemCertificate(id)).certificate;
this.setData({
certificate: {
...certificate,
issuedDate: new Date(certificate.issuedAt).toLocaleDateString('zh-CN'),
},
});
} catch (error) {
this.setData({ error: error instanceof Error ? error.message : '加载失败' });
} finally {
this.setData({ loading: false });
}
},
});
@@ -0,0 +1,9 @@
<view class="old-page">
<view wx:if="{{loading}}" class="old-card"><text class="old-muted">正在加载证书…</text></view>
<view wx:elif="{{error}}" class="old-card">{{error}}</view>
<block wx:elif="{{certificate}}">
<view wx:if="{{certificate.status==='REVOKED'}}" class="certificate-revoked">该证书已撤销{{certificate.revokeReason?':'+certificate.revokeReason:''}}</view>
<view class="old-card certificate"><text class="certificate-title">{{certificate.title}}</text><text class="certificate-name">{{certificate.recipientName}}</text><text class="certificate-message">{{certificate.message}}</text><text class="certificate-issuer">{{certificate.issuerName}}</text><text class="certificate-date">{{certificate.issuedDate}}</text><text class="certificate-no">证书编号:{{certificate.certificateNo}}</text></view>
<view class="old-card"><text class="old-title">使用说明</text><text class="old-muted">{{certificate.legalNotice}}</text></view>
</block>
</view>
@@ -0,0 +1,62 @@
@import '../../old-items.wxss';
.certificate {
position: relative;
padding: 64rpx 40rpx;
overflow: hidden;
border: 6rpx double #ef9c72;
text-align: center;
}
.certificate::before {
position: absolute;
top: -60rpx;
right: -30rpx;
color: rgba(240, 90, 40, 0.08);
content: '♥';
font-size: 260rpx;
}
.certificate-title {
display: block;
color: #b8421d;
font-size: 48rpx;
font-weight: 800;
letter-spacing: 6rpx;
}
.certificate-name {
display: block;
margin-top: 46rpx;
font-size: 40rpx;
font-weight: 700;
}
.certificate-message {
display: block;
margin-top: 28rpx;
color: #554b44;
line-height: 1.9;
text-align: left;
}
.certificate-issuer {
display: block;
margin-top: 50rpx;
font-weight: 700;
text-align: right;
}
.certificate-date {
display: block;
color: #687078;
font-size: 26rpx;
text-align: right;
}
.certificate-no {
display: block;
margin-top: 40rpx;
color: #918a83;
font-size: 24rpx;
}
.certificate-revoked {
margin-bottom: 24rpx;
padding: 20rpx;
color: #b42318;
background: #fff0ef;
border-radius: 18rpx;
}
@@ -0,0 +1 @@
{ "navigationBarTitleText": "捐赠溯源详情" }
@@ -0,0 +1,80 @@
import type { OldItemDonationItem } from '@lehuo/contracts';
import { getMyOldItemDonation, getMyOldItemImageUrl } from '../../services/old-items';
const labels: Record<string, string> = {
SUBMITTED: '已提交,等待审核',
REJECTED: '审核未通过',
WAITING_PICKUP: '审核通过,等待取件',
WAITING_DROP_OFF: '审核通过,等待投递',
RECEIVED: '物资已接收',
PROCESSING: '物资正在分拣消毒',
COMPLETED: '帮扶已完成',
CANCELLED: '捐赠已取消',
};
const inventoryLabels: Record<string, string> = {
IN_STOCK: '物资已入库',
SORTED: '物资已完成分拣',
DISINFECTED: '物资已完成消毒',
OUTBOUND: '物资已出库',
DELIVERED: '物资已送达帮扶地',
};
Page({
data: {
loading: true,
error: '',
donation: null as
| (OldItemDonationItem & {
statusLabel: string;
historyView: Array<{ label: string; time: string; note: string | null }>;
inventoryView: Array<{ label: string; time: string; location: string | null }>;
})
| null,
imageUrls: [] as string[],
},
onLoad(o) {
if (!o['id']) {
this.setData({ loading: false, error: '缺少捐赠记录参数' });
return;
}
void this.load(o['id']);
},
async load(id: string) {
try {
const d = (await getMyOldItemDonation(id)).donation;
const imageUrls = (
await Promise.all(
d.imagePublicIds.map(async (fileId) => {
try {
return (await getMyOldItemImageUrl(fileId)).downloadUrl;
} catch {
return '';
}
}),
)
).filter(Boolean);
this.setData({
imageUrls,
donation: {
...d,
statusLabel: labels[d.status] ?? d.status,
historyView: d.history.map((h) => ({
label: labels[h.toStatus] ?? h.toStatus,
time: new Date(h.createdAt).toLocaleString('zh-CN'),
note: h.note,
})),
inventoryView: d.inventoryTrace.map((event) => ({
label: inventoryLabels[event.status] ?? event.status,
time: new Date(event.eventAt).toLocaleString('zh-CN'),
location: event.locationText,
})),
},
});
} catch (e) {
this.setData({ error: e instanceof Error ? e.message : '加载失败' });
} finally {
this.setData({ loading: false });
}
},
rewards() {
void wx.navigateTo({ url: '/pages/old-item-rewards/old-item-rewards' });
},
});
@@ -0,0 +1 @@
<view class="old-page"><view wx:if="{{error}}" class="old-card">{{error}}</view><block wx:elif="{{donation}}"><view class="old-hero"><text class="old-hero__copy">公益编号 {{donation.donationNo}}</text><text class="old-hero__title">{{donation.statusLabel}}</text></view><view class="old-card"><text class="old-title">捐赠信息</text><text class="old-muted">数量:{{donation.quantity}} 件</text><text class="old-muted">说明:{{donation.description}}</text><text class="old-muted">方式:{{donation.method==='PICKUP'?'上门取件':'定点投递'}}</text><text wx:if="{{donation.targetNeed}}" class="old-muted">定向需求:{{donation.targetNeed.title}}</text><text wx:if="{{donation.rejectionReason}}" class="old-muted">驳回原因:{{donation.rejectionReason}}</text><view wx:if="{{imageUrls.length}}" class="old-photo-grid"><image wx:for="{{imageUrls}}" wx:key="*this" class="old-photo" src="{{item}}" mode="aspectFill"></image></view></view><view class="old-card"><text class="old-title">捐赠处理记录</text><view class="old-trace"><view wx:for="{{donation.historyView}}" wx:key="time"><text class="old-title">{{item.label}}</text><text class="old-muted">{{item.time}}</text><text wx:if="{{item.note}}" class="old-muted">{{item.note}}</text></view></view></view><view wx:if="{{donation.inventoryView.length}}" class="old-card"><text class="old-title">物资全流程溯源</text><view class="old-trace"><view wx:for="{{donation.inventoryView}}" wx:key="time"><text class="old-title">{{item.label}}</text><text class="old-muted">{{item.time}}</text><text wx:if="{{item.location}}" class="old-muted">帮扶地区:{{item.location}}</text></view></view></view><view class="old-card"><text class="old-title">公益荣誉凭证</text><text class="old-muted">捐赠完成后会生成电子荣誉证书并记入公益荣誉积分。证书不作为捐赠票据、税务凭证或财产权利证明。</text><button class="old-secondary" bindtap="rewards">查看证书与积分</button></view></block></view>
@@ -0,0 +1 @@
@import '../../old-items.wxss';
@@ -0,0 +1 @@
{ "navigationBarTitleText": "捐赠旧物" }
@@ -0,0 +1,154 @@
import type {
OldItemCategory,
OldItemCondition,
OldItemDonationMethod,
OldItemDropOffPoint,
} from '@lehuo/contracts';
import {
createOldItemDonation,
cryptoId,
getOldItemDropOffPoints,
uploadOldItemImage,
} from '../../services/old-items';
const categories = [
{ code: 'CLOTHING', label: '服饰' },
{ code: 'BOOKS', label: '书籍' },
{ code: 'HOUSEHOLD', label: '生活用品' },
{ code: 'SPORTS', label: '文体用品' },
];
const conditions = [
{ code: 'NEW', label: '全新' },
{ code: 'LIKE_NEW', label: '九成新' },
{ code: 'GOOD', label: '使用良好' },
{ code: 'FAIR', label: '可正常使用' },
];
const isCategory = (value: string): value is OldItemCategory =>
categories.some((item) => item.code === value);
const isCondition = (value: string): value is OldItemCondition =>
conditions.some((item) => item.code === value);
const isMethod = (value: string): value is OldItemDonationMethod =>
value === 'PICKUP' || value === 'DROP_OFF';
Page({
data: {
categories,
conditions,
category: 'CLOTHING',
itemCondition: 'GOOD',
quantity: 1,
description: '',
method: 'PICKUP',
contactName: '',
mobile: '',
address: '',
appointmentAt: '',
points: [] as OldItemDropOffPoint[],
pointIndex: -1,
localImages: [] as string[],
imagePublicIds: [] as string[],
targetNeedPublicId: '',
submitting: false,
error: '',
},
onLoad(options) {
this.setData({
targetNeedPublicId: options['need'] || '',
category: isCategory(options['category'] || '') ? options['category'] : 'CLOTHING',
});
void this.loadPoints();
},
async loadPoints() {
try {
this.setData({ points: (await getOldItemDropOffPoints()).items });
} catch {
this.setData({ points: [] });
}
},
setCategory(e: WechatMiniprogram.BaseEvent) {
const value = String(e.currentTarget.dataset['value']);
if (isCategory(value)) this.setData({ category: value });
},
setCondition(e: WechatMiniprogram.BaseEvent) {
const value = String(e.currentTarget.dataset['value']);
if (isCondition(value)) this.setData({ itemCondition: value });
},
setMethod(e: WechatMiniprogram.BaseEvent) {
const value = String(e.currentTarget.dataset['value']);
if (isMethod(value)) this.setData({ method: value });
},
input(e: WechatMiniprogram.Input) {
const field = String(e.currentTarget.dataset['field']);
this.setData({ [field]: e.detail.value });
},
pointChange(e: WechatMiniprogram.CustomEvent) {
this.setData({ pointIndex: Number(e.detail.value) });
},
async chooseImages() {
const remain = 6 - this.data.localImages.length;
if (remain <= 0) return;
try {
const picked = await wx.chooseMedia({
count: remain,
mediaType: ['image'],
sourceType: ['album', 'camera'],
sizeType: ['compressed'],
});
const paths = picked.tempFiles.map((x) => x.tempFilePath);
this.setData({ submitting: true, error: '正在安全上传图片…' });
const ids = [] as string[];
for (const path of paths) ids.push(await uploadOldItemImage(path));
this.setData({
localImages: [...this.data.localImages, ...paths],
imagePublicIds: [...this.data.imagePublicIds, ...ids],
error: '',
});
} catch (e) {
this.setData({ error: e instanceof Error ? e.message : '图片上传失败' });
} finally {
this.setData({ submitting: false });
}
},
removeImage(e: WechatMiniprogram.BaseEvent) {
const index = Number(e.currentTarget.dataset['index']);
this.setData({
localImages: this.data.localImages.filter((_, i) => i !== index),
imagePublicIds: this.data.imagePublicIds.filter((_, i) => i !== index),
});
},
async submit() {
if (this.data.submitting) return;
const point = this.data.points[this.data.pointIndex];
this.setData({ submitting: true, error: '' });
try {
const result = await createOldItemDonation({
clientRequestId: cryptoId(),
category: this.data.category as OldItemCategory,
itemCondition: this.data.itemCondition as OldItemCondition,
quantity: Number(this.data.quantity),
description: this.data.description,
method: this.data.method as OldItemDonationMethod,
contactName: this.data.contactName,
mobile: this.data.mobile,
address: this.data.method === 'PICKUP' ? this.data.address : undefined,
appointmentAt:
this.data.method === 'PICKUP'
? new Date(this.data.appointmentAt.replace(' ', 'T')).toISOString()
: undefined,
dropOffPointPublicId: this.data.method === 'DROP_OFF' ? point?.publicId : undefined,
targetNeedPublicId: this.data.targetNeedPublicId || undefined,
imagePublicIds: this.data.imagePublicIds,
});
await wx.showModal({
title: '提交成功',
content: `公益编号:${result.donation.donationNo}`,
showCancel: false,
});
void wx.redirectTo({
url: `/pages/old-item-detail/old-item-detail?id=${result.donation.publicId}`,
});
} catch (e) {
this.setData({ error: e instanceof Error ? e.message : '提交失败,请检查填写内容' });
} finally {
this.setData({ submitting: false });
}
},
});
@@ -0,0 +1 @@
<view class="old-page"><view class="old-card old-guide"><text class="old-title">捐赠须知</text><text>可捐:干净完好、功能正常的服饰、书籍、生活及文体用品。</text><text>禁捐:贴身衣物、破损霉变物品、药品、食品及存在安全风险的用品。</text></view><view class="old-card"><text class="old-title">1. 选择品类</text><view class="old-options"><button wx:for="{{categories}}" wx:key="code" class="{{category===item.code?'active':''}}" data-value="{{item.code}}" bindtap="setCategory">{{item.label}}</button></view><text class="old-field">新旧程度</text><view class="old-options"><button wx:for="{{conditions}}" wx:key="code" class="{{itemCondition===item.code?'active':''}}" data-value="{{item.code}}" bindtap="setCondition">{{item.label}}</button></view><label class="old-field">数量<input type="number" value="{{quantity}}" data-field="quantity" bindinput="input"/></label><label class="old-field">物品描述<textarea value="{{description}}" maxlength="1000" placeholder="请说明尺寸、使用情况等" data-field="description" bindinput="input"></textarea></label><text class="old-field">物品图片(最多 6 张)</text><view class="old-photo-grid"><view wx:for="{{localImages}}" wx:key="*this"><image class="old-photo" src="{{item}}" mode="aspectFill"></image><button class="old-remove" data-index="{{index}}" bindtap="removeImage">移除</button></view></view><button class="old-secondary" bindtap="chooseImages" disabled="{{submitting||localImages.length>=6}}">选择图片</button></view><view class="old-card"><text class="old-title">2. 捐赠方式</text><view class="old-options"><button class="{{method==='PICKUP'?'active':''}}" data-value="PICKUP" bindtap="setMethod">上门取件</button><button class="{{method==='DROP_OFF'?'active':''}}" data-value="DROP_OFF" bindtap="setMethod">定点投递</button></view><label class="old-field">联系人<input value="{{contactName}}" data-field="contactName" bindinput="input"/></label><label class="old-field">手机号<input type="number" maxlength="11" value="{{mobile}}" data-field="mobile" bindinput="input"/></label><block wx:if="{{method==='PICKUP'}}"><label class="old-field">取件地址<textarea value="{{address}}" data-field="address" bindinput="input"></textarea></label><label class="old-field">预约时间<input value="{{appointmentAt}}" placeholder="如 2026-08-20 14:00" data-field="appointmentAt" bindinput="input"/></label></block><block wx:else><label class="old-field">投递点<picker range="{{points}}" range-key="name" value="{{pointIndex}}" bindchange="pointChange"><view>{{pointIndex>=0?points[pointIndex].name:'请选择投递点'}}</view></picker></label><text wx:if="{{!points.length}}" class="old-muted">暂无启用投递点,请改用上门取件或稍后再试。</text></block></view><text wx:if="{{error}}" class="old-muted">{{error}}</text><button class="old-primary" disabled="{{submitting}}" bindtap="submit">{{submitting?'处理中…':'提交审核'}}</button></view>
@@ -0,0 +1 @@
@import '../../old-items.wxss';
@@ -0,0 +1 @@
{ "navigationBarTitleText": "捐赠规范指南" }
@@ -0,0 +1 @@
Page({});
@@ -0,0 +1 @@
<view class="old-page"><view class="old-hero"><text class="old-hero__title">让善意安全抵达</text><text class="old-hero__copy">提交前请确认物品符合以下规范</text></view><view class="old-card old-guide"><text class="old-title">可以捐赠</text><text>✓ 清洗干净、无明显污渍破损的四季衣物</text><text>✓ 内容健康、无严重缺页涂画的书籍</text><text>✓ 功能正常、配件齐全的生活用品</text><text>✓ 安全完好、可继续使用的文体用品</text></view><view class="old-card old-guide"><text class="old-title">禁止捐赠</text><text>× 贴身衣物、药品、食品、化妆品</text><text>× 霉变、虫蛀、严重破损或无法清洁的物品</text><text>× 易燃易爆、尖锐危险及来源不明物品</text><text>× 涉及盗版、违法或不适合公益流转的内容</text></view><view class="old-card old-guide"><text class="old-title">整理与交接</text><text>请分类装袋或装箱,易碎品做好保护并在描述中注明。上门取件以后台确认的预约安排为准;定点投递请在开放时间内送达并出示公益编号。</text></view></view>
@@ -0,0 +1 @@
@import '../../old-items.wxss';
@@ -0,0 +1 @@
{ "navigationBarTitleText": "旧物善用" }
@@ -0,0 +1,47 @@
import { getOldItemNeeds, getOldItemStats } from '../../services/old-items';
Page({
data: {
loading: true,
error: '',
stats: { totalItems: 0, assistedPeople: 0, monthlyKindActs: 0 },
needs: [] as Array<{
publicId: string;
title: string;
regionText: string;
remainingQuantity: number;
}>,
},
onShow() {
void this.load();
},
async load() {
this.setData({ loading: true, error: '' });
try {
const [stats, needs] = await Promise.all([getOldItemStats(), getOldItemNeeds()]);
this.setData({ stats, needs: needs.items.slice(0, 3) });
} catch (error) {
this.setData({ error: error instanceof Error ? error.message : '加载失败' });
} finally {
this.setData({ loading: false });
}
},
donate() {
void wx.navigateTo({ url: '/pages/old-item-donate/old-item-donate' });
},
records() {
void wx.navigateTo({ url: '/pages/old-item-records/old-item-records' });
},
rewards() {
void wx.navigateTo({ url: '/pages/old-item-rewards/old-item-rewards' });
},
needs() {
void wx.navigateTo({ url: '/pages/old-item-needs/old-item-needs' });
},
news() {
void wx.navigateTo({ url: '/pages/content/content?type=PUBLIC_WELFARE_NEWS' });
},
guide() {
void wx.navigateTo({ url: '/pages/old-item-guide/old-item-guide' });
},
});
@@ -0,0 +1,10 @@
<view class="old-page">
<view class="old-hero"><text class="old-hero__title">闲置不浪费,旧物有温度</text><text class="old-hero__copy">让每一件闲置物品,抵达真正需要的人身边</text></view>
<view wx:if="{{error}}" class="old-card"><text>{{error}}</text><button class="old-secondary" bindtap="load">重试</button></view>
<block wx:else>
<view class="old-card"><button class="old-primary" bindtap="donate">我要捐赠旧物</button><button class="old-secondary" bindtap="records">我的捐赠记录</button><button class="old-secondary" bindtap="rewards">我的证书与荣誉积分</button></view>
<view class="old-card old-stats"><view class="old-stat"><text>{{stats.totalItems}}</text><text>累计捐赠件数</text></view><view class="old-stat"><text>{{stats.assistedPeople}}</text><text>累计帮扶人数</text></view><view class="old-stat"><text>{{stats.monthlyKindActs}}</text><text>本月爱心人次</text></view></view>
<view class="old-grid"><button bindtap="needs"><text>物资需求公示</text><text>精准帮扶</text></button><button bindtap="records"><text>捐赠进度溯源</text><text>全程透明</text></button><button bindtap="news"><text>公益动态资讯</text><text>项目进展</text></button><button bindtap="guide"><text>捐赠规范指南</text><text>可捐 / 禁捐说明</text></button></view>
<view class="old-card"><text class="old-title">最新物资需求</text><view wx:for="{{needs}}" wx:key="publicId"><text class="old-muted">{{item.title}} · {{item.regionText}} · 尚需 {{item.remainingQuantity}} 件</text></view><text wx:if="{{!needs.length}}" class="old-muted">当前暂无已发布需求,请稍后查看。</text></view>
</block>
</view>
@@ -0,0 +1 @@
@import '../../old-items.wxss';
@@ -0,0 +1 @@
{ "navigationBarTitleText": "物资需求公示" }
@@ -0,0 +1,48 @@
import type { OldItemNeedItem } from '@lehuo/contracts';
import { getOldItemNeeds } from '../../services/old-items';
const labels: Record<string, string> = {
CLOTHING: '过冬衣物',
BOOKS: '学习用品',
HOUSEHOLD: '生活用品',
SPORTS: '文体用品',
};
Page({
data: {
loading: true,
error: '',
category: '',
tabs: [
{ value: '', label: '全部' },
{ value: 'CLOTHING', label: '过冬衣物' },
{ value: 'BOOKS', label: '学习用品' },
{ value: 'HOUSEHOLD', label: '生活用品' },
{ value: 'SPORTS', label: '文体用品' },
],
items: [] as Array<OldItemNeedItem & { categoryLabel: string }>,
},
onLoad() {
void this.load();
},
async load() {
this.setData({ loading: true, error: '' });
try {
const r = await getOldItemNeeds(this.data.category);
this.setData({
items: r.items.map((x) => ({ ...x, categoryLabel: labels[x.category] ?? x.category })),
});
} catch (e) {
this.setData({ error: e instanceof Error ? e.message : '加载失败' });
} finally {
this.setData({ loading: false });
}
},
filter(e: WechatMiniprogram.BaseEvent) {
this.setData({ category: String(e.currentTarget.dataset['category'] ?? '') });
void this.load();
},
donate(e: WechatMiniprogram.BaseEvent) {
void wx.navigateTo({
url: `/pages/old-item-donate/old-item-donate?need=${String(e.currentTarget.dataset['id'])}&category=${String(e.currentTarget.dataset['category'])}`,
});
},
});
@@ -0,0 +1 @@
<view class="old-page"><view class="old-hero"><text class="old-hero__title">精准物资需求</text><text class="old-hero__copy">需求经后台发布,捐赠可直接定向关联</text></view><view class="old-options"><button wx:for="{{tabs}}" wx:key="value" class="{{category===item.value?'active':''}}" data-category="{{item.value}}" bindtap="filter">{{item.label}}</button></view><view wx:if="{{error}}" class="old-card">{{error}}</view><view wx:for="{{items}}" wx:key="publicId" class="old-card"><text class="old-title">{{item.title}}</text><text class="old-muted">{{item.regionText}} · {{item.beneficiaryGroup}}</text><text class="old-muted">{{item.description}}</text><text class="old-muted">{{item.categoryLabel}} · 尚需 {{item.remainingQuantity}} / {{item.requestedQuantity}} 件</text><button class="old-primary" data-id="{{item.publicId}}" data-category="{{item.category}}" bindtap="donate">定向捐赠</button></view><view wx:if="{{!loading&&!error&&!items.length}}" class="old-empty">当前暂无已发布需求</view></view>
@@ -0,0 +1 @@
@import '../../old-items.wxss';
@@ -0,0 +1 @@
{ "navigationBarTitleText": "我的捐赠记录" }
@@ -0,0 +1,65 @@
import type { OldItemDonationItem } from '@lehuo/contracts';
import { getMyOldItemDonations } from '../../services/old-items';
const labels: Record<string, string> = {
SUBMITTED: '待审核',
REJECTED: '已驳回',
WAITING_PICKUP: '待取件',
WAITING_DROP_OFF: '待投递',
RECEIVED: '已接收',
PROCESSING: '分拣中',
COMPLETED: '已完成',
CANCELLED: '已取消',
};
const categoryLabels: Record<string, string> = {
CLOTHING: '服饰',
BOOKS: '书籍',
HOUSEHOLD: '生活用品',
SPORTS: '文体用品',
};
Page({
data: {
loading: true,
error: '',
status: '',
tabs: [
{ value: '', label: '全部' },
{ value: 'SUBMITTED', label: '待审核' },
{ value: 'WAITING_PICKUP', label: '待取件' },
{ value: 'COMPLETED', label: '已完成' },
{ value: 'REJECTED', label: '已驳回' },
],
items: [] as Array<
OldItemDonationItem & { statusLabel: string; categoryLabel: string; dateText: string }
>,
},
onShow() {
void this.load();
},
async load() {
this.setData({ loading: true, error: '' });
try {
const r = await getMyOldItemDonations(this.data.status);
this.setData({
items: r.items.map((x) => ({
...x,
statusLabel: labels[x.status] ?? x.status,
categoryLabel: categoryLabels[x.category] ?? x.category,
dateText: new Date(x.createdAt).toLocaleDateString('zh-CN'),
})),
});
} catch (e) {
this.setData({ error: e instanceof Error ? e.message : '加载失败' });
} finally {
this.setData({ loading: false });
}
},
filter(e: WechatMiniprogram.BaseEvent) {
this.setData({ status: String(e.currentTarget.dataset['status'] ?? '') });
void this.load();
},
open(e: WechatMiniprogram.BaseEvent) {
void wx.navigateTo({
url: `/pages/old-item-detail/old-item-detail?id=${String(e.currentTarget.dataset['id'])}`,
});
},
});
@@ -0,0 +1 @@
<view class="old-page"><view class="old-options"><button wx:for="{{tabs}}" wx:key="value" class="{{status===item.value?'active':''}}" data-status="{{item.value}}" bindtap="filter">{{item.label}}</button></view><view wx:if="{{error}}" class="old-card"><text>{{error}}</text><button class="old-secondary" bindtap="load">重试</button></view><button wx:for="{{items}}" wx:key="publicId" class="old-record" data-id="{{item.publicId}}" bindtap="open"><view class="old-record__head"><text class="old-title">{{item.categoryLabel}} · {{item.quantity}}件</text><text class="old-record__status">{{item.statusLabel}}</text></view><text class="old-muted">{{item.donationNo}}</text><text class="old-muted">{{item.dateText}} · {{item.method==='PICKUP'?'上门取件':'定点投递'}}</text></button><view wx:if="{{!loading&&!error&&!items.length}}" class="old-empty">暂无符合条件的捐赠记录</view></view>
@@ -0,0 +1 @@
@import '../../old-items.wxss';
@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "证书与荣誉积分"
}
@@ -0,0 +1,47 @@
import type { OldItemRewardsSummary } from '@lehuo/contracts';
import { getMyOldItemRewards } from '../../services/old-items';
Page({
data: {
loading: true,
error: '',
summary: null as
| (OldItemRewardsSummary & {
certificateView: Array<
OldItemRewardsSummary['certificates'][number] & { issuedDate: string }
>;
entryView: Array<OldItemRewardsSummary['entries'][number] & { createdDate: string }>;
})
| null,
},
onShow() {
void this.load();
},
async load() {
this.setData({ loading: true, error: '' });
try {
const summary = await getMyOldItemRewards();
this.setData({
summary: {
...summary,
certificateView: summary.certificates.map((item) => ({
...item,
issuedDate: new Date(item.issuedAt).toLocaleDateString('zh-CN'),
})),
entryView: summary.entries.map((item) => ({
...item,
createdDate: new Date(item.createdAt).toLocaleString('zh-CN'),
})),
},
});
} catch (error) {
this.setData({ error: error instanceof Error ? error.message : '加载失败' });
} finally {
this.setData({ loading: false });
}
},
openCertificate(event: WechatMiniprogram.BaseEvent) {
const id = event.currentTarget.dataset['id'] as string;
void wx.navigateTo({ url: `/pages/old-item-certificate/old-item-certificate?id=${id}` });
},
});
@@ -0,0 +1,10 @@
<view class="old-page">
<view class="old-hero"><text class="old-hero__copy">旧物善用 · 公益荣誉</text><text class="old-hero__title">每一份善意,都值得被记住</text></view>
<view wx:if="{{loading}}" class="old-card"><text class="old-muted">正在加载荣誉记录…</text></view>
<view wx:elif="{{error}}" class="old-card"><text>{{error}}</text><button class="old-secondary" bindtap="load">重试</button></view>
<block wx:elif="{{summary}}">
<view class="old-card reward-balance"><text class="reward-number">{{summary.balance}}</text><text class="reward-label">公益荣誉积分</text><text class="old-muted">仅用于记录公益参与,不可兑换、转让、提现或购买商品</text></view>
<view class="old-card"><text class="old-title">我的电子证书</text><view wx:for="{{summary.certificateView}}" wx:key="publicId" class="reward-row" data-id="{{item.publicId}}" bindtap="openCertificate"><view class="reward-content"><text class="old-title">{{item.title}}</text><text class="old-muted">{{item.certificateNo}} · {{item.issuedDate}}</text><text wx:if="{{item.status==='REVOKED'}}" class="reward-status">已撤销</text></view><text>查看 ›</text></view><text wx:if="{{!summary.certificates.length}}" class="old-muted">完成旧物捐赠后,将在这里看到电子荣誉证书。</text></view>
<view class="old-card"><text class="old-title">积分记录</text><view wx:for="{{summary.entryView}}" wx:key="publicId" class="reward-row"><view class="reward-content"><text>{{item.entryType==='EARNED'?'完成旧物捐赠':'后台校正'}}</text><text class="old-muted">{{item.donationNo || item.note}} · {{item.createdDate}}</text><text class="old-muted">结余 {{item.balanceAfter}}</text></view><text class="reward-delta {{item.pointsDelta<0?'reward-delta--negative':''}}">{{item.pointsDelta>0?'+':''}}{{item.pointsDelta}}</text></view><text wx:if="{{!summary.entries.length}}" class="old-muted">暂无积分记录。</text></view>
</block>
</view>
@@ -0,0 +1,43 @@
@import '../../old-items.wxss';
.reward-balance {
text-align: center;
}
.reward-number {
display: block;
color: #e85220;
font-size: 72rpx;
font-weight: 800;
}
.reward-label {
color: #687078;
font-size: 26rpx;
}
.reward-row {
display: flex;
padding: 24rpx 0;
border-bottom: 2rpx solid #f0f1f2;
justify-content: space-between;
gap: 20rpx;
}
.reward-row:last-child {
border-bottom: 0;
}
.reward-content {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
}
.reward-delta {
color: #2f9b58;
font-size: 34rpx;
font-weight: 700;
}
.reward-delta--negative {
color: #b42318;
}
.reward-status {
color: #b42318;
font-size: 25rpx;
}
@@ -46,4 +46,7 @@ Page({
organization() {
void wx.navigateTo({ url: '/pages/content/content?type=ORGANIZATION_INTRO' });
},
oldItems() {
void wx.navigateTo({ url: '/pages/old-item-home/old-item-home' });
},
});
@@ -3,9 +3,9 @@
<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 class="profile-trust"><view><text class="profile-trust__title">隐私保护</text><text>最少采集</text></view><view><text class="profile-trust__title">服务可查</text><text>进度留痕</text></view><view><text class="profile-trust__title">消息可达</text><text>及时通知</text></view></view></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="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="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="oldItems"><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>
@@ -9,12 +9,19 @@ Page({
tone: 'orange',
},
{
code: 'WOMEN_CHILDREN',
label: '妇女儿童帮扶',
description: '困境支持、成长陪伴与权益关爱',
icon: '爱',
code: 'WOMEN',
label: '困境妇女',
description: '困境支持、权益关爱与资源链接',
icon: '女',
tone: 'rose',
},
{
code: 'CHILD',
label: '儿童关爱',
description: '成长陪伴、家庭支持与儿童关爱',
icon: '童',
tone: 'orange',
},
{
code: 'DISABILITY',
label: '残障帮扶',
@@ -23,9 +30,9 @@ Page({
tone: 'blue',
},
{
code: 'DIFFICULT_FAMILY',
label: '困难家庭服务',
description: '生活困难、资源链接与持续关爱',
code: 'LIFE_DIFFICULTY',
label: '生活困难',
description: '生活支持、资源链接与持续关爱',
icon: '家',
tone: 'green',
},
@@ -1,7 +1,7 @@
<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>
<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>
@@ -0,0 +1,95 @@
import type {
CreateOldItemDonationRequest,
CreateUploadIntentResponse,
OldItemDonationItem,
OldItemDropOffPoint,
OldItemNeedItem,
OldItemCertificateItem,
OldItemRewardsSummary,
OldItemStats,
DownloadUrlResponse,
} from '@lehuo/contracts';
import { apiRequest } from './api-client';
export interface Page<T> {
items: T[];
page: number;
pageSize: number;
total: number;
}
export const getOldItemStats = () => apiRequest<OldItemStats>('/api/v1/old-items/stats');
export const getOldItemNeeds = (category = '') =>
apiRequest<Page<OldItemNeedItem>>(
`/api/v1/old-items/needs?page=1&pageSize=50${category ? `&category=${category}` : ''}`,
);
export const getOldItemDropOffPoints = () =>
apiRequest<{ items: OldItemDropOffPoint[] }>('/api/v1/old-items/drop-off-points');
export const createOldItemDonation = (input: CreateOldItemDonationRequest) =>
apiRequest<{ donation: OldItemDonationItem }>('/api/v1/old-items/donations', {
method: 'POST',
data: input,
});
export const getMyOldItemDonations = (status = '') =>
apiRequest<Page<OldItemDonationItem>>(
`/api/v1/old-items/me/donations?page=1&pageSize=50${status ? `&status=${status}` : ''}`,
);
export const getMyOldItemDonation = (id: string) =>
apiRequest<{ donation: OldItemDonationItem }>(`/api/v1/old-items/me/donations/${id}`);
export const getMyOldItemRewards = () =>
apiRequest<OldItemRewardsSummary>('/api/v1/old-items/me/rewards');
export const getMyOldItemCertificate = (id: string) =>
apiRequest<{ certificate: OldItemCertificateItem }>(`/api/v1/old-items/me/certificates/${id}`);
export const getMyOldItemImageUrl = (id: string) =>
apiRequest<DownloadUrlResponse>(`/api/v1/old-items/me/media/${id}/download`);
function fileInfo(path: string): Promise<{ size: number }> {
return new Promise((resolve, reject) =>
wx.getFileInfo({ filePath: path, success: resolve, fail: reject }),
);
}
function read(path: string): Promise<ArrayBuffer> {
return new Promise((resolve, reject) =>
wx
.getFileSystemManager()
.readFile({ filePath: path, success: (r) => resolve(r.data as ArrayBuffer), fail: reject }),
);
}
function put(url: string, data: ArrayBuffer, headers: Record<string, string>): Promise<void> {
return new Promise((resolve, reject) =>
wx.request({
url,
method: 'PUT',
data,
header: headers,
success: (r) =>
r.statusCode >= 200 && r.statusCode < 300 ? resolve() : reject(new Error('图片上传失败')),
fail: reject,
}),
);
}
export async function uploadOldItemImage(path: string) {
const info = await fileInfo(path);
const ext = (path.split('.').pop() || 'jpg').toLowerCase();
const mimeType = ext === 'png' ? 'image/png' : ext === 'webp' ? 'image/webp' : 'image/jpeg';
const intent = await apiRequest<CreateUploadIntentResponse>(
'/api/v1/old-items/media/upload-intents',
{
method: 'POST',
data: {
clientRequestId: cryptoId(),
businessType: 'OLD_ITEM_DONATION_IMAGE',
filename: `donation.${ext === 'jpeg' ? 'jpg' : ext}`,
mimeType,
size: info.size,
},
},
);
await put(intent.uploadUrl, await read(path), intent.uploadHeaders);
await apiRequest(`/api/v1/old-items/media/${intent.file.publicId}/confirm`, { method: 'POST' });
return intent.file.publicId;
}
export function cryptoId() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0,
v = c === 'x' ? r : (r & 3) | 8;
return v.toString(16);
});
}