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,314 @@
<template>
<view class="subscribed-agents">
<view v-if="loading" class="loading-state">
<text class="loading-text">...</text>
</view>
<view v-else-if="list.length === 0" class="empty-wrapper">
<empty-state text="Mobile.MySubscriptions.empty" />
</view>
<view v-else class="agent-list">
<view v-for="item in list" :key="item.id" class="agent-card">
<!-- 头部 -->
<view class="agent-header">
<view
class="agent-icon"
:style="{ backgroundColor: item.icon ? 'transparent' : '#1e6deb' }"
>
<image
v-if="item.icon"
:src="item.icon"
class="agent-icon-img"
mode="aspectFill"
/>
<text v-else class="iconfont icon-Grid"></text>
</view>
<view class="agent-title-box">
<text class="agent-name">{{ item.bizName }}</text>
<text class="agent-desc" v-if="item.planName">{{
item.planName
}}</text>
</view>
</view>
<!-- 信息网格 -->
<view class="agent-grid">
<view class="grid-item">
<text class="grid-label">{{
t("Mobile.MySubscriptions.subAmount")
}}</text>
<view class="grid-value-row">
<text class="amount-val"
>¥{{ formatNumber(item.plan?.price ?? 0, 2) }}</text
>
<text class="amount-unit">{{ getPeriodUnit(item.period) }}</text>
</view>
</view>
<view class="grid-item" v-if="item.endTime">
<text class="grid-label">{{
t("Mobile.MySubscriptions.expireTime")
}}</text>
<text class="grid-value">{{
formatDate(item.endTime || "", "YYYY-MM-DD")
}}</text>
</view>
</view>
<!-- 底部 -->
<view class="agent-footer">
<view
class="status-text"
:class="isActive(item.status) ? 'status-active' : 'status-expired'"
>
<text class="icon-char">{{
isActive(item.status) ? "✓" : "✗"
}}</text>
<text class="text">{{ getStatusText(item.status) }}</text>
</view>
<view class="renew-btn" @tap="handleRenew(item)">
<text class="btn-text">{{
t("Mobile.MySubscriptions.renewNow")
}}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import type { MySubscriptionItem } from "@/subpackages/types/interfaces/subscription";
import { SUCCESS_CODE } from "@/constants/codes.constants.uts";
import { apiGetMySubscription } from "@/subpackages/servers/subscription";
import { useI18n } from "@/utils/i18n";
import { useSubscriptionPurchase } from "../../../hooks/useSubscriptionPurchase.uts";
import { getPeriodUnit, getStatusText, isActive } from "../../../utils.uts";
import EmptyState from "@/components/empty-state/empty-state.uvue";
import { formatDate, formatNumber } from "@/utils/system";
const { t } = useI18n();
const list = ref<MySubscriptionItem[]>([]);
const loading = ref(true);
const fetchData = async () => {
loading.value = true;
try {
const res = await apiGetMySubscription({ bizType: "AGENT" });
if (res.code === SUCCESS_CODE && res.data) {
list.value = res.data.subscriptions || [];
}
} catch (e) {
// ignore
} finally {
loading.value = false;
}
};
const { handlePaySubscription, processingId } = useSubscriptionPurchase();
const handleRenew = async (item: MySubscriptionItem) => {
await handlePaySubscription(item.planId);
};
onMounted(() => {
fetchData();
});
</script>
<style lang="scss" scoped>
.subscribed-agents {
min-height: 200rpx;
.loading-state {
display: flex;
align-items: center;
justify-content: center;
padding: 80rpx 0;
.loading-text {
font-size: 28rpx;
color: #999;
}
}
.empty-wrapper {
padding: 40rpx 0;
}
.agent-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.agent-card {
display: flex;
flex-direction: column;
background: #fff;
border-radius: 16rpx;
padding: 28rpx 28rpx 0 28rpx;
border: 2rpx solid #e5e6eb;
overflow: hidden;
.agent-header {
display: flex;
flex-direction: row;
align-items: center;
gap: 24rpx;
margin-bottom: 40rpx;
.agent-icon {
width: 96rpx;
height: 96rpx;
border-radius: 20rpx;
background: #1e6deb;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 16rpx rgba(30, 109, 235, 0.15);
overflow: hidden;
.agent-icon-img {
width: 100%;
height: 100%;
border-radius: 20rpx;
}
.iconfont {
font-size: 48rpx;
color: #fff;
}
}
.agent-title-box {
display: flex;
flex-direction: column;
gap: 12rpx;
flex: 1;
overflow: hidden;
.agent-name {
font-size: 32rpx;
font-weight: 600;
color: #1d2129;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-desc {
font-size: 24rpx;
color: #8c96a0;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
}
}
.agent-grid {
display: flex;
flex-direction: row;
margin-bottom: 28rpx;
.grid-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 12rpx;
.grid-label {
font-size: 24rpx;
color: #8c96a0;
}
.grid-value {
font-size: 28rpx;
font-weight: 600;
color: #1d2129;
}
.grid-value-row {
display: flex;
flex-direction: row;
align-items: baseline;
.amount-val {
font-size: 28rpx;
font-weight: 600;
color: #5147ff;
}
.amount-unit {
font-size: 24rpx;
color: #8c96a0;
margin-left: 4rpx;
}
}
}
}
.agent-footer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 0 -28rpx 0 -28rpx;
padding: 24rpx 28rpx;
background: #f8fafc;
border-top: 2rpx solid #e5e6eb;
.status-text {
display: flex;
flex-direction: row;
align-items: center;
gap: 8rpx;
.icon-char {
font-size: 26rpx;
}
.text {
font-size: 26rpx;
font-weight: 500;
}
&.status-active {
color: #00b42a;
}
&.status-expired {
color: #f53f3f;
}
}
.renew-btn {
display: flex;
align-items: center;
justify-content: center;
height: 64rpx;
background: #1e6deb;
padding: 0 36rpx;
border-radius: 12rpx;
.btn-text {
font-size: 26rpx;
color: #fff;
font-weight: 600;
}
&:active {
opacity: 0.8;
transform: scale(0.98);
}
}
}
}
}
</style>

