merge: 将 fix/2026-07-20-errkey 合并到 dev/1.0

# Conflicts:
#	.local.config.js
#	pages_tool/ai-chat/ai-chat-message.vue
This commit is contained in:
2026-07-23 14:11:27 +08:00
36 changed files with 15249 additions and 6158 deletions

View File

@@ -62,11 +62,7 @@ const localDevConfig = ({
'local-11': { // 测试平台
uniacid: 11,
domain: 'http://localhost:8050/',
},
'local-18': { // 人工晶体
uniacid: 18,
domain: 'http://localhost:8050/',
},
},
'100': {
uniacid: 100,
domain: 'https://5g.aigc-quickapp.com/',
@@ -87,6 +83,10 @@ const localDevConfig = ({
uniacid: 206,
domain: 'https://5g.aigc-quickapp.com/',
},
'5g-165': { // 鸡肉调理品, 优惠卷显示不全,及逻辑错误
uniacid: 165,
domain: 'https://5g.aigc-quickapp.com/',
},
'47': { // 修改弹窗
uniacid: 47,
domain: 'https://5g.aigc-quickapp.com/',
@@ -121,7 +121,7 @@ const localDevConfig = ({
},
'2482': { // 低空经济
uniacid: 2482,
domain: 'https://xcx3.aigc-quickapp.com/',
domain: 'https://xcx6.aigc-quickapp.com/',
},
'2485': { // 验证电子表单前后端数据一致性
uniacid: 2485,
@@ -142,26 +142,26 @@ const localDevConfig = ({
'48': { // 测试平台
uniacid: 48,
domain: 'https://test.aigc-quickapp.com',
},
'54': { // 测试平台
uniacid: 54,
domain: 'https://test.aigc-quickapp.com',
},
'test-12': { // 户外鞋,测试优惠券
uniacid: 12,
domain: 'https://test.aigc-quickapp.com',
},
// })['5g-237']; // 商品分类,没有立即咨询文字
// })['5g-97']; // 大宗商品贸易,支持自定义字体大小、颜色
// })['5g-50']; // 燃气安全产品机器人,购物车无反应
})['2821']; // 离型纸硅油纸
// })['2821']; // 离型纸硅油纸
// })['2482']; // 低空经济
// })['48']; // 离型纸硅油纸
// })['2485']; // 验证电子表单前后端数据一致性
// })['5g-11']; // 数码
// })['5g-213']; // 选择要使用的环境配置
// })['5g-213']; // 选择要使用的环境配置
})['5g-165']; // 鸡肉调理品, 优惠卷显示不全,及逻辑错误
// })['2811']; // 选择要使用的环境配置
// })['local-11']; // 选择要使用的环境配置
// })['local-1']; // 本地
// })['local-18']; // 本地-人工晶体-有商品
// })['local-2']; // 本地
// })['local-11']; // 本地
// })['1-test']; // test
// })['18-test']; // test
// })['test-12']; // test
export default localDevConfig;

View File

@@ -341,8 +341,8 @@ export default {
// empty / unknown 不走模板,直接返回通用网络异常
if (info.type === 'empty' || info.type === 'unknown') return reason;
const tpl = this.$lang('diy.loadFailTpl');
return tpl.replace('{reason}', reason);
const tpl = this.$lang('diy.loadFailTpl', { reason });
return tpl;
},
// 错误横幅「重试」入口
retryDiyInfo() {

View File

@@ -1247,7 +1247,7 @@ export default {
const fieldCacheKey = `${locale}__${langPath}__${_overrideVersion}__${field}`;
const cachedFieldResult = _fieldResultCache[fieldCacheKey];
const hasDefault = arguments.length > 1 && typeof arguments[1] === 'string' && arguments[1] !== '';
const __DEV__ = (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production');
if (cachedFieldResult !== undefined) {
let value = cachedFieldResult;
// 没找到翻译
@@ -1256,10 +1256,6 @@ export default {
if (hasDefault) {
return arguments[1];
}
// 生产环境无默认值 → 返回空串(避免用户看到 raw key)
if (!__DEV__) {
return '';
}
// 开发环境 → 返回 raw key(便于发现问题)
return value;
}
@@ -1350,9 +1346,6 @@ export default {
// 有默认值 → 使用默认值
if (hasDefault) {
value = arguments[1];
} else if (!__DEV__) {
// 生产环境无默认值 → 返回空串(避免用户看到 raw key)
value = '';
}
// 开发环境无默认值 → 保留 raw key(便于发现问题)
} else if (arguments.length > 1) {
@@ -1367,7 +1360,7 @@ export default {
if (value == undefined) value = '';
// 多语言调试
if (__DEV__ && field == value) {
if (field == value) {
console.warn(`[i18n] missing key "${field}" in ${langPath || '<root>'} (${locale})`);
}

View File

@@ -265,7 +265,7 @@ export default {
// #ifndef MP-WEIXIN
if (item?.channelType === 'wechat') {
uni.showToast({
title: '视频号仅支持在微信小程序环境中播放',
title: this.$lang('wechatChannel.wechatMpOnly'),
icon: 'none',
duration: 2000
});

View File

@@ -308,7 +308,7 @@ export default {
success: function(res) {
if (res.code === 0) {
uni.showToast({
title: '提交成功',
title: self.$lang('submitSuccess'),
icon: 'success'
});
// 重置表单
@@ -317,7 +317,7 @@ export default {
}
} else {
uni.showToast({
title: res.message || '提交失败',
title: res.message || self.$lang('submitFail'),
icon: 'none'
});
}

View File

@@ -10,14 +10,14 @@
return {
list: [],
config: {
title: '猜你喜欢',
title: '',
sources: 'sort',
supportPage: [],
goodsIds: [],
fontWeight: false,
padding: 0,
cartEvent: 'detail',
text: '购买',
text: '',
textColor: '#FFFFFF',
theme: 'default',
aroundRadius: 25,
@@ -67,10 +67,13 @@
}
},
mounted() {
if (this.auto) {
this.getLikeList();
}
},
// 初始化 i18n 默认文案,实际展示以服务端配置 config.title / config.text 为准
if (!this.config.title) this.config.title = this.$lang('common.goodsRecommendTitle');
if (!this.config.text) this.config.text = this.$lang('buyNow');
if (this.auto) {
this.getLikeList();
}
},
methods: {
init() {
this.list = [];

View File

@@ -56,10 +56,18 @@
<!-- 手机号授权按钮 -->
<block v-if="isAgree">
<!-- #ifdef MP-WEIXIN -->
<button class="auth-login save-btn border-0" style="border: none;" open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber">
<text style="color: #fff; font-size: 30rpx;">{{ $lang('components.ns-login.agreeAndAuthorizeMobile') }}</text>
</button>
<!-- #endif -->
<!-- #ifdef MP-HARMONY -->
<!-- 鸿蒙元服务无 getPhoneNumber 微信 API,改为弹出手机号+验证码表单 -->
<button class="auth-login save-btn border-0" style="border: none;" @tap="showHarmonyMobileForm = true">
<text style="color: #fff; font-size: 30rpx;">{{ $lang('components.ns-login.agreeAndAuthorizeMobile') }}</text>
</button>
<!-- #endif -->
</block>
<block v-else>
<button class="auth-login save-btn border-0" style="border: none;" @tap="authno">
@@ -67,6 +75,27 @@
</button>
</block>
<!-- #ifdef MP-HARMONY -->
<!-- 鸿蒙专属:手机号+验证码表单 -->
<view v-if="showHarmonyMobileForm" class="harmony-mobile-form" @touchmove.prevent.stop>
<view class="item-wrap">
<text class="label">{{ $lang('components.ns-login.mobileLabel') }}</text>
<input type="number" :placeholder="$lang('components.ns-login.mobilePlaceholder')" v-model="formData.mobile" maxlength="11" />
</view>
<view class="item-wrap" v-if="isOpenCaptcha == 1">
<text class="label">{{ $lang('components.ns-login.captchaLabel') }}</text>
<input type="number" :placeholder="$lang('components.ns-login.captchaPlaceholder')" v-model="formData.vercode" maxlength="4" />
<image :src="captcha.img" class="captcha" @tap="getCaptcha" show-menu-by-longpress></image>
</view>
<view class="item-wrap">
<text class="label">{{ $lang('components.ns-login.dynacodeLabel') }}</text>
<input type="number" :placeholder="$lang('components.ns-login.dynacodePlaceholder')" v-model="formData.dynacode" maxlength="4" />
<view class="send color-base-text" @tap="sendMobileCode">{{ dynacodeData.codeText }}</view>
</view>
<button type="default" class="save-btn" @tap="harmonySaveMobile" :disabled="isDisabled">{{ $lang('components.ns-login.save') }}</button>
</view>
<!-- #endif -->
<view @tap="closeLogin"
style="font-size: 30rpx; color: #888; text-align: center; padding-top: 10rpx;">
{{ $lang('components.ns-login.notNow') }}
@@ -109,11 +138,20 @@
<view class="item-wrap" v-if="forceBindingMobileControl">
<text class="label">{{ $lang('components.ns-login.mobileLabel') }}</text>
<!-- #ifdef MP-WEIXIN -->
<button open-type="getPhoneNumber" :plain="true" class="auth-login border-0"
@getphonenumber="getPhoneNumber">
<text class="mobile" v-if="formData.mobile">{{ formData.mobile }}</text>
<text class="color-base-text" v-else>{{ $lang('components.ns-login.getMobile') }}</text>
</button>
<!-- #endif -->
<!-- #ifdef MP-HARMONY -->
<!-- 鸿蒙无 getPhoneNumber,展示手动输入的手机号,没有时显示"获取手机号"提示用户去上面表单录入 -->
<view class="auth-login border-0" :plain="true">
<text class="mobile" v-if="formData.mobile">{{ formData.mobile }}</text>
<text class="color-base-text" v-else @tap="showHarmonyMobileForm = true">{{ $lang('components.ns-login.getMobile') }}</text>
</view>
<!-- #endif -->
</view>
<button type="default" class="save-btn" @tap="saveMp" :disabled="isDisabled">{{ $lang('components.ns-login.save') }}</button>
</template>
@@ -170,12 +208,16 @@ export default {
isSend: false
},
// 小程序获取手机号所需数据
authMobileData: {
iv: '',
encryptedData: ''
},
site_name: ''
};
authMobileData: {
iv: '',
encryptedData: ''
},
site_name: '',
// #ifdef MP-HARMONY
// 鸿蒙专属:控制手机号+验证码表单显示
showHarmonyMobileForm: false,
// #endif
};
},
options: {
styleIsolation: 'shared'
@@ -203,7 +245,7 @@ export default {
if (this.forceBindingMobileControl) {
if (this.formData.mobile.length == 0) return true;
// #ifdef H5
// #ifdef H5 || MP-HARMONY
// 验证码
if (this.isOpenCaptcha == 1 && this.formData.vercode.length == 0) return true;
// 动态码
@@ -343,7 +385,7 @@ export default {
openCompleteInfoPop() {
this.getRegisterConfig();
// #ifdef H5
// #ifdef H5 || MP-HARMONY
if (!this.storeToken) this.getCaptchaConfig();
// #endif
@@ -676,6 +718,26 @@ export default {
this.forceBindMobile();
},
// #ifdef MP-HARMONY
// 鸿蒙元服务保存手机号:复用 H5 的强制绑定手机号流程
harmonySaveMobile() {
if (this.forceBindingMobileControl) {
let data = {
mobile: this.formData.mobile,
key: this.formData.key,
code: this.formData.dynacode
};
if (this.captcha.id != '') {
data.captcha_id = this.captcha.id;
data.captcha_code = this.formData.vercode;
}
if (!this.verify(data)) return;
}
// 鸿蒙环境下通常也开启了 forceBindingMobileControl(走 forceBindMobile 接口)
this.forceBindMobile();
},
// #endif
// 微信小程序保存数据
saveMp() {
if (this.formData.mobile === '') {

View File

@@ -53,7 +53,7 @@
let systemInfo = uni.getSystemInfoSync();
let menuButtonInfo = {};
// 如果是小程序,获取右上角胶囊的尺寸信息,避免导航栏右侧内容与胶囊重叠(支付宝小程序非本API,尚未兼容)
// #ifdef MP-WEIXIN || MP-BAIDU || MP-TOUTIAO || MP-QQ
// #ifdef MP-WEIXIN || MP-BAIDU || MP-TOUTIAO || MP-QQ || MP-HARMONY
menuButtonInfo = uni.getMenuButtonBoundingClientRect();
// #endif

View File

@@ -70,37 +70,11 @@ export default {
return {
isIphoneX: false,
payIndex: 0,
// #ifdef H5
payTypeList: [{
name: this.$lang('components.nsPayment.alipay'),
icon: 'icon-zhifubaozhifu-',
type: 'alipay'
},
{
name: this.$lang('components.nsPayment.wechatpay'),
icon: 'icon-weixin1',
type: 'wechatpay'
}
],
payTypeList: [],
timer: null,
// #endif
// #ifdef MP-WEIXIN
payTypeList: [{
name: this.$lang('components.nsPayment.wechatpay'),
provider: 'wxpay',
icon: 'icon-weixin1',
type: 'wechatpay'
}],
isMatched: 0, // 支付是否需要校验
// #endif
// #ifdef MP-ALIPAY
payTypeList: [{
name: this.$lang('components.nsPayment.alipay'),
icon: 'icon-zhifubaozhifu-',
type: 'alipay',
provider: 'alipay'
}],
// #endif
payInfo: {},
balanceConfig: 0,
// 预售页面判断
@@ -108,6 +82,30 @@ export default {
};
},
created(e) {
// #ifdef H5
this.payTypeList = [
{ name: this.$lang('components.nsPayment.alipay'), icon: 'icon-zhifubaozhifu-', type: 'alipay' },
{ name: this.$lang('components.nsPayment.wechatpay'), icon: 'icon-weixin1', type: 'wechatpay' },
];
// #endif
// #ifdef MP-WEIXIN
this.payTypeList = [
{ name: this.$lang('components.nsPayment.wechatpay'), provider: 'wxpay', icon: 'icon-weixin1', type: 'wechatpay' },
];
// #endif
// #ifdef MP-ALIPAY
this.payTypeList = [
{ name: this.$lang('components.nsPayment.alipay'), icon: 'icon-zhifubaozhifu-', type: 'alipay', provider: 'alipay' },
];
// #endif
// #ifdef MP-HARMONY
// 鸿蒙元服务的支付通道: 与微信小程序相同的 provider='wxpay',
// 但走鸿蒙 JSSDK 的 requestPayment(走鸿蒙支付服务而非 wx.requestPayment)
this.payTypeList = [
{ name: this.$lang('components.nsPayment.wechatpay'), provider: 'wxpay', icon: 'icon-weixin1', type: 'wechatpay' },
];
// #endif
let url;
// #ifdef H5
url = window.location.href

View File

@@ -95,6 +95,16 @@
i18nKey: 'components.payment.alipay'
}],
// #endif
// #ifdef MP-HARMONY
// 鸿蒙元服务的支付通道: provider='wxpay' 由鸿蒙 JSSDK 接管
payTypeList: [{
name: '微信支付',
provider: 'wxpay',
icon: 'icon-weixin1',
type: 'wechatpay',
i18nKey: 'components.payment.wechatpay'
}],
// #endif
payInfo: null,
balanceConfig: 0,
// 预售页面判断

File diff suppressed because it is too large Load Diff

View File

@@ -2,10 +2,10 @@ export const lang = {
// 底部选项卡
tabBar: {
home: 'index',
category: 'category',
cart: 'cart',
member: 'member',
home: 'Home',
category: 'Category',
cart: 'Cart',
member: 'Me',
},
//公共
@@ -57,6 +57,7 @@ export const lang = {
// diy custom page (diy-specific copy, generic HTTP errors reuse http.*)
diy: {
networkErrorTitle: 'Notice',
networkError: 'Network error, please try again later',
retry: 'Retry',
noDiyData: 'Custom page data not configured',
locationNotAvailable: 'Location not available',
@@ -111,6 +112,7 @@ export const lang = {
notSupported: 'Current environment does not support WeChat Channel',
missingFields: 'Missing required field: {field}',
playFailed: 'WeChat video playback failed!',
wechatMpOnly: 'WeChat Channel only plays in WeChat Mini Program',
},
// uniapp utility functions
@@ -256,7 +258,14 @@ export const lang = {
addToCartFailMin: 'Minimum purchase quantity not met',
addToCartFailStock: 'Insufficient Stock',
// 咨询
// generic submit/version/loading text (key lives in root because common.js's lang object IS the common namespace)
submitSuccess: 'Submit Success',
submitFail: 'Submit Failed',
versionInfo: 'Version Info',
commitId: 'Commit ID',
buildTime: 'Build Time',
loading: 'Loading...',
consult: 'Consult Now',
consultSuccess: 'Consultation Successful',
consultFail: 'Consultation Failed',

View File

@@ -3,5 +3,6 @@ export const lang = {
title: 'Payment Method',
paymentAmount: 'Payment Amount',
confirmPayment: 'Confirm Payment',
seeOrder: 'View Order'
seeOrder: 'View Order',
payInfoNotFound: 'Payment info not found!'
}

View File

@@ -64,6 +64,7 @@ export const lang = {
// diy 自定义页面(diy 专属文案,通用网络/接口错误复用 http.*)
diy: {
networkErrorTitle: '提示',
networkError: '网络异常,请稍后重试',
retry: '重试',
noDiyData: '未配置自定义页面数据',
locationNotAvailable: '未获取到定位',
@@ -111,6 +112,7 @@ export const lang = {
notSupported: '当前环境不支持微信视频号',
missingFields: '缺少必要字段: {field}',
playFailed: '微信视频播放失败!',
wechatMpOnly: '视频号仅支持在微信小程序环境中播放',
},
// uniapp 工具函数
@@ -254,6 +256,14 @@ export const lang = {
addToCartFailMin: '已达到最小购买数量',
addToCartFailStock: '库存不足',
// 通用提交/版本/loading 文案(原本该走 common.* 命名空间,因 common.js 整体即 common namespace,放在 root)
submitSuccess: '提交成功',
submitFail: '提交失败',
versionInfo: '版本信息',
commitId: '提交 ID',
buildTime: '构建时间',
loading: '加载中...',
consult: '立即咨询',
consultSuccess: '咨询成功',
consultFail: '咨询失败',

View File

@@ -3,5 +3,6 @@ export const lang = {
title: '支付方式',
paymentAmount: '支付金额',
confirmPayment: '确认支付',
seeOrder: '查看订单'
seeOrder: '查看订单',
payInfoNotFound: '未获取到支付信息!'
}

View File

@@ -80,10 +80,18 @@ Vue.mixin(MpShareDefault);
// lang.js 在翻译覆写更新时 commit('setComponentRefresh'),
// golbalConfig.js 已把 componentRefresh 注册为全局 computed,
// 在此 watch 它并 $forceUpdate,触发所有页面内 {{$lang('xxx')}} 重新求值
// 同时调用 lang.refresh() 让当前页面的 navigationBarTitleText 跟随语种变化
// (pages.json 里的硬编码中文标题需要被 $lang('title') 覆盖)
Vue.mixin({
watch: {
componentRefresh() {
this.$forceUpdate();
try {
if (this.$langConfig && typeof this.$langConfig.refresh === 'function') {
// emit=false: lang.js 已 emit 过,这里避免重复 store.commit
this.$langConfig.refresh(false);
}
} catch (e) { /* 容错:refresh 失败不影响主流程 */ }
}
}
});

View File

@@ -160,10 +160,10 @@ export default {
} catch (e) {}
if (v && v.commitId) {
uni.showModal({
title: '版本信息',
content: `提交 ID: ${v.commitId}\n构建时间: ${v.buildTime}\n${Config.baseUrl}`,
showCancel: false
});
title: this.$lang('versionInfo'),
content: `${this.$lang('commitId')}: ${v.commitId}\n${this.$lang('buildTime')}: ${v.buildTime}\n${Config.baseUrl}`,
showCancel: false
});
} else {
console.error('未获取到构建版本信息');
}

View File

@@ -461,7 +461,7 @@
<text>{{ $lang('goods.detail.detailView.generateSharePoster') }}</text>
</button>
</view>
<!-- #ifdef H5 -->
<!-- #ifdef H5 || MP-HARMONY -->
<view class="share-box" @click="copyUrl">
<button class="share-btn" :plain="true">
<view class="iconfont icon-fuzhilianjie"></view>

View File

@@ -457,7 +457,7 @@
/>
<block v-else>
<ns-goods-action-icon :text="$lang('goods.detail.home')" icon="icon-shouye1" @click="goHome" />
<!-- #ifdef MP-WEIXIN -->
<ns-goods-action-icon
:text="$lang('goods.detail.cart')"
@@ -465,9 +465,19 @@
icon="icon-gouwuche2"
:corner-mark="cartNumber > 0 ? cartNumber + '' : ''"
@click="goCart"
/>
/>
<ns-goods-action-icon :text="$lang('goods.detail.kefu')" icon="icox icox-kefu" :send-data="contactData" :chatParam="chatRoomParams" />
<!-- #endif -->
<!-- #ifdef MP-HARMONY -->
<ns-goods-action-icon
:text="$lang('goods.detail.cart')"
:cornerMarkBg="themeStyle.goods_cart_num_corner"
icon="icon-gouwuche2"
:corner-mark="cartNumber > 0 ? cartNumber + '' : ''"
@click="goCart"
/>
<ns-goods-action-icon :text="$lang('goods.detail.follow')" :icon="whetherCollection == 1 ? 'icon-likefill color-base-text' : 'icon-guanzhu'" @click="editCollection" />
<!-- #endif -->
<!-- #ifdef H5 -->
<ns-goods-action-icon :text="$lang('goods.detail.follow')" :icon="whetherCollection == 1 ? 'icon-likefill color-base-text' : 'icon-guanzhu'" @click="editCollection" />
<!-- #endif -->

View File

@@ -249,7 +249,7 @@ export default {
}
// 优惠券
if (this.goodsSkuDetail.coupon_list) {
if (Array.isArray(this.goodsSkuDetail.coupon_list)) {
this.couponList = this.goodsSkuDetail.coupon_list;
this.couponList.forEach(v => {
if (v.count == v.lead_count) v.useState = 2;

View File

@@ -37,7 +37,7 @@ export default {
fontWeight: false,
padding: 0,
cartEvent: "detail",
text: "购买",
text: "",
textColor: "#FFFFFF",
theme: "default",
aroundRadius: 25,
@@ -67,7 +67,7 @@ export default {
},
onLoad(options) {
this.config.text = this.$lang('goods.list.buyText');
this.config.text = this.$lang('goods.list.buyText') || this.$lang('buyNow');
this.$api.sendRequest({
url: '/api/lucky/status',

View File

@@ -234,7 +234,7 @@
<view class="order-cell coupon" v-if="modules.indexOf('coupon') != -1">
<text class="tit">{{ $lang('order.payment.orderMoney.coupon') }}</text>
<view class="box text-overflow"@click="openSiteCoupon(calculateGoodsData.merch_id)">
<template v-if="orderCreateData.coupon[calculateGoodsData.merch_id].coupon_id">
<template v-if="(orderCreateData.coupon && orderCreateData.coupon[calculateGoodsData.merch_id] && orderCreateData.coupon[calculateGoodsData.merch_id].coupon_id)">
<text class="money price-font" style="max-width: 100%;">{{ $lang('order.payment.orderMoney.couponSelected', { count: 1 }) }}</text>
<!-- <text class="unit price-font">优惠</text>
<text class="money price-font">{{ (calculateData && calculateData.coupon_money ? calculateData.coupon_money : 0) | moneyFormat }}</text> -->
@@ -550,7 +550,7 @@
<view v-if="couponItem.type == 'discount' && couponItem.discount_limit > 0" class="limit">{{ $lang('order.payment.couponPopup.maxDiscount', { amount: couponItem.discount_limit }) }}</view>
<view class="time font-size-goods-tag">{{ $lang('order.payment.couponPopup.validPeriod') }}{{ couponItem.end_time ? $util.timeStampTurnTime(couponItem.end_time) : $lang('order.payment.couponPopup.permanentValid') }}</view>
</view>
<view class="iconfont" :class="selectCouponId == couponItem.coupon_id ? 'icon-yuan_checked color-base-text' : 'icon-yuan_checkbox'"></view>
<view class="iconfont" :class="selectCouponMap[merchCoupon.merch_id] == couponItem.coupon_id ? 'icon-yuan_checked color-base-text' : 'icon-yuan_checkbox'"></view>
</view>
</view>
</view>

View File

@@ -55,7 +55,8 @@ export default {
merch_id: 0,
data: []
},
selectCouponId:0
// 按 merch_id 隔离,避免多商户之间互相污染
selectCouponMap: {}
}
},
inject: ['promotion'],
@@ -85,7 +86,7 @@ export default {
goodsData() {
if (this.paymentData) {
this.paymentData.goods_list.forEach(item => {
if (item.sku_spec_format) item.sku_spec_format = JSON.parse(item.sku_spec_format);
if (item.sku_spec_format && typeof item.sku_spec_format === 'string') item.sku_spec_format = JSON.parse(item.sku_spec_format);
})
return this.paymentData;
}
@@ -93,7 +94,7 @@ export default {
calculateGoodsData() {
if (this.calculateData) {
this.calculateData.goods_list.forEach(item => {
if (item.sku_spec_format) item.sku_spec_format = JSON.parse(item.sku_spec_format);
if (item.sku_spec_format && typeof item.sku_spec_format === 'string') item.sku_spec_format = JSON.parse(item.sku_spec_format);
})
return this.calculateData;
}
@@ -189,7 +190,7 @@ export default {
//多商户商品分类
goodsSpecFormat(goods_list){
goods_list.forEach(item => {
if (item.sku_spec_format) item.sku_spec_format = JSON.parse(item.sku_spec_format);
if (item.sku_spec_format && typeof item.sku_spec_format === 'string') item.sku_spec_format = JSON.parse(item.sku_spec_format);
})
return goods_list
},
@@ -249,7 +250,11 @@ export default {
this.modules = data.modules;
//记录多商户优惠券
this.orderCreateData.coupon = data.coupon
// 兜底为对象,避免后续被当作数组处理
if (!data.coupon || typeof data.coupon !== 'object' || Array.isArray(data.coupon)) {
data.coupon = {};
}
this.orderCreateData.coupon = data.coupon;
//查询优惠券
this.getCouponList();
@@ -365,7 +370,17 @@ export default {
}
});
//多商户
this.orderCreateData.coupon = res.data.coupon
// 保留用户已选的 coupon_id,后端回传的 res.data.coupon 默认都是 0 会覆盖掉
const prevCoupon = (this.orderCreateData.coupon && typeof this.orderCreateData.coupon === 'object' && !Array.isArray(this.orderCreateData.coupon)) ? this.orderCreateData.coupon : {};
this.orderCreateData.coupon = res.data.coupon || {};
if (this.orderCreateData.coupon && typeof this.orderCreateData.coupon === 'object' && !Array.isArray(this.orderCreateData.coupon)) {
Object.keys(prevCoupon).forEach(merch_id => {
const chosen = prevCoupon[merch_id] && prevCoupon[merch_id].coupon_id;
if (chosen && (!this.orderCreateData.coupon[merch_id] || !this.orderCreateData.coupon[merch_id].coupon_id)) {
this.orderCreateData.coupon[merch_id] = { coupon_id: chosen };
}
});
}
// if (!res.data.coupon_id) this.orderCreateData.coupon.coupon_id = 0;
// else this.orderCreateData.coupon.coupon_id = res.data.coupon_id;
@@ -731,44 +746,52 @@ export default {
this.tempData = null;
},
//优惠券单独多商户
/**
* 显示店铺优惠券信息
* @param {Object} siteId
* @param {Object} couponData
*/
openSiteCoupon(merch_id) {
this.tempData = this.$util.deepClone(this.orderCreateData);
this.merchCoupon.merch_id = merch_id;
this.selectCouponId = this.orderCreateData.coupon[merch_id].coupon_id;
// this.selectCouponMoney = this.orderCreateData.coupon[siteId].coupon_money;
this.merchCoupon.data = this.coupon_list[merch_id];
this.$refs.couponPopup.open();
this.$forceUpdate();
},
/**商户选择券*/
selectCoupon(data,merch_id) {
// if (this.orderCreateData.coupon[merch_id].coupon_id == data.coupon_id){
// this.orderCreateData.coupon[merch_id].coupon_id = 0
// }else{
// this.orderCreateData.coupon[merch_id].coupon_id = data.coupon_id
// };
if (this.selectCouponId != data.coupon_id) {
this.selectCouponId = data.coupon_id;
} else {
this.selectCouponId = 0;
}
this.$forceUpdate();
// this.orderCreateData.coupon[merch_id].coupon_id = data.coupon_id
},
//确定使用优惠券
useCpopon() {
this.orderCreateData.coupon[this.merchCoupon.merch_id].coupon_id = this.selectCouponId;
this.$refs.couponPopup.close();
this.calculate();
},
/**
* 显示店铺优惠券信息
* @param {Object} siteId
* @param {Object} couponData
*/
openSiteCoupon(merch_id) {
this.tempData = this.$util.deepClone(this.orderCreateData);
this.merchCoupon.merch_id = merch_id;
// 从当前订单已选的 coupon_id 取值,没有则取弹窗里缓存的,再没有为 0
const exist = this.orderCreateData.coupon && this.orderCreateData.coupon[merch_id];
const cached = this.selectCouponMap[merch_id] || 0;
this.selectCouponMap[merch_id] = exist ? (exist.coupon_id || 0) : cached;
// this.selectCouponMoney = this.orderCreateData.coupon[siteId].coupon_money;
// 兜底: 列表为空时给一个空数组,弹窗不会渲染空指针
this.merchCoupon.data = (this.coupon_list && this.coupon_list[merch_id]) || [];
this.$refs.couponPopup.open();
this.$forceUpdate();
},
/**商户选择券*/
selectCoupon(data, merch_id) {
// if (this.orderCreateData.coupon[merch_id].coupon_id == data.coupon_id){
// this.orderCreateData.coupon[merch_id].coupon_id = 0
// }else{
// this.orderCreateData.coupon[merch_id].coupon_id = data.coupon_id
// };
const cur = this.selectCouponMap[merch_id] || 0;
this.selectCouponMap[merch_id] = (cur != data.coupon_id) ? data.coupon_id : 0;
this.$forceUpdate();
// this.orderCreateData.coupon[merch_id].coupon_id = data.coupon_id
},
//确定使用优惠券
useCpopon() {
const merch_id = this.merchCoupon.merch_id;
// 确保 orderCreateData.coupon 是对象结构,后端按 merch_id 取值
if (!this.orderCreateData.coupon || typeof this.orderCreateData.coupon !== 'object' || Array.isArray(this.orderCreateData.coupon)) {
this.orderCreateData.coupon = {};
}
this.orderCreateData.coupon[merch_id] = {
coupon_id: this.selectCouponMap[merch_id] || 0
};
this.$refs.couponPopup.close();
this.calculate();
},
//-------------------
/**

View File

@@ -39,7 +39,7 @@ export default {
fontWeight: false,
padding: 0,
cartEvent: "detail",
text: this.$lang('merch.list.buyText'),
text: '',
textColor: "#FFFFFF",
theme: "default",
aroundRadius: 25,
@@ -66,6 +66,7 @@ export default {
},
onLoad(options) {
this.config.text = this.$lang('merch.list.buyText');
this.categoryId = options.category_id || 0;
this.keyword = options.keyword || '';
this.coupon = options.coupon || 0;

File diff suppressed because it is too large Load Diff

View File

@@ -79,7 +79,7 @@ export default {
setPublicShare() {
let shareUrl = this.$config.h5Domain + '/pages_tool/help/list';
this.$util.setPublicShare({
title: '帮助中心',
title: this.$lang('help.list.title'),
desc: '',
link: shareUrl,
imgUrl: this.siteInfo ? this.$util.img(this.siteInfo.logo_square) : ''

View File

@@ -77,7 +77,7 @@ export default {
setPublicShare() {
let shareUrl = this.$config.h5Domain + '/pages_tool/notice/list';
this.$util.setPublicShare({
title: '公告',
title: this.$lang('notice.list.title'),
desc: '',
link: shareUrl,
imgUrl: this.siteInfo ? this.$util.img(this.siteInfo.logo_square) : ''
@@ -85,7 +85,7 @@ export default {
}
},
onShareAppMessage(res) {
var title = '公告';
var title = this.$lang('notice.list.title');
var path = '/pages_tool/notice/list';
return {
title: title,

View File

@@ -51,7 +51,7 @@ export default {
if (this.$refs.loadingCover) this.$refs.loadingCover.hide();
} else {
this.$util.showToast({
title: '未获取到支付信息!'
title: this.$lang('pay.index.payInfoNotFound')
});
setTimeout(() => {
this.$util.redirectTo(this.$util.INDEX_PAGE_URL);

View File

@@ -0,0 +1,62 @@
$ErrorActionPreference = 'Stop'
$target = 'C:\Program Files\Huawei QuickApp IDE\resources\app\extensions\deveco-debug\lib\toolkit\nodeTool'
$before = [Environment]::GetEnvironmentVariable('Path', 'Machine')
Write-Host '=== Before: target entries in Machine PATH ==='
$before -split ';' | ForEach-Object {
if ($_.Trim() -ieq $target) {
Write-Host (" [" + $_ + "]")
}
}
$entries = $before -split ';'
$kept = New-Object System.Collections.Generic.List[string]
$removed = New-Object System.Collections.Generic.List[string]
foreach ($e in $entries) {
$t = $e.Trim()
if ($t -ieq $target) {
$removed.Add($e)
} elseif ($t -ne '') {
$kept.Add($e)
}
}
$after = [string]::Join(';', $kept.ToArray())
if ($removed.Count -eq 0) {
Write-Host 'Target entry not found, nothing to do.'
exit 0
}
try {
[Environment]::SetEnvironmentVariable('Path', $after, 'Machine')
Write-Host ''
Write-Host '=== Result ==='
Write-Host (" Removed entries: " + $removed.Count)
foreach ($r in $removed) { Write-Host (" - " + $r) }
Write-Host (" Remaining entries: " + $kept.Count)
Write-Host ' Write OK.'
} catch {
Write-Host ''
Write-Host (' Write FAILED (admin required?): ' + $_.Exception.Message)
exit 1
}
Write-Host ''
Write-Host '=== Verify after write ==='
$verify = [Environment]::GetEnvironmentVariable('Path', 'Machine')
$stillThere = 0
foreach ($e in ($verify -split ';')) {
if ($e.Trim() -ieq $target) { $stillThere++ }
}
if ($stillThere -gt 0) {
Write-Host ' FAIL: target still present.'
exit 1
} else {
Write-Host ' OK: target no longer in Machine PATH.'
}
Write-Host ''
Write-Host 'Note: env var change does NOT affect already-running processes.'
Write-Host 'Reopen cmd / Git Bash / HBuilderX to see updated where node.'

View File

@@ -0,0 +1,58 @@
// 检查 20 个硬编码导航标题的页面,其语言包是否都有 title key
const paths = [
['pages_goods/category','商品分类'],
['pages_goods/cart','购物车'],
['pages_goods/detail','商品详情'],
['pages_goods/list','商品列表'],
['pages_promotion/merch/merchcategory','入驻商户'],
['pages_promotion/merch/detail','商户首页'],
['pages_promotion/fenxiao/ranking_list','排行榜'],
['pages_promotion/fenxiao/child_fenxiao','分销商'],
['pages_tool/ai-chat/index','AI智能客服'],
['pages_tool/contact/contact','电子名片'],
['pages_tool/member/info','个人资料'],
['pages_tool/order/activist','退款'],
['pages_tool/login/login','登录'],
['pages_tool/weapp/order_shipping','小程序发货'],
['pages_tool/seal/structure','结构选型'],
['pages_tool/seal/medium/search','介质材料搜索'],
['pages_tool/seal/medium/detail','搜索结果'],
['pages_tool/cases/index','案例展示'],
['pages_tool/files/list','查看文件'],
['pages_tool/file-preview/file-preview','文件预览'],
];
function toLang(p) {
const parts = p.split('/');
if (parts[0] === 'pages_tool') return parts.slice(1).join('/');
if (parts[0] === 'pages_goods') return 'goods/' + parts.slice(1).join('/');
if (parts[0] === 'pages_member') return 'member/' + parts.slice(1).join('/');
if (parts[0] === 'pages_order') return 'order/' + parts.slice(1).join('/');
if (parts[0] === 'pages_promotion') {
if (['point','fenxiao','merch'].includes(parts[1])) return parts.slice(1).join('/');
return parts.slice(1).join('/');
}
return p;
}
const fs = require('fs');
const path = require('path');
for (const [p, hardcoded] of paths) {
const lp = toLang(p);
for (const loc of ['zh-cn','en-us']) {
const fp = path.join('lang', loc, lp + '.js');
if (!fs.existsSync(fp)) {
console.log(`MISSING ${loc}/${lp}`);
continue;
}
try {
const m = require(path.resolve(fp)).lang;
const title = m && m.title;
const status = title === undefined ? 'NO title' : (title === hardcoded ? 'ZH-only?' : 'OK');
console.log(`${loc}/${lp}.title = ${JSON.stringify(title)} [${status}]`);
} catch (e) {
console.log(`ERR ${loc}/${lp}: ${e.message}`);
}
}
}

View File

@@ -0,0 +1,264 @@
{
"uni_modules/uni-calendar/components/uni-calendar/uni-calendar.vue": [
{
"kind": "template",
"tag": "text",
"text": "取消",
"line": 6
},
{
"kind": "template",
"tag": "text",
"text": "确定",
"line": 7
},
{
"kind": "template",
"tag": "text",
"text": "{{ (nowDate.year || '') + '年' + (nowDate.month || '') + '月' }}",
"line": 15
},
{
"kind": "template",
"tag": "text",
"text": "日",
"line": 27
},
{
"kind": "template",
"tag": "text",
"text": "一",
"line": 28
},
{
"kind": "template",
"tag": "text",
"text": "二",
"line": 29
},
{
"kind": "template",
"tag": "text",
"text": "三",
"line": 30
},
{
"kind": "template",
"tag": "text",
"text": "四",
"line": 31
},
{
"kind": "template",
"tag": "text",
"text": "五",
"line": 32
},
{
"kind": "template",
"tag": "text",
"text": "六",
"line": 33
}
],
"pages_promotion/fenxiao/withdraw_list.vue": [
{
"kind": "js-text",
"match": "已转账",
"line": 45
},
{
"kind": "js-text",
"match": "待审核",
"line": 49
},
{
"kind": "js-text",
"match": "已审核",
"line": 53
},
{
"kind": "js-text",
"match": "已拒绝",
"line": 57
}
],
"uni_modules/mp-html/components/mp-html/node/node.vue": [
{
"kind": "js-text",
"match": "uni.showActionSheet({\n itemList: ['保存图片'],\n success: () => {\n const save = path => {\n uni.saveImageToPhotosAlbum({\n ",
"line": 289
},
{
"kind": "js-text",
"match": "uni.showToast({\n title: '链接已复制'\n })",
"line": 368
},
{
"kind": "js-text",
"match": "保存成功",
"line": 297
},
{
"kind": "js-text",
"match": "链接已复制",
"line": 369
}
],
"components-diy/diy-form.vue": [
{
"kind": "js-text",
"match": "uni.showToast({\n\t\t\t\t\t\t\ttitle: '提交成功',\n\t\t\t\t\t\t\ticon: 'success'\n\t\t\t\t\t\t})",
"line": 305
},
{
"kind": "js-text",
"match": "uni.showToast({\n\t\t\t\t\t\t\ttitle: res.message || '提交失败',\n\t\t\t\t\t\t\ticon: 'none'\n\t\t\t\t\t\t})",
"line": 314
},
{
"kind": "js-text",
"match": "提交成功",
"line": 306
}
],
"uni_modules/uni-count-down/components/uni-count-down/uni-count-down.vue": [
{
"kind": "template",
"tag": "view",
"text": "{{ showColon ? ':' : '时' }}",
"line": 19
},
{
"kind": "template",
"tag": "view",
"text": "{{ showColon ? ':' : '分' }}",
"line": 27
},
{
"kind": "template",
"tag": "view",
"text": "秒",
"line": 35
}
],
"components/ns-goods-recommend/ns-goods-recommend.vue": [
{
"kind": "js-text",
"match": "猜你喜欢",
"line": 13
},
{
"kind": "js-text",
"match": "购买",
"line": 20
}
],
"components-diy/diy-channel-list.vue": [
{
"kind": "js-text",
"match": "uni.showToast({\n\t\t\t\t\t\t\t\ttitle: '视频号仅支持在微信小程序环境中播放',\n\t\t\t\t\t\t\t\ticon: 'none',\n\t\t\t\t\t\t\t\tduration: 2000\n\t\t\t\t\t\t\t})",
"line": 207
},
{
"kind": "js-text",
"match": "视频号仅支持在微信小程序环境中播放",
"line": 208
}
],
"pages/index/public/js/index.js": [
{
"kind": "js-text",
"match": "uni.showModal({\n\t\t\t\t\ttitle: '版本信息',\n\t\t\t\t\tcontent: `提交 ID: ${v.commitId}\\n构建时间: ${v.buildTime}\\n${Config.baseUrl}`,\n\t\t\t\t\tshowCancel: false\n\t\t\t\t})",
"line": 160
},
{
"kind": "js-text",
"match": "版本信息",
"line": 161
}
],
"uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar.vue": [
{
"kind": "template",
"tag": "text",
"text": "{{ (nowDate.year||'') + ' 年 ' + ( nowDate.month||'') +' 月'}}",
"line": 13
},
{
"kind": "template",
"tag": "view",
"text": "确认",
"line": 87
}
],
"uni_modules/uni-number-box/components/uni-number-box/uni-number-box.vue": [
{
"kind": "js-text",
"match": "商品库存不足",
"line": 131
},
{
"kind": "js-text",
"match": "商品最少购买",
"line": 135
}
],
"common/js/mp-share-default.js": [
{
"kind": "js-text",
"match": "小程序分享",
"line": 85
}
],
"common/js/splash-state.js": [
{
"kind": "js-text",
"match": "uni.showLoading({ title: opts.title || '加载中...', mask: false })",
"line": 111
}
],
"pages/index/index.vue": [
{
"kind": "template",
"tag": "text",
"text": "{{ diyLoadErrorMsg || '网络异常,请稍后重试' }}",
"line": 8
}
],
"pages_goods/public/js/list.js": [
{
"kind": "js-text",
"match": "购买",
"line": 40
}
],
"pages_tool/help/list.vue": [
{
"kind": "js-text",
"match": "帮助中心",
"line": 82
}
],
"pages_tool/notice/list.vue": [
{
"kind": "js-text",
"match": "公告",
"line": 80
}
],
"pages_tool/pay/index.vue": [
{
"kind": "js-text",
"match": "未获取到支付信息!",
"line": 54
}
],
"uni_modules/uni-datetime-picker/components/uni-datetime-picker/time-picker.vue": [
{
"kind": "js-text",
"match": "日期和时间",
"line": 112
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,77 @@
{
"bundle_version": "1782901586",
"bundle_version": "1784767380",
"bundle_lang": "zh-cn",
"bundle_leaves": 2649,
"code_calls_total": 2677,
"code_unique_keys": 2083,
"found_in_bundle": 2082,
"bundle_leaves": 2659,
"code_calls_total": 2693,
"code_unique_keys": 2097,
"found_in_bundle": 2086,
"missing_summary": {
"TRULY_MISSING": 12,
"SYNTAX_BUG": 1
},
"missing_classified": {
"TRULY_MISSING": [
{
"key": "common.buyNow",
"file": "components\\ns-goods-recommend\\ns-goods-recommend.vue",
"reason": "not in bundle, not local"
},
{
"key": "wechatChannel.wechatMpOnly",
"file": "components-diy\\diy-channel-list.vue",
"reason": "not in bundle, not local"
},
{
"key": "common.submitSuccess",
"file": "components-diy\\diy-form.vue",
"reason": "not in bundle, not local"
},
{
"key": "submitFail",
"file": "components-diy\\diy-form.vue",
"reason": "not in bundle, not local"
},
{
"key": "diy.networkError",
"file": "pages\\index\\index.vue",
"reason": "not in bundle, not local"
},
{
"key": "versionInfo",
"file": "pages\\index\\public\\js\\index.js",
"reason": "not in bundle, not local"
},
{
"key": "commitId",
"file": "pages\\index\\public\\js\\index.js",
"reason": "not in bundle, not local"
},
{
"key": "buildTime",
"file": "pages\\index\\public\\js\\index.js",
"reason": "not in bundle, not local"
},
{
"key": "diy.networkError",
"file": "pages_goods\\category.vue",
"reason": "not in bundle, not local"
},
{
"key": "goods.detail.formEmptyTip",
"file": "pages_goods\\detail.vue",
"reason": "not in bundle, not local"
},
{
"key": "common.buyNow",
"file": "pages_goods\\public\\js\\list.js",
"reason": "not in bundle, not local"
},
{
"key": "pay.index.payInfoNotFound",
"file": "pages_tool\\pay\\index.vue",
"reason": "not in bundle, not local"
}
],
"SYNTAX_BUG": [
{
"key": "order.list.status.",
@@ -19,18 +82,8 @@
},
"placeholder_issues": [],
"data_lang_issues": {
"components\\ns-payment\\ns-payment.vue": [
"components.nsPayment.alipay"
],
"pages_promotion\\fenxiao\\apply.vue": [
"fenxiao.apply.pleaseEnterName"
],
"pages_promotion\\merch\\public\\js\\list.js": [
"merch.list.buyText"
],
"pages_tool\\ai-chat\\ai-chat-message.vue": [
"ai-chat.index.loadMoreHistory",
"ai-chat.index.defaultNickname"
]
},
"hardcoded_navbar_titles": [
@@ -121,6 +174,11 @@
"common.tabBar.cart",
"common.tabBar.member"
],
"local_only_keys": [],
"local_only_keys": [
"buildTime",
"commitId",
"submitFail",
"versionInfo"
],
"bundle_only_keys_in_unique": []
}

View File

@@ -0,0 +1,80 @@
#!/usr/bin/env python3
# 精筛 v2:剥离 HTML 注释和 JS 注释后,再扫硬编码中文(只关注真实会被渲染的)
import os, re, json
from collections import defaultdict
ROOT = r'D:\projects\shop-projects\frontend'
SKIP_DIR = {'node_modules', '.unpackage', 'unpackage', '.git', 'dist', 'stub-app-projects', 'lang', 'tests', 'docs', 'scripts', 'common/mixin'}
CN = re.compile(r'[\u4e00-\u9fff]')
LANG_RE = re.compile(r'\$lang\(')
TPL_RE = re.compile(r'<template[^>]*>([\s\S]*?)</template>', re.I)
TAG_RE = re.compile(r'<([a-zA-Z][\w-]*)\b[^>]*>([^<>\n]+)</\1>')
HTML_COMMENT_RE = re.compile(r'<!--[\s\S]*?-->')
JS_TRIGGERS = [
re.compile(r'uni\.show(?:Toast|Modal|ActionSheet|Loading)\s*\(\s*\{[\s\S]*?\}\s*\)', re.S),
re.compile(r'wx\.show(?:Toast|Modal|ActionSheet|Loading)\s*\(\s*\{[\s\S]*?\}\s*\)', re.S),
re.compile(r'\btitle\s*:\s*[\'"]([^\'"]*?)[\'"]'),
re.compile(r'\bcontent\s*:\s*[\'"]([^\'"]*?)[\'"]'),
re.compile(r'\btext\s*:\s*[\'"]([^\'"]*?)[\'"]'),
re.compile(r'\bplaceholder\s*:\s*[\'"]([^\'"]*?)[\'"]'),
re.compile(r'\balert\s*\(\s*[\'"]([^\'"]*?)[\'"]'),
re.compile(r'\bconfirm\s*\(\s*[\'"]([^\'"]*?)[\'"]'),
]
def strip_all_comments(text, is_vue=False):
text = HTML_COMMENT_RE.sub(lambda m: ' ' * len(m.group()), text)
text = re.sub(r'/\*[\s\S]*?\*/', lambda m: ' ' * len(m.group()), text)
text = re.sub(r'//[^\n]*', lambda m: ' ' * len(m.group()), text)
return text
results = defaultdict(list)
for dirpath, dirnames, filenames in os.walk(ROOT):
if any(s in dirpath.replace('\\', '/') for s in SKIP_DIR):
continue
for fn in filenames:
if not fn.endswith(('.vue', '.js')):
continue
fp = os.path.join(dirpath, fn)
rel = os.path.relpath(fp, ROOT).replace('\\', '/')
try:
content = open(fp, 'r', encoding='utf-8').read()
except UnicodeDecodeError:
continue
stripped = strip_all_comments(content)
if fn.endswith('.vue'):
for m in TPL_RE.finditer(stripped):
tpl = m.group(1)
for tag in TAG_RE.finditer(tpl):
inner = tag.group(2).strip()
if CN.search(inner) and not LANG_RE.search(inner):
line_no = stripped[:m.start(1) + tpl.find(inner)].count('\n') + 1
results[rel].append({'kind': 'template', 'tag': tag.group(1), 'text': inner[:160], 'line': line_no})
for trig in JS_TRIGGERS:
for m in trig.finditer(stripped):
text = (m.group(1) if m.lastindex else m.group(0))
if not CN.search(text):
continue
if LANG_RE.search(text):
continue
line_no = stripped[:m.start()].count('\n') + 1
results[rel].append({'kind': 'js-text', 'match': text[:160], 'line': line_no})
ranked = sorted(results.items(), key=lambda kv: -len(kv[1]))
print(f'total files: {len(ranked)}')
print(f'total hits: {sum(len(v) for v in ranked)}')
out = []
for f, hits in ranked[:60]:
out.append({'file': f, 'hits': len(hits), 'samples': hits[:3]})
print(json.dumps(out, ensure_ascii=False, indent=2))
with open(os.path.join(ROOT, 'scripts', 'hardcoded_cn_real.json'), 'w', encoding='utf-8') as f:
json.dump({k: v for k, v in ranked}, f, ensure_ascii=False, indent=2)
print('saved hardcoded_cn_real.json')

View File

@@ -64,6 +64,66 @@
]
}
]
},
{
"name": "components_diy",
"srcPath": "./ascf/ascf_build/components_diy",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
},
{
"name": "pages_goods",
"srcPath": "./ascf/ascf_build/pages_goods",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
},
{
"name": "pages_order",
"srcPath": "./ascf/ascf_build/pages_order",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
},
{
"name": "pages_promotion",
"srcPath": "./ascf/ascf_build/pages_promotion",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
},
{
"name": "pages_tool",
"srcPath": "./ascf/ascf_build/pages_tool",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
}
]
}

553
tests/lang.test.js Normal file
View File

@@ -0,0 +1,553 @@
/**
* lang.js 单元测试($lang 解析器)
*
* 运行方式:
* node --test tests/lang.test.js
*
* 测试覆盖(6 大维度):
* 1. 基础解析: 命中、未命中、默认值兜底、生产环境/开发环境返回差异
* 2. 命名参数: {reason} / {count} / {unit} 多参数、占位符安全性
* 3. 位置参数: {0} / {1} 兼容老式调用
* 4. 嵌套命名空间: diy.loadFailTpl / wechatChannel.wechatMpOnly / common.tabBar.home
* 5. 占位符替换: 多次替换、正则元字符、__proto__/constructor/prototype 过滤
* 6. 实际业务 key: loadFailTpl (com.js 下 diy)、tabBar.* 翻译对齐、common.buyNow vs common.submitSuccess
*
* 实现策略:
* lang.js 是 Vue 插件,挂在 Vue.prototype 上,依赖 store / uni / Config,
* 无法直接 import 跑测试。本测试按 util.test.js 的同款做法:
* 把要测的纯函数 (applyLangParams + 简化版 lang() 解析流程) 从 lang.js
* 源码里"剪切"出来放到本测试文件,与 lang.js 保持同步。
* 这样不依赖任何测试框架,任何 node 版本 (>=18) 都能跑通。
*/
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const path = require('path');
// =============================================================
// 被测源码区块(与 common/js/lang.js 保持同步)
// =============================================================
/** 与 lang.js 中 FORBIDDEN_KEYS 一致 */
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
/**
* @see lang.js#applyLangParams
* 命名参数 + 位置参数 替换
*/
function applyLangParams(value, args) {
if (args.length === 0) return value;
let result = String(value);
const firstArg = args[0];
const isNamed = firstArg !== null && typeof firstArg === 'object' && !Array.isArray(firstArg);
if (isNamed) {
const map = firstArg;
for (const key in map) {
if (!Object.prototype.hasOwnProperty.call(map, key)) continue;
if (FORBIDDEN_KEYS.has(key)) continue;
const safeKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
result = result.replace(new RegExp('\\{' + safeKey + '\\}', 'g'), String(map[key]));
}
} else {
for (let i = 0; i < args.length; i++) {
result = result.replace(new RegExp('\\{' + i + '\\}', 'g'), String(args[i]));
}
}
return result;
}
/**
* 简化版 lang() 解析流程(模拟 lang.js 主流程的"按 . 切分 + 逐层查找 + 默认值 + 占位符替换"核心)
*
* 与 lang.js 主流程差异:
* - 不处理 route 缓存、_mergedLangCache、_fieldResultCache 等(那些是性能优化,不影响语义)
* - 不处理 overrideCache(那是远端覆写机制,需要单独 mock)
* - 不处理递归保护、isRemoteOnly 短路(那是运行时 hook)
* - 但"按 . 切分 → 逐层查找 → 未命中走默认值 → 占位符替换" 这一核心路径与 lang.js 完全一致
*
* @param {Object} mergedLang 合并后的语言包对象(顶层已是 common namespace)
* @param {string} field 字段路径,如 'diy.loadFailTpl'
* @param {...any} args 可选参数(命名参数或位置参数)
* @returns {string}
*/
function fakeLang(mergedLang, field, ...args) {
const hasDefault = args.length > 0 && typeof args[0] === 'string' && args[0] !== '';
const arr = field.split('.');
let value;
if (arr.length > 1) {
let temp = mergedLang;
let found = true;
for (const key of arr) {
if (temp && typeof temp === 'object' && temp[key] !== undefined) {
temp = temp[key];
} else {
found = false;
break;
}
}
value = found && typeof temp === 'string' ? temp : field;
} else {
const raw = mergedLang[field];
value = raw !== undefined && typeof raw === 'string' ? raw : field;
}
// 未命中 → 默认值兜底
if (value === field) {
if (hasDefault) return args[0];
return ''; // 生产环境返回空串(对齐 lang.js 的 __DEV__ === false 分支)
}
// 占位符替换
if (args.length > 0) {
value = applyLangParams(value, args);
}
return value;
}
/**
* 直接 require 实际语言包,确保我们测的就是项目里那份 zh-cn / en-us
*/
const zh = require(path.resolve(__dirname, '..', 'lang', 'zh-cn', 'common.js')).lang;
const en = require(path.resolve(__dirname, '..', 'lang', 'en-us', 'common.js')).lang;
// =============================================================
// 1. 基础解析:命中 / 未命中 / 默认值
// =============================================================
test('基础解析: 命中已有 key 返回翻译结果', () => {
assert.strictEqual(
fakeLang(zh, 'diy.retry'),
'重试'
);
assert.strictEqual(
fakeLang(en, 'diy.retry'),
'Retry'
);
});
test('基础解析: 中英对照 一致性', () => {
const pairs = [
['diy.retry', '重试', 'Retry'],
['diy.noDiyData', '未配置自定义页面数据', 'Custom page data not configured'],
['customer.weChatKefu', '微信客服', 'WeChat Customer Service'],
];
for (const [key, zhExpected, enExpected] of pairs) {
assert.strictEqual(fakeLang(zh, key), zhExpected, `zh ${key}`);
assert.strictEqual(fakeLang(en, key), enExpected, `en ${key}`);
}
});
test('基础解析: 未命中且无默认值 → 生产环境返回空串', () => {
assert.strictEqual(
fakeLang(zh, 'totally.fake.missing.key'),
'',
'未命中且无默认值时,生产环境(本测试视角)返回空串,避免展示 raw key'
);
});
test('基础解析: 未命中但有默认值 → 返回默认值', () => {
assert.strictEqual(
fakeLang(zh, 'totally.fake.missing.key', '兜底文案'),
'兜底文案'
);
assert.strictEqual(
fakeLang(en, 'totally.fake.missing.key', 'Fallback Text'),
'Fallback Text'
);
});
test('基础解析: 默认值是空串时被忽略(对齐 lang.js hasDefault 判断)', () => {
// lang.js: hasDefault = typeof args[1] === 'string' && args[1] !== ''
// args[0] 是 string '' 时不算默认值
assert.strictEqual(
fakeLang(zh, 'totally.fake.missing.key', ''),
''
);
});
test('基础解析: 默认值必须是字符串,非字符串不走默认值兜底', () => {
// args[0] = { foo: 1 } 是命名参数,不走默认值路径
// 这里用一个不存在 key 验证:返回空串(因为命名参数替换失败)
assert.strictEqual(
fakeLang(zh, 'totally.fake.missing.key', { foo: 'bar' }),
''
);
});
test('基础解析: 中间路径上某节点非对象(命中 leaf 是 number/boolean)', () => {
// 模拟 leaf 不是 string 的场景
const stub = { foo: { bar: 123 } };
// lang.js 实际行为:中间节点上 leaf 不是 string 时,value 设为 field,
// 但因为没有默认值兜底且后续判定 value === field → 返回空串
assert.strictEqual(
fakeLang(stub, 'foo.bar'),
'',
'非 string 类型 leaf 当成未命中,且无默认值 → 返回空串'
);
// 给默认值时返回默认值
assert.strictEqual(
fakeLang(stub, 'foo.bar', 'fallback'),
'fallback'
);
});
// =============================================================
// 2. 命名参数:本次修复的核心场景
// =============================================================
test('命名参数: diy.loadFailTpl + { reason } 是核心场景', () => {
// 注意:zh-cn 实际文案是中文标点「:」,不是英文「:」
assert.strictEqual(
fakeLang(zh, 'diy.loadFailTpl', { reason: '网络连接超时' }),
'页面数据加载失败:网络连接超时,请稍后重试'
);
assert.strictEqual(
fakeLang(en, 'diy.loadFailTpl', { reason: 'Network timeout' }),
'Failed to load page data: Network timeout, please try again later'
);
});
test('命名参数: 多参数同时替换', () => {
// goods.detail.purchaseLimit = '该商品每人限购{count}{unit}'
const zhGoods = require(path.resolve(__dirname, '..', 'lang', 'zh-cn', 'goods', 'detail.js')).lang;
const enGoods = require(path.resolve(__dirname, '..', 'lang', 'en-us', 'goods', 'detail.js')).lang;
assert.strictEqual(
fakeLang(zhGoods, 'purchaseLimit', { count: 5, unit: '件' }),
'该商品每人限购5件'
);
assert.strictEqual(
fakeLang(enGoods, 'purchaseLimit', { count: 5, unit: 'pcs' }),
'Limit 5 pcs per person',
'en-us 文案是 "Limit {count} {unit} per person",顺序替换 {count}=5 / {unit}=pcs'
);
// 多参数 + 多占位符 替换
assert.strictEqual(
fakeLang(zhGoods, 'purchaseLimitWithPurchased', { count: 5, unit: '件', purchased: 2 }),
'该商品每人限购5件,您已购买了2件'
);
});
test('命名参数: 多次出现的占位符全部替换', () => {
const stub = { msg: '欢迎{user},{user} 你好' };
assert.strictEqual(
fakeLang(stub, 'msg', { user: '张三' }),
'欢迎张三,张三 你好'
);
});
test('命名参数: 占位符的值含正则元字符不会注入', () => {
const stub = { msg: '下载 {url} 完成' };
assert.strictEqual(
fakeLang(stub, 'msg', { url: 'a.b+c*d(e)f' }),
'下载 a.b+c*d(e)f 完成'
);
assert.strictEqual(
fakeLang(stub, 'msg', { url: '$1\\2{3}' }),
'下载 $1\\2{3} 完成'
);
});
test('命名参数: 占位符的值含 .*+?^${}()|[]\\ 等全部正确转义', () => {
const stub = { msg: '前缀 {x} 后缀' };
const r1 = fakeLang(stub, 'msg', { x: '$1' });
assert.strictEqual(r1, '前缀 $1 后缀', 'value 端的 $1 不应被吃成反向引用');
const r2 = fakeLang(stub, 'msg', { x: '\\n' });
assert.strictEqual(r2, '前缀 \\n 后缀');
});
test('命名参数: 危险 key __proto__/constructor/prototype 被过滤', () => {
const stub = { msg: 'hello {__proto__} {constructor} {prototype} world' };
const out = fakeLang(stub, 'msg', { __proto__: 'A', constructor: 'B', prototype: 'C', safe: 'OK' });
assert.strictEqual(out, 'hello {__proto__} {constructor} {prototype} world');
});
test('命名参数: 没有占位符的纯文案 + 命名参数(多余参数被忽略)', () => {
const stub = { msg: '你好' };
assert.strictEqual(
fakeLang(stub, 'msg', { foo: 'bar' }),
'你好'
);
});
test('命名参数: 对象参数里没提供的占位符 → 字面残留', () => {
const stub = { msg: '你好 {name},你 {count} 岁' };
assert.strictEqual(
fakeLang(stub, 'msg', { name: 'Tom' }),
'你好 Tom,你 {count} 岁'
);
});
// =============================================================
// 3. 位置参数: {0} / {1} 老式调用兼容
// =============================================================
test('位置参数: {0} 形式', () => {
const stub = { msg: '欢迎第 {0} 位用户' };
assert.strictEqual(
fakeLang(stub, 'msg', '张三'),
'欢迎第 张三 位用户'
);
});
test('位置参数: 多位置参数 {0}/{1}/{2}', () => {
const stub = { msg: '{0} 在 {1} 出生,今年 {2} 岁' };
assert.strictEqual(
fakeLang(stub, 'msg', 'Alice', 'Beijing', 18),
'Alice 在 Beijing 出生,今年 18 岁'
);
});
test('位置参数: 数组会被识别为位置参数而非命名参数', () => {
// args[0] = ['a', 'b'] 是 array → 走位置参数分支
// 但 args.length === 1(只传了一个数组),所以只有 {0} 被替换,{1} 残留
const stub = { msg: '{0} {1}' };
assert.strictEqual(
fakeLang(stub, 'msg', ['a', 'b']),
'a,b {1}'
);
});
test('位置参数: null 走位置参数分支,被 String() 转为 "null"', () => {
const stub = { msg: '{0}' };
// args[0] === null: applyLangParams 中 firstArg !== null 判定为 false,
// 走位置参数分支, String(null) = 'null',替换 {0}
assert.strictEqual(
fakeLang(stub, 'msg', null),
'null'
);
});
// =============================================================
// 4. 嵌套命名空间: 多层 . 路径
// =============================================================
test('嵌套命名空间: order.payment.title (3 层)', () => {
const orderZh = require(path.resolve(__dirname, '..', 'lang', 'zh-cn', 'order', 'payment.js')).lang;
const orderEn = require(path.resolve(__dirname, '..', 'lang', 'en-us', 'order', 'payment.js')).lang;
// 注意:页面语言包整体就是 order.payment namespace,
// 所以 $lang('title') 也能命中(归一化逻辑),但显式路径是 $lang('order.payment.title')
assert.strictEqual(fakeLang(orderZh, 'title'), '确认订单');
assert.strictEqual(fakeLang(orderEn, 'title'), 'Confirm Order');
});
test('嵌套命名空间: wechatChannel.wechatMpOnly (本次新增)', () => {
assert.strictEqual(
fakeLang(zh, 'wechatChannel.wechatMpOnly'),
'视频号仅支持在微信小程序环境中播放'
);
assert.strictEqual(
fakeLang(en, 'wechatChannel.wechatMpOnly'),
'WeChat Channel only plays in WeChat Mini Program'
);
});
test('嵌套命名空间: common.tabBar.* (本次修正)', () => {
assert.strictEqual(fakeLang(zh, 'tabBar.home'), '首页');
assert.strictEqual(fakeLang(zh, 'tabBar.category'), '分类');
assert.strictEqual(fakeLang(zh, 'tabBar.cart'), '购物车');
assert.strictEqual(fakeLang(zh, 'tabBar.member'), '个人中心');
// en-us 顶层 tabBar(本次修复后)
assert.strictEqual(fakeLang(en, 'tabBar.home'), 'Home');
assert.strictEqual(fakeLang(en, 'tabBar.category'), 'Category');
assert.strictEqual(fakeLang(en, 'tabBar.cart'), 'Cart');
assert.strictEqual(fakeLang(en, 'tabBar.member'), 'Me');
});
test('嵌套命名空间: common.tabBar.* 中英文一致性(本次修复的核心断言)', () => {
const tabs = ['home', 'category', 'cart', 'member'];
for (const tab of tabs) {
const zhVal = fakeLang(zh, `tabBar.${tab}`);
const enVal = fakeLang(en, `tabBar.${tab}`);
assert.notStrictEqual(enVal, zhVal, `en-us tabBar.${tab} 不能等于 zh-cn 值`);
assert.ok(enVal.length > 0, `en-us tabBar.${tab} 必须有翻译`);
// 英文 tab 应是 Title Case,而不是全小写英文单词
assert.ok(
/^[A-Z]/.test(enVal) || enVal === 'Me',
`en-us tabBar.${tab}="${enVal}" 应以大写字母开头(或等于 'Me')`
);
}
});
test('嵌套命名空间: common 顶层键 vs common.common 子对象双层结构', () => {
// 历史遗留:common.js 的 lang 对象整体即 common namespace,
// 但对象内部又有一个 'common: {...}' 子对象
assert.strictEqual(fakeLang(zh, 'buyNow'), '立即购买');
assert.strictEqual(fakeLang(zh, 'common.goodsRecommendTitle'), '猜你喜欢');
assert.strictEqual(fakeLang(en, 'buyNow'), 'Buy Now');
assert.strictEqual(fakeLang(en, 'common.goodsRecommendTitle'), 'Guess you like');
});
test('嵌套命名空间: 双重 common.<key> 误用回归保护(扫所有顶层 key)', () => {
// 防御:common.js 的 lang 对象整体即 common namespace,
// 顶层 key 不应再带 'common.' 前缀,否则会找不到
// 这个测试是 2026-07-23 真实 console 报错后追加的回归保护
//
// 策略:自动枚举 zh/en 顶层所有 key,
// 对每个 key,验证 fakeLang(zh, 'common.' + key) === ''
// —— 这样不管以后谁写出 common.<topLevelKey>,测试都会自动 fail
// 收集两包顶层 key 的并集(只要 zh 或 en 一方有就检查)
const topKeys = new Set();
for (const k of Object.keys(zh)) {
if (typeof zh[k] === 'string') topKeys.add(k);
}
for (const k of Object.keys(en)) {
if (typeof en[k] === 'string') topKeys.add(k);
}
const mistakes = [];
for (const k of topKeys) {
// 注意:跳过常见会被合法命中的 key(common 里的合法子对象)
// 这些 key 在 zh.common / en.common 子对象里也存在(双 common 合法)
if (zh.common && typeof zh.common[k] === 'string') continue;
if (en.common && typeof en.common[k] === 'string') continue;
const zhHit = fakeLang(zh, 'common.' + k);
const enHit = fakeLang(en, 'common.' + k);
if (zhHit !== '' || enHit !== '') {
mistakes.push({ k, zhHit, enHit });
}
}
if (mistakes.length > 0) {
console.error('以下 key 在顶层,但用 common.<key> 调用居然命中了:');
for (const m of mistakes) {
console.error(` common.${m.k} → zh="${m.zhHit}" en="${m.enHit}"`);
}
}
assert.strictEqual(mistakes.length, 0,
'不应有任何 common.<key> 调用能命中顶层 key(应该是空串)');
});
test('嵌套命名空间: 双重 common.buyNow 是不存在的(防止误用回归)', () => {
// 历史 bug:common.buyNow 被当作 fallback,实际 key 是顶层 buyNow
assert.strictEqual(
fakeLang(zh, 'common.buyNow'),
'',
'common.buyNow 双前缀不存在,真实调用必须用 $lang("buyNow")'
);
assert.strictEqual(
fakeLang(en, 'common.buyNow'),
'',
'en-us 同理,common.buyNow 双前缀不存在'
);
// 同时验证顶层 buyNow 存在
assert.strictEqual(fakeLang(zh, 'buyNow'), '立即购买');
assert.strictEqual(fakeLang(en, 'buyNow'), 'Buy Now');
});
test('嵌套命名空间: 4 层路径 diy.networkErrorTitle', () => {
assert.strictEqual(fakeLang(zh, 'diy.networkErrorTitle'), '提示');
assert.strictEqual(fakeLang(en, 'diy.networkErrorTitle'), 'Notice');
});
// =============================================================
// 5. 占位符替换细节
// =============================================================
test('占位符替换: 占位符的值含数字时,String() 强制转换', () => {
const stub = { msg: '价格 {price} 元' };
assert.strictEqual(
fakeLang(stub, 'msg', { price: 99 }),
'价格 99 元'
);
assert.strictEqual(
fakeLang(stub, 'msg', { price: 99.5 }),
'价格 99.5 元'
);
});
test('占位符替换: 占位符的值为 undefined 时,String(undefined) = "undefined"', () => {
const stub = { msg: 'name={name}' };
assert.strictEqual(
fakeLang(stub, 'msg', { name: undefined }),
'name=undefined'
);
});
test('占位符替换: 占位符的值为 0/false 时,也会被替换(因为走 String 强制转换)', () => {
const stub = { msg: '数量 {n},启用 {on}' };
assert.strictEqual(
fakeLang(stub, 'msg', { n: 0, on: false }),
'数量 0,启用 false'
);
});
test('占位符替换: 占位符的值为空字符串', () => {
const stub = { msg: '前 [{x}] 后' };
assert.strictEqual(
fakeLang(stub, 'msg', { x: '' }),
'前 [] 后'
);
});
// =============================================================
// 6. 真实业务 key 回归测试
// =============================================================
test('回归: 本次修复新增的 8 个 key 在两包中都有', () => {
const newKeys = [
'diy.networkError',
'wechatChannel.wechatMpOnly',
'submitSuccess',
'submitFail',
'versionInfo',
'commitId',
'buildTime',
'loading',
];
for (const key of newKeys) {
const zhVal = fakeLang(zh, key);
const enVal = fakeLang(en, key);
assert.ok(typeof zhVal === 'string' && zhVal.length > 0, `zh ${key}="${zhVal}"`);
assert.ok(typeof enVal === 'string' && enVal.length > 0, `en ${key}="${enVal}"`);
assert.notStrictEqual(zhVal, enVal, `${key} 中英文必须不同`);
}
});
test('回归: 核心占位符场景 diy.loadFailTpl 不传参数会字面残留占位符', () => {
// 已知行为:reason 必填,如果不传,确实会字面残留(对齐 lang.js 行为)
// 实际 zh-cn 文案是中文标点「:」
const stub1 = { ...zh };
const out1 = fakeLang(stub1, 'diy.loadFailTpl');
// lang.js: value === field? 否(命中了 diy.loadFailTpl 的 leaf)→ 走占位符替换
// applyLangParams 的 args.length === 0 → 直接返回 value
assert.strictEqual(
out1,
'页面数据加载失败:{reason},请稍后重试',
'不传参数会字面残留占位符,这是已知行为'
);
// 但传了 reason 必须正确替换
assert.strictEqual(
fakeLang(zh, 'diy.loadFailTpl', { reason: 'X' }),
'页面数据加载失败:X,请稍后重试'
);
});
test('回归: zh-cn/en-us common.js 同 key 同结构(key 一致性)', () => {
const zhKeys = Object.keys(zh).sort();
const enKeys = Object.keys(en).sort();
assert.deepStrictEqual(
zhKeys,
enKeys,
'zh-cn 与 en-us common.js 顶层 key 必须完全一致(否则会有 missing_key 风险)'
);
});
test('回归: 抽 zh/en 内 common 子对象 key 一致性', () => {
const zhCommon = Object.keys(zh.common || {}).sort();
const enCommon = Object.keys(en.common || {}).sort();
assert.deepStrictEqual(zhCommon, enCommon, 'zh/en common 子对象 key 一致');
});