feat(portal): redesign tenant home view with enhanced layout and interactive features

This commit is contained in:
2025-12-26 19:24:40 +08:00
parent 35b46386c7
commit d7ea411f25
2 changed files with 291 additions and 73 deletions

View File

@@ -18,14 +18,13 @@
class="h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none bg-white cursor-pointer"> 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="all">全部</option>
<option value="completed">已完成</option> <option value="completed">已完成</option>
<option value="refunding">退款申请中</option> <optiefunde value="refunding">退款申请中</optiefunde <option value="refunded">退</option>
<option value="refunded">已退款</option>
</select> </select>
</div> </div>
<div class="ml-auto relative w-64"> <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> <i class="pi pi-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i>
<input type="text" placeholder="搜索订单号或买家..." <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"> 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>
</div> </div>
@@ -45,34 +44,46 @@
</thead> </thead>
<tbody class="divide-y divide-slate-100"> <tbody class="divide-y divide-slate-100">
<tr v-for="order in filteredOrders" :key="order.id" class="hover:bg-slate-50 transition-colors"> <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 font-mono text-slate-600 align-middle">
{{ order.id }}
</td>
<td class="px-6 py-4 align-middle"> <td class="px-6 py-4 align-middle">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<img :src="order.cover" <img :src="order.cover"
class="w-16 h-10 object-cover rounded bg-slate-100 flex-shrink-0"> 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">{{ <span class="font-bold text-slate-900 truncate max-w-[240px]" :title="order.title">{{
order.title }}</span> order.title }}</span>
</div> </div>
</td> </td>
<td class="px-6 py-4 align-middle"> <td class="px-6 py-4 align-middle">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<img :src="order.buyerAvatar" class="w-8 h-8 rounded-full flex-shrink-0"> <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> <span class="text-slate-700 truncate max-w-[100px]">{{
order.buyerName
}}</span>
</div> </div>
</td> </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-right font-bold text-slate-900 align-middle">
<td class="px-6 py-4 text-slate-500 whitespace-nowrap align-middle">{{ order.date }}</td> ¥ {{ 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"> <td class="px-6 py-4 align-middle">
<span class="inline-block px-2.5 py-1 rounded text-xs font-bold whitespace-nowrap" <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
:class="statusStyle(order.status).bg + ' ' + statusStyle(order.status).text"> ">
{{ statusStyle(order.status).label }} {{ statusStyle(order.status).label }}
</span> </span>
</td> </td>
<td class="px-6 py-4 text-right align-middle whitespace-nowrap"> <td class="px-6 py-4 text-right align-middle whitespace-nowrap">
<button @click="viewDetail(order)" <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> 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)" <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> 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> </td>
</tr> </tr>
</tbody> </tbody>
@@ -92,7 +103,7 @@
<span class="font-mono font-bold">{{ selectedOrder.id }}</span> <span class="font-mono font-bold">{{ selectedOrder.id }}</span>
</div> </div>
<div class="flex gap-4"> <div class="flex gap-4">
<img :src="selectedOrder.cover" class="w-20 h-14 object-cover rounded"> <img :src="selectedOrder.cover" class="w-20 h-14 object-cover rounded" />
<div> <div>
<h3 class="font-bold text-slate-900">{{ selectedOrder.title }}</h3> <h3 class="font-bold text-slate-900">{{ selectedOrder.title }}</h3>
<p class="text-sm text-slate-500 mt-1">类型: {{ selectedOrder.type }}</p> <p class="text-sm text-slate-500 mt-1">类型: {{ selectedOrder.type }}</p>
@@ -121,26 +132,33 @@
</div> </div>
<template #footer> <template #footer>
<button @click="detailDialog = false" <button @click="detailDialog = false"
class="px-4 py-2 border border-slate-200 rounded text-sm hover:bg-slate-50">关闭</button> class="px-4 py-2 border border-slate-200 rounded text-sm hover:bg-slate-50">
关闭
</button>
<button <button
class="px-4 py-2 border border-primary-200 text-primary-600 rounded text-sm hover:bg-primary-50 ml-2">联系买家</button> class="px-4 py-2 border border-primary-200 text-primary-600 rounded text-sm hover:bg-primary-50 ml-2">
联系买家
</button>
</template> </template>
</Dialog> </Dialog>
<!-- Refund Dialog --> <!-- Refund Dialog -->
<Dialog v-model:visible="refundDialog" modal header="处理退款申请" :style="{ width: '25rem' }"> <Dialog v-model:visible="refundDialog" modal header="处理退款申请" :style="{ width: '25rem' }">
<div class="text-sm text-slate-600 mb-6"> <div class="text-sm text-slate-600 mb-6">
您正在处理订单 <span class="font-mono font-bold">{{ selectedOrder?.id }}</span> 的退款申请 您正在处理订单
<br>同意后金额将原路退回给买家 <span class="font-mono font-bold">{{ selectedOrder?.id }}</span> 的退款申请
<br />同意后金额将原路退回给买家
</div> </div>
<div class="space-y-3"> <div class="space-y-3">
<label class="flex items-center gap-3 p-3 border rounded-lg cursor-pointer hover:bg-slate-50" <label class="flex items-center gap-3 p-3 border rounded-lg cursor-pointer hover:bg-slate-50" :class="refundAction === 'accept'
:class="refundAction === 'accept' ? 'border-green-500 bg-green-50' : 'border-slate-200'"> ? 'border-green-500 bg-green-50'
: 'border-slate-200'
">
<RadioButton v-model="refundAction" value="accept" /> <RadioButton v-model="refundAction" value="accept" />
<span>同意退款</span> <span>同意退款</span>
</label> </label>
<label class="flex items-center gap-3 p-3 border rounded-lg cursor-pointer hover:bg-slate-50" <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'
:class="refundAction === 'reject' ? 'border-red-500 bg-red-50' : 'border-slate-200'"> ">
<RadioButton v-model="refundAction" value="reject" /> <RadioButton v-model="refundAction" value="reject" />
<span>拒绝退款</span> <span>拒绝退款</span>
</label> </label>
@@ -150,10 +168,13 @@
rows="2" placeholder="请输入拒绝理由..."></textarea> rows="2" placeholder="请输入拒绝理由..."></textarea>
</div> </div>
<template #footer> <template #footer>
<button @click="refundDialog = false" <button @click="refundDialog = false" class="px-4 py-2 text-slate-500 hover:text-slate-700 text-sm">
class="px-4 py-2 text-slate-500 hover:text-slate-700 text-sm">取消</button> 取消
</button>
<button @click="confirmRefund" <button @click="confirmRefund"
class="px-4 py-2 bg-slate-900 text-white rounded text-sm hover:bg-slate-800">确认处理</button> class="px-4 py-2 bg-slate-900 text-white rounded text-sm hover:bg-slate-800">
确认处理
</button>
</template> </template>
</Dialog> </Dialog>
@@ -162,57 +183,63 @@
</template> </template>
<script setup> <script setup>
import Dialog from 'primevue/dialog'; import Dialog from "primevue/dialog";
import RadioButton from 'primevue/radiobutton'; import RadioButton from "primevue/radiobutton";
import Toast from 'primevue/toast'; import Toast from "primevue/toast";
import { useToast } from 'primevue/usetoast'; import { useToast } from "primevue/usetoast";
import { computed, ref } from 'vue'; import { computed, ref } from "vue";
const toast = useToast(); const toast = useToast();
const filterStatus = ref('all'); const filterStatus = ref("all");
const detailDialog = ref(false); const detailDialog = ref(false);
const refundDialog = ref(false); const refundDialog = ref(false);
const selectedOrder = ref(null); const selectedOrder = ref(null);
const refundAction = ref('accept'); const refundAction = ref("accept");
const orders = ref([ const orders = ref([
{ {
id: '82934712', id: "82934712",
title: '《霸王别姬》全本实录珍藏版', title: "《霸王别姬》全本实录珍藏版",
type: '视频', type: "视频",
cover: 'https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60', cover:
buyerName: '戏迷小张', "https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60",
buyerAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Zhang', buyerName: "戏迷小张",
buyerId: '9527', buyerAvatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Zhang",
amount: '9.90', buyerId: "9527",
date: '2025-12-24 14:30', amount: "9.90",
status: 'completed' date: "2025-12-24 14:30",
status: "completed",
}, },
{ {
id: '82934715', id: "82934715",
title: '京剧打击乐基础教程', title: "京剧打击乐基础教程",
type: '视频', type: "视频",
cover: 'https://images.unsplash.com/photo-1533174072545-e8d4aa97edf9?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60', cover:
buyerName: '票友老李', "https://images.unsplash.com/photo-1533174072545-e8d4aa97edf9?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60",
buyerAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Li', buyerName: "票友老李",
buyerId: '8848', buyerAvatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Li",
amount: '19.90', buyerId: "8848",
date: '2025-12-25 09:15', amount: "19.90",
status: 'refunding' date: "2025-12-25 09:15",
} status: "refunding",
},
]); ]);
const filteredOrders = computed(() => { const filteredOrders = computed(() => {
if (filterStatus.value === 'all') return orders.value; if (filterStatus.value === "all") return orders.value;
return orders.value.filter(o => o.status === filterStatus.value); return orders.value.filter((o) => o.status === filterStatus.value);
}); });
const statusStyle = (status) => { const statusStyle = (status) => {
switch (status) { switch (status) {
case 'completed': return { bg: 'bg-green-50', text: 'text-green-600', label: '已完成' }; case "completed":
case 'refunding': return { bg: 'bg-orange-50', text: 'text-orange-600', label: '退款申请中' }; return { bg: "bg-green-50", text: "text-green-600", label: "已完成" };
case 'refunded': return { bg: 'bg-slate-100', text: 'text-slate-500', label: '已退款' }; case "refunding":
default: return { bg: 'bg-slate-100', text: 'text-slate-500', label: '未知' }; 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: "未知" };
} }
}; };
@@ -223,7 +250,7 @@ const viewDetail = (order) => {
const handleRefund = (order) => { const handleRefund = (order) => {
selectedOrder.value = order; selectedOrder.value = order;
refundAction.value = 'accept'; refundAction.value = "accept";
refundDialog.value = true; refundDialog.value = true;
}; };
@@ -231,10 +258,10 @@ const confirmRefund = () => {
// Mock API // Mock API
refundDialog.value = false; refundDialog.value = false;
toast.add({ toast.add({
severity: 'success', severity: "success",
summary: '处理完成', summary: "处理完成",
detail: refundAction.value === 'accept' ? '已同意退款' : '已拒绝退款申请', detail: refundAction.value === "accept" ? "已同意退款" : "已拒绝退款申请",
life: 3000 life: 3000,
}); });
// In real app, refresh list // In real app, refresh list
}; };

