develop-updateui #1
@@ -1,31 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { client as rpcClient } from '@/api/rpcclient';
|
|
||||||
import AppButton from '@/components/ui/AppButton.vue';
|
import AppButton from '@/components/ui/AppButton.vue';
|
||||||
import { useAppToast } from '@/composables/useAppToast';
|
|
||||||
import { useUsageQuery } from '@/composables/useUsageQuery';
|
import { useUsageQuery } from '@/composables/useUsageQuery';
|
||||||
import { getApiErrorMessage } from '@/lib/utils';
|
|
||||||
import BillingTopupDialog from '@/routes/settings/Billing/components/BillingTopupDialog.vue';
|
import BillingTopupDialog from '@/routes/settings/Billing/components/BillingTopupDialog.vue';
|
||||||
import BillingUsageSection from '@/routes/settings/Billing/components/BillingUsageSection.vue';
|
import BillingUsageSection from '@/routes/settings/Billing/components/BillingUsageSection.vue';
|
||||||
import SettingsSectionCard from '@/routes/settings/components/SettingsSectionCard.vue';
|
import SettingsSectionCard from '@/routes/settings/components/SettingsSectionCard.vue';
|
||||||
import type { Plan as ModelPlan } from '@/server/gen/proto/app/v1/common';
|
import type { Plan as ModelPlan } from '@/server/gen/proto/app/v1/common';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { useTranslation } from 'i18next-vue';
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import SettingsRow from '../components/SettingsRow.vue';
|
import SettingsRow from '../components/SettingsRow.vue';
|
||||||
import PaymentHistory from './components/PaymentHistory';
|
import PaymentHistory from './components/PaymentHistory';
|
||||||
import PlanSelection from './components/PlanSelection';
|
import PlanSelection from './components/PlanSelection';
|
||||||
import UpgradePlan from './components/UpgradePlan';
|
import UpgradePlan from './components/UpgradePlan';
|
||||||
|
|
||||||
const toast = useAppToast();
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const { refetch: refetchUsage } = useUsageQuery();
|
const { refetch: refetchUsage } = useUsageQuery();
|
||||||
|
|
||||||
const topupDialogVisible = ref(false);
|
const topupDialogVisible = ref(false);
|
||||||
const topupAmount = ref<number | null>(null);
|
const topupAmount = ref<number | null>(null);
|
||||||
const topupLoading = ref(false);
|
|
||||||
const topupPresets = [10, 20, 50, 100];
|
|
||||||
|
|
||||||
const upgradeDialogVisible = ref(false);
|
const upgradeDialogVisible = ref(false);
|
||||||
const selectedPlan = ref<ModelPlan | null>(null);
|
const selectedPlan = ref<ModelPlan | null>(null);
|
||||||
@@ -63,41 +55,11 @@ const handleUpgradeSuccess = async () => {
|
|||||||
await refreshBillingState();
|
await refreshBillingState();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTopup = async (amount: number) => {
|
|
||||||
topupLoading.value = true;
|
|
||||||
try {
|
|
||||||
await rpcClient.topupWallet({ amount });
|
|
||||||
await refreshBillingState();
|
|
||||||
|
|
||||||
toast.add({
|
|
||||||
severity: 'success',
|
|
||||||
summary: t('settings.billing.toast.topupSuccessSummary'),
|
|
||||||
detail: t('settings.billing.toast.topupSuccessDetail', { amount: auth.formatMoney(amount) }),
|
|
||||||
life: 3000,
|
|
||||||
});
|
|
||||||
topupDialogVisible.value = false;
|
|
||||||
topupAmount.value = null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
toast.add({
|
|
||||||
severity: 'error',
|
|
||||||
summary: t('settings.billing.toast.topupFailedSummary'),
|
|
||||||
detail: getApiErrorMessage(error, t('settings.billing.toast.topupFailedDetail')),
|
|
||||||
life: 5000,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
topupLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const openTopupDialog = () => {
|
const openTopupDialog = () => {
|
||||||
topupAmount.value = null;
|
topupAmount.value = null;
|
||||||
topupDialogVisible.value = true;
|
topupDialogVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectPreset = (amount: number) => {
|
|
||||||
topupAmount.value = amount;
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -131,16 +93,7 @@ const selectPreset = (amount: number) => {
|
|||||||
<PaymentHistory />
|
<PaymentHistory />
|
||||||
</SettingsSectionCard>
|
</SettingsSectionCard>
|
||||||
|
|
||||||
<BillingTopupDialog
|
<BillingTopupDialog v-model="topupDialogVisible" />
|
||||||
:visible="topupDialogVisible"
|
|
||||||
:presets="topupPresets"
|
|
||||||
:amount="topupAmount"
|
|
||||||
:loading="topupLoading"
|
|
||||||
@update:visible="topupDialogVisible = $event"
|
|
||||||
@update:amount="topupAmount = $event"
|
|
||||||
@selectPreset="selectPreset"
|
|
||||||
@submit="handleTopup(topupAmount || 0)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UpgradePlan
|
<UpgradePlan
|
||||||
:visible="upgradeDialogVisible"
|
:visible="upgradeDialogVisible"
|
||||||
|
|||||||
@@ -1,28 +1,67 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { client } from '@/api/rpcclient';
|
||||||
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
||||||
import AppButton from '@/components/ui/AppButton.vue';
|
import AppButton from '@/components/ui/AppButton.vue';
|
||||||
import AppDialog from '@/components/ui/AppDialog.vue';
|
import AppDialog from '@/components/ui/AppDialog.vue';
|
||||||
import AppInput from '@/components/ui/AppInput.vue';
|
import AppInput from '@/components/ui/AppInput.vue';
|
||||||
|
import { useAppToast } from '@/composables/useAppToast';
|
||||||
|
import { useUsageQuery } from '@/composables/useUsageQuery';
|
||||||
|
import { getApiErrorMessage } from '@/lib/utils';
|
||||||
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
|
||||||
defineProps<{
|
const visible = defineModel<boolean>();
|
||||||
visible: boolean;
|
const toast = useAppToast();
|
||||||
presets: number[];
|
const auth = useAuthStore();
|
||||||
amount: number | null;
|
const { t } = useTranslation();
|
||||||
loading: boolean;
|
const { refetch: refetchUsage } = useUsageQuery();
|
||||||
}>();
|
|
||||||
|
const topupDialogVisible = ref(false);
|
||||||
|
const topupAmount = ref<number | null>(null);
|
||||||
|
const topupLoading = ref(false);
|
||||||
|
const topupPresets = [10, 20, 50, 100];
|
||||||
|
|
||||||
|
const refreshBillingState = async () => {
|
||||||
|
await Promise.allSettled([
|
||||||
|
auth.fetchMe(),
|
||||||
|
refetchUsage(),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTopup = async (amount: number) => {
|
||||||
|
topupLoading.value = true;
|
||||||
|
try {
|
||||||
|
await client.topupWallet({ amount });
|
||||||
|
await refreshBillingState();
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: t('settings.billing.toast.topupSuccessSummary'),
|
||||||
|
detail: t('settings.billing.toast.topupSuccessDetail', { amount: auth.formatMoney(amount) }),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
topupDialogVisible.value = false;
|
||||||
|
topupAmount.value = null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: t('settings.billing.toast.topupFailedSummary'),
|
||||||
|
detail: getApiErrorMessage(error, t('settings.billing.toast.topupFailedDetail')),
|
||||||
|
life: 5000,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
topupLoading.value = false;
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:visible', value: boolean): void;
|
|
||||||
(e: 'update:amount', value: number | null): void;
|
|
||||||
(e: 'selectPreset', amount: number): void;
|
|
||||||
(e: 'submit'): void;
|
|
||||||
}>();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppDialog
|
<AppDialog
|
||||||
:visible="visible"
|
:visible="visible!"
|
||||||
@update:visible="emit('update:visible', $event)"
|
@update:visible="visible = $event"
|
||||||
:title="$t('settings.billing.topupDialog.title')"
|
:title="$t('settings.billing.topupDialog.title')"
|
||||||
maxWidthClass="max-w-md"
|
maxWidthClass="max-w-md"
|
||||||
>
|
>
|
||||||
@@ -33,15 +72,15 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
<div class="grid grid-cols-4 gap-3">
|
<div class="grid grid-cols-4 gap-3">
|
||||||
<button
|
<button
|
||||||
v-for="preset in presets"
|
v-for="preset in topupPresets"
|
||||||
:key="preset"
|
:key="preset"
|
||||||
:class="[
|
:class="[
|
||||||
'py-2 px-3 rounded-md bg-header text-sm font-medium transition-all hover:bg-gray-500',
|
'py-2 px-3 rounded-md bg-header text-sm font-medium transition-all hover:bg-gray-500',
|
||||||
amount === preset
|
topupAmount === preset
|
||||||
? 'bg-primary text-white'
|
? 'bg-primary text-white'
|
||||||
: 'bg-muted/50 text-foreground hover:bg-muted'
|
: 'bg-muted/50 text-foreground hover:bg-muted'
|
||||||
]"
|
]"
|
||||||
@click="emit('selectPreset', preset)"
|
@click="topupAmount = preset"
|
||||||
>
|
>
|
||||||
${{ preset }}
|
${{ preset }}
|
||||||
</button>
|
</button>
|
||||||
@@ -52,15 +91,15 @@ const emit = defineEmits<{
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-lg font-semibold text-foreground">$</span>
|
<span class="text-lg font-semibold text-foreground">$</span>
|
||||||
<AppInput
|
<AppInput
|
||||||
:model-value="amount"
|
:model-value="topupAmount"
|
||||||
type="number"
|
type="number"
|
||||||
:placeholder="$t('settings.billing.topupDialog.enterAmount')"
|
:placeholder="$t('settings.billing.topupDialog.enterAmount')"
|
||||||
inputClass="flex-1"
|
inputClass="flex-1"
|
||||||
min="1"
|
min="1"
|
||||||
step="1"
|
step="1"
|
||||||
@update:model-value="emit('update:amount', typeof $event === 'number' || $event === null
|
@update:model-value="topupAmount = typeof $event === 'number' || $event === null
|
||||||
? $event
|
? $event
|
||||||
: ($event === '' ? null : Number($event)))"
|
: ($event === '' ? null : Number($event))"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,16 +114,16 @@ const emit = defineEmits<{
|
|||||||
<AppButton
|
<AppButton
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
:disabled="loading"
|
:disabled="topupLoading"
|
||||||
@click="emit('update:visible', false)"
|
@click="visible = false; topupAmount = null"
|
||||||
>
|
>
|
||||||
{{ $t('common.cancel') }}
|
{{ $t('common.cancel') }}
|
||||||
</AppButton>
|
</AppButton>
|
||||||
<AppButton
|
<AppButton
|
||||||
size="sm"
|
size="sm"
|
||||||
:loading="loading"
|
:loading="topupLoading"
|
||||||
:disabled="!amount || amount < 1 || loading"
|
:disabled="!topupAmount || topupAmount < 1 || topupLoading"
|
||||||
@click="emit('submit')"
|
@click="handleTopup(topupAmount!)"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<CheckIcon class="w-4 h-4" />
|
<CheckIcon class="w-4 h-4" />
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { client } from "@/api/rpcclient";
|
import { client } from "@/api/rpcclient";
|
||||||
import DownloadIcon from "@/components/icons/DownloadIcon.vue";
|
import DownloadIcon from "@/components/icons/DownloadIcon.vue";
|
||||||
import ListIcon from "@/components/icons/ListIcon.vue";
|
import ListIcon from "@/components/icons/ListIcon.vue";
|
||||||
|
import BaseTable from "@/components/ui/BaseTable.vue";
|
||||||
import { useAppToast } from "@/composables/useAppToast";
|
import { useAppToast } from "@/composables/useAppToast";
|
||||||
import { getApiErrorMessage, getStatusStyles } from "@/lib/utils";
|
import { getApiErrorMessage, getStatusStyles } from "@/lib/utils";
|
||||||
import { PaymentHistoryItem } from "@/server/gen/proto/app/v1/common";
|
import { PaymentHistoryItem } from "@/server/gen/proto/app/v1/common";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useQuery } from "@pinia/colada";
|
import { useQuery } from "@pinia/colada";
|
||||||
import { useTranslation } from "i18next-vue";
|
import { useTranslation } from "i18next-vue";
|
||||||
|
import type { ColumnDef } from "@tanstack/vue-table";
|
||||||
|
|
||||||
const normalizeHistoryStatus = (status?: string) => {
|
const normalizeHistoryStatus = (status?: string) => {
|
||||||
switch ((status || '').toLowerCase()) {
|
switch ((status || '').toLowerCase()) {
|
||||||
@@ -28,7 +30,7 @@ const normalizeHistoryStatus = (status?: string) => {
|
|||||||
|
|
||||||
const PaymentHistory = defineComponent({
|
const PaymentHistory = defineComponent({
|
||||||
name: 'PaymentHistory',
|
name: 'PaymentHistory',
|
||||||
setup(props, ctx) {
|
setup() {
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useAppToast();
|
const toast = useAppToast();
|
||||||
@@ -75,7 +77,7 @@ const PaymentHistory = defineComponent({
|
|||||||
key: ['paymentHistory'],
|
key: ['paymentHistory'],
|
||||||
query: () => client.listPaymentHistory().then(res => (res.payments || []).map(mapHistoryItem)),
|
query: () => client.listPaymentHistory().then(res => (res.payments || []).map(mapHistoryItem)),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDownloadInvoice = async (item: PaymentHistoryItem) => {
|
const handleDownloadInvoice = async (item: PaymentHistoryItem) => {
|
||||||
if (!item.id) return;
|
if (!item.id) return;
|
||||||
|
|
||||||
@@ -120,11 +122,87 @@ const PaymentHistory = defineComponent({
|
|||||||
downloadingInvoiceId.value = null;
|
downloadingInvoiceId.value = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const columns: ColumnDef<ReturnType<typeof mapHistoryItem>>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'date',
|
||||||
|
header: t('settings.billing.table.date'),
|
||||||
|
cell: ({ getValue }) => (
|
||||||
|
<p class="text-sm font-medium text-foreground">{getValue<string>()}</p>
|
||||||
|
),
|
||||||
|
meta: {
|
||||||
|
headerClass: 'col-span-3',
|
||||||
|
cellClass: 'col-span-3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'amount',
|
||||||
|
header: t('settings.billing.table.amount'),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<p class="text-sm text-foreground">{auth.formatMoney(row.original.amount)}</p>
|
||||||
|
),
|
||||||
|
meta: {
|
||||||
|
headerClass: 'col-span-2',
|
||||||
|
cellClass: 'col-span-2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'plan',
|
||||||
|
header: t('settings.billing.table.plan'),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<>
|
||||||
|
<p class="text-sm text-foreground">{row.original.plan}</p>
|
||||||
|
{row.original.details?.length ? (
|
||||||
|
<p class="mt-1 text-xs text-foreground/60">
|
||||||
|
{row.original.details.join(' · ')}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
meta: {
|
||||||
|
headerClass: 'col-span-3',
|
||||||
|
cellClass: 'col-span-3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'status',
|
||||||
|
header: t('settings.billing.table.status'),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span class={`inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium ${getStatusStyles(row.original.status)}`}>
|
||||||
|
{t('settings.billing.status.' + row.original.status)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
meta: {
|
||||||
|
headerClass: 'col-span-2',
|
||||||
|
cellClass: 'col-span-2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'invoice',
|
||||||
|
enableSorting: false,
|
||||||
|
header: t('settings.billing.table.invoice'),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-2 px-3 py-1.5 text-sm text-foreground/70 hover:text-foreground hover:bg-muted/50 rounded-md transition-all disabled:opacity-60 disabled:cursor-wait"
|
||||||
|
disabled={downloadingInvoiceId.value === row.original.id}
|
||||||
|
onClick={() => handleDownloadInvoice(row.original as PaymentHistoryItem)}
|
||||||
|
>
|
||||||
|
<DownloadIcon class="w-4 h-4" />
|
||||||
|
<span>{downloadingInvoiceId.value === row.original.id ? '...' : t('settings.billing.download')}</span>
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
meta: {
|
||||||
|
headerClass: 'col-span-2 flex justify-center',
|
||||||
|
cellClass: 'col-span-2 justify-center',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
<div class="px-6 py-4">
|
<div class="px-6 py-4">
|
||||||
<div class="flex items-center gap-4 mb-4">
|
<div class="flex items-center gap-4 mb-4">
|
||||||
<div class="w-10 h-10 rounded-md bg-info/10 flex items-center justify-center shrink-0">
|
<div class="w-10 h-10 rounded-md bg-accent/10 flex items-center justify-center shrink-0">
|
||||||
<ListIcon class="w-6 h-6 text-info" />
|
<ListIcon class="w-6 h-6 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-medium text-foreground">{t('settings.billing.paymentHistory')}</p>
|
<p class="text-sm font-medium text-foreground">{t('settings.billing.paymentHistory')}</p>
|
||||||
@@ -132,71 +210,36 @@ const PaymentHistory = defineComponent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border border-border rounded-lg overflow-hidden">
|
<BaseTable
|
||||||
<div class="grid grid-cols-12 gap-4 px-4 py-3 text-xs font-medium text-foreground/60 uppercase tracking-wider bg-muted/30">
|
data={data.value || []}
|
||||||
<div class="col-span-3">{t('settings.billing.table.date')}</div>
|
columns={columns}
|
||||||
<div class="col-span-2">{t('settings.billing.table.amount')}</div>
|
loading={isLoading.value}
|
||||||
<div class="col-span-3">{t('settings.billing.table.plan')}</div>
|
emptyText={t('settings.billing.noPaymentHistory')}
|
||||||
<div class="col-span-2">{t('settings.billing.table.status')}</div>
|
>
|
||||||
<div class="col-span-2 text-right">{t('settings.billing.table.invoice')}</div>
|
{{
|
||||||
</div>
|
loading: () => (
|
||||||
{isLoading.value && (<div class="px-4 py-6 space-y-3">
|
<div class="px-4 py-6 space-y-3">
|
||||||
{Array.from({ length: 3 }).map((_, index) => (
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
<div key={index} class="grid grid-cols-12 gap-4 items-center animate-pulse">
|
<div key={index} class="grid grid-cols-12 gap-4 items-center animate-pulse">
|
||||||
<div class="col-span-3 h-4 rounded bg-muted/50" />
|
<div class="col-span-3 h-4 rounded bg-muted/50" />
|
||||||
<div class="col-span-2 h-4 rounded bg-muted/50" />
|
<div class="col-span-2 h-4 rounded bg-muted/50" />
|
||||||
<div class="col-span-3 h-4 rounded bg-muted/50" />
|
<div class="col-span-3 h-4 rounded bg-muted/50" />
|
||||||
<div class="col-span-2 h-6 rounded bg-muted/50" />
|
<div class="col-span-2 h-6 rounded bg-muted/50" />
|
||||||
<div class="col-span-2 h-8 rounded bg-muted/50" />
|
<div class="col-span-2 h-8 rounded bg-muted/50" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
),
|
||||||
</div>)}
|
empty: () => (
|
||||||
{data.value?.length === 0 && !isLoading.value && (<div class="text-center py-12 text-foreground/60">
|
<div class="text-center py-12 text-foreground/60">
|
||||||
<div class="w-16 h-16 rounded-full bg-muted/50 flex items-center justify-center mx-auto mb-4">
|
<div class="w-16 h-16 rounded-full bg-muted/50 flex items-center justify-center mx-auto mb-4">
|
||||||
<DownloadIcon class="w-8 h-8 text-foreground/40" />
|
<DownloadIcon class="w-8 h-8 text-foreground/40" />
|
||||||
</div>
|
</div>
|
||||||
<p>{t('settings.billing.noPaymentHistory')}</p>
|
<p>{t('settings.billing.noPaymentHistory')}</p>
|
||||||
</div>)}
|
|
||||||
|
|
||||||
{data.value?.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
class="grid grid-cols-12 gap-4 px-4 py-3 items-center hover:bg-muted/30 transition-all border-t border-border"
|
|
||||||
>
|
|
||||||
<div class="col-span-3">
|
|
||||||
<p class="text-sm font-medium text-foreground">{item.date}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-2">
|
)
|
||||||
<p class="text-sm text-foreground">{auth.formatMoney(item.amount)}</p>
|
}}
|
||||||
</div>
|
</BaseTable>
|
||||||
<div class="col-span-3">
|
|
||||||
<p class="text-sm text-foreground">{item.plan}</p>
|
|
||||||
{
|
|
||||||
item.details?.length ? (
|
|
||||||
<p class="mt-1 text-xs text-foreground/60">
|
|
||||||
{item.details.join(' · ')}
|
|
||||||
</p>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="col-span-2">
|
|
||||||
<span class={`inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium ${getStatusStyles(item.status)}`}>
|
|
||||||
{t('settings.billing.status.' + item.status)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-span-2 flex justify-end">
|
|
||||||
<button
|
|
||||||
class="flex items-center gap-2 px-3 py-1.5 text-sm text-foreground/70 hover:text-foreground hover:bg-muted/50 rounded-md transition-all disabled:opacity-60 disabled:cursor-wait"
|
|
||||||
disabled={downloadingInvoiceId.value === item.id}
|
|
||||||
onClick={() => handleDownloadInvoice(item)}
|
|
||||||
>
|
|
||||||
<DownloadIcon class="w-4 h-4" />
|
|
||||||
<span>{downloadingInvoiceId.value === item.id ? '...' : t('settings.billing.download')}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user