新增财务仪表盘并统一金额到角
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { apiClient } from '@/shared/api/client'
|
||||
import type { ApiResponse, PaginatedResult } from '@/shared/types/types'
|
||||
|
||||
export interface FinanceSummary {
|
||||
total_flow_amount_cent: number
|
||||
total_refund_amount_cent: number
|
||||
pending_refund_amount_cent: number
|
||||
channel_net_amount_cent: number
|
||||
platform_income_amount: number
|
||||
owner_should_income_amount: number
|
||||
owner_wallet_income_amount: number
|
||||
settlement_diff_amount: number
|
||||
successful_pay_count: number
|
||||
successful_refund_count: number
|
||||
pending_refund_count: number
|
||||
settled_order_count: number
|
||||
financial_exception_count: number
|
||||
}
|
||||
|
||||
export interface FinanceDailyItem {
|
||||
date: string
|
||||
total_flow_amount_cent: number
|
||||
total_refund_amount_cent: number
|
||||
pending_refund_amount_cent: number
|
||||
channel_net_amount_cent: number
|
||||
platform_income_amount: number
|
||||
owner_should_income_amount: number
|
||||
owner_wallet_income_amount: number
|
||||
settlement_diff_amount: number
|
||||
successful_pay_count: number
|
||||
successful_refund_count: number
|
||||
pending_refund_count: number
|
||||
settled_order_count: number
|
||||
}
|
||||
|
||||
export interface FinanceDashboard {
|
||||
summary: FinanceSummary
|
||||
daily_items: FinanceDailyItem[]
|
||||
generated_at: string
|
||||
}
|
||||
|
||||
export interface FinanceDetail {
|
||||
order_id: number
|
||||
order_no: string
|
||||
order_status: string
|
||||
settlement_status: string
|
||||
refund_status: string
|
||||
renter_id: number
|
||||
renter_phone: string
|
||||
renter_nickname: string
|
||||
owner_id: number
|
||||
owner_phone: string
|
||||
owner_nickname: string
|
||||
order_rent_amount: number
|
||||
order_deposit_amount: number
|
||||
checkout_rent_amount: number
|
||||
checkout_renter_refund: number
|
||||
checkout_owner_income: number
|
||||
checkout_platform_fee: number
|
||||
owner_wallet_income_amount: number
|
||||
paid_amount_cent: number
|
||||
refunded_amount_cent: number
|
||||
refunding_amount_cent: number
|
||||
failed_refund_amount_cent: number
|
||||
channel_net_amount_cent: number
|
||||
platform_net_amount: number
|
||||
settlement_diff_amount: number
|
||||
finance_status: string
|
||||
created_at: string
|
||||
settled_at?: string
|
||||
}
|
||||
|
||||
export interface FinanceDateQuery {
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
}
|
||||
|
||||
export interface FinanceDetailQuery extends FinanceDateQuery {
|
||||
order_no?: string
|
||||
user_id?: string
|
||||
order_status?: string
|
||||
settlement_status?: string
|
||||
date_type?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
function cleanParams(query: object) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(query).filter(([, value]) => value !== '' && value !== undefined)
|
||||
)
|
||||
}
|
||||
|
||||
export async function fetchFinanceDashboard(query: FinanceDateQuery = {}) {
|
||||
const { data } = await apiClient.get<ApiResponse<FinanceDashboard>>('/admin/finance/dashboard', {
|
||||
params: cleanParams(query),
|
||||
})
|
||||
return {
|
||||
...data.data,
|
||||
daily_items: Array.isArray(data.data?.daily_items) ? data.data.daily_items : [],
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchFinanceDetails(query: FinanceDetailQuery = {}) {
|
||||
const { data } = await apiClient.get<ApiResponse<PaginatedResult<FinanceDetail>>>(
|
||||
'/admin/finance/details',
|
||||
{ params: cleanParams(query) }
|
||||
)
|
||||
const result = data.data
|
||||
return {
|
||||
items: Array.isArray(result?.items) ? result.items : [],
|
||||
total: Number(result?.total ?? 0),
|
||||
page: Number(result?.page ?? query.page ?? 1),
|
||||
page_size: Number(result?.page_size ?? query.page_size ?? 20),
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { ref } from 'vue'
|
||||
|
||||
import type { WithdrawalDetail } from '@/features/admin/api/adminWithdrawal'
|
||||
import { reviewWithdrawal, confirmPayment } from '@/features/admin/api/adminWithdrawal'
|
||||
import { formatMoney } from '@/shared/utils/money'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
@@ -160,15 +161,15 @@ function accountTypeLabel(type: string) {
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="提现金额">
|
||||
<span style="color: #f56c6c; font-weight: 600; font-size: 16px">
|
||||
¥{{ withdrawal.amount.toFixed(2) }}
|
||||
¥{{ formatMoney(withdrawal.amount) }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="手续费">
|
||||
¥{{ withdrawal.fee.toFixed(2) }}
|
||||
¥{{ formatMoney(withdrawal.fee) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="实际到账" :span="2">
|
||||
<span style="color: #67c23a; font-weight: 600; font-size: 16px">
|
||||
¥{{ withdrawal.actual_amount.toFixed(2) }}
|
||||
¥{{ formatMoney(withdrawal.actual_amount) }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
@@ -273,7 +274,7 @@ function accountTypeLabel(type: string) {
|
||||
>
|
||||
<template #title>
|
||||
<div style="font-size: 13px">
|
||||
请手动转账 <strong>¥{{ withdrawal.actual_amount.toFixed(2) }}</strong> 到用户收款账号,
|
||||
请手动转账 <strong>¥{{ formatMoney(withdrawal.actual_amount) }}</strong> 到用户收款账号,
|
||||
完成后点击"确认打款"按钮。
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from './api/adminUsers'
|
||||
export * from './api/adminMgr'
|
||||
export * from './api/adminWallet'
|
||||
export * from './api/adminPayments'
|
||||
export * from './api/adminFinance'
|
||||
export * from './api/adminAudit'
|
||||
export * from './api/systemConfigs'
|
||||
export * from './composables/useAdminTable'
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
<script setup lang="ts">
|
||||
import { Refresh, Search } from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
|
||||
import {
|
||||
fetchFinanceDashboard,
|
||||
type FinanceDashboard,
|
||||
type FinanceDailyItem,
|
||||
} from '@/features/admin/api/adminFinance'
|
||||
import { formatMoneyWithSymbol } from '@/shared/utils/money'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
|
||||
const loading = ref(false)
|
||||
const dashboard = ref<FinanceDashboard | null>(null)
|
||||
const filters = reactive({
|
||||
start_date: defaultStartDate(),
|
||||
end_date: defaultEndDate(),
|
||||
})
|
||||
|
||||
const dailyItems = computed(() => dashboard.value?.daily_items ?? [])
|
||||
|
||||
onMounted(loadDashboard)
|
||||
|
||||
async function loadDashboard() {
|
||||
loading.value = true
|
||||
try {
|
||||
dashboard.value = await fetchFinanceDashboard(filters)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function moneyCent(value: number) {
|
||||
return formatMoneyWithSymbol(Number(value || 0) / 100)
|
||||
}
|
||||
|
||||
function money(value: number) {
|
||||
return formatMoneyWithSymbol(value)
|
||||
}
|
||||
|
||||
function defaultStartDate() {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - 6)
|
||||
return formatInputDate(date)
|
||||
}
|
||||
|
||||
function defaultEndDate() {
|
||||
return formatInputDate(new Date())
|
||||
}
|
||||
|
||||
function formatInputDate(date: Date) {
|
||||
return date.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
function diffType(value: number) {
|
||||
return Math.abs(Number(value || 0)) >= 0.05 ? 'danger' : 'success'
|
||||
}
|
||||
|
||||
function rowDiffClass(row: FinanceDailyItem) {
|
||||
return Math.abs(Number(row.settlement_diff_amount || 0)) >= 0.05 ? 'amount-danger' : ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header-row">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Finance Dashboard</p>
|
||||
<h1>财务仪表盘</h1>
|
||||
<p>按天查看第三方收款退款、平台收入、号主入账和结算差异。</p>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-date-picker
|
||||
v-model="filters.start_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="开始日期"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="filters.end_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="结束日期"
|
||||
/>
|
||||
<el-button type="primary" :icon="Search" :loading="loading" @click="loadDashboard">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button :icon="Refresh" :loading="loading" @click="loadDashboard">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="dashboard" class="metric-grid">
|
||||
<div class="metric-card">
|
||||
<span>总流水</span>
|
||||
<strong>{{ moneyCent(dashboard.summary.total_flow_amount_cent) }}</strong>
|
||||
<small>{{ dashboard.summary.successful_pay_count }} 笔成功收款</small>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>总退款</span>
|
||||
<strong>{{ moneyCent(dashboard.summary.total_refund_amount_cent) }}</strong>
|
||||
<small>{{ dashboard.summary.successful_refund_count }} 笔成功退款</small>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>渠道净流入</span>
|
||||
<strong>{{ moneyCent(dashboard.summary.channel_net_amount_cent) }}</strong>
|
||||
<small>成功收款 - 成功退款</small>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>平台收入</span>
|
||||
<strong>{{ money(dashboard.summary.platform_income_amount) }}</strong>
|
||||
<small>{{ dashboard.summary.settled_order_count }} 个已结算订单</small>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>号主应得</span>
|
||||
<strong>{{ money(dashboard.summary.owner_should_income_amount) }}</strong>
|
||||
<small>结账单口径</small>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>号主实际入账</span>
|
||||
<strong>{{ money(dashboard.summary.owner_wallet_income_amount) }}</strong>
|
||||
<small>钱包流水口径</small>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>退款中</span>
|
||||
<strong>{{ moneyCent(dashboard.summary.pending_refund_amount_cent) }}</strong>
|
||||
<small>{{ dashboard.summary.pending_refund_count }} 笔待回执</small>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>结算差异</span>
|
||||
<strong>
|
||||
<el-tag :type="diffType(dashboard.summary.settlement_diff_amount)">
|
||||
{{ money(dashboard.summary.settlement_diff_amount) }}
|
||||
</el-tag>
|
||||
</strong>
|
||||
<small>{{ dashboard.summary.financial_exception_count }} 个异常订单</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" class="table-panel" :data="dailyItems">
|
||||
<el-table-column prop="date" label="日期" width="130" />
|
||||
<el-table-column label="总流水" width="130">
|
||||
<template #default="{ row }">{{ moneyCent(row.total_flow_amount_cent) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总退款" width="130">
|
||||
<template #default="{ row }">{{ moneyCent(row.total_refund_amount_cent) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="渠道净流入" width="140">
|
||||
<template #default="{ row }">{{ moneyCent(row.channel_net_amount_cent) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="平台收入" width="130">
|
||||
<template #default="{ row }">{{ money(row.platform_income_amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="号主应得" width="130">
|
||||
<template #default="{ row }">{{ money(row.owner_should_income_amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="号主入账" width="130">
|
||||
<template #default="{ row }">{{ money(row.owner_wallet_income_amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结算差异" width="130">
|
||||
<template #default="{ row }">
|
||||
<span :class="rowDiffClass(row)">{{ money(row.settlement_diff_amount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收款/退款/结算" min-width="170">
|
||||
<template #default="{ row }">
|
||||
{{ row.successful_pay_count }} / {{ row.successful_refund_count }} /
|
||||
{{ row.settled_order_count }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<p v-if="dashboard" class="generated-at">
|
||||
数据生成时间:{{ formatDateTime(dashboard.generated_at) }}
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.amount-danger {
|
||||
color: #dc2626;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.generated-at {
|
||||
margin: 12px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,252 @@
|
||||
<script setup lang="ts">
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
|
||||
import {
|
||||
fetchFinanceDetails,
|
||||
type FinanceDetail,
|
||||
} from '@/features/admin/api/adminFinance'
|
||||
import { formatMoneyWithSymbol } from '@/shared/utils/money'
|
||||
import { orderStatusLabel } from '@/utils/statusLabels'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const details = ref<FinanceDetail[]>([])
|
||||
const currentPage = ref(1)
|
||||
const currentPageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const filters = reactive({
|
||||
start_date: defaultStartDate(),
|
||||
end_date: defaultEndDate(),
|
||||
date_type: 'settled',
|
||||
order_no: '',
|
||||
user_id: '',
|
||||
order_status: '',
|
||||
settlement_status: '',
|
||||
})
|
||||
|
||||
onMounted(loadDetails)
|
||||
|
||||
async function loadDetails() {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await fetchFinanceDetails({
|
||||
...filters,
|
||||
page: currentPage.value,
|
||||
page_size: currentPageSize.value,
|
||||
})
|
||||
details.value = result.items
|
||||
total.value = result.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filters.start_date = defaultStartDate()
|
||||
filters.end_date = defaultEndDate()
|
||||
filters.date_type = 'settled'
|
||||
filters.order_no = ''
|
||||
filters.user_id = ''
|
||||
filters.order_status = ''
|
||||
filters.settlement_status = ''
|
||||
currentPage.value = 1
|
||||
void loadDetails()
|
||||
}
|
||||
|
||||
async function handlePageChange() {
|
||||
await loadDetails()
|
||||
}
|
||||
|
||||
function money(value: number) {
|
||||
return formatMoneyWithSymbol(value)
|
||||
}
|
||||
|
||||
function moneyCent(value: number) {
|
||||
return formatMoneyWithSymbol(Number(value || 0) / 100)
|
||||
}
|
||||
|
||||
function financeStatusLabel(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
normal: '正常',
|
||||
refund_pending: '退款中',
|
||||
refund_failed: '退款失败',
|
||||
settlement_diff: '结算差异',
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
function financeStatusType(status: string) {
|
||||
if (status === 'normal') return 'success'
|
||||
if (status === 'settlement_diff' || status === 'refund_failed') return 'danger'
|
||||
if (status === 'refund_pending') return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function settlementStatusLabel(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
unsettled: '未结算',
|
||||
settling: '结算中',
|
||||
settled: '已结算',
|
||||
}
|
||||
return map[status] || status || '-'
|
||||
}
|
||||
|
||||
function defaultStartDate() {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - 29)
|
||||
return formatInputDate(date)
|
||||
}
|
||||
|
||||
function defaultEndDate() {
|
||||
return formatInputDate(new Date())
|
||||
}
|
||||
|
||||
function formatInputDate(date: Date) {
|
||||
return date.toISOString().slice(0, 10)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header-row">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Finance Details</p>
|
||||
<h1>财务明细</h1>
|
||||
<p>按订单核对收款、退款、平台收入、号主应得与钱包实际入账。</p>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" :icon="Search" :loading="loading" @click="loadDetails">
|
||||
查询
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form class="filter-panel" label-position="top">
|
||||
<el-form-item label="日期类型">
|
||||
<el-select v-model="filters.date_type" class="full-control">
|
||||
<el-option label="结算日期" value="settled" />
|
||||
<el-option label="下单日期" value="created" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="开始日期">
|
||||
<el-date-picker
|
||||
v-model="filters.start_date"
|
||||
class="full-control"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="结束日期">
|
||||
<el-date-picker
|
||||
v-model="filters.end_date"
|
||||
class="full-control"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="订单号">
|
||||
<el-input v-model="filters.order_no" clearable placeholder="按订单号筛选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户 ID">
|
||||
<el-input v-model="filters.user_id" clearable placeholder="租客或号主 ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="订单状态">
|
||||
<el-select v-model="filters.order_status" clearable placeholder="全部状态" class="full-control">
|
||||
<el-option label="待支付" value="pending_payment" />
|
||||
<el-option label="使用中" value="renting" />
|
||||
<el-option label="待结账" value="pending_checkout_confirm" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="已关闭" value="closed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="结算状态">
|
||||
<el-select
|
||||
v-model="filters.settlement_status"
|
||||
clearable
|
||||
placeholder="全部结算"
|
||||
class="full-control"
|
||||
>
|
||||
<el-option label="未结算" value="unsettled" />
|
||||
<el-option label="已结算" value="settled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" class="table-panel" :data="details">
|
||||
<el-table-column label="订单" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<RouterLink :to="`/admin/orders/${row.order_id}`">{{ row.order_no }}</RouterLink>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="150">
|
||||
<template #default="{ row }">
|
||||
<div>{{ orderStatusLabel(row.order_status) }}</div>
|
||||
<small>{{ settlementStatusLabel(row.settlement_status) }}</small>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="租客/号主" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.renter_phone || row.renter_nickname || row.renter_id }}</div>
|
||||
<small>{{ row.owner_phone || row.owner_nickname || row.owner_id }}</small>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收款" width="110">
|
||||
<template #default="{ row }">{{ moneyCent(row.paid_amount_cent) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="已退款" width="110">
|
||||
<template #default="{ row }">{{ moneyCent(row.refunded_amount_cent) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="退款中" width="110">
|
||||
<template #default="{ row }">{{ moneyCent(row.refunding_amount_cent) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="净流入" width="110">
|
||||
<template #default="{ row }">{{ moneyCent(row.channel_net_amount_cent) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="平台收入" width="110">
|
||||
<template #default="{ row }">{{ money(row.checkout_platform_fee) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="号主应得" width="110">
|
||||
<template #default="{ row }">{{ money(row.checkout_owner_income) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="号主入账" width="110">
|
||||
<template #default="{ row }">{{ money(row.owner_wallet_income_amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="差异" width="110">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'amount-danger': Math.abs(row.settlement_diff_amount) >= 0.05 }">
|
||||
{{ money(row.settlement_diff_amount) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="财务状态" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="financeStatusType(row.finance_status)">
|
||||
{{ financeStatusLabel(row.finance_status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结算时间" min-width="170">
|
||||
<template #default="{ row }">{{ row.settled_at ? formatDateTime(row.settled_at) : '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<AdminTablePagination
|
||||
v-if="total > 0"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="currentPageSize"
|
||||
:total="total"
|
||||
:loading="loading"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.amount-danger {
|
||||
color: #dc2626;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,7 @@ import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { fetchAdminFileBlob } from '@/shared/api/files'
|
||||
import { formatMoneyWithSymbol } from '@/shared/utils/money'
|
||||
import {
|
||||
adminMarkListingAbnormal,
|
||||
adminOfflineListing,
|
||||
@@ -78,7 +79,7 @@ async function submitAction() {
|
||||
}
|
||||
|
||||
function money(value: number) {
|
||||
return `¥${Math.round(Number(value || 0))}`
|
||||
return formatMoneyWithSymbol(value)
|
||||
}
|
||||
|
||||
function listingPrice(row: Listing) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
|
||||
import { fetchAdminFileBlob } from '@/shared/api/files'
|
||||
import { formatMoneyWithSymbol } from '@/shared/utils/money'
|
||||
import {
|
||||
adjustListingReviewPrice,
|
||||
approveListing,
|
||||
@@ -242,7 +243,7 @@ function openEvidence(row: Listing) {
|
||||
}
|
||||
|
||||
function money(value: number) {
|
||||
return `¥${Math.round(Number(value || 0))}`
|
||||
return formatMoneyWithSymbol(value)
|
||||
}
|
||||
|
||||
function quantity(value: number) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type RefundStatus,
|
||||
} from '@/features/orders'
|
||||
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments'
|
||||
import { formatMoney, formatMoneyWithSymbol } from '@/shared/utils/money'
|
||||
import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
import { formatListingNo } from '@/utils/listingDisplay'
|
||||
@@ -107,7 +108,7 @@ function orderEstimatedEndAt() {
|
||||
}
|
||||
|
||||
function money(value: unknown) {
|
||||
return Math.round(Number(value || 0))
|
||||
return formatMoney(Number(value || 0))
|
||||
}
|
||||
|
||||
async function handleRefund() {
|
||||
@@ -167,7 +168,7 @@ function paymentBizTypeLabel(type: string) {
|
||||
}
|
||||
|
||||
function moneyCent(value: number) {
|
||||
return `¥${(Number(value || 0) / 100).toFixed(2)}`
|
||||
return formatMoneyWithSymbol(Number(value || 0) / 100)
|
||||
}
|
||||
|
||||
function formatHandoffRecordType(type: string) {
|
||||
@@ -240,7 +241,7 @@ function formatHandoffRecordType(type: string) {
|
||||
<span>退款状态</span>
|
||||
<strong>{{ refundStatusLabel(refundStatus.refund_status) }}</strong>
|
||||
<small v-if="refundStatus.refund_amount_cent > 0"
|
||||
>¥{{ (refundStatus.refund_amount_cent / 100).toFixed(2) }}</small
|
||||
>{{ moneyCent(refundStatus.refund_amount_cent) }}</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type PaymentConfig,
|
||||
} from '@/features/admin/api/paymentConfig'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
import { formatMoney } from '@/shared/utils/money'
|
||||
import { readError } from '@/utils/error'
|
||||
import PaymentConfigDialog from '../components/PaymentConfigDialog.vue'
|
||||
|
||||
@@ -230,7 +231,7 @@ function formatEnvironment(env: string) {
|
||||
}
|
||||
|
||||
function formatAmount(amountCent: number) {
|
||||
return (amountCent / 100).toFixed(2)
|
||||
return formatMoney(amountCent / 100)
|
||||
}
|
||||
|
||||
function getStatusType(status: string) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Document, Search } from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
|
||||
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments'
|
||||
import { formatMoneyWithSymbol } from '@/shared/utils/money'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
||||
|
||||
@@ -69,7 +70,7 @@ async function handlePageChange() {
|
||||
}
|
||||
|
||||
function moneyCent(value: number) {
|
||||
return `¥${(Number(value || 0) / 100).toFixed(2)}`
|
||||
return formatMoneyWithSymbol(Number(value || 0) / 100)
|
||||
}
|
||||
|
||||
function paymentStatusType(status: string) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
unfreezeAdminUser,
|
||||
type AdminUserItem,
|
||||
} from '@/features/admin/api/adminUsers'
|
||||
import { formatMoney } from '@/shared/utils/money'
|
||||
import { useAdminPaginatedTable } from '@/features/admin/composables/useAdminPaginatedTable'
|
||||
import { userStatusLabel } from '@/utils/statusLabels'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
@@ -93,7 +94,7 @@ function readError(error: unknown, fallback: string) {
|
||||
}
|
||||
|
||||
function money(value: number | string | undefined) {
|
||||
return Number(value || 0).toFixed(2)
|
||||
return formatMoney(Number(value || 0))
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Search } from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
|
||||
import { fetchAdminWalletLedger, type AdminWalletLedger } from '@/features/admin/api/adminWallet'
|
||||
import { formatMoneyWithSymbol } from '@/shared/utils/money'
|
||||
import { balanceTypeLabel, ledgerDirectionLabel } from '@/utils/statusLabels'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
||||
@@ -64,7 +65,7 @@ async function handlePageChange() {
|
||||
}
|
||||
|
||||
function money(value: number) {
|
||||
return `¥${Math.round(Number(value || 0))}`
|
||||
return formatMoneyWithSymbol(value)
|
||||
}
|
||||
|
||||
function directionType(direction: string) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
confirmPayment,
|
||||
type WithdrawalDetail,
|
||||
} from '@/features/admin/api/adminWithdrawal'
|
||||
import { formatMoney } from '@/shared/utils/money'
|
||||
|
||||
import WithdrawalDetailDialog from '../components/WithdrawalDetailDialog.vue'
|
||||
|
||||
@@ -219,7 +220,7 @@ function onDetailDialogSaved() {
|
||||
</el-table-column>
|
||||
<el-table-column label="提现金额" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #f56c6c; font-weight: 600"> ¥{{ row.amount.toFixed(2) }} </span>
|
||||
<span style="color: #f56c6c; font-weight: 600"> ¥{{ formatMoney(row.amount) }} </span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收款方式" min-width="180">
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { fetchPostRentalNotice, type PostRentalNotice } from '@/features/orders/api/orders'
|
||||
import { formatDateMinute } from '@/utils/time'
|
||||
import { uploadFile } from '@/shared/api/files'
|
||||
import { formatMoney } from '@/shared/utils/money'
|
||||
|
||||
const session = useSessionStore()
|
||||
const router = useRouter()
|
||||
@@ -316,7 +317,7 @@ function resolveAvatarURL(url: string | undefined | null) {
|
||||
<div class="balance-row">
|
||||
<div class="balance-info">
|
||||
<span class="balance-label">账户可用余额(元)</span>
|
||||
<strong class="balance-value">¥{{ Math.round(Number(balance || 0)) }}</strong>
|
||||
<strong class="balance-value">¥{{ formatMoney(balance) }}</strong>
|
||||
</div>
|
||||
<button class="withdraw-btn" @click="handleWithdraw">提现</button>
|
||||
</div>
|
||||
@@ -417,7 +418,7 @@ function resolveAvatarURL(url: string | undefined | null) {
|
||||
<span class="ledger-time">{{ formatDateMinute(item.created_at) }}</span>
|
||||
</div>
|
||||
<div class="ledger-right" :class="item.direction === 'in' ? 'in-color' : 'out-color'">
|
||||
{{ item.direction === 'in' ? '+' : '-' }}¥{{ Math.round(Number(item.amount || 0)) }}
|
||||
{{ item.direction === 'in' ? '+' : '-' }}¥{{ formatMoney(item.amount) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import type { Listing } from '@/features/listings'
|
||||
import { formatMoney } from '@/shared/utils/money'
|
||||
import {
|
||||
formatHafCoinM,
|
||||
getCoinWan,
|
||||
@@ -135,11 +136,11 @@ function formatStatNumber(value: number) {
|
||||
<div class="card-price">
|
||||
<div class="price-item total">
|
||||
<small>总租金</small>
|
||||
<strong>¥{{ getListingDisplayPrice(listing) }}</strong>
|
||||
<strong>¥{{ formatMoney(getListingDisplayPrice(listing)) }}</strong>
|
||||
</div>
|
||||
<div class="price-item deposit">
|
||||
<small>押金</small>
|
||||
<span>¥{{ listing.deposit_amount }}</span>
|
||||
<span>¥{{ formatMoney(listing.deposit_amount) }}</span>
|
||||
</div>
|
||||
<div class="price-action">
|
||||
<button class="rent-btn">立即租用</button>
|
||||
|
||||
@@ -67,7 +67,7 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
const orderTotal = computed(() => {
|
||||
if (!listing.value) return '0'
|
||||
if (!listing.value) return '0.0'
|
||||
return formatMoney(getListingDisplayPrice(listing.value))
|
||||
})
|
||||
|
||||
@@ -102,7 +102,7 @@ const detailMetrics = computed(() => {
|
||||
tone: 'coin',
|
||||
},
|
||||
{ label: '价格', value: `¥${orderTotal.value}`, tone: 'price' },
|
||||
{ label: '押金', value: `¥${listing.value.deposit_amount}`, tone: '' },
|
||||
{ label: '押金', value: `¥${formatMoney(listing.value.deposit_amount)}`, tone: '' },
|
||||
]
|
||||
})
|
||||
|
||||
@@ -401,7 +401,7 @@ function listingPrice(item: Listing) {
|
||||
<strong>{{ resource.quantity }}</strong>
|
||||
<em>
|
||||
<b>{{ resource.mode || '--' }}</b>
|
||||
<small v-if="resource.amount > 0">¥{{ resource.amount }}</small>
|
||||
<small v-if="resource.amount > 0">¥{{ formatMoney(resource.amount) }}</small>
|
||||
<small v-else-if="resource.mode === '收费'">{{ resource.price || '¥0' }}</small>
|
||||
<small v-else>无额外收费</small>
|
||||
</em>
|
||||
@@ -458,14 +458,14 @@ function listingPrice(item: Listing) {
|
||||
<div class="order-price-breakdown">
|
||||
<div>
|
||||
<span>基础租金</span>
|
||||
<strong>¥{{ orderPriceBreakdown.rent }}</strong>
|
||||
<strong>¥{{ formatMoney(orderPriceBreakdown.rent) }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>额外物品</span>
|
||||
<strong>¥{{ orderPriceBreakdown.consumable }}</strong>
|
||||
<strong>¥{{ formatMoney(orderPriceBreakdown.consumable) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<em>押金另付 ¥{{ listing.deposit_amount }}</em>
|
||||
<em>押金另付 ¥{{ formatMoney(listing.deposit_amount) }}</em>
|
||||
</div>
|
||||
<dl class="order-check-list">
|
||||
<div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type ListingPublishOptions,
|
||||
} from '@/features/listings/api/listingOptions'
|
||||
import { fetchListings, type Listing } from '@/features/listings/api/listings'
|
||||
import { formatMoney } from '@/shared/utils/money'
|
||||
import {
|
||||
defaultHomeAnnouncements,
|
||||
defaultHomeBanners,
|
||||
@@ -620,8 +621,8 @@ function parseQuantityUnit(price: string) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-price-box">
|
||||
<strong>¥{{ getListingDisplayPrice(item) }}</strong>
|
||||
<span class="rent-sub">押金¥{{ item.deposit_amount }}</span>
|
||||
<strong>¥{{ formatMoney(getListingDisplayPrice(item)) }}</strong>
|
||||
<span class="rent-sub">押金¥{{ formatMoney(item.deposit_amount) }}</span>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { showToast } from 'vant'
|
||||
import MobileBottomNav from '@/components/MobileBottomNav.vue'
|
||||
|
||||
import { ensureSupportChat } from '@/features/chats/api/chats'
|
||||
import { formatMoney } from '@/shared/utils/money'
|
||||
import {
|
||||
emptyListingPublishOptions,
|
||||
type ListingPublishOptions,
|
||||
@@ -695,8 +696,8 @@ function chipTone(label: string) {
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="price-col">
|
||||
<strong>¥{{ getListingDisplayPrice(item) }}</strong>
|
||||
<span class="rent-sub">押金 ¥{{ item.deposit_amount }}</span>
|
||||
<strong>¥{{ formatMoney(getListingDisplayPrice(item)) }}</strong>
|
||||
<span class="rent-sub">押金 ¥{{ formatMoney(item.deposit_amount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -94,8 +94,8 @@ const detailMetrics = computed(() => {
|
||||
value: dailyLoss ? `${dailyLoss}/天` : '--',
|
||||
tone: 'coin',
|
||||
},
|
||||
{ label: '价格', value: `¥${getListingDisplayPrice(listing.value)}`, tone: 'price' },
|
||||
{ label: '押金', value: `¥${listing.value.deposit_amount}`, tone: '' },
|
||||
{ label: '价格', value: `¥${formatMoney(getListingDisplayPrice(listing.value))}`, tone: 'price' },
|
||||
{ label: '押金', value: `¥${formatMoney(listing.value.deposit_amount)}`, tone: '' },
|
||||
]
|
||||
})
|
||||
|
||||
@@ -369,7 +369,7 @@ async function copyListingCode() {
|
||||
<strong>{{ resource.quantity }}</strong>
|
||||
<em>
|
||||
<b>{{ resource.mode || '--' }}</b>
|
||||
<small v-if="resource.amount > 0">¥{{ resource.amount }}</small>
|
||||
<small v-if="resource.amount > 0">¥{{ formatMoney(resource.amount) }}</small>
|
||||
<small v-else-if="resource.mode === '收费'">{{ resource.price || '¥0' }}</small>
|
||||
<small v-else>无额外收费</small>
|
||||
</em>
|
||||
@@ -414,8 +414,8 @@ async function copyListingCode() {
|
||||
<span class="price-amount">¥{{ orderTotal }}</span>
|
||||
</div>
|
||||
<div class="order-price-detail">
|
||||
<span>租金 ¥{{ orderPriceBreakdown.rent }}</span>
|
||||
<span>额外 ¥{{ orderPriceBreakdown.consumable }}</span>
|
||||
<span>租金 ¥{{ formatMoney(orderPriceBreakdown.rent) }}</span>
|
||||
<span>额外 ¥{{ formatMoney(orderPriceBreakdown.consumable) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<van-button
|
||||
|
||||
@@ -6,6 +6,7 @@ import { showToast, showDialog } from 'vant'
|
||||
import { fetchOrderChat } from '@/features/chats/api/chats'
|
||||
import { createDispute } from '@/features/disputes/api/disputes'
|
||||
import { uploadFile } from '@/shared/api/files'
|
||||
import { formatMoney } from '@/shared/utils/money'
|
||||
import {
|
||||
acceptCheckout,
|
||||
cancelOrder,
|
||||
@@ -529,7 +530,7 @@ function readUnitPrice(priceText: string) {
|
||||
}
|
||||
|
||||
function roundMoney(value: number) {
|
||||
return Math.round(value)
|
||||
return Math.round(Number(value || 0) * 10) / 10
|
||||
}
|
||||
|
||||
function roundQuantity(value: number) {
|
||||
@@ -537,7 +538,7 @@ function roundQuantity(value: number) {
|
||||
}
|
||||
|
||||
function money(value: unknown) {
|
||||
return `${roundMoney(readNumber(value))}`
|
||||
return formatMoney(readNumber(value))
|
||||
}
|
||||
|
||||
function formatHandoffRecordType(type: string) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type Order,
|
||||
type PaymentOrder,
|
||||
} from '@/features/orders/api/orders'
|
||||
import { formatMoney } from '@/shared/utils/money'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { formatDateMinute } from '@/utils/time'
|
||||
import { formatListingNo } from '@/utils/listingDisplay'
|
||||
@@ -119,7 +120,7 @@ function readError(error: unknown, fallback: string) {
|
||||
}
|
||||
|
||||
function money(value: unknown) {
|
||||
return Math.round(Number(value || 0))
|
||||
return formatMoney(Number(value || 0))
|
||||
}
|
||||
|
||||
function isOwner(order: Order) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import QRCode from 'qrcode'
|
||||
import { fetchOrderChat } from '@/features/chats/api/chats'
|
||||
import { createDispute } from '@/features/disputes'
|
||||
import { uploadFile } from '@/shared/api/files'
|
||||
import { formatMoney } from '@/shared/utils/money'
|
||||
import {
|
||||
acceptCheckout,
|
||||
cancelOrder,
|
||||
@@ -588,7 +589,7 @@ function readUnitPrice(priceText: string) {
|
||||
}
|
||||
|
||||
function roundMoney(value: number) {
|
||||
return Math.round(value)
|
||||
return Math.round(Number(value || 0) * 10) / 10
|
||||
}
|
||||
|
||||
function roundQuantity(value: number) {
|
||||
@@ -596,7 +597,7 @@ function roundQuantity(value: number) {
|
||||
}
|
||||
|
||||
function money(value: unknown) {
|
||||
return `${roundMoney(readNumber(value))}`
|
||||
return formatMoney(readNumber(value))
|
||||
}
|
||||
|
||||
function orderRentAmount(item: Order) {
|
||||
@@ -1252,7 +1253,7 @@ async function copyListingCode() {
|
||||
<div class="pay-summary">
|
||||
<div class="pay-summary-row">
|
||||
<span>支付金额</span>
|
||||
<strong>¥{{ (activePayment.amount_cent / 100).toFixed(2) }}</strong>
|
||||
<strong>¥{{ formatMoney(activePayment.amount_cent / 100) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="paymentPayURL()" class="pay-qr-section">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { CopyDocument, Search } from '@element-plus/icons-vue'
|
||||
|
||||
import { fetchOrders, type Order } from '@/features/orders'
|
||||
import { formatMoney } from '@/shared/utils/money'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { orderStatusLabel } from '@/utils/statusLabels'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
@@ -139,7 +140,7 @@ function ownerActualIncome(order: Order) {
|
||||
}
|
||||
|
||||
function money(value: unknown) {
|
||||
return Math.round(Number(value || 0))
|
||||
return formatMoney(Number(value || 0))
|
||||
}
|
||||
|
||||
function shortenOrderNo(orderNo: string) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { computed, ref } from 'vue'
|
||||
import MobileBottomNav from '@/components/MobileBottomNav.vue'
|
||||
|
||||
import { usePublishForm } from '@/features/seller/composables/usePublishForm'
|
||||
import { formatMoney } from '@/shared/utils/money'
|
||||
import type { AgreementContent } from '@/features/listings/api/listingOptions'
|
||||
|
||||
const {
|
||||
@@ -549,7 +550,7 @@ function selectRadio<T extends string>(value: T, setter: (value: T) => void) {
|
||||
</template>
|
||||
</van-field>
|
||||
<button class="deposit-recommend-btn" type="button" @click="useRecommendedDeposit">
|
||||
推荐押金 ¥{{ recommendedDepositAmount }}
|
||||
推荐押金 ¥{{ formatMoney(recommendedDepositAmount) }}
|
||||
</button>
|
||||
<van-field label="每日损耗" required class="publish-field">
|
||||
<template #input>
|
||||
@@ -638,7 +639,7 @@ function selectRadio<T extends string>(value: T, setter: (value: T) => void) {
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field
|
||||
:model-value="calculatedCoinBasePrice ? `¥${calculatedCoinBasePrice}` : ''"
|
||||
:model-value="calculatedCoinBasePrice ? `¥${formatMoney(calculatedCoinBasePrice)}` : ''"
|
||||
label="纯币基础价"
|
||||
readonly
|
||||
required
|
||||
@@ -652,13 +653,13 @@ function selectRadio<T extends string>(value: T, setter: (value: T) => void) {
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field
|
||||
:model-value="`¥${calculatedConsumablePrice}`"
|
||||
:model-value="`¥${formatMoney(calculatedConsumablePrice)}`"
|
||||
label="额外消耗品"
|
||||
readonly
|
||||
class="publish-field result-field"
|
||||
/>
|
||||
<van-field
|
||||
:model-value="calculatedSellerPrice ? `¥${calculatedSellerPrice}` : ''"
|
||||
:model-value="calculatedSellerPrice ? `¥${formatMoney(calculatedSellerPrice)}` : ''"
|
||||
label="卖家价格"
|
||||
readonly
|
||||
required
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { usePublishForm } from '@/features/seller/composables/usePublishForm'
|
||||
import { formatMoney } from '@/shared/utils/money'
|
||||
import type { AgreementContent } from '@/features/listings/api/listingOptions'
|
||||
import OptionChips from './components/OptionChips.vue'
|
||||
import PublishSection from './components/PublishSection.vue'
|
||||
@@ -489,13 +490,13 @@ function selectDailyLoss(value: string | number) {
|
||||
:placeholder="priceConfig.deposit_placeholder"
|
||||
/>
|
||||
<button class="recommend-button" type="button" @click="useRecommendedDeposit">
|
||||
使用推荐 ¥{{ recommendedDepositAmount }}
|
||||
使用推荐 ¥{{ formatMoney(recommendedDepositAmount) }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="deposit-breakdown">
|
||||
<span v-for="item in depositBreakdownItems" :key="`${item.label}-${item.count}`">
|
||||
{{ item.label }}<template v-if="item.count > 1"> x{{ item.count }}</template> ¥{{
|
||||
item.amount
|
||||
formatMoney(item.amount)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
@@ -579,15 +580,15 @@ function selectDailyLoss(value: string | number) {
|
||||
</div>
|
||||
<div class="price-cell">
|
||||
<span>纯币基础价</span>
|
||||
<strong>{{ calculatedCoinBasePrice ? `¥${calculatedCoinBasePrice}` : '--' }}</strong>
|
||||
<strong>{{ calculatedCoinBasePrice ? `¥${formatMoney(calculatedCoinBasePrice)}` : '--' }}</strong>
|
||||
</div>
|
||||
<div class="price-cell">
|
||||
<span>额外消耗品</span>
|
||||
<strong>¥{{ calculatedConsumablePrice }}</strong>
|
||||
<strong>¥{{ formatMoney(calculatedConsumablePrice) }}</strong>
|
||||
</div>
|
||||
<div class="price-cell accent">
|
||||
<span>发布价格</span>
|
||||
<strong>{{ calculatedSellerPrice ? `¥${calculatedSellerPrice}` : '--' }}</strong>
|
||||
<strong>{{ calculatedSellerPrice ? `¥${formatMoney(calculatedSellerPrice)}` : '--' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -607,21 +608,21 @@ function selectDailyLoss(value: string | number) {
|
||||
<div class="summary-panel">
|
||||
<div class="summary-main">
|
||||
<span>卖家发布价格</span>
|
||||
<strong>{{ calculatedSellerPrice ? `¥${calculatedSellerPrice}` : '--' }}</strong>
|
||||
<strong>{{ calculatedSellerPrice ? `¥${formatMoney(calculatedSellerPrice)}` : '--' }}</strong>
|
||||
</div>
|
||||
<div class="summary-breakdown">
|
||||
<div class="summary-breakdown-title">价格明细</div>
|
||||
<div>
|
||||
<span>纯币价格</span>
|
||||
<strong>{{ calculatedCoinBasePrice ? `¥${calculatedCoinBasePrice}` : '--' }}</strong>
|
||||
<strong>{{ calculatedCoinBasePrice ? `¥${formatMoney(calculatedCoinBasePrice)}` : '--' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>额外物品价格</span>
|
||||
<strong>¥{{ calculatedConsumablePrice }}</strong>
|
||||
<strong>¥{{ formatMoney(calculatedConsumablePrice) }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>押金价格</span>
|
||||
<strong>{{ form.deposit_amount === '' ? '--' : `¥${form.deposit_amount}` }}</strong>
|
||||
<strong>{{ form.deposit_amount === '' ? '--' : `¥${formatMoney(Number(form.deposit_amount))}` }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-list">
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
submitListingReview,
|
||||
type Listing,
|
||||
} from '@/features/listings'
|
||||
import { formatMoney, formatMoneyWithSymbol } from '@/shared/utils/money'
|
||||
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
|
||||
import { formatListingCode, getListingSellerPrice } from '@/utils/listingDisplay'
|
||||
|
||||
@@ -103,7 +104,7 @@ function replaceListing(next: Listing) {
|
||||
}
|
||||
|
||||
function listingPrice(row: Listing) {
|
||||
return `¥${Math.round(getListingSellerPrice(row))}`
|
||||
return formatMoneyWithSymbol(getListingSellerPrice(row))
|
||||
}
|
||||
|
||||
async function copyListingCode(row: Listing) {
|
||||
@@ -226,7 +227,7 @@ function isPendingReview(row: Listing) {
|
||||
</div>
|
||||
<div>
|
||||
<span>押金</span>
|
||||
<strong>¥{{ item.deposit_amount }}</strong>
|
||||
<strong>¥{{ formatMoney(item.deposit_amount) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ function readError(error: unknown, fallback: string) {
|
||||
}
|
||||
|
||||
function formatMoney(value: number) {
|
||||
return `¥${Number(value || 0).toFixed(2)}`
|
||||
return `¥${(Math.round(Number(value || 0) * 10) / 10).toFixed(1)}`
|
||||
}
|
||||
|
||||
function walletBizTypeLabel(type: string) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type WithdrawalRequest,
|
||||
} from '../api/withdrawal'
|
||||
import { fetchWalletBalance, type WalletAccount } from '../api/wallet'
|
||||
import { formatMoney, formatMoneyWithSymbol } from '@/shared/utils/money'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -84,7 +85,7 @@ async function handleSubmit() {
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认提现 ¥${withdrawForm.value.amount.toFixed(2)} 到 ${selectedAccount.value?.account_name} (${selectedAccount.value?.account_no}) ?`,
|
||||
`确认提现 ${formatMoneyWithSymbol(withdrawForm.value.amount)} 到 ${selectedAccount.value?.account_name} (${selectedAccount.value?.account_no}) ?`,
|
||||
'确认提现',
|
||||
{
|
||||
confirmButtonText: '确认',
|
||||
@@ -175,7 +176,7 @@ function accountTypeLabel(type: string) {
|
||||
<el-icon><Wallet /></el-icon>
|
||||
可用余额
|
||||
</div>
|
||||
<div class="balance-value">¥{{ account?.available_balance.toFixed(2) || '0.00' }}</div>
|
||||
<div class="balance-value">¥{{ formatMoney(account?.available_balance) }}</div>
|
||||
</div>
|
||||
<div class="balance-item">
|
||||
<div class="balance-label">
|
||||
@@ -183,7 +184,7 @@ function accountTypeLabel(type: string) {
|
||||
冻结余额
|
||||
</div>
|
||||
<div class="balance-value frozen">
|
||||
¥{{ account?.frozen_balance.toFixed(2) || '0.00' }}
|
||||
¥{{ formatMoney(account?.frozen_balance) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -251,7 +252,7 @@ function accountTypeLabel(type: string) {
|
||||
|
||||
<el-form-item label="到账金额">
|
||||
<div class="actual-amount">
|
||||
¥{{ withdrawForm.amount > 0 ? withdrawForm.amount.toFixed(2) : '0.00' }}
|
||||
¥{{ formatMoney(withdrawForm.amount > 0 ? withdrawForm.amount : 0) }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
@@ -264,7 +265,7 @@ function accountTypeLabel(type: string) {
|
||||
show-icon
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
余额不足,可用余额:¥{{ account.available_balance.toFixed(2) }}
|
||||
余额不足,可用余额:¥{{ formatMoney(account.available_balance) }}
|
||||
</el-alert>
|
||||
|
||||
<el-alert type="info" :closable="false" show-icon style="margin-bottom: 16px">
|
||||
@@ -308,7 +309,7 @@ function accountTypeLabel(type: string) {
|
||||
<el-table-column prop="withdraw_no" label="提现单号" min-width="180" />
|
||||
<el-table-column label="提现金额" width="120">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #f56c6c; font-weight: 600"> ¥{{ row.amount.toFixed(2) }} </span>
|
||||
<span style="color: #f56c6c; font-weight: 600"> ¥{{ formatMoney(row.amount) }} </span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收款方式" width="150">
|
||||
|
||||
@@ -97,6 +97,18 @@ const allNavGroups: NavGroup[] = [
|
||||
index: 'finance',
|
||||
icon: Coin,
|
||||
children: [
|
||||
{
|
||||
label: '财务仪表盘',
|
||||
to: '/admin/finance/dashboard',
|
||||
icon: DataLine,
|
||||
permission: 'wallet:view',
|
||||
},
|
||||
{
|
||||
label: '财务明细',
|
||||
to: '/admin/finance/details',
|
||||
icon: Document,
|
||||
permission: 'wallet:view',
|
||||
},
|
||||
{ label: '资金流水', to: '/admin/wallet-ledger', icon: Wallet, permission: 'wallet:view' },
|
||||
{ label: '支付流水', to: '/admin/payments', icon: CreditCard, permission: 'wallet:view' },
|
||||
{ label: '提现审核', to: '/admin/withdrawals', icon: Money, permission: 'withdrawal:list' },
|
||||
|
||||
@@ -64,6 +64,18 @@ export const adminRoutes: RouteRecordRaw[] = [
|
||||
component: () => import('@/features/admin/views/AdminChatsView.vue'),
|
||||
meta: adminMeta,
|
||||
},
|
||||
{
|
||||
path: '/admin/finance/dashboard',
|
||||
name: 'admin-finance-dashboard',
|
||||
component: () => import('@/features/admin/views/AdminFinanceDashboardView.vue'),
|
||||
meta: adminMeta,
|
||||
},
|
||||
{
|
||||
path: '/admin/finance/details',
|
||||
name: 'admin-finance-details',
|
||||
component: () => import('@/features/admin/views/AdminFinanceDetailsView.vue'),
|
||||
meta: adminMeta,
|
||||
},
|
||||
{
|
||||
path: '/admin/wallet-ledger',
|
||||
name: 'admin-wallet-ledger',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { formatMoneyWithSymbol } from '@/shared/utils/money'
|
||||
|
||||
export function useMoney() {
|
||||
return (value: number | undefined | null) => `¥${Math.round(Number(value || 0))}`
|
||||
return (value: number | undefined | null) => formatMoneyWithSymbol(value)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* @example roundMoney(12.36) -> 12.4
|
||||
*/
|
||||
export function roundMoney(value: number): number {
|
||||
return Math.round(value * 10) / 10
|
||||
return Math.round(Number(value || 0) * 10) / 10
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,13 +46,13 @@ export function getListingDisplayPrice(item: Listing) {
|
||||
|
||||
export function getListingRentPrice(item: Listing) {
|
||||
const buyerCoinBasePrice = readPriceBreakdownNumber(item, 'buyer_coin_base_price')
|
||||
if (buyerCoinBasePrice > 0) return Math.round(buyerCoinBasePrice)
|
||||
return Math.max(0, Math.round(getListingDisplayPrice(item) - getListingConsumablePrice(item)))
|
||||
if (buyerCoinBasePrice > 0) return roundMoney(buyerCoinBasePrice)
|
||||
return Math.max(0, roundMoney(getListingDisplayPrice(item) - getListingConsumablePrice(item)))
|
||||
}
|
||||
|
||||
export function getListingConsumablePrice(item: Listing) {
|
||||
const consumablePrice = readPriceBreakdownNumber(item, 'consumable_price')
|
||||
if (consumablePrice > 0) return Math.round(consumablePrice)
|
||||
if (consumablePrice > 0) return roundMoney(consumablePrice)
|
||||
return getListingResources(item).reduce((sum, resource) => sum + resource.amount, 0)
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ export function getListingResources(item: Listing): ListingDisplayResource[] {
|
||||
mode: typeof row.mode === 'string' ? row.mode : '',
|
||||
amount:
|
||||
row.mode === '收费'
|
||||
? Math.round(
|
||||
? roundMoney(
|
||||
readUnknownNumber(row.quantity) *
|
||||
readUnitPrice(typeof row.price === 'string' ? row.price : '')
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ export const commonOnlineTimes = [
|
||||
]
|
||||
|
||||
export function roundMoney(value: number) {
|
||||
return Math.round(value)
|
||||
return Math.round(value * 10) / 10
|
||||
}
|
||||
|
||||
export function roundRatio(value: number) {
|
||||
@@ -31,7 +31,7 @@ export function roundRatio(value: number) {
|
||||
}
|
||||
|
||||
export function formatNumber(value: number) {
|
||||
return Number.isInteger(value) ? `${value}` : `${Math.round(value * 10) / 10}`
|
||||
return (Math.round(value * 10) / 10).toFixed(1)
|
||||
}
|
||||
|
||||
export function readUnitPrice(priceText: string) {
|
||||
|
||||
Reference in New Issue
Block a user