From 39a2e53e61808d05cdb7b9a635702371928bbd57 Mon Sep 17 00:00:00 2001 From: g1307092382 <1307092382@qq.com> Date: Thu, 13 Aug 2026 11:32:21 +0800 Subject: [PATCH] fix(miniprogram): restore secondary pages on real devices --- .../miniprogram/constants/business-options.ts | 57 +++++++++++++++++++ .../pages/activity-detail/activity-detail.ts | 4 +- .../application-form/application-form.ts | 19 ++++++- .../assigned-case-detail.ts | 8 +-- .../pages/content-detail/content-detail.ts | 2 +- .../pages/content-detail/content-detail.wxml | 2 +- .../miniprogram/pages/profile/profile.wxml | 2 +- .../miniprogram/pages/profile/profile.wxss | 4 +- .../pages/project-detail/project-detail.ts | 4 +- .../pages/project-detail/project-detail.wxml | 2 +- .../pages/project-detail/project-detail.wxss | 4 +- .../miniprogram/services/api-client.ts | 10 +++- apps/miniprogram/package.json | 6 +- apps/miniprogram/scripts/check-pages.mjs | 47 +++++++++++++++ .../scripts/check-runtime-imports.mjs | 29 ++++++++++ docs/10_TASK_STATE.md | 14 +++++ eslint.config.mjs | 1 + 17 files changed, 192 insertions(+), 23 deletions(-) create mode 100644 apps/miniprogram/miniprogram/constants/business-options.ts create mode 100644 apps/miniprogram/scripts/check-pages.mjs create mode 100644 apps/miniprogram/scripts/check-runtime-imports.mjs diff --git a/apps/miniprogram/miniprogram/constants/business-options.ts b/apps/miniprogram/miniprogram/constants/business-options.ts new file mode 100644 index 0000000..007f4e6 --- /dev/null +++ b/apps/miniprogram/miniprogram/constants/business-options.ts @@ -0,0 +1,57 @@ +import type { ApplicationApplyMode, ServiceCategory, ServiceRecordType } from '@lehuo/contracts'; + +/** + * 小程序运行时使用的稳定业务字典。 + * 类型继续由共享 contracts 约束,但真机运行不得依赖未打包的 workspace 模块。 + */ +export const APPLICATION_APPLY_MODES: readonly ApplicationApplyMode[] = [ + 'SELF', + 'FAMILY', + 'COMMUNITY', + 'VOLUNTEER', + 'OTHER', +]; + +export const APPLICATION_APPLY_MODE_LABELS: Record = { + SELF: '为自己申请', + FAMILY: '帮家人申请', + COMMUNITY: '社区或机构代办', + VOLUNTEER: '志愿者代办', + OTHER: '其他代办', +}; + +export const SERVICE_CATEGORIES: readonly ServiceCategory[] = [ + 'OLD_AGE', + 'WOMEN', + 'CHILD', + 'DISABILITY', + 'LIFE_DIFFICULTY', + 'OTHER', +]; + +export const SERVICE_CATEGORY_LABELS: Record = { + OLD_AGE: '关爱老人', + WOMEN: '困境妇女', + CHILD: '儿童关爱', + DISABILITY: '残障帮扶', + LIFE_DIFFICULTY: '生活困难', + OTHER: '其他服务', +}; + +export const SERVICE_RECORD_TYPES: readonly ServiceRecordType[] = [ + 'HOME_VISIT', + 'PHONE_CONTACT', + 'ESCORT', + 'MATERIAL_SUPPORT', + 'ACTIVITY', + 'OTHER', +]; + +export const SERVICE_RECORD_TYPE_LABELS: Record = { + HOME_VISIT: '上门探访', + PHONE_CONTACT: '电话关怀', + ESCORT: '陪同协助', + MATERIAL_SUPPORT: '物资支持', + ACTIVITY: '公益活动', + OTHER: '其他服务', +}; diff --git a/apps/miniprogram/miniprogram/pages/activity-detail/activity-detail.ts b/apps/miniprogram/miniprogram/pages/activity-detail/activity-detail.ts index 61ba08f..c603efd 100644 --- a/apps/miniprogram/miniprogram/pages/activity-detail/activity-detail.ts +++ b/apps/miniprogram/miniprogram/pages/activity-detail/activity-detail.ts @@ -4,7 +4,9 @@ import { fetchPublicActivity, registerActivity } from '../../services/participat Page({ data: { loading: true, busy: false, message: '', activity: null as ActivityDetail | null }, onLoad(options: Record) { - if (options['publicId']) void this.load(options['publicId']); + const publicId = options['publicId']; + if (publicId) void this.load(publicId); + else this.setData({ loading: false, message: '缺少活动标识,请返回活动列表重新选择。' }); }, async load(publicId: string) { try { diff --git a/apps/miniprogram/miniprogram/pages/application-form/application-form.ts b/apps/miniprogram/miniprogram/pages/application-form/application-form.ts index 6b0bae0..9266b74 100644 --- a/apps/miniprogram/miniprogram/pages/application-form/application-form.ts +++ b/apps/miniprogram/miniprogram/pages/application-form/application-form.ts @@ -9,7 +9,7 @@ import { APPLICATION_APPLY_MODES, SERVICE_CATEGORIES, SERVICE_CATEGORY_LABELS, -} from '@lehuo/contracts'; +} from '../../constants/business-options'; import { MiniProgramApiError } from '../../services/api-client'; import { @@ -20,6 +20,15 @@ import { createClientRequestId } from '../../utils/request-id'; const TOTAL_STEPS = 5; +const LOCAL_SERVICE_CATEGORIES: ServiceCategoryOption[] = [ + { 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: '由工作人员进一步了解并协助判断' }, +]; + function isServiceCategory(value: string): value is ServiceCategory { return (SERVICE_CATEGORIES as readonly string[]).includes(value); } @@ -35,7 +44,7 @@ Page({ loading: true, submitting: false, errorMessage: '', - categories: [] as ServiceCategoryOption[], + categories: LOCAL_SERVICE_CATEGORIES, modeOptions: APPLICATION_APPLY_MODES.map((code) => ({ code, label: APPLICATION_APPLY_MODE_LABELS[code], @@ -76,7 +85,11 @@ Page({ const result = await fetchServiceCategories(); this.setData({ categories: result.items, loading: false }); } catch (error) { - this.setData({ loading: false, errorMessage: this.errorText(error) }); + this.setData({ + loading: false, + categories: LOCAL_SERVICE_CATEGORIES, + errorMessage: `${this.errorText(error)};您仍可先填写申请,提交前请确认服务连接。`, + }); } }, diff --git a/apps/miniprogram/miniprogram/pages/assigned-case-detail/assigned-case-detail.ts b/apps/miniprogram/miniprogram/pages/assigned-case-detail/assigned-case-detail.ts index fd2e867..162fe9b 100644 --- a/apps/miniprogram/miniprogram/pages/assigned-case-detail/assigned-case-detail.ts +++ b/apps/miniprogram/miniprogram/pages/assigned-case-detail/assigned-case-detail.ts @@ -1,8 +1,6 @@ -import { - SERVICE_RECORD_TYPES, - SERVICE_RECORD_TYPE_LABELS, - type ServiceCaseDetail, -} from '@lehuo/contracts'; +import type { ServiceCaseDetail } from '@lehuo/contracts'; + +import { SERVICE_RECORD_TYPES, SERVICE_RECORD_TYPE_LABELS } from '../../constants/business-options'; import { MiniProgramApiError } from '../../services/api-client'; import { diff --git a/apps/miniprogram/miniprogram/pages/content-detail/content-detail.ts b/apps/miniprogram/miniprogram/pages/content-detail/content-detail.ts index 33a6c26..5728c4d 100644 --- a/apps/miniprogram/miniprogram/pages/content-detail/content-detail.ts +++ b/apps/miniprogram/miniprogram/pages/content-detail/content-detail.ts @@ -5,7 +5,7 @@ Page({ onLoad(o) { const id = o['publicId']; if (!id) { - this.setData({ loading: false, message: '内容参数无效' }); + this.setData({ loading: false, message: '缺少内容标识,请返回资讯列表重新选择。' }); return; } void this.load(id); diff --git a/apps/miniprogram/miniprogram/pages/content-detail/content-detail.wxml b/apps/miniprogram/miniprogram/pages/content-detail/content-detail.wxml index a465aeb..1a48037 100644 --- a/apps/miniprogram/miniprogram/pages/content-detail/content-detail.wxml +++ b/apps/miniprogram/miniprogram/pages/content-detail/content-detail.wxml @@ -1 +1 @@ -加载中…{{message}}
{{post.title}}{{post.summary}}{{post.content}}发布时间:{{post.publishedAt}}
+加载中…{{message}}{{post.title}}{{post.summary}}{{post.content}}发布时间:{{post.publishedAt}} diff --git a/apps/miniprogram/miniprogram/pages/profile/profile.wxml b/apps/miniprogram/miniprogram/pages/profile/profile.wxml index 84d6afa..ea63799 100644 --- a/apps/miniprogram/miniprogram/pages/profile/profile.wxml +++ b/apps/miniprogram/miniprogram/pages/profile/profile.wxml @@ -2,7 +2,7 @@ 暂时无法打开个人中心{{error}} - 乐{{user.displayName || '乐活用户'}}账号已安全登录{{isVolunteer ? '认证志愿者' : '公益伙伴'}}隐私保护最少采集服务可查进度留痕消息可达及时通知 + 乐{{user.displayName || '乐活用户'}}账号已安全登录{{isVolunteer ? '认证志愿者' : '公益伙伴'}}隐私保护最少采集服务可查进度留痕消息可达及时通知 我的服务 diff --git a/apps/miniprogram/miniprogram/pages/profile/profile.wxss b/apps/miniprogram/miniprogram/pages/profile/profile.wxss index e4df36c..7c0595f 100644 --- a/apps/miniprogram/miniprogram/pages/profile/profile.wxss +++ b/apps/miniprogram/miniprogram/pages/profile/profile.wxss @@ -30,11 +30,11 @@ .profile-trust view:last-child { border-right: 0; } -.profile-trust strong, +.profile-trust__title, .profile-trust text { display: block; } -.profile-trust strong { +.profile-trust__title { font-size: 25rpx; } .profile-trust text { diff --git a/apps/miniprogram/miniprogram/pages/project-detail/project-detail.ts b/apps/miniprogram/miniprogram/pages/project-detail/project-detail.ts index 34ef41f..fee29a3 100644 --- a/apps/miniprogram/miniprogram/pages/project-detail/project-detail.ts +++ b/apps/miniprogram/miniprogram/pages/project-detail/project-detail.ts @@ -3,7 +3,9 @@ import { fetchPublicProject } from '../../services/participation'; Page({ data: { loading: true, message: '', project: null as ProjectDetail | null }, onLoad(options: Record) { - if (options['publicId']) void this.load(options['publicId']); + const publicId = options['publicId']; + if (publicId) void this.load(publicId); + else this.setData({ loading: false, message: '缺少项目标识,请返回项目列表重新选择。' }); }, async load(publicId: string) { try { diff --git a/apps/miniprogram/miniprogram/pages/project-detail/project-detail.wxml b/apps/miniprogram/miniprogram/pages/project-detail/project-detail.wxml index cd8d891..8f047e1 100644 --- a/apps/miniprogram/miniprogram/pages/project-detail/project-detail.wxml +++ b/apps/miniprogram/miniprogram/pages/project-detail/project-detail.wxml @@ -1 +1 @@ -项目暂时无法查看{{message}}{{project.name}}{{project.statusLabel}}{{project.summary}}服务对象{{project.targetPeople || '以项目说明为准'}}活动数量{{project.activityCount}}服务工单{{project.serviceCaseCount}}项目周期{{project.startDate}} 至 {{project.endDate}}项目介绍{{project.content}}参与说明查看项目关联活动,选择适合的时间报名参与;提交后可在志愿者中心查询审核与签到状态。 +项目暂时无法查看{{message}}{{project.name}}{{project.statusLabel}}{{project.summary}}服务对象{{project.targetPeople === null ? '以项目说明为准' : project.targetPeople}}活动数量{{project.activityCount}}服务工单{{project.serviceCaseCount}}项目周期{{project.startDate}} 至 {{project.endDate}}项目介绍{{project.content}}参与说明查看项目关联活动,选择适合的时间报名参与;提交后可在志愿者中心查询审核与签到状态。 diff --git a/apps/miniprogram/miniprogram/pages/project-detail/project-detail.wxss b/apps/miniprogram/miniprogram/pages/project-detail/project-detail.wxss index 7684058..a2395ec 100644 --- a/apps/miniprogram/miniprogram/pages/project-detail/project-detail.wxss +++ b/apps/miniprogram/miniprogram/pages/project-detail/project-detail.wxss @@ -111,14 +111,14 @@ border-right: 0; } .project-facts text, -.project-facts strong { +.project-fact-value { display: block; } .project-facts text { color: var(--color-text-secondary); font-size: 22rpx; } -.project-facts strong { +.project-fact-value { margin-top: 6rpx; color: var(--color-primary); font-size: 28rpx; diff --git a/apps/miniprogram/miniprogram/services/api-client.ts b/apps/miniprogram/miniprogram/services/api-client.ts index 90f4760..0565425 100644 --- a/apps/miniprogram/miniprogram/services/api-client.ts +++ b/apps/miniprogram/miniprogram/services/api-client.ts @@ -2,7 +2,10 @@ import type { ApiErrorResponse, ApiSuccessResponse } from '@lehuo/contracts'; import { readSession, removeSession } from './session'; -const API_BASE_URL = getApp().globalData.apiBaseUrl; +function resolveApiBaseUrl(): string { + const stored = String(wx.getStorageSync('lehuo_api_base_url') || '').trim(); + return stored || getApp().globalData.apiBaseUrl.trim(); +} type ApiRequestOptions = Omit< WechatMiniprogram.RequestOption, @@ -20,7 +23,8 @@ export class MiniProgramApiError extends Error { } export function apiRequest(path: string, options: ApiRequestOptions = {}): Promise { - if (!API_BASE_URL) { + const apiBaseUrl = resolveApiBaseUrl(); + if (!apiBaseUrl) { return Promise.reject( new MiniProgramApiError( '小程序尚未配置服务地址,请联系工作人员', @@ -33,7 +37,7 @@ export function apiRequest(path: string, options: ApiRequestOptions = {}): Pr const session = readSession(); wx.request | ApiErrorResponse>({ ...options, - url: `${API_BASE_URL}${path}`, + url: `${apiBaseUrl}${path}`, header: { 'content-type': 'application/json', ...(session ? { authorization: `Bearer ${session.accessToken}` } : {}), diff --git a/apps/miniprogram/package.json b/apps/miniprogram/package.json index 27c93ca..876b0fd 100644 --- a/apps/miniprogram/package.json +++ b/apps/miniprogram/package.json @@ -6,9 +6,11 @@ "@lehuo/contracts": "workspace:*" }, "scripts": { - "build": "tsc --noEmit", + "build": "pnpm check:pages && pnpm check:runtime-imports && tsc --noEmit", + "check:pages": "node scripts/check-pages.mjs", + "check:runtime-imports": "node scripts/check-runtime-imports.mjs", "test:unit": "vitest run", - "typecheck": "tsc --noEmit" + "typecheck": "pnpm check:pages && pnpm check:runtime-imports && tsc --noEmit" }, "devDependencies": { "@types/wechat-miniprogram": "^3.4.8", diff --git a/apps/miniprogram/scripts/check-pages.mjs b/apps/miniprogram/scripts/check-pages.mjs new file mode 100644 index 0000000..aa8d00e --- /dev/null +++ b/apps/miniprogram/scripts/check-pages.mjs @@ -0,0 +1,47 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; + +const projectRoot = resolve(import.meta.dirname, '..'); +const miniRoot = join(projectRoot, 'miniprogram'); +const app = JSON.parse(readFileSync(join(miniRoot, 'app.json'), 'utf8')); +const allowedTags = new Set([ + 'block', + 'button', + 'checkbox', + 'checkbox-group', + 'image', + 'input', + 'label', + 'picker', + 'switch', + 'text', + 'textarea', + 'view', +]); +const failures = []; + +for (const page of app.pages) { + const base = join(miniRoot, page); + for (const extension of ['.ts', '.json', '.wxml', '.wxss']) { + if (!existsSync(`${base}${extension}`)) failures.push(`${page}${extension} 缺失`); + } + const wxml = readFileSync(`${base}.wxml`, 'utf8'); + if (!wxml.trim()) failures.push(`${page}.wxml 为空`); + for (const match of wxml.matchAll(/<\/?([A-Za-z][A-Za-z0-9-]*)/g)) { + if (!allowedTags.has(match[1])) failures.push(`${page}.wxml 使用不支持标签 <${match[1]}>`); + } + const source = readFileSync(`${base}.ts`, 'utf8'); + for (const match of source.matchAll( + /wx\.(?:navigateTo|redirectTo)\(\{\s*url:\s*[`'"]([^`'"]+)/g, + )) { + const target = match[1].split('?')[0].replace(/^\//, ''); + if (target.includes('${')) continue; + if (!app.pages.includes(target)) failures.push(`${page}.ts 路由未注册:${target}`); + } +} + +if (failures.length) { + console.error(failures.join('\n')); + process.exit(1); +} +console.log(`小程序页面完整性检查通过:${app.pages.length} 个页面`); diff --git a/apps/miniprogram/scripts/check-runtime-imports.mjs b/apps/miniprogram/scripts/check-runtime-imports.mjs new file mode 100644 index 0000000..005c660 --- /dev/null +++ b/apps/miniprogram/scripts/check-runtime-imports.mjs @@ -0,0 +1,29 @@ +import { readFileSync, readdirSync, statSync } from 'node:fs'; +import { join, relative } from 'node:path'; + +const root = new URL('../miniprogram/', import.meta.url); +const sourceRoot = decodeURIComponent(root.pathname).replace(/^\/(?:([A-Za-z]:))/, '$1'); +const failures = []; + +function walk(directory) { + for (const name of readdirSync(directory)) { + const path = join(directory, name); + if (statSync(path).isDirectory()) walk(path); + else if (path.endsWith('.ts') && !path.endsWith('.spec.ts')) inspect(path); + } +} + +function inspect(path) { + const source = readFileSync(path, 'utf8'); + const imports = source.matchAll( + /import\s+(?!type\b)[\s\S]*?from\s+['"](@lehuo\/contracts)['"];?/g, + ); + for (const match of imports) failures.push(relative(sourceRoot, path)); +} + +walk(sourceRoot); +if (failures.length) { + console.error(`小程序存在未打包的运行时 workspace 依赖:\n${[...new Set(failures)].join('\n')}`); + process.exit(1); +} +console.log('小程序运行时依赖检查通过'); diff --git a/docs/10_TASK_STATE.md b/docs/10_TASK_STATE.md index 0115612..d190ef7 100644 --- a/docs/10_TASK_STATE.md +++ b/docs/10_TASK_STATE.md @@ -6,6 +6,20 @@ 2026-08-13 +任务:微信真机二级页面白屏修复。 + +根因:服务申请和协作任务详情/服务记录页面在运行时从 workspace 包 `@lehuo/contracts` 导入业务常量。现有小程序工程没有生成 `miniprogram_npm`,常规 TypeScript 检查会擦除类型导入并通过,但真机无法解析运行时 workspace 模块,导致页面脚本在 `Page()` 注册前终止并显示空白。另发现内容详情使用非小程序原生 `article` 标签、项目详情使用 `strong` 标签,以及详情页缺少路由参数时可能持续处于加载状态。 + +完成:将申请方式、服务分类和服务记录类型的真机运行时字典迁入小程序本地模块,继续用共享契约类型做编译约束;API 地址改为按请求动态读取,并安全兼容空存储值;服务申请在服务分类接口暂不可用时展示本地批准分类与明确连接提示,不再出现无内容页面;项目、活动和内容详情补充缺少参数的恢复状态;替换不受支持的 WXML 标签。新增页面完整性门禁和运行时 workspace 导入门禁,校验 18 个页面四件套、静态路由注册、WXML 标签和真机运行依赖,纳入小程序 typecheck/build。 + +范围:未新增模拟业务数据、未绕过认证/权限/审计;本地服务分类仅用于表单可见与选择,最终提交仍调用真实 API。服务对象档案和服务记录继续只对获得 `assigned_cases:read` 权限且被指派的人员展示最小必要数据。 + +验证:格式检查、Lint、全仓类型、27 个单元测试、34 个真实 MySQL 集成测试和全仓生产构建通过;新增页面完整性门禁通过 18 个页面,运行时 workspace 导入门禁通过。8 个 migration 在隔离 `lehuo_test` 库无待执行项。仍需用户在微信开发者工具执行“清缓存并重新编译”,再进行真机二级页面复验。 + +--- + +2026-08-13 + 任务:参考图小程序端与管理端视觉全量还原(延期能力仅预留)。 完成:建立 primitive→semantic→component 三层设计令牌;按参考图重建小程序品牌首页、八宫格服务入口、公告、项目图文卡、正式底部导航、图文服务申请、项目详情事实区和绿色个人中心;保留既有真实申请、项目、活动、志愿报名、协作任务、消息和内容业务流程。管理端新增深色图标侧栏、工作概览、真实 KPI、月度申请/完成趋势、服务类型分布、快捷入口和工单横向状态步骤条,并沿用既有脱敏表格、筛选、详情、权限和审计逻辑。新增四张虚构公益品牌场景素材并压缩用于本地包,不作为真实成果证据。 diff --git a/eslint.config.mjs b/eslint.config.mjs index 49410a9..34f2574 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -9,6 +9,7 @@ export default tseslint.config( '**/node_modules/**', '**/miniprogram_npm/**', '**/generated/prisma/**', + 'apps/miniprogram/scripts/*.mjs', 'coverage/**', 'eslint.config.mjs', ],