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,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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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;
}
}

View 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}`;
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

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>