From eae415ecad0c3b71a39de9549ecba17f37655cf8 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Mon, 20 Jul 2026 11:16:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(recommend-diy):=20=E6=8E=A8=E8=8D=90?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=206=20=E4=B8=AA=20diy-recommend-*=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=20+=20mixin=20+=20lang?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 策略: 微信小程序前端采用"专属 DIY 组件"模式实现推荐入驻主页, 不写 4 个独立 .vue 页面,而是在 components-diy/ 下新增 6 个动态数据组件, 商家在"自定义页面"装修面板里自由拖拽组合。组件 mounted 后自动 ajax 拉 /recommend/api/home/homeSummary 真实数据。 新增: 1. 6 个 vue 组件 - diy-recommend-code 推荐码展示 + 复制 + 分享链接 - diy-recommend-balance 可提现余额 / 待结算 / 累计收益 + 考察期剩余天数 - diy-recommend-stat 累计推荐 / 已入驻 / 累计收益 3 个数字 - diy-recommend-records 最近 3 条推荐(动态) - diy-recommend-rules 奖励规则 4 条(API 推,可后台覆写) - diy-recommend-action 立即提现 + 提现记录 + 入驻申请 三按钮 2. 公共 mixin — common/js/recommend-diy-data.js - fetchRecommendInfo() ajax 拉 homeSummary(去抖 10s) - computed: balance / code / probation / statistics / recentRecords / rewardRules - formatMoney() 数字本地化 - 失败/未登录时 recommendInfo = null,模板显示占位 3. pages.json - easycom 加规则 "diy-recommend-*" → components-diy/diy-recommend-$1.vue - 已有 autoscan=true 自动发现;新增条目保证 builds 稳定 4. 语言包 - lang/zh-cn/components-diy/diy-recommend.js - lang/en-us/components-diy/diy-recommend.js 5. 验证脚本 - scripts/verify_diy_recommend.js — 6 文件三段式校验 技术细节: - 所有 vue 文件模板里 data-component-name 与 easycom 名字强一致 - 6 组件之间互不依赖,可任意组合到任意 Diy 页面 - API 数据通过 mixin 共享,组件不重复拉取 - 占位/loading/error 三态完整 - 大部分样式沿用项目既有 home-grown SCSS 写法,不引入新依赖 - bundle 体积增量 < 30KB i18n-check.js 通过: zh-cn: 224 files / 2678 keys en-us: 224 files / 2678 keys diff: 0 ✅ 未做(后续可选): - 4 个真正 .vue 页面(此 PR 不要,改用 DIY) - getMyQrcode 真实二维码生成 - 提现账户管理 UI --- common/js/recommend-diy-data.js | 98 ++++++++++++ components-diy/diy-recommend-action.vue | 82 ++++++++++ components-diy/diy-recommend-balance.vue | 108 +++++++++++++ components-diy/diy-recommend-code.vue | 167 +++++++++++++++++++++ components-diy/diy-recommend-records.vue | 147 ++++++++++++++++++ components-diy/diy-recommend-rules.vue | 103 +++++++++++++ components-diy/diy-recommend-stat.vue | 67 +++++++++ lang/en-us/components-diy/diy-recommend.js | 38 +++++ lang/zh-cn/components-diy/diy-recommend.js | 52 +++++++ pages.json | 3 +- scripts/verify_diy_recommend.js | 30 ++++ 11 files changed, 894 insertions(+), 1 deletion(-) create mode 100644 common/js/recommend-diy-data.js create mode 100644 components-diy/diy-recommend-action.vue create mode 100644 components-diy/diy-recommend-balance.vue create mode 100644 components-diy/diy-recommend-code.vue create mode 100644 components-diy/diy-recommend-records.vue create mode 100644 components-diy/diy-recommend-rules.vue create mode 100644 components-diy/diy-recommend-stat.vue create mode 100644 lang/en-us/components-diy/diy-recommend.js create mode 100644 lang/zh-cn/components-diy/diy-recommend.js create mode 100644 scripts/verify_diy_recommend.js 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 = /