386 lines
13 KiB
Vue
386 lines
13 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-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>
|
||
|
||
<!-- 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 prop="amount" label="金额" width="100">
|
||
<template #default="{ row }">
|
||
<span class="amount">¥{{ row.amount }}</span>
|
||
</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="支付金额" prop="amount">
|
||
<el-input-number v-model="createForm.amount" :min="0.01" :precision="2" />
|
||
</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="支付金额">¥{{ currentOrder.amount }}</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>
|
||
|
||
<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: '',
|
||
})
|
||
|
||
const pagination = reactive({
|
||
page: 1,
|
||
limit: 20,
|
||
total: 0,
|
||
})
|
||
|
||
const createForm = reactive({
|
||
userId: '',
|
||
amount: 0,
|
||
serviceContent: '',
|
||
paymentTime: '',
|
||
appointmentId: '',
|
||
notes: '',
|
||
})
|
||
|
||
const createRules = {
|
||
userId: [{ required: true, message: '请输入用户ID', trigger: 'blur' }],
|
||
amount: [{ required: true, message: '请输入支付金额', 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 (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
|
||
}
|
||
} 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 = ''
|
||
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: '',
|
||
amount: 0,
|
||
serviceContent: '',
|
||
paymentTime: '',
|
||
appointmentId: '',
|
||
notes: '',
|
||
})
|
||
createDialogVisible.value = true
|
||
}
|
||
|
||
async function handleCreate() {
|
||
if (!createFormRef.value) 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,
|
||
amount: createForm.amount,
|
||
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 {
|
||
await ElMessageBox.confirm('确定要取消此订单吗?相关佣金也将被取消。', '确认取消', {
|
||
type: 'warning',
|
||
})
|
||
|
||
const response = await api.put(`/api/v1/admin/payment-orders/${row.id}/cancel`)
|
||
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('取消失败')
|
||
}
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
.table-card {
|
||
.amount {
|
||
color: #e6a23c;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
</style>
|