feat(portal): enhance creator dashboard and order management with new layout and features

This commit is contained in:
2025-12-26 19:10:40 +08:00
parent b40c529cd8
commit 35b46386c7
6 changed files with 561 additions and 158 deletions

View File

@@ -1,6 +1,6 @@
<template> <template>
<nav class="fixed top-0 w-full z-50 bg-white border-b border-slate-200 h-16"> <nav class="fixed top-0 w-full z-50 bg-white border-b border-slate-200 h-16">
<div class="mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8 h-full flex items-center justify-between"> <div class="mx-auto max-w-screen-xl h-full flex items-center justify-between">
<!-- Left: Logo --> <!-- Left: Logo -->
<router-link to="/" class="flex items-center gap-2"> <router-link to="/" class="flex items-center gap-2">
<div class="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center text-white font-bold text-xl">Q</div> <div class="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center text-white font-bold text-xl">Q</div>

View File

@@ -0,0 +1,81 @@
<template>
<div class="min-h-screen flex flex-col bg-slate-50">
<TopNavbar />
<main class="flex-grow pt-16">
<div class="mx-auto max-w-screen-xl py-8 flex gap-8">
<!-- Creator Sidebar (Dark Theme) -->
<aside class="w-[260px] flex-shrink-0 hidden lg:block">
<div
class="bg-slate-900 rounded-2xl shadow-sm overflow-hidden sticky top-24 text-slate-300 min-h-[600px] flex flex-col">
<!-- Header -->
<div class="p-6 border-b border-slate-800">
<div class="flex items-center gap-3">
<div
class="w-10 h-10 bg-gradient-to-br from-primary-500 to-primary-700 rounded-lg flex items-center justify-center text-white font-bold text-lg shadow-lg">
<i class="pi pi-palette"></i>
</div>
<div>
<div class="font-bold text-white leading-tight">创作者中心</div>
<div class="text-xs text-slate-500 mt-1">Creator Studio</div>
</div>
</div>
</div>
<!-- Menus -->
<nav class="p-4 space-y-1 flex-1">
<router-link to="/creator"
exact-active-class="bg-primary-600 text-white shadow-md shadow-primary-900/20"
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-slate-800 hover:text-white transition-all group">
<i class="pi pi-th-large text-lg group-hover:scale-110 transition-transform"></i>
<span class="font-medium">管理概览</span>
</router-link>
<div class="px-4 py-2 text-xs font-bold text-slate-500 uppercase tracking-wider mt-4">内容与交易</div>
<router-link to="/creator/contents"
active-class="bg-primary-600 text-white shadow-md shadow-primary-900/20"
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-slate-800 hover:text-white transition-all group">
<i class="pi pi-file-edit text-lg group-hover:scale-110 transition-transform"></i>
<span class="font-medium">内容管理</span>
</router-link>
<router-link to="/creator/orders"
active-class="bg-primary-600 text-white shadow-md shadow-primary-900/20"
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-slate-800 hover:text-white transition-all group">
<i class="pi pi-shopping-cart text-lg group-hover:scale-110 transition-transform"></i>
<span class="font-medium">订单管理</span>
</router-link>
<div class="px-4 py-2 text-xs font-bold text-slate-500 uppercase tracking-wider mt-4">配置</div>
<router-link to="/creator/settings"
active-class="bg-primary-600 text-white shadow-md shadow-primary-900/20"
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-slate-800 hover:text-white transition-all group">
<i class="pi pi-cog text-lg group-hover:scale-110 transition-transform"></i>
<span class="font-medium">频道设置</span>
</router-link>
</nav>
<!-- Footer Link -->
<div class="p-4 border-t border-slate-800">
<router-link to="/t/1"
class="flex items-center gap-2 px-4 py-2 text-sm text-slate-400 hover:text-white transition-colors">
<i class="pi pi-external-link"></i> 预览我的主页
</router-link>
</div>
</div>
</aside>
<!-- Main Content -->
<div class="flex-grow min-w-0">
<router-view />
</div>
</div>
</main>
<AppFooter />
</div>
</template>
<script setup>
import AppFooter from '../components/AppFooter.vue';
import TopNavbar from '../components/TopNavbar.vue';
</script>

