Compare commits

..

1 Commits

Author SHA1 Message Date
3a88941828 build(vue.config): 修复生产构建门禁的node执行路径问题
将执行test-gate的命令从硬编码的"node"改为使用process.execPath,避免依赖PATH环境变量查找node二进制,解决HBuilderX等IDE下PATH无node条目导致的构建失败问题,同时打印当前使用的node版本
2026-07-20 15:38:15 +08:00
12 changed files with 9 additions and 896 deletions

View File

@@ -1,98 +0,0 @@
/**
* 通用 mixin - 推荐系统 diy 组件的内部 ajax 数据获取
*
* 用法:
* import recommendData from '@/common/js/recommend-diy-data.js';
* export default { mixins: [recommendData], ... }
*
* 它会:
* 1. 在 mounted 时异步调 /recommend/api/home/homeSummary 一次
* 2. 暴露 this.recommendInfo 给组件用
* 3. 处理失败(token 失效、未登录)显示占位
*
* 为什么不用 vuex / pinia:
* diy 组件可能在多个页面多次实例化,各自独立拉一次,可避免跨页面污染。
*/
import http from '@/common/js/http.js';
import { $lang } from '@/common/js/lang.js';
export default {
data() {
return {
recommendInfo: null, // 后端原始响应 data
recommendLoading: false,
recommendError: '',
_lastFetchTs: 0, // 防抖:同时多次 mounted 不重复请求
};
},
computed: {
// 余额(可提现 + 冻结 + 累计收益)
balance() {
return (this.recommendInfo && this.recommendInfo.balance) || {};
},
// 累计统计
statistics() {
return (this.recommendInfo && this.recommendInfo.statistics) || {};
},
// 推荐码
code() {
return (this.recommendInfo && this.recommendInfo.code) || {};
},
// 考察期
probation() {
return (this.recommendInfo && this.recommendInfo.probation) || {};
},
// 最近推荐
recentRecords() {
return (this.recommendInfo && this.recommendInfo.recent_records) || [];
},
// 奖励规则
rewardRules() {
return (this.recommendInfo && this.recommendInfo.reward_rules) || [];
},
// 是否登录(可避免显示错误的余额)
hasRecommendData() {
return !!this.recommendInfo;
},
},
mounted() {
this.fetchRecommendInfo();
},
methods: {
// 异步拉首页聚合数据(去抖,每 10s 最多一次)
fetchRecommendInfo(force = false) {
const now = Date.now();
if (!force && now - this._lastFetchTs < 10000) {
return;
}
this._lastFetchTs = now;
this.recommendLoading = true;
this.recommendError = '';
// 通过 http 通用方法走 token(与全局其他请求一致)
http.get('/recommend/api/home/homeSummary').then(res => {
this.recommendLoading = false;
if (res && res.code === 0 && res.data) {
this.recommendInfo = res.data;
} else {
// 未登录或 token 失败,留 recommendInfo=null 让模板显示占位
this.recommendError = (res && res.message) || $lang ? $lang('components-diy.diy-recommend.loadFail') : '加载失败';
}
}).catch(err => {
this.recommendLoading = false;
this.recommendError = (err && (err.errMsg || err.message)) || 'network error';
});
},
// 刷新(用户下拉触发)
refreshRecommendInfo() {
return this.fetchRecommendInfo(true);
},
// 数字格式化工具
formatMoney(n) {
if (n == null || n === '') return '0';
const num = parseFloat(n);
if (isNaN(num)) return '0';
return num.toLocaleString('zh-CN', { maximumFractionDigits: 2 });
},
},
};

View File

