Files
lehuo/apps/miniprogram/miniprogram/pages/application-form/application-form.ts
T
admin 7e71455c6b chore(release): freeze v0.2.0
版本冻结 v0.2.0,固化 2026-09-24 本轮全部变更。

admin-web:
- 修复旧物善用管理页缺失样式类 .workspace-view / .view-header / .table-wrap,
  此前页面无内边距导致内容被侧边栏遮挡、窄屏表格无横向滚动
- 修复服务统计页缺失样式类 .dashboard-grid / .dashboard-card / .stat-row,
  此前 8 个指标卡退化为裸文本堆叠
- 补齐未定义的 --shadow-card 变量(财务编辑面板阴影此前失效)
- 侧边栏 13 项导航由手写重复改为 NAV_ITEMS 数据驱动,并按业务顺序重排
- AdminIcon 补齐 coins / phone / volunteer / flag / calendar / recycle 语义图标
- 侧边栏行高与间距压缩使 13 项一屏放下,滚动条隐藏
- 小于等于 760px 断点导航由 display:none 改为横向滚动,移动端恢复可切换

api:
- prisma.config.ts 显式加载仓库根 .env,修复 prisma generate / migrate 取不到 DATABASE_URL
- dev / bootstrap:admin / test:integration 增加 tsx --env-file-if-exists=../../.env

miniprogram:
- custom-tab-bar 选中态与申请表单分步流程抽为可测纯函数并补充单测
- 页面完整性门禁脚本增强

工程:
- 版本统一升位 0.1.0 到 0.2.0(根与 4 个 workspace 包)
- .gitignore 增加 .pnpm-store/、.workbuddy/、微信开发者工具私人配置
  与仓库根目录误生成的工程配置,避免本地产物与机器相关信息入库

验证:pnpm lint 通过、pnpm test:unit 19 个用例通过、miniprogram typecheck 通过、
admin-web typecheck 与 build 通过、pnpm install --frozen-lockfile 未改写 lockfile、
无头浏览器端到端登录复验通过、待提交内容密钥扫描无命中。

说明:本轮定位并排除了一起 Prisma 生成目录残缺事故——API 开发进程与并发的
prisma generate 争抢同一输出目录,Windows 文件锁导致只生成 34/56 个模型文件,
使 TS 全量类型解析失败并产生数千条 lint 报错;停止竞争进程后干净重新生成即恢复。
该生成目录已被 .gitignore 忽略,不进入版本库。
2026-09-24 19:50:07 +08:00

