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
This commit is contained in:
yuzhiran
2026-06-20 20:49:15 +08:00
parent a1e1f0b3c3
commit 8ee27fdd32
11 changed files with 648 additions and 593 deletions
+129 -25
View File
@@ -39,16 +39,21 @@
</view>
</view>
<!-- 配额与会员信息 -->
<view class="quota-card" v-if="isLoggedIn">
<view class="quota-row">
<view class="quota-info">
<text class="quota-plan">{{ memberInfo.planName || '免费版' }}</text>
<text class="quota-count">引力值 {{ memberInfo.gravity ?? 0 }}</text>
</view>
<view class="quota-actions">
<text class="quota-btn primary" @click="goBuyCredits">补充引力值</text>
<text class="quota-btn" :class="memberInfo.plan !== 'free' ? 'owned' : ''" @click="goUpgrade">{{ memberInfo.plan !== 'free' ? '已开通' : '升级会员' }}</text>
<!-- 引力值卡片代替原配额卡片 -->
<view class="gravity-card" v-if="isLoggedIn">
<view class="gravity-card-inner">
<view class="gravity-top-row">
<view class="gravity-header">
<text class="gravity-icon"></text>
<text class="gravity-label">我的引力值</text>
</view>
<text class="gravity-num">{{ memberInfo.gravity ?? 0 }}</text>
</view>
<text class="gravity-hint">每次面试消耗 5 引力值 · 分享可获得更多</text>
<view class="gravity-actions">
<text class="gravity-btn share" @click="goSharePage">分享得引力值</text>
<text class="gravity-btn contribute" @click="goContributePage">贡献面经</text>
<text class="gravity-btn h5buy" @click="goH5Buy">官网购买</text>
</view>
</view>
</view>
@@ -72,17 +77,20 @@
<text class="menu-text">面试复盘</text>
<text class="menu-arrow"></text>
</view>
<!-- 会员中心菜单注释隐藏微信审核 -->
<!--
<view class="menu-item" @click="goVip">
<view class="menu-icon-wrap wrap-purple"><text class="menu-icon">💎</text></view>
<text class="menu-text">会员中心</text>
<text class="menu-arrow"></text>
</view>
-->
<view class="menu-item" @click="requireLogin(goResume, '我的简历')">
<view class="menu-icon-wrap wrap-green"><text class="menu-icon">📄</text></view>
<text class="menu-text">我的简历</text>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" @click="requireLogin(goShare, '我的分享')">
<view class="menu-item" @click="requireLogin(goSharePage, '我的分享')">
<view class="menu-icon-wrap wrap-orange"><text class="menu-icon">📤</text></view>
<text class="menu-text">我的分享</text>
<text class="menu-arrow"></text>
@@ -104,6 +112,46 @@
<button class="logout-btn" @click="doLogout">退出登录</button>
</view>
</view>
<!-- 引力值获取方式 -->
<view class="modal-overlay" v-if="showGetGravityModal" @click="showGetGravityModal = false">
<view class="modal-content" @click.stop>
<text class="modal-title">获取引力值</text>
<text class="modal-hint">引力值可用于面试简历优化下载等</text>
<!-- 分享得引力值 -->
<view class="gp-get-method" @click="goSharePage">
<view class="gp-method-icon-wrap"><text class="gp-method-icon">🎁</text></view>
<view class="gp-method-info">
<text class="gp-method-name">分享给好友</text>
<text class="gp-method-desc">每成功邀请一位好友注册可得 5 引力值上不封顶</text>
</view>
<text class="gp-method-arrow"></text>
</view>
<!-- 贡献面经 -->
<view class="gp-get-method" @click="goContributePage">
<view class="gp-method-icon-wrap"><text class="gp-method-icon">📝</text></view>
<view class="gp-method-info">
<text class="gp-method-name">贡献面经</text>
<text class="gp-method-desc">每发布一篇面经可获得 3 引力值助力他人也提升自己</text>
</view>
<text class="gp-method-arrow"></text>
</view>
<!-- 官网购买 -->
<view class="gp-get-method" @click="goH5Buy">
<view class="gp-method-icon-wrap"><text class="gp-method-icon">🛒</text></view>
<view class="gp-method-info">
<text class="gp-method-name">官网购买</text>
<text class="gp-method-desc">通过官网网页端可购买引力值套餐支持更多支付方式</text>
</view>
<text class="gp-method-arrow"></text>
</view>
<text class="modal-close" @click="showGetGravityModal = false">关闭</text>
</view>
</view>
</view>
</template>
@@ -179,17 +227,36 @@ const checkAdmin = () => {
}
const goLogin = () => uni.navigateTo({ url: '/pages/login/login' })
const goBuyCredits = () => uni.navigateTo({ url: '/pages/member/member?buy=interview' })
const goUpgrade = () => {
if (memberInfo.value.plan !== 'free') return // already on a paid plan
// 引力值获取
const showGetGravityModal = ref(false)
const openBuyModal = () => { showGetGravityModal.value = true }
const goH5Buy = () => {
const token = uni.getStorageSync('token') || ''
const url = `https://zhiyin.yzrcloud.cn/?buy=gravity${token ? '&token=' + token : ''}`
// #ifdef H5
uni.navigateTo({ url: '/pages/member/member' })
// #endif
// #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 goCareer = () => uni.navigateTo({ url: '/pages/career/career' })
const goHistory = () => uni.switchTab({ url: '/pages/history/history' })
const goReviewReview = () => uni.navigateTo({ url: '/pages/review/review' })
const goVip = () => uni.navigateTo({ url: '/pages/member/member' })
const goResume = () => uni.navigateTo({ url: '/pages/resume/resume' })
const goShare = () => uni.navigateTo({ url: '/pages/share/share' })
const goSharePage = () => uni.navigateTo({ url: '/pages/share/share' })
const goContributePage = () => uni.navigateTo({ url: '/pages/contribute/contribute' })
const goAdmin = () => uni.navigateTo({ url: '/pages/admin/admin' })
const goAbout = () => uni.navigateTo({ url: '/pages/about/about' })
@@ -228,16 +295,29 @@ const doLogout = () => {
.guest-info { flex: 1; }
.guest-name { font-size: 30rpx; font-weight: 600; color: #FFFFFF; }
.quota-card { margin: -48rpx 32rpx 16rpx; background: #FFFFFF; border-radius: var(--radius-lg); padding: 28rpx 24rpx; box-shadow: var(--shadow-sm); position: relative; z-index: 1; }
.quota-row { display: flex; align-items: center; justify-content: space-between; }
.quota-info { display: flex; flex-direction: column; gap: 4rpx; }
.quota-plan { font-size: 28rpx; font-weight: 700; color: var(--color-text); }
.quota-count { font-size: 22rpx; color: #6B7280; }
.quota-actions { display: flex; gap: 12rpx; }
.quota-btn { font-size: 22rpx; padding: 8rpx 20rpx; border-radius: var(--radius-sm); font-weight: 500; white-space: nowrap; }
.quota-btn.primary { background: linear-gradient(135deg, var(--color-gradient-start), var(--color-gradient-mid)); color: #FFF; }
.quota-btn.owned { background: #F3F4F6; color: #9CA3AF; }
.quota-btn:not(.primary):not(.owned) { background: #FEF3C7; color: #92400E; border: 2rpx solid #F59E0B; }
/* 引力值卡片(代替原配额卡片) */
.gravity-card { margin: -48rpx 32rpx 16rpx; position: relative; z-index: 1; }
.gravity-card-inner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: var(--radius-xl);
padding: 28rpx 24rpx 24rpx;
box-shadow: 0 8rpx 32rpx rgba(102,126,234,0.25);
}
.gravity-top-row { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 8rpx; }
.gravity-header { display: flex; align-items: center; gap: 8rpx; }
.gravity-icon { font-size: 32rpx; }
.gravity-label { font-size: 24rpx; color: rgba(255,255,255,0.8); font-weight: 500; }
.gravity-num { font-size: 48rpx; font-weight: 800; color: #FFFFFF; line-height: 1; }
.gravity-hint { font-size: 20rpx; color: rgba(255,255,255,0.6); margin-bottom: 20rpx; }
.gravity-actions { display: flex; gap: 16rpx; }
.gravity-btn {
flex: 1; text-align: center; padding: 18rpx 0; border-radius: var(--radius-md);
font-size: 26rpx; font-weight: 600; white-space: nowrap;
}
.gravity-btn.share { background: rgba(255,255,255,0.2); color: #FFFFFF; border: 2rpx solid rgba(255,255,255,0.3); }
.gravity-btn.h5buy { background: #FFFFFF; color: #667eea; }
.gravity-btn.contribute { background: rgba(255,255,255,0.15); color: #FFFFFF; border: 2rpx solid rgba(255,255,255,0.2); }
.gravity-btn:active { transform: scale(0.96); }
.menu-area { padding: 0 32rpx 32rpx; margin-top: 8rpx; }
.menu-group { background: #FFFFFF; border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 24rpx; box-shadow: var(--shadow-sm); }
@@ -258,4 +338,28 @@ const doLogout = () => {
.logout-wrap { margin-top: 8rpx; }
.logout-btn { background: #FFFFFF; color: var(--color-error); font-size: 28rpx; font-weight: 500; border-radius: var(--radius-md); height: 88rpx; line-height: 88rpx; border: 2rpx solid #FECACA; }
.logout-btn:active { background: #FEF2F2; transform: scale(0.96); }
/* 弹窗通用 */
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 100; }
.modal-content { background: #FFF; border-radius: var(--radius-xl); padding: 40rpx 32rpx; width: 600rpx; max-height: 80vh; overflow-y: auto; display: flex; flex-direction: column; align-items: center; gap: 16rpx; }
.modal-title { font-size: 30rpx; font-weight: 700; color: var(--color-text); }
.modal-hint { font-size: 22rpx; color: #6B7280; text-align: center; }
.modal-close { font-size: 24rpx; color: #9CA3AF; padding: 12rpx 24rpx; }
/* 获取引力值弹窗 - 分享/贡献方法 */
.gp-get-method {
width: 100%; display: flex; align-items: center; gap: 16rpx;
background: #F9FAFB; border-radius: var(--radius-md); padding: 20rpx;
border: 2rpx solid #E5E7EB; margin-top: 8rpx;
}
.gp-get-method:active { transform: scale(0.97); background: #F3F4F6; }
.gp-method-icon-wrap {
width: 64rpx; height: 64rpx; border-radius: var(--radius-md);
background: #EEF2FF; display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.gp-method-icon { font-size: 32rpx; }
.gp-method-info { flex: 1; display: flex; flex-direction: column; gap: 4rpx; }
.gp-method-name { font-size: 26rpx; font-weight: 600; color: var(--color-text); }
.gp-method-desc { font-size: 20rpx; color: #6B7280; line-height: 1.4; }
.gp-method-arrow { font-size: 32rpx; color: #D1D5DB; }
</style>