View File

@@ -1,10 +1,201 @@
<template> <template>
<div> <div>
<!-- Placeholder for Tenant Home View --> <!-- Cover & Header Info Merged -->
<div class="bg-white p-8 rounded-xl shadow-sm border border-slate-100 mx-auto max-w-screen-xl my-8 text-center"> <div class="relative h-[400px] bg-slate-900 overflow-hidden group">
<h1 class="text-2xl font-bold mb-4">Tenant Home Page</h1> <!-- Background Image -->
<p class="text-slate-500">Tenant ID: {{ $route.params.id }}</p> <img :src="tenant.cover"
<p class="text-slate-400 mt-2">(Implementation pending based on PAGE_TENANT_HOME.md)</p> class="w-full h-full object-cover opacity-90 transition-transform duration-700 group-hover:scale-105">
</div> <!-- Gradient Overlay -->
</div> <div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent"></div>
<!-- Content Overlay -->
<div class="absolute bottom-0 w-full z-10">
<div class="mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8 pb-8 flex flex-col md:flex-row items-end gap-8">
<!-- Avatar -->
<div
class="w-32 h-32 rounded-full border-4 border-white/20 shadow-2xl overflow-hidden flex-shrink-0 backdrop-blur-sm">
<img :src="tenant.avatar" class="w-full h-full object-cover">
</div>
<!-- Info -->
<div class="flex-1 min-w-0 pb-2 text-white">
<div class="flex items-center gap-3 mb-2">
<h1 class="text-4xl font-bold text-white truncate max-w-[600px] drop-shadow-md"
:title="tenant.name">{{ tenant.name }}</h1>
<!-- Cert Badge -->
<i v-if="tenant.certType === 'personal'"
class="pi pi-check-circle text-yellow-400 text-2xl drop-shadow-sm" title="个人认证"></i>
<i v-else-if="tenant.certType === 'enterprise'"
class="pi pi-shield text-blue-400 text-2xl drop-shadow-sm" title="企业认证"></i>
</div>
<p class="text-lg text-slate-200 line-clamp-1 font-medium drop-shadow-sm">{{ tenant.bio }}</p>
</div>
<!-- Actions & Stats -->
<div class="flex flex-col items-end gap-5 pb-2">
<div class="flex gap-3">
<button @click="toggleFollow"
class="h-11 w-32 rounded-full font-bold text-base transition-all flex items-center justify-center gap-2 backdrop-blur-md"
:class="isFollowing ? 'bg-white/10 text-white border border-white/20 hover:bg-white/20' : 'bg-primary-600 text-white hover:bg-primary-700 border border-transparent shadow-lg shadow-primary-900/30'">
<i class="pi" :class="isFollowing ? 'pi-check' : 'pi-plus'"></i>
{{ isFollowing ? '已关注' : '关注' }}
</button>
<button
class="h-11 px-6 border border-white/20 text-white rounded-full font-bold hover:bg-white/10 backdrop-blur-md transition-colors">私信</button>
<button
class="h-11 w-11 border border-white/20 text-white rounded-full flex items-center justify-center hover:bg-white/10 backdrop-blur-md transition-colors"><i
class="pi pi-ellipsis-h"></i></button>
</div>
<div class="flex gap-6 text-sm text-slate-300 font-medium">
<div><span class="font-bold text-white text-xl">1.2</span> 关注</div>
<div><span class="font-bold text-white text-xl">458</span> 内容</div>
<div><span class="font-bold text-white text-xl">8.9k</span> 获赞</div>
</div>
</div>
</div>
</div>
</div>
<!-- Sticky Nav -->
<div class="sticky top-16 z-20 bg-white border-b border-slate-200 shadow-sm">
<div class="mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8 flex items-center justify-between h-14">
<div class="flex gap-8 h-full">
<button v-for="tab in tabs" :key="tab.value" @click="currentTab = tab.value"
class="h-full border-b-2 font-bold text-sm px-1 transition-colors relative top-[1px]"
:class="currentTab === tab.value ? 'border-primary-600 text-primary-600' : 'border-transparent text-slate-500 hover:text-slate-700'">
{{ tab.label }}
</button>
</div>
<!-- In-Tenant Search -->
<div class="relative group">
<i
class="pi pi-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 group-hover:text-primary-500 transition-colors"></i>
<input type="text" placeholder="搜素频道内容"
class="h-9 pl-9 pr-4 rounded-full bg-slate-100 border-none text-sm focus:bg-white focus:ring-2 focus:ring-primary-100 transition-all w-48 focus:w-64">
</div>
</div>
</div>
<!-- Content Area -->
<div class="mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8 py-8 min-h-[600px]">
<!-- 1. Home Tab -->
<div v-if="currentTab === 'home'" class="space-y-10">
<!-- Featured (Pinned) -->
<div class="relative h-[400px] rounded-2xl overflow-hidden group cursor-pointer">
<img
src="https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?ixlib=rb-1.2.1&auto=format&fit=crop&w=1200&q=80"
class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105">
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent"></div>
<div class="absolute top-4 left-4 px-2 py-1 bg-red-600 text-white text-xs font-bold rounded">置顶</div>
<div class="absolute bottom-0 left-0 p-8 text-white w-full">
<div class="flex items-center justify-between">
<div>
<div class="text-sm font-bold opacity-80 mb-2">[京剧] 经典唱段合集</div>
<h2 class="text-3xl font-bold mb-2">程派荒山泪夜织选段沉浸式视听体验</h2>
</div>
<div class="text-2xl font-bold text-amber-400">¥ 19.90</div>
</div>
</div>
</div>
<!-- Latest -->
<div>
<h3 class="text-xl font-bold text-slate-900 mb-6 pl-4 border-l-4 border-primary-600">最新动态</h3>
<div class="grid grid-cols-1 gap-6">
<div v-for="i in 5" :key="i"
class="bg-white rounded-xl border border-slate-100 p-5 flex gap-6 hover:shadow-md transition-shadow group cursor-pointer"
@click="$router.push(`/contents/${i}`)">
<div class="w-64 h-36 bg-slate-100 rounded-lg flex-shrink-0 overflow-hidden relative">
<img
:src="`https://images.unsplash.com/photo-1533174072545-e8d4aa97edf9?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60`"
class="w-full h-full object-cover">
<div class="absolute bottom-2 left-2 flex gap-1">
<span class="px-1.5 py-0.5 bg-black/60 text-white text-xs rounded flex items-center gap-1"><i
class="pi pi-play-circle"></i> 12:40</span>
</div>
<span v-if="i === 2"
class="absolute top-2 right-2 px-1.5 py-0.5 bg-green-500 text-white text-xs font-bold rounded">限免</span>
</div>
<div class="flex-1 flex flex-col">
<div>
<h3
class="text-lg font-bold text-slate-900 mb-2 group-hover:text-primary-600 transition-colors line-clamp-1">
梅兰芳大剧院现场实录2024 京剧名家演唱会</h3>
<p class="text-sm text-slate-500 line-clamp-3 leading-relaxed">
本场演出汇集了当今京剧界的老中青三代名家精彩呈现了四郎探母红鬃烈马等经典剧目的核心唱段高清多机位拍摄还原现场震撼效果...</p>
</div>
<div class="mt-auto pt-4 flex items-center justify-between">
<div class="text-xs text-slate-400">2小时前 · 1.2w 阅读</div>
<div class="text-lg font-bold text-red-600">¥ 9.90</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 4. About Tab -->
<div v-if="currentTab === 'about'" class="max-w-3xl mx-auto">
<div class="prose prose-slate prose-lg text-slate-700">
<h2 class="font-bold text-2xl mb-6 pb-4 border-b border-slate-100">关于我们</h2>
<p>梅派传人小林师从著名京剧表演艺术家深耕京剧艺术二十余年致力于通过新媒体形式传播国粹文化让更多年轻人爱上戏曲</p>
<p>本频道主要发布</p>
<ul>
<li>经典剧目高清实录</li>
<li>唱腔身段教学课程</li>
<li>戏曲文化深度解析</li>
</ul>
<div class="my-8">
<img
src="https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80"
class="rounded-xl w-full">
<p class="text-center text-sm text-slate-400 mt-2">2023年全国巡演剧照</p>
</div>
<h3>联系方式</h3>
<p>商务合作business@example.com (点击查看)</p>
</div>
</div>
</div>
<!-- Floating Share FAB -->
<div class="fixed bottom-8 right-8 z-50">
<button
class="w-14 h-14 bg-slate-900 text-white rounded-full shadow-xl flex items-center justify-center hover:scale-110 transition-transform"
title="分享频道">
<i class="pi pi-share-alt text-xl"></i>
</button>
</div>
</div>
</template> </template>
<script setup>
import { useToast } from 'primevue/usetoast';
import { reactive, ref } from 'vue';
const toast = useToast();
const currentTab = ref('home');
const isFollowing = ref(false);
const tenant = reactive({
name: '梅派传人小林',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Master1',
cover: 'https://images.unsplash.com/photo-1611454453122-7b02c6b58188?ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80',
bio: '专注京剧程派艺术传承与推广,分享戏曲之美。',
certType: 'personal'
});
const tabs = [
{ label: '主页', value: 'home' },
{ label: '关于', value: 'about' }
];
const toggleFollow = () => {
isFollowing.value = !isFollowing.value;
if (isFollowing.value) {
toast.add({ severity: 'success', summary: '关注成功', detail: '已开启更新提醒', life: 3000 });
}
};
</script>