diff --git a/miniapp/components/preference-editor/index.js b/miniapp/components/preference-editor/index.js
index ce21fc4..ee8a038 100644
--- a/miniapp/components/preference-editor/index.js
+++ b/miniapp/components/preference-editor/index.js
@@ -8,6 +8,8 @@
* 不变量(AGENTS.md 第 2 条):用户已勾选"必须符合"的字段不可被前端自动放宽为
* "最好符合",UI 必须显式提示用户此操作将影响匹配精度。
*/
+const { ACCEPT_CHILDREN_OPTIONS } = require('../../constants')
+
Component({
options: { addGlobalClass: true },
properties: {
@@ -19,6 +21,24 @@ Component({
required: '必须符合',
preferred: '最好符合',
},
+ // 子女(是否接受有孩)下拉:选项均为中文,value 为后端枚举码
+ childrenOptions: ACCEPT_CHILDREN_OPTIONS,
+ childrenLabelMap: {
+ any: '不限',
+ accept: '可接受',
+ reject: '不接受',
+ },
+ childrenIndex: -1,
+ },
+ observers: {
+ preference(pref) {
+ if (!pref) {
+ this.setData({ childrenIndex: -1 })
+ return
+ }
+ const idx = this.data.childrenOptions.findIndex((o) => o.value === pref.acceptChildren)
+ this.setData({ childrenIndex: idx })
+ },
},
methods: {
// 切换某字段的 importance:required ↔ preferred
@@ -54,9 +74,13 @@ Component({
},
onAcceptChildrenChange(e) {
if (this.data.readonly) return;
+ const idx = Number(e.detail.value)
+ const opt = this.data.childrenOptions[idx]
+ if (!opt) return
+ this.setData({ childrenIndex: idx })
this.triggerEvent('change', {
field: 'acceptChildren',
- value: e.detail.value,
+ value: opt.value,
});
},
},
diff --git a/miniapp/components/preference-editor/index.wxml b/miniapp/components/preference-editor/index.wxml
index be20aef..35cf774 100644
--- a/miniapp/components/preference-editor/index.wxml
+++ b/miniapp/components/preference-editor/index.wxml
@@ -111,13 +111,13 @@
子女
- {{preference.acceptChildren === 'any' ? '不限' : preference.acceptChildren === 'accept' ? '可接受' : '不接受'}}
+ {{childrenLabelMap[preference.acceptChildren] || '不限'}}
diff --git a/miniapp/components/recommend-card/index.js b/miniapp/components/recommend-card/index.js
index 2905f81..c9bfbb9 100644
--- a/miniapp/components/recommend-card/index.js
+++ b/miniapp/components/recommend-card/index.js
@@ -7,7 +7,7 @@
* 不暴露"放宽了 X 条件才得到这个推荐"的字样,前端只显示后端返回的 reasons。
* - 第 5 条:红娘不可替用户操作——本卡片仅由当前用户操作,不接受 matchmakerId 参数。
*/
-const { STATUS_BADGE } = require('../../constants');
+const { STATUS_BADGE, profileLabelOf } = require('../../constants');
Component({
options: { addGlobalClass: true },
@@ -29,6 +29,21 @@ Component({
medium: 'info',
normal: 'warning',
},
+ // 由后端枚举码派生的中文展示值(界面不得出现英文码)
+ educationLabel: '',
+ occupationLabel: '',
+ },
+ observers: {
+ recommendation(rec) {
+ if (!rec) {
+ this.setData({ educationLabel: '', occupationLabel: '' })
+ return
+ }
+ this.setData({
+ educationLabel: profileLabelOf('educationCode', rec.educationCode),
+ occupationLabel: profileLabelOf('occupationCode', rec.occupationCode),
+ })
+ },
},
methods: {
onViewDetail() {
diff --git a/miniapp/components/recommend-card/index.wxml b/miniapp/components/recommend-card/index.wxml
index c836231..ab6b7a6 100644
--- a/miniapp/components/recommend-card/index.wxml
+++ b/miniapp/components/recommend-card/index.wxml
@@ -2,8 +2,8 @@
{{recommendation.displayName || '善缘用户'}}{{recommendation.age}}岁在线
- {{recommendation.regionName}} · {{recommendation.heightCm}}cm · {{recommendation.educationCode}}
- {{recommendation.occupationCode}}
+ {{recommendation.regionName}} · {{recommendation.heightCm}}cm · {{educationLabel}}
+ {{occupationLabel}}
{{item}}
diff --git a/miniapp/components/user-card/index.js b/miniapp/components/user-card/index.js
index bceb654..75b3373 100644
--- a/miniapp/components/user-card/index.js
+++ b/miniapp/components/user-card/index.js
@@ -7,6 +7,8 @@
* …自定义按钮…
*
*/
+const { profileLabelOf } = require('../../constants')
+
Component({
options: { addGlobalClass: true, multipleSlots: true },
properties: {
@@ -15,6 +17,25 @@ Component({
selectable: { type: Boolean, value: false }, // 是否显示选择框
selected: { type: Boolean, value: false },
},
+ data: {
+ // 由后端枚举码派生的中文展示值(界面不得出现英文码)
+ educationLabel: '',
+ occupationLabel: '',
+ maritalLabel: '',
+ },
+ observers: {
+ user(u) {
+ if (!u) {
+ this.setData({ educationLabel: '', occupationLabel: '', maritalLabel: '' })
+ return
+ }
+ this.setData({
+ educationLabel: profileLabelOf('educationCode', u.educationCode),
+ occupationLabel: profileLabelOf('occupationCode', u.occupationCode),
+ maritalLabel: profileLabelOf('maritalStatus', u.maritalStatus),
+ })
+ },
+ },
methods: {
onTap() {
if (!this.data.user) return;
diff --git a/miniapp/components/user-card/index.wxml b/miniapp/components/user-card/index.wxml
index ec79bb6..2dac148 100644
--- a/miniapp/components/user-card/index.wxml
+++ b/miniapp/components/user-card/index.wxml
@@ -19,16 +19,16 @@
{{user.heightCm}}cm ·
{{user.regionName}}
- · {{user.educationCode}}
+ · {{educationLabel}}
-
- {{user.occupationCode}}
+
+ {{occupationLabel}}
{{item}}
-
- {{user.maritalStatus}}
+
+ {{maritalLabel}}
· 有孩
diff --git a/miniapp/constants/index.js b/miniapp/constants/index.js
index f08aaac..50ad511 100644
--- a/miniapp/constants/index.js
+++ b/miniapp/constants/index.js
@@ -10,6 +10,7 @@ const statusEnums = require('./statusEnums');
const permissionCodes = require('./permissionCodes');
const storageKeys = require('./storageKeys');
const businessLimits = require('./businessLimits');
+const profileLabels = require('./profileLabels');
module.exports = {
// 路由
@@ -60,4 +61,15 @@ module.exports = {
// 业务限制
BUSINESS_LIMITS: businessLimits.BUSINESS_LIMITS,
mergeAppConfig: businessLimits.mergeAppConfig,
+
+ // 资料/择偶枚举码 → 中文简体字典(界面不得出现英文枚举码)
+ PROFILE_LABELS: profileLabels,
+ profileLabelOf: profileLabels.labelOf,
+ yesNoLabel: profileLabels.yesNoLabel,
+ futureDanyangLabel: profileLabels.futureDanyangLabel,
+ MATCHMAKER_SERVICE_STATUS_LABEL: profileLabels.MATCHMAKER_SERVICE_STATUS_LABEL,
+ ACCEPT_CHILDREN_OPTIONS: profileLabels.ACCEPT_CHILDREN_OPTIONS,
+ MARITAL_OPTIONS: profileLabels.MARITAL_OPTIONS,
+ EDUCATION_OPTIONS: profileLabels.EDUCATION_OPTIONS,
+ OCCUPATION_OPTIONS: profileLabels.OCCUPATION_OPTIONS,
};
diff --git a/miniapp/constants/profileLabels.js b/miniapp/constants/profileLabels.js
new file mode 100644
index 0000000..48304ee
--- /dev/null
+++ b/miniapp/constants/profileLabels.js
@@ -0,0 +1,189 @@
+/**
+ * 资料 / 择偶条件枚举码 → 中文简体 唯一字典
+ * ============================================================
+ * 铁律:界面上不允许出现任何英文枚举码(如 never_married / bachelor /
+ * government / any / accept / reject)。所有会展示给用户的状态码、选项码
+ * 都必须在本文件里有中文 label。
+ *
+ * 为什么要集中:
+ * - 小程序里最典型的缺陷是「后端枚举码直接当展示值」上屏(例如某卡片
+ * 直接渲染 educationCode=bachelor)。静态类型检查 / WXSS 编译 / 结构
+ * 守卫都发现不了,只有人眼在真机才看得见。
+ * - 后端 demo 种子与实际表单存的码存在历史不一致(表单用 occasional,
+ * 种子用 sometimes;表单用 yes/no,种子用 是/否)。labelOf 统一覆盖
+ * 两套并集,保证详情页无论后端返回哪种都显示中文。
+ *
+ * labelOf(group, code):
+ * - 空值返回 ''
+ * - 已知码返回中文
+ * - 未知码返回原值(便于排查;生产路径不应触发,否则说明字典需补)
+ */
+
+// ============ 婚姻状况 ============
+const MARITAL = {
+ never_married: '未婚',
+ divorced: '离异',
+ widowed: '丧偶',
+ single: '未婚', // profile-confirm 旧别名
+}
+
+// ============ 子女情况 ============
+const CHILDREN_STATUS = {
+ none: '无子女',
+ has: '有子女',
+}
+
+// ============ 学历 ============
+// 覆盖表单值(high_school) 与种子实际值(senior_high/junior_high) 及别名
+const EDUCATION = {
+ high_school: '高中/中专',
+ senior_high: '高中/中专',
+ junior_high: '初中',
+ college: '大专',
+ bachelor: '本科',
+ master: '硕士',
+ phd: '博士',
+ doctor: '博士', // profile-confirm 旧别名
+}
+
+// ============ 职业 ============
+// 表单类目(生产路径)+ demo 种子实际值(自由英文词)
+const OCCUPATION = {
+ government: '机关事业单位',
+ education: '教育',
+ medical: '医疗',
+ enterprise: '企业职员',
+ technical: '技术人员',
+ self_employed: '个体经营',
+ freelance: '自由职业',
+ service: '服务行业',
+ agriculture: '农业',
+ other: '其他',
+ // demo 种子实际值
+ clerical: '行政文职',
+ designer: '设计',
+ factory: '工厂生产',
+ nurse: '医护',
+ engineer: '工程师',
+ programmer: '程序员',
+ sales: '销售',
+ doctor: '医生',
+ researcher: '科研',
+ teacher: '教师',
+}
+
+// ============ 择偶条件:是否接受有孩(preference-editor 子女下拉) ============
+const ACCEPT_CHILDREN = {
+ any: '不限',
+ accept: '可接受',
+ reject: '不接受',
+}
+
+// ============ 吸烟 / 饮酒 ============
+// 覆盖表单值(occasional) 与种子实际值(sometimes)
+const SMOKING = {
+ never: '不吸烟',
+ occasional: '偶尔吸烟',
+ sometimes: '偶尔吸烟',
+ often: '经常吸烟',
+}
+const DRINKING = {
+ never: '不喝酒',
+ occasional: '偶尔喝酒',
+ sometimes: '偶尔喝酒',
+ often: '经常喝酒',
+}
+
+// ============ 红娘服务状态(matchmaker 历史服务) ============
+const MATCHMAKER_SERVICE_STATUS_LABEL = {
+ submitted: '已提交',
+ assigned: '已分配',
+ in_service: '服务中',
+ waiting_user: '待您确认',
+ completed: '已完成',
+ cancelled: '已取消',
+}
+
+const GROUPS = {
+ maritalStatus: MARITAL,
+ childrenStatus: CHILDREN_STATUS,
+ educationCode: EDUCATION,
+ occupationCode: OCCUPATION,
+ acceptChildren: ACCEPT_CHILDREN,
+ smokingStatus: SMOKING,
+ drinkingStatus: DRINKING,
+}
+
+/**
+ * 通用映射:已知码→中文,未知→原值
+ * @param {string} group GROUPS 的键
+ * @param {*} code 后端返回的原始码
+ * @returns {string}
+ */
+function labelOf(group, code) {
+ if (code === undefined || code === null || code === '') return ''
+ const map = GROUPS[group]
+ if (!map) return String(code)
+ return map[code] !== undefined ? map[code] : String(code)
+}
+
+/** 是/否类(长期居住丹阳等):兼容布尔、yes/no、是/否 三种来源 */
+function yesNoLabel(v) {
+ if (v === true) return '是'
+ if (v === false) return '否'
+ if (v === 'yes') return '是'
+ if (v === 'no') return '否'
+ if (v === '是' || v === '否') return v
+ return v === undefined || v === null || v === '' ? '' : String(v)
+}
+
+/** 未来规划(未来是否愿意继续在丹阳):willing/uncertain/unwilling 或 是/否 */
+function futureDanyangLabel(v) {
+ if (v === 'willing' || v === '是') return '愿意'
+ if (v === 'uncertain') return '不确定'
+ if (v === 'unwilling' || v === '否') return '不愿意'
+ return v === undefined || v === null || v === '' ? '' : String(v)
+}
+
+// ============ picker 选项数组(value/label 形式,供 wxml range + range-key 使用) ============
+const ACCEPT_CHILDREN_OPTIONS = [
+ { value: 'any', label: '不限' },
+ { value: 'accept', label: '可接受' },
+ { value: 'reject', label: '不接受' },
+]
+
+const MARITAL_OPTIONS = [
+ { value: 'never_married', label: '未婚' },
+ { value: 'divorced', label: '离异' },
+ { value: 'widowed', label: '丧偶' },
+]
+
+const EDUCATION_OPTIONS = [
+ { value: 'senior_high', label: '高中/中专' },
+ { value: 'junior_high', label: '初中' },
+ { value: 'college', label: '大专' },
+ { value: 'bachelor', label: '本科' },
+ { value: 'master', label: '硕士' },
+ { value: 'phd', label: '博士' },
+]
+
+const OCCUPATION_OPTIONS = Object.keys(OCCUPATION).map((v) => ({ value: v, label: OCCUPATION[v] }))
+
+module.exports = {
+ MARITAL,
+ CHILDREN_STATUS,
+ EDUCATION,
+ OCCUPATION,
+ ACCEPT_CHILDREN,
+ SMOKING,
+ DRINKING,
+ MATCHMAKER_SERVICE_STATUS_LABEL,
+ GROUPS,
+ labelOf,
+ yesNoLabel,
+ futureDanyangLabel,
+ ACCEPT_CHILDREN_OPTIONS,
+ MARITAL_OPTIONS,
+ EDUCATION_OPTIONS,
+ OCCUPATION_OPTIONS,
+}
diff --git a/miniapp/pages/about/about.json b/miniapp/pages/about/about.json
index bdc9a79..0ff2c6f 100644
--- a/miniapp/pages/about/about.json
+++ b/miniapp/pages/about/about.json
@@ -1 +1 @@
-{"navigationBarTitleText":"about","usingComponents":{}}
+{"navigationBarTitleText":"关于我们","usingComponents":{}}
diff --git a/miniapp/pages/activity-mutual-waiting/activity-mutual-waiting.wxml b/miniapp/pages/activity-mutual-waiting/activity-mutual-waiting.wxml
index 3146018..3657a8c 100644
--- a/miniapp/pages/activity-mutual-waiting/activity-mutual-waiting.wxml
+++ b/miniapp/pages/activity-mutual-waiting/activity-mutual-waiting.wxml
@@ -20,7 +20,7 @@
💬
- 向Ta表达兴趣
+ 向对方表达兴趣
双方确认后才能开启线上沟通
diff --git a/miniapp/pages/activity-register/activity-register.json b/miniapp/pages/activity-register/activity-register.json
index 63e93a1..3d87183 100644
--- a/miniapp/pages/activity-register/activity-register.json
+++ b/miniapp/pages/activity-register/activity-register.json
@@ -1 +1 @@
-{"navigationBarTitleText":"activity-register","usingComponents":{}}
+{"navigationBarTitleText":"活动报名","usingComponents":{}}
diff --git a/miniapp/pages/activity-register/activity-register.wxml b/miniapp/pages/activity-register/activity-register.wxml
index 6e90da9..0f2ad7f 100644
--- a/miniapp/pages/activity-register/activity-register.wxml
+++ b/miniapp/pages/activity-register/activity-register.wxml
@@ -1 +1 @@
-页面: activity-register
+活动报名(建设中)
diff --git a/miniapp/pages/bind-phone/bind-phone.json b/miniapp/pages/bind-phone/bind-phone.json
index 65abc13..3fd1e80 100644
--- a/miniapp/pages/bind-phone/bind-phone.json
+++ b/miniapp/pages/bind-phone/bind-phone.json
@@ -1 +1 @@
-{"navigationBarTitleText":"bind-phone","usingComponents":{}}
+{"navigationBarTitleText":"绑定手机","usingComponents":{}}
diff --git a/miniapp/pages/bind-phone/bind-phone.wxml b/miniapp/pages/bind-phone/bind-phone.wxml
index b55ec3c..54b5783 100644
--- a/miniapp/pages/bind-phone/bind-phone.wxml
+++ b/miniapp/pages/bind-phone/bind-phone.wxml
@@ -1 +1 @@
-bind-phone
+手机号绑定(建设中)
diff --git a/miniapp/pages/chat/chat.ts b/miniapp/pages/chat/chat.ts
index aa2d791..c586d6b 100644
--- a/miniapp/pages/chat/chat.ts
+++ b/miniapp/pages/chat/chat.ts
@@ -242,7 +242,7 @@ Page({
const { otherUserId } = this.data;
if (!otherUserId) { appStore.toast('暂无更多信息'); return; }
wx.showActionSheet({
- itemList: ['查看 Ta 的资料', '举报 Ta', '拉黑 Ta'],
+ itemList: ['查看对方的资料', '举报对方', '拉黑对方'],
success: (res) => {
if (res.tapIndex === 0) wx.navigateTo({ url: to.detail(otherUserId) });
else if (res.tapIndex === 1) wx.navigateTo({ url: to.report(otherUserId) });
diff --git a/miniapp/pages/discover/discover.wxml b/miniapp/pages/discover/discover.wxml
index 03dcb46..5ae67b3 100644
--- a/miniapp/pages/discover/discover.wxml
+++ b/miniapp/pages/discover/discover.wxml
@@ -107,7 +107,7 @@
暂无符合条件的人选
- Required 条件不会自动放宽,可以调整筛选后再看看
+ 必须符合的条件不会自动放宽,可以调整筛选后再看看
清空筛选
diff --git a/miniapp/pages/fate-center/fate-center.wxml b/miniapp/pages/fate-center/fate-center.wxml
index c385a68..a5e78f3 100644
--- a/miniapp/pages/fate-center/fate-center.wxml
+++ b/miniapp/pages/fate-center/fate-center.wxml
@@ -85,7 +85,7 @@
catchtap="onLikeBack"
data-user-id="{{item.userId}}"
disabled="{{actionLoadingUserId === item.userId}}"
- >喜欢TA
+ >喜欢对方
diff --git a/miniapp/pages/help-center/help-center.json b/miniapp/pages/help-center/help-center.json
index c192ebf..ff86c22 100644
--- a/miniapp/pages/help-center/help-center.json
+++ b/miniapp/pages/help-center/help-center.json
@@ -1 +1 @@
-{"navigationBarTitleText":"help-center","usingComponents":{}}
+{"navigationBarTitleText":"帮助中心","usingComponents":{}}
diff --git a/miniapp/pages/index/index.wxml b/miniapp/pages/index/index.wxml
index 305cf9c..c1a4541 100644
--- a/miniapp/pages/index/index.wxml
+++ b/miniapp/pages/index/index.wxml
@@ -72,7 +72,7 @@
- 暂时没有完全符合双方条件的人我们坚持 Required 条件,不会为了数量降低筛选标准
+ 暂时没有完全符合双方条件的人我们坚持必须符合的条件,不会为了数量降低筛选标准
调整条件公益红娘
diff --git a/miniapp/pages/login/login.json b/miniapp/pages/login/login.json
index 55423d3..be8d3ad 100644
--- a/miniapp/pages/login/login.json
+++ b/miniapp/pages/login/login.json
@@ -1 +1 @@
-{"navigationBarTitleText":"login","usingComponents":{}}
+{"navigationBarTitleText":"登录","usingComponents":{}}
diff --git a/miniapp/pages/matchmaker/matchmaker.ts b/miniapp/pages/matchmaker/matchmaker.ts
index 8fe5210..43b211f 100644
--- a/miniapp/pages/matchmaker/matchmaker.ts
+++ b/miniapp/pages/matchmaker/matchmaker.ts
@@ -8,7 +8,7 @@
* - 推荐:红娘推荐列表跳 fate-center 页(AGENTS 第 5 条:红娘不可替用户操作)
*/
import { matchmakerStore, appStore } from '../../stores';
-import { ROUTE, to, buildPath, MATCHMAKER_SERVICE_STATUS } from '../../constants';
+import { ROUTE, to, buildPath, MATCHMAKER_SERVICE_STATUS, MATCHMAKER_SERVICE_STATUS_LABEL } from '../../constants';
Page({
data: {
@@ -59,10 +59,17 @@ Page({
activeService.status === MATCHMAKER_SERVICE_STATUS.IN_SERVICE ||
activeService.status === MATCHMAKER_SERVICE_STATUS.WAITING_USER
);
- const historyServices = activeService
+ const withStatusText = (s: any) => ({
+ ...s,
+ statusText: MATCHMAKER_SERVICE_STATUS_LABEL[s.status] || s.statusText || s.status || '',
+ })
+ const historyServices = (activeService
? myServices.filter((s: any) => s.serviceId !== activeService.serviceId)
- : myServices;
- this.setData({ loading: false, firstLoaded: true, activeService, historyServices, showRecommendations });
+ : myServices).map(withStatusText)
+ const activeServiceWithText = activeService
+ ? { ...activeService, statusText: MATCHMAKER_SERVICE_STATUS_LABEL[activeService.status] || activeService.statusText || activeService.status || '' }
+ : null
+ this.setData({ loading: false, firstLoaded: true, activeService: activeServiceWithText, historyServices, showRecommendations });
} catch (err: any) {
this.setData({ loading: false, firstLoaded: true });
appStore.toastError(err, '加载红娘服务失败');
diff --git a/miniapp/pages/my-activities/my-activities.json b/miniapp/pages/my-activities/my-activities.json
index d41c4cd..feebc7a 100644
--- a/miniapp/pages/my-activities/my-activities.json
+++ b/miniapp/pages/my-activities/my-activities.json
@@ -1 +1 @@
-{"navigationBarTitleText":"my-activities","usingComponents":{}}
+{"navigationBarTitleText":"我的活动","usingComponents":{}}
diff --git a/miniapp/pages/my-activities/my-activities.wxml b/miniapp/pages/my-activities/my-activities.wxml
index e0489cb..34c9d32 100644
--- a/miniapp/pages/my-activities/my-activities.wxml
+++ b/miniapp/pages/my-activities/my-activities.wxml
@@ -1 +1 @@
-页面: my-activities
+我的活动(建设中)
diff --git a/miniapp/pages/profile-confirm/profile-confirm.js b/miniapp/pages/profile-confirm/profile-confirm.js
index b6d2518..c075de1 100644
--- a/miniapp/pages/profile-confirm/profile-confirm.js
+++ b/miniapp/pages/profile-confirm/profile-confirm.js
@@ -12,7 +12,7 @@
*/
const { userStore, appStore } = require('../../stores');
const { profileService } = require('../../services');
-const { ROUTE, STORAGE_KEYS, PROFILE_REVIEW } = require('../../constants');
+const { ROUTE, STORAGE_KEYS, PROFILE_REVIEW, profileLabelOf } = require('../../constants');
// 审核状态 → StatusBadge kind 映射
const REVIEW_BADGE = {
@@ -23,28 +23,6 @@ const REVIEW_BADGE = {
[PROFILE_REVIEW.REJECTED]: { label: '未通过', kind: 'danger' },
};
-// 婚姻状况字典(前端展示用,权威值仍由服务端 Profile.maritalStatus 给出)
-const MARITAL_LABEL = {
- single: '未婚',
- divorced: '离异',
- widowed: '丧偶',
-};
-
-// 学历字典
-const EDUCATION_LABEL = {
- high_school: '高中/中专',
- college: '大专',
- bachelor: '本科',
- master: '硕士',
- doctor: '博士',
-};
-
-// 子女情况字典
-const CHILDREN_LABEL = {
- none: '无子女',
- has: '有子女',
-};
-
/**
* 计算资料完整度(客户端派生展示)
*
@@ -165,9 +143,9 @@ Page({
resubmitAllowed,
completion,
reviewBadge: REVIEW_BADGE[reviewStatus] || REVIEW_BADGE[PROFILE_REVIEW.NOT_SUBMITTED],
- maritalLabel: MARITAL_LABEL[profile && profile.maritalStatus] || (profile && profile.maritalStatus) || '-',
- educationLabel: EDUCATION_LABEL[profile && profile.educationCode] || (profile && profile.educationCode) || '-',
- childrenLabel: CHILDREN_LABEL[profile && (profile.hasChildren ? 'has' : 'none')] || '-',
+ maritalLabel: profileLabelOf('maritalStatus', profile && profile.maritalStatus) || '-',
+ educationLabel: profileLabelOf('educationCode', profile && profile.educationCode) || '-',
+ childrenLabel: profile && (profile.hasChildren ? '有子女' : '无子女'),
});
} catch (err) {
appStore.toastError(err, '加载失败');
diff --git a/miniapp/pages/profile-detail/profile-detail.js b/miniapp/pages/profile-detail/profile-detail.js
index 18eb470..a93dd2d 100644
--- a/miniapp/pages/profile-detail/profile-detail.js
+++ b/miniapp/pages/profile-detail/profile-detail.js
@@ -11,7 +11,7 @@
*/
const { matchStore, appStore, blockUser } = require('../../stores');
const { recommendationService } = require('../../services');
-const { to, DANGER_ACTION, RELATION_STATUS, BUSINESS_ERROR_CODE } = require('../../constants');
+const { to, DANGER_ACTION, RELATION_STATUS, BUSINESS_ERROR_CODE, profileLabelOf, yesNoLabel, futureDanyangLabel } = require('../../constants');
Page({
data: {
@@ -96,22 +96,30 @@ Page({
const metaParts = [];
if (user.heightCm) metaParts.push(`${user.heightCm}cm`);
if (user.regionName) metaParts.push(user.regionName);
- if (user.educationCode) metaParts.push(user.educationCode);
- if (user.occupationCode) metaParts.push(user.occupationCode);
+ const eduLabel = profileLabelOf('educationCode', user.educationCode);
+ if (eduLabel) metaParts.push(eduLabel);
+ const occLabel = profileLabelOf('occupationCode', user.occupationCode);
+ if (occLabel) metaParts.push(occLabel);
const basicInfo = [];
- if (user.maritalStatus) basicInfo.push({ label: '婚姻状况', value: user.maritalStatus });
+ const maritalLabel = profileLabelOf('maritalStatus', user.maritalStatus);
+ if (maritalLabel) basicInfo.push({ label: '婚姻状况', value: maritalLabel });
if (user.childrenStatus || user.hasChildren !== undefined) {
- basicInfo.push({ label: '子女情况', value: user.childrenStatus || (user.hasChildren ? '有孩' : '无孩') });
+ basicInfo.push({ label: '子女情况', value: user.childrenStatus ? profileLabelOf('childrenStatus', user.childrenStatus) : (user.hasChildren ? '有孩' : '无孩') });
}
- if (user.educationCode) basicInfo.push({ label: '学历', value: user.educationCode });
- if (user.occupationCode) basicInfo.push({ label: '职业', value: user.occupationCode });
- if (user.smokingStatus) basicInfo.push({ label: '吸烟', value: user.smokingStatus });
- if (user.drinkingStatus) basicInfo.push({ label: '饮酒', value: user.drinkingStatus });
- if (user.longTermDanyang) basicInfo.push({ label: '定居意愿', value: user.longTermDanyang, full: true });
- if (user.futureDanyang) basicInfo.push({ label: '未来规划', value: user.futureDanyang, full: true });
+ if (eduLabel) basicInfo.push({ label: '学历', value: eduLabel });
+ if (occLabel) basicInfo.push({ label: '职业', value: occLabel });
+ const smokingLabel = profileLabelOf('smokingStatus', user.smokingStatus);
+ if (smokingLabel) basicInfo.push({ label: '吸烟', value: smokingLabel });
+ const drinkingLabel = profileLabelOf('drinkingStatus', user.drinkingStatus);
+ if (drinkingLabel) basicInfo.push({ label: '饮酒', value: drinkingLabel });
+ const longTermLabel = yesNoLabel(user.longTermDanyang);
+ if (longTermLabel) basicInfo.push({ label: '定居意愿', value: longTermLabel, full: true });
+ const futureLabel = futureDanyangLabel(user.futureDanyang);
+ if (futureLabel) basicInfo.push({ label: '未来规划', value: futureLabel, full: true });
const attitudeInfo = [];
+ // relationshipGoal / marriagePlan 由后端返回中文文案(结婚 / 恋爱 等),未知码原样透传
if (user.relationshipGoal) attitudeInfo.push({ label: '寻找', value: user.relationshipGoal, full: true });
if (user.marriagePlan) attitudeInfo.push({ label: '结婚计划', value: user.marriagePlan, full: true });
diff --git a/miniapp/pages/service-appointment/service-appointment.wxml b/miniapp/pages/service-appointment/service-appointment.wxml
index 9dfa4d1..1e57357 100644
--- a/miniapp/pages/service-appointment/service-appointment.wxml
+++ b/miniapp/pages/service-appointment/service-appointment.wxml
@@ -1 +1 @@
-U51 服务预约预约到站服务选择合适的时间和服务项目,提交后等待工作人员确认。服务站点{{serviceLocationName || '已选服务站点'}}服务项目{{service}}›预约日期{{date || '请选择日期'}}希望沟通的事项(选填)提交预约不代表已确认。工作人员确认后,预约状态会更新为“已确认”。{{submitting ? '提交中…' : '提交预约'}}
+服务预约预约到站服务选择合适的时间和服务项目,提交后等待工作人员确认。服务站点{{serviceLocationName || '已选服务站点'}}服务项目{{service}}›预约日期{{date || '请选择日期'}}希望沟通的事项(选填)提交预约不代表已确认。工作人员确认后,预约状态会更新为“已确认”。{{submitting ? '提交中…' : '提交预约'}}
diff --git a/miniapp/pages/service-history/service-history.wxml b/miniapp/pages/service-history/service-history.wxml
index 844d340..2682a92 100644
--- a/miniapp/pages/service-history/service-history.wxml
+++ b/miniapp/pages/service-history/service-history.wxml
@@ -1 +1 @@
-U64 公益服务记录每一次服务,都清楚可查这里只展示与你有关的公益服务事实,不展示咨询正文。…正在加载!{{error}}{{item.icon}}{{item.title}}{{item.date}}{{item.status}}记还没有公益服务记录预约到站服务或申请公益咨询后,会在这里留下时间线。咨询与公益红娘记录彼此分离;工作人员只能查看职责范围内的信息。
+公益服务记录每一次服务,都清楚可查这里只展示与你有关的公益服务事实,不展示咨询正文。…正在加载!{{error}}{{item.icon}}{{item.title}}{{item.date}}{{item.status}}记还没有公益服务记录预约到站服务或申请公益咨询后,会在这里留下时间线。咨询与公益红娘记录彼此分离;工作人员只能查看职责范围内的信息。
diff --git a/miniapp/pages/settings/settings.json b/miniapp/pages/settings/settings.json
index 1686018..0b57d57 100644
--- a/miniapp/pages/settings/settings.json
+++ b/miniapp/pages/settings/settings.json
@@ -1 +1 @@
-{"navigationBarTitleText":"settings","usingComponents":{}}
+{"navigationBarTitleText":"设置","usingComponents":{}}
diff --git a/scripts/audit-miniapp-language.mjs b/scripts/audit-miniapp-language.mjs
new file mode 100644
index 0000000..2d14380
--- /dev/null
+++ b/scripts/audit-miniapp-language.mjs
@@ -0,0 +1,201 @@
+#!/usr/bin/env node
+/**
+ * 小程序文案语种审计:找出「会显示给用户、但不是中文简体」的文本。
+ *
+ * 为什么需要它:小程序里最典型的一类缺陷是**后端枚举码直接当展示值**——
+ * 例如某个 的下拉里出现
+ * any / accept / reject,而同一行的选中值却是「不限」。静态类型检查、
+ * WXSS/WXML 编译、结构守卫全都不会报,只有人眼在真机上才发现。
+ *
+ * 检查项:
+ * A 文本节点里的 ASCII 单词(剥掉注释与 {{ }} 表达式后仍有英文)
+ * B 中文属性(placeholder/title/label/subtitle/action/desc…)里的 ASCII 单词
+ * C 字面量选项里出现纯英文
+ * D JS 里 label: 'English' 这类未汉化的文案字段
+ * E 繁体字(与简体异形的常用字表)——「其他非中文简体」里的常见一类
+ * F 页面 .json 的 navigationBarTitleText 为英文
+ *
+ * 用法:node scripts/audit-miniapp-language.mjs [--json] [--out=file]
+ * 退出码:有命中 → 1;干净 → 0。(可直接当守卫用)
+ */
+import { readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
+import { join, relative, resolve } from 'node:path'
+
+const STACK_FILE = 'E:/develop/公益婚恋服务平台/.workbuddy/audit_stack.txt'
+
+try {
+ main()
+} catch (e) {
+ try { writeFileSync(STACK_FILE, 'AUDIT_CRASH:\n' + ((e && e.stack) || String(e)), 'utf8') } catch (_) {}
+ console.error((e && e.stack) || e)
+ process.exit(3)
+}
+
+function main() {
+ const root = resolve(import.meta.dirname, '..')
+ const MINIAPP = join(root, 'miniapp')
+ const AS_JSON = process.argv.includes('--json')
+ const outArg = process.argv.find((a) => a.startsWith('--out='))
+
+ /** 允许出现在界面上的非中文片段(单位、专名缩写等),按需增补并写清理由 */
+ const ASCII_ALLOW = new Set([
+ 'cm', 'kg', 'ml', 'km', 'm', // 计量单位
+ 'ID', 'id', // 证件/编号类专名
+ 'APP', 'App', // 平台专名
+ 'VIP',
+ 'AI',
+ 'OK',
+ 'Lv', 'LV',
+ 'MB', 'GB', 'KB', // 体积
+ 'TEL',
+ 'URL',
+ 'el', // 个别组件前缀(如 el-tag 为历史遗留 Web 组件,不在小程序渲染路径)
+ // 图片/文档格式专名(与 cm/kg 同理,属技术缩写而非「其他语言」)
+ 'JPG', 'PNG', 'GIF', 'PDF', 'WEBP',
+ ])
+
+ /**
+ * 与简体**异形**的常用繁体字。只收录「简体写法不同」的字,
+ * 简繁同形的字(如 趣/域/婚/子/女/定/案 等)绝不能放进来,否则全是误报。
+ */
+ const TRADITIONAL_CHARS = new Set(
+ '個們這來時說對開關聯繫預約資標準確認興學歷職業區狀況選擇設檔訊報幫隱權註銷帳號審過實證齡體車產戶現電話碼驗動參簽紅薦發務條詳細臺灣門網絡軟機腦錄簡無線藍員會構團隊義誌願評價滿歡適檢醫療護飲當還讓給與從麼樣為長愛見覺點種問間東風雲視圖書課師圓場園館廳營記軍農極變層屬歲廣慶應處養雙頁頂項順顧題額類頻飛飯飽餓鮮藥衛親眾優傳偉傷兒'
+ )
+
+ /** 需要审计的用户可见属性 */
+ const TEXT_ATTRS = [
+ 'placeholder', 'title', 'subtitle', 'action', 'label', 'desc', 'description',
+ 'content', 'text', 'tip', 'tips', 'confirm-text', 'cancel-text', 'empty-text',
+ 'hint', 'notice', 'caption', 'unit',
+ ]
+
+ const findings = []
+ const add = (level, file, line, kind, detail) => {
+ findings.push({ level, file: relative(root, file).replace(/\\/g, '/'), line, kind, detail })
+ }
+
+ function walk(dir, filter, out = []) {
+ for (const name of readdirSync(dir)) {
+ if (name === 'node_modules' || name === '.git' || name === 'dist') continue
+ const p = join(dir, name)
+ const st = statSync(p)
+ if (st.isDirectory()) walk(p, filter, out)
+ else if (filter(name)) out.push(p)
+ }
+ return out
+ }
+
+ /** 把 注释替换成等长空白,保持行号不变 */
+ function stripWxmlComments(src) {
+ return src.replace(//g, (m) => m.replace(/[^\n]/g, ' '))
+ }
+
+ /** 剥离 {{ }} 表达式(表达式里出现英文是正常的:变量名/枚举判断) */
+ function stripMustache(src) {
+ return src.replace(/\{\{[\s\S]*?\}\}/g, (m) => ' '.repeat(m.length))
+ }
+
+ function linesOf(src) {
+ return src.split(/\r?\n/)
+ }
+
+ function asciiWords(text) {
+ const words = text.match(/[A-Za-z][A-Za-z0-9_'-]*/g) || []
+ return words.filter((w) => !ASCII_ALLOW.has(w))
+ }
+
+ /* ---------------- A/B/C:WXML ---------------- */
+ for (const file of walk(join(MINIAPP, 'pages'), (n) => n.endsWith('.wxml'))
+ .concat(walk(join(MINIAPP, 'components'), (n) => n.endsWith('.wxml')))) {
+ const src = stripWxmlComments(readFileSync(file, 'utf8'))
+ const lines = linesOf(src)
+
+ // A 文本节点
+ lines.forEach((line, i) => {
+ // 先在整行上剥离 {{ }},否则 mustache 内的 > / <(如 minlength > 0 && x < y)
+ // 会破坏「>文本<」的提取,把比较运算符误判为文本节点。
+ const strippedLine = stripMustache(line)
+ const texts = [...strippedLine.matchAll(/>([^<>]*) m[1])
+ for (const raw of texts) {
+ const text = stripMustache(raw).trim()
+ if (!text) continue
+ const words = asciiWords(text)
+ if (words.length) add('error', file, i + 1, 'A-文本节点', `"${text.trim()}" → ${words.join(', ')}`)
+ }
+ })
+
+ // B 中文属性
+ // 负向环视:排除 bind:action / catch:action / data-action 等事件绑定——
+ // 它们的取值是处理器标识(onXxx),不是界面可见文案。
+ const attrRe = new RegExp(`(? {
+ for (const m of line.matchAll(attrRe)) {
+ const value = stripMustache(m[2])
+ if (!value.trim() || value.includes('{{')) continue
+ const words = asciiWords(value)
+ if (words.length) add('error', file, i + 1, 'B-属性', `${m[1]}="${value}" → ${words.join(', ')}`)
+ }
+ })
+
+ // C picker 字面量选项
+ lines.forEach((line, i) => {
+ for (const m of line.matchAll(/range="\{\{\s*\[([^\]]*)\]\s*\}\}"/g)) {
+ const items = m[1].split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean)
+ const bad = items.filter((s) => s && !/[\u4e00-\u9fa5]/.test(s))
+ if (bad.length) add('error', file, i + 1, 'C-picker 选项', `纯英文/非中文选项: ${bad.join(', ')}`)
+ }
+ })
+ }
+
+ /* ---------------- D:JS/TS 未汉化文案字段 ---------------- */
+ const LABEL_KEYS = '(label|title|subtitle|text|placeholder|desc|description|name|tip|notice)'
+ for (const file of walk(MINIAPP, (n) => /\.(js|ts)$/.test(n))) {
+ const src = readFileSync(file, 'utf8')
+ const lines = linesOf(src)
+ lines.forEach((line, i) => {
+ // 跳过注释行
+ if (/^\s*(\/\/|\*|\/\*)/.test(line)) return
+ const re = new RegExp(`${LABEL_KEYS}\\s*:\\s*(['"])([A-Za-z][A-Za-z0-9 _'-]*)\\1`, 'g')
+ for (const m of line.matchAll(re)) {
+ if (ASCII_ALLOW.has(m[3])) continue
+ add('error', file, i + 1, 'D-文案字段', `${m[1]}: '${m[3]}'`)
+ }
+ })
+ }
+
+ /* ---------------- E:繁体字 ---------------- */
+ for (const file of walk(MINIAPP, (n) => /\.(wxml|js|ts|json)$/.test(n))) {
+ const src = file.endsWith('.wxml') ? stripWxmlComments(readFileSync(file, 'utf8')) : readFileSync(file, 'utf8')
+ linesOf(src).forEach((line, i) => {
+ const hits = [...new Set([...line].filter((ch) => TRADITIONAL_CHARS.has(ch)))]
+ if (hits.length) add('error', file, i + 1, 'E-繁体字', hits.join(''))
+ })
+ }
+
+ /* ---------------- F:页面 .json navigationBarTitleText 英文 ---------------- */
+ for (const file of walk(join(MINIAPP, 'pages'), (n) => n.endsWith('.json'))) {
+ let json
+ try { json = JSON.parse(readFileSync(file, 'utf8')) } catch (_) { continue }
+ const t = json && json.navigationBarTitleText
+ if (typeof t === 'string' && t.trim() && /[A-Za-z]/.test(t) && !/[\u4e00-\u9fa5]/.test(t)) {
+ add('error', file, 1, 'F-窗口标题', `navigationBarTitleText="${t}"`)
+ }
+ }
+
+ /* ---------------- 输出 ---------------- */
+ if (outArg) {
+ writeFileSync(outArg.slice('--out='.length), JSON.stringify(findings, null, 2), 'utf8')
+ }
+ if (AS_JSON) {
+ console.log(JSON.stringify(findings, null, 2))
+ } else {
+ const byKind = {}
+ for (const f of findings) (byKind[f.kind] ||= []).push(f)
+ for (const [kind, list] of Object.entries(byKind)) {
+ console.log(`\n【${kind}】${list.length} 处`)
+ for (const f of list) console.log(` ${f.file}:${f.line} ${f.detail}`)
+ }
+ console.log(`\n合计命中:${findings.length}`)
+ }
+ process.exit(findings.length ? 1 : 0)
+}
diff --git a/scripts/run-ui-contracts.mjs b/scripts/run-ui-contracts.mjs
index a909045..a588089 100644
--- a/scripts/run-ui-contracts.mjs
+++ b/scripts/run-ui-contracts.mjs
@@ -39,6 +39,11 @@ const UI_TESTS_DIR = resolve(root, 'ui-tests')
// 界面显示陈旧/默认值。2026-09-23 实测数据统计页漏斗卡恒为 0 即此因。
// 该缺陷无 TS/lint/构建报错,前 19 个守卫全绿也抓不到,只有真跑起来看渲染
// 结果才暴露 → 必须由源码结构断言接管)。
+// 20 → 21:新增 miniapp-capsule-safe-contracts(胶囊按钮安全区几何,防 Tab 页与右上角
+// 胶囊按钮重合——这类布局缺陷目视才发现,结构化断言钉住安全区变量)。
+// 21 → 22:新增 miniapp-language-contracts(界面语种守卫:复用 scripts/audit-miniapp-language.mjs,
+// 断言「全量扫描 0 命中」,兜住英文/非中文简体枚举码 any/accept/reject、never_married、
+// bachelor、U51… 直接上屏这类只有真机才暴露的缺陷)。
// 新增/删减守卫时同步修改本默认值。
// 可用环境变量 UI_GUARDS_MIN 覆盖 —— 唯一目的是让「数量下限」这条分支本身可被自检
// (`UI_GUARDS_MIN=99 node scripts/run-ui-contracts.mjs` 必须拒绝报绿)。
diff --git a/ui-tests/miniapp-language-contracts.mjs b/ui-tests/miniapp-language-contracts.mjs
new file mode 100644
index 0000000..9724414
--- /dev/null
+++ b/ui-tests/miniapp-language-contracts.mjs
@@ -0,0 +1,54 @@
+import assert from 'node:assert/strict'
+import { spawnSync } from 'node:child_process'
+import { dirname, resolve } from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+/**
+ * 小程序界面语种契约守卫
+ * ============================
+ * 复用 scripts/audit-miniapp-language.mjs 的全量扫描(A 文本节点 / B 属性 /
+ * C picker 选项 / D JS 文案字段 / E 繁体字 / F 窗口标题),断言「0 命中」。
+ *
+ * 为什么必须独立成守卫:2026-09 真机事故——后端枚举码(never_married / bachelor /
+ * any / accept / reject)直接当展示值上屏,静态类型检查 / WXML·WXSS 编译 / 结构
+ * 守卫全都抓不到,只有人眼在真机才看得见。这是「界面必须全中文简体」唯一的自动化兜底。
+ *
+ * 单一事实源:本守卫不重复实现扫描逻辑,而是直接派生 audit 脚本——改扫描规则只改一处。
+ */
+
+const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
+const auditScript = resolve(root, 'scripts/audit-miniapp-language.mjs')
+
+const r = spawnSync(process.execPath, [auditScript, '--json'], {
+ cwd: root,
+ encoding: 'utf8',
+ env: { ...process.env, NO_COLOR: '1' },
+})
+
+let findings = []
+if (r.status === 3) {
+ // 审计脚本自身崩溃(异常分支),绝不能当成「通过」默默绿掉
+ throw new Error(
+ '语种审计脚本执行失败(exit 3),请先修复 scripts/audit-miniapp-language.mjs:\n' +
+ (r.stderr || r.stdout || '(无输出)'),
+ )
+}
+if (r.stdout && r.stdout.trim()) {
+ try {
+ findings = JSON.parse(r.stdout)
+ } catch {
+ // 理论上 --json 只输出 JSON;解析失败则交给下面的长度断言兜底
+ }
+}
+
+assert.equal(
+ findings.length,
+ 0,
+ `小程序仍存在英文/非中文简体上屏(${findings.length} 处)。示例:\n` +
+ findings
+ .slice(0, 8)
+ .map((f) => ` ${f.file}:${f.line} [${f.kind}] ${f.detail}`)
+ .join('\n'),
+)
+
+console.log(`Miniapp language contracts passed(语种审计 0 命中)`)