@@ -1,82 +0,0 @@
<template>
<view data-component-name="diy-recommend-action" class="diy-recommend-action" :style="warpCss">
<!-- 主按钮:立即提现 -->
<view class="primary-btn" hover-class="btn-hover" @tap="goWithdraw">
<text>{{ $lang('components-diy.diy-recommend-action.withdrawNow') }}</text>
</view>
<!-- 副按钮:提现记录(横排两列) -->
<view class="secondary-row">
<view class="secondary-btn" hover-class="btn-hover" @tap="goWithdrawList">
<text class="iconfont icon-calendar"></text>
<text>{{ $lang('components-diy.diy-recommend-action.withdrawList') }}</text>
</view>
<view class="secondary-btn" hover-class="btn-hover" @tap="goApply">
<text class="iconfont icon-edit"></text>
<text>{{ $lang('components-diy.diy-recommend-action.applyEnterprise') }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'diy-recommend-action',
computed: {
warpCss() {
return {
padding: '24rpx',
backgroundColor: '#fff',
};
},
},
methods: {
goWithdraw() {
uni.navigateTo({ url: '/pages_recommend/withdraw/apply' });
},
goWithdrawList() {
uni.navigateTo({ url: '/pages_recommend/withdraw/list' });
},
goApply() {
uni.navigateTo({ url: '/pages_recommend/apply/apply' });
},
},
};
</script>
<style lang="scss" scoped>
.diy-recommend-action {
.primary-btn {
background: linear-gradient(135deg, #1976d2, #0d47a1);
color: #fff;
text-align: center;
border-radius: 50rpx;
padding: 24rpx 0;
font-size: 32rpx;
font-weight: 600;
letter-spacing: 4rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(25,118,210,0.32);
}
.secondary-row {
display: flex;
gap: 16rpx;
}
.secondary-btn {
flex: 1;
background: #f5f5f5;
color: #444;
text-align: center;
border-radius: 12rpx;
padding: 20rpx 0;
font-size: 26rpx;
.iconfont {
margin-right: 8rpx;
color: #1976d2;
}
}
}
.btn-hover {
opacity: 0.85;
}
</style>

View File

@@ -1,108 +0,0 @@
<template>
<view data-component-name="diy-recommend-balance" class="diy-recommend-balance" :style="warpCss">
<view class="balance-card">
<view class="balance-row">
<view class="balance-half available">
<text class="label">{{ $lang('components-diy.diy-recommend-balance.available') }}</text>
<text class="big-num price-font">¥{{ formatMoney(balance.available) }}</text>
<text class="sub-tip" v-if="balance.available > 0 && probation.total_days">
{{ $lang('components-diy.diy-recommend-balance.passedProbation') }}
</text>
</view>
<view class="balance-half probation">
<text class="label">{{ $lang('components-diy.diy-recommend-balance.frozen') }}</text>
<text class="big-num price-font">¥{{ formatMoney(balance.frozen) }}</text>
<text class="sub-tip" v-if="probation.remaining_days > 0">
{{ $lang('components-diy.diy-recommend-balance.probationDays', { days: probation.remaining_days }) }}
</text>
</view>
</view>
<!-- 累计总收益(底部) -->
<view class="total-row">
<text class="label">{{ $lang('components-diy.diy-recommend-balance.totalEarned') }}</text>
<text class="big-num price-font">¥{{ formatMoney(balance.total_earned) }}</text>
</view>
</view>
</view>
</template>
<script>
import recommendData from '@/common/js/recommend-diy-data.js';
export default {
name: 'diy-recommend-balance',
mixins: [recommendData],
computed: {
warpCss() {
return {
padding: '24rpx',
backgroundColor: '#fff',
};
},
},
};
</script>
<style lang="scss" scoped>
.diy-recommend-balance {
.balance-card {
background: linear-gradient(180deg, #d9f2ff, #f0f9ff);
border-radius: 16rpx;
padding: 32rpx 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0,102,204,0.08);
}
.balance-row {
display: flex;
justify-content: space-between;
gap: 24rpx;
margin-bottom: 24rpx;
}
.balance-half {
flex: 1;
display: flex;
flex-direction: column;
padding: 16rpx 20rpx;
border-radius: 12rpx;
background: rgba(255,255,255,0.65);
&.available {
.big-num { color: #1976d2; }
}
&.probation {
background: rgba(255,243,224,0.85);
.big-num { color: #e65100; }
}
.label {
font-size: 24rpx;
color: #666;
}
.big-num {
font-size: 44rpx;
font-weight: 700;
margin: 8rpx 0;
}
.sub-tip {
font-size: 22rpx;
color: #888;
}
}
.total-row {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1rpx solid #e0e0e0;
padding-top: 16rpx;
.label {
font-size: 26rpx;
color: #444;
}
.big-num {
font-size: 36rpx;
font-weight: 600;
color: #d81b60;
}
}
}
</style>

View File

@@ -1,167 +0,0 @@
<template>
<view data-component-name="diy-recommend-code" class="diy-recommend-code" :style="warpCss">
<!-- 标题 -->
<view class="title-line">
<view class="icon iconfont icon-recommend-prize"></view>
<text class="title">{{ $lang('components-diy.diy-recommend-code.myCode') }}</text>
</view>
<!-- 卡片 -->
<view class="code-card" v-if="code.code">
<view class="code-text" @tap="copyCode">{{ code.code }}</view>
<view class="copy-btn" @tap="copyCode">
<text class="iconfont icon-copy"></text>
<text>{{ $lang('components-diy.diy-recommend-code.copy') }}</text>
</view>
</view>
<view class="code-card placeholder" v-else>
<text class="placeholder-text">{{ recommendError || $lang('components-diy.diy-recommend-code.loading') }}</text>
</view>
<!-- 复制 toast -->
<view v-if="copyToast" class="copy-toast">
{{ $lang('components-diy.diy-recommend-code.copySuccess') }}
</view>
<!-- 分享链接区 -->
<view class="share-bar" v-if="code.share_link">
<text class="iconfont icon-share"></text>
<text class="share-tip">{{ $lang('components-diy.diy-recommend-code.shareTip') }}</text>
<view class="share-link-card" @tap="copyShareLink">
<text class="link-text">{{ code.share_link }}</text>
</view>
</view>
</view>
</template>
<script>
import recommendData from '@/common/js/recommend-diy-data.js';
export default {
name: 'diy-recommend-code',
mixins: [recommendData],
data() {
return {
copyToast: false,
_toastTimer: null,
};
},
computed: {
warpCss() {
return {
padding: (this.value && this.value.paddingTop || 16) + 'rpx ' +
(this.value && this.value.paddingLeft || 24) + 'rpx',
backgroundColor: (this.value && this.value.componentBgColor) || 'transparent',
};
},
},
methods: {
copyCode() {
if (!this.code.code) return;
uni.setClipboardData({
data: this.code.code,
success: () => this.showCopyToast(),
});
},
copyShareLink() {
if (!this.code.share_link) return;
uni.setClipboardData({
data: this.code.share_link,
success: () => this.showCopyToast(),
});
},
showCopyToast() {
this.copyToast = true;
if (this._toastTimer) clearTimeout(this._toastTimer);
this._toastTimer = setTimeout(() => { this.copyToast = false; }, 1500);
},
},
};
</script>
<style lang="scss" scoped>
.diy-recommend-code {
.title-line {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.icon {
font-size: 32rpx;
color: #ff9800;
margin-right: 12rpx;
}
.title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
}
.code-card {
background: linear-gradient(135deg, #fff3e0, #ffe0b2);
border: 2rpx dashed #ff9800;
border-radius: 12rpx;
padding: 36rpx 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
.code-text {
font-size: 56rpx;
font-weight: 700;
color: #e65100;
letter-spacing: 4rpx;
font-family: Menlo, Monaco, monospace;
}
.copy-btn {
display: flex;
flex-direction: column;
align-items: center;
background: #ff9800;
color: #fff;
border-radius: 8rpx;
padding: 12rpx 20rpx;
font-size: 24rpx;
.iconfont { font-size: 30rpx; margin-bottom: 4rpx; }
}
&.placeholder {
background: #f5f5f5;
border-color: #ccc;
justify-content: center;
min-height: 100rpx;
.placeholder-text {
color: #999;
font-size: 28rpx;
}
}
}
.share-bar {
margin-top: 16rpx;
.icon-share { font-size: 28rpx; color: #ff9800; margin-right: 8rpx; }
.share-tip {
font-size: 24rpx;
color: #666;
}
.share-link-card {
margin-top: 12rpx;
background: #fafafa;
border-radius: 8rpx;
padding: 16rpx 20rpx;
.link-text {
font-size: 24rpx;
color: #444;
word-break: break-all;
}
}
}
.copy-toast {
position: fixed;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.7);
color: #fff;
padding: 16rpx 32rpx;
border-radius: 8rpx;
font-size: 28rpx;
z-index: 9999;
}
}
</style>

View File

@@ -1,147 +0,0 @@
<template>
<view data-component-name="diy-recommend-records" class="diy-recommend-records" :style="warpCss">
<view class="header-row">
<text class="title">{{ $lang('components-diy.diy-recommend-records.recent') }}</text>
<text class="link" @tap="goAllRecords">{{ $lang('components-diy.diy-recommend-records.viewAll') }} ></text>
</view>
<view v-if="recentRecords.length === 0" class="empty">
<text>{{ $lang('components-diy.diy-recommend-records.empty') }}</text>
</view>
<view v-else class="record-list">
<view v-for="(r, i) in recentRecords" :key="r.id || i" class="record-item">
<view class="left">
<view class="avatar"></view>
<view class="info">
<text class="name">{{ r.company_name || r.id }}</text>
<text class="time">{{ formatTime(r.apply_time) }}</text>
</view>
</view>
<view class="right">
<text class="amount price-font" v-if="r.amount_text">{{ r.amount_text }}</text>
<view class="status-tag" :class="'status-' + r.status">{{ r.status_text }}</view>
</view>
</view>
</view>
</view>
</template>
<script>
import recommendData from '@/common/js/recommend-diy-data.js';
export default {
name: 'diy-recommend-records',
mixins: [recommendData],
computed: {
warpCss() {
return {
padding: '16rpx 24rpx 24rpx',
backgroundColor: '#fff',
};
},
},
methods: {
goAllRecords() {
uni.navigateTo({ url: '/pages_recommend/records/list' });
},
formatTime(ts) {
if (!ts) return '';
const d = new Date(parseInt(ts) * 1000);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${dd}`;
},
},
};
</script>
<style lang="scss" scoped>
.diy-recommend-records {
.header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.link {
font-size: 24rpx;
color: #999;
}
}
.empty {
padding: 40rpx;
text-align: center;
color: #999;
font-size: 24rpx;
}
.record-list {
.record-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child { border-bottom: none; }
}
.left {
display: flex;
align-items: center;
flex: 1;
overflow: hidden;
}
.avatar {
width: 64rpx;
height: 64rpx;
border-radius: 12rpx;
background: linear-gradient(135deg, #bbdefb, #64b5f6);
margin-right: 16rpx;
flex-shrink: 0;
}
.info {
flex: 1;
min-width: 0;
}
.name {
font-size: 26rpx;
color: #333;
font-weight: 500;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
display: block;
}
.time {
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
}
.right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 6rpx;
}
.amount {
font-size: 30rpx;
font-weight: 700;
color: #e65100;
}
.status-tag {
font-size: 22rpx;
padding: 4rpx 14rpx;
border-radius: 16rpx;
background: #f5f5f5;
color: #666;
&.status-0 { background: #fff3e0; color: #e65100; } // 待审核
&.status-1 { background: #e3f2fd; color: #1976d2; } // 审核通过(待缴费)
&.status-2 { background: #e8f5e9; color: #4caf50; } // 审核通过(已缴费)
&.status-3 { background: #ffebee; color: #c62828; } // 审核不通过
}
}
}
</style>

View File

@@ -1,103 +0,0 @@
<template>
<view data-component-name="diy-recommend-rules" class="diy-recommend-rules" :style="warpCss">
<view class="header-row">
<text class="iconfont icon-book"></text>
<text class="title">{{ $lang('components-diy.diy-recommend-rules.title') }}</text>
</view>
<view class="rules-list">
<view v-for="(r, i) in displayRules" :key="i" class="rule-row">
<view class="rule-icon" :class="'icon-' + (r.icon || 'money')">
<text class="iconfont" :class="iconFontClass(r.icon)"></text>
</view>
<view class="rule-text">{{ r.title || r.text }}</view>
</view>
</view>
</view>
</template>
<script>
import recommendData from '@/common/js/recommend-diy-data.js';
export default {
name: 'diy-recommend-rules',
mixins: [recommendData],
computed: {
// 可被后台 value.rules 覆盖,默认用 API 返回
displayRules() {
if (this.value && Array.isArray(this.value.rules) && this.value.rules.length) {
return this.value.rules;
}
return this.rewardRules || [];
},
warpCss() {
return {
padding: '24rpx',
backgroundColor: '#fff',
};
},
},
methods: {
iconFontClass(icon) {
const map = {
money: 'icon-money',
calendar: 'icon-calendar',
wallet: 'icon-wallet',
infinite: 'icon-infinite',
};
return map[icon] || 'icon-money';
},
},
};
</script>
<style lang="scss" scoped>
.diy-recommend-rules {
.header-row {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.iconfont {
font-size: 32rpx;
color: #1976d2;
margin-right: 8rpx;
}
.title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
}
.rules-list {
background: #f3f9ff;
border-radius: 12rpx;
padding: 16rpx 20rpx;
}
.rule-row {
display: flex;
align-items: flex-start;
padding: 12rpx 0;
border-bottom: 1rpx dashed #d6e9f8;
&:last-child { border-bottom: none; }
}
.rule-icon {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background: #1976d2;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
margin-right: 16rpx;
flex-shrink: 0;
.iconfont { font-size: 24rpx; }
}
.rule-text {
font-size: 24rpx;
color: #444;
line-height: 1.5;
flex: 1;
}
}
</style>

View File

@@ -1,67 +0,0 @@
<template>
<view data-component-name="diy-recommend-stat" class="diy-recommend-stat" :style="warpCss">
<view class="stat-row">
<view class="stat-item">
<text class="num price-font">{{ statistics.total_recommend || 0 }}</text>
<text class="label">{{ $lang('components-diy.diy-recommend-stat.totalRecommend') }}</text>
</view>
<view class="stat-item">
<text class="num price-font color-green">{{ statistics.total_entered || 0 }}</text>
<text class="label">{{ $lang('components-diy.diy-recommend-stat.totalEntered') }}</text>
</view>
<view class="stat-item">
<text class="num price-font color-orange">¥{{ formatMoney(statistics.total_earned) }}</text>
<text class="label">{{ $lang('components-diy.diy-recommend-stat.totalEarned') }}</text>
</view>
</view>
</view>
</template>
<script>
import recommendData from '@/common/js/recommend-diy-data.js';
export default {
name: 'diy-recommend-stat',
mixins: [recommendData],
computed: {
warpCss() {
return {
padding: '24rpx',
backgroundColor: '#fff',
};
},
},
};
</script>
<style lang="scss" scoped>
.diy-recommend-stat {
.stat-row {
display: flex;
background: #fff;
border-radius: 12rpx;
padding: 24rpx 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
.stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
border-right: 1rpx solid #f0f0f0;
&:last-child { border-right: none; }
}
.num {
font-size: 36rpx;
font-weight: 700;
color: #333;
&.color-green { color: #4caf50; }
&.color-orange { color: #ff9800; }
}
.label {
font-size: 22rpx;
color: #888;
margin-top: 6rpx;
}
}
}
</style>

View File

@@ -1,38 +0,0 @@
/**
* 推荐系统 diy 组件 6 个组件共用 i18n 文案 (English)
* 同 zh-cn 一致,文案翻译为英文
*/
export const lang = {
'diy-recommend-code': {
myCode: 'My Referral Code',
copy: 'Copy',
copySuccess: 'Copied',
shareTip: 'Copy the link and share it with enterprises to bind',
},
'diy-recommend-balance': {
available: 'Withdrawable',
frozen: 'Frozen',
probationDays: '{days} days remaining',
passedProbation: 'Probation passed',
totalEarned: 'Total earned',
},
'diy-recommend-stat': {
totalRecommend: 'Referred',
totalEntered: 'Entered',
totalEarned: 'Earned',
},
'diy-recommend-records': {
recent: 'Recent Referrals',
viewAll: 'View all',
empty: 'No referrals yet',
},
'diy-recommend-rules': {
title: 'Reward Rules',
},
'diy-recommend-action': {
withdrawNow: 'Withdraw Now',
withdrawList: 'Records',
applyEnterprise: 'Apply',
},
loadFail: 'Failed to load, please retry',
};

View File

@@ -1,52 +0,0 @@
/**
* 推荐系统 diy 组件 6 个组件共用 i18n 文案
* 加载路径: pages-tool 中 $lang('components-diy.diy-recommend-code.xxx')
* 模块前缀已经写好,这里只导出组件名下的 key
*/
export const lang = {
// ---- diy-recommend-code ----
'diy-recommend-code': {
myCode: '我的推荐码',
copy: '复制',
copySuccess: '已复制',
shareTip: '复制推荐链接分享给企业,对方填表时填入即可绑定',
},
// ---- diy-recommend-balance ----
'diy-recommend-balance': {
available: '可提现余额',
frozen: '待结算(冻结中)',
probationDays: '考察期 {days} 天',
passedProbation: '已过考察期',
totalEarned: '累计总收益',
},
// ---- diy-recommend-stat ----
'diy-recommend-stat': {
totalRecommend: '累计推荐',
totalEntered: '已入驻',
totalEarned: '累计收益',
},
// ---- diy-recommend-records ----
'diy-recommend-records': {
recent: '最近推荐',
viewAll: '查看全部',
empty: '暂无推荐记录',
},
// ---- diy-recommend-rules ----
'diy-recommend-rules': {
title: '奖励规则',
},
// ---- diy-recommend-action ----
'diy-recommend-action': {
withdrawNow: '立即提现',
withdrawList: '提现记录',
applyEnterprise: '入驻申请',
},
// ---- 通用 ----
loadFail: '加载失败,请稍后重试',
};

View File

@@ -2422,8 +2422,7 @@
"uni-*(\\W.*)": "@/uni_modules/uni$1/components/uni$1/uni$1.vue",
"mescroll-uni": "@/components/mescroll/my-list-mescroll.vue",
"ns-goods-sku-*(\\W.*)": "@/components/ns-goods-sku/ns-goods-sku$1.vue",
"diy-*(\\W.*)": "@/components-diy/diy$1.vue",
"diy-recommend-*": "@/components-diy/diy-recommend-$1.vue"
"diy-*(\\W.*)": "@/components-diy/diy$1.vue"
}
},
"preloadRule": {

View File

@@ -1,30 +0,0 @@
// Quick parse check for the 6 diy-recommend components
const fs = require('fs');
const path = require('path');
const files = [
'diy-recommend-code.vue',
'diy-recommend-balance.vue',
'diy-recommend-stat.vue',
'diy-recommend-records.vue',
'diy-recommend-rules.vue',
'diy-recommend-action.vue',
];
const base = path.join(__dirname, '..', 'components-diy');
let ok = 0;
for (const f of files) {
const p = path.join(base, f);
if (!fs.existsSync(p)) { console.log('✗', f, 'MISSING'); continue; }
const content = fs.readFileSync(p, 'utf8');
// Check has template/script/style
const hasT = /<template>/i.test(content) && /<\/template>/i.test(content);
const hasS = /<script>/i.test(content) && /<\/script>/i.test(content);
const hasC = /<style /i.test(content) && /<\/style>/i.test(content);
const nameMatch = content.match(/name:\s*['"](\S+)['"]/);
const name = nameMatch ? nameMatch[1] : '?';
console.log(`✓ ${f.padEnd(34)} name="${name}" T:${hasT} S:${hasS} C:${hasC}`);
if (hasT && hasS && hasC) ok++;
}
console.log(`\n${ok}/${files.length} OK`);
process.exit(ok === files.length ? 0 : 1);

View File

@@ -21,12 +21,18 @@ function _isProductionRelease() {
* 生产构建门禁: 同步执行 test-gate.js, 失败立即终止构建 (零产物)。
* 与 package.json 的 "prebuild": "npm run test" 互补 —— 那条只覆盖 npm run build,
* 本函数覆盖所有会 require vue.config.js 的入口 (HBuilderX / vue-cli-service / uni-cli)。
*
* 关键: 使用 process.execPath 而非字符串 "node",避免依赖 PATH 查找 node。
* 原因: HBuilderX 等 IDE 启动子进程时,PATH 中可能根本没有 node 条目 (本项目
* 就曾因 Machine PATH 残留指向 v14.21.3 的 Huawei node 而触发问题)。
* process.execPath 始终是"当前正在运行 vue.config.js 的那个 node 二进制",
* HBuilderX 自带 node (v22.22.2) ≥ 18,天然满足 test-gate 的版本要求。
*/
function _enforceTestGate() {
if (!_isProductionRelease()) return; // 开发态 / 运行态不拦截
console.log('\n🛡️ [test-gate] 检测到生产构建,先执行测试门禁...');
console.log(`\n🛡️ [test-gate] 检测到生产构建,先执行测试门禁 (${process.versions.node})...`);
try {
execSync('node scripts/test-gate.js', {
execSync(`"${process.execPath}" scripts/test-gate.js`, {
cwd: __dirname,
stdio: 'inherit',
});