feat: tenant-scoped routing and portal navigation
This commit is contained in:
@@ -2,16 +2,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 h-full flex items-center justify-between">
|
||||
<!-- Left: Logo -->
|
||||
<router-link to="/" class="flex items-center gap-2">
|
||||
<router-link :to="tenantRoute('/')" 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>
|
||||
<span class="text-xl font-bold text-slate-900 hidden sm:block">Quyun</span>
|
||||
</router-link>
|
||||
|
||||
<!-- Center-Left: Nav Links (Desktop) -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<router-link to="/" class="text-slate-600 font-medium hover:text-primary-600" active-class="text-primary-600">首页</router-link>
|
||||
<router-link to="/explore" class="text-slate-600 font-medium hover:text-primary-600" active-class="text-primary-600">发现</router-link>
|
||||
<router-link to="/topics" class="text-slate-600 font-medium hover:text-primary-600" active-class="text-primary-600">专题</router-link>
|
||||
<router-link :to="tenantRoute('/')" class="text-slate-600 font-medium hover:text-primary-600" active-class="text-primary-600">首页</router-link>
|
||||
<router-link :to="tenantRoute('/explore')" class="text-slate-600 font-medium hover:text-primary-600" active-class="text-primary-600">发现</router-link>
|
||||
<router-link :to="tenantRoute('/topics')" class="text-slate-600 font-medium hover:text-primary-600" active-class="text-primary-600">专题</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Center-Right: Global Search -->
|
||||
@@ -30,13 +30,13 @@
|
||||
<div class="flex items-center gap-4">
|
||||
<template v-if="isLoggedIn">
|
||||
<!-- Notification -->
|
||||
<router-link to="/me/notifications" class="relative w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-50 text-slate-600">
|
||||
<router-link :to="tenantRoute('/me/notifications')" class="relative w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-50 text-slate-600">
|
||||
<i class="pi pi-bell text-xl"></i>
|
||||
<span class="absolute top-2 right-2 w-2 h-2 bg-red-500 rounded-full border border-white"></span>
|
||||
</router-link>
|
||||
|
||||
<!-- Creator Entry -->
|
||||
<router-link to="/creator/apply" class="hidden sm:flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-600 hover:bg-slate-50 rounded-lg border border-slate-200">
|
||||
<router-link :to="tenantRoute('/creator/apply')" class="hidden sm:flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-600 hover:bg-slate-50 rounded-lg border border-slate-200">
|
||||
<i class="pi pi-pencil"></i>
|
||||
<span>创作</span>
|
||||
</router-link>
|
||||
@@ -53,8 +53,8 @@
|
||||
<p class="text-sm font-bold text-slate-900">{{ user.nickname }}</p>
|
||||
<p class="text-xs text-slate-500 truncate">{{ user.phone }}</p>
|
||||
</div>
|
||||
<router-link to="/me" class="block px-4 py-2 text-sm text-slate-700 hover:bg-slate-50">个人中心</router-link>
|
||||
<router-link to="/creator" class="block px-4 py-2 text-sm text-slate-700 hover:bg-slate-50">创作者中心</router-link>
|
||||
<router-link :to="tenantRoute('/me')" class="block px-4 py-2 text-sm text-slate-700 hover:bg-slate-50">个人中心</router-link>
|
||||
<router-link :to="tenantRoute('/creator')" class="block px-4 py-2 text-sm text-slate-700 hover:bg-slate-50">创作者中心</router-link>
|
||||
<div class="border-t border-slate-50 mt-1"></div>
|
||||
<button @click="logout" class="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50">退出登录</button>
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<router-link to="/auth/login" class="bg-primary-600 text-white px-6 py-2 rounded-full font-medium hover:bg-primary-700 transition-all shadow-sm shadow-primary-100 active:scale-95">登录 / 注册</router-link>
|
||||
<router-link :to="tenantRoute('/auth/login')" class="bg-primary-600 text-white px-6 py-2 rounded-full font-medium hover:bg-primary-700 transition-all shadow-sm shadow-primary-100 active:scale-95">登录 / 注册</router-link>
|
||||
</template>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
@@ -78,11 +78,13 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { tenantPath } from '../utils/tenant';
|
||||
|
||||
const isLoggedIn = ref(false);
|
||||
const user = ref({});
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
|
||||
const checkAuth = () => {
|
||||
const token = localStorage.getItem('token');
|
||||
@@ -113,6 +115,6 @@ const logout = () => {
|
||||
localStorage.removeItem('user');
|
||||
isLoggedIn.value = false;
|
||||
user.value = {};
|
||||
router.push('/');
|
||||
router.push(tenantRoute('/'));
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<!-- Menus -->
|
||||
<nav class="p-4 space-y-1 flex-1">
|
||||
<router-link to="/creator"
|
||||
<router-link :to="tenantRoute('/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>
|
||||
@@ -32,13 +32,13 @@
|
||||
|
||||
<div class="px-4 py-2 text-xs font-bold text-slate-500 uppercase tracking-wider mt-4">内容与交易</div>
|
||||
|
||||
<router-link to="/creator/contents"
|
||||
<router-link :to="tenantRoute('/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"
|
||||
<router-link :to="tenantRoute('/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>
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
<div class="px-4 py-2 text-xs font-bold text-slate-500 uppercase tracking-wider mt-4">配置</div>
|
||||
|
||||
<router-link to="/creator/settings"
|
||||
<router-link :to="tenantRoute('/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>
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
<!-- Footer Link -->
|
||||
<div class="p-4 border-t border-slate-800">
|
||||
<router-link :to="'/t/' + (tenantId || '1')"
|
||||
<router-link :to="tenantRoute('/')"
|
||||
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>
|
||||
@@ -76,27 +76,16 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import AppFooter from '../components/AppFooter.vue';
|
||||
import TopNavbar from '../components/TopNavbar.vue';
|
||||
import { creatorApi } from '../api/creator';
|
||||
import { tenantPath } from '../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantId = ref('');
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
|
||||
const isFullWidth = computed(() => {
|
||||
return ['creator-content-new', 'creator-content-edit'].includes(route.name);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await creatorApi.getSettings();
|
||||
if (res && res.id) {
|
||||
tenantId.value = res.id;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -20,52 +20,52 @@
|
||||
|
||||
<!-- Menus -->
|
||||
<nav class="p-4 space-y-1">
|
||||
<router-link to="/me" exact-active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
<router-link :to="tenantRoute('/me')" exact-active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-home text-lg"></i>
|
||||
<span>概览</span>
|
||||
</router-link>
|
||||
<router-link to="/me/orders" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
<router-link :to="tenantRoute('/me/orders')" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-shopping-bag text-lg"></i>
|
||||
<span>我的订单</span>
|
||||
</router-link>
|
||||
<router-link to="/me/wallet" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
<router-link :to="tenantRoute('/me/wallet')" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-wallet text-lg"></i>
|
||||
<span>我的钱包</span>
|
||||
</router-link>
|
||||
<router-link to="/me/coupons" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
<router-link :to="tenantRoute('/me/coupons')" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-ticket text-lg"></i>
|
||||
<span>我的优惠券</span>
|
||||
</router-link>
|
||||
<router-link to="/me/library" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
<router-link :to="tenantRoute('/me/library')" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-book text-lg"></i>
|
||||
<span>已购内容</span>
|
||||
</router-link>
|
||||
<router-link to="/me/favorites" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
<router-link :to="tenantRoute('/me/favorites')" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-heart text-lg"></i>
|
||||
<span>我的收藏</span>
|
||||
</router-link>
|
||||
<router-link to="/me/likes" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
<router-link :to="tenantRoute('/me/likes')" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-thumbs-up text-lg"></i>
|
||||
<span>我的点赞</span>
|
||||
</router-link>
|
||||
<router-link to="/me/notifications" active-class="bg-primary-50 text-primary-600 font-semibold" class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<router-link :to="tenantRoute('/me/notifications')" active-class="bg-primary-50 text-primary-600 font-semibold" class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-bell text-lg"></i>
|
||||
<span>消息中心</span>
|
||||
</router-link>
|
||||
<div class="my-2 border-t border-slate-100"></div>
|
||||
<router-link to="/me/profile" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
<router-link :to="tenantRoute('/me/profile')" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-user text-lg"></i>
|
||||
<span>个人资料</span>
|
||||
</router-link>
|
||||
<router-link to="/me/security" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
<router-link :to="tenantRoute('/me/security')" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-shield text-lg"></i>
|
||||
<span>账号安全</span>
|
||||
@@ -86,8 +86,12 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import AppFooter from '../components/AppFooter.vue';
|
||||
import TopNavbar from '../components/TopNavbar.vue';
|
||||
import { tenantPath } from '../utils/tenant';
|
||||
|
||||
const user = ref(JSON.parse(localStorage.getItem('user') || '{}'));
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
</script>
|
||||
|
||||
@@ -8,7 +8,7 @@ const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
path: '/t/:tenantCode',
|
||||
component: LayoutMain,
|
||||
children: [
|
||||
{
|
||||
@@ -37,19 +37,19 @@ const router = createRouter({
|
||||
component: () => import('../views/TopicsView.vue') // Placeholder
|
||||
},
|
||||
{
|
||||
path: 'creator/apply',
|
||||
name: 'creator-apply',
|
||||
component: () => import('../views/creator/ApplyView.vue')
|
||||
path: 'creator/apply',
|
||||
name: 'creator-apply',
|
||||
component: () => import('../views/creator/ApplyView.vue')
|
||||
},
|
||||
{
|
||||
path: 'creator/contents/new',
|
||||
name: 'creator-content-new',
|
||||
component: () => import('../views/creator/ContentsEditView.vue')
|
||||
path: 'creator/contents/new',
|
||||
name: 'creator-content-new',
|
||||
component: () => import('../views/creator/ContentsEditView.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/auth',
|
||||
path: '/t/:tenantCode/auth',
|
||||
component: LayoutAuth,
|
||||
children: [
|
||||
{
|
||||
@@ -60,129 +60,129 @@ const router = createRouter({
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/me',
|
||||
component: LayoutUser,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'user-dashboard',
|
||||
component: () => import('../views/user/DashboardView.vue')
|
||||
},
|
||||
{
|
||||
path: 'orders',
|
||||
name: 'user-orders',
|
||||
component: () => import('../views/user/OrdersView.vue')
|
||||
},
|
||||
{
|
||||
path: 'orders/:id',
|
||||
name: 'user-order-detail',
|
||||
component: () => import('../views/order/DetailView.vue')
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
name: 'user-wallet',
|
||||
component: () => import('../views/user/WalletView.vue')
|
||||
},
|
||||
{
|
||||
path: 'coupons',
|
||||
name: 'user-coupons',
|
||||
component: () => import('../views/user/CouponsView.vue')
|
||||
},
|
||||
{
|
||||
path: 'library',
|
||||
name: 'user-library',
|
||||
component: () => import('../views/user/LibraryView.vue')
|
||||
},
|
||||
{
|
||||
path: 'favorites',
|
||||
name: 'user-favorites',
|
||||
component: () => import('../views/user/FavoritesView.vue')
|
||||
},
|
||||
{
|
||||
path: 'likes',
|
||||
name: 'user-likes',
|
||||
component: () => import('../views/user/LikesView.vue')
|
||||
},
|
||||
{
|
||||
path: 'notifications',
|
||||
name: 'user-notifications',
|
||||
component: () => import('../views/user/NotificationsView.vue')
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
name: 'user-profile',
|
||||
component: () => import('../views/user/ProfileView.vue')
|
||||
},
|
||||
{
|
||||
path: 'security',
|
||||
name: 'user-security',
|
||||
component: () => import('../views/user/SecurityView.vue')
|
||||
}
|
||||
]
|
||||
path: '/t/:tenantCode/me',
|
||||
component: LayoutUser,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'user-dashboard',
|
||||
component: () => import('../views/user/DashboardView.vue')
|
||||
},
|
||||
{
|
||||
path: 'orders',
|
||||
name: 'user-orders',
|
||||
component: () => import('../views/user/OrdersView.vue')
|
||||
},
|
||||
{
|
||||
path: 'orders/:id',
|
||||
name: 'user-order-detail',
|
||||
component: () => import('../views/order/DetailView.vue')
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
name: 'user-wallet',
|
||||
component: () => import('../views/user/WalletView.vue')
|
||||
},
|
||||
{
|
||||
path: 'coupons',
|
||||
name: 'user-coupons',
|
||||
component: () => import('../views/user/CouponsView.vue')
|
||||
},
|
||||
{
|
||||
path: 'library',
|
||||
name: 'user-library',
|
||||
component: () => import('../views/user/LibraryView.vue')
|
||||
},
|
||||
{
|
||||
path: 'favorites',
|
||||
name: 'user-favorites',
|
||||
component: () => import('../views/user/FavoritesView.vue')
|
||||
},
|
||||
{
|
||||
path: 'likes',
|
||||
name: 'user-likes',
|
||||
component: () => import('../views/user/LikesView.vue')
|
||||
},
|
||||
{
|
||||
path: 'notifications',
|
||||
name: 'user-notifications',
|
||||
component: () => import('../views/user/NotificationsView.vue')
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
name: 'user-profile',
|
||||
component: () => import('../views/user/ProfileView.vue')
|
||||
},
|
||||
{
|
||||
path: 'security',
|
||||
name: 'user-security',
|
||||
component: () => import('../views/user/SecurityView.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/creator',
|
||||
component: LayoutCreator,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'creator-dashboard',
|
||||
component: () => import('../views/creator/DashboardView.vue')
|
||||
},
|
||||
{
|
||||
path: 'contents',
|
||||
name: 'creator-contents',
|
||||
component: () => import('../views/creator/ContentsView.vue')
|
||||
},
|
||||
{
|
||||
path: 'contents/new',
|
||||
name: 'creator-content-new',
|
||||
component: () => import('../views/creator/ContentsEditView.vue')
|
||||
},
|
||||
{
|
||||
path: 'contents/:id',
|
||||
name: 'creator-content-edit',
|
||||
component: () => import('../views/creator/ContentsEditView.vue')
|
||||
},
|
||||
{
|
||||
path: 'orders',
|
||||
name: 'creator-orders',
|
||||
component: () => import('../views/creator/OrdersView.vue')
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'creator-settings',
|
||||
component: () => import('../views/creator/SettingsView.vue')
|
||||
}
|
||||
]
|
||||
path: '/t/:tenantCode/creator',
|
||||
component: LayoutCreator,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'creator-dashboard',
|
||||
component: () => import('../views/creator/DashboardView.vue')
|
||||
},
|
||||
{
|
||||
path: 'contents',
|
||||
name: 'creator-contents',
|
||||
component: () => import('../views/creator/ContentsView.vue')
|
||||
},
|
||||
{
|
||||
path: 'contents/new',
|
||||
name: 'creator-content-new',
|
||||
component: () => import('../views/creator/ContentsEditView.vue')
|
||||
},
|
||||
{
|
||||
path: 'contents/:id',
|
||||
name: 'creator-content-edit',
|
||||
component: () => import('../views/creator/ContentsEditView.vue')
|
||||
},
|
||||
{
|
||||
path: 'orders',
|
||||
name: 'creator-orders',
|
||||
component: () => import('../views/creator/OrdersView.vue')
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'creator-settings',
|
||||
component: () => import('../views/creator/SettingsView.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/checkout',
|
||||
component: LayoutMain,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'checkout',
|
||||
component: () => import('../views/order/CheckoutView.vue')
|
||||
}
|
||||
]
|
||||
path: '/t/:tenantCode/checkout',
|
||||
component: LayoutMain,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'checkout',
|
||||
component: () => import('../views/order/CheckoutView.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/payment/:id',
|
||||
component: LayoutMain,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'payment',
|
||||
component: () => import('../views/order/PaymentView.vue')
|
||||
}
|
||||
]
|
||||
path: '/t/:tenantCode/payment/:id',
|
||||
component: LayoutMain,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'payment',
|
||||
component: () => import('../views/order/PaymentView.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
// Fallback
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'not-found',
|
||||
component: () => import('../views/misc/NotFoundView.vue')
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'not-found',
|
||||
component: () => import('../views/misc/NotFoundView.vue')
|
||||
}
|
||||
],
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Simple Fetch Wrapper
|
||||
const BASE_URL = '/v1';
|
||||
import { getTenantCode } from './tenant';
|
||||
|
||||
export async function request(endpoint, options = {}) {
|
||||
const tenantCode = getTenantCode();
|
||||
const baseUrl = tenantCode ? `/t/${tenantCode}/v1` : '/v1';
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
const headers = {
|
||||
@@ -22,7 +24,7 @@ export async function request(endpoint, options = {}) {
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${BASE_URL}${endpoint}`, {
|
||||
const res = await fetch(`${baseUrl}${endpoint}`, {
|
||||
...options,
|
||||
headers
|
||||
});
|
||||
@@ -40,9 +42,10 @@ export async function request(endpoint, options = {}) {
|
||||
if (res.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
const loginPath = tenantCode ? `/t/${tenantCode}/auth/login` : '/auth/login';
|
||||
// Redirect to login if not already there
|
||||
if (!window.location.pathname.startsWith('/auth/login')) {
|
||||
window.location.href = '/auth/login';
|
||||
if (!window.location.pathname.includes('/auth/login')) {
|
||||
window.location.href = loginPath;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
18
frontend/portal/src/utils/tenant.js
Normal file
18
frontend/portal/src/utils/tenant.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export function getTenantCode() {
|
||||
const match = window.location.pathname.match(/^\/t\/([^/]+)(?:\/|$)/);
|
||||
return match ? match[1] : '';
|
||||
}
|
||||
|
||||
export function resolveTenantCode(route) {
|
||||
if (route && route.params && route.params.tenantCode) {
|
||||
return String(route.params.tenantCode);
|
||||
}
|
||||
return getTenantCode();
|
||||
}
|
||||
|
||||
export function tenantPath(path, route) {
|
||||
const tenantCode = resolveTenantCode(route);
|
||||
const base = tenantCode ? `/t/${tenantCode}` : '';
|
||||
const normalized = path.startsWith('/') ? path : `/${path}`;
|
||||
return `${base}${normalized}`;
|
||||
}
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<!-- Content Grid -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div v-for="item in contents" :key="item.id" class="bg-white rounded-xl border border-slate-100 overflow-hidden hover:shadow-lg transition-all group cursor-pointer active:scale-[0.99]" @click="$router.push(`/contents/${item.id}`)">
|
||||
<div v-for="item in contents" :key="item.id" class="bg-white rounded-xl border border-slate-100 overflow-hidden hover:shadow-lg transition-all group cursor-pointer active:scale-[0.99]" @click="$router.push(tenantRoute(`/contents/${item.id}`))">
|
||||
<!-- Cover -->
|
||||
<div class="aspect-video bg-slate-100 relative">
|
||||
<img :src="item.cover || `https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60`" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105">
|
||||
@@ -92,8 +92,12 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { contentApi } from '../api/content';
|
||||
import { tenantPath } from '../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
const selectedGenre = ref('全部');
|
||||
const selectedPrice = ref('all');
|
||||
const sort = ref('latest');
|
||||
@@ -146,4 +150,4 @@ const filters = {
|
||||
{ label: '会员专享', value: 'member' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { contentApi } from '../../api/content';
|
||||
import { tenantApi } from '../../api/tenant';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
const contents = ref([]);
|
||||
const bannerItems = ref([]);
|
||||
const trendingItems = ref([]);
|
||||
@@ -97,7 +101,7 @@ onMounted(fetchData);
|
||||
<div class="absolute bottom-0 left-0 p-10 max-w-2xl text-white">
|
||||
<div class="inline-block px-3 py-1 bg-red-600 text-white text-xs font-bold rounded mb-3">置顶推荐</div>
|
||||
<h2 class="text-4xl font-bold mb-4 leading-tight cursor-pointer hover:underline"
|
||||
@click="$router.push(`/contents/${item.id}`)">{{ item.title }}</h2>
|
||||
@click="$router.push(tenantRoute(`/contents/${item.id}`))">{{ item.title }}</h2>
|
||||
<p class="text-lg text-slate-200 line-clamp-2">{{ item.description || item.title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -167,7 +171,7 @@ onMounted(fetchData);
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div v-for="creator in matchedCreators" :key="creator.id"
|
||||
class="flex items-center gap-3 p-3 rounded-xl hover:bg-slate-50 transition-colors cursor-pointer border border-transparent hover:border-slate-200"
|
||||
@click="$router.push(`/creators/${creator.id}`)">
|
||||
@click="$router.push(tenantRoute(`/t/${creator.id}`))">
|
||||
<img :src="creator.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${creator.id}`"
|
||||
class="w-12 h-12 rounded-full border border-slate-100">
|
||||
<div class="flex-1 min-w-0">
|
||||
@@ -178,7 +182,7 @@ onMounted(fetchData);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<router-link v-for="item in contents" :key="item.id" :to="`/contents/${item.id}`"
|
||||
<router-link v-for="item in contents" :key="item.id" :to="tenantRoute(`/contents/${item.id}`)"
|
||||
class="block bg-white rounded-2xl shadow-sm border border-slate-100 p-6 hover:shadow-xl hover:border-primary-100 transition-all duration-300 group cursor-pointer active:scale-[0.99]">
|
||||
<div class="flex gap-8">
|
||||
<div class="flex-1 min-w-0 flex flex-col">
|
||||
@@ -254,10 +258,10 @@ onMounted(fetchData);
|
||||
<div class="space-y-4">
|
||||
<div v-for="creator in recommendedCreators" :key="creator.id" class="flex items-center gap-3">
|
||||
<img :src="creator.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${creator.id}`"
|
||||
class="w-10 h-10 rounded-full cursor-pointer" @click="$router.push(`/creators/${creator.id}`)">
|
||||
class="w-10 h-10 rounded-full cursor-pointer" @click="$router.push(tenantRoute(`/t/${creator.id}`))">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-bold text-slate-900 text-sm truncate hover:text-primary-600 cursor-pointer"
|
||||
@click="$router.push(`/creators/${creator.id}`)">{{ creator.name }}</div>
|
||||
@click="$router.push(tenantRoute(`/t/${creator.id}`))">{{ creator.name }}</div>
|
||||
<div class="text-xs text-slate-500 truncate">粉丝 {{ creator.stats?.followers || 0 }}</div>
|
||||
</div>
|
||||
<button
|
||||
@@ -278,7 +282,7 @@ onMounted(fetchData);
|
||||
:class="index === 0 ? 'text-red-500' : (index === 1 ? 'text-orange-500' : 'text-yellow-500')">{{
|
||||
index + 1 }}</span>
|
||||
<div class="flex-1">
|
||||
<h4 @click="$router.push(`/contents/${item.id}`)"
|
||||
<h4 @click="$router.push(tenantRoute(`/contents/${item.id}`))"
|
||||
class="text-sm font-medium text-slate-800 line-clamp-2 hover:text-primary-600 cursor-pointer">
|
||||
{{ item.title }}</h4>
|
||||
<span class="text-xs text-slate-400 mt-1 block">{{ item.views }} 阅读</span>
|
||||
@@ -301,4 +305,3 @@ onMounted(fetchData);
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 1. Hero Topic -->
|
||||
<div class="relative w-full h-[400px] rounded-2xl overflow-hidden mb-12 group cursor-pointer shadow-lg" @click="$router.push(`/explore?topic=${topics[0].id}`)">
|
||||
<div class="relative w-full h-[400px] rounded-2xl overflow-hidden mb-12 group cursor-pointer shadow-lg" @click="$router.push(tenantRoute(`/explore?topic=${topics[0].id}`))">
|
||||
<img :src="topics[0].cover" 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/90 via-black/20 to-transparent"></div>
|
||||
<div class="absolute bottom-0 left-0 p-10 max-w-3xl text-white">
|
||||
@@ -30,7 +30,7 @@
|
||||
<div v-for="(topic, idx) in topics.slice(1)" :key="topic.id"
|
||||
class="group bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden hover:shadow-xl transition-all cursor-pointer flex flex-col"
|
||||
:class="{ 'lg:col-span-2 flex-row': idx === 0 }"
|
||||
@click="$router.push(`/explore?topic=${topic.id}`)"
|
||||
@click="$router.push(tenantRoute(`/explore?topic=${topic.id}`))"
|
||||
>
|
||||
<!-- Cover -->
|
||||
<div class="relative overflow-hidden" :class="idx === 0 ? 'w-1/2' : 'h-48'">
|
||||
@@ -72,7 +72,11 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { tenantPath } from '../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
const topics = ref([
|
||||
{
|
||||
id: 1,
|
||||
@@ -120,4 +124,4 @@ const topics = ref([
|
||||
count: 15
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import Toast from 'primevue/toast';
|
||||
import { authApi } from '../../api/auth';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const toast = useToast();
|
||||
const step = ref(1);
|
||||
const phone = ref('');
|
||||
@@ -30,7 +32,7 @@ const login = async () => {
|
||||
localStorage.setItem('user', JSON.stringify(res.user));
|
||||
toast.add({ severity: 'success', summary: '登录成功', detail: '欢迎回来', life: 1000 });
|
||||
setTimeout(() => {
|
||||
router.push('/');
|
||||
router.push(tenantPath('/', route));
|
||||
}, 1000);
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: '登录失败', detail: e.message, life: 3000 });
|
||||
@@ -139,4 +141,4 @@ const login = async () => {
|
||||
</div>
|
||||
<Toast />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -101,8 +101,8 @@
|
||||
<p class="text-lg text-slate-600 mb-12 max-w-lg mx-auto">您的入驻申请已成功提交,平台将在 1-3 个工作日内完成审核。审核结果将通过短信和系统通知发送给您。</p>
|
||||
|
||||
<div class="flex justify-center gap-4">
|
||||
<router-link to="/" class="px-8 py-3 bg-white border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 font-medium">返回首页</router-link>
|
||||
<router-link to="/me" class="px-8 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 font-medium">查看个人中心</router-link>
|
||||
<router-link :to="tenantRoute('/')" class="px-8 py-3 bg-white border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 font-medium">返回首页</router-link>
|
||||
<router-link :to="tenantRoute('/me')" class="px-8 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 font-medium">查看个人中心</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -111,7 +111,11 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
const step = ref(1);
|
||||
const submitting = ref(false);
|
||||
const fileInput = ref(null);
|
||||
|
||||
@@ -228,9 +228,11 @@ import { computed, reactive, ref, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { commonApi } from '../../api/common';
|
||||
import { creatorApi } from '../../api/creator';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
const toast = useToast();
|
||||
const fileInput = ref(null);
|
||||
const currentUploadType = ref('');
|
||||
@@ -499,7 +501,7 @@ const saveContent = async (targetStatus) => {
|
||||
toast.add({ severity: 'success', summary: targetStatus === 'draft' ? '保存成功' : '发布成功', detail: targetStatus === 'draft' ? '已保存为草稿' : '内容已提交审核', life: 3000 });
|
||||
}
|
||||
|
||||
setTimeout(() => router.push('/creator/contents'), 1500);
|
||||
setTimeout(() => router.push(tenantRoute('/creator/contents')), 1500);
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: '操作失败', detail: e.message, life: 3000 });
|
||||
} finally {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<h1 class="text-2xl font-bold text-slate-900">内容管理</h1>
|
||||
<router-link to="/creator/contents/new"
|
||||
<router-link :to="tenantRoute('/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>
|
||||
@@ -83,7 +83,7 @@
|
||||
</div>
|
||||
<h3 class="text-slate-900 font-bold mb-1">暂无内容</h3>
|
||||
<p class="text-slate-500 text-sm mb-6">您还没有发布任何内容,快去创作吧!</p>
|
||||
<router-link to="/creator/contents/new" class="px-5 py-2 bg-primary-600 text-white rounded-lg text-sm font-bold hover:bg-primary-700 transition-colors">
|
||||
<router-link :to="tenantRoute('/creator/contents/new')" class="px-5 py-2 bg-primary-600 text-white rounded-lg text-sm font-bold hover:bg-primary-700 transition-colors">
|
||||
立即发布
|
||||
</router-link>
|
||||
</div>
|
||||
@@ -100,7 +100,7 @@
|
||||
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/${item.id}`"
|
||||
<router-link :to="tenantRoute(`/creator/contents/${item.id}`)"
|
||||
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>
|
||||
@@ -119,7 +119,7 @@
|
||||
class="bg-blue-50 text-blue-600 text-[11px] px-2 py-0.5 rounded-full font-bold whitespace-nowrap">{{
|
||||
item.key }}</span>
|
||||
<h3 class="font-bold text-slate-900 text-lg truncate hover:text-primary-600 cursor-pointer transition-colors"
|
||||
@click="$router.push(`/creator/contents/${item.id}`)">
|
||||
@click="$router.push(tenantRoute(`/creator/contents/${item.id}`))">
|
||||
{{ item.title }}</h3>
|
||||
</div>
|
||||
<!-- Status Badge -->
|
||||
@@ -170,7 +170,7 @@
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-4 pt-3 border-t border-slate-50 mt-3">
|
||||
<button class="text-sm text-slate-500 hover:text-primary-600 font-medium cursor-pointer flex items-center gap-1"
|
||||
@click="$router.push(`/creator/contents/${item.id}`)">
|
||||
@click="$router.push(tenantRoute(`/creator/contents/${item.id}`))">
|
||||
<i class="pi pi-file-edit"></i> 编辑
|
||||
</button>
|
||||
<button v-if="item.status === 'published'"
|
||||
@@ -211,13 +211,17 @@
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import ConfirmDialog from 'primevue/confirmdialog';
|
||||
import Paginator from 'primevue/paginator';
|
||||
import { commonApi } from '../../api/common';
|
||||
import { creatorApi } from '../../api/creator';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
@@ -407,4 +411,4 @@ const handleDelete = (id) => {
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<h1 class="text-2xl font-bold text-slate-900">管理概览</h1>
|
||||
<div class="flex gap-4">
|
||||
<router-link to="/creator/contents/new"
|
||||
<router-link :to="tenantRoute('/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>
|
||||
@@ -39,7 +39,7 @@
|
||||
<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')">
|
||||
@click="$router.push(tenantRoute('/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>
|
||||
@@ -83,7 +83,7 @@
|
||||
<div class="text-xs text-slate-500" v-if="hasPayoutAccount">已绑定:{{ payoutAccounts[0].name }} ({{ payoutAccounts[0].account.slice(-4) }})</div>
|
||||
<div class="text-xs text-orange-600 font-bold flex items-center gap-1" v-else>
|
||||
<i class="pi pi-exclamation-circle"></i> 未配置收款账户
|
||||
<router-link to="/creator/settings"
|
||||
<router-link :to="tenantRoute('/creator/settings')"
|
||||
class="underline hover:text-orange-800 ml-1">去配置</router-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -134,8 +134,12 @@ import Dialog from 'primevue/dialog';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { creatorApi } from '../../api/creator';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
const toast = useToast();
|
||||
const showWithdraw = ref(false);
|
||||
const withdrawMethod = ref('wallet');
|
||||
@@ -207,4 +211,4 @@ const handleWithdraw = async () => {
|
||||
toast.add({ severity: 'error', summary: '提现失败', detail: '请稍后重试', life: 3000 });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -8,9 +8,17 @@
|
||||
<h1 class="text-4xl font-bold text-slate-900 mb-4">404</h1>
|
||||
<p class="text-xl text-slate-600 mb-8">抱歉,您访问的页面走丢了。</p>
|
||||
<div class="flex justify-center gap-4">
|
||||
<router-link to="/" class="px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors">返回首页</router-link>
|
||||
<router-link :to="tenantRoute('/')" class="px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors">返回首页</router-link>
|
||||
<button @click="$router.back()" class="px-6 py-3 border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 transition-colors">返回上一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
</script>
|
||||
|
||||
@@ -104,9 +104,11 @@
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { orderApi } from '../../api/order';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
|
||||
const orderId = route.params.id || '82934712';
|
||||
const amount = '9.90'; // Should fetch order details first
|
||||
@@ -135,7 +137,7 @@ const simulateSuccess = () => {
|
||||
isScanning.value = false;
|
||||
isSuccess.value = true;
|
||||
setTimeout(() => {
|
||||
router.replace(`/me/orders/${orderId}`);
|
||||
router.replace(tenantRoute(`/me/orders/${orderId}`));
|
||||
}, 1500);
|
||||
}, 1000);
|
||||
};
|
||||
@@ -153,7 +155,7 @@ onMounted(() => {
|
||||
isScanning.value = false;
|
||||
isSuccess.value = true;
|
||||
clearInterval(pollTimer);
|
||||
setTimeout(() => router.replace(`/me/orders/${orderId}`), 1500);
|
||||
setTimeout(() => router.replace(tenantRoute(`/me/orders/${orderId}`)), 1500);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Poll status failed', e);
|
||||
@@ -165,4 +167,4 @@ onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
<!-- Featured (Pinned) -->
|
||||
<div class="relative h-[400px] rounded-2xl overflow-hidden group cursor-pointer"
|
||||
v-if="featuredContent"
|
||||
@click="$router.push(`/contents/${featuredContent.id}`)">
|
||||
@click="$router.push(tenantRoute(`/contents/${featuredContent.id}`))">
|
||||
<img :src="featuredContent.cover" 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>
|
||||
@@ -115,7 +115,7 @@
|
||||
<div v-for="item in contents" :key="item.id"
|
||||
v-show="!featuredContent || item.id !== featuredContent.id"
|
||||
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/${item.id}`)">
|
||||
@click="$router.push(tenantRoute(`/contents/${item.id}`))">
|
||||
<div class="w-64 h-36 bg-slate-100 rounded-lg flex-shrink-0 overflow-hidden relative">
|
||||
<img :src="item.cover || 'https://via.placeholder.com/300x168?text=No+Cover'" class="w-full h-full object-cover">
|
||||
</div>
|
||||
@@ -172,8 +172,10 @@ import { reactive, ref, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { tenantApi } from '../../api/tenant';
|
||||
import { contentApi } from '../../api/content';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
const toast = useToast();
|
||||
const currentTab = ref('home');
|
||||
const isFollowing = ref(false);
|
||||
@@ -280,4 +282,4 @@ const tabs = [
|
||||
{ label: '主页', value: 'home' },
|
||||
{ label: '关于', value: 'about' }
|
||||
];
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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([
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user