版本冻结 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 忽略,不进入版本库。
260 lines
8.8 KiB
TypeScript
260 lines
8.8 KiB
TypeScript
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 : '提交失败,请稍后重试,已填写内容不会清空';
|
||
},
|
||
});
|