Implement compression quota refunds and admin manual subscription
This commit is contained in:
69
frontend/src/app/layouts/AdminLayout.vue
Normal file
69
frontend/src/app/layouts/AdminLayout.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="mx-auto max-w-6xl px-4 py-8">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-slate-900">管理后台</h1>
|
||||
<p class="text-sm text-slate-600">仅管理员可访问系统概览与运营配置。</p>
|
||||
</div>
|
||||
<router-link
|
||||
to="/dashboard"
|
||||
class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50"
|
||||
>
|
||||
返回控制台
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex flex-col gap-6 lg:flex-row">
|
||||
<nav class="w-full shrink-0 rounded-xl border border-slate-200 bg-white p-4 lg:w-60">
|
||||
<div class="space-y-1 text-sm text-slate-700">
|
||||
<router-link
|
||||
to="/admin"
|
||||
class="block rounded-md px-3 py-2 hover:bg-slate-100"
|
||||
active-class="bg-slate-100 text-slate-900"
|
||||
>
|
||||
概览
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/admin/users"
|
||||
class="block rounded-md px-3 py-2 hover:bg-slate-100"
|
||||
active-class="bg-slate-100 text-slate-900"
|
||||
>
|
||||
用户管理
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/admin/tasks"
|
||||
class="block rounded-md px-3 py-2 hover:bg-slate-100"
|
||||
active-class="bg-slate-100 text-slate-900"
|
||||
>
|
||||
任务管理
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/admin/billing"
|
||||
class="block rounded-md px-3 py-2 hover:bg-slate-100"
|
||||
active-class="bg-slate-100 text-slate-900"
|
||||
>
|
||||
订阅与额度
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/admin/integrations"
|
||||
class="block rounded-md px-3 py-2 hover:bg-slate-100"
|
||||
active-class="bg-slate-100 text-slate-900"
|
||||
>
|
||||
支付与邮件
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/admin/config"
|
||||
class="block rounded-md px-3 py-2 hover:bg-slate-100"
|
||||
active-class="bg-slate-100 text-slate-900"
|
||||
>
|
||||
系统配置
|
||||
</router-link>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
91
frontend/src/app/layouts/DashboardLayout.vue
Normal file
91
frontend/src/app/layouts/DashboardLayout.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
const username = computed(() => auth.user?.username ?? auth.user?.email ?? '用户')
|
||||
|
||||
function logout() {
|
||||
auth.logout()
|
||||
void router.push({ name: 'home' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-full bg-slate-50">
|
||||
<header class="border-b border-slate-200 bg-white">
|
||||
<div class="mx-auto flex max-w-6xl items-center justify-between px-4 py-3">
|
||||
<div class="flex items-center gap-4">
|
||||
<RouterLink
|
||||
to="/"
|
||||
class="text-lg font-semibold tracking-tight text-slate-900 hover:no-underline"
|
||||
>
|
||||
ImageForge
|
||||
</RouterLink>
|
||||
<span class="text-sm text-slate-500">控制台</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="hidden text-sm text-slate-600 md:inline">你好,{{ username }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50"
|
||||
@click="logout"
|
||||
>
|
||||
退出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="mx-auto grid max-w-6xl grid-cols-1 gap-6 px-4 py-8 md:grid-cols-12">
|
||||
<aside class="md:col-span-3">
|
||||
<nav class="rounded-lg border border-slate-200 bg-white p-2 text-sm">
|
||||
<RouterLink
|
||||
to="/dashboard"
|
||||
class="block rounded-md px-3 py-2 text-slate-700 hover:bg-slate-50"
|
||||
exact-active-class="bg-indigo-50 text-indigo-700"
|
||||
>
|
||||
概览
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/dashboard/history"
|
||||
class="block rounded-md px-3 py-2 text-slate-700 hover:bg-slate-50"
|
||||
exact-active-class="bg-indigo-50 text-indigo-700"
|
||||
>
|
||||
历史任务
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/dashboard/api-keys"
|
||||
class="block rounded-md px-3 py-2 text-slate-700 hover:bg-slate-50"
|
||||
exact-active-class="bg-indigo-50 text-indigo-700"
|
||||
>
|
||||
API Keys
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/dashboard/billing"
|
||||
class="block rounded-md px-3 py-2 text-slate-700 hover:bg-slate-50"
|
||||
exact-active-class="bg-indigo-50 text-indigo-700"
|
||||
>
|
||||
订阅与发票
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/dashboard/settings"
|
||||
class="block rounded-md px-3 py-2 text-slate-700 hover:bg-slate-50"
|
||||
exact-active-class="bg-indigo-50 text-indigo-700"
|
||||
>
|
||||
账号设置
|
||||
</RouterLink>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="md:col-span-9">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
86
frontend/src/app/layouts/PublicLayout.vue
Normal file
86
frontend/src/app/layouts/PublicLayout.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
const isLoggedIn = computed(() => auth.isLoggedIn)
|
||||
|
||||
function logout() {
|
||||
auth.logout()
|
||||
void router.push({ name: 'home' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-full">
|
||||
<header class="sticky top-0 z-20 border-b border-slate-200 bg-white/80 backdrop-blur">
|
||||
<div class="mx-auto flex max-w-6xl items-center justify-between px-4 py-3">
|
||||
<div class="flex items-center gap-6">
|
||||
<RouterLink
|
||||
to="/"
|
||||
class="text-lg font-semibold tracking-tight text-slate-900 hover:no-underline"
|
||||
>
|
||||
ImageForge
|
||||
</RouterLink>
|
||||
<nav class="hidden items-center gap-4 text-sm text-slate-600 md:flex">
|
||||
<RouterLink to="/pricing" class="hover:text-slate-900">价格</RouterLink>
|
||||
<RouterLink to="/docs" class="hover:text-slate-900">开发者</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<template v-if="isLoggedIn">
|
||||
<RouterLink
|
||||
to="/dashboard"
|
||||
class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50"
|
||||
>
|
||||
控制台
|
||||
</RouterLink>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50"
|
||||
@click="logout"
|
||||
>
|
||||
退出
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<RouterLink
|
||||
to="/login"
|
||||
class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50"
|
||||
>
|
||||
登录
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/register"
|
||||
class="rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
注册
|
||||
</RouterLink>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="mx-auto max-w-6xl px-4 py-8">
|
||||
<router-view />
|
||||
</main>
|
||||
|
||||
<footer class="border-t border-slate-200 bg-white">
|
||||
<div
|
||||
class="mx-auto flex max-w-6xl flex-col items-start justify-between gap-2 px-4 py-6 text-sm text-slate-500 md:flex-row md:items-center"
|
||||
>
|
||||
<div>© {{ new Date().getFullYear() }} ImageForge</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<RouterLink to="/terms" class="hover:text-slate-700">服务条款</RouterLink>
|
||||
<RouterLink to="/privacy" class="hover:text-slate-700">隐私政策</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
93
frontend/src/app/router.ts
Normal file
93
frontend/src/app/router.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { Pinia } from 'pinia'
|
||||
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
|
||||
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
export function createAppRouter(pinia: Pinia) {
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/app/layouts/PublicLayout.vue'),
|
||||
children: [
|
||||
{ path: '', name: 'home', component: () => import('@/pages/HomePage.vue') },
|
||||
{ path: 'pricing', name: 'pricing', component: () => import('@/pages/PricingPage.vue') },
|
||||
{ path: 'docs', name: 'docs', component: () => import('@/pages/DocsPage.vue') },
|
||||
{ path: 'login', name: 'login', component: () => import('@/pages/LoginPage.vue') },
|
||||
{ path: 'register', name: 'register', component: () => import('@/pages/RegisterPage.vue') },
|
||||
{ path: 'verify-email', name: 'verify-email', component: () => import('@/pages/VerifyEmailPage.vue') },
|
||||
{ path: 'forgot-password', name: 'forgot-password', component: () => import('@/pages/ForgotPasswordPage.vue') },
|
||||
{ path: 'reset-password', name: 'reset-password', component: () => import('@/pages/ResetPasswordPage.vue') },
|
||||
{ path: 'terms', name: 'terms', component: () => import('@/pages/TermsPage.vue') },
|
||||
{ path: 'privacy', name: 'privacy', component: () => import('@/pages/PrivacyPage.vue') },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
component: () => import('@/app/layouts/DashboardLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'dashboard', component: () => import('@/pages/dashboard/DashboardHomePage.vue') },
|
||||
{
|
||||
path: 'history',
|
||||
name: 'dashboard-history',
|
||||
component: () => import('@/pages/dashboard/DashboardHistoryPage.vue'),
|
||||
},
|
||||
{
|
||||
path: 'api-keys',
|
||||
name: 'dashboard-api-keys',
|
||||
component: () => import('@/pages/dashboard/DashboardApiKeysPage.vue'),
|
||||
},
|
||||
{
|
||||
path: 'billing',
|
||||
name: 'dashboard-billing',
|
||||
component: () => import('@/pages/dashboard/DashboardBillingPage.vue'),
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'dashboard-settings',
|
||||
component: () => import('@/pages/dashboard/DashboardSettingsPage.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
component: () => import('@/app/layouts/AdminLayout.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true },
|
||||
children: [
|
||||
{ path: '', name: 'admin', component: () => import('@/pages/admin/AdminHomePage.vue') },
|
||||
{ path: 'users', name: 'admin-users', component: () => import('@/pages/admin/AdminUsersPage.vue') },
|
||||
{ path: 'tasks', name: 'admin-tasks', component: () => import('@/pages/admin/AdminTasksPage.vue') },
|
||||
{ path: 'billing', name: 'admin-billing', component: () => import('@/pages/admin/AdminBillingPage.vue') },
|
||||
{ path: 'integrations', name: 'admin-integrations', component: () => import('@/pages/admin/AdminIntegrationsPage.vue') },
|
||||
{ path: 'config', name: 'admin-config', component: () => import('@/pages/admin/AdminConfigPage.vue') },
|
||||
],
|
||||
},
|
||||
{ path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('@/pages/NotFoundPage.vue') },
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
})
|
||||
|
||||
router.beforeEach((to) => {
|
||||
const auth = useAuthStore(pinia)
|
||||
|
||||
if (to.meta?.requiresAuth && !auth.isLoggedIn) {
|
||||
return { name: 'login', query: { redirect: to.fullPath } }
|
||||
}
|
||||
|
||||
if ((to.name === 'login' || to.name === 'register') && auth.isLoggedIn) {
|
||||
return { name: 'dashboard' }
|
||||
}
|
||||
|
||||
if (to.meta?.requiresAdmin && auth.user?.role !== 'admin') {
|
||||
return { name: 'dashboard' }
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
Reference in New Issue
Block a user