feat: 小程序统一走 Go 后台,咨询与登录切到 /api/v1

去掉协会 WebView 和魔方账密页,微信登录与咨询接口共用同一 token 和拦截器。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-09-15 00:26:28 +08:00
co-authored by Cursor
parent 6e97f01378
commit b09d82df8d
40 changed files with 3852 additions and 285 deletions
+106
View File
@@ -0,0 +1,106 @@
<template>
<yxg-page title="成长会员">
<text v-if="loading" class="hint">加载中</text>
<text v-else-if="error" class="err" @click="load">{{ error }} · 重试</text>
<template v-else>
<view v-if="me?.active" class="card on">
<text class="h1">会员有效 · {{ planLabel }}</text>
<text v-if="me.expires_at" class="meta">到期{{ formatDate(me.expires_at) }}</text>
<text class="meta">问答额度剩余{{ me.ask_quota_left ?? 0 }}</text>
</view>
<view v-else class="card">
<text class="lead">开通后可查看画像与关系理解的完整分析并获得更多 AI 问答次数</text>
</view>
<text class="sec">成长会员</text>
<view v-for="p in plans" :key="p.key" class="plan" :class="{ featured: p.featured }" @click="subscribe(p.key)">
<text v-if="p.tag" class="tag">{{ p.tag }}</text>
<text class="pl">{{ p.label }}</text>
<text class="pp">{{ p.price }}</text>
<text class="pd">{{ p.desc }}</text>
</view>
<text class="note">当前为模拟支付便于本地验收</text>
</template>
</yxg-page>
</template>
<script setup>
import YxgPage from '@/components/yxg/yxg-page.vue'
import { yxgApi } from '@/util/yxgApi.js'
import { ensureAccount } from '@/util/yxgAuth.js'
import { formatDate } from '@/util/yxgCatalog.js'
const plans = [
{ key: 'month', label: '月卡', price: '模拟开通', desc: '按月灵活体验', featured: false, tag: '' },
{ key: 'quarter', label: '季卡', price: '模拟开通', desc: '三个月持续成长', featured: true, tag: '推荐' },
{ key: 'year', label: '年卡', price: '模拟开通', desc: '全年陪伴与报告', featured: false, tag: '' },
]
const loading = ref(true)
const paying = ref(false)
const error = ref('')
const me = ref(null)
const planLabel = computed(() => {
const p = me.value?.plan
if (p === 'quarter') return '季卡'
if (p === 'year') return '年卡'
if (p === 'month') return '月卡'
return p || '成长会员'
})
async function load() {
loading.value = true
error.value = ''
if (!(await ensureAccount('/membership'))) {
loading.value = false
return
}
try {
me.value = await yxgApi.getMembership()
} catch (e) {
error.value = e instanceof Error ? e.message : '加载失败'
} finally {
loading.value = false
}
}
async function subscribe(plan) {
if (paying.value) return
paying.value = true
error.value = ''
try {
const { order_id } = await yxgApi.createOrder({ kind: 'membership', plan })
await yxgApi.payMock(order_id)
me.value = await yxgApi.getMembership()
uni.showToast({ title: '开通成功', icon: 'none' })
} catch (e) {
error.value = e instanceof Error ? e.message : '支付失败'
} finally {
paying.value = false
}
}
onShow(load)
</script>
<style>
page { background-color: #ffd1c7; }
</style>
<style scoped>
.hint, .err, .note { display: block; padding: 12px 16px; font-size: 13px; color: #999; }
.err { color: #e54d42; }
.card { margin: 12px 16px; padding: 16px; background: #fff; border-radius: 16px; }
.card.on { background: linear-gradient(145deg, #fff4e0, #ffd98a); }
.h1 { display: block; font-size: 17px; font-weight: 700; color: #8a5a18; }
.meta, .lead { display: block; margin-top: 6px; font-size: 13px; color: #666; }
.sec { display: block; margin: 16px 16px 8px; font-size: 16px; font-weight: 700; color: #333; }
.plan {
margin: 0 16px 10px; padding: 14px; background: #fff; border-radius: 16px; position: relative;
}
.plan.featured { box-shadow: 0 4px 14px rgba(200, 146, 58, 0.15); }
.tag {
position: absolute; top: 10px; right: 10px; font-size: 10px; color: #fff;
background: #e54d42; padding: 2px 6px; border-radius: 6px;
}
.pl { display: block; font-size: 16px; font-weight: 700; color: #333; }
.pp { display: block; margin-top: 4px; font-size: 13px; color: #e54d42; }
.pd { display: block; margin-top: 2px; font-size: 12px; color: #999; }
</style>