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

551 lines
18 KiB
Vue
Raw Normal View History

<template>
<div class="api-management">
<!-- 左侧导航栏 -->
<aside class="sidebar">
<div class="logo">
2025-11-13 17:01:39 +08:00
<div class="logo-icon">
<svg viewBox="0 0 194 41" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M54.6165 13.6062L60.593 32.3932H60.8224L66.8111 13.6062H72.6065L64.0824 38.3335H57.3452L48.8089 13.6062H54.6165ZM75.4862 38.3335V19.788H80.6296V38.3335H75.4862ZM78.07 17.3974C77.3053 17.3974 76.6493 17.1439 76.1019 16.6368C75.5626 16.1216 75.293 15.5058 75.293 14.7895C75.293 14.0811 75.5626 13.4734 76.1019 12.9663C76.6493 12.4512 77.3053 12.1936 78.07 12.1936C78.8346 12.1936 79.4866 12.4512 80.0259 12.9663C80.5733 13.4734 80.8469 14.0811 80.8469 14.7895C80.8469 15.5058 80.5733 16.1216 80.0259 16.6368C79.4866 17.1439 78.8346 17.3974 78.07 17.3974ZM91.5836 38.6353C90.175 38.6353 88.8992 38.2731 87.7562 37.5487C86.6213 36.8162 85.7198 35.7416 85.0517 34.325C84.3916 32.9003 84.0616 31.1536 84.0616 29.0849C84.0616 26.9599 84.4037 25.1931 85.0879 23.7845C85.7721 22.3678 86.6816 21.3093 87.8166 20.6091C88.9596 19.9007 90.2112 19.5466 91.5716 19.5466C92.6099 19.5466 93.4752 19.7236 94.1674 20.0778C94.8677 20.4239 95.4312 20.8586 95.8578 21.3818C96.2924 21.8969 96.6225 22.404 96.8478 22.9031H97.0048V13.6062H102.136V38.3335H97.0652V35.3633H96.8478C96.6064 35.8785 96.2643 36.3896 95.8216 36.8967C95.3869 37.3958 94.8194 37.8103 94.1191 38.1403C93.4269 38.4703 92.5817 38.6353 91.5836 38.6353ZM93.2136 34.5423C94.0427 34.5423 94.743 34.3169 95.3145 33.8662C95.894 33.4074 96.3367 32.7674 96.6426 31.9464C96.9565 31.1254 97.1135 30.1635 97.1135 29.0608C97.1135 27.958 96.9605 27.0002 96.6547 26.1872C96.3488 25.3742 95.9061 24.7464 95.3265 24.3037C94.747 23.861 94.0427 23.6396 93.2136 23.6396C92.3684 23.6396 91.6561 23.869 91.0765 24.3278C90.497 24.7866 90.0583 25.4225 89.7605 26.2355C89.4627 27.0485 89.3137 27.9902 89.3137 29.0608C89.3137 30.1394 89.4627 31.0932 89.7605 31.9223C90.0663 32.7433 90.505 33.3872 91.0765 33.8541C91.6561 34.3129 92.3684 34.5423 93.2136 34.5423ZM106.462 38.3335V13.6062H122.834V17.9166H111.69V23.8086H121.747V28.119H111.69V38.3335H106.462ZM131.397 13.6062V38.3335H126.254V13.6062H131.397ZM143.897 38.6957C142.021 38.6957 140.399 38.2973 139.031 37.5004C137.671 36.6955 136.62 35.5766 135.88 34.1439C135.139 32.7031 134.769 31.0328 134.769 29.1332C134.769 27.2175 135.139 25.5432 135.88 24.1105C136.62 22.6697 137.671 21.5508 139.031 20.7539C140.399 19.949 142.021 19.5466 143.897 19.5466C145.772 19.5466 147.39 19.949 148.75 20.7539C150.119 21.5508 151.173 22.6697 151.914 24.1105C152.654 25.5432 153.025 27.2175 153.025 29.1332C153.025 31.0328 152.654 32.7031 151.914 34.1439C151.173 35.5766 150.119 36.6955 148.75 37.5004C147.39 38.2973 145.772 38.6957 143.897 38.6957ZM143.921 34.7113C144.774 34.7113 145.486 34.4699 146.058 33.9869C146.629 33.4959 147.06 32.8278 147.35 31.9826C147.648 31.1375 147.797 30.1756 147.797 29.097C147.797 28.0184 147.648 27.0565 147.35 26.2113C147.06 25.3662 146.629 24.6981 146.058 24.2071C145.486 23.7161 144.774 23.4706 143.921 23.4706C143.06 23.4706 142.335 23.7161 141.748 24.2071C141.168 24.6981 140.729 25.3662 140.431 26.2113C140.142 27.0565 139.997 28.0184 139.997 29.097C139.997 30.1756 140.142 31.1375 140.431 31.9826C140.729 32.8278 141.168 33.4959 141.748 33.9869C142.335 34.4699 143.06 34.7113 143.921 34.7113ZM159.562 38.3335L154.516 19.788H159.719L162.593 32.2483H162.762L165.756 19.788H170.864L173.906 32.1758H174.063L176.888 19.788H182.08L177.045 38.3335H171.6L168.413 26.6701H168.183L164.996 38.3335H159.562Z" fill="white"/>
<g clip-path="url(#clip0)">
<path d="M5.7406 1.64568C2.43935 1.64568 0.00938034 1.57298 0.000366208 1.64568C0.000366202 1.71906 2.11292 5.37389 4.70642 9.58611C7.28533 13.813 12.3557 22.0905 15.9545 27.9611L20.8685 35.9875C21.8796 37.6388 23.6773 38.6457 25.6136 38.6457L26.4301 38.6457C26.4301 38.6457 31.0568 38.7204 31.9965 37.2228C32.2255 36.8288 32.346 36.3807 32.3461 35.925L32.3461 21.8996L31.1801 21.8996L31.1801 26.5969C31.1801 31.1005 31.1655 31.3074 30.889 31.5861C30.7288 31.7476 30.4663 31.8801 30.306 31.8801C29.9855 31.8801 29.7811 31.5866 27.1439 27.257C26.2551 25.8039 24.0844 22.2371 22.2924 19.3312C20.5148 16.4253 17.3534 11.2596 15.2699 7.85467L11.4672 1.64568L5.7406 1.64568ZM31.6615 2.99627C31.443 4.1703 30.525 6.06354 29.6654 7.10564C28.4561 8.60263 26.4304 9.83531 24.5072 10.2316C23.4727 10.4518 23.196 10.5988 23.808 10.5988C24.0267 10.5989 24.5801 10.7012 25.0316 10.8332C28.2662 11.7578 31.0495 14.7674 31.6468 17.9816L31.8363 18.9797L32.0111 18.1135C32.5648 15.3249 34.4443 12.8151 36.9066 11.5676C38.0139 10.9952 39.2815 10.5988 39.9808 10.5988C40.6217 10.5988 40.403 10.4957 39.2084 10.2463C37.46 9.87934 36.1049 9.11623 34.6625 7.66326C33.2638 6.26901 32.5496 5.02127 32.0834 3.17205L31.8217 2.11541L31.6615 2.99627Z" fill="url(#paint0)"/>
</g>
<defs>
<linearGradient id="paint0" x1="23.4862" y1="5.3947" x2="32.9093" y2="11.1248" gradientUnits="userSpaceOnUse">
<stop offset="0.0001" stop-color="#33DDE5"/>
<stop offset="1" stop-color="#0F9CFF"/>
</linearGradient>
<clipPath id="clip0">
<rect width="40.3334" height="40.3334" fill="white"/>
</clipPath>
</defs>
</svg>
</div>
</div>
<nav class="nav-menu">
<div class="nav-item" @click="goToDashboard">
<el-icon><Grid /></el-icon>
2025-11-13 17:01:39 +08:00
<span>{{ $t('nav.dashboard') }}</span>
</div>
<div class="nav-item" @click="goToMembers">
<el-icon><User /></el-icon>
2025-11-13 17:01:39 +08:00
<span>{{ $t('nav.members') }}</span>
</div>
<div class="nav-item" @click="goToOrders">
<el-icon><ShoppingCart /></el-icon>
2025-11-13 17:01:39 +08:00
<span>{{ $t('nav.orders') }}</span>
</div>
<div class="nav-item active">
<el-icon><Document /></el-icon>
2025-11-13 17:01:39 +08:00
<span>{{ $t('nav.apiManagement') }}</span>
</div>
<div class="nav-item" @click="goToTasks">
<el-icon><Document /></el-icon>
2025-11-13 17:01:39 +08:00
<span>{{ $t('nav.tasks') }}</span>
</div>
<div class="nav-item" @click="goToSettings">
<el-icon><Setting /></el-icon>
2025-11-13 17:01:39 +08:00
<span>{{ $t('nav.systemSettings') }}</span>
</div>
</nav>
<div class="sidebar-footer">
<div class="online-users">
2025-11-13 17:01:39 +08:00
{{ $t('nav.onlineUsers') }}: <span class="highlight">{{ onlineUsers }}</span>
</div>
<div class="system-uptime">
2025-11-13 17:01:39 +08:00
{{ $t('nav.systemUptime') }}: <span class="highlight">{{ systemUptime }}</span>
</div>
</div>
</aside>
<!-- 主内容区域 -->
<main class="main-content">
<!-- 顶部搜索栏 -->
<header class="top-header">
<div class="search-bar">
<el-icon class="search-icon"><Search /></el-icon>
2025-11-13 17:01:39 +08:00
<input type="text" :placeholder="$t('common.searchPlaceholder')" class="search-input" />
</div>
<div class="header-actions">
2025-11-13 17:01:39 +08:00
<LanguageSwitcher />
<div class="user-avatar">
2025-11-13 17:01:39 +08:00
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
<el-icon class="arrow-down"><ArrowDown /></el-icon>
</div>
</div>
</header>
<!-- API密钥输入内容 -->
<section class="api-content">
<div class="content-header">
2025-11-13 17:01:39 +08:00
<h2>{{ $t('apiManagement.title') }}</h2>
</div>
<div class="api-form-container">
<el-form :model="apiForm" label-width="120px" class="api-form">
2025-11-13 17:01:39 +08:00
<el-form-item :label="$t('apiManagement.apiKey')">
<el-input
v-model="apiForm.apiKey"
type="password"
2025-11-13 17:01:39 +08:00
:placeholder="$t('apiManagement.apiKeyPlaceholder')"
show-password
style="width: 100%; max-width: 600px;"
/>
</el-form-item>
2025-11-13 17:01:39 +08:00
<el-form-item :label="$t('apiManagement.tokenExpiration')">
<div style="display: flex; align-items: center; gap: 12px; width: 100%; max-width: 600px;">
<el-input
v-model.number="apiForm.jwtExpirationHours"
type="number"
2025-11-13 17:01:39 +08:00
:placeholder="$t('apiManagement.tokenPlaceholder')"
style="flex: 1;"
:min="1"
:max="720"
/>
2025-11-13 17:01:39 +08:00
<span style="color: #6b7280; font-size: 14px;">{{ $t('apiManagement.hours') }}</span>
<span style="color: #9ca3af; font-size: 12px;" v-if="apiForm.jwtExpirationHours">
({{ formatJwtExpiration(apiForm.jwtExpirationHours) }})
</span>
</div>
<div style="margin-top: 8px; color: #6b7280; font-size: 12px;">
2025-11-13 17:01:39 +08:00
{{ $t('apiManagement.rangeHint') }}
</div>
</el-form-item>
<el-form-item>
2025-11-13 17:01:39 +08:00
<el-button type="primary" @click="saveApiKey" :loading="saving">{{ $t('common.save') }}</el-button>
<el-button @click="resetForm">{{ $t('common.reset') }}</el-button>
</el-form-item>
</el-form>
</div>
</section>
</main>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
2025-11-13 17:01:39 +08:00
import { useI18n } from 'vue-i18n'
import {
Grid,
User,
ShoppingCart,
Document,
Setting,
Search,
ArrowDown
} from '@element-plus/icons-vue'
import api from '@/api/request'
2025-11-13 17:01:39 +08:00
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
const router = useRouter()
2025-11-13 17:01:39 +08:00
const { t } = useI18n()
const saving = ref(false)
const loading = ref(false)
2025-11-13 17:01:39 +08:00
// 系统状态数据
const onlineUsers = ref('0/500')
const systemUptime = ref('加载中...')
const apiForm = reactive({
apiKey: '',
jwtExpirationHours: 24 // 默认24小时
})
// 导航功能
const goToDashboard = () => {
router.push('/admin/dashboard')
}
const goToMembers = () => {
router.push('/member-management')
}
const goToOrders = () => {
router.push('/admin/orders')
}
const goToTasks = () => {
router.push('/generate-task-record')
}
const goToSettings = () => {
router.push('/system-settings')
}
// 格式化JWT过期时间显示
const formatJwtExpiration = (hours) => {
if (!hours) return ''
if (hours < 24) {
2025-11-13 17:01:39 +08:00
return `${hours}${t('apiManagement.hours')}`
} else if (hours < 720) {
const days = Math.floor(hours / 24)
const remainingHours = hours % 24
if (remainingHours === 0) {
2025-11-13 17:01:39 +08:00
return `${days}${t('apiManagement.days')}`
}
2025-11-13 17:01:39 +08:00
return `${days}${t('apiManagement.days')}${remainingHours}${t('apiManagement.hours')}`
} else {
2025-11-13 17:01:39 +08:00
return `30${t('apiManagement.days')}`
}
}
// 加载当前API密钥和JWT配置仅显示部分
const loadApiKey = async () => {
loading.value = true
try {
const response = await api.get('/api-key')
if (response.data?.maskedKey) {
// 不显示掩码后的密钥,只用于验证
console.log('当前API密钥已配置')
}
// 加载JWT过期时间转换为小时
if (response.data?.jwtExpiration) {
apiForm.jwtExpirationHours = Math.round(response.data.jwtExpiration / 3600000)
} else if (response.data?.jwtExpirationHours) {
apiForm.jwtExpirationHours = Math.round(response.data.jwtExpirationHours)
}
} catch (error) {
console.error('加载配置失败:', error)
} finally {
loading.value = false
}
}
// 保存API密钥和JWT配置
const saveApiKey = async () => {
// 检查是否有任何输入
const hasApiKey = apiForm.apiKey && apiForm.apiKey.trim() !== ''
const hasJwtExpiration = apiForm.jwtExpirationHours != null && apiForm.jwtExpirationHours > 0
// 验证输入:至少需要提供一个配置项
if (!hasApiKey && !hasJwtExpiration) {
ElMessage.warning('请至少输入API密钥或设置Token过期时间')
return
}
// 验证JWT过期时间范围
if (hasJwtExpiration && (apiForm.jwtExpirationHours < 1 || apiForm.jwtExpirationHours > 720)) {
ElMessage.warning('Token过期时间必须在1-720小时之间1小时-30天')
return
}
saving.value = true
try {
const requestData = {}
// 如果提供了API密钥添加到请求中
if (hasApiKey) {
requestData.apiKey = apiForm.apiKey.trim()
}
// 如果提供了JWT过期时间转换为毫秒并添加到请求中
if (hasJwtExpiration) {
requestData.jwtExpiration = apiForm.jwtExpirationHours * 3600000 // 转换为毫秒
}
const response = await api.put('/api-key', requestData)
if (response.data?.success) {
ElMessage.success(response.data.message || '配置保存成功,请重启应用以使配置生效')
// 清空API密钥输入框保留JWT过期时间
apiForm.apiKey = ''
} else {
ElMessage.error(response.data?.error || '保存失败')
}
} catch (error) {
console.error('保存配置失败:', error)
ElMessage.error('保存失败: ' + (error.response?.data?.message || error.message || '未知错误'))
} finally {
saving.value = false
}
}
// 重置表单
const resetForm = () => {
apiForm.apiKey = ''
// 重新加载JWT过期时间
loadApiKey()
}
// 页面加载时获取当前API密钥状态
onMounted(() => {
loadApiKey()
2025-11-13 17:01:39 +08:00
fetchSystemStats()
})
2025-11-13 17:01:39 +08:00
// 获取系统统计数据
const fetchSystemStats = async () => {
try {
// 临时使用计算值后续可以从API获取
// 计算在线用户数(这里简化处理)
const randomOnline = Math.floor(Math.random() * 50) + 10
onlineUsers.value = `${randomOnline}/500`
// 计算系统运行时间(基于当前时间简单模拟)
const hours = new Date().getHours()
const minutes = new Date().getMinutes()
systemUptime.value = `${hours}小时${minutes}`
} catch (error) {
console.error('获取系统统计失败:', error)
onlineUsers.value = '0/500'
systemUptime.value = '未知'
}
}
</script>
<style scoped>
.api-management {
display: flex;
min-height: 100vh;
background: #f8f9fa;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 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;
2025-11-13 17:01:39 +08:00
padding: 0 50px;
margin-bottom: 32px;
}
.logo-icon {
2025-11-13 17:01:39 +08:00
width: 100%;
height: auto;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
2025-11-13 17:01:39 +08:00
.logo-icon svg, .logo-icon img {
width: 100%;
height: auto;
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: 1;
display: flex;
flex-direction: column;
background: #f8f9fa;
}
/* 顶部搜索栏 */
.top-header {
background: white;
border-bottom: 1px solid #e9ecef;
padding: 16px 24px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.search-bar {
position: relative;
display: flex;
align-items: center;
}
.search-icon {
position: absolute;
left: 12px;
color: #9ca3af;
font-size: 16px;
z-index: 1;
}
.search-input {
width: 300px;
padding: 10px 12px 10px 40px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
background: white;
outline: none;
transition: border-color 0.2s ease;
}
.search-input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.search-input::placeholder {
color: #9ca3af;
}
.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;
}
/* API内容区域 */
.api-content {
padding: 24px;
flex: 1;
background: white;
margin: 24px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.content-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32px;
}
.content-header h2 {
font-size: 24px;
font-weight: 600;
color: #1e293b;
margin: 0;
}
.api-form-container {
max-width: 800px;
}
.api-form {
background: #f9fafb;
padding: 32px;
border-radius: 8px;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.api-management {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
}
.nav-menu {
display: flex;
overflow-x: auto;
padding: 0 16px;
}
.nav-item {
white-space: nowrap;
margin-right: 16px;
margin-bottom: 0;
}
.sidebar-footer {
display: none;
}
.search-input {
width: 200px;
}
.api-content {
padding: 16px;
}
}
</style>