feat: tenant-scoped routing and portal navigation

This commit is contained in:
2026-01-08 21:30:46 +08:00
parent f3aa92078a
commit 3e095c57f3
52 changed files with 1111 additions and 670 deletions

View File

@@ -2,7 +2,7 @@
<div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Stat Cards -->
<router-link to="/me/wallet"
<router-link :to="tenantRoute('/me/wallet')"
class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 flex items-center gap-4 hover:shadow-md hover:border-primary-100 transition-all cursor-pointer">
<div class="w-12 h-12 rounded-full bg-blue-50 text-blue-600 flex items-center justify-center text-xl"><i
class="pi pi-wallet"></i></div>
@@ -42,13 +42,13 @@
<div class="mt-8 bg-white rounded-xl shadow-sm border border-slate-100 p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-slate-900">最近订单</h2>
<router-link to="/me/orders"
<router-link :to="tenantRoute('/me/orders')"
class="text-sm text-primary-600 hover:text-primary-700 font-medium px-2 py-1 rounded hover:bg-primary-50 transition-colors">查看全部
<i class="pi pi-angle-right"></i></router-link>
</div>
<div class="space-y-4">
<div v-for="order in recentOrders" :key="order.id" @click="$router.push(`/me/orders/${order.id}`)"
<div v-for="order in recentOrders" :key="order.id" @click="$router.push(tenantRoute(`/me/orders/${order.id}`))"
class="flex items-center gap-4 p-4 border border-slate-100 rounded-lg hover:border-primary-100 hover:shadow-sm transition-all cursor-pointer active:scale-[0.99] group">
<div class="w-16 h-16 bg-slate-100 rounded flex-shrink-0 flex items-center justify-center relative overflow-hidden">
<template v-if="order.type === 'recharge' || !order.items?.length">
@@ -89,11 +89,15 @@
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { userApi } from '../../api/user';
import { tenantPath } from '../../utils/tenant';
const wallet = ref({ balance: 0, points: 0 });
const couponCount = ref(0);
const recentOrders = ref([]);
const route = useRoute();
const tenantRoute = (path) => tenantPath(path, route);
const statusColor = (status) => {
const map = {

View File

@@ -9,7 +9,7 @@
<!-- Content Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div v-for="item in items" :key="item.id" class="group relative bg-white border border-slate-200 rounded-xl overflow-hidden hover:shadow-md transition-all hover:border-primary-200 cursor-pointer" @click="$router.push(`/contents/${item.id}`)">
<div v-for="item in items" :key="item.id" class="group relative bg-white border border-slate-200 rounded-xl overflow-hidden hover:shadow-md transition-all hover:border-primary-200 cursor-pointer" @click="$router.push(tenantRoute(`/contents/${item.id}`))">
<!-- Cover -->
<div class="aspect-video bg-slate-100 relative overflow-hidden">
@@ -43,7 +43,7 @@
<i class="pi pi-star text-2xl text-slate-300"></i>
</div>
<p class="text-slate-500 text-lg">暂无收藏内容</p>
<router-link to="/" class="mt-4 inline-block text-primary-600 font-medium hover:underline">去发现好内容</router-link>
<router-link :to="tenantRoute('/')" class="mt-4 inline-block text-primary-600 font-medium hover:underline">去发现好内容</router-link>
</div>
<Toast />
@@ -52,15 +52,17 @@
<script setup>
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useRoute } from 'vue-router';
import Toast from 'primevue/toast';
import { useToast } from 'primevue/usetoast';
import { userApi } from '../../api/user';
import { tenantPath } from '../../utils/tenant';
const router = useRouter();
const toast = useToast();
const items = ref([]);
const loading = ref(true);
const route = useRoute();
const tenantRoute = (path) => tenantPath(path, route);
const fetchFavorites = async () => {
try {

View File

@@ -1,7 +1,11 @@
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { userApi } from '../../api/user';
import { tenantPath } from '../../utils/tenant';
const route = useRoute();
const tenantRoute = (path) => tenantPath(path, route);
const libraryItems = ref([]);
const loading = ref(true);
@@ -53,7 +57,7 @@ const getStatusLabel = (item) => {
<div
v-for="item in libraryItems"
:key="item.id"
@click="item.status === 'published' ? $router.push(`/contents/${item.id}`) : null"
@click="item.status === 'published' ? $router.push(tenantRoute(`/contents/${item.id}`)) : null"
class="group relative bg-white border border-slate-200 rounded-xl overflow-hidden hover:shadow-md transition-all hover:border-primary-200 flex flex-col sm:flex-row"
:class="item.status === 'published' ? 'cursor-pointer active:scale-[0.99]' : 'opacity-75 cursor-not-allowed'"
>
@@ -98,8 +102,8 @@ const getStatusLabel = (item) => {
<div v-if="!loading && libraryItems.length === 0" class="flex flex-col items-center justify-center py-20">
<div class="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mb-4"><i class="pi pi-book text-3xl text-slate-300"></i></div>
<p class="text-slate-500">暂无已购内容</p>
<router-link to="/" class="mt-4 text-primary-600 font-medium hover:underline">去发现好内容</router-link>
<router-link :to="tenantRoute('/')" class="mt-4 text-primary-600 font-medium hover:underline">去发现好内容</router-link>
</div>
</div>
</div>
</template>
</template>

View File

@@ -6,7 +6,7 @@
<!-- Content Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div v-for="item in items" :key="item.id" class="group relative bg-white border border-slate-200 rounded-xl overflow-hidden hover:shadow-md transition-all hover:border-primary-200 cursor-pointer" @click="$router.push(`/contents/${item.id}`)">
<div v-for="item in items" :key="item.id" class="group relative bg-white border border-slate-200 rounded-xl overflow-hidden hover:shadow-md transition-all hover:border-primary-200 cursor-pointer" @click="$router.push(tenantRoute(`/contents/${item.id}`))">
<!-- Cover -->
<div class="aspect-video bg-slate-100 relative overflow-hidden">
@@ -38,7 +38,7 @@
<i class="pi pi-thumbs-up text-2xl text-slate-300"></i>
</div>
<p class="text-slate-500 text-lg">暂无点赞内容</p>
<router-link to="/" class="mt-4 inline-block text-primary-600 font-medium hover:underline">去发现好内容</router-link>
<router-link :to="tenantRoute('/')" class="mt-4 inline-block text-primary-600 font-medium hover:underline">去发现好内容</router-link>
</div>
<Toast />
@@ -47,10 +47,14 @@
<script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import Toast from 'primevue/toast';
import { useToast } from 'primevue/usetoast';
import { tenantPath } from '../../utils/tenant';
const toast = useToast();
const route = useRoute();
const tenantRoute = (path) => tenantPath(path, route);
const items = ref([
{

View File

@@ -43,7 +43,7 @@
<!-- Order Body -->
<div class="p-4 flex flex-col sm:flex-row gap-6">
<!-- Product Info (Clickable Area) -->
<div class="flex-1 flex gap-4 cursor-pointer" @click="$router.push(`/me/orders/${order.id}`)">
<div class="flex-1 flex gap-4 cursor-pointer" @click="$router.push(tenantRoute(`/me/orders/${order.id}`))">
<div class="w-24 h-16 bg-slate-100 rounded flex-shrink-0 relative overflow-hidden group-hover:opacity-90 transition-opacity flex items-center justify-center">
<template v-if="order.type === 'recharge' || !order.items?.length">
<i class="pi pi-wallet text-3xl text-primary-500"></i>
@@ -77,7 +77,7 @@
</div>
<div class="flex flex-col gap-2">
<button v-if="order.status === 'created'" class="px-4 py-1.5 bg-primary-600 text-white text-sm font-medium rounded-lg hover:bg-primary-700 transition-colors shadow-sm active:scale-95 cursor-pointer">去支付</button>
<router-link :to="`/me/orders/${order.id}`" v-if="order.status === 'paid' || order.status === 'completed'" class="px-4 py-1.5 border border-slate-300 text-slate-700 text-sm font-medium rounded-lg hover:bg-slate-50 hover:border-slate-400 transition-colors inline-block text-center cursor-pointer active:scale-95">查看详情</router-link>
<router-link :to="tenantRoute(`/me/orders/${order.id}`)" v-if="order.status === 'paid' || order.status === 'completed'" class="px-4 py-1.5 border border-slate-300 text-slate-700 text-sm font-medium rounded-lg hover:bg-slate-50 hover:border-slate-400 transition-colors inline-block text-center cursor-pointer active:scale-95">查看详情</router-link>
<button v-if="order.status === 'completed' && order.type !== 'recharge'" class="px-4 py-1.5 text-primary-600 text-sm hover:underline cursor-pointer">申请售后</button>
<button v-if="order.status === 'created'" class="text-xs text-slate-400 hover:text-slate-600 cursor-pointer">取消订单</button>
</div>
@@ -99,10 +99,12 @@
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import { userApi } from '../../api/user';
import { useRouter } from 'vue-router';
import { tenantPath } from '../../utils/tenant';
const router = useRouter();
const route = useRoute();
const tenantRoute = (path) => tenantPath(path, route);
const currentTab = ref('all');
const tabs = [
{ label: '全部订单', value: 'all' },
@@ -150,4 +152,4 @@ const statusColor = (status) => {
};
return map[status] || 'text-slate-500';
};
</script>
</script>

View File

@@ -36,7 +36,7 @@
<div class="text-sm text-slate-500">未认证发布内容前需完成认证</div>
</div>
</div>
<button @click="$router.push('/creator/apply')" class="px-4 py-2 text-primary-600 font-medium hover:text-primary-700 text-sm transition-colors">去认证</button>
<button @click="$router.push(tenantRoute('/creator/apply'))" class="px-4 py-2 text-primary-600 font-medium hover:text-primary-700 text-sm transition-colors">去认证</button>
</div>
</div>
@@ -72,16 +72,20 @@
<script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import Dialog from 'primevue/dialog';
import ConfirmDialog from 'primevue/confirmdialog';
import Toast from 'primevue/toast';
import { useConfirm } from 'primevue/useconfirm';
import { useToast } from 'primevue/usetoast';
import { tenantPath } from '../../utils/tenant';
const confirm = useConfirm();
const toast = useToast();
const verifyDialog = ref(false);
const currentAction = ref('');
const route = useRoute();
const tenantRoute = (path) => tenantPath(path, route);
const openVerify = (action) => {
currentAction.value = action;
@@ -108,4 +112,4 @@ const confirmDelete = () => {
}
});
};
</script>
</script>