View File

@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
import LayoutMain from '../layout/LayoutMain.vue' import LayoutMain from '../layout/LayoutMain.vue'
import LayoutAuth from '../layout/LayoutAuth.vue' import LayoutAuth from '../layout/LayoutAuth.vue'
import LayoutUser from '../layout/LayoutUser.vue' import LayoutUser from '../layout/LayoutUser.vue'
import LayoutCreator from '../layout/LayoutCreator.vue'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@@ -39,6 +40,11 @@ const router = createRouter({
path: 'creator/apply', path: 'creator/apply',
name: 'creator-apply', name: 'creator-apply',
component: () => import('../views/creator/ApplyView.vue') component: () => import('../views/creator/ApplyView.vue')
},
{
path: 'creator/contents/new',
name: 'creator-content-new',
component: () => import('../views/creator/ContentsEditView.vue')
} }
] ]
}, },
@@ -75,33 +81,33 @@ const router = createRouter({
{ {
path: 'wallet', path: 'wallet',
name: 'user-wallet', name: 'user-wallet',
component: () => import('../views/user/WalletView.vue') // Placeholder component: () => import('../views/user/WalletView.vue')
}, },
{ {
path: 'library', path: 'library',
name: 'user-library', name: 'user-library',
component: () => import('../views/user/LibraryView.vue') // Placeholder component: () => import('../views/user/LibraryView.vue')
}, },
{ {
path: 'notifications', path: 'notifications',
name: 'user-notifications', name: 'user-notifications',
component: () => import('../views/user/NotificationsView.vue') // Placeholder component: () => import('../views/user/NotificationsView.vue')
}, },
{ {
path: 'profile', path: 'profile',
name: 'user-profile', name: 'user-profile',
component: () => import('../views/user/ProfileView.vue') // Placeholder component: () => import('../views/user/ProfileView.vue')
}, },
{ {
path: 'security', path: 'security',
name: 'user-security', name: 'user-security',
component: () => import('../views/user/SecurityView.vue') // Placeholder component: () => import('../views/user/SecurityView.vue')
} }
] ]
}, },
{ {
path: '/creator', path: '/creator',
component: LayoutUser, // Initially use LayoutUser, later maybe specialized LayoutCreator component: LayoutCreator,
children: [ children: [
{ {
path: '', path: '',
@@ -127,7 +133,7 @@ const router = createRouter({
}, },
{ {
path: '/checkout', path: '/checkout',
component: LayoutMain, // Or a simplified checkout layout component: LayoutMain,
children: [ children: [
{ {
path: '', path: '',
@@ -163,4 +169,4 @@ const router = createRouter({
} }
}) })
export default router export default router

View File

@@ -1,161 +1,178 @@
<template> <template>
<div class="p-8"> <div>
<div class="flex items-center justify-between mb-8"> <div class="flex items-center justify-between mb-8">
<h1 class="text-2xl font-bold text-slate-900">内容管理</h1> <h1 class="text-2xl font-bold text-slate-900">内容管理</h1>
<router-link to="/creator/contents/new" class="px-6 py-2.5 bg-primary-600 text-white rounded-lg font-bold hover:bg-primary-700 transition-colors shadow-sm shadow-primary-200 cursor-pointer active:scale-95 flex items-center gap-2"> <router-link to="/creator/contents/new"
<i class="pi pi-plus"></i> 发布新内容 class="px-6 py-2.5 bg-primary-600 text-white rounded-lg font-bold hover:bg-primary-700 transition-colors shadow-sm shadow-primary-200 cursor-pointer active:scale-95 flex items-center gap-2">
</router-link> <i class="pi pi-plus"></i> 发布新内容
</div> </router-link>
</div>
<!-- Filters --> <!-- 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="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"> <div class="flex items-center gap-2">
<span class="text-sm font-bold text-slate-500">状态:</span> <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"> <select v-model="filterStatus"
<option value="all">全部</option> class="h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none bg-white cursor-pointer">
<option value="published">已发布</option> <option value="all">全部</option>
<option value="audit">审核中</option> <option value="published">已发布</option>
<option value="rejected">已驳回</option> <option value="audit">审核中</option>
<option value="draft">草稿箱</option> <option value="rejected">已驳回</option>
</select> <option value="draft">草稿箱</option>
</div> </select>
<div class="flex items-center gap-2"> </div>
<span class="text-sm font-bold text-slate-500">曲种:</span> <div class="flex items-center gap-2">
<select v-model="filterGenre" class="h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none bg-white cursor-pointer"> <span class="text-sm font-bold text-slate-500">曲种:</span>
<option value="all">全部</option> <select v-model="filterGenre"
<option value="京剧">京剧</option> class="h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none bg-white cursor-pointer">
<option value="昆曲">昆曲</option> <option value="all">全部</option>
<option value="剧"></option> <option value="剧"></option>
</select> <option value="昆曲">昆曲</option>
</div> <option value="越剧">越剧</option>
<div class="ml-auto relative"> </select>
<i class="pi pi-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i> </div>
<input type="text" placeholder="搜索标题..." class="h-9 pl-9 pr-4 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none w-48 transition-all focus:w-64"> <div class="ml-auto relative">
</div> <i class="pi pi-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i>
</div> <input type="text" placeholder="搜索标题..."
class="h-9 pl-9 pr-4 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none w-48 transition-all focus:w-64">
</div>
</div>
<!-- Content List --> <!-- Content List -->
<div class="space-y-4"> <div class="space-y-4">
<div v-for="item in filteredList" :key="item.id" class="bg-white rounded-xl shadow-sm border border-slate-100 p-5 flex gap-6 hover:shadow-md transition-shadow group relative"> <div v-for="item in filteredList" :key="item.id"
class="bg-white rounded-xl shadow-sm border border-slate-100 p-5 flex gap-6 hover:shadow-md transition-shadow group relative">
<!-- Cover -->
<div class="w-40 h-[90px] bg-slate-100 rounded-lg flex-shrink-0 overflow-hidden relative">
<img :src="item.cover" class="w-full h-full object-cover">
<div class="absolute inset-0 bg-black/50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<router-link :to="`/creator/contents/new`" class="text-white text-xs font-bold border border-white px-3 py-1 rounded hover:bg-white hover:text-black transition-colors">编辑</router-link>
</div>
</div>
<!-- Info --> <!-- Cover -->
<div class="flex-1 min-w-0 flex flex-col justify-between"> <div class="w-40 h-[90px] bg-slate-100 rounded-lg flex-shrink-0 overflow-hidden relative">
<div> <img :src="item.cover" class="w-full h-full object-cover">
<div class="flex items-center justify-between mb-2"> <div
<div class="flex items-center gap-2"> class="absolute inset-0 bg-black/50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<span class="text-xs px-1.5 py-0.5 border rounded text-slate-500">[{{ item.genre }}]</span> <router-link :to="`/creator/contents/new`"
<h3 class="font-bold text-slate-900 text-lg truncate hover:text-primary-600 cursor-pointer transition-colors">{{ item.title }}</h3> class="text-white text-xs font-bold border border-white px-3 py-1 rounded hover:bg-white hover:text-black transition-colors">编辑</router-link>
</div> </div>
<!-- Status Badge --> </div>
<div class="flex items-center gap-2">
<span v-if="item.status === 'rejected'" class="text-red-500 text-xs flex items-center gap-1 cursor-help" title="点击查看原因">
<i class="pi pi-info-circle"></i> {{ item.rejectReason }}
</span>
<span class="px-2.5 py-1 rounded text-xs font-bold" :class="statusStyle(item.status).bg + ' ' + statusStyle(item.status).text">
{{ statusStyle(item.status).label }}
</span>
</div>
</div>
<div class="flex items-center gap-6 text-sm text-slate-500">
<span v-if="item.price > 0" class="text-red-600 font-bold">¥ {{ item.price }}</span>
<span v-else class="text-green-600 font-bold">免费</span>
<span><i class="pi pi-eye mr-1"></i> {{ item.views }}</span>
<span><i class="pi pi-thumbs-up mr-1"></i> {{ item.likes }}</span>
<span>{{ item.date }}</span>
</div>
</div>
<!-- Actions --> <!-- Info -->
<div class="flex items-center gap-4 pt-3 border-t border-slate-50 mt-2"> <div class="flex-1 min-w-0 flex flex-col justify-between">
<button class="text-sm text-slate-500 hover:text-primary-600 font-medium cursor-pointer"><i class="pi pi-file-edit mr-1"></i> 编辑</button> <div>
<button v-if="item.status === 'published'" class="text-sm text-slate-500 hover:text-orange-600 font-medium cursor-pointer"><i class="pi pi-arrow-down mr-1"></i> 下架</button> <div class="flex items-center justify-between mb-2">
<button v-if="item.status === 'offline'" class="text-sm text-slate-500 hover:text-green-600 font-medium cursor-pointer"><i class="pi pi-arrow-up mr-1"></i> 上架</button> <div class="flex items-center gap-2">
<button class="text-sm text-slate-500 hover:text-red-600 font-medium ml-auto cursor-pointer"><i class="pi pi-trash mr-1"></i> 删除</button> <span class="text-xs px-1.5 py-0.5 border rounded text-slate-500">[{{ item.genre }}]</span>
</div> <h3
</div> class="font-bold text-slate-900 text-lg truncate hover:text-primary-600 cursor-pointer transition-colors">
</div> {{ item.title }}</h3>
</div> </div>
</div> <!-- Status Badge -->
<div class="flex items-center gap-2">
<span v-if="item.status === 'rejected'"
class="text-red-500 text-xs flex items-center gap-1 cursor-help" title="点击查看原因">
<i class="pi pi-info-circle"></i> {{ item.rejectReason }}
</span>
<span class="px-2.5 py-1 rounded text-xs font-bold"
:class="statusStyle(item.status).bg + ' ' + statusStyle(item.status).text">
{{ statusStyle(item.status).label }}
</span>
</div>
</div>
<div class="flex items-center gap-6 text-sm text-slate-500">
<span v-if="item.price > 0" class="text-red-600 font-bold">¥ {{ item.price }}</span>
<span v-else class="text-green-600 font-bold">免费</span>
<span><i class="pi pi-eye mr-1"></i> {{ item.views }}</span>
<span><i class="pi pi-thumbs-up mr-1"></i> {{ item.likes }}</span>
<span>{{ item.date }}</span>
</div>
</div>
<!-- Actions -->
<div class="flex items-center gap-4 pt-3 border-t border-slate-50 mt-2">
<button class="text-sm text-slate-500 hover:text-primary-600 font-medium cursor-pointer"><i
class="pi pi-file-edit mr-1"></i> 编辑</button>
<button v-if="item.status === 'published'"
class="text-sm text-slate-500 hover:text-orange-600 font-medium cursor-pointer"><i
class="pi pi-arrow-down mr-1"></i> 下架</button>
<button v-if="item.status === 'offline'"
class="text-sm text-slate-500 hover:text-green-600 font-medium cursor-pointer"><i
class="pi pi-arrow-up mr-1"></i> 上架</button>
<button class="text-sm text-slate-500 hover:text-red-600 font-medium ml-auto cursor-pointer"><i
class="pi pi-trash mr-1"></i> 删除</button>
</div>
</div>
</div>
</div>
</div>
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue'; import { computed, ref } from 'vue';
const filterStatus = ref('all'); const filterStatus = ref('all');
const filterGenre = ref('all'); const filterGenre = ref('all');
const list = ref([ const list = ref([
{ {
id: 1, id: 1,
title: '《锁麟囊》春秋亭 (程砚秋)', title: '《锁麟囊》春秋亭 (程砚秋)',
genre: '京剧', genre: '京剧',
cover: 'https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=60', cover: 'https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=60',
status: 'published', status: 'published',
price: 9.9, price: 9.9,
views: '12.5k', views: '12.5k',
likes: 850, likes: 850,
date: '2025-12-24' date: '2025-12-24'
}, },
{ {
id: 2, id: 2,
title: '昆曲《牡丹亭》游园惊梦', title: '昆曲《牡丹亭》游园惊梦',
genre: '昆曲', genre: '昆曲',
cover: 'https://images.unsplash.com/photo-1557683316-973673baf926?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=60', cover: 'https://images.unsplash.com/photo-1557683316-973673baf926?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=60',
status: 'audit', status: 'audit',
price: 0, price: 0,
views: '-', views: '-',
likes: '-', likes: '-',
date: '2025-12-25' date: '2025-12-25'
}, },
{ {
id: 3, id: 3,
title: '越剧《红楼梦》葬花', title: '越剧《红楼梦》葬花',
genre: '越剧', genre: '越剧',
cover: 'https://images.unsplash.com/photo-1469571486292-0ba58a3f068b?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=60', cover: 'https://images.unsplash.com/photo-1469571486292-0ba58a3f068b?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=60',
status: 'rejected', status: 'rejected',
rejectReason: '封面图清晰度不足', rejectReason: '封面图清晰度不足',
price: 19.9, price: 19.9,
views: '-', views: '-',
likes: '-', likes: '-',
date: '2025-12-23' date: '2025-12-23'
}, },
{ {
id: 4, id: 4,
title: '未命名的草稿', title: '未命名的草稿',
genre: '京剧', genre: '京剧',
cover: '', cover: '',
status: 'draft', status: 'draft',
price: 0, price: 0,
views: '-', views: '-',
likes: '-', likes: '-',
date: '2025-12-26' date: '2025-12-26'
} }
]); ]);
const filteredList = computed(() => { const filteredList = computed(() => {
return list.value.filter(item => { return list.value.filter(item => {
const matchStatus = filterStatus.value === 'all' || item.status === filterStatus.value; const matchStatus = filterStatus.value === 'all' || item.status === filterStatus.value;
const matchGenre = filterGenre.value === 'all' || item.genre === filterGenre.value; const matchGenre = filterGenre.value === 'all' || item.genre === filterGenre.value;
return matchStatus && matchGenre; return matchStatus && matchGenre;
}); });
}); });
const statusStyle = (status) => { const statusStyle = (status) => {
switch(status) { switch (status) {
case 'published': return { bg: 'bg-green-50', text: 'text-green-600', label: '已发布' }; case 'published': return { bg: 'bg-green-50', text: 'text-green-600', label: '已发布' };
case 'audit': return { bg: 'bg-orange-50', text: 'text-orange-600', label: '审核中' }; case 'audit': return { bg: 'bg-orange-50', text: 'text-orange-600', label: '审核中' };
case 'rejected': return { bg: 'bg-red-50', text: 'text-red-600', label: '已驳回' }; case 'rejected': return { bg: 'bg-red-50', text: 'text-red-600', label: '已驳回' };
case 'draft': return { bg: 'bg-slate-100', text: 'text-slate-500', label: '草稿' }; case 'draft': return { bg: 'bg-slate-100', text: 'text-slate-500', label: '草稿' };
default: return { bg: 'bg-slate-100', text: 'text-slate-500', label: '未知' }; default: return { bg: 'bg-slate-100', text: 'text-slate-500', label: '未知' };
} }
}; };
</script> </script>

View File

@@ -1,7 +1,71 @@
<template> <template>
<div class="p-8"> <div>
<h1 class="text-2xl font-bold mb-4">Creator Dashboard</h1> <div class="flex items-center justify-between mb-8">
<p class="text-slate-500">Welcome to the Creator Center.</p> <h1 class="text-2xl font-bold text-slate-900">管理概览</h1>
<p class="text-slate-400 mt-2">(Implementation pending based on PAGE_TENANT_MANAGEMENT.md)</p> <div class="flex gap-4">
</div> <router-link to="/creator/contents/new"
class="px-6 py-2.5 bg-primary-600 text-white rounded-lg font-bold hover:bg-primary-700 transition-colors shadow-sm shadow-primary-200 cursor-pointer active:scale-95 flex items-center gap-2">
<i class="pi pi-plus"></i> 发布新内容
</router-link>
</div>
</div>
<!-- Key Metrics -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div v-for="metric in metrics" :key="metric.label"
class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
<div class="text-sm text-slate-500 mb-2">{{ metric.label }}</div>
<div class="flex items-baseline gap-2">
<span class="text-3xl font-bold text-slate-900">{{ metric.value }}</span>
<span class="text-xs font-bold" :class="metric.trend > 0 ? 'text-green-600' : 'text-red-600'">
<i class="pi" :class="metric.trend > 0 ? 'pi-arrow-up' : 'pi-arrow-down'"
style="font-size: 0.7rem"></i>
{{ Math.abs(metric.trend) }}%
</span>
</div>
<div class="text-xs text-slate-400 mt-2">{{ metric.subtext }}</div>
</div>
</div>
<!-- Pending Orders (Quick Access) -->
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
<h3 class="font-bold text-slate-900 mb-4">待处理事项</h3>
<div class="flex gap-4">
<div
class="flex-1 p-4 bg-orange-50 border border-orange-100 rounded-xl flex items-center justify-between cursor-pointer hover:bg-orange-100 transition-colors"
@click="$router.push('/creator/orders')">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-orange-200 text-orange-700 flex items-center justify-center"><i
class="pi pi-refresh"></i></div>
<div>
<div class="font-bold text-slate-900">退款申请</div>
<div class="text-xs text-orange-600">需要处理</div>
</div>
</div>
<div class="text-2xl font-bold text-orange-700">2</div>
</div>
<div
class="flex-1 p-4 bg-blue-50 border border-blue-100 rounded-xl flex items-center justify-between cursor-pointer hover:bg-blue-100 transition-colors">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-blue-200 text-blue-700 flex items-center justify-center"><i
class="pi pi-comments"></i></div>
<div>
<div class="font-bold text-slate-900">新私信</div>
<div class="text-xs text-blue-600">粉丝互动</div>
</div>
</div>
<div class="text-2xl font-bold text-blue-700">5</div>
</div>
</div>
</div>
</div>
</template> </template>
<script setup>
import { ref } from 'vue';
const metrics = ref([
{ label: '总关注用户', value: '12,548', trend: 1.2, subtext: '昨日 +125' },
{ label: '累计总收益', value: '¥ 8,920.50', trend: 5.4, subtext: '近30天 +2,300' },
]);
</script>

View File

@@ -1,6 +1,241 @@
<template> <template>
<div class="p-8"> <div>
<h1 class="text-2xl font-bold mb-4">Creator Orders</h1> <div class="flex items-center justify-between mb-8">
<p class="text-slate-400">(List of orders)</p> <h1 class="text-2xl font-bold text-slate-900">订单管理</h1>
</div> <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" 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 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> </template>
<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 } from 'vue';
const toast = useToast();
const filterStatus = ref('all');
const detailDialog = ref(false);
const refundDialog = ref(false);
const selectedOrder = ref(null);
const refundAction = ref('accept');
const orders = ref([
{
id: '82934712',
title: '《霸王别姬》全本实录珍藏版',
type: '视频',
cover: 'https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60',
buyerName: '戏迷小张',
buyerAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Zhang',
buyerId: '9527',
amount: '9.90',
date: '2025-12-24 14:30',
status: 'completed'
},
{
id: '82934715',
title: '京剧打击乐基础教程',
type: '视频',
cover: 'https://images.unsplash.com/photo-1533174072545-e8d4aa97edf9?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60',
buyerName: '票友老李',
buyerAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Li',
buyerId: '8848',
amount: '19.90',
date: '2025-12-25 09:15',
status: 'refunding'
}
]);
const filteredOrders = computed(() => {
if (filterStatus.value === 'all') return orders.value;
return orders.value.filter(o => o.status === filterStatus.value);
});
const statusStyle = (status) => {
switch (status) {
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';
refundDialog.value = true;
};
const confirmRefund = () => {
// Mock API
refundDialog.value = false;
toast.add({
severity: 'success',
summary: '处理完成',
detail: refundAction.value === 'accept' ? '已同意退款' : '已拒绝退款申请',
life: 3000
});
// In real app, refresh list
};
</script>