chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,266 @@
.plan-cards {
margin-top: 24rpx;
.section-title-container {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 36rpx;
margin-bottom: 24rpx;
.section-title-bar {
width: 6rpx;
height: 30rpx;
background: linear-gradient(180deg, #1e6deb 0%, #008b70 100%);
border-radius: 4rpx;
margin-right: 12rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #1d2129;
line-height: 32rpx;
}
}
.cards-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.plan-card {
background: #fff;
border-radius: 16rpx;
padding: 0;
border: 2rpx solid #e5e6eb;
position: relative;
overflow: hidden;
&.plan-current {
border: 2rpx solid #1e6deb;
background: rgba(30, 109, 235, 0.02);
}
.plan-badges {
position: absolute;
top: -2rpx;
right: -2rpx;
z-index: 10;
.badge {
height: 52rpx;
padding: 0 28rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
border-radius: 0 16rpx 0 24rpx;
.badge-text {
font-size: 24rpx;
font-weight: 600;
color: #fff;
line-height: 24rpx;
}
&.badge-active {
background: #1e6deb;
}
&.badge-hot {
background: linear-gradient(135deg, #ff9465 0%, #ff5252 100%);
}
}
}
.plan-card-body {
padding: 36rpx 32rpx 40rpx 32rpx;
display: flex;
flex-direction: column;
.plan-name {
font-size: 32rpx;
font-weight: 600;
color: #1d2129;
margin-bottom: 8rpx;
padding-right: 140rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.plan-desc {
font-size: 24rpx;
color: #86909c;
margin-bottom: 16rpx;
line-height: 1.4;
padding-right: 140rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.plan-price-row {
display: flex;
flex-direction: row;
align-items: baseline;
margin-bottom: 12rpx;
.plan-price-symbol {
font-size: 32rpx;
font-weight: 700;
color: #1e6deb;
margin-right: 4rpx;
}
.plan-price-value {
font-size: 56rpx;
font-weight: 800;
color: #1e6deb;
line-height: 56rpx;
}
.plan-period {
font-size: 26rpx;
color: #4e5969;
font-weight: 500;
margin-left: 8rpx;
}
}
.plan-credits-tag {
align-self: flex-start;
background: #e8f3ff;
border-radius: 8rpx;
padding: 6rpx 16rpx;
margin-bottom: 24rpx;
display: flex;
flex-direction: row;
align-items: center;
.plan-credits-text {
font-size: 24rpx;
font-weight: 600;
color: #1e6deb;
line-height: 24rpx;
}
}
.benefits {
margin-bottom: 24rpx;
.benefit-group {
display: flex;
flex-direction: column;
gap: 20rpx;
.benefit-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 16rpx;
.benefit-icon-container {
width: 32rpx;
height: 32rpx;
border-radius: 16rpx;
background-color: #f2f3f5;
display: flex;
align-items: center;
justify-content: center;
.benefit-icon-text {
font-size: 18rpx;
font-weight: bold;
line-height: 18rpx;
}
&.icon-check {
.benefit-icon-text {
color: #4e5969;
}
}
&.icon-cross {
.benefit-icon-text {
color: #86909c;
}
}
}
.benefit-text {
font-size: 26rpx;
color: #1d2129;
line-height: 36rpx;
}
}
}
}
.plan-action-area {
margin-bottom: 32rpx;
width: 100%;
.plan-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;
}
&.plan-btn-processing {
opacity: 0.6;
}
&.plan-btn-current {
background: #efebe6;
.plan-btn-text {
color: #5c564f;
}
}
&.plan-btn-disabled {
background: #f5f5f5;
box-shadow: none;
pointer-events: none;
.plan-btn-text {
color: #c0c4cc;
}
&:active {
transform: none;
opacity: 1;
}
}
.plan-btn-text {
font-size: 26rpx;
color: #fff;
font-weight: 600;
line-height: 26rpx;
}
}
}
.plan-divider {
margin: 8rpx 0 24rpx;
border-top: 2rpx dashed #e5e6eb;
}
}
}
}

View File

@@ -0,0 +1,178 @@
<template>
<view class="plan-cards">
<view class="section-title-container">
<view class="section-title-bar"></view>
<text class="section-title">{{
t("Mobile.MySubscriptions.planGrid")
}}</text>
</view>
<view class="cards-list">
<view
v-for="plan in plans"
:key="plan.id"
class="plan-card"
:class="{ 'plan-current': plan.id === currentPlanId }"
>
<!-- 标签 -->
<view class="plan-badges">
<view v-if="plan.id === currentPlanId" class="badge badge-active">
<text class="badge-text">{{
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="plan-card-body">
<!-- 套餐信息 -->
<text class="plan-name">{{ plan.name }}</text>
<text v-if="plan.description" class="plan-desc">{{
plan.description
}}</text>
<view class="plan-price-row">
<text class="plan-price-symbol">¥</text>
<text class="plan-price-value">{{
formatNumber(plan.price, 2)
}}</text>
<text class="plan-period">{{ getPeriodUnit(plan.period) }}</text>
</view>
<view class="plan-credits-tag">
<text class="plan-credits-text">
{{
`${formatNumber(plan.creditAmount, 0)} ${t("Mobile.MySubscriptions.creditUnit")}${t("Mobile.MySubscriptions.perMonth")}`
}}
</text>
</view>
<!-- 按钮(移入副标题及价格信息下方,与 PC 端保持一致) -->
<view class="plan-action-area">
<view
class="plan-btn"
:class="{
'plan-btn-processing': processingId === plan.id,
'plan-btn-current': plan.id === currentPlanId,
'plan-btn-disabled':
plan.price <= 0 && plan.id !== currentPlanId,
}"
@tap="handleAction(plan)"
>
<text class="plan-btn-text">{{ getButtonText(plan) }}</text>
</view>
</view>
<!-- 分割线(与 PC 端保持一致) -->
<view class="plan-divider"></view>
<!-- 权益列表 -->
<view class="benefits" v-if="getBaseBenefits(plan).length > 0">
<view class="benefit-group">
<view
v-for="(item, index) in getBaseBenefits(plan)"
:key="index"
class="benefit-item"
>
<view
class="benefit-icon-container"
:class="item.selected ? 'icon-check' : 'icon-cross'"
>
<text class="benefit-icon-text">{{
item.selected ? "✓" : "✗"
}}</text>
</view>
<text class="benefit-text">{{ item.description }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import type {
SystemSubscriptionPlan,
SystemSubscriptionPlanGroup,
SystemSubscriptionPlanItem,
} from "@/subpackages/types/interfaces/subscription";
import { useI18n } from "@/utils/i18n";
import { getPeriodUnit } from "../../utils.uts";
import { formatDate, formatNumber } from "@/utils/system";
import { useSubscriptionPurchase } from "../../hooks/useSubscriptionPurchase";
const { t } = useI18n();
const { processingId, handlePaySubscription } = useSubscriptionPurchase();
interface Props {
plans: SystemSubscriptionPlan[];
currentPlanId: number | null;
currentEndTime: string | null;
currentPlanPrice?: number | null;
}
const props = withDefaults(defineProps<Props>(), {
currentPlanPrice: null,
});
const getActionVerb = (planPrice: number): string => {
if (props.currentPlanId == null) {
return t("Mobile.MySubscriptions.subscribeNow");
}
const currentPrice = props.currentPlanPrice ?? 0;
if (planPrice <= currentPrice) {
return t("Mobile.MySubscriptions.subscribeNow");
}
return t("Mobile.MySubscriptions.upgradeNow");
};
const getButtonText = (plan: SystemSubscriptionPlan): string => {
if (plan.id === props.currentPlanId) {
const currentPlanBtnText = t("Mobile.MySubscriptions.currentPlanButton");
const renewNowText = t("Mobile.MySubscriptions.renewNow");
return `${currentPlanBtnText}(${renewNowText})`;
}
const verbText = getActionVerb(plan.price);
let periodText = t("Mobile.MySubscriptions.continuousMonthly");
if (plan.period === 3) {
periodText = t("Mobile.MySubscriptions.continuousQuarterly");
} else if (plan.period === 12) {
periodText = t("Mobile.MySubscriptions.continuousYearly");
}
return `${verbText}${plan.name}${periodText}`;
};
const handleAction = async (plan: SystemSubscriptionPlan) => {
if (plan.price <= 0 && plan.id !== props.currentPlanId) return;
await handlePaySubscription(plan.id);
};
const getBaseBenefits = (
plan: SystemSubscriptionPlan,
): SystemSubscriptionPlanItem[] => {
const list = [] as SystemSubscriptionPlanItem[];
const groups = plan.itemGroups;
if (groups != null) {
for (let i = 0; i < groups.length; i++) {
const g = groups[i];
if (g.groupType === "BASE" && g.items != null) {
for (let j = 0; j < g.items.length; j++) {
list.push(g.items[j]);
}
}
}
}
return list;
};
</script>
<style lang="scss" scoped>
@import "./plan-cards.scss";
</style>