Files
quyun-v2/frontend/portal/src/views/creator/OrdersView.vue

378 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import Dialog from "primevue/dialog";
import RadioButton from "primevue/radiobutton";
import Toast from "primevue/toast";
import { useToast } from "primevue/usetoast";
import { computed, ref, onMounted, watch } from "vue";
import { creatorApi } from "../../api/creator";
const toast = useToast();
const filterStatus = ref("all");
const searchKeyword = ref("");
const detailDialog = ref(false);
const refundDialog = ref(false);
const selectedOrder = ref(null);
const refundAction = ref("accept");
const refundReason = ref("");
const orders = ref([]);
const loading = ref(false);
const fetchOrders = async () => {
loading.value = true;
try {
const params = {
status: filterStatus.value === "all" ? "" : filterStatus.value,
keyword: searchKeyword.value,
};
const res = await creatorApi.listOrders(params);
orders.value = (res || []).map((o) => ({
id: o.id,
title: o.title || "未知内容",
type: "数字内容",
cover: o.cover,
buyerName: o.buyer_name,
buyerAvatar: o.buyer_avatar,
amount: o.amount,
date: o.create_time,
status: o.status,
}));
} catch (e) {
console.error(e);
} finally {
loading.value = false;
}
};
onMounted(fetchOrders);
watch(filterStatus, fetchOrders);
const filteredOrders = computed(() => orders.value);
const statusStyle = (status) => {
switch (status) {
case "paid":
case "completed":
return { bg: "bg-green-50", text: "text-green-600", label: "已完成" };
case "refunding":
return {
bg: "bg-orange-50",
text: "text-orange-600",
label: "退款申请中",
};
case "refunded":
return { bg: "bg-slate-100", text: "text-slate-500", label: "已退款" };
default:
return { bg: "bg-slate-100", text: "text-slate-500", label: "未知" };
}
};
const viewDetail = (order) => {
selectedOrder.value = order;
detailDialog.value = true;
};
const handleRefund = (order) => {
selectedOrder.value = order;
refundAction.value = "accept";
refundReason.value = "";
refundDialog.value = true;
};
const confirmRefund = async () => {
try {
await creatorApi.refundOrder(selectedOrder.value.id, {
action: refundAction.value,
reason: refundReason.value,
});
refundDialog.value = false;
toast.add({ severity: "success", summary: "处理成功", life: 3000 });
fetchOrders();
} catch (e) {
toast.add({
severity: "error",
summary: "处理失败",
detail: e.message,
life: 3000,
});
}
};
</script>
<template>
<div>
<div class="flex items-center justify-between mb-8">
<h1 class="text-2xl font-bold text-slate-900">订单管理</h1>
<div class="flex gap-4">
<button
class="px-4 py-2 border border-slate-200 rounded-lg text-sm font-bold text-slate-600 hover:bg-slate-50 cursor-pointer"
>
<i class="pi pi-download mr-1"></i> 导出报表
</button>
</div>
</div>
<!-- Filters -->
<div
class="bg-white rounded-xl shadow-sm border border-slate-100 p-4 mb-6 flex flex-wrap gap-4 items-center"
>
<div class="flex items-center gap-2">
<span class="text-sm font-bold text-slate-500">状态:</span>
<select
v-model="filterStatus"
class="h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none bg-white cursor-pointer"
>
<option value="all">全部</option>
<option value="completed">已完成</option>
<option value="refunding">退款申请中</option>
<option value="refunded">已退款</option>
</select>
</div>
<div class="ml-auto relative w-64">
<i
class="pi pi-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"
></i>
<input
type="text"
v-model="searchKeyword"
@keyup.enter="fetchOrders"
placeholder="搜索订单号或买家..."
class="w-full h-9 pl-9 pr-4 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none transition-all"
/>
</div>
</div>
<!-- Order Table -->
<div
class="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden"
>
<table class="w-full text-left text-sm">
<thead
class="bg-slate-50 text-slate-500 font-bold border-b border-slate-200"
>
<tr>
<th class="px-6 py-4 whitespace-nowrap">订单号</th>
<th class="px-6 py-4 w-[30%]">内容信息</th>
<th class="px-6 py-4 whitespace-nowrap">买家</th>
<th class="px-6 py-4 text-right whitespace-nowrap">实付金额</th>
<th class="px-6 py-4 whitespace-nowrap">下单时间</th>
<th class="px-6 py-4 whitespace-nowrap">状态</th>
<th class="px-6 py-4 text-right whitespace-nowrap">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
<tr
v-for="order in filteredOrders"
:key="order.id"
class="hover:bg-slate-50 transition-colors"
>
<td class="px-6 py-4 font-mono text-slate-600 align-middle">
{{ order.id }}
</td>
<td class="px-6 py-4 align-middle">
<div class="flex items-center gap-3">
<img
:src="order.cover"
class="w-16 h-10 object-cover rounded bg-slate-100 flex-shrink-0"
/>
<span
class="font-bold text-slate-900 truncate max-w-[240px]"
:title="order.title"
>{{ order.title }}</span
>
</div>
</td>
<td class="px-6 py-4 align-middle">
<div class="flex items-center gap-2">
<img
:src="order.buyerAvatar"
class="w-8 h-8 rounded-full flex-shrink-0"
/>
<span class="text-slate-700 truncate max-w-[100px]">{{
order.buyerName
}}</span>
</div>
</td>
<td
class="px-6 py-4 text-right font-bold text-slate-900 align-middle"
>
¥ {{ order.amount }}
</td>
<td class="px-6 py-4 text-slate-500 whitespace-nowrap align-middle">
{{ order.date }}
</td>
<td class="px-6 py-4 align-middle">
<span
class="inline-block px-2.5 py-1 rounded text-xs font-bold whitespace-nowrap"
:class="
statusStyle(order.status).bg +
' ' +
statusStyle(order.status).text
"
>
{{ statusStyle(order.status).label }}
</span>
</td>
<td class="px-6 py-4 text-right align-middle whitespace-nowrap">
<button
@click="viewDetail(order)"
class="text-primary-600 hover:text-primary-700 font-medium mr-4 cursor-pointer hover:bg-primary-50 px-2 py-1 rounded transition-colors"
>
详情
</button>
<button
v-if="order.status === 'refunding'"
@click="handleRefund(order)"
class="text-red-600 hover:text-red-700 font-medium cursor-pointer hover:bg-red-50 px-2 py-1 rounded transition-colors"
>
处理退款
</button>
</td>
</tr>
</tbody>
</table>
<!-- Empty State -->
<div
v-if="filteredOrders.length === 0"
class="text-center py-12 text-slate-400"
>
暂无相关订单
</div>
</div>
<!-- Detail Dialog -->
<Dialog
v-model:visible="detailDialog"
modal
header="订单详情"
:style="{ width: '30rem' }"
>
<div v-if="selectedOrder" class="space-y-6">
<div
class="flex justify-between items-center pb-4 border-b border-slate-100"
>
<span class="text-sm text-slate-500">订单号</span>
<span class="font-mono font-bold">{{ selectedOrder.id }}</span>
</div>
<div class="flex gap-4">
<img
:src="selectedOrder.cover"
class="w-20 h-14 object-cover rounded"
/>
<div>
<h3 class="font-bold text-slate-900">{{ selectedOrder.title }}</h3>
<p class="text-sm text-slate-500 mt-1">
类型: {{ selectedOrder.type }}
</p>
</div>
</div>
<div class="bg-slate-50 p-4 rounded-lg space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-slate-500">买家</span>
<span class="font-medium"
>{{ selectedOrder.buyerName }} (ID:
{{ selectedOrder.buyerId }})</span
>
</div>
<div class="flex justify-between">
<span class="text-slate-500">支付方式</span>
<span>微信支付</span>
</div>
<div class="flex justify-between pt-2 border-t border-slate-200">
<span class="font-bold text-slate-900">实付</span>
<span class="font-bold text-red-600 text-lg"
>¥ {{ selectedOrder.amount }}</span
>
</div>
</div>
<div
v-if="selectedOrder.status === 'refunding'"
class="bg-red-50 p-4 rounded-lg border border-red-100"
>
<h4 class="text-red-700 font-bold text-sm mb-2">退款申请信息</h4>
<p class="text-sm text-red-600">申请原因内容无法播放/质量问题</p>
<p class="text-sm text-red-600 mt-1">
申请说明视频一直加载不出来
</p>
</div>
</div>
<template #footer>
<button
@click="detailDialog = false"
class="px-4 py-2 border border-slate-200 rounded text-sm hover:bg-slate-50"
>
关闭
</button>
<button
class="px-4 py-2 border border-primary-200 text-primary-600 rounded text-sm hover:bg-primary-50 ml-2"
>
联系买家
</button>
</template>
</Dialog>
<!-- Refund Dialog -->
<Dialog
v-model:visible="refundDialog"
modal
header="处理退款申请"
:style="{ width: '25rem' }"
>
<div class="text-sm text-slate-600 mb-6">
您正在处理订单
<span class="font-mono font-bold">{{ selectedOrder?.id }}</span>
的退款申请 <br />同意后金额将原路退回给买家
</div>
<div class="space-y-3">
<label
class="flex items-center gap-3 p-3 border rounded-lg cursor-pointer hover:bg-slate-50"
:class="
refundAction === 'accept'
? 'border-green-500 bg-green-50'
: 'border-slate-200'
"
>
<RadioButton v-model="refundAction" value="accept" />
<span>同意退款</span>
</label>
<label
class="flex items-center gap-3 p-3 border rounded-lg cursor-pointer hover:bg-slate-50"
:class="
refundAction === 'reject'
? 'border-red-500 bg-red-50'
: 'border-slate-200'
"
>
<RadioButton v-model="refundAction" value="reject" />
<span>拒绝退款</span>
</label>
</div>
<div v-if="refundAction === 'reject'" class="mt-4">
<textarea
v-model="refundReason"
class="w-full p-2 border border-slate-200 rounded text-sm focus:border-red-500 outline-none"
rows="2"
placeholder="请输入拒绝理由..."
></textarea>
</div>
<template #footer>
<button
@click="refundDialog = false"
class="px-4 py-2 text-slate-500 hover:text-slate-700 text-sm"
>
取消
</button>
<button
@click="confirmRefund"
class="px-4 py-2 bg-slate-900 text-white rounded text-sm hover:bg-slate-800"
>
确认处理
</button>
</template>
</Dialog>
<Toast />
</div>
</template>