View File

@@ -0,0 +1,31 @@
.subscribed-content {
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;
}
}
.tab-panel {
margin-top: 20rpx;
min-height: 400rpx;
}
}

View File

@@ -0,0 +1,46 @@
<template>
<view class="subscribed-content">
<view class="section-title-container">
<view class="section-title-bar"></view>
<text class="section-title">{{ t("Mobile.MySubscriptions.subscribedContent") }}</text>
</view>
<segmented-control
:options="tabOptions"
:default-index="0"
@change="handleTabChange"
/>
<view class="tab-panel">
<SubscribedAgents v-show="activeTab === 'agents'" />
<SubscribedSkills v-show="activeTab === 'skills'" />
<SubscribedCredits v-show="activeTab === 'credits'" />
</view>
</view>
</template>
<script setup lang="uts">
import { useI18n } from '@/utils/i18n';
import SegmentedControl from '@/components/segmented-control/segmented-control.uvue';
import SubscribedAgents from './subscribed-agents/subscribed-agents.uvue';
import SubscribedSkills from './subscribed-skills/subscribed-skills.uvue';
import SubscribedCredits from './subscribed-credits/subscribed-credits.uvue';
const { t } = useI18n();
const activeTab = ref<string>('agents');
const tabOptions = [
{ label: 'Mobile.MySubscriptions.tabAgents', value: 'agents' },
{ label: 'Mobile.MySubscriptions.tabSkills', value: 'skills' },
{ label: 'Mobile.MySubscriptions.tabCredits', value: 'credits' },
];
const handleTabChange = (value: string | number) => {
activeTab.value = value as string;
};
</script>
<style lang="scss" scoped>
@import './subscribed-content.scss';
</style>

View File

