账单页面及弹窗基于naive ui重构
This commit is contained in:
parent
bbacb2deb6
commit
31b392b359
@ -1,15 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog v-model="show" :options="{ title: $t('Account Recharge') }">
|
<n-modal
|
||||||
<template #body-content>
|
v-model:show="show"
|
||||||
<div
|
preset="card"
|
||||||
|
:title="$t('Account Recharge')"
|
||||||
|
:style="modalStyle"
|
||||||
|
:mask-closable="true"
|
||||||
|
:close-on-esc="true"
|
||||||
|
class="add-prepaid-credits-modal"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span class="text-lg font-semibold">{{ $t('Account Recharge') }}</span>
|
||||||
|
</template>
|
||||||
|
<n-space vertical :size="20">
|
||||||
|
<n-alert
|
||||||
v-if="showMessage"
|
v-if="showMessage"
|
||||||
class="mb-5 inline-flex gap-1.5 text-base text-gray-700"
|
type="info"
|
||||||
|
:title="$t('Information')"
|
||||||
|
closable
|
||||||
>
|
>
|
||||||
<FeatherIcon class="h-4" name="info" />
|
{{ $t('Please recharge your account balance before changing payment method.') }}
|
||||||
<span>
|
</n-alert>
|
||||||
{{ $t('Please recharge your account balance before changing payment method.') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<PrepaidCreditsForm
|
<PrepaidCreditsForm
|
||||||
@success="
|
@success="
|
||||||
() => {
|
() => {
|
||||||
@ -18,12 +28,13 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</template>
|
</n-space>
|
||||||
</Dialog>
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { NModal, NSpace, NAlert } from 'naive-ui';
|
||||||
|
import { computed, ref, onMounted, onUnmounted } from 'vue';
|
||||||
import PrepaidCreditsForm from '../BuyPrepaidCreditsForm.vue';
|
import PrepaidCreditsForm from '../BuyPrepaidCreditsForm.vue';
|
||||||
import { Dialog, FeatherIcon } from 'jingrow-ui';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
showMessage: {
|
showMessage: {
|
||||||
@ -34,4 +45,53 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
const show = defineModel();
|
const show = defineModel();
|
||||||
</script>
|
const windowWidth = ref(window.innerWidth);
|
||||||
|
|
||||||
|
const isMobile = computed(() => windowWidth.value <= 768);
|
||||||
|
const modalStyle = computed(() => ({
|
||||||
|
width: isMobile.value ? '95vw' : '700px',
|
||||||
|
maxWidth: isMobile.value ? '95vw' : '90vw',
|
||||||
|
}));
|
||||||
|
|
||||||
|
function handleResize() {
|
||||||
|
windowWidth.value = window.innerWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
handleResize();
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.add-prepaid-credits-modal .n-card) {
|
||||||
|
width: 700px;
|
||||||
|
max-width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.add-prepaid-credits-modal .n-card-body) {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:deep(.add-prepaid-credits-modal .n-card) {
|
||||||
|
width: 95vw !important;
|
||||||
|
max-width: 95vw !important;
|
||||||
|
margin: 20px auto;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.add-prepaid-credits-modal .n-card-body) {
|
||||||
|
padding: 20px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.add-prepaid-credits-modal .n-card__header) {
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,13 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog v-model="show" :options="{ title: $t('Billing Details') }">
|
<n-modal
|
||||||
<template #body-content>
|
v-model:show="show"
|
||||||
<div
|
preset="card"
|
||||||
|
:title="$t('Billing Details')"
|
||||||
|
:style="modalStyle"
|
||||||
|
:mask-closable="true"
|
||||||
|
:close-on-esc="true"
|
||||||
|
class="billing-details-modal"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span class="text-lg font-semibold">{{ $t('Billing Details') }}</span>
|
||||||
|
</template>
|
||||||
|
<n-space vertical :size="20">
|
||||||
|
<n-alert
|
||||||
v-if="showMessage"
|
v-if="showMessage"
|
||||||
class="mb-5 inline-flex gap-1.5 text-base text-gray-700"
|
type="info"
|
||||||
|
:title="$t('Information')"
|
||||||
|
closable
|
||||||
>
|
>
|
||||||
<FeatherIcon class="h-4" name="info" />
|
{{ $t('Please add billing details to your account before continuing.') }}
|
||||||
<span> {{ $t('Please add billing details to your account before continuing.') }}</span>
|
</n-alert>
|
||||||
</div>
|
|
||||||
<BillingDetails
|
<BillingDetails
|
||||||
ref="billingRef"
|
ref="billingRef"
|
||||||
@success="
|
@success="
|
||||||
@ -17,13 +29,13 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</template>
|
</n-space>
|
||||||
</Dialog>
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { NModal, NSpace, NAlert } from 'naive-ui';
|
||||||
|
import { computed, ref, onMounted, onUnmounted } from 'vue';
|
||||||
import BillingDetails from './BillingDetails.vue';
|
import BillingDetails from './BillingDetails.vue';
|
||||||
import { FeatherIcon, Dialog } from 'jingrow-ui';
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
showMessage: {
|
showMessage: {
|
||||||
@ -31,7 +43,57 @@ const props = defineProps({
|
|||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
const show = defineModel();
|
const show = defineModel();
|
||||||
const billingRef = ref(null);
|
const billingRef = ref(null);
|
||||||
</script>
|
const windowWidth = ref(window.innerWidth);
|
||||||
|
|
||||||
|
const isMobile = computed(() => windowWidth.value <= 768);
|
||||||
|
const modalStyle = computed(() => ({
|
||||||
|
width: isMobile.value ? '95vw' : '700px',
|
||||||
|
maxWidth: isMobile.value ? '95vw' : '90vw',
|
||||||
|
}));
|
||||||
|
|
||||||
|
function handleResize() {
|
||||||
|
windowWidth.value = window.innerWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
handleResize();
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.billing-details-modal .n-card) {
|
||||||
|
width: 700px;
|
||||||
|
max-width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.billing-details-modal .n-card-body) {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:deep(.billing-details-modal .n-card) {
|
||||||
|
width: 95vw !important;
|
||||||
|
max-width: 95vw !important;
|
||||||
|
margin: 20px auto;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.billing-details-modal .n-card-body) {
|
||||||
|
padding: 20px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.billing-details-modal .n-card__header) {
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,56 +1,65 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-4 py-10">
|
<n-space vertical :size="24">
|
||||||
<div class="flex flex-col">
|
<n-card class="payment-details-card" :title="$t('Account Balance')">
|
||||||
<div class="flex items-center justify-between text-base text-gray-900">
|
<n-space vertical :size="16">
|
||||||
<div class="flex flex-col gap-1.5">
|
<div class="balance-section">
|
||||||
<div class="text-lg font-semibold text-gray-900">{{ $t('Account Balance') }}</div>
|
<div class="balance-info">
|
||||||
<div class="text-2xl font-bold text-blue-600 py-6">
|
<div class="balance-amount">
|
||||||
{{ availableCredits || currency + ' 0.00' }}
|
{{ availableCredits || currency + ' 0.00' }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<n-button
|
||||||
<div class="shrink-0">
|
type="primary"
|
||||||
<Button
|
|
||||||
:label="$t('Recharge')"
|
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
showMessage = false;
|
showMessage = false;
|
||||||
showAddPrepaidCreditsDialog = true;
|
showAddPrepaidCreditsDialog = true;
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
|
:size="buttonSize"
|
||||||
|
:block="isMobile"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #icon>
|
||||||
<FeatherIcon class="h-4" name="plus" />
|
<i-lucide-plus class="h-4 w-4" />
|
||||||
</template>
|
</template>
|
||||||
</Button>
|
{{ $t('Recharge') }}
|
||||||
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</n-space>
|
||||||
<div class="my-3 h-px bg-gray-100" />
|
</n-card>
|
||||||
<div class="flex items-center justify-between text-base text-gray-900">
|
|
||||||
<div class="flex flex-col gap-1.5">
|
<n-card class="payment-details-card" :title="$t('Billing Address')">
|
||||||
<div class="font-medium">{{ $t('Billing Address') }}</div>
|
<n-space vertical :size="16">
|
||||||
<div v-if="billingDetailsSummary" class="leading-5 text-gray-700">
|
<div class="address-section">
|
||||||
{{ billingDetailsSummary }}
|
<div class="address-info">
|
||||||
|
<div v-if="billingDetailsSummary" class="address-text">
|
||||||
|
{{ billingDetailsSummary }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="address-empty">
|
||||||
|
{{ $t('No address') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-gray-700">{{ $t('No address') }}</div>
|
<n-button
|
||||||
</div>
|
:type="billingDetailsSummary ? 'default' : 'primary'"
|
||||||
<div class="shrink-0">
|
|
||||||
<Button
|
|
||||||
:label="billingAddressButtonLabel"
|
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
showMessage = false;
|
showMessage = false;
|
||||||
showBillingDetailsDialog = true;
|
showBillingDetailsDialog = true;
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
|
:size="buttonSize"
|
||||||
|
:block="isMobile"
|
||||||
>
|
>
|
||||||
<template v-if="!billingDetailsSummary" #prefix>
|
<template v-if="!billingDetailsSummary" #icon>
|
||||||
<FeatherIcon class="h-4" name="plus" />
|
<i-lucide-plus class="h-4 w-4" />
|
||||||
</template>
|
</template>
|
||||||
</Button>
|
{{ billingAddressButtonLabel }}
|
||||||
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</n-space>
|
||||||
</div>
|
</n-card>
|
||||||
</div>
|
</n-space>
|
||||||
|
|
||||||
<BillingDetailsDialog
|
<BillingDetailsDialog
|
||||||
v-if="showBillingDetailsDialog"
|
v-if="showBillingDetailsDialog"
|
||||||
v-model="showBillingDetailsDialog"
|
v-model="showBillingDetailsDialog"
|
||||||
@ -65,16 +74,11 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import DropdownItem from './DropdownItem.vue';
|
import { NCard, NSpace, NButton } from 'naive-ui';
|
||||||
import BillingDetailsDialog from './BillingDetailsDialog.vue';
|
import BillingDetailsDialog from './BillingDetailsDialog.vue';
|
||||||
import AddPrepaidCreditsDialog from './AddPrepaidCreditsDialog.vue';
|
import AddPrepaidCreditsDialog from './AddPrepaidCreditsDialog.vue';
|
||||||
import { Dropdown, Button, FeatherIcon, createResource } from 'jingrow-ui';
|
import { createResource } from 'jingrow-ui';
|
||||||
import {
|
import { computed, ref, inject, onMounted, onUnmounted, nextTick, getCurrentInstance } from 'vue';
|
||||||
confirmDialog,
|
|
||||||
renderDialog,
|
|
||||||
} from '../../utils/components';
|
|
||||||
import { computed, ref, inject, h, defineAsyncComponent, onMounted, nextTick, getCurrentInstance } from 'vue';
|
|
||||||
import router from '../../router';
|
|
||||||
|
|
||||||
// 英文国家名到中文的映射
|
// 英文国家名到中文的映射
|
||||||
const countryToZh = {
|
const countryToZh = {
|
||||||
@ -96,6 +100,10 @@ const {
|
|||||||
|
|
||||||
const showBillingDetailsDialog = ref(false);
|
const showBillingDetailsDialog = ref(false);
|
||||||
const showAddPrepaidCreditsDialog = ref(false);
|
const showAddPrepaidCreditsDialog = ref(false);
|
||||||
|
const windowWidth = ref(window.innerWidth);
|
||||||
|
|
||||||
|
const isMobile = computed(() => windowWidth.value <= 768);
|
||||||
|
const buttonSize = computed(() => (isMobile.value ? 'medium' : 'medium'));
|
||||||
|
|
||||||
const currency = computed(() => (team.pg.currency == 'CNY' ? '¥' : '$'));
|
const currency = computed(() => (team.pg.currency == 'CNY' ? '¥' : '$'));
|
||||||
|
|
||||||
@ -193,8 +201,14 @@ const billingAddressButtonLabel = computed(() => {
|
|||||||
return billingDetailsSummary.value ? $t('Edit') : $t('Add Billing Address');
|
return billingDetailsSummary.value ? $t('Edit') : $t('Add Billing Address');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function handleResize() {
|
||||||
|
windowWidth.value = window.innerWidth;
|
||||||
|
}
|
||||||
|
|
||||||
// 修改onMounted钩子,添加自动设置支付方式为余额支付的逻辑
|
// 修改onMounted钩子,添加自动设置支付方式为余额支付的逻辑
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
handleResize();
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
// 如果当前没有设置支付方式,自动设置为余额支付
|
// 如果当前没有设置支付方式,自动设置为余额支付
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (!team.pg.payment_mode) {
|
if (!team.pg.payment_mode) {
|
||||||
@ -203,6 +217,10 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
|
||||||
function payUnpaidInvoices() {
|
function payUnpaidInvoices() {
|
||||||
let _unpaidInvoices = unpaidInvoices.data;
|
let _unpaidInvoices = unpaidInvoices.data;
|
||||||
if (_unpaidInvoices.length > 1) {
|
if (_unpaidInvoices.length > 1) {
|
||||||
@ -231,4 +249,63 @@ function updatePaymentMode(mode) {
|
|||||||
}
|
}
|
||||||
if (!changePaymentMode.loading) changePaymentMode.submit({ mode });
|
if (!changePaymentMode.loading) changePaymentMode.submit({ mode });
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.payment-details-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-details-card:hover {
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-section,
|
||||||
|
.address-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-info,
|
||||||
|
.address-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-amount {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--n-color-primary);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--n-text-color);
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-empty {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--n-text-color-3);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.balance-section,
|
||||||
|
.address-section {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.balance-amount {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,68 +1,178 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sticky top-0 z-10 shrink-0">
|
<div class="billing-container">
|
||||||
<Header>
|
<Header>
|
||||||
<FBreadcrumbs
|
<n-breadcrumb>
|
||||||
:items="[{ label: $t('Billing'), route: { name: 'Billing' } }]"
|
<n-breadcrumb-item>
|
||||||
/>
|
<router-link :to="{ name: 'Billing' }">
|
||||||
|
{{ $t('Billing') }}
|
||||||
|
</router-link>
|
||||||
|
</n-breadcrumb-item>
|
||||||
|
</n-breadcrumb>
|
||||||
</Header>
|
</Header>
|
||||||
<TabsWithRouter
|
<div v-if="$team?.pg?.is_desk_user || $session.hasBillingAccess" class="billing-content">
|
||||||
v-if="$team?.pg?.is_desk_user || $session.hasBillingAccess"
|
<n-tabs
|
||||||
:tabs="filteredTabs"
|
v-model:value="activeTab"
|
||||||
/>
|
type="line"
|
||||||
<div
|
animated
|
||||||
v-else
|
:bar-width="isMobile ? 0 : undefined"
|
||||||
class="mx-auto mt-60 w-fit rounded border border-dashed px-12 py-8 text-center text-gray-600"
|
:size="isMobile ? 'medium' : 'large'"
|
||||||
>
|
class="billing-tabs"
|
||||||
<i-lucide-alert-triangle class="mx-auto mb-4 h-6 w-6 text-red-600" />
|
>
|
||||||
<ErrorMessage :message="$t('You do not have permission to view the billing page')" />
|
<n-tab-pane
|
||||||
|
v-for="tab in filteredTabs"
|
||||||
|
:key="tab.route.name"
|
||||||
|
:name="tab.route.name"
|
||||||
|
:tab="tab.label"
|
||||||
|
>
|
||||||
|
<router-view :tab="tab" />
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-permission-container">
|
||||||
|
<n-result
|
||||||
|
status="403"
|
||||||
|
:title="$t('Access Denied')"
|
||||||
|
:description="$t('You do not have permission to view the billing page')"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i-lucide-alert-triangle class="h-12 w-12 text-red-600" />
|
||||||
|
</template>
|
||||||
|
</n-result>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script setup>
|
||||||
import { Tabs, Breadcrumbs } from 'jingrow-ui';
|
import { computed, ref, onMounted, onUnmounted, getCurrentInstance } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { NTabs, NTabPane, NBreadcrumb, NBreadcrumbItem, NResult } from 'naive-ui';
|
||||||
import Header from '../components/Header.vue';
|
import Header from '../components/Header.vue';
|
||||||
import TabsWithRouter from '../components/TabsWithRouter.vue';
|
|
||||||
|
|
||||||
export default {
|
const instance = getCurrentInstance();
|
||||||
name: 'Billing',
|
const $t = instance?.appContext.config.globalProperties.$t || ((key) => key);
|
||||||
components: {
|
const $team = instance?.appContext.config.globalProperties.$team;
|
||||||
Header,
|
const $session = instance?.appContext.config.globalProperties.$session;
|
||||||
FBreadcrumbs: Breadcrumbs,
|
|
||||||
FTabs: Tabs,
|
const route = useRoute();
|
||||||
TabsWithRouter,
|
const router = useRouter();
|
||||||
|
|
||||||
|
const windowWidth = ref(window.innerWidth);
|
||||||
|
const isMobile = computed(() => windowWidth.value <= 768);
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ label: $t('Overview'), route: { name: 'BillingOverview' } },
|
||||||
|
{ label: $t('Order Records'), route: { name: 'BillingOrders' } },
|
||||||
|
{ label: $t('Balance Details'), route: { name: 'BillingBalances' } },
|
||||||
|
{
|
||||||
|
label: $t('Developer Earnings'),
|
||||||
|
route: { name: 'BillingMarketplacePayouts' },
|
||||||
|
requireDeveloper: true,
|
||||||
|
requirePro: true
|
||||||
},
|
},
|
||||||
data() {
|
];
|
||||||
return {
|
|
||||||
currentTab: 0,
|
const filteredTabs = computed(() => {
|
||||||
tabs: [
|
return tabs.filter(tab => {
|
||||||
{ label: this.$t('Overview'), route: { name: 'BillingOverview' } },
|
// 如果需要开发者权限但用户不是开发者,则隐藏
|
||||||
{ label: this.$t('Order Records'), route: { name: 'BillingOrders' } },
|
if (tab.requireDeveloper && !$team.pg.is_developer) {
|
||||||
{ label: this.$t('Balance Details'), route: { name: 'BillingBalances' } },
|
return false;
|
||||||
{
|
|
||||||
label: this.$t('Developer Earnings'),
|
|
||||||
route: { name: 'BillingMarketplacePayouts' },
|
|
||||||
requireDeveloper: true,
|
|
||||||
requirePro: true
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
filteredTabs() {
|
|
||||||
return this.tabs.filter(tab => {
|
|
||||||
// 如果需要开发者权限但用户不是开发者,则隐藏
|
|
||||||
if (tab.requireDeveloper && !this.$team.pg.is_developer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果需要专业版权限但用户不是专业版用户,则隐藏
|
|
||||||
if (tab.requirePro && !this.$team.pg.is_pro) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果需要专业版权限但用户不是专业版用户,则隐藏
|
||||||
|
if (tab.requirePro && !$team.pg.is_pro) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeTab = computed({
|
||||||
|
get() {
|
||||||
|
return route.name || 'BillingOverview';
|
||||||
},
|
},
|
||||||
};
|
set(value) {
|
||||||
</script>
|
router.push({ name: value });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleResize() {
|
||||||
|
windowWidth.value = window.innerWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
handleResize();
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.billing-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.billing-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.billing-tabs {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tabs-nav) {
|
||||||
|
padding: 0 24px;
|
||||||
|
background: white;
|
||||||
|
border-bottom: 1px solid var(--n-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tabs-tab) {
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tabs-tab--active) {
|
||||||
|
color: var(--n-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tab-pane) {
|
||||||
|
padding: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-permission-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 400px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:deep(.n-tabs-nav) {
|
||||||
|
padding: 0 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tabs-tab) {
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tabs-nav-scroll-content) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,205 +1,233 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-5">
|
<div class="billing-balances-container">
|
||||||
<ObjectList :options="options">
|
<n-card class="balances-card">
|
||||||
<template #header-right>
|
<ObjectList :options="options">
|
||||||
<Button
|
<template #header-right>
|
||||||
icon="download"
|
<n-button
|
||||||
appearance="primary"
|
type="primary"
|
||||||
@click="exportToCsv"
|
:size="buttonSize"
|
||||||
:loading="exporting"
|
:loading="exporting"
|
||||||
>
|
:block="isMobile"
|
||||||
{{ $t('Export') }}
|
@click="exportToCsv"
|
||||||
</Button>
|
>
|
||||||
</template>
|
<template #icon>
|
||||||
</ObjectList>
|
<i-lucide-download class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
{{ $t('Export') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</ObjectList>
|
||||||
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted, getCurrentInstance } from 'vue';
|
||||||
|
import { NCard, NButton } from 'naive-ui';
|
||||||
import ObjectList from '../components/ObjectList.vue';
|
import ObjectList from '../components/ObjectList.vue';
|
||||||
import { Button, createResource } from 'jingrow-ui';
|
import { createResource } from 'jingrow-ui';
|
||||||
import { unparse } from 'papaparse';
|
import { unparse } from 'papaparse';
|
||||||
|
|
||||||
export default {
|
const instance = getCurrentInstance();
|
||||||
name: 'BillingBalances',
|
const $t = instance?.appContext.config.globalProperties.$t || ((key) => key);
|
||||||
props: ['tab'],
|
const $team = instance?.appContext.config.globalProperties.$team;
|
||||||
components: {
|
|
||||||
ObjectList,
|
|
||||||
Button
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
exporting: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
options() {
|
|
||||||
return {
|
|
||||||
pagetype: 'Balance Transaction',
|
|
||||||
fields: ['type', 'source', 'invoice', 'description'],
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
label: this.$t('Time'),
|
|
||||||
fieldname: 'creation',
|
|
||||||
format(value) {
|
|
||||||
return new Date(value).toLocaleString('zh-CN', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit'
|
|
||||||
}).replace(/\//g, '-');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: this.$t('Description'),
|
|
||||||
fieldname: 'description',
|
|
||||||
format(value, row) {
|
|
||||||
if (value !== null && value !== undefined) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.type === 'Applied To Invoice' && row.invoice) {
|
|
||||||
return this.$t('Applied to Invoice {invoice}', { invoice: row.invoice });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.source === 'Prepaid Credits') {
|
const exporting = ref(false);
|
||||||
return this.$t('Balance Recharge');
|
const windowWidth = ref(window.innerWidth);
|
||||||
}
|
|
||||||
|
|
||||||
if (row.source === 'Free Credits') {
|
const isMobile = computed(() => windowWidth.value <= 768);
|
||||||
return this.$t('Free Credits');
|
const buttonSize = computed(() => (isMobile.value ? 'medium' : 'medium'));
|
||||||
}
|
|
||||||
|
|
||||||
return row.amount < 0 ? row.type : row.source;
|
const props = defineProps(['tab']);
|
||||||
}
|
const options = computed(() => {
|
||||||
},
|
return {
|
||||||
{
|
pagetype: 'Balance Transaction',
|
||||||
label: this.$t('Amount'),
|
fields: ['type', 'source', 'invoice', 'description'],
|
||||||
fieldname: 'amount',
|
columns: [
|
||||||
align: 'right',
|
{
|
||||||
format: this.formatCurrency
|
label: $t('Time'),
|
||||||
},
|
fieldname: 'creation',
|
||||||
{
|
format(value) {
|
||||||
label: this.$t('Balance'),
|
return new Date(value).toLocaleString('zh-CN', {
|
||||||
fieldname: 'ending_balance',
|
year: 'numeric',
|
||||||
align: 'right',
|
month: '2-digit',
|
||||||
format: this.formatCurrency
|
day: '2-digit',
|
||||||
}
|
hour: '2-digit',
|
||||||
],
|
minute: '2-digit',
|
||||||
filters: {
|
second: '2-digit'
|
||||||
pagestatus: 1,
|
}).replace(/\//g, '-');
|
||||||
team: this.$team.name
|
|
||||||
},
|
|
||||||
orderBy: 'creation desc'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
// 创建导出资源
|
|
||||||
this.exportResource = createResource({
|
|
||||||
url: 'jcloud.api.billing.get_balance_transactions',
|
|
||||||
onSuccess: this.handleExportSuccess,
|
|
||||||
onError: this.handleExportError
|
|
||||||
});
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
formatCurrency(value) {
|
|
||||||
if (value === 0) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return this.$format.userCurrency(value);
|
|
||||||
},
|
|
||||||
formatDate(dateString) {
|
|
||||||
return new Date(dateString).toLocaleString('zh-CN', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit'
|
|
||||||
}).replace(/\//g, '-');
|
|
||||||
},
|
|
||||||
exportToCsv() {
|
|
||||||
this.exporting = true;
|
|
||||||
this.exportResource.submit({
|
|
||||||
page: 1,
|
|
||||||
page_size: 1000
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handleExportSuccess(response) {
|
|
||||||
const transactions = response.transactions || [];
|
|
||||||
|
|
||||||
// 定义CSV字段
|
|
||||||
const fields = [
|
|
||||||
this.$t('Time'),
|
|
||||||
this.$t('Description'),
|
|
||||||
this.$t('Amount'),
|
|
||||||
this.$t('Balance')
|
|
||||||
];
|
|
||||||
|
|
||||||
// 准备数据
|
|
||||||
const csvData = transactions.map(row => {
|
|
||||||
// 优先使用原始描述,如果存在的话
|
|
||||||
let description = row.description;
|
|
||||||
|
|
||||||
// 如果描述不存在,则使用备用逻辑
|
|
||||||
if (!description) {
|
|
||||||
if (row.type === 'Applied To Invoice' && row.invoice) {
|
|
||||||
description = this.$t('Applied to Invoice {invoice}', { invoice: row.invoice });
|
|
||||||
} else if (row.source === 'Prepaid Credits') {
|
|
||||||
description = this.$t('Balance Recharge');
|
|
||||||
} else if (row.source === 'Free Credits') {
|
|
||||||
description = this.$t('Free Credits');
|
|
||||||
} else {
|
|
||||||
description = row.amount < 0 ? row.type : row.source;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
return [
|
{
|
||||||
this.formatDate(row.creation),
|
label: $t('Description'),
|
||||||
description,
|
fieldname: 'description',
|
||||||
row.amount,
|
format(value, row) {
|
||||||
row.ending_balance
|
if (value !== null && value !== undefined) {
|
||||||
];
|
return value;
|
||||||
});
|
}
|
||||||
|
|
||||||
// 添加表头
|
if (row.type === 'Applied To Invoice' && row.invoice) {
|
||||||
csvData.unshift(fields);
|
return $t('Applied to Invoice {invoice}', { invoice: row.invoice });
|
||||||
|
}
|
||||||
// 生成CSV
|
|
||||||
let csv = unparse(csvData);
|
if (row.source === 'Prepaid Credits') {
|
||||||
|
return $t('Balance Recharge');
|
||||||
// 添加BOM以支持中文
|
}
|
||||||
csv = '\uFEFF' + csv;
|
|
||||||
|
if (row.source === 'Free Credits') {
|
||||||
// 触发下载
|
return $t('Free Credits');
|
||||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
}
|
||||||
const today = new Date().toISOString().split('T')[0];
|
|
||||||
const filename = `${this.$t('Balance Records')}-${today}.csv`;
|
return row.amount < 0 ? row.type : row.source;
|
||||||
const link = document.createElement('a');
|
}
|
||||||
link.href = URL.createObjectURL(blob);
|
},
|
||||||
link.download = filename;
|
{
|
||||||
link.click();
|
label: $t('Amount'),
|
||||||
URL.revokeObjectURL(link.href);
|
fieldname: 'amount',
|
||||||
|
align: 'right',
|
||||||
this.exporting = false;
|
format: formatCurrency
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('Balance'),
|
||||||
|
fieldname: 'ending_balance',
|
||||||
|
align: 'right',
|
||||||
|
format: formatCurrency
|
||||||
|
}
|
||||||
|
],
|
||||||
|
filters: {
|
||||||
|
pagestatus: 1,
|
||||||
|
team: $team.name
|
||||||
},
|
},
|
||||||
handleExportError(error) {
|
orderBy: 'creation desc'
|
||||||
console.error(this.$t('Failed to export data'), error);
|
};
|
||||||
this.exporting = false;
|
});
|
||||||
}
|
|
||||||
|
// 创建导出资源
|
||||||
|
const exportResource = createResource({
|
||||||
|
url: 'jcloud.api.billing.get_balance_transactions',
|
||||||
|
onSuccess: handleExportSuccess,
|
||||||
|
onError: handleExportError
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatCurrency(value) {
|
||||||
|
if (value === 0) {
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
};
|
const $format = instance?.appContext.config.globalProperties.$format;
|
||||||
</script>
|
return $format?.userCurrency(value) || value;
|
||||||
<style scoped>
|
|
||||||
/* 添加悬浮效果 */
|
|
||||||
.flex-col:hover {
|
|
||||||
background-color: #f0f0f0; /* 可按需调整颜色 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 允许内容复制 */
|
function formatDate(dateString) {
|
||||||
.flex-col * {
|
return new Date(dateString).toLocaleString('zh-CN', {
|
||||||
user-select: text;
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
}).replace(/\//g, '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportToCsv() {
|
||||||
|
exporting.value = true;
|
||||||
|
exportResource.submit({
|
||||||
|
page: 1,
|
||||||
|
page_size: 1000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExportSuccess(response) {
|
||||||
|
const transactions = response.transactions || [];
|
||||||
|
|
||||||
|
// 定义CSV字段
|
||||||
|
const fields = [
|
||||||
|
$t('Time'),
|
||||||
|
$t('Description'),
|
||||||
|
$t('Amount'),
|
||||||
|
$t('Balance')
|
||||||
|
];
|
||||||
|
|
||||||
|
// 准备数据
|
||||||
|
const csvData = transactions.map(row => {
|
||||||
|
// 优先使用原始描述,如果存在的话
|
||||||
|
let description = row.description;
|
||||||
|
|
||||||
|
// 如果描述不存在,则使用备用逻辑
|
||||||
|
if (!description) {
|
||||||
|
if (row.type === 'Applied To Invoice' && row.invoice) {
|
||||||
|
description = $t('Applied to Invoice {invoice}', { invoice: row.invoice });
|
||||||
|
} else if (row.source === 'Prepaid Credits') {
|
||||||
|
description = $t('Balance Recharge');
|
||||||
|
} else if (row.source === 'Free Credits') {
|
||||||
|
description = $t('Free Credits');
|
||||||
|
} else {
|
||||||
|
description = row.amount < 0 ? row.type : row.source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
formatDate(row.creation),
|
||||||
|
description,
|
||||||
|
row.amount,
|
||||||
|
row.ending_balance
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加表头
|
||||||
|
csvData.unshift(fields);
|
||||||
|
|
||||||
|
// 生成CSV
|
||||||
|
let csv = unparse(csvData);
|
||||||
|
|
||||||
|
// 添加BOM以支持中文
|
||||||
|
csv = '\uFEFF' + csv;
|
||||||
|
|
||||||
|
// 触发下载
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const filename = `${$t('Balance Records')}-${today}.csv`;
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = filename;
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(link.href);
|
||||||
|
|
||||||
|
exporting.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExportError(error) {
|
||||||
|
console.error($t('Failed to export data'), error);
|
||||||
|
exporting.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResize() {
|
||||||
|
windowWidth.value = window.innerWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
handleResize();
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.billing-balances-container {
|
||||||
|
padding: 24px;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balances-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.billing-balances-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -1,105 +1,171 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-5">
|
<div class="billing-marketplace-payouts-container">
|
||||||
<ObjectList :options="options" />
|
<n-card class="payouts-card">
|
||||||
<Dialog
|
<ObjectList :options="options" />
|
||||||
v-model="payoutDialog"
|
</n-card>
|
||||||
:options="{ size: '6xl', title: showPayout?.name }"
|
<n-modal
|
||||||
|
v-model:show="payoutDialog"
|
||||||
|
preset="card"
|
||||||
|
:title="showPayout?.name"
|
||||||
|
:style="modalStyle"
|
||||||
|
:mask-closable="true"
|
||||||
|
:close-on-esc="true"
|
||||||
|
class="payout-dialog"
|
||||||
>
|
>
|
||||||
<template #body-content>
|
<template #header>
|
||||||
<template v-if="showPayout">
|
<span class="text-lg font-semibold">{{ showPayout?.name }}</span>
|
||||||
<div
|
|
||||||
v-if="showPayout.status === 'Empty'"
|
|
||||||
class="text-base text-gray-600"
|
|
||||||
>
|
|
||||||
{{ $t('No content to display') }}
|
|
||||||
</div>
|
|
||||||
<PayoutTable v-else :payoutId="showPayout.name" />
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
<template v-if="showPayout">
|
||||||
|
<n-empty
|
||||||
|
v-if="showPayout.status === 'Empty'"
|
||||||
|
:description="$t('No content to display')"
|
||||||
|
/>
|
||||||
|
<PayoutTable v-else :payoutId="showPayout.name" />
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted, getCurrentInstance } from 'vue';
|
||||||
|
import { NCard, NModal, NEmpty } from 'naive-ui';
|
||||||
import ObjectList from '../components/ObjectList.vue';
|
import ObjectList from '../components/ObjectList.vue';
|
||||||
import PayoutTable from '../components/PayoutTable.vue';
|
import PayoutTable from '../components/PayoutTable.vue';
|
||||||
|
|
||||||
export default {
|
const instance = getCurrentInstance();
|
||||||
name: 'BillingMarketplacePayouts',
|
const $t = instance?.appContext.config.globalProperties.$t || ((key) => key);
|
||||||
props: ['tab'],
|
const $team = instance?.appContext.config.globalProperties.$team;
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
payoutDialog: false,
|
|
||||||
showPayout: null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
ObjectList,
|
|
||||||
PayoutTable
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
options() {
|
|
||||||
return {
|
|
||||||
pagetype: 'Payout Order',
|
|
||||||
fields: [
|
|
||||||
'period_end',
|
|
||||||
'mode_of_payment',
|
|
||||||
'status',
|
|
||||||
'net_total_cny',
|
|
||||||
'net_total_usd'
|
|
||||||
],
|
|
||||||
filterControls: () => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
type: 'select',
|
|
||||||
label: this.$t('Status'),
|
|
||||||
class: !this.$isMobile ? 'w-36' : '',
|
|
||||||
fieldname: 'status',
|
|
||||||
options: [
|
|
||||||
{ label: '', value: '' },
|
|
||||||
{ label: this.$t('Pending Settlement'), value: 'Draft' },
|
|
||||||
{ label: this.$t('Paid'), value: 'Paid' },
|
|
||||||
{ label: this.$t('Settled'), value: 'Commissioned' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
},
|
|
||||||
orderBy: 'creation desc',
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
label: this.$t('Date'),
|
|
||||||
fieldname: 'period_end',
|
|
||||||
format(value) {
|
|
||||||
return Intl.DateTimeFormat('en-US', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric'
|
|
||||||
}).format(new Date(value));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ label: this.$t('Payment Method'), fieldname: 'mode_of_payment' },
|
|
||||||
{ label: this.$t('Status'), fieldname: 'status', type: 'Badge' },
|
|
||||||
{
|
|
||||||
label: this.$t('Total'),
|
|
||||||
fieldname: 'net_total_cny',
|
|
||||||
align: 'right',
|
|
||||||
format: (_, row) => {
|
|
||||||
let total = 0;
|
|
||||||
if (this.$team.pg.currency === 'CNY') {
|
|
||||||
total = row.net_total_cny + row.net_total_usd * 82;
|
|
||||||
} else {
|
|
||||||
total = row.net_total_cny / 82 + row.net_total_usd;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.$format.userCurrency(total);
|
const props = defineProps(['tab']);
|
||||||
}
|
|
||||||
}
|
const payoutDialog = ref(false);
|
||||||
],
|
const showPayout = ref(null);
|
||||||
onRowClick: row => {
|
const windowWidth = ref(window.innerWidth);
|
||||||
this.showPayout = row;
|
|
||||||
this.payoutDialog = true;
|
const isMobile = computed(() => windowWidth.value <= 768);
|
||||||
|
const modalStyle = computed(() => ({
|
||||||
|
width: isMobile.value ? '95vw' : '1200px',
|
||||||
|
maxWidth: isMobile.value ? '95vw' : '90vw',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const options = computed(() => {
|
||||||
|
return {
|
||||||
|
pagetype: 'Payout Order',
|
||||||
|
fields: [
|
||||||
|
'period_end',
|
||||||
|
'mode_of_payment',
|
||||||
|
'status',
|
||||||
|
'net_total_cny',
|
||||||
|
'net_total_usd'
|
||||||
|
],
|
||||||
|
filterControls: () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
label: $t('Status'),
|
||||||
|
class: !isMobile.value ? 'w-36' : '',
|
||||||
|
fieldname: 'status',
|
||||||
|
options: [
|
||||||
|
{ label: '', value: '' },
|
||||||
|
{ label: $t('Pending Settlement'), value: 'Draft' },
|
||||||
|
{ label: $t('Paid'), value: 'Paid' },
|
||||||
|
{ label: $t('Settled'), value: 'Commissioned' }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
};
|
];
|
||||||
|
},
|
||||||
|
orderBy: 'creation desc',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
label: $t('Date'),
|
||||||
|
fieldname: 'period_end',
|
||||||
|
format(value) {
|
||||||
|
return Intl.DateTimeFormat('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
}).format(new Date(value));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ label: $t('Payment Method'), fieldname: 'mode_of_payment' },
|
||||||
|
{ label: $t('Status'), fieldname: 'status', type: 'Badge' },
|
||||||
|
{
|
||||||
|
label: $t('Total'),
|
||||||
|
fieldname: 'net_total_cny',
|
||||||
|
align: 'right',
|
||||||
|
format: (_, row) => {
|
||||||
|
let total = 0;
|
||||||
|
const $format = instance?.appContext.config.globalProperties.$format;
|
||||||
|
if ($team.pg.currency === 'CNY') {
|
||||||
|
total = row.net_total_cny + row.net_total_usd * 82;
|
||||||
|
} else {
|
||||||
|
total = row.net_total_cny / 82 + row.net_total_usd;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $format?.userCurrency(total) || total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
onRowClick: row => {
|
||||||
|
showPayout.value = row;
|
||||||
|
payoutDialog.value = true;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleResize() {
|
||||||
|
windowWidth.value = window.innerWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
handleResize();
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.billing-marketplace-payouts-container {
|
||||||
|
padding: 24px;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payouts-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.payout-dialog .n-card) {
|
||||||
|
width: 1200px;
|
||||||
|
max-width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.payout-dialog .n-card-body) {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.billing-marketplace-payouts-container {
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
</script>
|
:deep(.payout-dialog .n-card) {
|
||||||
|
width: 95vw !important;
|
||||||
|
max-width: 95vw !important;
|
||||||
|
margin: 20px auto;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.payout-dialog .n-card-body) {
|
||||||
|
padding: 20px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.payout-dialog .n-card__header) {
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,147 +1,224 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-4">
|
<div class="billing-orders-container">
|
||||||
<div class="mb-4 flex justify-between">
|
<n-card class="orders-card">
|
||||||
<div>
|
<template #header>
|
||||||
<FInput
|
<div class="orders-header">
|
||||||
v-model="filters.search"
|
<n-input
|
||||||
:placeholder="$t('Search orders...')"
|
v-model:value="filters.search"
|
||||||
type="text"
|
:placeholder="$t('Search orders...')"
|
||||||
class="w-60"
|
clearable
|
||||||
@input="debouncedSearch"
|
:size="inputSize"
|
||||||
>
|
class="search-input"
|
||||||
<template #prefix>
|
@input="debouncedSearch"
|
||||||
<i-lucide-search class="h-4 w-4 text-gray-500" />
|
|
||||||
</template>
|
|
||||||
</FInput>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<Button
|
|
||||||
icon="download"
|
|
||||||
appearance="primary"
|
|
||||||
@click="exportToCsv"
|
|
||||||
>
|
|
||||||
{{ $t('Export') }}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
icon="refresh-cw"
|
|
||||||
appearance="minimal"
|
|
||||||
@click="resetAndFetch"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card class="overflow-hidden !rounded-none !border-0">
|
|
||||||
<template v-if="orders.length === 0">
|
|
||||||
<div class="flex h-60 flex-col items-center justify-center space-y-2 p-4 text-center">
|
|
||||||
<i-lucide-file-text class="h-8 w-8 text-gray-400" />
|
|
||||||
<p class="text-base font-medium">{{ $t('No Order Records') }}</p>
|
|
||||||
<p class="text-sm text-gray-600">{{ $t('No order records to display') }}</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="w-full">
|
|
||||||
<thead>
|
|
||||||
<tr class="border-b text-left text-sm font-medium text-gray-600">
|
|
||||||
<th
|
|
||||||
v-for="column in columns"
|
|
||||||
:key="column.key"
|
|
||||||
class="whitespace-nowrap px-4 py-3"
|
|
||||||
:class="column.class"
|
|
||||||
>
|
|
||||||
{{ column.label }}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
v-for="order in orders"
|
|
||||||
:key="order.name || order.order_id"
|
|
||||||
class="border-b text-sm hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<td class="px-4 py-3">{{ formatDate(order.creation) }}</td>
|
|
||||||
<td class="px-4 py-3">{{ order.title || '-' }}</td>
|
|
||||||
<td class="px-4 py-3">{{ order.order_id || '-' }}</td>
|
|
||||||
<td class="px-4 py-3">{{ order.trade_no || '-' }}</td>
|
|
||||||
<td class="px-4 py-3">{{ order.order_type || '-' }}</td>
|
|
||||||
<td class="px-4 py-3">{{ order.payment_method || '-' }}</td>
|
|
||||||
<td class="px-4 py-3">{{ order.description || '-' }}</td>
|
|
||||||
<td class="px-4 py-3 text-right">{{ formatCurrency(order.total) }}</td>
|
|
||||||
<td class="px-4 py-3">
|
|
||||||
<StatusIndicator :status="getStatusProps(order.status)" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between p-4">
|
|
||||||
<div class="text-sm text-gray-600">
|
|
||||||
{{ $t('Showing {count} orders, total {total}', { count: orders.length, total: totalCount }) }}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
v-if="hasMoreToLoad"
|
|
||||||
@click="loadMore"
|
|
||||||
:loading="loadingMore"
|
|
||||||
appearance="primary"
|
|
||||||
>
|
>
|
||||||
{{ $t('Load More') }}
|
<template #prefix>
|
||||||
</Button>
|
<i-lucide-search class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
</n-input>
|
||||||
|
<n-space :size="12">
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
:size="buttonSize"
|
||||||
|
:loading="exporting"
|
||||||
|
@click="exportToCsv"
|
||||||
|
:block="isMobile"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i-lucide-download class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
{{ $t('Export') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
:size="buttonSize"
|
||||||
|
@click="resetAndFetch"
|
||||||
|
:loading="initialLoading"
|
||||||
|
:block="isMobile"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i-lucide-refresh-cw class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
|
||||||
|
<n-empty
|
||||||
|
v-if="!initialLoading && orders.length === 0"
|
||||||
|
:description="$t('No order records to display')"
|
||||||
|
class="empty-state"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i-lucide-file-text class="h-12 w-12 text-gray-400" />
|
||||||
|
</template>
|
||||||
|
</n-empty>
|
||||||
|
|
||||||
|
<n-data-table
|
||||||
|
v-else
|
||||||
|
:columns="tableColumns"
|
||||||
|
:data="orders"
|
||||||
|
:loading="initialLoading"
|
||||||
|
:pagination="false"
|
||||||
|
:scroll-x="isMobile ? 1200 : undefined"
|
||||||
|
:bordered="false"
|
||||||
|
:single-line="false"
|
||||||
|
class="orders-table"
|
||||||
|
size="medium"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="orders.length > 0" class="orders-footer">
|
||||||
|
<div class="orders-info">
|
||||||
|
{{ $t('Showing {count} orders, total {total}', { count: orders.length, total: totalCount }) }}
|
||||||
|
</div>
|
||||||
|
<n-button
|
||||||
|
v-if="hasMoreToLoad"
|
||||||
|
type="primary"
|
||||||
|
:loading="loadingMore"
|
||||||
|
:size="buttonSize"
|
||||||
|
:block="isMobile"
|
||||||
|
@click="loadMore"
|
||||||
|
>
|
||||||
|
{{ $t('Load More') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue';
|
import { ref, reactive, computed, onMounted, onUnmounted, h, getCurrentInstance } from 'vue';
|
||||||
import { Button, Card, Input as FInput, createResource } from 'jingrow-ui';
|
import { NCard, NInput, NButton, NSpace, NDataTable, NEmpty, NTag } from 'naive-ui';
|
||||||
import StatusIndicator from '../components/StatusIndicator.vue';
|
import { createResource } from 'jingrow-ui';
|
||||||
import { unparse } from 'papaparse';
|
import { unparse } from 'papaparse';
|
||||||
|
|
||||||
export default {
|
const instance = getCurrentInstance();
|
||||||
name: 'BillingOrders',
|
const $t = instance?.appContext.config.globalProperties.$t || ((key) => key);
|
||||||
components: {
|
const pageSize = 20;
|
||||||
Button,
|
const orders = ref([]);
|
||||||
Card,
|
const totalCount = ref(0);
|
||||||
FInput,
|
const currentPage = ref(1);
|
||||||
StatusIndicator,
|
const initialLoading = ref(true);
|
||||||
},
|
const loadingMore = ref(false);
|
||||||
setup() {
|
const exporting = ref(false);
|
||||||
const instance = getCurrentInstance();
|
const windowWidth = ref(window.innerWidth);
|
||||||
const $t = instance?.appContext.config.globalProperties.$t || ((key) => key);
|
|
||||||
const pageSize = 20;
|
const isMobile = computed(() => windowWidth.value <= 768);
|
||||||
const orders = ref([]);
|
const inputSize = computed(() => (isMobile.value ? 'medium' : 'medium'));
|
||||||
const totalCount = ref(0);
|
const buttonSize = computed(() => (isMobile.value ? 'medium' : 'medium'));
|
||||||
const currentPage = ref(1);
|
|
||||||
const initialLoading = ref(true);
|
const filters = reactive({
|
||||||
const loadingMore = ref(false);
|
search: ''
|
||||||
|
});
|
||||||
|
|
||||||
const filters = reactive({
|
// 计算是否还有更多数据可加载
|
||||||
search: ''
|
const hasMoreToLoad = computed(() => {
|
||||||
});
|
return orders.value.length < totalCount.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 定义表格列
|
||||||
|
const tableColumns = computed(() => {
|
||||||
|
const statusColumn = {
|
||||||
|
title: $t('Status'),
|
||||||
|
key: 'status',
|
||||||
|
width: isMobile.value ? 100 : 120,
|
||||||
|
render(row) {
|
||||||
|
const statusProps = getStatusProps(row.status);
|
||||||
|
return h(NTag, {
|
||||||
|
type: statusProps.color === 'green' ? 'success' :
|
||||||
|
statusProps.color === 'orange' ? 'warning' :
|
||||||
|
statusProps.color === 'red' ? 'error' : 'info',
|
||||||
|
size: isMobile.value ? 'small' : 'medium'
|
||||||
|
}, { default: () => statusProps.label });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseColumns = [
|
||||||
|
{
|
||||||
|
title: $t('Time'),
|
||||||
|
key: 'creation',
|
||||||
|
width: isMobile.value ? 140 : 160,
|
||||||
|
ellipsis: { tooltip: true },
|
||||||
|
render(row) {
|
||||||
|
return formatDate(row.creation);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('Title'),
|
||||||
|
key: 'title',
|
||||||
|
width: isMobile.value ? 120 : 150,
|
||||||
|
ellipsis: { tooltip: true },
|
||||||
|
render(row) {
|
||||||
|
return row.title || '-';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('Order ID'),
|
||||||
|
key: 'order_id',
|
||||||
|
width: isMobile.value ? 120 : 150,
|
||||||
|
ellipsis: { tooltip: true },
|
||||||
|
render(row) {
|
||||||
|
return row.order_id || '-';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('Transaction ID'),
|
||||||
|
key: 'trade_no',
|
||||||
|
width: isMobile.value ? 120 : 150,
|
||||||
|
ellipsis: { tooltip: true },
|
||||||
|
render(row) {
|
||||||
|
return row.trade_no || '-';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('Order Type'),
|
||||||
|
key: 'order_type',
|
||||||
|
width: isMobile.value ? 100 : 120,
|
||||||
|
ellipsis: { tooltip: true },
|
||||||
|
render(row) {
|
||||||
|
return row.order_type || '-';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('Payment Method'),
|
||||||
|
key: 'payment_method',
|
||||||
|
width: isMobile.value ? 100 : 120,
|
||||||
|
ellipsis: { tooltip: true },
|
||||||
|
render(row) {
|
||||||
|
return row.payment_method || '-';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('Description'),
|
||||||
|
key: 'description',
|
||||||
|
width: isMobile.value ? 150 : 200,
|
||||||
|
ellipsis: { tooltip: true },
|
||||||
|
render(row) {
|
||||||
|
return row.description || '-';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('Amount'),
|
||||||
|
key: 'total',
|
||||||
|
width: isMobile.value ? 100 : 120,
|
||||||
|
align: 'right',
|
||||||
|
render(row) {
|
||||||
|
return formatCurrency(row.total);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
statusColumn
|
||||||
|
];
|
||||||
|
|
||||||
|
// 移动端隐藏部分列
|
||||||
|
if (isMobile.value) {
|
||||||
|
return baseColumns.filter(col =>
|
||||||
|
['creation', 'title', 'total', 'status'].includes(col.key)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseColumns;
|
||||||
|
});
|
||||||
|
|
||||||
// 定义表格列
|
// 创建获取订单的资源
|
||||||
const columns = [
|
const ordersResource = createResource({
|
||||||
{ key: 'creation', label: $t('Time'), class: '' },
|
|
||||||
{ key: 'title', label: $t('Title'), class: '' },
|
|
||||||
{ key: 'order_id', label: $t('Order ID'), class: '' },
|
|
||||||
{ key: 'trade_no', label: $t('Transaction ID'), class: '' },
|
|
||||||
{ key: 'order_type', label: $t('Order Type'), class: '' },
|
|
||||||
{ key: 'payment_method', label: $t('Payment Method'), class: '' },
|
|
||||||
{ key: 'description', label: $t('Description'), class: '' },
|
|
||||||
{ key: 'total', label: $t('Amount'), class: 'text-right' },
|
|
||||||
{ key: 'status', label: $t('Status'), class: '' }
|
|
||||||
];
|
|
||||||
|
|
||||||
// 计算是否还有更多数据可加载
|
|
||||||
const hasMoreToLoad = computed(() => {
|
|
||||||
return orders.value.length < totalCount.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建获取订单的资源
|
|
||||||
const ordersResource = createResource({
|
|
||||||
url: 'jcloud.api.billing.get_orders',
|
url: 'jcloud.api.billing.get_orders',
|
||||||
transform(response) {
|
transform(response) {
|
||||||
return {
|
return {
|
||||||
@ -165,178 +242,247 @@ export default {
|
|||||||
initialLoading.value = false;
|
initialLoading.value = false;
|
||||||
loadingMore.value = false;
|
loadingMore.value = false;
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
initialLoading.value = false;
|
initialLoading.value = false;
|
||||||
loadingMore.value = false;
|
loadingMore.value = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 导出资源
|
// 导出资源
|
||||||
const exportResource = createResource({
|
const exportResource = createResource({
|
||||||
url: 'jcloud.api.billing.get_orders',
|
url: 'jcloud.api.billing.get_orders',
|
||||||
onSuccess(response) {
|
onSuccess(response) {
|
||||||
const orders = response.orders || [];
|
const orders = response.orders || [];
|
||||||
|
|
||||||
// 定义CSV字段
|
|
||||||
const fields = [
|
|
||||||
$t('Title'),
|
|
||||||
$t('Order ID'),
|
|
||||||
$t('Transaction ID'),
|
|
||||||
$t('Order Type'),
|
|
||||||
$t('Payment Method'),
|
|
||||||
$t('Description'),
|
|
||||||
$t('Amount'),
|
|
||||||
$t('Status'),
|
|
||||||
$t('Creation Time')
|
|
||||||
];
|
|
||||||
|
|
||||||
// 准备数据
|
|
||||||
const csvData = orders.map(order => [
|
|
||||||
order.title || '',
|
|
||||||
order.order_id || '',
|
|
||||||
order.trade_no || '',
|
|
||||||
order.order_type || '',
|
|
||||||
order.payment_method || '',
|
|
||||||
order.description || '',
|
|
||||||
order.total || 0,
|
|
||||||
order.status || '',
|
|
||||||
order.creation || ''
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 添加表头
|
|
||||||
csvData.unshift(fields);
|
|
||||||
|
|
||||||
// 生成CSV
|
|
||||||
let csv = unparse(csvData);
|
|
||||||
|
|
||||||
// 添加BOM以支持中文
|
|
||||||
csv = '\uFEFF' + csv;
|
|
||||||
|
|
||||||
// 触发下载
|
|
||||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
|
||||||
const today = new Date().toISOString().split('T')[0];
|
|
||||||
const filename = `${$t('Order Records')}-${today}.csv`;
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = URL.createObjectURL(blob);
|
|
||||||
link.download = filename;
|
|
||||||
link.click();
|
|
||||||
URL.revokeObjectURL(link.href);
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
// 导出失败处理
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 方法:加载初始数据
|
// 定义CSV字段
|
||||||
function fetchOrders() {
|
const fields = [
|
||||||
|
$t('Title'),
|
||||||
|
$t('Order ID'),
|
||||||
|
$t('Transaction ID'),
|
||||||
|
$t('Order Type'),
|
||||||
|
$t('Payment Method'),
|
||||||
|
$t('Description'),
|
||||||
|
$t('Amount'),
|
||||||
|
$t('Status'),
|
||||||
|
$t('Creation Time')
|
||||||
|
];
|
||||||
|
|
||||||
|
// 准备数据
|
||||||
|
const csvData = orders.map(order => [
|
||||||
|
order.title || '',
|
||||||
|
order.order_id || '',
|
||||||
|
order.trade_no || '',
|
||||||
|
order.order_type || '',
|
||||||
|
order.payment_method || '',
|
||||||
|
order.description || '',
|
||||||
|
order.total || 0,
|
||||||
|
order.status || '',
|
||||||
|
order.creation || ''
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 添加表头
|
||||||
|
csvData.unshift(fields);
|
||||||
|
|
||||||
|
// 生成CSV
|
||||||
|
let csv = unparse(csvData);
|
||||||
|
|
||||||
|
// 添加BOM以支持中文
|
||||||
|
csv = '\uFEFF' + csv;
|
||||||
|
|
||||||
|
// 触发下载
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const filename = `${$t('Order Records')}-${today}.csv`;
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = filename;
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(link.href);
|
||||||
|
exporting.value = false;
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
exporting.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 方法:加载初始数据
|
||||||
|
function fetchOrders() {
|
||||||
if (currentPage.value === 1) {
|
if (currentPage.value === 1) {
|
||||||
initialLoading.value = true;
|
initialLoading.value = true;
|
||||||
} else {
|
} else {
|
||||||
loadingMore.value = true;
|
loadingMore.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ordersResource.submit({
|
ordersResource.submit({
|
||||||
page: currentPage.value,
|
page: currentPage.value,
|
||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
search: filters.search,
|
search: filters.search,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 方法:重置并获取第一页数据
|
// 方法:重置并获取第一页数据
|
||||||
function resetAndFetch() {
|
function resetAndFetch() {
|
||||||
currentPage.value = 1;
|
currentPage.value = 1;
|
||||||
fetchOrders();
|
fetchOrders();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 方法:加载更多数据
|
// 方法:加载更多数据
|
||||||
function loadMore() {
|
function loadMore() {
|
||||||
if (loadingMore.value) return;
|
if (loadingMore.value) return;
|
||||||
|
|
||||||
currentPage.value += 1;
|
currentPage.value += 1;
|
||||||
loadingMore.value = true;
|
loadingMore.value = true;
|
||||||
fetchOrders();
|
fetchOrders();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索防抖
|
// 搜索防抖
|
||||||
let searchTimeout;
|
let searchTimeout;
|
||||||
function debouncedSearch() {
|
function debouncedSearch() {
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
searchTimeout = setTimeout(() => {
|
searchTimeout = setTimeout(() => {
|
||||||
resetAndFetch();
|
resetAndFetch();
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出为CSV
|
// 导出为CSV
|
||||||
function exportToCsv() {
|
function exportToCsv() {
|
||||||
exportResource.submit({
|
exporting.value = true;
|
||||||
page: 1,
|
exportResource.submit({
|
||||||
page_size: 1000, // 设置较大的值获取更多数据用于导出
|
page: 1,
|
||||||
search: filters.search,
|
page_size: 1000, // 设置较大的值获取更多数据用于导出
|
||||||
});
|
search: filters.search,
|
||||||
}
|
});
|
||||||
|
}
|
||||||
// 格式化金额
|
|
||||||
function formatCurrency(amount) {
|
// 格式化金额
|
||||||
if (amount === undefined || amount === null) return '-';
|
function formatCurrency(amount) {
|
||||||
return new Intl.NumberFormat('zh-CN', {
|
if (amount === undefined || amount === null) return '-';
|
||||||
style: 'currency',
|
return new Intl.NumberFormat('zh-CN', {
|
||||||
currency: 'CNY',
|
style: 'currency',
|
||||||
}).format(amount);
|
currency: 'CNY',
|
||||||
}
|
}).format(amount);
|
||||||
|
}
|
||||||
// 格式化日期
|
|
||||||
function formatDate(dateString) {
|
// 格式化日期
|
||||||
if (!dateString) return '-';
|
function formatDate(dateString) {
|
||||||
|
if (!dateString) return '-';
|
||||||
const date = new Date(dateString);
|
|
||||||
if (isNaN(date.getTime())) return '-';
|
const date = new Date(dateString);
|
||||||
|
if (isNaN(date.getTime())) return '-';
|
||||||
// 格式化为 YYYY-MM-DD HH:MM 格式
|
|
||||||
const year = date.getFullYear();
|
// 格式化为 YYYY-MM-DD HH:MM 格式
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
const year = date.getFullYear();
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
const hours = String(date.getHours()).padStart(2, '0');
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
|
||||||
}
|
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||||
|
}
|
||||||
// 获取状态样式
|
|
||||||
function getStatusProps(status) {
|
// 获取状态样式
|
||||||
const statusMap = {
|
function getStatusProps(status) {
|
||||||
'待支付': { label: $t('Pending Payment'), color: 'orange' },
|
const statusMap = {
|
||||||
'已支付': { label: $t('Paid'), color: 'green' },
|
'待支付': { label: $t('Pending Payment'), color: 'orange' },
|
||||||
'交易成功': { label: $t('Transaction Successful'), color: 'green' },
|
'已支付': { label: $t('Paid'), color: 'green' },
|
||||||
'已取消': { label: $t('Cancelled'), color: 'red' },
|
'交易成功': { label: $t('Transaction Successful'), color: 'green' },
|
||||||
'已退款': { label: $t('Refunded'), color: 'red' },
|
'已取消': { label: $t('Cancelled'), color: 'red' },
|
||||||
};
|
'已退款': { label: $t('Refunded'), color: 'red' },
|
||||||
|
};
|
||||||
return statusMap[status] || { label: status || '', color: 'blue' };
|
|
||||||
}
|
return statusMap[status] || { label: status || '', color: 'blue' };
|
||||||
|
}
|
||||||
// 生命周期钩子
|
|
||||||
onMounted(() => {
|
function handleResize() {
|
||||||
fetchOrders();
|
windowWidth.value = window.innerWidth;
|
||||||
});
|
}
|
||||||
|
|
||||||
return {
|
// 生命周期钩子
|
||||||
columns,
|
onMounted(() => {
|
||||||
orders,
|
handleResize();
|
||||||
totalCount,
|
window.addEventListener('resize', handleResize);
|
||||||
currentPage,
|
fetchOrders();
|
||||||
initialLoading,
|
});
|
||||||
loadingMore,
|
|
||||||
hasMoreToLoad,
|
onUnmounted(() => {
|
||||||
filters,
|
window.removeEventListener('resize', handleResize);
|
||||||
fetchOrders,
|
});
|
||||||
resetAndFetch,
|
</script>
|
||||||
loadMore,
|
|
||||||
debouncedSearch,
|
<style scoped>
|
||||||
exportToCsv,
|
.billing-orders-container {
|
||||||
formatCurrency,
|
padding: 24px;
|
||||||
formatDate,
|
max-width: 1400px;
|
||||||
getStatusProps
|
margin: 0 auto;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
.orders-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding-top: 16px;
|
||||||
|
margin-top: 16px;
|
||||||
|
border-top: 1px solid var(--n-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-info {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--n-text-color-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 60px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-table {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.orders-header {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
</script>
|
.orders-footer {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.billing-orders-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-header {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,19 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="billing-overview-container">
|
||||||
v-if="team.pg"
|
<div v-if="team.pg" class="billing-overview-content">
|
||||||
class="flex flex-1 flex-col gap-8 overflow-y-auto px-60 pt-6"
|
<PaymentDetails />
|
||||||
>
|
</div>
|
||||||
<PaymentDetails />
|
<div v-else class="loading-container">
|
||||||
</div>
|
<n-spin size="large" />
|
||||||
<div v-else class="mt-12 flex flex-1 items-center justify-center">
|
</div>
|
||||||
<Spinner class="h-8" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed, provide, inject, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
import { NSpin } from 'naive-ui';
|
||||||
import PaymentDetails from '../components/billing/PaymentDetails.vue';
|
import PaymentDetails from '../components/billing/PaymentDetails.vue';
|
||||||
import { Spinner, createResource } from 'jingrow-ui';
|
import { createResource } from 'jingrow-ui';
|
||||||
import { computed, provide, inject } from 'vue';
|
|
||||||
|
|
||||||
const team = inject('team');
|
const team = inject('team');
|
||||||
|
|
||||||
@ -37,4 +37,31 @@ provide('billing', {
|
|||||||
),
|
),
|
||||||
unpaidInvoices
|
unpaidInvoices
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.billing-overview-container {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.billing-overview-content {
|
||||||
|
padding: 24px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 400px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.billing-overview-content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,209 +1,221 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-5">
|
<div class="billing-payment-methods-container">
|
||||||
<ObjectList :options="options" />
|
<n-card class="payment-methods-card">
|
||||||
|
<ObjectList :options="options" />
|
||||||
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script setup>
|
||||||
import { defineAsyncComponent, h } from 'vue';
|
import { defineAsyncComponent, h, computed, getCurrentInstance } from 'vue';
|
||||||
|
import { NCard } from 'naive-ui';
|
||||||
import ObjectList from '../components/ObjectList.vue';
|
import ObjectList from '../components/ObjectList.vue';
|
||||||
import { Badge, FeatherIcon, Tooltip } from 'jingrow-ui';
|
import { Badge, FeatherIcon, Tooltip } from 'jingrow-ui';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { confirmDialog, renderDialog, icon } from '../utils/components';
|
import { confirmDialog, renderDialog, icon } from '../utils/components';
|
||||||
|
|
||||||
export default {
|
const instance = getCurrentInstance();
|
||||||
name: 'BillingPaymentMethods',
|
const $t = instance?.appContext.config.globalProperties.$t || ((key) => key);
|
||||||
props: ['tab'],
|
const $team = instance?.appContext.config.globalProperties.$team;
|
||||||
components: {
|
|
||||||
ObjectList
|
const props = defineProps(['tab']);
|
||||||
},
|
|
||||||
computed: {
|
const options = computed(() => {
|
||||||
options() {
|
return {
|
||||||
return {
|
pagetype: 'Stripe Payment Method',
|
||||||
pagetype: 'Stripe Payment Method',
|
fields: [
|
||||||
fields: [
|
'name',
|
||||||
'name',
|
'is_default',
|
||||||
'is_default',
|
'expiry_month',
|
||||||
'expiry_month',
|
'expiry_year',
|
||||||
'expiry_year',
|
'brand',
|
||||||
'brand',
|
'stripe_mandate_id'
|
||||||
'stripe_mandate_id'
|
],
|
||||||
],
|
emptyStateMessage: $t('No cards added'),
|
||||||
emptyStateMessage: this.$t('No cards added'),
|
columns: [
|
||||||
columns: [
|
{
|
||||||
{
|
label: $t('Card Name'),
|
||||||
label: this.$t('Card Name'),
|
fieldname: 'name_on_card'
|
||||||
fieldname: 'name_on_card'
|
},
|
||||||
},
|
{
|
||||||
{
|
label: $t('Card'),
|
||||||
label: this.$t('Card'),
|
fieldname: 'last_4',
|
||||||
fieldname: 'last_4',
|
width: 1.5,
|
||||||
width: 1.5,
|
format(value) {
|
||||||
format(value) {
|
return `•••• ${value}`;
|
||||||
return `•••• ${value}`;
|
},
|
||||||
},
|
prefix: row => {
|
||||||
prefix: row => {
|
return cardBrandIcon(row.brand);
|
||||||
return this.cardBrandIcon(row.brand);
|
},
|
||||||
},
|
suffix(row) {
|
||||||
suffix(row) {
|
if (row.is_default) {
|
||||||
if (row.is_default) {
|
return h(
|
||||||
return h(
|
Badge,
|
||||||
Badge,
|
{
|
||||||
{
|
theme: 'green'
|
||||||
theme: 'green'
|
},
|
||||||
},
|
() => $t('Default')
|
||||||
() => this.$t('Default')
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: this.$t('Expiry Date'),
|
|
||||||
width: 0.5,
|
|
||||||
format(value, row) {
|
|
||||||
return `${row.expiry_month}/${row.expiry_year}`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: this.$t('Authorization'),
|
|
||||||
type: 'Component',
|
|
||||||
width: 1,
|
|
||||||
align: 'center',
|
|
||||||
component({ row }) {
|
|
||||||
if (row.stripe_mandate_id) {
|
|
||||||
return h(FeatherIcon, {
|
|
||||||
name: 'check-circle',
|
|
||||||
class: 'h-4 w-4 text-green-600'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '',
|
|
||||||
type: 'Component',
|
|
||||||
align: 'right',
|
|
||||||
component({ row }) {
|
|
||||||
if (row.is_default && row.stripe_payment_method) {
|
|
||||||
return h(
|
|
||||||
Tooltip,
|
|
||||||
{
|
|
||||||
text: this.$t('This card failed to pay last time. Please use another card.')
|
|
||||||
},
|
|
||||||
() =>
|
|
||||||
h(FeatherIcon, {
|
|
||||||
name: 'alert-circle',
|
|
||||||
class: 'h-4 w-4 text-red-600'
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '',
|
|
||||||
fieldname: 'creation',
|
|
||||||
type: 'Timestamp',
|
|
||||||
align: 'right'
|
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
rowActions: ({ listResource, row }) => {
|
},
|
||||||
return [
|
{
|
||||||
{
|
label: $t('Expiry Date'),
|
||||||
label: this.$t('Set as Default'),
|
width: 0.5,
|
||||||
onClick: () => {
|
format(value, row) {
|
||||||
|
return `${row.expiry_month}/${row.expiry_year}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('Authorization'),
|
||||||
|
type: 'Component',
|
||||||
|
width: 1,
|
||||||
|
align: 'center',
|
||||||
|
component({ row }) {
|
||||||
|
if (row.stripe_mandate_id) {
|
||||||
|
return h(FeatherIcon, {
|
||||||
|
name: 'check-circle',
|
||||||
|
class: 'h-4 w-4 text-green-600'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
type: 'Component',
|
||||||
|
align: 'right',
|
||||||
|
component({ row }) {
|
||||||
|
if (row.is_default && row.stripe_payment_method) {
|
||||||
|
return h(
|
||||||
|
Tooltip,
|
||||||
|
{
|
||||||
|
text: $t('This card failed to pay last time. Please use another card.')
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
h(FeatherIcon, {
|
||||||
|
name: 'alert-circle',
|
||||||
|
class: 'h-4 w-4 text-red-600'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
fieldname: 'creation',
|
||||||
|
type: 'Timestamp',
|
||||||
|
align: 'right'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rowActions: ({ listResource, row }) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: $t('Set as Default'),
|
||||||
|
onClick: () => {
|
||||||
|
toast.promise(
|
||||||
|
listResource.runDocMethod.submit({
|
||||||
|
method: 'set_default',
|
||||||
|
name: row.name
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
loading: $t('Setting as default...'),
|
||||||
|
success: $t('Default card set'),
|
||||||
|
error: $t('Failed to set default card')
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
condition: () => !row.is_default
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('Remove'),
|
||||||
|
onClick: () => {
|
||||||
|
if (row.is_default && $team.pg.payment_mode === 'Card') {
|
||||||
|
toast.error($t('Cannot remove default card'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
confirmDialog({
|
||||||
|
title: $t('Remove Card'),
|
||||||
|
message: $t('Are you sure you want to remove this card?'),
|
||||||
|
onSuccess: ({ hide }) => {
|
||||||
toast.promise(
|
toast.promise(
|
||||||
listResource.runDocMethod.submit({
|
listResource.delete.submit(row.name, {
|
||||||
method: 'set_default',
|
onSuccess() {
|
||||||
name: row.name
|
hide();
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
loading: this.$t('Setting as default...'),
|
loading: $t('Removing card...'),
|
||||||
success: this.$t('Default card set'),
|
success: $t('Card removed'),
|
||||||
error: this.$t('Failed to set default card')
|
error: error =>
|
||||||
|
error.messages?.length
|
||||||
|
? error.messages.join('\n')
|
||||||
|
: error.message || $t('Failed to remove card')
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
|
||||||
condition: () => !row.is_default
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: this.$t('Remove'),
|
|
||||||
onClick: () => {
|
|
||||||
if (row.is_default && this.$team.pg.payment_mode === 'Card') {
|
|
||||||
toast.error(this.$t('Cannot remove default card'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
confirmDialog({
|
|
||||||
title: this.$t('Remove Card'),
|
|
||||||
message: this.$t('Are you sure you want to remove this card?'),
|
|
||||||
onSuccess: ({ hide }) => {
|
|
||||||
toast.promise(
|
|
||||||
listResource.delete.submit(row.name, {
|
|
||||||
onSuccess() {
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
loading: this.$t('Removing card...'),
|
|
||||||
success: this.$t('Card removed'),
|
|
||||||
error: error =>
|
|
||||||
error.messages?.length
|
|
||||||
? error.messages.join('\n')
|
|
||||||
: error.message || this.$t('Failed to remove card')
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
];
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
},
|
||||||
|
orderBy: 'creation desc',
|
||||||
|
primaryAction() {
|
||||||
|
return {
|
||||||
|
label: $t('Add Card'),
|
||||||
|
slots: {
|
||||||
|
prefix: icon('plus')
|
||||||
},
|
},
|
||||||
orderBy: 'creation desc',
|
onClick: () => {
|
||||||
primaryAction() {
|
let StripeCardDialog = defineAsyncComponent(() =>
|
||||||
return {
|
import('../components/StripeCardDialog.vue')
|
||||||
label: this.$t('Add Card'),
|
);
|
||||||
slots: {
|
renderDialog(StripeCardDialog);
|
||||||
prefix: icon('plus')
|
|
||||||
},
|
|
||||||
onClick: () => {
|
|
||||||
let StripeCardDialog = defineAsyncComponent(() =>
|
|
||||||
import('../components/StripeCardDialog.vue')
|
|
||||||
);
|
|
||||||
renderDialog(StripeCardDialog);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
methods: {
|
});
|
||||||
formatCurrency(value) {
|
|
||||||
if (value === 0) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return this.$format.userCurrency(value);
|
|
||||||
},
|
|
||||||
cardBrandIcon(brand) {
|
|
||||||
let component = {
|
|
||||||
'master-card': defineAsyncComponent(() =>
|
|
||||||
import('@/components/icons/cards/MasterCard.vue')
|
|
||||||
),
|
|
||||||
visa: defineAsyncComponent(() =>
|
|
||||||
import('@/components/icons/cards/Visa.vue')
|
|
||||||
),
|
|
||||||
amex: defineAsyncComponent(() =>
|
|
||||||
import('@/components/icons/cards/Amex.vue')
|
|
||||||
),
|
|
||||||
jcb: defineAsyncComponent(() =>
|
|
||||||
import('@/components/icons/cards/JCB.vue')
|
|
||||||
),
|
|
||||||
generic: defineAsyncComponent(() =>
|
|
||||||
import('@/components/icons/cards/Generic.vue')
|
|
||||||
),
|
|
||||||
'union-pay': defineAsyncComponent(() =>
|
|
||||||
import('@/components/icons/cards/UnionPay.vue')
|
|
||||||
)
|
|
||||||
}[brand || 'generic'];
|
|
||||||
|
|
||||||
return h(component, { class: 'h-4 w-6' });
|
function cardBrandIcon(brand) {
|
||||||
}
|
let component = {
|
||||||
|
'master-card': defineAsyncComponent(() =>
|
||||||
|
import('@/components/icons/cards/MasterCard.vue')
|
||||||
|
),
|
||||||
|
visa: defineAsyncComponent(() =>
|
||||||
|
import('@/components/icons/cards/Visa.vue')
|
||||||
|
),
|
||||||
|
amex: defineAsyncComponent(() =>
|
||||||
|
import('@/components/icons/cards/Amex.vue')
|
||||||
|
),
|
||||||
|
jcb: defineAsyncComponent(() =>
|
||||||
|
import('@/components/icons/cards/JCB.vue')
|
||||||
|
),
|
||||||
|
generic: defineAsyncComponent(() =>
|
||||||
|
import('@/components/icons/cards/Generic.vue')
|
||||||
|
),
|
||||||
|
'union-pay': defineAsyncComponent(() =>
|
||||||
|
import('@/components/icons/cards/UnionPay.vue')
|
||||||
|
)
|
||||||
|
}[brand || 'generic'];
|
||||||
|
|
||||||
|
return h(component, { class: 'h-4 w-6' });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.billing-payment-methods-container {
|
||||||
|
padding: 24px;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-methods-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.billing-payment-methods-container {
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
</script>
|
</style>
|
||||||
@ -106,6 +106,7 @@ Domains,域名,
|
|||||||
Download,下载,
|
Download,下载,
|
||||||
Draft,草案,
|
Draft,草案,
|
||||||
Due Date,到期日,
|
Due Date,到期日,
|
||||||
|
Export,导出,
|
||||||
Export as CSV,导出为CSV,
|
Export as CSV,导出为CSV,
|
||||||
Duration,持续时间,
|
Duration,持续时间,
|
||||||
JERP Partner,JERP合作伙伴,
|
JERP Partner,JERP合作伙伴,
|
||||||
|
|||||||
|
Loading…
x
Reference in New Issue
Block a user