mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-03 06:52:13 +08:00
refactor(frontend): 将备份和数据管理页面合并为设置页的标签页
将独立的 /admin/backup 和 /admin/data-management 页面整合到设置页, 作为「备份」和「Sora 存储」标签页,减少侧边栏条目,集中管理配置。 - 移除 BackupView 和 DataManagementView 的 AppLayout 包装 - 在 SettingsView 中以子组件形式嵌入,使用 v-show 切换标签 - 删除独立路由和侧边栏菜单入口 - 备份/数据标签页下隐藏主保存按钮(各自有独立保存) - 优化标签栏样式适配7个标签,PC端支持细滚动条 - 清理未使用的图标组件和 i18n 键
This commit is contained in:
@@ -357,51 +357,6 @@ const ServerIcon = {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const DatabaseIcon = {
|
|
||||||
render: () =>
|
|
||||||
h(
|
|
||||||
'svg',
|
|
||||||
{ fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' },
|
|
||||||
[
|
|
||||||
h('path', {
|
|
||||||
'stroke-linecap': 'round',
|
|
||||||
'stroke-linejoin': 'round',
|
|
||||||
d: 'M3.75 5.25C3.75 4.007 7.443 3 12 3s8.25 1.007 8.25 2.25S16.557 7.5 12 7.5 3.75 6.493 3.75 5.25z'
|
|
||||||
}),
|
|
||||||
h('path', {
|
|
||||||
'stroke-linecap': 'round',
|
|
||||||
'stroke-linejoin': 'round',
|
|
||||||
d: 'M3.75 5.25v4.5C3.75 10.993 7.443 12 12 12s8.25-1.007 8.25-2.25v-4.5'
|
|
||||||
}),
|
|
||||||
h('path', {
|
|
||||||
'stroke-linecap': 'round',
|
|
||||||
'stroke-linejoin': 'round',
|
|
||||||
d: 'M3.75 9.75v4.5c0 1.243 3.693 2.25 8.25 2.25s8.25-1.007 8.25-2.25v-4.5'
|
|
||||||
}),
|
|
||||||
h('path', {
|
|
||||||
'stroke-linecap': 'round',
|
|
||||||
'stroke-linejoin': 'round',
|
|
||||||
d: 'M3.75 14.25v4.5C3.75 19.993 7.443 21 12 21s8.25-1.007 8.25-2.25v-4.5'
|
|
||||||
})
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CloudArrowUpIcon = {
|
|
||||||
render: () =>
|
|
||||||
h(
|
|
||||||
'svg',
|
|
||||||
{ fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' },
|
|
||||||
[
|
|
||||||
h('path', {
|
|
||||||
'stroke-linecap': 'round',
|
|
||||||
'stroke-linejoin': 'round',
|
|
||||||
d: 'M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z'
|
|
||||||
})
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const BellIcon = {
|
const BellIcon = {
|
||||||
render: () =>
|
render: () =>
|
||||||
h(
|
h(
|
||||||
@@ -626,8 +581,6 @@ const adminNavItems = computed((): NavItem[] => {
|
|||||||
if (authStore.isSimpleMode) {
|
if (authStore.isSimpleMode) {
|
||||||
const filtered = baseItems.filter(item => !item.hideInSimpleMode)
|
const filtered = baseItems.filter(item => !item.hideInSimpleMode)
|
||||||
filtered.push({ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon })
|
filtered.push({ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon })
|
||||||
filtered.push({ path: '/admin/backup', label: t('nav.backup'), icon: CloudArrowUpIcon })
|
|
||||||
filtered.push({ path: '/admin/data-management', label: t('nav.dataManagement'), icon: DatabaseIcon })
|
|
||||||
filtered.push({ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon })
|
filtered.push({ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon })
|
||||||
// Add admin custom menu items after settings
|
// Add admin custom menu items after settings
|
||||||
for (const cm of customMenuItemsForAdmin.value) {
|
for (const cm of customMenuItemsForAdmin.value) {
|
||||||
@@ -636,8 +589,6 @@ const adminNavItems = computed((): NavItem[] => {
|
|||||||
return filtered
|
return filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
baseItems.push({ path: '/admin/backup', label: t('nav.backup'), icon: CloudArrowUpIcon })
|
|
||||||
baseItems.push({ path: '/admin/data-management', label: t('nav.dataManagement'), icon: DatabaseIcon })
|
|
||||||
baseItems.push({ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon })
|
baseItems.push({ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon })
|
||||||
// Add admin custom menu items after settings
|
// Add admin custom menu items after settings
|
||||||
for (const cm of customMenuItemsForAdmin.value) {
|
for (const cm of customMenuItemsForAdmin.value) {
|
||||||
|
|||||||
@@ -340,8 +340,6 @@ export default {
|
|||||||
redeemCodes: 'Redeem Codes',
|
redeemCodes: 'Redeem Codes',
|
||||||
ops: 'Ops',
|
ops: 'Ops',
|
||||||
promoCodes: 'Promo Codes',
|
promoCodes: 'Promo Codes',
|
||||||
backup: 'DB Backup',
|
|
||||||
dataManagement: 'Data Management',
|
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
myAccount: 'My Account',
|
myAccount: 'My Account',
|
||||||
lightMode: 'Light Mode',
|
lightMode: 'Light Mode',
|
||||||
@@ -3938,6 +3936,8 @@ export default {
|
|||||||
users: 'Users',
|
users: 'Users',
|
||||||
gateway: 'Gateway',
|
gateway: 'Gateway',
|
||||||
email: 'Email',
|
email: 'Email',
|
||||||
|
backup: 'Backup',
|
||||||
|
data: 'Sora Storage',
|
||||||
},
|
},
|
||||||
emailTabDisabledTitle: 'Email Verification Not Enabled',
|
emailTabDisabledTitle: 'Email Verification Not Enabled',
|
||||||
emailTabDisabledHint: 'Enable email verification in the Security tab to configure SMTP settings.',
|
emailTabDisabledHint: 'Enable email verification in the Security tab to configure SMTP settings.',
|
||||||
|
|||||||
@@ -340,8 +340,6 @@ export default {
|
|||||||
redeemCodes: '兑换码',
|
redeemCodes: '兑换码',
|
||||||
ops: '运维监控',
|
ops: '运维监控',
|
||||||
promoCodes: '优惠码',
|
promoCodes: '优惠码',
|
||||||
backup: '数据库备份',
|
|
||||||
dataManagement: '数据管理',
|
|
||||||
settings: '系统设置',
|
settings: '系统设置',
|
||||||
myAccount: '我的账户',
|
myAccount: '我的账户',
|
||||||
lightMode: '浅色模式',
|
lightMode: '浅色模式',
|
||||||
@@ -4112,6 +4110,8 @@ export default {
|
|||||||
users: '用户默认值',
|
users: '用户默认值',
|
||||||
gateway: '网关服务',
|
gateway: '网关服务',
|
||||||
email: '邮件设置',
|
email: '邮件设置',
|
||||||
|
backup: '数据备份',
|
||||||
|
data: 'Sora 存储',
|
||||||
},
|
},
|
||||||
emailTabDisabledTitle: '邮箱验证未启用',
|
emailTabDisabledTitle: '邮箱验证未启用',
|
||||||
emailTabDisabledHint: '请在「安全与认证」选项卡中启用邮箱验证后,再配置 SMTP 设置。',
|
emailTabDisabledHint: '请在「安全与认证」选项卡中启用邮箱验证后,再配置 SMTP 设置。',
|
||||||
|
|||||||
@@ -350,30 +350,6 @@ const routes: RouteRecordRaw[] = [
|
|||||||
descriptionKey: 'admin.promo.description'
|
descriptionKey: 'admin.promo.description'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/admin/backup',
|
|
||||||
name: 'AdminBackup',
|
|
||||||
component: () => import('@/views/admin/BackupView.vue'),
|
|
||||||
meta: {
|
|
||||||
requiresAuth: true,
|
|
||||||
requiresAdmin: true,
|
|
||||||
title: 'Database Backup',
|
|
||||||
titleKey: 'admin.backup.title',
|
|
||||||
descriptionKey: 'admin.backup.description'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin/data-management',
|
|
||||||
name: 'AdminDataManagement',
|
|
||||||
component: () => import('@/views/admin/DataManagementView.vue'),
|
|
||||||
meta: {
|
|
||||||
requiresAuth: true,
|
|
||||||
requiresAdmin: true,
|
|
||||||
title: 'Data Management',
|
|
||||||
titleKey: 'admin.dataManagement.title',
|
|
||||||
descriptionKey: 'admin.dataManagement.description'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/admin/settings',
|
path: '/admin/settings',
|
||||||
name: 'AdminSettings',
|
name: 'AdminSettings',
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppLayout>
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<!-- S3 Storage Config -->
|
<!-- S3 Storage Config -->
|
||||||
<div class="card p-6">
|
<div class="card p-6">
|
||||||
@@ -275,13 +274,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</teleport>
|
</teleport>
|
||||||
</AppLayout>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
|
||||||
import { adminAPI } from '@/api'
|
import { adminAPI } from '@/api'
|
||||||
import { useAppStore } from '@/stores'
|
import { useAppStore } from '@/stores'
|
||||||
import type { BackupS3Config, BackupScheduleConfig, BackupRecord } from '@/api/admin/backup'
|
import type { BackupS3Config, BackupScheduleConfig, BackupRecord } from '@/api/admin/backup'
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppLayout>
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="card p-6">
|
<div class="card p-6">
|
||||||
<div class="mb-4 flex flex-wrap items-center justify-between gap-3">
|
<div class="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||||
@@ -183,13 +182,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</AppLayout>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
|
||||||
import type { SoraS3Profile } from '@/api/admin/settings'
|
import type { SoraS3Profile } from '@/api/admin/settings'
|
||||||
import { adminAPI } from '@/api'
|
import { adminAPI } from '@/api'
|
||||||
import { useAppStore } from '@/stores'
|
import { useAppStore } from '@/stores'
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<!-- Settings Form -->
|
<!-- Settings Form -->
|
||||||
<form v-else @submit.prevent="saveSettings" class="space-y-6">
|
<form v-else @submit.prevent="saveSettings" class="space-y-6">
|
||||||
<!-- Tab Navigation -->
|
<!-- Tab Navigation -->
|
||||||
<div class="sticky top-0 z-10 overflow-x-auto scrollbar-hide">
|
<div class="sticky top-0 z-10 overflow-x-auto settings-tabs-scroll">
|
||||||
<nav class="settings-tabs">
|
<nav class="settings-tabs">
|
||||||
<button
|
<button
|
||||||
v-for="tab in settingsTabs"
|
v-for="tab in settingsTabs"
|
||||||
@@ -1649,8 +1649,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div><!-- /Tab: Email -->
|
</div><!-- /Tab: Email -->
|
||||||
|
|
||||||
|
<!-- Tab: Backup -->
|
||||||
|
<div v-show="activeTab === 'backup'">
|
||||||
|
<BackupSettings />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab: Data Management -->
|
||||||
|
<div v-show="activeTab === 'data'">
|
||||||
|
<DataManagementSettings />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Save Button -->
|
<!-- Save Button -->
|
||||||
<div class="flex justify-end">
|
<div v-show="activeTab !== 'backup' && activeTab !== 'data'" class="flex justify-end">
|
||||||
<button type="submit" :disabled="saving" class="btn btn-primary">
|
<button type="submit" :disabled="saving" class="btn btn-primary">
|
||||||
<svg v-if="saving" class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
<svg v-if="saving" class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||||
<circle
|
<circle
|
||||||
@@ -1692,6 +1702,8 @@ import GroupBadge from '@/components/common/GroupBadge.vue'
|
|||||||
import GroupOptionItem from '@/components/common/GroupOptionItem.vue'
|
import GroupOptionItem from '@/components/common/GroupOptionItem.vue'
|
||||||
import Toggle from '@/components/common/Toggle.vue'
|
import Toggle from '@/components/common/Toggle.vue'
|
||||||
import ImageUpload from '@/components/common/ImageUpload.vue'
|
import ImageUpload from '@/components/common/ImageUpload.vue'
|
||||||
|
import BackupSettings from '@/views/admin/BackupView.vue'
|
||||||
|
import DataManagementSettings from '@/views/admin/DataManagementView.vue'
|
||||||
import { useClipboard } from '@/composables/useClipboard'
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
import { useAppStore } from '@/stores'
|
import { useAppStore } from '@/stores'
|
||||||
import { useAdminSettingsStore } from '@/stores/adminSettings'
|
import { useAdminSettingsStore } from '@/stores/adminSettings'
|
||||||
@@ -1706,7 +1718,7 @@ const { t } = useI18n()
|
|||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const adminSettingsStore = useAdminSettingsStore()
|
const adminSettingsStore = useAdminSettingsStore()
|
||||||
|
|
||||||
type SettingsTab = 'general' | 'security' | 'users' | 'gateway' | 'email'
|
type SettingsTab = 'general' | 'security' | 'users' | 'gateway' | 'email' | 'backup' | 'data'
|
||||||
const activeTab = ref<SettingsTab>('general')
|
const activeTab = ref<SettingsTab>('general')
|
||||||
const settingsTabs = [
|
const settingsTabs = [
|
||||||
{ key: 'general' as SettingsTab, icon: 'home' as const },
|
{ key: 'general' as SettingsTab, icon: 'home' as const },
|
||||||
@@ -1714,6 +1726,8 @@ const settingsTabs = [
|
|||||||
{ key: 'users' as SettingsTab, icon: 'user' as const },
|
{ key: 'users' as SettingsTab, icon: 'user' as const },
|
||||||
{ key: 'gateway' as SettingsTab, icon: 'server' as const },
|
{ key: 'gateway' as SettingsTab, icon: 'server' as const },
|
||||||
{ key: 'email' as SettingsTab, icon: 'mail' as const },
|
{ key: 'email' as SettingsTab, icon: 'mail' as const },
|
||||||
|
{ key: 'backup' as SettingsTab, icon: 'database' as const },
|
||||||
|
{ key: 'data' as SettingsTab, icon: 'cube' as const },
|
||||||
]
|
]
|
||||||
const { copyToClipboard } = useClipboard()
|
const { copyToClipboard } = useClipboard()
|
||||||
|
|
||||||
@@ -2378,9 +2392,38 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ============ Settings Tab Navigation ============ */
|
/* ============ Settings Tab Navigation ============ */
|
||||||
|
|
||||||
|
/* Scroll container: thin scrollbar on PC, auto-hide on mobile */
|
||||||
|
.settings-tabs-scroll {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: transparent transparent;
|
||||||
|
}
|
||||||
|
.settings-tabs-scroll:hover {
|
||||||
|
scrollbar-color: rgb(0 0 0 / 0.15) transparent;
|
||||||
|
}
|
||||||
|
:root.dark .settings-tabs-scroll:hover {
|
||||||
|
scrollbar-color: rgb(255 255 255 / 0.2) transparent;
|
||||||
|
}
|
||||||
|
.settings-tabs-scroll::-webkit-scrollbar {
|
||||||
|
height: 3px;
|
||||||
|
}
|
||||||
|
.settings-tabs-scroll::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.settings-tabs-scroll::-webkit-scrollbar-thumb {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.settings-tabs-scroll:hover::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(0 0 0 / 0.15);
|
||||||
|
}
|
||||||
|
:root.dark .settings-tabs-scroll:hover::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(255 255 255 / 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.settings-tabs {
|
.settings-tabs {
|
||||||
@apply inline-flex min-w-full gap-1 rounded-2xl
|
@apply inline-flex min-w-full gap-0.5 rounded-2xl
|
||||||
border border-gray-100 bg-white/80 p-1.5 backdrop-blur-sm
|
border border-gray-100 bg-white/80 p-1 backdrop-blur-sm
|
||||||
dark:border-dark-700/50 dark:bg-dark-800/80;
|
dark:border-dark-700/50 dark:bg-dark-800/80;
|
||||||
box-shadow: 0 1px 3px rgb(0 0 0 / 0.04), 0 1px 2px rgb(0 0 0 / 0.02);
|
box-shadow: 0 1px 3px rgb(0 0 0 / 0.04), 0 1px 2px rgb(0 0 0 / 0.02);
|
||||||
}
|
}
|
||||||
@@ -2392,8 +2435,8 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-tab {
|
.settings-tab {
|
||||||
@apply relative flex flex-1 items-center justify-center gap-2
|
@apply relative flex flex-1 items-center justify-center gap-1.5
|
||||||
whitespace-nowrap rounded-xl px-4 py-2.5
|
whitespace-nowrap rounded-xl px-2.5 py-2
|
||||||
text-sm font-medium
|
text-sm font-medium
|
||||||
text-gray-500 dark:text-dark-400
|
text-gray-500 dark:text-dark-400
|
||||||
transition-all duration-200 ease-out;
|
transition-all duration-200 ease-out;
|
||||||
@@ -2420,7 +2463,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-tab-icon {
|
.settings-tab-icon {
|
||||||
@apply flex h-7 w-7 items-center justify-center rounded-lg
|
@apply flex h-6 w-6 items-center justify-center rounded-lg
|
||||||
transition-all duration-200;
|
transition-all duration-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user