From 8ee27fdd323ff9553bb8212111de3334a0ac5664 Mon Sep 17 00:00:00 2001 From: yuzhiran Date: Sat, 20 Jun 2026 20:49:15 +0800 Subject: [PATCH] feat: refactor member to pay-per-use gravity purchase; mv webview to clipboard+browser - member.vue: rewrite from subscription plans (free/growth/sprint) to H5-only pay-per-use gravity purchase with quantity selector + QR code - user.vue: gravity card replacing quota card, add share/contribute/H5-buy entry points, plus gravity acquisition modal (share/contribute/buy) - share.vue: layout fix (flex column), smarter copyLink with cached URL, WeChat timeline hint instead of open-type - share.controller.ts: add GET /:shareCode redirect route (IP record + 302) - interview.vue: guest mode fix, H5 buy modal, clipboard copy instead of webview for mini-program - App.vue: handleH5UrlParams for ?token=&buy=gravity auto-login - composables/useGravityPurchase.ts: reusable gravity purchase composable - remove webview.vue (no longer used), replace with clipboard+browser flow - AGENTS.md: sync all above changes, fix duplicate numbering --- AGENTS.md | 22 +- .../src/modules/schedule/schedule.module.ts | 3 +- backend/src/modules/share/share.controller.ts | 27 +- zhiyin-app/src/App.vue | 20 + .../src/composables/useGravityPurchase.ts | 193 ++++++ zhiyin-app/src/pages/admin/admin.vue | 4 +- zhiyin-app/src/pages/career/career.vue | 11 +- zhiyin-app/src/pages/interview/interview.vue | 48 +- zhiyin-app/src/pages/member/member.vue | 641 ++++-------------- zhiyin-app/src/pages/share/share.vue | 118 +++- zhiyin-app/src/pages/user/user.vue | 154 ++++- 11 files changed, 648 insertions(+), 593 deletions(-) create mode 100644 zhiyin-app/src/composables/useGravityPurchase.ts diff --git a/AGENTS.md b/AGENTS.md index c76d504..f5e0b81 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,9 +21,10 @@ zhiyin/ ├── zhiyin-app/ # uni-app 3.x 前端 (H5 + 微信小程序) │ └── src/ │ ├── pages/ # 20 个页面 (pages.json 路由) +│ ├── composables/ # 可复用组合式函数(如 useGravityPurchase) │ ├── services/api.ts # API 调用封装 (uni.request) │ ├── config.ts # 端点定义 + api() 辅助函数 -│ └── App.vue # 设计 Token + 全局样式 +│ └── App.vue # 设计 Token + 全局样式 + H5 URL 参数处理 └── docs/ # 产品/架构/部署/路线图文档 ``` @@ -96,10 +97,13 @@ zhiyin/ - 环境变量: `AI_PRIMARY_KEY`, `AI_BACKUP_KEY` ### 支付(微信支付 v3) -- Native 支付(H5 扫码): `POST /payment/create` -- JSAPI 支付(小程序内): `POST /payment/jsapi` -- 支付回调: `POST /payment/notify`(@Public,验签 + 解密 + 自动开会员) +- Native 支付(H5 扫码): `POST /payment/create-product`(按量购买引力值) +- JSAPI 支付(小程序内): `POST /payment/jsapi-product`(按量购买引力值) +- 支付回调: `POST /payment/notify`(@Public,验签 + 解密 + 自动到账) +- 支付结果轮询: `GET /payment/check/:outTradeNo` +- 产品定价: `GET /member/plans`(含 products 字段,定义引力值单价和赠送量) - 需要微信商户证书文件(通过 postbuild 复制到 dist) +- **注意**: 当前会员体系已从按月订阅制改为按量购买引力值制(小程序内复制链接到浏览器打开购买,H5 直接扫码支付) --- @@ -196,7 +200,7 @@ cd zhiyin-app && npm run build:mp-weixin && node scripts/upload-mp.js |------|------|---------| | Phase 0: 战略升级 | ✅ 完成 | 定价重构(免费 + ¥19.9/月),三层壁垒设计 | | Phase 0.5: 壁垒构建 | ✅ 完成 | 数据飞轮(面经贡献+题库),留存入围(进步轨迹+打卡日历+每日一题) | -| Phase 1: MVP 上线 | 🚧 当前 | 小程序 v1.0.16 已上传、引力值体系统一(VIP 不再无限次)、管理后台完善、H5 已部署、生产模式已启用 | +| Phase 1: MVP 上线 | 🚧 当前 | 小程序 v1.0.16 已上传、引力值体系统一(订阅制改为按量购买)、管理后台完善、H5 已部署、生产模式已启用 | | Phase 1.5: 商业化 | 📋 规划 | 引力值运营策略、每日一题定时推送、PMF 验证 | | Phase 2: 增强 + 题库 | 📋 规划 | 50+ 校招岗位、技能缺口分析、公司真题库建设 | | Phase 3: 秋招冲刺 | 📋 规划 | 高校合作、B 端服务、KOC 推广 | @@ -258,7 +262,7 @@ VITE_APP_NAME=AI磁场 - 远程仓库: `http://127.0.0.1:2999/txai-dev/zhiyin.git`(本机 Gitea,带 token 认证) - 默认分支: `master` -- 最新 tag: `v1.0.11`(小程序上传版本号源自 git tag) +- 最新 tag: `v1.0.16`(小程序上传版本号源自 git tag) --- @@ -272,10 +276,12 @@ VITE_APP_NAME=AI磁场 6. **API 限流**: 100 次/60 秒(在 `app.module.ts` 中配置),注意避免在定时任务和批量操作中被限 7. **验证码**: 生产模式(`NODE_ENV=production`)使用真实 SMTP 发邮件验证码;非生产模式手机验证码固定为 `123456`、邮件验证码在响应中返回 `devCode` 8. **MongoDB**: 8 个核心集合 + 2 个分享集合 -9. **引力值体系**: 所有计划统一走引力值消耗(面试 5、优化 3、下载 2)。VIP 不再免额度,成长版每月 250 引力值,冲刺版每月 600 引力值,每日凌晨 2 点定时补给。免费用户注册送 5 引力值。 +9. **引力值体系**: 所有计划统一走引力值消耗(面试 5、优化 3、下载 2)。VIP 不再免额度,成长版每月 250 引力值,冲刺版每月 600 引力值,每日凌晨 2 点定时补给。免费用户注册送 5 引力值。小程序内通过分享得引力值/贡献面经/复制官网链接到浏览器打开购买三种方式获取引力值;H5 直接扫码支付按量购买(¥5/份)。 10. **api.ts 陷阱**: 对象字面量必须在 `export const apiService = {` 或 `const apiService = { ... export default apiService` 中包裹,否则 uni-app 构建报错 `Expected ";" but found ":"`。git pull 后经常丢失这行声明,需手动补回 11. **H5 构建 assets 清理**: `assets/` 中的旧 hash 文件不能随意删除——`index-*.js`(主 bundle)动态 import 了所有 page chunk,删除仍在引用的文件会导致浏览器 `NS_ERROR_CORRUPTED_CONTENT` -11. **管理后台自动验证**: `admin.vue` 中 `onMounted` 自动调用 `doVerify()`,进入后台即检测 JWT 中 `role` 是否为 `admin`,不再需要手动点击"验证管理员身份"按钮 +12. **管理后台自动验证**: `admin.vue` 中 `onMounted` 自动调用 `doVerify()`,进入后台即检测 JWT 中 `role` 是否为 `admin`,不再需要手动点击"验证管理员身份"按钮 +13. **分享重定向路由**: `share.controller.ts` 中 `GET /api/share/:shareCode` 是公开路由(泛匹配,放最后避免拦截其他路由),访问时记录访问者 IP → 302 重定向到 H5 首页 +14. **小程序官网购买走剪贴板**: 小程序内"官网购买"不再使用 webview 内嵌 H5,改为 `uni.setClipboardData` 复制带 JWT token 的 URL 到剪贴板,提示用户在手机浏览器中打开购买(`App.vue` 的 `handleH5UrlParams` 解析 `?token=` 和 `?buy=gravity` 参数自动登录跳转) --- diff --git a/backend/src/modules/schedule/schedule.module.ts b/backend/src/modules/schedule/schedule.module.ts index 6c69a1e..88cd356 100644 --- a/backend/src/modules/schedule/schedule.module.ts +++ b/backend/src/modules/schedule/schedule.module.ts @@ -6,7 +6,6 @@ import { VipExpiryService } from './vip-expiry.service' import { GravityTopUpService } from './gravity-top-up.service' import { DailyQuestion, DailyQuestionSchema } from '../schemas/daily-question.schema' import { User, UserSchema } from '../user/user.schema' -import { PricingService } from '../schemas/pricing.service' @Module({ imports: [ @@ -15,6 +14,6 @@ import { PricingService } from '../schemas/pricing.service' { name: User.name, schema: UserSchema }, ]), ], - providers: [WechatTokenService, DailyQuestionPushService, VipExpiryService, GravityTopUpService, PricingService], + providers: [WechatTokenService, DailyQuestionPushService, VipExpiryService, GravityTopUpService], }) export class ScheduleModule {} diff --git a/backend/src/modules/share/share.controller.ts b/backend/src/modules/share/share.controller.ts index 45f9ff4..b14b389 100644 --- a/backend/src/modules/share/share.controller.ts +++ b/backend/src/modules/share/share.controller.ts @@ -1,9 +1,11 @@ -import { Controller, Get, Post, Body, Param, Query, HttpException, HttpStatus, UseGuards } from '@nestjs/common' +import { Controller, Get, Post, Body, Param, Query, HttpException, HttpStatus, UseGuards, Req, Res } from '@nestjs/common' +import { Request, Response } from 'express' import { JwtService } from '@nestjs/jwt' import { ShareService } from './share.service' import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard' import { CurrentUser } from '../../common/decorators/current-user.decorator' import { Public } from '../../common/decorators/public.decorator' +import * as crypto from 'crypto' @Controller('share') export class ShareController { @@ -66,4 +68,27 @@ export class ShareController { ) { return this.shareService.visitors(userId, Number(page) || 1, Number(pageSize) || 20) } + + // 泛匹配路由放在最后,避免拦截 stats/records/visitors 等 + @Public() + @Get(':shareCode') + async redirect( + @Param('shareCode') shareCode: string, + @Req() req: Request, + @Res() res: Response, + ) { + try { + const ip = req.ip || req.socket?.remoteAddress || 'unknown' + const visitorId = crypto.createHash('md5').update(ip).digest('hex').slice(0, 16) + let visitorUserId: string | undefined + const token = req.query.token as string | undefined + if (token) { + try { const payload = this.jwtService.verify(token) as any; visitorUserId = payload.userId } catch {} + } + await this.shareService.visit(shareCode, visitorId, visitorUserId) + } catch (e) { + // 访问记录失败不影响跳转 + } + res.redirect(HttpStatus.FOUND, `https://zhiyin.yzrcloud.cn/?share=${shareCode}`) + } } diff --git a/zhiyin-app/src/App.vue b/zhiyin-app/src/App.vue index ce9e3f0..5d78a6f 100644 --- a/zhiyin-app/src/App.vue +++ b/zhiyin-app/src/App.vue @@ -6,8 +6,28 @@ onLaunch(() => { // #ifdef MP-WEIXIN initPrivacy() // #endif + // #ifdef H5 + handleH5UrlParams() + // #endif }) +// #ifdef H5 +function handleH5UrlParams() { + const params = new URLSearchParams(window.location.search) + const token = params.get('token') + const buy = params.get('buy') + if (token) { + uni.setStorageSync('token', token) + } + if (buy === 'gravity') { + // 延迟等 app 初始化完成再跳转 + setTimeout(() => { + uni.navigateTo({ url: '/pages/member/member' }) + }, 300) + } +} +// #endif + // #ifdef MP-WEIXIN function initPrivacy() { if (wx.onNeedPrivacyAuthorization) { diff --git a/zhiyin-app/src/composables/useGravityPurchase.ts b/zhiyin-app/src/composables/useGravityPurchase.ts new file mode 100644 index 0000000..3bc5691 --- /dev/null +++ b/zhiyin-app/src/composables/useGravityPurchase.ts @@ -0,0 +1,193 @@ +import { ref, computed, nextTick } from 'vue' +import { api } from '../config' + +export function useGravityPurchase(onPaymentSuccess?: () => void) { + const showQuantityModal = ref(false) + const buyQuantity = ref(1) + const products = ref([]) + const buyProductType = ref('interview') + const payLoading = ref(false) + const payError = ref('') + const showPayModal = ref(false) + const payCodeUrl = ref('') + const currentOutTradeNo = ref('') + const isMp = ref(false) + const paySuccess = ref(false) + + const unitPrice = computed(() => { + const p = products.value.find(p => p.type === buyProductType.value) + return p?.price || 0 + }) + const totalPrice = computed(() => unitPrice.value * buyQuantity.value / 100) + const buyGravityPerUnit = computed(() => { + const p = products.value.find(p => p.type === buyProductType.value) + return p?.gravity || 0 + }) + + const changeQty = (delta: number) => { + const next = buyQuantity.value + delta + if (next >= 1 && next <= 99) buyQuantity.value = next + } + const clampQty = () => { + if (buyQuantity.value < 1) buyQuantity.value = 1 + if (buyQuantity.value > 99) buyQuantity.value = 99 + } + + const loadProducts = async () => { + try { + const res = await uni.request({ url: api('/member/plans'), method: 'GET' }) + if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.products) { + const prodList: any[] = [] + for (const [key, val] of Object.entries(res.data.products as Record)) { + if (val?.price > 0) prodList.push({ type: key, ...val }) + } + products.value = prodList + } + } catch (e) { /* silent */ } + } + + const openGravityPurchase = async (productType = 'interview') => { + buyProductType.value = productType + buyQuantity.value = 1 + // #ifdef MP-WEIXIN + isMp.value = true + // #endif + // 加载产品信息 + await loadProducts() + showQuantityModal.value = true + } + + const cancelPay = () => { + showPayModal.value = false + payCodeUrl.value = '' + payLoading.value = false + payError.value = '' + } + + const confirmProductBuy = () => { + showQuantityModal.value = false + startProductPay(buyProductType.value, buyQuantity.value) + } + + const startProductPay = async (type: string, quantity = 1) => { + const token = uni.getStorageSync('token') || '' + if (!token) { + uni.showToast({ title: '请先登录', icon: 'none' }) + return + } + showPayModal.value = true + payLoading.value = true + payError.value = '' + + if (isMp.value) { + try { + let res = await uni.request({ + url: api('/payment/jsapi-product'), method: 'POST', + data: { type, quantity }, + header: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + timeout: 30000, + }) + payLoading.value = false + + if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.payParams) { + const pp = res.data.payParams as any + currentOutTradeNo.value = res.data.outTradeNo || '' + uni.requestPayment({ + provider: 'wxpay', + timeStamp: pp.timeStamp, + nonceStr: pp.nonceStr, + package: pp.package, + signType: pp.signType || 'RSA', + paySign: pp.paySign, + success: () => { + const no = currentOutTradeNo.value || res.data.outTradeNo + pollPayResult(no) + }, + fail: () => { + payError.value = '支付未完成' + uni.showToast({ title: '支付未完成', icon: 'none' }) + }, + }) + } else if (!res.statusCode || res.statusCode === 0) { + payError.value = '网络连接失败,请检查网络后重试' + uni.showToast({ title: '网络连接失败', icon: 'none' }) + } else { + const errMsg = res.data?.message || '购买失败' + payError.value = errMsg + uni.showToast({ title: errMsg, icon: 'none' }) + } + } catch (e) { + payLoading.value = false + payError.value = '网络错误,请重试' + uni.showToast({ title: '网络错误', icon: 'none' }) + } + } else { + try { + const res = await uni.request({ + url: api('/payment/create-product'), method: 'POST', + data: { type, quantity }, + header: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + }) + payLoading.value = false + + if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.codeUrl) { + payCodeUrl.value = res.data.codeUrl + currentOutTradeNo.value = res.data.outTradeNo + // 页面需要自己渲染二维码(依赖 uqrcode) + pollPayResult(res.data.outTradeNo) + } else if (!res.statusCode || res.statusCode === 0) { + payError.value = '网络连接失败,请检查网络后重试' + uni.showToast({ title: '网络连接失败', icon: 'none' }) + } else { + payError.value = res.data?.message || '购买失败' + uni.showToast({ title: res.data?.message || '购买失败', icon: 'none' }) + } + } catch (e) { + payLoading.value = false + payError.value = '网络错误,请重试' + uni.showToast({ title: '网络错误', icon: 'none' }) + } + } + } + + const pollPayResult = async (outTradeNo: string) => { + if (!outTradeNo) return + const maxAttempts = 30 + let attempts = 0 + const token = uni.getStorageSync('token') || '' + + const poll = async () => { + attempts++ + try { + const res = await uni.request({ + url: api(`/payment/check/${outTradeNo}`), method: 'GET', + header: { 'Authorization': `Bearer ${token}` }, + }) + if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.status === 'success') { + paySuccess.value = true + showPayModal.value = false + uni.showToast({ title: '充值成功!', icon: 'success' }) + onPaymentSuccess?.() + return + } + } catch (e) { /* ignore */ } + + if (attempts < maxAttempts) { + setTimeout(poll, 2000) + } else { + payError.value = '支付结果查询超时,请联系客服' + uni.showToast({ title: '支付查询超时', icon: 'none' }) + } + } + setTimeout(poll, 2000) + } + + return { + showQuantityModal, buyQuantity, products, buyProductType, + unitPrice, totalPrice, buyGravityPerUnit, + payLoading, payError, showPayModal, payCodeUrl, paySuccess, + isMp, + changeQty, clampQty, loadProducts, openGravityPurchase, + confirmProductBuy, cancelPay, + } +} diff --git a/zhiyin-app/src/pages/admin/admin.vue b/zhiyin-app/src/pages/admin/admin.vue index cc0f687..55840aa 100644 --- a/zhiyin-app/src/pages/admin/admin.vue +++ b/zhiyin-app/src/pages/admin/admin.vue @@ -975,8 +975,8 @@ onMounted(() => { doVerify() }) .admin-input { height: 72rpx; background: #FFF; border: 2rpx solid var(--color-border); border-radius: var(--radius-sm); padding: 0 20rpx; font-size: 24rpx; } .btn-verify { height: 72rpx; background: linear-gradient(135deg, var(--color-gradient-start), var(--color-gradient-mid)); color: #FFF; border-radius: var(--radius-sm); font-size: 26rpx; border: none; } .body { padding: 20rpx 32rpx 48rpx; margin-top: -40rpx; } -.tabs { display: flex; gap: 8rpx; background: #FFF; border-radius: var(--radius-md); padding: 6rpx; margin-bottom: 20rpx; } -.tab { flex: 1; text-align: center; padding: 14rpx; font-size: 24rpx; color: var(--color-text-secondary); border-radius: var(--radius-sm); white-space: nowrap; } +.tabs { display: flex; flex-wrap: wrap; gap: 8rpx; background: #FFF; border-radius: var(--radius-md); padding: 6rpx; margin-bottom: 20rpx; } +.tab { padding: 14rpx 20rpx; font-size: 24rpx; color: var(--color-text-secondary); border-radius: var(--radius-sm); white-space: nowrap; } .tab.active { background: var(--color-primary); color: #FFF; font-weight: 600; } .stat-cards { display: flex; gap: 16rpx; } .stat-card { flex: 1; background: #FFF; border-radius: var(--radius-lg); padding: 28rpx; text-align: center; box-shadow: var(--shadow-sm); } diff --git a/zhiyin-app/src/pages/career/career.vue b/zhiyin-app/src/pages/career/career.vue index 8ee595e..89879c9 100644 --- a/zhiyin-app/src/pages/career/career.vue +++ b/zhiyin-app/src/pages/career/career.vue @@ -150,7 +150,7 @@ const doAnalyze = async () => { data: { ...profile }, header: { 'Authorization': `Bearer ${token.value}`, 'Content-Type': 'application/json' }, }) - if (res.statusCode === 200) { + if (res.statusCode >= 200 && res.statusCode < 300) { if (res.data.error) { error.value = res.data.error return @@ -184,7 +184,7 @@ const doChat = async () => { data: { message: msg, history: chatHistory.value.slice(0, -1) }, header: { 'Authorization': `Bearer ${token.value}`, 'Content-Type': 'application/json' }, }) - if (res.statusCode === 200) { + if (res.statusCode >= 200 && res.statusCode < 300) { chatHistory.value.push({ role: 'assistant', content: res.data.reply || (res.data.error || '') }) } else { chatHistory.value.push({ role: 'assistant', content: '回复失败,请稍后重试' }) @@ -211,12 +211,13 @@ const goInterview = (position) => { .hero-title { font-size: 40rpx; font-weight: 700; color: var(--color-text); margin-top: 16rpx; } .hero-desc { font-size: 26rpx; color: var(--color-secondary); margin-top: 8rpx; } -.form-card { background: #fff; border-radius: var(--radius-lg); padding: 32rpx; box-shadow: var(--shadow-sm); } -.form-group { margin-bottom: 28rpx; } +.form-card { background: #fff; border-radius: var(--radius-lg); padding: 32rpx; box-shadow: var(--shadow-sm); overflow: hidden; } +.form-group { margin-bottom: 28rpx; width: 100%; } .form-group:last-child { margin-bottom: 0; } .form-label { font-size: 26rpx; font-weight: 600; color: var(--color-text); display: block; margin-bottom: 12rpx; } .required { color: var(--color-error); } -.form-input { width: 100%; height: 80rpx; border: 2rpx solid var(--color-border); border-radius: var(--radius-md); padding: 0 24rpx; font-size: 26rpx; color: var(--color-text); box-sizing: border-box; background: #FAFBFC; } +.form-input { width: 100%; height: 80rpx; border: 2rpx solid var(--color-border); border-radius: var(--radius-md); padding: 0 24rpx; font-size: 26rpx; color: var(--color-text); box-sizing: border-box; background: #FAFBFC; max-width: 100%; } +picker { width: 100%; } .select-trigger { display: flex; align-items: center; } .form-textarea { width: 100%; height: 160rpx; border: 2rpx solid var(--color-border); border-radius: var(--radius-md); padding: 20rpx 24rpx; font-size: 26rpx; color: var(--color-text); box-sizing: border-box; background: #FAFBFC; resize: none; } diff --git a/zhiyin-app/src/pages/interview/interview.vue b/zhiyin-app/src/pages/interview/interview.vue index 9bacd81..f6cf630 100644 --- a/zhiyin-app/src/pages/interview/interview.vue +++ b/zhiyin-app/src/pages/interview/interview.vue @@ -90,28 +90,22 @@ - + - - + + 引力值不足 您的引力值不足,请补充后继续面试(每次面试消耗 5 引力值) - - 补充引力值 - ¥5 起 - ¥5 = 5 引力值,可面试 1 次 - - - 推荐 - 开通成长版会员 - ¥19.9/月 - 每月 250 引力值,解锁全部权益 + + 官网购买引力值 + 前往网页版充值 + 打开官网 H5 页面,支持多种支付方式 - 取消 + 取消 @@ -122,7 +116,6 @@ import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue' import { onLoad } from '@dcloudio/uni-app' import { api, API_ENDPOINTS } from '../../config' import DigitalHuman from '../../components/digital-human.vue' - const messages = ref([{ role: 'ai', content: '你好!我是 AI 面试官,请选择岗位开始模拟面试!' }]) const inputText = ref('') const aiLoading = ref(false) @@ -134,7 +127,7 @@ const scrollToId = ref('') const position = ref('') const avatarMode = ref(true) const showPositionPicker = ref(false) -const showPurchaseModal = ref(false) +const showH5BuyModal = ref(false) const positions = ref([]) const positionsLoading = ref(false) const aiSpeechText = ref('') @@ -323,8 +316,27 @@ function onAvatarSilent() { } const goResult = () => uni.navigateTo({ url: `/pages/report/report?interviewId=${interviewId.value}` }) -const goBuyProduct = () => uni.navigateTo({ url: '/pages/member/member?buy=interview' }) -const goBuyMember = () => uni.navigateTo({ url: '/pages/member/member' }) + +// 官网购买引力值 +const goH5Buy = () => { + showH5BuyModal.value = true +} +const goH5BuyAndClose = () => { + showH5BuyModal.value = false + const token = uni.getStorageSync('token') || '' + const url = `https://zhiyin.yzrcloud.cn/?buy=gravity${token ? '&token=' + token : ''}` + // #ifdef MP-WEIXIN + uni.setClipboardData({ + data: url, + success: () => { + uni.showToast({ title: '链接已复制,请在手机浏览器中打开', icon: 'none', duration: 3000 }) + }, + fail: () => { + uni.showToast({ title: '复制失败,请手动访问 zhiyin.yzrcloud.cn', icon: 'none', duration: 3000 }) + }, + }) + // #endif +} const scrollToBottom = () => { nextTick(() => { scrollToId.value = ''; setTimeout(() => { scrollToId.value = 'msg-bottom' }, 100) }) } diff --git a/zhiyin-app/src/pages/member/member.vue b/zhiyin-app/src/pages/member/member.vue index bcc51bf..26dfd80 100644 --- a/zhiyin-app/src/pages/member/member.vue +++ b/zhiyin-app/src/pages/member/member.vue @@ -1,584 +1,221 @@