Files
AIGC/demo/frontend/src/views/SystemSettings.vue

1513 lines
39 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="system-settings">
<!-- 左侧导航栏 -->
<aside class="sidebar">
<div class="logo">
<img src="/images/backgrounds/logo-admin.svg" alt="Logo" />
</div>
<nav class="nav-menu">
<div class="nav-item" @click="goToDashboard">
<el-icon><Grid /></el-icon>
<span>{{ $t('nav.dashboard') }}</span>
</div>
<div class="nav-item" @click="goToMembers">
<el-icon><User /></el-icon>
<span>{{ $t('nav.members') }}</span>
</div>
<div class="nav-item" @click="goToOrders">
<el-icon><ShoppingCart /></el-icon>
<span>{{ $t('nav.orders') }}</span>
</div>
<div class="nav-item" @click="goToAPI">
<el-icon><Document /></el-icon>
<span>{{ $t('nav.apiManagement') }}</span>
</div>
<div class="nav-item" @click="goToTasks">
<el-icon><Document /></el-icon>
<span>{{ $t('nav.tasks') }}</span>
</div>
<div class="nav-item" @click="goToErrorStats">
<el-icon><Warning /></el-icon>
<span>错误统计</span>
</div>
<div class="nav-item active">
<el-icon><Setting /></el-icon>
<span>{{ $t('nav.systemSettings') }}</span>
</div>
</nav>
<div class="sidebar-footer">
<div class="online-users">
{{ $t('nav.todayVisitors') }}: <span class="highlight">{{ onlineUsers }}</span>
</div>
<div class="system-uptime">
{{ $t('nav.systemUptime') }}: <span class="highlight">{{ systemUptime }}</span>
</div>
</div>
</aside>
<!-- 主内容区域 -->
<main class="main-content">
<!-- 顶部操作栏 -->
<header class="top-header">
<div class="page-title">
<h2>{{ $t('nav.systemSettings') }}</h2>
</div>
<div class="header-actions">
<LanguageSwitcher />
<el-dropdown @command="handleUserCommand">
<div class="user-avatar">
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
<el-icon class="arrow-down"><ArrowDown /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="exitAdmin">
{{ $t('admin.exitAdmin') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</header>
<!-- 设置选项卡 -->
<div class="settings-tabs">
<div class="tab-nav">
<div
class="tab-item"
:class="{ active: activeTab === 'membership' }"
@click="activeTab = 'membership'"
>
<el-icon><User /></el-icon>
<span>{{ $t('systemSettings.membership') }}</span>
</div>
<!-- 任务清理管理标签暂时隐藏 -->
<div
v-if="false"
class="tab-item"
:class="{ active: activeTab === 'cleanup' }"
@click="activeTab = 'cleanup'"
>
<el-icon><Delete /></el-icon>
<span>{{ $t('systemSettings.cleanup') }}</span>
</div>
<div
class="tab-item"
:class="{ active: activeTab === 'aiModel' }"
@click="activeTab = 'aiModel'"
>
<el-icon><Setting /></el-icon>
<span>{{ $t('systemSettings.aiModel') }}</span>
</div>
</div>
<!-- 会员收费标准选项卡 -->
<div v-if="activeTab === 'membership'" class="tab-content">
<h2 class="page-title">{{ $t('systemSettings.membership') }}</h2>
<div class="membership-cards">
<el-card v-for="level in membershipLevels" :key="level.id" class="membership-card">
<div class="card-header">
<h3>{{ level.name }}</h3>
</div>
<div class="card-body">
<p class="price">¥{{ level.price || 0 }}/{{ Math.floor((level.resourcePoints || level.pointsBonus || 0) / 30) }}{{ $t('subscription.items') }}</p>
<p class="description">{{ level.resourcePoints || level.pointsBonus || 0 }}{{ $t('subscription.points') }}</p>
</div>
<div class="card-footer">
<el-button type="primary" @click="editLevel(level)">{{ $t('common.edit') }}</el-button>
</div>
</el-card>
</div>
</div>
<!-- 任务清理管理选项卡 -->
<div v-if="activeTab === 'cleanup'" class="tab-content">
<h2 class="page-title">{{ $t('systemSettings.cleanup') }}</h2>
<!-- 清理统计信息 -->
<div class="cleanup-stats">
<el-card class="stats-card">
<template #header>
<div class="card-header">
<h3>{{ $t('systemSettings.cleanupStatsInfo') }}</h3>
<el-button type="primary" @click="refreshStats" :loading="loadingStats">
<el-icon><Refresh /></el-icon>
{{ $t('systemSettings.refresh') }}
</el-button>
</div>
</template>
<div class="stats-content" v-if="cleanupStats">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-label">{{ $t('systemSettings.currentTotalTasks') }}</div>
<div class="stat-value">{{ cleanupStats.currentTasks?.textToVideo?.total + cleanupStats.currentTasks?.imageToVideo?.total || 0 }}</div>
</div>
<div class="stat-item">
<div class="stat-label">{{ $t('systemSettings.completedTasks') }}</div>
<div class="stat-value">{{ cleanupStats.currentTasks?.textToVideo?.completed + cleanupStats.currentTasks?.imageToVideo?.completed || 0 }}</div>
</div>
<div class="stat-item">
<div class="stat-label">{{ $t('systemSettings.failedTasks') }}</div>
<div class="stat-value">{{ cleanupStats.currentTasks?.textToVideo?.failed + cleanupStats.currentTasks?.imageToVideo?.failed || 0 }}</div>
</div>
<div class="stat-item">
<div class="stat-label">{{ $t('systemSettings.archivedTasks') }}</div>
<div class="stat-value">{{ cleanupStats.archives?.completedTasks || 0 }}</div>
</div>
<div class="stat-item">
<div class="stat-label">{{ $t('systemSettings.cleanupLogsCount') }}</div>
<div class="stat-value">{{ cleanupStats.archives?.cleanupLogs || 0 }}</div>
</div>
<div class="stat-item">
<div class="stat-label">{{ $t('systemSettings.retentionDays') }}</div>
<div class="stat-value">{{ cleanupStats.config?.retentionDays || 30 }}{{ $t('systemSettings.days') }}</div>
</div>
</div>
</div>
</el-card>
</div>
<!-- 清理操作 -->
<div class="cleanup-actions">
<el-card class="actions-card">
<template #header>
<div class="card-header">
<h3>{{ $t('systemSettings.cleanupActions') }}</h3>
</div>
</template>
<div class="actions-content">
<div class="action-buttons">
<el-button
type="primary"
@click="performFullCleanup"
:loading="loadingCleanup"
class="action-btn"
>
<el-icon><Delete /></el-icon>
{{ $t('systemSettings.performFullCleanup') }}
</el-button>
<el-button
type="warning"
@click="showUserCleanupDialog = true"
class="action-btn"
>
<el-icon><User /></el-icon>
{{ $t('systemSettings.cleanupUserTasks') }}
</el-button>
</div>
<div class="action-description">
<p><strong>{{ $t('systemSettings.fullCleanupDesc') }}</strong>{{ $t('systemSettings.fullCleanupDescDetail') }}</p>
<p><strong>{{ $t('systemSettings.userCleanupDesc') }}</strong>{{ $t('systemSettings.userCleanupDescDetail') }}</p>
</div>
</div>
</el-card>
</div>
<!-- 清理配置 -->
<div class="cleanup-config">
<el-card class="config-card">
<template #header>
<div class="card-header">
<h3>{{ $t('systemSettings.cleanupConfig') }}</h3>
</div>
</template>
<div class="config-content">
<el-form :model="cleanupConfig" label-width="120px">
<el-form-item :label="$t('systemSettings.taskRetentionDays')">
<el-input-number
v-model="cleanupConfig.retentionDays"
:min="1"
:max="365"
controls-position="right"
/>
<span class="config-tip">{{ $t('systemSettings.taskRetentionTip') }}</span>
</el-form-item>
<el-form-item :label="$t('systemSettings.archiveRetentionDays')">
<el-input-number
v-model="cleanupConfig.archiveRetentionDays"
:min="30"
:max="3650"
controls-position="right"
/>
<span class="config-tip">{{ $t('systemSettings.archiveRetentionTip') }}</span>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveCleanupConfig" :loading="loadingConfig">
{{ $t('common.save') }}
</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
</div>
</div>
<!-- AI模型设置选项卡 -->
<div v-if="activeTab === 'aiModel'" class="tab-content">
<h2 class="page-title">{{ $t('systemSettings.aiModel') }}</h2>
<el-card class="ai-model-card">
<template #header>
<div class="card-header">
<h3>{{ $t('systemSettings.promptOptimization') }}</h3>
</div>
</template>
<div class="ai-model-content">
<el-form label-width="180px">
<el-form-item :label="$t('systemSettings.promptOptimizationModel')">
<el-input v-model="promptOptimizationModel" style="width: 400px;" placeholder="gpt-5.1-thinking"></el-input>
<div class="model-tip">{{ $t('systemSettings.promptOptimizationModelTip') }}</div>
</el-form-item>
<el-form-item :label="$t('systemSettings.storyboardSystemPrompt')">
<el-input
v-model="storyboardSystemPrompt"
type="textarea"
:rows="4"
style="width: 500px;"
:placeholder="$t('systemSettings.storyboardSystemPromptPlaceholder')">
</el-input>
<div class="model-tip">{{ $t('systemSettings.storyboardSystemPromptTip') }}</div>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="saveAiModelSettings"
:loading="savingAiModel"
class="ai-save-btn"
>
<el-icon v-if="!savingAiModel"><Check /></el-icon>
{{ $t('common.save') }}
</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
</div>
</div>
</main>
<!-- 编辑会员收费标准对话框 -->
<el-dialog
v-model="editDialogVisible"
width="480px"
:before-close="handleCloseEditDialog"
class="membership-modal"
:show-close="false"
>
<template #header>
<div class="modal-header">
<h2 class="modal-title">{{ $t('systemSettings.membership') }}</h2>
<button class="close-btn" @click="handleCloseEditDialog">×</button>
</div>
</template>
<div class="modal-content">
<el-form :model="editForm" :rules="editRules" ref="editFormRef">
<div class="form-group">
<label class="form-label">{{ $t('systemSettings.membershipLevel') }}</label>
<el-select v-model="editForm.level" :placeholder="$t('systemSettings.selectLevelPlaceholder')" style="width: 100%;" disabled>
<el-option :label="$t('systemSettings.freeMembership')" value="free"></el-option>
<el-option :label="$t('systemSettings.standardMembership')" value="standard"></el-option>
<el-option :label="$t('systemSettings.professionalMembership')" value="professional"></el-option>
</el-select>
</div>
<div class="form-group">
<label class="form-label">{{ $t('systemSettings.membershipPrice') }}</label>
<div class="price-input">
<span class="price-prefix">¥</span>
<input
type="text"
v-model="editForm.price"
placeholder="0.00"
class="form-control"
@input="handlePriceInput"
/>
</div>
</div>
<div class="form-group">
<label class="form-label">{{ $t('systemSettings.resourcePointsAmount') }}</label>
<input
type="number"
v-model="editForm.resourcePoints"
placeholder="0"
min="0"
class="form-control"
/>
</div>
<div class="form-group">
<label class="form-label">{{ $t('systemSettings.validityPeriod') }}</label>
<div class="radio-group">
<div class="radio-option">
<input
type="radio"
id="yearly"
v-model="editForm.validityPeriod"
value="yearly"
class="radio-input"
>
<label for="yearly" class="radio-label">{{ $t('systemSettings.yearly') }}</label>
</div>
</div>
</div>
</el-form>
</div>
<template #footer>
<div class="modal-footer">
<button class="btn btn-cancel" @click="handleCloseEditDialog">{{ $t('common.cancel') }}</button>
<button class="btn btn-save" @click="saveEdit">{{ $t('common.save') }}</button>
</div>
</template>
</el-dialog>
<!-- 用户清理对话框 -->
<el-dialog
v-model="showUserCleanupDialog"
:title="$t('systemSettings.cleanupUserTasks')"
width="480px"
:before-close="handleCloseUserCleanupDialog"
>
<div class="user-cleanup-content">
<el-form :model="userCleanupForm" :rules="userCleanupRules" ref="userCleanupFormRef">
<el-form-item :label="$t('members.username')" prop="username">
<el-input
v-model="userCleanupForm.username"
:placeholder="$t('systemSettings.enterUsername')"
clearable
/>
</el-form-item>
<el-form-item>
<el-alert
:title="$t('systemSettings.warning')"
type="warning"
:closable="false"
show-icon
>
<template #default>
<p>{{ $t('systemSettings.cleanupWarning') }}</p>
<ul>
<li>{{ $t('systemSettings.successTasksArchived') }}</li>
<li>{{ $t('systemSettings.failedTasksLogged') }}</li>
<li>{{ $t('systemSettings.originalTasksDeleted') }}</li>
</ul>
<p><strong>{{ $t('systemSettings.irreversibleWarning') }}</strong></p>
</template>
</el-alert>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCloseUserCleanupDialog">{{ $t('common.cancel') }}</el-button>
<el-button
type="danger"
@click="performUserCleanup"
:loading="loadingUserCleanup"
>
{{ $t('systemSettings.confirmCleanup') }}
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useI18n } from 'vue-i18n'
import {
Grid,
User,
ShoppingCart,
Document,
Setting,
Search,
ArrowDown,
Delete,
Refresh,
Check,
Warning
} from '@element-plus/icons-vue'
import api from '@/api/request'
import cleanupApi from '@/api/cleanup'
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
const router = useRouter()
const { t } = useI18n()
// 选项卡状态
const activeTab = ref('membership')
// 系统状态数据
const onlineUsers = ref('0')
const systemUptime = ref(t('nav.loading'))
// 会员收费标准相关
const membershipLevels = ref([])
const loadingLevels = ref(false)
const editDialogVisible = ref(false)
const editFormRef = ref(null)
const editForm = reactive({
id: null,
level: '',
price: '',
resourcePoints: 0,
validityPeriod: 'yearly'
})
const editRules = computed(() => ({
level: [{ required: true, message: t('systemSettings.selectLevelRequired'), trigger: 'change' }],
price: [
{ required: true, message: t('systemSettings.enterPriceRequired'), trigger: 'blur' },
{ pattern: /^\d+(\.\d+)?$/, message: t('systemSettings.enterValidNumber'), trigger: 'blur' }
],
resourcePoints: [{ required: true, message: t('systemSettings.enterResourcePointsRequired'), trigger: 'blur' }],
validityPeriod: [{ required: true, message: t('systemSettings.selectValidityRequired'), trigger: 'change' }]
}))
// 任务清理相关
const cleanupStats = ref(null)
const loadingStats = ref(false)
const loadingCleanup = ref(false)
const loadingUserCleanup = ref(false)
const loadingConfig = ref(false)
const showUserCleanupDialog = ref(false)
const userCleanupFormRef = ref(null)
const userCleanupForm = reactive({
username: ''
})
const userCleanupRules = computed(() => ({
username: [
{ required: true, message: t('systemSettings.enterUsernameRequired'), trigger: 'blur' },
{ min: 2, max: 50, message: t('systemSettings.usernameLengthLimit'), trigger: 'blur' }
]
}))
const cleanupConfig = reactive({
retentionDays: 30,
archiveRetentionDays: 365
})
// AI模型设置相关
const promptOptimizationModel = ref('gpt-5.1-thinking')
const storyboardSystemPrompt = ref('')
const savingAiModel = ref(false)
const goToDashboard = () => {
router.push('/admin/dashboard')
}
const goToMembers = () => {
router.push('/member-management')
}
const goToOrders = () => {
router.push('/admin/orders')
}
const goToAPI = () => {
router.push('/api-management')
}
const goToTasks = () => {
router.push('/generate-task-record')
}
const goToErrorStats = () => {
router.push('/admin/error-statistics')
}
const goToSettings = () => {
router.push('/system-settings')
}
// 处理用户头像下拉菜单
const handleUserCommand = (command) => {
if (command === 'exitAdmin') {
// 退出后台,返回个人首页
router.push('/profile')
}
}
const editLevel = (level) => {
// 映射后端数据到前端表单
editForm.id = level.id
editForm.level = level.key
editForm.price = level.price ? String(level.price) : '0'
editForm.resourcePoints = level.pointsBonus || level.resourcePoints || 0
editForm.validityPeriod = 'yearly' // 默认年付
editDialogVisible.value = true
}
const handleCloseEditDialog = () => {
editDialogVisible.value = false
if (editFormRef.value) {
editFormRef.value.resetFields()
}
}
const handlePriceInput = (value) => {
// 确保只输入数字
editForm.price = value.replace(/[^\d.]/g, '')
}
const saveEdit = async () => {
const valid = await editFormRef.value.validate()
if (!valid) return
try {
const priceInt = parseInt(editForm.price)
if (Number.isNaN(priceInt) || priceInt < 0) {
ElMessage.error(t('systemSettings.enterValidNumber'))
return
}
const pointsInt = parseInt(editForm.resourcePoints)
if (Number.isNaN(pointsInt) || pointsInt < 0) {
ElMessage.error(t('systemSettings.enterValidNumber'))
return
}
// 直接更新membership_levels表
const updateData = { price: priceInt, pointsBonus: pointsInt }
console.log('准备更新会员等级:', editForm.id, updateData)
const response = await api.put(`/members/levels/${editForm.id}`, updateData)
console.log('会员等级更新响应:', response.data)
if (response.data?.success) {
ElMessage.success(t('systemSettings.membershipUpdateSuccess'))
editDialogVisible.value = false
await loadMembershipLevels()
} else {
ElMessage.error(t('systemSettings.membershipUpdateFailed') + ': ' + (response.data?.message || '未知错误'))
}
} catch (error) {
console.error('Update membership level failed:', error)
ElMessage.error(t('systemSettings.membershipUpdateFailed') + ': ' + (error.response?.data?.message || error.message))
}
}
// 加载会员等级配置从membership_levels表读取
const loadMembershipLevels = async () => {
loadingLevels.value = true
try {
// 从membership_levels表读取数据
const levelsResp = await api.get('/members/levels', {
params: { _t: Date.now() },
headers: { 'Cache-Control': 'no-cache' }
})
if (levelsResp.data?.success && levelsResp.data?.data) {
const levels = levelsResp.data.data
membershipLevels.value = levels.map(level => ({
id: level.id,
key: level.name,
name: level.displayName || level.name,
price: level.price || 0,
resourcePoints: level.pointsBonus || 0,
pointsBonus: level.pointsBonus || 0,
description: t('systemSettings.includesPointsPerMonth', { points: level.pointsBonus || 0 })
}))
} else {
throw new Error('获取会员等级数据失败')
}
} catch (error) {
console.error('Load membership config failed:', error)
console.error('Error details:', error.response?.data || error.message)
// 显示更详细的错误信息
const errorMessage = error.response?.data?.message || error.response?.data?.error || error.message || t('systemSettings.unknown')
ElMessage.warning(`${t('systemSettings.loadMembershipFailed')}: ${errorMessage}, ${t('systemSettings.usingDefaultConfig')}`)
// API调用失败清空数据并提示用户检查数据库配置
membershipLevels.value = []
ElMessage.error('无法加载会员等级配置请检查数据库中membership_levels表是否有数据')
} finally {
loadingLevels.value = false
}
}
// 任务清理相关方法
const getAuthHeaders = () => {
const token = localStorage.getItem('token')
return token ? { 'Authorization': `Bearer ${token}` } : {}
}
const refreshStats = async (showMessage = true) => {
loadingStats.value = true
try {
const response = await cleanupApi.getCleanupStats()
cleanupStats.value = response.data
if (showMessage) {
ElMessage.success(t('systemSettings.statsRefreshSuccess'))
}
} catch (error) {
console.error('Get statistics failed:', error)
ElMessage.error(t('systemSettings.statsRefreshFailed'))
} finally {
loadingStats.value = false
}
}
const performFullCleanup = async () => {
loadingCleanup.value = true
try {
const response = await cleanupApi.performFullCleanup()
ElMessage.success(t('systemSettings.fullCleanupSuccess'))
console.log('Cleanup result:', response.data)
// 刷新统计信息
await refreshStats()
} catch (error) {
console.error('Execute full cleanup failed:', error)
ElMessage.error(t('systemSettings.fullCleanupFailed'))
} finally {
loadingCleanup.value = false
}
}
const handleCloseUserCleanupDialog = () => {
showUserCleanupDialog.value = false
if (userCleanupFormRef.value) {
userCleanupFormRef.value.resetFields()
}
}
const performUserCleanup = async () => {
const valid = await userCleanupFormRef.value.validate()
if (!valid) return
loadingUserCleanup.value = true
try {
const response = await cleanupApi.cleanupUserTasks(userCleanupForm.username)
ElMessage.success(t('systemSettings.userCleanupSuccess'))
console.log('Cleanup result:', response.data)
// 刷新统计信息
await refreshStats()
// 关闭对话框
handleCloseUserCleanupDialog()
} catch (error) {
console.error('Cleanup user tasks failed:', error)
ElMessage.error(t('systemSettings.userCleanupFailed'))
} finally {
loadingUserCleanup.value = false
}
}
const performUserCleanup_old = async () => {
const valid = await userCleanupFormRef.value.validate()
if (!valid) return
loadingUserCleanup.value = true
try {
const response = await fetch(`/api/cleanup/user-tasks/${userCleanupForm.username}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders()
}
})
if (response.ok) {
const result = await response.json()
ElMessage.success(t('systemSettings.userCleanupSuccess'))
console.log('User cleanup result:', result)
// 关闭对话框并刷新统计信息
handleCloseUserCleanupDialog()
await refreshStats()
} else {
ElMessage.error(t('systemSettings.userCleanupFailed'))
}
} catch (error) {
console.error('Cleanup user tasks failed:', error)
ElMessage.error(t('systemSettings.userCleanupFailed'))
} finally {
loadingUserCleanup.value = false
}
}
const saveCleanupConfig = async () => {
loadingConfig.value = true
try {
// 这里可以添加保存配置的API调用
// 目前只是模拟保存
await new Promise(resolve => setTimeout(resolve, 1000))
ElMessage.success(t('systemSettings.configSaveSuccess'))
} catch (error) {
console.error('Save cleanup config failed:', error)
ElMessage.error(t('systemSettings.configSaveFailed'))
} finally {
loadingConfig.value = false
}
}
// 加载AI模型设置
const loadAiModelSettings = async () => {
try {
const response = await fetch('/api/admin/settings', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
if (response.ok) {
const data = await response.json()
if (data.promptOptimizationModel) {
promptOptimizationModel.value = data.promptOptimizationModel
}
if (data.storyboardSystemPrompt !== undefined) {
storyboardSystemPrompt.value = data.storyboardSystemPrompt
}
}
} catch (error) {
console.error('Load AI model settings failed:', error)
}
}
// 保存AI模型设置
const saveAiModelSettings = async () => {
savingAiModel.value = true
try {
const response = await fetch('/api/admin/settings', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({
promptOptimizationModel: promptOptimizationModel.value,
storyboardSystemPrompt: storyboardSystemPrompt.value
})
})
if (response.ok) {
ElMessage.success(t('systemSettings.aiModelSaveSuccess'))
} else {
throw new Error('Save failed')
}
} catch (error) {
console.error('Save AI model settings failed:', error)
ElMessage.error(t('systemSettings.aiModelSaveFailed'))
} finally {
savingAiModel.value = false
}
}
// 页面加载时获取统计信息和会员等级配置
onMounted(() => {
refreshStats(false) // 初始加载不显示成功提示
loadMembershipLevels()
fetchSystemStats()
loadAiModelSettings()
})
// 获取系统统计数据(当天访问人数和系统运行时间)
const fetchSystemStats = async () => {
try {
const response = await fetch('/api/admin/online-stats', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
const data = await response.json()
if (data.success) {
onlineUsers.value = data.todayVisitors || 0
systemUptime.value = data.uptime || t('systemSettings.unknown')
} else {
onlineUsers.value = '0'
systemUptime.value = t('systemSettings.unknown')
}
} catch (error) {
console.error('Get online stats failed:', error)
onlineUsers.value = '0'
systemUptime.value = t('systemSettings.unknown')
}
}
</script>
<style scoped>
.system-settings {
display: flex;
height: 100vh;
background-color: #f5f7fa;
font-family: 'Arial', sans-serif;
}
/* 左侧导航栏 */
.sidebar {
width: 240px;
background: white;
border-right: 1px solid #e9ecef;
display: flex;
flex-direction: column;
padding: 24px 0;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.05);
}
.logo {
display: flex;
align-items: center;
justify-content: center;
padding: 0 24px;
margin-bottom: 32px;
}
.logo img {
width: 100%;
height: auto;
max-width: 180px;
object-fit: contain;
}
.nav-menu {
flex: 1;
padding: 0 16px;
}
.nav-item {
display: flex;
align-items: center;
padding: 12px 16px;
margin-bottom: 4px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
color: #6b7280;
font-size: 14px;
font-weight: 500;
}
.nav-item:hover {
background: #f3f4f6;
color: #374151;
}
.nav-item.active {
background: #dbeafe;
color: #3b82f6;
}
.nav-item .el-icon {
margin-right: 12px;
font-size: 18px;
}
.nav-item span {
font-size: 14px;
font-weight: 500;
}
.sidebar-footer {
padding: 20px;
border-top: 1px solid #e9ecef;
background: #f8f9fa;
margin-top: auto;
}
.online-users,
.system-uptime {
font-size: 13px;
color: #6b7280;
margin-bottom: 8px;
line-height: 1.5;
}
.highlight {
color: #3b82f6;
font-weight: 600;
}
.main-content {
flex-grow: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.top-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 30px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
z-index: 100;
}
.page-title h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #1f2937;
}
.header-actions {
display: flex;
align-items: center;
gap: 20px;
}
.user-avatar {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
transition: background 0.2s ease;
}
.user-avatar:hover {
background: #f3f4f6;
}
.user-avatar img {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
.user-avatar .arrow-down {
font-size: 12px;
color: #6b7280;
}
.content-section {
flex-grow: 1;
padding: 30px;
background-color: #f5f7fa;
}
/* 设置选项卡样式 */
.settings-tabs {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.tab-nav {
display: flex;
background: white;
border-bottom: 1px solid #e2e8f0;
padding: 0 30px;
}
.tab-item {
display: flex;
align-items: center;
padding: 20px 24px;
margin-right: 8px;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s ease;
color: #64748b;
font-size: 16px;
font-weight: 500;
}
.tab-item:hover {
color: #334155;
background: #f8fafc;
}
.tab-item.active {
color: #3b82f6;
border-bottom-color: #3b82f6;
background: #eff6ff;
}
.tab-item .el-icon {
margin-right: 8px;
font-size: 18px;
}
.tab-content {
flex-grow: 1;
padding: 30px;
background-color: #f5f7fa;
}
/* 清理功能样式 */
.cleanup-stats,
.cleanup-actions,
.cleanup-config {
margin-bottom: 24px;
}
.stats-card,
.actions-card,
.config-card {
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0;
}
.card-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #1e293b;
}
.stats-content {
padding: 20px 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.stat-item {
text-align: center;
padding: 20px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.stat-label {
font-size: 14px;
color: #64748b;
margin-bottom: 8px;
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: #1e293b;
}
.actions-content {
padding: 20px 0;
}
.action-buttons {
display: flex;
gap: 16px;
margin-bottom: 20px;
}
.action-btn {
min-width: 160px;
}
.action-description {
padding: 16px;
background: #f8fafc;
border-radius: 8px;
border-left: 4px solid #3b82f6;
}
.action-description p {
margin: 0 0 8px 0;
font-size: 14px;
color: #64748b;
line-height: 1.5;
}
.action-description p:last-child {
margin-bottom: 0;
}
.config-content {
padding: 20px 0;
}
.config-tip {
margin-left: 12px;
font-size: 12px;
color: #94a3b8;
}
/* 用户清理对话框样式 */
.user-cleanup-content {
padding: 20px 0;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.page-title {
font-size: 24px;
color: #333;
margin-bottom: 25px;
font-weight: bold;
}
.membership-cards {
display: flex;
gap: 25px;
flex-wrap: wrap;
}
.membership-card {
flex: 1;
min-width: 280px;
max-width: 350px;
border-radius: 12px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.membership-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
}
.card-header {
padding: 20px;
text-align: center;
border-bottom: 1px solid #eee;
}
.card-header h3 {
font-size: 22px;
color: #333;
margin: 0;
}
.card-body {
padding: 25px 20px;
text-align: center;
flex-grow: 1;
}
.card-body .price {
font-size: 36px;
font-weight: bold;
color: #409eff;
margin-bottom: 10px;
}
.card-body .description {
font-size: 15px;
color: #606266;
line-height: 1.6;
}
.card-footer {
padding: 20px;
text-align: center;
border-top: 1px solid #eee;
}
.el-button {
width: 80%;
padding: 12px 0;
font-size: 16px;
border-radius: 8px;
}
/* 会员收费标准模态框样式 - 完全匹配HTML代码 */
.membership-modal {
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
border: 1px solid #e1e5eb;
overflow: hidden;
}
.membership-modal .el-dialog__body {
padding: 0;
}
/* 弹窗头部 */
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #f0f2f5;
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin: 0;
}
.close-btn {
width: 32px;
height: 32px;
border: none;
background: #f5f7fa;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #666;
transition: all 0.2s;
}
.close-btn:hover {
background: #e6f3ff;
color: #1890ff;
}
/* 表单内容区域 */
.modal-content {
padding: 24px;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: #333;
font-weight: 500;
}
/* 输入框和下拉框样式 */
.form-control {
width: 100%;
height: 40px;
padding: 0 12px;
border: 1px solid #d9d9d9;
border-radius: 6px;
font-size: 14px;
color: #333;
transition: all 0.2s;
background-color: white;
}
.form-control:focus {
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.form-control::placeholder {
color: #bfbfbf;
}
/* 价格输入框特殊样式 */
.price-input {
position: relative;
}
.price-prefix {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #666;
z-index: 10;
}
.price-input .form-control {
padding-left: 30px;
}
/* 单选按钮组 */
.radio-group {
display: flex;
gap: 16px;
}
.radio-option {
display: flex;
align-items: center;
}
.radio-input {
display: none;
}
.radio-label {
display: flex;
align-items: center;
cursor: pointer;
font-size: 14px;
color: #666;
padding: 8px 16px;
border: 1px solid #d9d9d9;
border-radius: 6px;
transition: all 0.2s;
}
.radio-input:checked + .radio-label {
border-color: #1890ff;
background-color: #e6f7ff;
color: #1890ff;
}
/* 按钮区域 */
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 20px 24px;
border-top: 1px solid #f0f2f5;
}
.btn {
padding: 0 20px;
height: 36px;
border: 1px solid;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-cancel {
background: white;
border-color: #d9d9d9;
color: #666;
}
.btn-cancel:hover {
border-color: #1890ff;
color: #1890ff;
}
.btn-save {
background: #1890ff;
border-color: #1890ff;
color: white;
}
.btn-save:hover {
background: #40a9ff;
border-color: #40a9ff;
}
/* AI模型设置保存按钮样式 */
.ai-save-btn {
width: auto !important;
min-width: 140px;
padding: 12px 32px !important;
font-size: 15px !important;
font-weight: 500;
border-radius: 8px !important;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
border: none !important;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
transition: all 0.3s ease !important;
display: inline-flex;
align-items: center;
gap: 8px;
}
.ai-save-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
background: linear-gradient(135deg, #5a6fd6 0%, #6a4190 100%) !important;
}
.ai-save-btn:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.4);
}
.ai-save-btn .el-icon {
font-size: 16px;
}
/* 响应式调整 */
@media (max-width: 480px) {
.membership-modal {
border-radius: 0;
}
.modal-content {
padding: 16px;
}
.radio-group {
flex-direction: column;
gap: 8px;
}
}
/* Responsive adjustments */
@media (max-width: 1200px) {
.membership-card {
max-width: none;
}
}
@media (max-width: 768px) {
.sidebar {
width: 60px;
padding: 15px 0;
}
.sidebar .logo span,
.sidebar .nav-item span,
.sidebar-footer {
display: none;
}
.sidebar .logo {
padding: 0 10px 20px;
}
.sidebar .logo-icon {
margin-right: 0;
}
.nav-menu {
padding: 0 10px;
}
.nav-item {
justify-content: center;
padding: 10px;
}
.nav-item .el-icon {
margin-right: 0;
}
.top-header {
padding: 10px 20px;
}
.content-section {
padding: 20px;
}
.page-title {
font-size: 20px;
margin-bottom: 20px;
}
.membership-cards {
flex-direction: column;
align-items: center;
}
.membership-card {
width: 90%;
max-width: 400px;
}
}
</style>