546 lines
18 KiB
Vue
546 lines
18 KiB
Vue
<template>
|
||
<div class="payment-orders-container">
|
||
<!-- Search and Filter Bar -->
|
||
<el-card class="filter-card">
|
||
<el-form :inline="true" :model="filters" class="filter-form">
|
||
<el-form-item label="用户ID">
|
||
<el-input v-model="filters.userId" placeholder="输入用户ID" clearable />
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select v-model="filters.status" placeholder="全部" clearable style="width: 120px">
|
||
<el-option label="全部" value="" />
|
||
<el-option label="有效" value="active" />
|
||
<el-option label="已取消" value="cancelled" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="货币类型">
|
||
<el-select v-model="filters.currency" placeholder="全部" clearable style="width: 120px">
|
||
<el-option label="全部" value="" />
|
||
<el-option label="人民币" value="rmb" />
|
||
<el-option label="比索" value="peso" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="支付时间">
|
||
<el-date-picker
|
||
v-model="dateRange"
|
||
type="daterange"
|
||
range-separator="至"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
value-format="YYYY-MM-DD"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||
<el-button @click="handleReset">重置</el-button>
|
||
<el-button type="success" @click="showCreateDialog">创建支付订单</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
|
||
<!-- Amount Statistics -->
|
||
<el-card class="statistics-card">
|
||
<div class="statistics-wrapper">
|
||
<div class="statistics-item">
|
||
<span class="statistics-label">人民币总额</span>
|
||
<span class="statistics-value rmb">¥{{ statistics.totalRmb }}</span>
|
||
</div>
|
||
<div class="statistics-item">
|
||
<span class="statistics-label">比索总额</span>
|
||
<span class="statistics-value peso">₱{{ statistics.totalPeso }}</span>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<!-- Orders Table -->
|
||
<el-card class="table-card">
|
||
<el-table v-loading="loading" :data="orders" stripe border>
|
||
<el-table-column prop="orderNo" label="订单编号" width="180" />
|
||
<el-table-column label="用户" width="150">
|
||
<template #default="{ row }">
|
||
<span>{{ row.user?.nickname || '-' }}</span>
|
||
<br />
|
||
<small class="text-muted">UID: {{ row.user?.uid || '-' }}</small>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="金额" width="180">
|
||
<template #default="{ row }">
|
||
<div class="amount-info">
|
||
<span v-if="row.amountPeso" class="amount peso">₱{{ row.amountPeso }}</span>
|
||
<span v-if="row.amountRmb" class="amount rmb">¥{{ row.amountRmb }}</span>
|
||
<span v-if="!row.amountPeso && !row.amountRmb && row.amount" class="amount">¥{{ row.amount }}</span>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="serviceContent" label="服务内容" min-width="200" show-overflow-tooltip />
|
||
<el-table-column prop="paymentTime" label="支付时间" width="160">
|
||
<template #default="{ row }">
|
||
{{ formatDate(row.paymentTime) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="佣金" width="120">
|
||
<template #default="{ row }">
|
||
<span v-if="row.commission" class="commission">¥{{ row.commission.amount }}</span>
|
||
<span v-else class="no-commission">无</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="status" label="状态" width="100">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.status === 'active' ? 'success' : 'danger'">
|
||
{{ row.status === 'active' ? '有效' : '已取消' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="150" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button type="primary" link @click="showDetails(row)">详情</el-button>
|
||
<el-button
|
||
v-if="row.status === 'active'"
|
||
type="danger"
|
||
link
|
||
@click="handleCancel(row)"
|
||
>取消</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<!-- Pagination -->
|
||
<div class="pagination-wrapper">
|
||
<el-pagination
|
||
v-model:current-page="pagination.page"
|
||
v-model:page-size="pagination.limit"
|
||
:total="pagination.total"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@size-change="handleSizeChange"
|
||
@current-change="handlePageChange"
|
||
/>
|
||
</div>
|
||
</el-card>
|
||
|
||
<!-- Create Dialog -->
|
||
<el-dialog v-model="createDialogVisible" title="创建支付订单" width="500px">
|
||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px">
|
||
<el-form-item label="用户ID" prop="userId">
|
||
<el-input v-model="createForm.userId" placeholder="输入用户UID(6位数字)" />
|
||
</el-form-item>
|
||
<el-form-item label="比索金额">
|
||
<el-input-number v-model="createForm.amountPeso" :min="0" :precision="2" placeholder="比索" />
|
||
<span class="currency-hint">₱ 比索(可选)</span>
|
||
</el-form-item>
|
||
<el-form-item label="人民币金额">
|
||
<el-input-number v-model="createForm.amountRmb" :min="0" :precision="2" placeholder="人民币" />
|
||
<span class="currency-hint">¥ 人民币(可选)</span>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-alert type="info" :closable="false" show-icon>
|
||
比索和人民币至少填写一项,如果两个都填写表示客户同时支付了两种货币
|
||
</el-alert>
|
||
</el-form-item>
|
||
<el-form-item label="服务内容" prop="serviceContent">
|
||
<el-input v-model="createForm.serviceContent" type="textarea" :rows="3" placeholder="输入服务内容" />
|
||
</el-form-item>
|
||
<el-form-item label="支付时间" prop="paymentTime">
|
||
<el-date-picker
|
||
v-model="createForm.paymentTime"
|
||
type="datetime"
|
||
placeholder="选择支付时间"
|
||
value-format="YYYY-MM-DD HH:mm:ss"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="关联预约单">
|
||
<el-input v-model="createForm.appointmentId" placeholder="可选,输入预约单ID" />
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input v-model="createForm.notes" type="textarea" :rows="2" placeholder="可选备注" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="createDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="creating" @click="handleCreate">创建</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- Details Dialog -->
|
||
<el-dialog v-model="detailsDialogVisible" title="订单详情" width="600px">
|
||
<el-descriptions v-if="currentOrder" :column="2" border>
|
||
<el-descriptions-item label="订单编号">{{ currentOrder.orderNo }}</el-descriptions-item>
|
||
<el-descriptions-item label="状态">
|
||
<el-tag :type="currentOrder.status === 'active' ? 'success' : 'danger'">
|
||
{{ currentOrder.status === 'active' ? '有效' : '已取消' }}
|
||
</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="用户">{{ currentOrder.user?.nickname }} (UID: {{ currentOrder.user?.uid }})</el-descriptions-item>
|
||
<el-descriptions-item label="支付金额">
|
||
<div class="amount-detail">
|
||
<span v-if="currentOrder.amountPeso" class="peso">₱{{ currentOrder.amountPeso }} (比索)</span>
|
||
<span v-if="currentOrder.amountRmb" class="rmb">¥{{ currentOrder.amountRmb }} (人民币)</span>
|
||
<span v-if="!currentOrder.amountPeso && !currentOrder.amountRmb && currentOrder.amount">¥{{ currentOrder.amount }}</span>
|
||
</div>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="服务内容" :span="2">{{ currentOrder.serviceContent }}</el-descriptions-item>
|
||
<el-descriptions-item label="支付时间">{{ formatDate(currentOrder.paymentTime) }}</el-descriptions-item>
|
||
<el-descriptions-item label="创建时间">{{ formatDate(currentOrder.createdAt) }}</el-descriptions-item>
|
||
<el-descriptions-item label="备注" :span="2">{{ currentOrder.notes || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="创建人">{{ currentOrder.creator?.realName || currentOrder.creator?.username }}</el-descriptions-item>
|
||
<el-descriptions-item label="关联预约单">{{ currentOrder.appointment?.appointmentNo || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item v-if="currentOrder.status === 'cancelled'" label="取消原因" :span="2">
|
||
<span class="cancel-reason">{{ currentOrder.cancelReason || '-' }}</span>
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
|
||
<el-divider v-if="currentOrder?.commission">佣金信息</el-divider>
|
||
<el-descriptions v-if="currentOrder?.commission" :column="2" border>
|
||
<el-descriptions-item label="邀请人">{{ currentOrder.commission.inviter?.nickname }} (UID: {{ currentOrder.commission.inviter?.uid }})</el-descriptions-item>
|
||
<el-descriptions-item label="佣金比例">{{ currentOrder.commission.commissionRate }}</el-descriptions-item>
|
||
<el-descriptions-item label="佣金金额">¥{{ currentOrder.commission.commissionAmount }}</el-descriptions-item>
|
||
<el-descriptions-item label="佣金状态">
|
||
<el-tag :type="currentOrder.commission.status === 'credited' ? 'success' : 'danger'">
|
||
{{ currentOrder.commission.status === 'credited' ? '已入账' : '已取消' }}
|
||
</el-tag>
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import api from '@/utils/api'
|
||
|
||
// State
|
||
const loading = ref(false)
|
||
const creating = ref(false)
|
||
const orders = ref([])
|
||
const dateRange = ref([])
|
||
const createDialogVisible = ref(false)
|
||
const detailsDialogVisible = ref(false)
|
||
const currentOrder = ref(null)
|
||
const createFormRef = ref(null)
|
||
|
||
const filters = reactive({
|
||
userId: '',
|
||
status: '',
|
||
currency: '',
|
||
})
|
||
|
||
const statistics = reactive({
|
||
totalRmb: '0.00',
|
||
totalPeso: '0.00',
|
||
})
|
||
|
||
const pagination = reactive({
|
||
page: 1,
|
||
limit: 20,
|
||
total: 0,
|
||
})
|
||
|
||
const createForm = reactive({
|
||
userId: '',
|
||
amountPeso: null,
|
||
amountRmb: null,
|
||
serviceContent: '',
|
||
paymentTime: '',
|
||
appointmentId: '',
|
||
notes: '',
|
||
})
|
||
|
||
const createRules = {
|
||
userId: [{ required: true, message: '请输入用户ID', trigger: 'blur' }],
|
||
serviceContent: [{ required: true, message: '请输入服务内容', trigger: 'blur' }],
|
||
paymentTime: [{ required: true, message: '请选择支付时间', trigger: 'change' }],
|
||
}
|
||
|
||
// Methods
|
||
async function fetchOrders() {
|
||
loading.value = true
|
||
try {
|
||
const params = {
|
||
page: pagination.page,
|
||
limit: pagination.limit,
|
||
}
|
||
if (filters.userId) params.userId = filters.userId
|
||
if (filters.status) params.status = filters.status
|
||
if (filters.currency) params.currency = filters.currency
|
||
if (dateRange.value?.length === 2) {
|
||
params.startDate = dateRange.value[0]
|
||
params.endDate = dateRange.value[1]
|
||
}
|
||
|
||
const response = await api.get('/api/v1/admin/payment-orders', { params })
|
||
if (response.data.code === 0) {
|
||
orders.value = response.data.data.records
|
||
pagination.total = response.data.data.pagination.total
|
||
|
||
// Update statistics from API response
|
||
if (response.data.data.statistics) {
|
||
statistics.totalRmb = response.data.data.statistics.totalRmb || '0.00'
|
||
statistics.totalPeso = response.data.data.statistics.totalPeso || '0.00'
|
||
} else {
|
||
statistics.totalRmb = '0.00'
|
||
statistics.totalPeso = '0.00'
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to fetch orders:', error)
|
||
ElMessage.error('获取订单列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function handleSearch() {
|
||
pagination.page = 1
|
||
fetchOrders()
|
||
}
|
||
|
||
function handleReset() {
|
||
filters.userId = ''
|
||
filters.status = ''
|
||
filters.currency = ''
|
||
dateRange.value = []
|
||
pagination.page = 1
|
||
fetchOrders()
|
||
}
|
||
|
||
function handlePageChange(page) {
|
||
pagination.page = page
|
||
fetchOrders()
|
||
}
|
||
|
||
function handleSizeChange(size) {
|
||
pagination.limit = size
|
||
pagination.page = 1
|
||
fetchOrders()
|
||
}
|
||
|
||
function showCreateDialog() {
|
||
Object.assign(createForm, {
|
||
userId: '',
|
||
amountPeso: null,
|
||
amountRmb: null,
|
||
serviceContent: '',
|
||
paymentTime: '',
|
||
appointmentId: '',
|
||
notes: '',
|
||
})
|
||
createDialogVisible.value = true
|
||
}
|
||
|
||
async function handleCreate() {
|
||
if (!createFormRef.value) return
|
||
|
||
// Custom validation for amounts - check for null/undefined, not just falsy
|
||
const hasPeso = createForm.amountPeso !== null && createForm.amountPeso !== undefined && createForm.amountPeso > 0
|
||
const hasRmb = createForm.amountRmb !== null && createForm.amountRmb !== undefined && createForm.amountRmb > 0
|
||
|
||
if (!hasPeso && !hasRmb) {
|
||
ElMessage.warning('请至少填写一种货币金额(比索或人民币)')
|
||
return
|
||
}
|
||
|
||
await createFormRef.value.validate(async (valid) => {
|
||
if (!valid) return
|
||
|
||
creating.value = true
|
||
try {
|
||
const response = await api.post('/api/v1/admin/payment-orders', {
|
||
userId: createForm.userId,
|
||
amountPeso: hasPeso ? createForm.amountPeso : undefined,
|
||
amountRmb: hasRmb ? createForm.amountRmb : undefined,
|
||
serviceContent: createForm.serviceContent,
|
||
paymentTime: createForm.paymentTime,
|
||
appointmentId: createForm.appointmentId || undefined,
|
||
notes: createForm.notes || undefined,
|
||
})
|
||
|
||
if (response.data.code === 0) {
|
||
ElMessage.success('创建成功')
|
||
createDialogVisible.value = false
|
||
fetchOrders()
|
||
|
||
if (response.data.data.commission) {
|
||
ElMessage.info(`已生成佣金 ¥${response.data.data.commission.commissionAmount}`)
|
||
}
|
||
} else {
|
||
ElMessage.error(response.data.error?.message || '创建失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to create order:', error)
|
||
const errorMsg = error.response?.data?.error?.message || '创建失败'
|
||
ElMessage.error(errorMsg)
|
||
} finally {
|
||
creating.value = false
|
||
}
|
||
})
|
||
}
|
||
|
||
async function showDetails(row) {
|
||
try {
|
||
const response = await api.get(`/api/v1/admin/payment-orders/${row.id}`)
|
||
if (response.data.code === 0) {
|
||
currentOrder.value = response.data.data
|
||
detailsDialogVisible.value = true
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to get order details:', error)
|
||
ElMessage.error('获取订单详情失败')
|
||
}
|
||
}
|
||
|
||
async function handleCancel(row) {
|
||
try {
|
||
const { value: cancelReason } = await ElMessageBox.prompt(
|
||
'请输入取消原因,相关佣金也将被取消。',
|
||
'取消订单',
|
||
{
|
||
confirmButtonText: '确认取消',
|
||
cancelButtonText: '返回',
|
||
inputPlaceholder: '请输入取消原因',
|
||
inputType: 'textarea',
|
||
inputValidator: (value) => {
|
||
if (!value || !value.trim()) {
|
||
return '请输入取消原因'
|
||
}
|
||
return true
|
||
},
|
||
type: 'warning',
|
||
}
|
||
)
|
||
|
||
const response = await api.put(`/api/v1/admin/payment-orders/${row.id}/cancel`, {
|
||
cancelReason
|
||
})
|
||
if (response.data.code === 0) {
|
||
ElMessage.success('订单已取消')
|
||
fetchOrders()
|
||
} else {
|
||
ElMessage.error(response.data.error?.message || '取消失败')
|
||
}
|
||
} catch (error) {
|
||
if (error !== 'cancel') {
|
||
console.error('Failed to cancel order:', error)
|
||
ElMessage.error(error.response?.data?.error?.message || '取消失败')
|
||
}
|
||
}
|
||
}
|
||
|
||
function formatDate(date) {
|
||
if (!date) return '-'
|
||
return new Date(date).toLocaleString('zh-CN')
|
||
}
|
||
|
||
// Initialize
|
||
onMounted(() => {
|
||
fetchOrders()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.payment-orders-container {
|
||
.filter-card {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.statistics-card {
|
||
margin-bottom: 20px;
|
||
|
||
.statistics-wrapper {
|
||
display: flex;
|
||
gap: 40px;
|
||
}
|
||
|
||
.statistics-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.statistics-label {
|
||
color: #606266;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.statistics-value {
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
|
||
&.rmb {
|
||
color: #e6a23c;
|
||
}
|
||
|
||
&.peso {
|
||
color: #409eff;
|
||
}
|
||
}
|
||
}
|
||
|
||
.table-card {
|
||
.amount-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.amount {
|
||
font-weight: bold;
|
||
|
||
&.peso {
|
||
color: #409eff;
|
||
}
|
||
|
||
&.rmb {
|
||
color: #e6a23c;
|
||
}
|
||
}
|
||
|
||
.commission {
|
||
color: #67c23a;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.no-commission {
|
||
color: #909399;
|
||
}
|
||
|
||
.text-muted {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
.pagination-wrapper {
|
||
margin-top: 20px;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.currency-hint {
|
||
margin-left: 10px;
|
||
color: #909399;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.amount-detail {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
|
||
.peso {
|
||
color: #409eff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.rmb {
|
||
color: #e6a23c;
|
||
font-weight: bold;
|
||
}
|
||
}
|
||
|
||
.cancel-reason {
|
||
color: #f56c6c;
|
||
}
|
||
}
|
||
</style>
|