260 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
ApplicationApplyMode,
CreateServiceApplicationRequest,
ServiceCategory,
ServiceCategoryOption,
} from '@lehuo/contracts';
import {
APPLICATION_APPLY_MODE_LABELS,
APPLICATION_APPLY_MODES,
SERVICE_CATEGORIES,
SERVICE_CATEGORY_LABELS,
} from '../../constants/business-options';
import { createApplicationFormEntry } from './application-form-flow';
import { MiniProgramApiError } from '../../services/api-client';
import {
createServiceApplication,
fetchServiceCategories,
} from '../../services/service-applications';
import { createClientRequestId } from '../../utils/request-id';
const DEFAULT_FORM_ENTRY = createApplicationFormEntry(false);
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);
}
function isApplyMode(value: string): value is ApplicationApplyMode {
return (APPLICATION_APPLY_MODES as readonly string[]).includes(value);
}
Page({
data: {
...DEFAULT_FORM_ENTRY,
loading: true,
submitting: false,
errorMessage: '',
categories: LOCAL_SERVICE_CATEGORIES,
modeOptions: APPLICATION_APPLY_MODES.map((code) => ({
code,
label: APPLICATION_APPLY_MODE_LABELS[code],
})),
clientRequestId: createClientRequestId(),
serviceCategory: '',
applyMode: '',
beneficiaryName: '',
contactMobile: '',
regionCode: '',
description: '',
preferredContactTime: '',
needsHomeVisit: false,
mobilityDifficulty: false,
privacyConsent: false,
serviceCategoryLabel: '',
applyModeLabel: '',
contactMobileMasked: '',
},
onLoad(options: Record<string, string | undefined>) {
if (options.mode && APPLICATION_APPLY_MODES.includes(options.mode as ApplicationApplyMode)) {
const mode = options.mode as ApplicationApplyMode;
this.setData({ applyMode: mode, applyModeLabel: APPLICATION_APPLY_MODE_LABELS[mode] });
}
if (options.category && isServiceCategory(options.category)) {
this.setData({
...createApplicationFormEntry(true),
serviceCategory: options.category,
serviceCategoryLabel: SERVICE_CATEGORY_LABELS[options.category],
});
return;
}
void this.loadCategories();
},
async loadCategories() {
this.setData({ loading: true, errorMessage: '' });
try {
const result = await fetchServiceCategories();
this.setData({
categories: result.items.map((item) => ({
...item,
image: CATEGORY_IMAGES[item.code],
})),
loading: false,
});
} catch (error) {
this.setData({
loading: false,
categories: LOCAL_SERVICE_CATEGORIES,
errorMessage: `${this.errorText(error)};您仍可先填写申请,提交前请确认服务连接。`,
});
}
},
retryLoad() {
void this.loadCategories();
},
selectCategory(event: WechatMiniprogram.TouchEvent) {
const code = event.currentTarget.dataset.code as ServiceCategory;
this.setData({
serviceCategory: code,
serviceCategoryLabel: SERVICE_CATEGORY_LABELS[code],
choosingCategory: false,
step: 1,
clientRequestId: createClientRequestId(),
errorMessage: '',
});
},
selectMode(event: WechatMiniprogram.TouchEvent) {
const code = event.currentTarget.dataset.code as ApplicationApplyMode;
this.setData({
applyMode: code,
applyModeLabel: APPLICATION_APPLY_MODE_LABELS[code],
clientRequestId: createClientRequestId(),
errorMessage: '',
});
},
updateBeneficiaryName(event: WechatMiniprogram.Input) {
this.updateField({ beneficiaryName: event.detail.value });
},
updateContactMobile(event: WechatMiniprogram.Input) {
this.updateField({ contactMobile: event.detail.value });
},
updateRegionCode(event: WechatMiniprogram.Input) {
this.updateField({ regionCode: event.detail.value });
},
updateDescription(event: WechatMiniprogram.Input) {
this.updateField({ description: event.detail.value });
},
updatePreferredContactTime(event: WechatMiniprogram.Input) {
this.updateField({ preferredContactTime: event.detail.value });
},
updateNeedsHomeVisit(event: WechatMiniprogram.SwitchChange) {
this.updateField({ needsHomeVisit: event.detail.value });
},
updateMobilityDifficulty(event: WechatMiniprogram.SwitchChange) {
this.updateField({ mobilityDifficulty: event.detail.value });
},
updatePrivacyConsent(event: WechatMiniprogram.CheckboxGroupChange) {
this.setData({ privacyConsent: event.detail.value.includes('accepted'), errorMessage: '' });
},
nextStep() {
const message = this.validateStep(this.data.step);
if (message) {
this.setData({ errorMessage: message });
return;
}
const next = Math.min(this.data.totalSteps, this.data.step + 1);
this.setData({
step: next,
errorMessage: '',
contactMobileMasked: this.data.contactMobile.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2'),
});
},
previousStep() {
this.setData({ step: Math.max(1, this.data.step - 1), errorMessage: '' });
},
async submitApplication() {
if (!this.data.privacyConsent) {
this.setData({ errorMessage: '请阅读并同意隐私授权后再提交' });
return;
}
if (
!isServiceCategory(this.data.serviceCategory) ||
!isApplyMode(this.data.applyMode) ||
this.data.submitting
)
return;
this.setData({ submitting: true, errorMessage: '' });
const input: CreateServiceApplicationRequest = {
clientRequestId: this.data.clientRequestId,
serviceCategory: this.data.serviceCategory,
applyMode: this.data.applyMode,
beneficiaryName: this.data.beneficiaryName.trim(),
contactMobile: this.data.contactMobile.trim(),
regionCode: this.data.regionCode.trim(),
description: this.data.description.trim(),
...(this.data.preferredContactTime.trim()
? { preferredContactTime: this.data.preferredContactTime.trim() }
: {}),
needsHomeVisit: this.data.needsHomeVisit,
mobilityDifficulty: this.data.mobilityDifficulty,
privacyConsent: true,
};
try {
const result = await createServiceApplication(input);
void wx.redirectTo({
url: `/pages/application-success/application-success?publicId=${encodeURIComponent(result.application.publicId)}`,
});
} catch (error) {
this.setData({ submitting: false, errorMessage: this.errorText(error) });
}
},
updateField(values: Record<string, string | boolean>) {
this.setData({
...values,
clientRequestId: createClientRequestId(),
errorMessage: '',
});
},
validateStep(step: number): string {
if (step === 1 && !this.data.applyMode) return '请选择您与服务对象的关系';
if (step === 2) {
if (!this.data.beneficiaryName.trim()) return '请填写服务对象姓名或称呼';
if (!/^1[3-9]\d{9}$/.test(this.data.contactMobile.trim())) return '请填写正确的手机号码';
if (this.data.regionCode.trim().length < 2) return '请填写所在市、区或街道';
}
if (step === 3 && this.data.description.trim().length < 10) {
return '请至少用 10 个字描述目前的困难或希望得到的帮助';
}
return '';
},
errorText(error: unknown): string {
if (error instanceof MiniProgramApiError) {
return `${error.message}${error.requestId ? `(编号:${error.requestId})` : ''}`;
}
return error instanceof Error ? error.message : '提交失败,请稍后重试,已填写内容不会清空';
},
});