195 lines
7.1 KiB
Plaintext
195 lines
7.1 KiB
Plaintext
<template>
|
||
<view class="my-subs">
|
||
<view class="header">
|
||
<text class="title">我的订阅</text>
|
||
<button class="ghost" @click="goPlanList">订阅更多</button>
|
||
</view>
|
||
|
||
<view v-if="loading" class="loading">加载中...</view>
|
||
<view v-else-if="items.length === 0" class="empty">暂无订阅</view>
|
||
|
||
<view v-else class="list">
|
||
<view class="card" v-for="s in items" :key="s['id']">
|
||
<view class="row between">
|
||
<text class="name">{{ getPlanName(s) }}</text>
|
||
<text class="status" :class="'st-' + getSubscriptionStatus(s)">{{ statusText(getSubscriptionStatus(s)) }}</text>
|
||
</view>
|
||
<view class="row">
|
||
<text class="label">周期</text>
|
||
<text class="value">{{ getBillingPeriodText(s) }}</text>
|
||
</view>
|
||
<view class="row">
|
||
<text class="label">价格</text>
|
||
<text class="value">¥{{ getPlanPrice(s) }}</text>
|
||
</view>
|
||
<view class="row">
|
||
<text class="label">开始</text>
|
||
<text class="value">{{ fmt(s.getString('start_date')) }}</text>
|
||
</view>
|
||
<view class="row">
|
||
<text class="label">下次扣费</text>
|
||
<text class="value">{{ fmt(s.getString('next_billing_date')) }}</text>
|
||
</view>
|
||
<view class="actions">
|
||
<label class="toggle">
|
||
<switch :checked="getAutoRenew(s)" @change="(e: UniSwitchChangeEvent) => toggleAutoRenew(s, e.detail.value as boolean)" />
|
||
<text class="toggle-text">自动续费</text>
|
||
</label>
|
||
<button class="danger" @click="cancelAtPeriodEnd(s)" :disabled="getSubscriptionStatus(s) !== 'active'">到期取消</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, onMounted } from 'vue'
|
||
import supaClient from '@/components/supadb/aksupainstance.uts'
|
||
import { getCurrentUserId } from '@/utils/store.uts'
|
||
|
||
const loading = ref<boolean>(true)
|
||
const items = ref<Array<UTSJSONObject>>([])
|
||
|
||
function getPlanObj(s: UTSJSONObject): UTSJSONObject | null {
|
||
const planRaw = s.get('plan')
|
||
if (planRaw == null) return null
|
||
if (planRaw instanceof UTSJSONObject) return planRaw as UTSJSONObject
|
||
return JSON.parse(JSON.stringify(planRaw)) as UTSJSONObject
|
||
}
|
||
|
||
function getPlanName(s: UTSJSONObject): string {
|
||
const planObj = getPlanObj(s)
|
||
return planObj != null ? (planObj.getString('name') ?? '订阅') : '订阅'
|
||
}
|
||
|
||
function getSubscriptionStatus(s: UTSJSONObject): string {
|
||
return s.getString('status') ?? 'active'
|
||
}
|
||
|
||
function getBillingPeriodText(s: UTSJSONObject): string {
|
||
const planObj = getPlanObj(s)
|
||
const period = planObj != null ? (planObj.getString('billing_period') ?? 'monthly') : 'monthly'
|
||
return period === 'yearly' ? '年付' : '月付'
|
||
}
|
||
|
||
function getPlanPrice(s: UTSJSONObject): string {
|
||
const planObj = getPlanObj(s)
|
||
const price = planObj != null ? (planObj.getNumber('price') ?? 0) : 0
|
||
return price.toString()
|
||
}
|
||
|
||
function getAutoRenew(s: UTSJSONObject): boolean {
|
||
return s.getBoolean('auto_renew') ?? false
|
||
}
|
||
|
||
const fmt = (s: string | null): string => {
|
||
if (s == null || s.length === 0) return '-'
|
||
const d = new Date(s)
|
||
if (Number.isNaN(d.getTime())) return '-'
|
||
return `${d.getFullYear()}-${(d.getMonth()+1).toString().padStart(2,"0")}-${d.getDate().toString().padStart(2,"0")}`
|
||
}
|
||
|
||
const statusText = (st: string): string => {
|
||
if (st === 'trial') return '试用'
|
||
if (st === 'active') return '生效'
|
||
if (st === 'past_due') return '逾期'
|
||
if (st === 'canceled') return '已取消'
|
||
if (st === 'expired') return '已过期'
|
||
return st
|
||
}
|
||
|
||
const loadSubs = async () => {
|
||
try {
|
||
loading.value = true
|
||
const userId = getCurrentUserId()
|
||
if (userId == null || userId.length === 0) {
|
||
items.value = []
|
||
return
|
||
}
|
||
// join: ml_user_subscriptions + ml_subscription_plans
|
||
const res = await supaClient
|
||
.from('ml_user_subscriptions')
|
||
.select('*, plan:ml_subscription_plans(*)', {})
|
||
.eq('user_id', userId)
|
||
.order('created_at', { ascending: false })
|
||
.execute()
|
||
items.value = Array.isArray(res.data) ? (res.data as Array<UTSJSONObject>) : []
|
||
} catch (e) {
|
||
console.error('加载订阅失败:', e)
|
||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const toggleAutoRenew = async (s: UTSJSONObject, value: boolean) => {
|
||
try {
|
||
const id = s.getString('id') ?? ''
|
||
const res = await supaClient
|
||
.from('ml_user_subscriptions')
|
||
.update({ auto_renew: value })
|
||
.eq('id', id)
|
||
.execute()
|
||
if (res.error != null) throw new Error(res.error?.message ?? '未知错误')
|
||
s.set('auto_renew', value)
|
||
uni.showToast({ title: value ? '已开启自动续费' : '已关闭自动续费', icon: 'success' })
|
||
} catch (e) {
|
||
console.error('更新自动续费失败:', e)
|
||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
const cancelAtPeriodEnd = async (s: UTSJSONObject) => {
|
||
try {
|
||
const id = s.getString('id') ?? ''
|
||
const res = await supaClient
|
||
.from('ml_user_subscriptions')
|
||
.update({ cancel_at_period_end: true })
|
||
.eq('id', id)
|
||
.execute()
|
||
if (res.error != null) throw new Error(res.error?.message ?? '未知错误')
|
||
s.set('cancel_at_period_end', true)
|
||
s.set('status', 'active')
|
||
uni.showToast({ title: '已设置到期取消', icon: 'success' })
|
||
} catch (e) {
|
||
console.error('设置到期取消失败:', e)
|
||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
const goPlanList = () => {
|
||
uni.navigateTo({ url: '/pages/mall/consumer/subscription/plan-list' })
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadSubs()
|
||
})
|
||
// 注意:uni-app x 的 <script setup> 中不支持 onShow,使用 onMounted 代替
|
||
// 如果需要页面显示时刷新,可以在页面选项中定义 onShow
|
||
</script>
|
||
|
||
<style scoped>
|
||
.my-subs { padding: 12px; }
|
||
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
||
.title { font-size: 18px; font-weight: 700; }
|
||
.ghost { background: #fff; border: 1px solid #ddd; color: #333; border-radius: 6px; padding: 6px 10px; }
|
||
.loading, .empty { padding: 24px; text-align: center; color: #888; }
|
||
.list { display: flex; flex-direction: column; }
|
||
.card { background: #fff; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); margin-bottom: 12px; }
|
||
.card:last-child { margin-bottom: 0; }
|
||
.row { display: flex; padding: 4px 0; }
|
||
.label { margin-right: 8px; }
|
||
.between { justify-content: space-between; align-items: center; }
|
||
.name { font-size: 16px; font-weight: 700; }
|
||
.status { font-size: 12px; padding: 2px 8px; border-radius: 999px; background: #eee; color: #333; }
|
||
.st-trial { background: #e6f7ff; color: #1677ff; }
|
||
.st-active { background: #f6ffed; color: #52c41a; }
|
||
.st-past_due { background: #fff7e6; color: #fa8c16; }
|
||
.st-canceled, .st-expired { background: #fff1f0; color: #f5222d; }
|
||
.label { color: #666; width: 80px; }
|
||
.value { color: #111; flex: 1; }
|
||
.actions { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; }
|
||
.toggle { display: flex; align-items: center; }
|
||
.toggle-text { margin-right: 6px; }
|
||
.danger { background: #f5222d; color: #fff; border-radius: 6px; padding: 6px 10px; }
|
||
</style> |