feat: add media supply billing center
This commit is contained in:
@@ -70,6 +70,60 @@ export interface OpsMediaSupplyWalletsParams {
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
export interface OpsMediaSupplyBillingEntry {
|
||||
id: number
|
||||
tenant_id: number
|
||||
tenant_name?: string | null
|
||||
user_id: number
|
||||
user_name?: string | null
|
||||
user_phone?: string | null
|
||||
order_id?: number | null
|
||||
order_title?: string | null
|
||||
order_status?: string | null
|
||||
external_order_code?: string | null
|
||||
delta_cents: number
|
||||
balance_after_cents: number
|
||||
reason: string
|
||||
note?: string | null
|
||||
sales_cents: number
|
||||
cost_cents: number
|
||||
gross_profit_cents: number
|
||||
created_by?: number | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface OpsMediaSupplyBillingSummary {
|
||||
total_entries: number
|
||||
total_sales_cents: number
|
||||
total_cost_cents: number
|
||||
gross_profit_cents: number
|
||||
net_wallet_delta_cents: number
|
||||
order_debit_cents: number
|
||||
order_refund_cents: number
|
||||
recharge_cents: number
|
||||
manual_adjustment_cents: number
|
||||
}
|
||||
|
||||
export interface OpsMediaSupplyBillingsResponse {
|
||||
items: OpsMediaSupplyBillingEntry[]
|
||||
total: number
|
||||
page: number
|
||||
size: number
|
||||
summary: OpsMediaSupplyBillingSummary
|
||||
}
|
||||
|
||||
export interface OpsMediaSupplyBillingsParams {
|
||||
keyword?: string
|
||||
tenant_id?: number
|
||||
user_id?: number
|
||||
order_id?: number
|
||||
reason?: string
|
||||
start_at?: string
|
||||
end_at?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
export const opsMediaSupplyApi = {
|
||||
listResources(params: OpsMediaSupplyResourcesParams) {
|
||||
return http.get<OpsMediaSupplyResourcesResponse>(
|
||||
@@ -107,6 +161,12 @@ export const opsMediaSupplyApi = {
|
||||
params as Record<string, unknown>,
|
||||
)
|
||||
},
|
||||
listBillings(params: OpsMediaSupplyBillingsParams) {
|
||||
return http.get<OpsMediaSupplyBillingsResponse>(
|
||||
'/media-supply/billings',
|
||||
params as Record<string, unknown>,
|
||||
)
|
||||
},
|
||||
adjustWallet(payload: {
|
||||
tenant_id: number
|
||||
user_id: number
|
||||
|
||||
@@ -214,7 +214,162 @@
|
||||
{{ formatDate(record.updated_at) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-button type="link" @click="openWalletModal(record)">调整余额</a-button>
|
||||
<div class="wallet-actions">
|
||||
<a-button type="link" @click="openUserBillings(record)">账单</a-button>
|
||||
<a-button type="link" @click="openWalletModal(record)">调整余额</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="billings" tab="账单中心">
|
||||
<div class="ops-card media-ops-card billing-card">
|
||||
<div class="billing-summary">
|
||||
<div class="billing-summary__item">
|
||||
<span>销售额</span>
|
||||
<strong>{{ formatMoney(billingSummary.total_sales_cents) }}</strong>
|
||||
</div>
|
||||
<div class="billing-summary__item">
|
||||
<span>成本</span>
|
||||
<strong>{{ formatMoney(billingSummary.total_cost_cents) }}</strong>
|
||||
</div>
|
||||
<div class="billing-summary__item billing-summary__item--profit">
|
||||
<span>毛利</span>
|
||||
<strong>{{ formatMoney(billingSummary.gross_profit_cents) }}</strong>
|
||||
<small>毛利率 {{ formatPercent(grossProfitRate) }}</small>
|
||||
</div>
|
||||
<div class="billing-summary__item">
|
||||
<span>账单数</span>
|
||||
<strong>{{ billingSummary.total_entries }}</strong>
|
||||
</div>
|
||||
<div class="billing-summary__item">
|
||||
<span>净账变</span>
|
||||
<strong>{{ formatMoney(billingSummary.net_wallet_delta_cents) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ops-toolbar media-ops-toolbar billing-toolbar">
|
||||
<a-input
|
||||
v-model:value="billingFilter.keyword"
|
||||
allow-clear
|
||||
placeholder="客户 / 手机 / 租户 / 订单 / 备注"
|
||||
style="width: 280px"
|
||||
@press-enter="resetBillings"
|
||||
/>
|
||||
<a-input
|
||||
v-model:value="billingFilter.tenantId"
|
||||
allow-clear
|
||||
placeholder="租户 ID"
|
||||
style="width: 120px"
|
||||
@press-enter="resetBillings"
|
||||
/>
|
||||
<a-input
|
||||
v-model:value="billingFilter.userId"
|
||||
allow-clear
|
||||
placeholder="用户 ID"
|
||||
style="width: 120px"
|
||||
@press-enter="resetBillings"
|
||||
/>
|
||||
<a-input
|
||||
v-model:value="billingFilter.orderId"
|
||||
allow-clear
|
||||
placeholder="订单 ID"
|
||||
style="width: 120px"
|
||||
@press-enter="resetBillings"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="billingFilter.reason"
|
||||
allow-clear
|
||||
placeholder="账单类型"
|
||||
style="width: 140px"
|
||||
:options="billingReasonOptions"
|
||||
@change="resetBillings"
|
||||
/>
|
||||
<a-range-picker
|
||||
v-model:value="billingDateRange"
|
||||
show-time
|
||||
style="width: 360px"
|
||||
@change="resetBillings"
|
||||
/>
|
||||
<a-button @click="setBillingThisMonth">本月</a-button>
|
||||
<a-button @click="showAllBillings">全部</a-button>
|
||||
<a-button type="primary" @click="resetBillings">查询</a-button>
|
||||
<a-button @click="clearBillingFilters">重置</a-button>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="billingColumns"
|
||||
:data-source="billingRows"
|
||||
:loading="billingLoading"
|
||||
:pagination="billingPagination"
|
||||
row-key="billing_key"
|
||||
:scroll="{ x: 1780 }"
|
||||
@change="onBillingTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'created_at'">
|
||||
<div class="mono-line">{{ formatDate(record.created_at) }}</div>
|
||||
<div class="muted">#{{ record.id }}</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'user'">
|
||||
<div class="resource-main">
|
||||
<strong>
|
||||
{{ record.user_name || record.user_phone || `用户 ${record.user_id}` }}
|
||||
</strong>
|
||||
<div class="resource-sub">
|
||||
<code>U{{ record.user_id }}</code>
|
||||
<span>{{ record.user_phone || '未绑定手机' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'tenant'">
|
||||
<div>{{ record.tenant_name || `租户 ${record.tenant_id}` }}</div>
|
||||
<div class="muted">T{{ record.tenant_id }}</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'order'">
|
||||
<div class="resource-main">
|
||||
<strong>{{ record.order_title || record.note || '--' }}</strong>
|
||||
<div class="resource-sub">
|
||||
<code v-if="record.order_id">O{{ record.order_id }}</code>
|
||||
<span v-if="record.external_order_code">{{ record.external_order_code }}</span>
|
||||
<a-tag v-if="record.order_status" :color="orderStatusColor(record.order_status)">
|
||||
{{ orderStatusLabel(record.order_status) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'reason'">
|
||||
<a-tag :color="billingReasonColor(record.reason)">
|
||||
{{ billingReasonLabel(record.reason) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'delta_cents'">
|
||||
<strong :class="amountClass(record.delta_cents)">
|
||||
{{ formatSignedMoney(record.delta_cents) }}
|
||||
</strong>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'sales_cents'">
|
||||
<strong :class="amountClass(record.sales_cents)">
|
||||
{{ formatMoney(record.sales_cents) }}
|
||||
</strong>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'cost_cents'">
|
||||
<strong :class="amountClass(record.cost_cents)">
|
||||
{{ formatMoney(record.cost_cents) }}
|
||||
</strong>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'gross_profit_cents'">
|
||||
<div class="price-stack">
|
||||
<strong :class="amountClass(record.gross_profit_cents)">
|
||||
{{ formatMoney(record.gross_profit_cents) }}
|
||||
</strong>
|
||||
<span>{{ formatPercent(profitRate(record)) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'balance_after_cents'">
|
||||
<strong>{{ formatMoney(record.balance_after_cents) }}</strong>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
@@ -287,12 +442,14 @@
|
||||
import { EditOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import type { TableColumnsType, TablePaginationConfig } from 'ant-design-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import dayjs, { type Dayjs } from 'dayjs'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
|
||||
import { showOpsError } from '@/lib/errors'
|
||||
import {
|
||||
opsMediaSupplyApi,
|
||||
type OpsMediaSupplyBillingEntry,
|
||||
type OpsMediaSupplyBillingSummary,
|
||||
type OpsMediaSupplyResource,
|
||||
type OpsMediaSupplyWallet,
|
||||
} from '@/lib/media-supply'
|
||||
@@ -301,9 +458,25 @@ interface WalletRow extends OpsMediaSupplyWallet {
|
||||
wallet_key: string
|
||||
}
|
||||
|
||||
interface BillingRow extends OpsMediaSupplyBillingEntry {
|
||||
billing_key: string
|
||||
}
|
||||
|
||||
const MODEL_ID_AUTHORITY_NEWS = 1
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
const emptyBillingSummary: OpsMediaSupplyBillingSummary = {
|
||||
total_entries: 0,
|
||||
total_sales_cents: 0,
|
||||
total_cost_cents: 0,
|
||||
gross_profit_cents: 0,
|
||||
net_wallet_delta_cents: 0,
|
||||
order_debit_cents: 0,
|
||||
order_refund_cents: 0,
|
||||
recharge_cents: 0,
|
||||
manual_adjustment_cents: 0,
|
||||
}
|
||||
|
||||
const activeTab = ref('resources')
|
||||
const resourcePage = ref(1)
|
||||
const resourcePageSize = ref(PAGE_SIZE)
|
||||
@@ -332,6 +505,17 @@ const walletForm = reactive({
|
||||
note: '',
|
||||
})
|
||||
|
||||
const billingPage = ref(1)
|
||||
const billingPageSize = ref(PAGE_SIZE)
|
||||
const billingLoading = ref(false)
|
||||
const billingTotal = ref(0)
|
||||
const billingRows = ref<BillingRow[]>([])
|
||||
const billingSummary = ref<OpsMediaSupplyBillingSummary>({ ...emptyBillingSummary })
|
||||
const billingDateRange = ref<[Dayjs, Dayjs] | null>([
|
||||
dayjs().startOf('month'),
|
||||
dayjs().endOf('day'),
|
||||
])
|
||||
|
||||
const resourceFilter = reactive({
|
||||
keyword: '',
|
||||
remarkKeyword: '',
|
||||
@@ -347,11 +531,26 @@ const walletFilter = reactive({
|
||||
tenantId: '',
|
||||
})
|
||||
|
||||
const billingFilter = reactive({
|
||||
keyword: '',
|
||||
tenantId: '',
|
||||
userId: '',
|
||||
orderId: '',
|
||||
reason: undefined as string | undefined,
|
||||
})
|
||||
|
||||
const visibilityOptions = [
|
||||
{ label: '客户可见', value: 'visible' },
|
||||
{ label: '客户隐藏', value: 'hidden' },
|
||||
]
|
||||
|
||||
const billingReasonOptions = [
|
||||
{ label: '投稿扣费', value: 'order_debit' },
|
||||
{ label: '订单退款', value: 'order_refund' },
|
||||
{ label: '充值', value: 'recharge' },
|
||||
{ label: '手动调整', value: 'adjustment' },
|
||||
]
|
||||
|
||||
const resourceColumns: TableColumnsType<OpsMediaSupplyResource> = [
|
||||
{ title: '媒体名称', key: 'name', width: 320, fixed: 'left' },
|
||||
{ title: '售价 / 成本', key: 'price', width: 150, align: 'right' },
|
||||
@@ -375,7 +574,20 @@ const walletColumns: TableColumnsType<WalletRow> = [
|
||||
{ title: '余额', key: 'balance_cents', width: 160, align: 'right' },
|
||||
{ title: '账单', key: 'ledger', width: 180 },
|
||||
{ title: '更新时间', key: 'updated_at', width: 180 },
|
||||
{ title: '操作', key: 'actions', width: 130, fixed: 'right', align: 'center' },
|
||||
{ title: '操作', key: 'actions', width: 160, fixed: 'right', align: 'center' },
|
||||
]
|
||||
|
||||
const billingColumns: TableColumnsType<BillingRow> = [
|
||||
{ title: '时间', key: 'created_at', width: 180, fixed: 'left' },
|
||||
{ title: '客户', key: 'user', width: 240 },
|
||||
{ title: '租户', key: 'tenant', width: 200 },
|
||||
{ title: '订单 / 备注', key: 'order', width: 320 },
|
||||
{ title: '类型', key: 'reason', width: 120, align: 'center' },
|
||||
{ title: '账变', key: 'delta_cents', width: 140, align: 'right' },
|
||||
{ title: '销售额', key: 'sales_cents', width: 140, align: 'right' },
|
||||
{ title: '成本', key: 'cost_cents', width: 140, align: 'right' },
|
||||
{ title: '毛利', key: 'gross_profit_cents', width: 150, align: 'right' },
|
||||
{ title: '余额快照', key: 'balance_after_cents', width: 150, align: 'right' },
|
||||
]
|
||||
|
||||
const resourcePagination = computed<TablePaginationConfig>(() => ({
|
||||
@@ -394,10 +606,27 @@ const walletPagination = computed<TablePaginationConfig>(() => ({
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}))
|
||||
|
||||
const billingPagination = computed<TablePaginationConfig>(() => ({
|
||||
current: billingPage.value,
|
||||
pageSize: billingPageSize.value,
|
||||
total: billingTotal.value,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}))
|
||||
|
||||
const grossProfitRate = computed(() =>
|
||||
billingSummary.value.total_sales_cents === 0
|
||||
? null
|
||||
: billingSummary.value.gross_profit_cents / billingSummary.value.total_sales_cents,
|
||||
)
|
||||
|
||||
watch(activeTab, (key) => {
|
||||
if (key === 'wallets' && walletRows.value.length === 0) {
|
||||
void loadWallets()
|
||||
}
|
||||
if (key === 'billings' && billingRows.value.length === 0) {
|
||||
void loadBillings()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
@@ -449,6 +678,33 @@ async function loadWallets(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBillings(): Promise<void> {
|
||||
billingLoading.value = true
|
||||
try {
|
||||
const result = await opsMediaSupplyApi.listBillings({
|
||||
keyword: billingFilter.keyword.trim() || undefined,
|
||||
tenant_id: parsePositiveNumber(billingFilter.tenantId),
|
||||
user_id: parsePositiveNumber(billingFilter.userId),
|
||||
order_id: parsePositiveNumber(billingFilter.orderId),
|
||||
reason: billingFilter.reason,
|
||||
start_at: billingDateRange.value?.[0]?.toISOString(),
|
||||
end_at: billingDateRange.value?.[1]?.toISOString(),
|
||||
page: billingPage.value,
|
||||
page_size: billingPageSize.value,
|
||||
})
|
||||
billingRows.value = result.items.map((item) => ({
|
||||
...item,
|
||||
billing_key: String(item.id),
|
||||
}))
|
||||
billingTotal.value = result.total
|
||||
billingSummary.value = result.summary
|
||||
} catch (error) {
|
||||
showOpsError(error)
|
||||
} finally {
|
||||
billingLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetResources(): void {
|
||||
resourcePage.value = 1
|
||||
void loadResources()
|
||||
@@ -470,12 +726,37 @@ function resetWallets(): void {
|
||||
void loadWallets()
|
||||
}
|
||||
|
||||
function resetBillings(): void {
|
||||
billingPage.value = 1
|
||||
void loadBillings()
|
||||
}
|
||||
|
||||
function clearWalletFilters(): void {
|
||||
walletFilter.keyword = ''
|
||||
walletFilter.tenantId = ''
|
||||
resetWallets()
|
||||
}
|
||||
|
||||
function clearBillingFilters(): void {
|
||||
billingFilter.keyword = ''
|
||||
billingFilter.tenantId = ''
|
||||
billingFilter.userId = ''
|
||||
billingFilter.orderId = ''
|
||||
billingFilter.reason = undefined
|
||||
billingDateRange.value = [dayjs().startOf('month'), dayjs().endOf('day')]
|
||||
resetBillings()
|
||||
}
|
||||
|
||||
function setBillingThisMonth(): void {
|
||||
billingDateRange.value = [dayjs().startOf('month'), dayjs().endOf('day')]
|
||||
resetBillings()
|
||||
}
|
||||
|
||||
function showAllBillings(): void {
|
||||
billingDateRange.value = null
|
||||
resetBillings()
|
||||
}
|
||||
|
||||
function onResourceTableChange(pagination: TablePaginationConfig): void {
|
||||
resourcePage.value = pagination.current ?? 1
|
||||
resourcePageSize.value = pagination.pageSize ?? PAGE_SIZE
|
||||
@@ -488,6 +769,29 @@ function onWalletTableChange(pagination: TablePaginationConfig): void {
|
||||
void loadWallets()
|
||||
}
|
||||
|
||||
function onBillingTableChange(pagination: TablePaginationConfig): void {
|
||||
billingPage.value = pagination.current ?? 1
|
||||
billingPageSize.value = pagination.pageSize ?? PAGE_SIZE
|
||||
void loadBillings()
|
||||
}
|
||||
|
||||
function openUserBillings(row: WalletRow): void {
|
||||
billingFilter.keyword = ''
|
||||
billingFilter.tenantId = String(row.tenant_id)
|
||||
billingFilter.userId = String(row.user_id)
|
||||
billingFilter.orderId = ''
|
||||
billingFilter.reason = undefined
|
||||
billingPage.value = 1
|
||||
if (activeTab.value === 'billings') {
|
||||
void loadBillings()
|
||||
return
|
||||
}
|
||||
activeTab.value = 'billings'
|
||||
if (billingRows.value.length > 0) {
|
||||
void loadBillings()
|
||||
}
|
||||
}
|
||||
|
||||
async function queueSync(): Promise<void> {
|
||||
syncLoading.value = true
|
||||
try {
|
||||
@@ -587,7 +891,29 @@ async function submitWalletAdjustment(): Promise<void> {
|
||||
|
||||
function formatMoney(cents?: number | null): string {
|
||||
if (typeof cents !== 'number' || !Number.isFinite(cents)) return '--'
|
||||
return `¥${(cents / 100).toFixed(2)}`
|
||||
const prefix = cents < 0 ? '-' : ''
|
||||
return `${prefix}¥${(Math.abs(cents) / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
function formatSignedMoney(cents?: number | null): string {
|
||||
if (typeof cents !== 'number' || !Number.isFinite(cents)) return '--'
|
||||
const prefix = cents > 0 ? '+' : cents < 0 ? '-' : ''
|
||||
return `${prefix}¥${(Math.abs(cents) / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
function formatPercent(value: number | null): string {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) return '--'
|
||||
return `${(value * 100).toFixed(1)}%`
|
||||
}
|
||||
|
||||
function profitRate(row: OpsMediaSupplyBillingEntry): number | null {
|
||||
if (!row.sales_cents) return null
|
||||
return row.gross_profit_cents / row.sales_cents
|
||||
}
|
||||
|
||||
function amountClass(cents?: number | null): string {
|
||||
if (typeof cents !== 'number' || cents === 0) return 'amount-neutral'
|
||||
return cents > 0 ? 'amount-positive' : 'amount-negative'
|
||||
}
|
||||
|
||||
function formatSellPrice(cents?: number | null): string {
|
||||
@@ -643,6 +969,37 @@ function statusColor(status: string): string {
|
||||
if (status === 'disabled') return 'default'
|
||||
return 'blue'
|
||||
}
|
||||
|
||||
function billingReasonLabel(reason: string): string {
|
||||
if (reason === 'order_debit') return '投稿扣费'
|
||||
if (reason === 'order_refund') return '订单退款'
|
||||
if (reason === 'recharge') return '充值'
|
||||
if (reason === 'adjustment') return '调整'
|
||||
return reason || '--'
|
||||
}
|
||||
|
||||
function billingReasonColor(reason: string): string {
|
||||
if (reason === 'order_debit') return 'blue'
|
||||
if (reason === 'order_refund') return 'orange'
|
||||
if (reason === 'recharge') return 'green'
|
||||
if (reason === 'adjustment') return 'purple'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
function orderStatusLabel(status: string): string {
|
||||
if (status === 'queued') return '排队'
|
||||
if (status === 'submitting') return '提交中'
|
||||
if (status === 'submitted') return '已提交'
|
||||
if (status === 'failed') return '失败'
|
||||
return status || '--'
|
||||
}
|
||||
|
||||
function orderStatusColor(status: string): string {
|
||||
if (status === 'failed') return 'red'
|
||||
if (status === 'submitted') return 'green'
|
||||
if (status === 'submitting') return 'gold'
|
||||
return 'blue'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -676,11 +1033,67 @@ function statusColor(status: string): string {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.billing-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.media-ops-toolbar {
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.billing-toolbar {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.billing-summary {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(5, minmax(150px, 1fr));
|
||||
}
|
||||
|
||||
.billing-summary__item {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-height: 86px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.billing-summary__item span {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.billing-summary__item strong {
|
||||
color: #0f172a;
|
||||
font-size: 22px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.billing-summary__item small {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.billing-summary__item--profit {
|
||||
background: #ecfdf5;
|
||||
border-color: #bbf7d0;
|
||||
}
|
||||
|
||||
.billing-summary__item--profit strong {
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.resource-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -741,16 +1154,54 @@ function statusColor(status: string): string {
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.wallet-actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mono-line,
|
||||
.amount-positive,
|
||||
.amount-negative,
|
||||
.amount-neutral,
|
||||
.wallet-balance,
|
||||
.price-stack strong {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.amount-positive {
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.amount-negative {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.amount-neutral {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.edit-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.billing-summary {
|
||||
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.media-ops-header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.billing-summary {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user