@@ -0,0 +1,283 @@
<template>
<view class="subscribed-credits">
<view v-if="loading" class="loading-state">
<text class="loading-text">...</text>
</view>
<view v-else-if="list.length === 0" class="empty-wrapper">
<empty-state text="Mobile.MySubscriptions.empty" />
</view>
<view v-else class="credit-list">
<view v-for="item in list" :key="item.id" class="credit-card">
<!-- 头部 -->
<view class="credit-header">
<text class="credit-name">{{ item.remark || item.batchNo }}</text>
<text class="credit-date">{{
formatDate(item.created || "", "YYYY-MM-DD")
}}</text>
</view>
<!-- 数据四宫格 -->
<view class="credit-grid">
<view class="grid-item">
<text class="grid-label">{{
t("Mobile.MySubscriptions.totalCredits")
}}</text>
<text class="grid-value"
>+{{ formatNumber(item.totalAmount, 0) }}</text
>
</view>
<view class="grid-item">
<text class="grid-label">{{
t("Mobile.MySubscriptions.usedAmount")
}}</text>
<text class="grid-value">{{
formatNumber(item.usedAmount, 0)
}}</text>
</view>
<view class="grid-item">
<text class="grid-label">{{
t("Mobile.MySubscriptions.expireTime")
}}</text>
<text class="grid-value">{{
item.expireTime
? formatDate(item.expireTime || "", "YYYY-MM-DD")
: "-"
}}</text>
</view>
<view class="grid-item">
<text class="grid-label">{{
t("Mobile.MySubscriptions.purchaseAmount")
}}</text>
<text
class="grid-value highlight-price"
>{{ item.extra?.price != null ? `¥ ${formatNumber(item.extra!.price, 2)}` : '-' }}</text
>
</view>
</view>
<!-- 底部 -->
<view class="credit-footer">
<text class="footer-remain">
{{
`${t("Mobile.MySubscriptions.remaining")}${formatNumber(item.remainAmount, 0)} ${t("Mobile.MySubscriptions.creditUnit")}`
}}
</text>
<view
v-if="getCreditStatus(item)"
class="status-tag"
:class="getCreditStatus(item)?.cls"
>
<text class="status-text">{{ getCreditStatus(item)?.text }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import type { CreditBatchItem } from "@/subpackages/types/interfaces/subscription";
import { SUCCESS_CODE } from "@/constants/codes.constants.uts";
import { apiGetCreditBatches } from "@/subpackages/servers/subscription";
import { useI18n } from "@/utils/i18n";
import EmptyState from "@/components/empty-state/empty-state.uvue";
import { formatDate, formatNumber } from "@/utils/system";
const { t } = useI18n();
const list = ref<CreditBatchItem[]>([]);
const loading = ref(true);
interface CreditStatus {
text: string;
cls: string;
}
const getCreditStatus = (item: CreditBatchItem): CreditStatus | null => {
if (item.expired) {
return {
text: t("Mobile.MySubscriptions.expired"),
cls: "status-expired",
};
}
if (item.remainAmount <= 0) {
return {
text: t("Mobile.MySubscriptions.fullyUsed"),
cls: "status-expired",
};
}
const ratio = item.totalAmount > 0 ? item.usedAmount / item.totalAmount : 0;
if (ratio >= 0.8) {
return {
text: t("Mobile.MySubscriptions.aboutToUseUp"),
cls: "status-warning",
};
}
return {
text: t("Mobile.MySubscriptions.normalUsage"),
cls: "status-active",
};
};
const fetchData = async () => {
loading.value = true;
try {
const res = await apiGetCreditBatches({ creditType: "PURCHASE" });
if (res.code === SUCCESS_CODE && res.data) {
list.value = Array.isArray(res.data) ? res.data : [];
}
} catch (e) {
// ignore
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchData();
});
</script>
<style lang="scss" scoped>
.subscribed-credits {
min-height: 200rpx;
.loading-state {
display: flex;
align-items: center;
justify-content: center;
padding: 80rpx 0;
.loading-text {
font-size: 28rpx;
color: #999;
}
}
.empty-wrapper {
padding: 40rpx 0;
}
.credit-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.credit-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
overflow: hidden;
border: 2rpx solid #e5e6eb;
.credit-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
.credit-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 16rpx;
}
.credit-date {
font-size: 22rpx;
color: #999;
flex-shrink: 0;
}
}
.credit-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 16rpx;
padding: 20rpx 0;
border-top: 2rpx solid #e5e6eb;
.grid-item {
width: calc(50% - 8rpx);
display: flex;
flex-direction: column;
gap: 8rpx;
.grid-label {
font-size: 22rpx;
color: #999;
}
.grid-value {
font-size: 28rpx;
font-weight: 600;
color: #333;
&.highlight-price {
color: #f59e0b;
}
}
}
}
.credit-footer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 20rpx -24rpx -24rpx -24rpx;
padding: 24rpx 24rpx;
background: #f8fafc;
border-top: 2rpx solid #e5e6eb;
.footer-remain {
font-size: 24rpx;
color: #666;
}
.status-tag {
padding: 4rpx 16rpx;
border-radius: 6rpx;
.status-text {
font-size: 22rpx;
font-weight: 500;
}
&.status-active {
background: rgba(82, 196, 26, 0.1);
.status-text {
color: #52c41a;
}
}
&.status-warning {
background: rgba(245, 158, 11, 0.1);
.status-text {
color: #f59e0b;
}
}
&.status-expired {
background: rgba(255, 77, 79, 0.1);
.status-text {
color: #ff4d4f;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,296 @@
<template>
<view class="subscribed-skills">
<view v-if="loading" class="loading-state">
<text class="loading-text">...</text>
</view>
<view v-else-if="list.length === 0" class="empty-wrapper">
<empty-state text="Mobile.MySubscriptions.empty" />
</view>
<view v-else class="skill-list">
<view v-for="item in list" :key="item.id" class="skill-card">
<view class="skill-header">
<view class="skill-header-left">
<view class="skill-icon" :style="{ backgroundColor: item.icon ? 'transparent' : '#5147ff' }">
<image v-if="item.icon" :src="item.icon" class="skill-icon-img" mode="aspectFill" />
<text v-else class="iconfont icon-Grid"></text>
</view>
<view class="skill-title-box">
<text class="skill-name">{{ item.bizName }}</text>
<text class="skill-desc" v-if="item.planName">{{ item.planName }}</text>
</view>
</view>
<view class="skill-header-right">
<!-- 买断 -->
<view v-if="isForever(item.period)" class="tag tag-bought">
<text class="tag-text">{{
t("Mobile.MySubscriptions.boughtOut")
}}</text>
</view>
<!-- 订阅状态 -->
<view
v-else
class="status-tag"
:class="isActive(item.status) ? 'status-active' : 'status-expired'"
>
<text class="status-text">{{ getStatusText(item.status) }}</text>
</view>
</view>
</view>
<view class="skill-body">
<!-- 买断模式 -->
<view v-if="isForever(item.period)" class="skill-row">
<text class="skill-label">{{
t("Mobile.MySubscriptions.buyoutPriceLabel")
}}</text>
<text class="skill-value">{{
`¥ ${formatNumber(item.plan?.price ?? 0, 2)}`
}}</text>
</view>
<view v-if="isForever(item.period)" class="skill-row">
<text class="skill-label">{{
t("Mobile.MySubscriptions.permanentUse")
}}</text>
</view>
<!-- 订阅模式 -->
<view v-if="!isForever(item.period)" class="skill-row">
<text class="skill-label">{{
t("Mobile.MySubscriptions.subAmount")
}}</text>
<text class="skill-value">
{{
`¥ ${formatNumber(item.plan?.price ?? 0, 2)} ${getPeriodUnit(item.period)}`
}}
</text>
</view>
<view v-if="!isForever(item.period)" class="skill-row">
<text class="skill-label">{{
t("Mobile.MySubscriptions.expireTime")
}}</text>
<text class="skill-value">{{
item.endTime ? formatDate(item.endTime || "", "YYYY-MM-DD") : "-"
}}</text>
</view>
<view v-if="!isForever(item.period)" class="skill-row">
<text class="skill-label">{{ getPayTypeText(item.period) }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import type { MySubscriptionItem } from "@/subpackages/types/interfaces/subscription";
import { SUCCESS_CODE } from "@/constants/codes.constants.uts";
import { apiGetMySubscription } from "@/subpackages/servers/subscription";
import { useI18n } from "@/utils/i18n";
import {
getPeriodUnit,
getStatusText,
isActive,
isForever,
getPayTypeText,
} from "../../../utils.uts";
import EmptyState from "@/components/empty-state/empty-state.uvue";
import { formatDate, formatNumber } from "@/utils/system";
const { t } = useI18n();
const list = ref<MySubscriptionItem[]>([]);
const loading = ref(true);
const fetchData = async () => {
loading.value = true;
try {
const res = await apiGetMySubscription({ bizType: "SKILL" });
if (res.code === SUCCESS_CODE && res.data) {
list.value = res.data.subscriptions || [];
}
} catch (e) {
// ignore
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchData();
});
</script>
<style lang="scss" scoped>
.subscribed-skills {
min-height: 200rpx;
.loading-state {
display: flex;
align-items: center;
justify-content: center;
padding: 80rpx 0;
.loading-text {
font-size: 28rpx;
color: #999;
}
}
.empty-wrapper {
padding: 40rpx 0;
}
.skill-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.skill-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
.skill-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
.skill-header-left {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
gap: 16rpx;
overflow: hidden;
margin-right: 12rpx;
.skill-icon {
width: 72rpx;
height: 72rpx;
border-radius: 12rpx;
background: #5147ff;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.skill-icon-img {
width: 100%;
height: 100%;
border-radius: 12rpx;
}
.iconfont {
font-size: 36rpx;
color: #fff;
}
}
.skill-title-box {
flex: 1;
display: flex;
flex-direction: column;
gap: 4rpx;
overflow: hidden;
.skill-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.skill-desc {
font-size: 22rpx;
color: #999;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
}
}
.skill-header-right {
flex-shrink: 0;
.tag {
padding: 4rpx 16rpx;
border-radius: 6rpx;
.tag-text {
font-size: 22rpx;
font-weight: 500;
color: #5147ff;
}
&.tag-bought {
background: rgba(81, 71, 255, 0.1);
}
}
.status-tag {
padding: 4rpx 16rpx;
border-radius: 6rpx;
.status-text {
font-size: 22rpx;
font-weight: 500;
}
&.status-active {
background: rgba(82, 196, 26, 0.1);
.status-text {
color: #52c41a;
}
}
&.status-expired {
background: rgba(255, 77, 79, 0.1);
.status-text {
color: #ff4d4f;
}
}
}
}
}
.skill-body {
display: flex;
flex-direction: column;
gap: 12rpx;
.skill-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
.skill-label {
font-size: 24rpx;
color: #999;
}
.skill-value {
font-size: 24rpx;
color: #333;
font-weight: 500;
}
}
}
}
}
</style>