Files
qiming/qiming-mobile/subpackages/pages/chat-conversation-component/components/agent-subscription-modal/agent-subscription-modal.uvue

561 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<modal-popup
ref="modalRef"
:title="t('Mobile.Chat.subscriptionModalTitle')"
width="90vw"
@update-visible="handleUpdateVisible"
>
<view class="agent-sub-container">
<!-- 骨架屏 / 加载状态 -->
<view v-if="loading" class="loading-state">
<text class="loading-icon">⏳</text>
<text class="loading-text">{{ t("Mobile.Common.loading") }}</text>
</view>
<!-- 空状态 -->
<view v-else-if="plans.length === 0" class="empty-state">
<text class="empty-icon">📁</text>
<text class="empty-text">{{ t("Mobile.MySubscriptions.empty") }}</text>
</view>
<!-- 套餐卡片列表 -->
<scroll-view
v-else
class="plans-scroll"
scroll-y="true"
:show-scrollbar="false"
>
<view class="plans-list">
<view
v-for="plan in plans"
:key="plan.id"
class="plan-card"
:class="{
'plan-current': plan.id === activePlanId,
'plan-expired':
plan.id === activePlanId &&
currentSubscription?.status === 'EXPIRED',
'plan-cancelled':
plan.id === activePlanId &&
currentSubscription?.status === 'CANCELLED',
}"
>
<!-- 热门标签 / 订阅状态标签 -->
<view
class="card-badge-row"
v-if="plan.id === activePlanId || plan.isHot"
>
<view
v-if="plan.id === activePlanId"
class="badge"
:class="{
'badge-active':
currentSubscription == null ||
currentSubscription.status === 'ACTIVE',
'badge-expired':
currentSubscription != null &&
currentSubscription.status === 'EXPIRED',
'badge-cancelled':
currentSubscription != null &&
currentSubscription.status === 'CANCELLED',
}"
>
<text class="badge-text">{{
currentSubscription != null &&
plan.id === currentSubscription.planId
? getSubscriptionStatusText(currentSubscription.status)
: t("Mobile.MySubscriptions.currentPlan")
}}</text>
</view>
<view v-else-if="plan.isHot" class="badge badge-hot">
<text class="badge-text">{{
t("Mobile.MySubscriptions.statusHot")
}}</text>
</view>
</view>
<!-- 计划核心信息 -->
<view class="card-main">
<view class="plan-info-header">
<text class="plan-name">{{ plan.name }}</text>
<text class="plan-desc" v-if="plan.description">{{
plan.description
}}</text>
</view>
<!-- 价格与计费周期 -->
<view class="plan-price-box">
<text class="price-symbol">¥</text>
<text class="price-value">{{
formatNumber(plan.price, 2)
}}</text>
<text class="price-period">{{
getPeriodUnit(plan.period)
}}</text>
</view>
<!-- 权益清单 -->
<view class="plan-limits">
<view class="limit-item" v-if="plan.callLimitCount != null">
<text class="limit-bullet">✦</text>
<text class="limit-label"
>{{ t("Mobile.Chat.callLimitLabel") }}</text
>
<text class="limit-val">{{
plan.callLimitCount === -1
? t("Mobile.Chat.unlimited")
: plan.callLimitCount + t("Mobile.Chat.timesPerMonth")
}}</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="card-action">
<view
class="action-btn"
:class="{
'btn-current':
plan.id === activePlanId &&
currentSubscription?.status !== 'CANCELLED',
'btn-processing': processingId === plan.id,
'btn-disabled': plan.price <= 0,
}"
@tap="handleSelectPlan(plan)"
>
<text class="btn-text">{{ getActionText(plan) }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</modal-popup>
</template>
<script setup lang="uts">
import { ref, computed } from "vue";
import { useI18n } from "@/utils/i18n";
import {
apiGetAgentSubscriptionPlanList,
apiGetMySubscription,
} from "@/subpackages/servers/subscription";
import {
SystemSubscriptionPlan,
MySubscriptionItem,
} from "@/subpackages/types/interfaces/subscription";
import { formatNumber } from "@/utils/system";
import { useSubscriptionPurchase } from "@/subpackages/pages/my-subscriptions/hooks/useSubscriptionPurchase.uts";
import { getPeriodUnit } from "@/subpackages/pages/my-subscriptions/utils.uts";
import { SUCCESS_CODE } from "@/constants/codes.constants.uts";
const { t } = useI18n();
const props = defineProps({
agentId: {
type: Number,
required: true,
},
currentPlanId: {
type: Number,
default: null,
},
currentPlanPrice: {
type: Number,
default: null,
},
});
const emit = defineEmits(["purchase-success", "update-visible"]);
const handleUpdateVisible = (visible: boolean) => {
emit("update-visible", visible);
};
const modalRef = ref<any>(null);
const loading = ref(false);
const plans = ref<SystemSubscriptionPlan[]>([]);
const { handlePaySubscription, processingId } = useSubscriptionPurchase();
const currentSubscription = ref<MySubscriptionItem | null>(null);
/**
* 动态计算生效中的 planId
*/
const activePlanId = computed((): number | null => {
if (currentSubscription.value != null) {
return currentSubscription.value!.planId;
}
return props.currentPlanId;
});
/**
* 动态计算生效中的 plan 价格,便于判断升级关系
*/
const activePlanPrice = computed((): number | null => {
if (
currentSubscription.value != null &&
currentSubscription.value!.plan != null
) {
return currentSubscription.value!.plan.price;
}
return props.currentPlanPrice;
});
/**
* 获取订阅状态文本
*/
const getSubscriptionStatusText = (status: string): string => {
if (status === "ACTIVE") {
return t("Mobile.MySubscriptions.stateActive");
}
if (status === "EXPIRED") {
return t("Mobile.MySubscriptions.stateExpired");
}
if (status === "CANCELLED") {
return t("Mobile.MySubscriptions.stateCancelled");
}
return "";
};
/**
* 计算操作按钮的文本(对齐 PC 端逻辑)
* - 当前使用中套餐(ACTIVE/EXPIRED): "当前套餐(续订)",支持点击续订
* - 当前套餐已取消(CANCELLED): "订阅"
* - 无订阅 / 价格 <= 当前使用金额: "订阅"
* - 价格 > 当前使用金额: "升级"
*/
const getActionText = (plan: SystemSubscriptionPlan): string => {
const curPlanId = activePlanId.value;
if (plan.id === curPlanId) {
if (currentSubscription.value != null) {
const status = currentSubscription.value!.status;
if (status === "CANCELLED") {
return t("Mobile.MySubscriptions.subscribeNow");
}
}
// 使用中/已过期的当前套餐:显示"当前套餐(续订)"
const currentPlanText = t("Mobile.MySubscriptions.currentPlanButton");
const renewText = t("Mobile.MySubscriptions.renewNow");
return `${currentPlanText}(${renewText})`;
}
if (curPlanId == null) {
return t("Mobile.MySubscriptions.subscribeNow");
}
// 使用当前生效套餐的金额作为判断基准
const currentPrice = activePlanPrice.value ?? 0;
if (plan.price <= currentPrice) {
return t("Mobile.MySubscriptions.subscribeNow");
}
return t("Mobile.MySubscriptions.upgradeNow");
};
/**
* 加载智能体订阅及套餐列表
*/
const fetchPlans = async () => {
if (props.agentId <= 0) return;
loading.value = true;
try {
// 1. 获取当前智能体订阅情况
const subRes = await apiGetMySubscription({
bizId: props.agentId.toString(),
bizType: "AGENT",
});
if (subRes.code === SUCCESS_CODE && subRes.data != null) {
currentSubscription.value = subRes.data!.currentSubscription;
} else {
currentSubscription.value = null;
}
// 2. 获取套餐列表
const res = await apiGetAgentSubscriptionPlanList({
agentId: props.agentId,
status: 1,
});
if (res.code === SUCCESS_CODE && res.data != null) {
plans.value = res.data!;
}
} catch (e) {
console.error("[AgentSubscriptionModal] 加载数据失败:", e);
} finally {
loading.value = false;
}
};
const handleSelectPlan = async (plan: SystemSubscriptionPlan) => {
if (plan.price <= 0) return;
await handlePaySubscription(plan.id);
};
/**
* 打开弹窗
*/
const open = () => {
modalRef.value?.open();
fetchPlans();
};
/**
* 关闭弹窗
*/
const close = () => {
modalRef.value?.close();
};
defineExpose({
open,
close,
});
</script>
<style lang="scss" scoped>
.agent-sub-container {
border-radius: 24rpx;
display: flex;
flex-direction: column;
max-height: 70vh;
}
.loading-state,
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 0;
.loading-icon,
.empty-icon {
font-size: 56rpx;
margin-bottom: 16rpx;
}
.loading-text,
.empty-text {
font-size: 26rpx;
color: #999;
}
}
.plans-scroll {
flex: 1;
width: 100%;
}
.plans-list {
display: flex;
flex-direction: column;
gap: 24rpx;
padding: 4rpx;
}
.plan-card {
position: relative;
background-color: #ffffff;
border: 2rpx solid #eaeaea;
border-radius: 24rpx;
padding: 24rpx;
display: flex;
flex-direction: column;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.03);
transition: all 0.25s ease;
&.plan-current {
border-color: #007aff;
background-color: #f7faff;
box-shadow: 0 8rpx 32rpx rgba(0, 122, 255, 0.08);
}
&.plan-expired {
border-color: #86909c;
background-color: #f2f3f5;
box-shadow: 0 8rpx 32rpx rgba(134, 144, 156, 0.08);
}
&.plan-cancelled {
border-color: #f53f3f;
background-color: #fff7f7;
box-shadow: 0 8rpx 32rpx rgba(245, 63, 63, 0.08);
}
}
.card-badge-row {
position: absolute;
top: -2rpx;
right: -2rpx;
z-index: 10;
display: flex;
flex-direction: row;
.badge {
height: 48rpx;
padding: 0 24rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
border-radius: 0 24rpx 0 32rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
&-active {
background: linear-gradient(135deg, #007aff, #0056b3);
}
&-expired {
background: linear-gradient(135deg, #86909c, #4e5969);
}
&-cancelled {
background: linear-gradient(135deg, #f53f3f, #b71c1c);
}
&-hot {
background: linear-gradient(135deg, #ff9500, #ff5e00);
}
.badge-text {
font-size: 20rpx;
color: #ffffff;
font-weight: bold;
line-height: 1;
}
}
}
.card-main {
flex: 1;
margin-bottom: 24rpx;
}
.plan-info-header {
display: flex;
flex-direction: column;
gap: 8rpx;
margin-bottom: 16rpx;
padding-right: 120rpx;
.plan-name {
font-size: 32rpx;
color: #333333;
font-weight: bold;
}
.plan-desc {
font-size: 22rpx;
color: #777777;
line-height: 1.4;
}
}
.plan-price-box {
display: flex;
flex-direction: row;
align-items: baseline;
margin-bottom: 24rpx;
.price-symbol {
font-size: 28rpx;
font-weight: bold;
margin-right: 4rpx;
}
.price-value {
font-size: 48rpx;
font-weight: 800;
}
.price-period {
font-size: 26rpx;
color: #888888;
margin-left: 8rpx;
}
}
.plan-limits {
display: flex;
flex-direction: column;
gap: 12rpx;
background-color: #f9f9f9;
border-radius: 16rpx;
padding: 16rpx 20rpx;
.limit-item {
display: flex;
flex-direction: row;
align-items: center;
font-size: 24rpx;
.limit-bullet {
color: #007aff;
margin-right: 12rpx;
font-weight: bold;
}
.limit-label {
color: #666666;
}
.limit-val {
color: #333333;
font-weight: 600;
}
}
}
.card-action {
width: 100%;
.action-btn {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
height: 76rpx;
background: #1e6deb;
border-radius: 12rpx;
box-shadow: none;
box-sizing: border-box;
&:active {
transform: scale(0.98);
opacity: 0.9;
}
&.btn-current {
background: #efebe6;
.btn-text {
color: #5c564f;
}
}
&.btn-processing {
opacity: 0.6;
}
&.btn-disabled {
background: #f5f5f5;
box-shadow: none;
pointer-events: none;
.btn-text {
color: #c0c4cc;
}
&:active {
transform: none;
opacity: 1;
}
}
.btn-text {
font-size: 26rpx;
color: #fff;
font-weight: 600;
line-height: 26rpx;
}
}
}
</style>