diff --git a/common/js/recommend-diy-data.js b/common/js/recommend-diy-data.js
new file mode 100644
index 0000000..67e1dbe
--- /dev/null
+++ b/common/js/recommend-diy-data.js
@@ -0,0 +1,98 @@
+/**
+ * 通用 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 });
+ },
+ },
+};
diff --git a/components-diy/diy-recommend-action.vue b/components-diy/diy-recommend-action.vue
new file mode 100644
index 0000000..7ac0692
--- /dev/null
+++ b/components-diy/diy-recommend-action.vue
@@ -0,0 +1,82 @@
+
+
+
+
+ {{ $lang('components-diy.diy-recommend-action.withdrawNow') }}
+
+
+
+
+
+
+ {{ $lang('components-diy.diy-recommend-action.withdrawList') }}
+
+
+
+ {{ $lang('components-diy.diy-recommend-action.applyEnterprise') }}
+
+
+
+
+
+
+
+
diff --git a/components-diy/diy-recommend-balance.vue b/components-diy/diy-recommend-balance.vue
new file mode 100644
index 0000000..f990e5b
--- /dev/null
+++ b/components-diy/diy-recommend-balance.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+ {{ $lang('components-diy.diy-recommend-balance.available') }}
+ ¥{{ formatMoney(balance.available) }}
+
+ {{ $lang('components-diy.diy-recommend-balance.passedProbation') }}
+
+
+
+ {{ $lang('components-diy.diy-recommend-balance.frozen') }}
+ ¥{{ formatMoney(balance.frozen) }}
+
+ {{ $lang('components-diy.diy-recommend-balance.probationDays', { days: probation.remaining_days }) }}
+
+
+
+
+
+
+ {{ $lang('components-diy.diy-recommend-balance.totalEarned') }}
+ ¥{{ formatMoney(balance.total_earned) }}
+
+
+
+
+
+
+
+
diff --git a/components-diy/diy-recommend-code.vue b/components-diy/diy-recommend-code.vue
new file mode 100644
index 0000000..9d5024c
--- /dev/null
+++ b/components-diy/diy-recommend-code.vue
@@ -0,0 +1,167 @@
+
+
+
+
+
+ {{ $lang('components-diy.diy-recommend-code.myCode') }}
+
+
+
+
+ {{ code.code }}
+
+
+ {{ $lang('components-diy.diy-recommend-code.copy') }}
+
+
+
+ {{ recommendError || $lang('components-diy.diy-recommend-code.loading') }}
+
+
+
+
+ {{ $lang('components-diy.diy-recommend-code.copySuccess') }}
+
+
+
+
+
+ {{ $lang('components-diy.diy-recommend-code.shareTip') }}
+
+ {{ code.share_link }}
+
+
+
+
+
+
+
+
diff --git a/components-diy/diy-recommend-records.vue b/components-diy/diy-recommend-records.vue
new file mode 100644
index 0000000..e91fe07
--- /dev/null
+++ b/components-diy/diy-recommend-records.vue
@@ -0,0 +1,147 @@
+
+
+
+
+
+ {{ $lang('components-diy.diy-recommend-records.empty') }}
+
+
+
+
+
+
+ {{ r.company_name || r.id }}
+ {{ formatTime(r.apply_time) }}
+
+
+
+ {{ r.amount_text }}
+ {{ r.status_text }}
+
+
+
+
+
+
+
+
+
diff --git a/components-diy/diy-recommend-rules.vue b/components-diy/diy-recommend-rules.vue
new file mode 100644
index 0000000..2f76844
--- /dev/null
+++ b/components-diy/diy-recommend-rules.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+ {{ r.title || r.text }}
+
+
+
+
+
+
+
+
diff --git a/components-diy/diy-recommend-stat.vue b/components-diy/diy-recommend-stat.vue
new file mode 100644
index 0000000..ce254f1
--- /dev/null
+++ b/components-diy/diy-recommend-stat.vue
@@ -0,0 +1,67 @@
+
+
+
+
+ {{ statistics.total_recommend || 0 }}
+ {{ $lang('components-diy.diy-recommend-stat.totalRecommend') }}
+
+
+ {{ statistics.total_entered || 0 }}
+ {{ $lang('components-diy.diy-recommend-stat.totalEntered') }}
+
+
+ ¥{{ formatMoney(statistics.total_earned) }}
+ {{ $lang('components-diy.diy-recommend-stat.totalEarned') }}
+
+
+
+
+
+
+
+
diff --git a/lang/en-us/components-diy/diy-recommend.js b/lang/en-us/components-diy/diy-recommend.js
new file mode 100644
index 0000000..ccc3beb
--- /dev/null
+++ b/lang/en-us/components-diy/diy-recommend.js
@@ -0,0 +1,38 @@
+/**
+ * 推荐系统 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',
+};
diff --git a/lang/zh-cn/components-diy/diy-recommend.js b/lang/zh-cn/components-diy/diy-recommend.js
new file mode 100644
index 0000000..10440f0
--- /dev/null
+++ b/lang/zh-cn/components-diy/diy-recommend.js
@@ -0,0 +1,52 @@
+/**
+ * 推荐系统 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: '加载失败,请稍后重试',
+};
diff --git a/pages.json b/pages.json
index 2a33b8f..602e5a7 100644
--- a/pages.json
+++ b/pages.json
@@ -2422,7 +2422,8 @@
"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-*(\\W.*)": "@/components-diy/diy$1.vue",
+ "diy-recommend-*": "@/components-diy/diy-recommend-$1.vue"
}
},
"preloadRule": {
diff --git a/scripts/verify_diy_recommend.js b/scripts/verify_diy_recommend.js
new file mode 100644
index 0000000..d3a6dba
--- /dev/null
+++ b/scripts/verify_diy_recommend.js
@@ -0,0 +1,30 @@
+// 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 = //i.test(content) && /<\/template>/i.test(content);
+ const hasS = /