新增系统配置页(运营管理系统后台)
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import request from '@/utils/request'
|
||||
import type { ApiResponse, LoginResponseData, UserInfo } from '@/types/api'
|
||||
import type { ApiResponse, LoginResponseData, UserInfo, ConfigItem, CaptchaData, CaptchaEnabled } from '@/types/api'
|
||||
|
||||
export const login = (username: string, password: string): Promise<ApiResponse<LoginResponseData>> => {
|
||||
export const login = (username: string, password: string, captchaId?: string, captchaCode?: string): Promise<ApiResponse<LoginResponseData>> => {
|
||||
return request({
|
||||
url: '/api/admin/login',
|
||||
method: 'post',
|
||||
data: { username, password }
|
||||
data: { username, password, captchaId, captchaCode }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,3 +15,48 @@ export const getCurrentUser = (): Promise<ApiResponse<UserInfo>> => {
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export const getAllConfigs = (): Promise<ApiResponse<ConfigItem[]>> => {
|
||||
return request({
|
||||
url: '/api/admin/configs',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export const updateConfigs = (configs: Array<{ configKey: string; configValue: string }>): Promise<ApiResponse> => {
|
||||
return request({
|
||||
url: '/api/admin/configs',
|
||||
method: 'put',
|
||||
data: { configs }
|
||||
})
|
||||
}
|
||||
|
||||
export const testEmail = (toEmail: string): Promise<ApiResponse> => {
|
||||
return request({
|
||||
url: '/api/admin/configs/test-email',
|
||||
method: 'post',
|
||||
data: { toEmail }
|
||||
})
|
||||
}
|
||||
|
||||
export const generateCaptcha = (): Promise<ApiResponse<CaptchaData>> => {
|
||||
return request({
|
||||
url: '/api/admin/captcha',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export const verifyCaptcha = (captchaId: string, code: string): Promise<ApiResponse> => {
|
||||
return request({
|
||||
url: '/api/admin/captcha/verify',
|
||||
method: 'post',
|
||||
data: { captchaId, code }
|
||||
})
|
||||
}
|
||||
|
||||
export const checkCaptchaEnabled = (): Promise<ApiResponse<CaptchaEnabled>> => {
|
||||
return request({
|
||||
url: '/api/admin/captcha/enabled',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import request from '@/utils/request'
|
||||
import type { ApiResponse, CaptchaData, CaptchaEnabled } from '@/types/api'
|
||||
|
||||
export const login = (username: string, password: string) => {
|
||||
export const login = (username: string, password: string, captchaId?: string, captchaCode?: string) => {
|
||||
return request({
|
||||
url: '/api/player/login',
|
||||
method: 'post',
|
||||
data: { username, password }
|
||||
data: { username, password, captchaId, captchaCode }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,3 +22,32 @@ export const getAccountInfo = () => {
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export const generateCaptcha = (): Promise<ApiResponse<CaptchaData>> => {
|
||||
return request({
|
||||
url: '/api/player/captcha',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export const verifyCaptcha = (captchaId: string, code: string): Promise<ApiResponse> => {
|
||||
return request({
|
||||
url: '/api/player/captcha/verify',
|
||||
method: 'post',
|
||||
data: { captchaId, code }
|
||||
})
|
||||
}
|
||||
|
||||
export const checkCaptchaEnabled = (): Promise<ApiResponse<CaptchaEnabled>> => {
|
||||
return request({
|
||||
url: '/api/player/captcha/enabled',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export const checkPlayerServiceStatus = (): Promise<ApiResponse<{ enabled: boolean; closeMsg: string }>> => {
|
||||
return request({
|
||||
url: '/api/player/service-status',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ import { ref, h } from 'vue'
|
||||
import { NLayout, NLayoutSider, NLayoutHeader, NLayoutContent, NLayoutFooter, NMenu, NDropdown } from 'naive-ui'
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||
import { useAdminStore } from '@/stores/admin'
|
||||
import { RiDashboardLine, RiArrowDownSLine, RiUserLine, RiLogoutBoxRLine, RiSettings3Line, RiUserSettingsLine } from '@remixicon/vue'
|
||||
import { RiDashboardLine, RiArrowDownSLine, RiUserLine, RiLogoutBoxRLine, RiSettings3Line, RiUserSettingsLine, RiToolsLine } from '@remixicon/vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -71,6 +71,11 @@ const menuOptions = [
|
||||
label: () => h(RouterLink, { to: '/admin/user-management' }, { default: () => '用户管理' }),
|
||||
key: 'UserManagement',
|
||||
icon: () => h(RiUserSettingsLine, { size: '20px' })
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: '/admin/system-config' }, { default: () => '系统配置' }),
|
||||
key: 'SystemConfig',
|
||||
icon: () => h(RiToolsLine, { size: '20px' })
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -45,6 +45,12 @@ const routes = [
|
||||
name: 'UserManagement',
|
||||
component: () => import('@/views/admin/UserManagement.vue'),
|
||||
meta: { title: '用户管理', requiresAdminAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'system-config',
|
||||
name: 'SystemConfig',
|
||||
component: () => import('@/views/admin/SystemConfig.vue'),
|
||||
meta: { title: '系统配置', requiresAdminAuth: true }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ export const useAdminStore = defineStore('admin', () => {
|
||||
localStorage.removeItem('admin_userInfo')
|
||||
}
|
||||
|
||||
const login = async (username: string, password: string) => {
|
||||
const login = async (username: string, password: string, captchaId?: string, captchaCode?: string) => {
|
||||
try {
|
||||
const data = await loginApi(username, password)
|
||||
const data = await loginApi(username, password, captchaId, captchaCode)
|
||||
if (data.success && data.data) {
|
||||
setToken(data.data.token)
|
||||
setUserInfo(data.data.user)
|
||||
|
||||
@@ -16,8 +16,8 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
sessionStorage.removeItem('player_token')
|
||||
}
|
||||
|
||||
const login = async (username: string, password: string) => {
|
||||
const response = await loginApi(username, password)
|
||||
const login = async (username: string, password: string, captchaId?: string, captchaCode?: string) => {
|
||||
const response = await loginApi(username, password, captchaId, captchaCode)
|
||||
if (response.success && response.data) {
|
||||
setToken(response.data)
|
||||
return true
|
||||
|
||||
@@ -33,3 +33,40 @@ export interface UserInfo {
|
||||
realName?: string
|
||||
roleId: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统配置项
|
||||
*/
|
||||
export interface ConfigItem {
|
||||
id: number
|
||||
configKey: string
|
||||
configValue: string
|
||||
configType: string
|
||||
description?: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统配置分组
|
||||
*/
|
||||
export interface ConfigGroup {
|
||||
type: string
|
||||
title: string
|
||||
configs: ConfigItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码响应数据
|
||||
*/
|
||||
export interface CaptchaData {
|
||||
captchaId: string
|
||||
svg: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码启用状态
|
||||
*/
|
||||
export interface CaptchaEnabled {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
@@ -13,6 +13,16 @@
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="captchaEnabled" path="captchaCode" label="验证码">
|
||||
<n-space style="width: 100%">
|
||||
<n-input
|
||||
v-model:value="formValue.captchaCode"
|
||||
placeholder="请输入验证码"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
<div class="captcha-image" @click="handleRefreshCaptcha" v-html="captchaSvg"></div>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button type="primary" block @click="handleLogin" :loading="loading">
|
||||
登录
|
||||
@@ -24,10 +34,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { NCard, NForm, NFormItem, NInput, NButton, useMessage } from 'naive-ui'
|
||||
import { NCard, NForm, NFormItem, NInput, NButton, NSpace, useMessage } from 'naive-ui'
|
||||
import { useAdminStore } from '@/stores/admin'
|
||||
import { generateCaptcha, checkCaptchaEnabled } from '@/api/admin'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
@@ -35,10 +46,14 @@ const adminStore = useAdminStore()
|
||||
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
const captchaEnabled = ref(false)
|
||||
const captchaSvg = ref('')
|
||||
const captchaId = ref('')
|
||||
|
||||
const formValue = ref({
|
||||
username: '',
|
||||
password: ''
|
||||
password: '',
|
||||
captchaCode: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
@@ -51,6 +66,17 @@ const rules = {
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
trigger: 'blur'
|
||||
},
|
||||
captchaCode: {
|
||||
required: true,
|
||||
message: '请输入验证码',
|
||||
trigger: 'blur',
|
||||
validator: (rule: any, value: string) => {
|
||||
if (captchaEnabled.value && !value) {
|
||||
return new Error('请输入验证码')
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,20 +85,62 @@ const handleLogin = async () => {
|
||||
await formRef.value?.validate()
|
||||
loading.value = true
|
||||
|
||||
const result = await adminStore.login(formValue.value.username, formValue.value.password)
|
||||
const result = await adminStore.login(
|
||||
formValue.value.username,
|
||||
formValue.value.password,
|
||||
captchaId.value,
|
||||
formValue.value.captchaCode
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
message.success(result.message)
|
||||
router.push('/admin/dashboard')
|
||||
} else {
|
||||
message.error(result.message)
|
||||
if (captchaEnabled.value) {
|
||||
handleRefreshCaptcha()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error)
|
||||
if (captchaEnabled.value) {
|
||||
handleRefreshCaptcha()
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefreshCaptcha = async () => {
|
||||
try {
|
||||
const response = await generateCaptcha()
|
||||
if (response.success && response.data) {
|
||||
captchaSvg.value = response.data.svg
|
||||
captchaId.value = response.data.captchaId
|
||||
formValue.value.captchaCode = ''
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取验证码失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const checkCaptchaStatus = async () => {
|
||||
try {
|
||||
const response = await checkCaptchaEnabled()
|
||||
if (response.success && response.data) {
|
||||
captchaEnabled.value = response.data.enabled
|
||||
if (captchaEnabled.value) {
|
||||
handleRefreshCaptcha()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查验证码状态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkCaptchaStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -83,4 +151,25 @@ const handleLogin = async () => {
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
.captcha-image {
|
||||
width: 120px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.captcha-image:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.captcha-image :deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
447
frontend/src/views/admin/SystemConfig.vue
Normal file
447
frontend/src/views/admin/SystemConfig.vue
Normal file
@@ -0,0 +1,447 @@
|
||||
<template>
|
||||
<div class="system-config-container">
|
||||
<n-card title="系统配置" class="config-card">
|
||||
<n-tabs v-model:value="activeTab" type="line" animated>
|
||||
<!-- 基础配置标签页 -->
|
||||
<n-tab-pane name="basic" tab="基础配置">
|
||||
<n-form
|
||||
ref="basicFormRef"
|
||||
:model="basicForm"
|
||||
label-placement="left"
|
||||
label-width="180px"
|
||||
:rules="basicRules"
|
||||
>
|
||||
<n-grid :cols="24" :x-gap="24">
|
||||
<n-gi :span="12">
|
||||
<n-form-item label="后端域名(IP)地址" path="backend_host">
|
||||
<n-input v-model:value="basicForm.backend_host" placeholder="请输入后端IP地址" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="12">
|
||||
<n-form-item label="后端端口" path="backend_port">
|
||||
<n-input-number v-model:value="basicForm.backend_port" :min="1" :max="65535" style="width: 100%" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="12">
|
||||
<n-form-item label="日志级别" path="log_level">
|
||||
<n-select
|
||||
v-model:value="basicForm.log_level"
|
||||
:options="logLevelOptions"
|
||||
placeholder="请选择日志级别"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="12">
|
||||
<n-form-item label="登录验证码" path="login_captcha_enabled">
|
||||
<n-switch v-model:value="basicForm.login_captcha_enabled" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="12">
|
||||
<n-form-item label="玩家服务中心开启访问" path="player_service_enabled">
|
||||
<n-switch v-model:value="basicForm.player_service_enabled" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="24">
|
||||
<n-form-item label="玩家服务中心关闭提示" path="player_service_close_msg">
|
||||
<n-input
|
||||
v-model:value="basicForm.player_service_close_msg"
|
||||
type="textarea"
|
||||
placeholder="请输入玩家服务中心关闭提示文本"
|
||||
:rows="3"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 安全配置标签页 -->
|
||||
<n-tab-pane name="security" tab="安全配置">
|
||||
<n-form
|
||||
ref="securityFormRef"
|
||||
:model="securityForm"
|
||||
label-placement="left"
|
||||
label-width="180px"
|
||||
:rules="securityRules"
|
||||
>
|
||||
<n-grid :cols="24" :x-gap="24">
|
||||
<n-gi :span="24">
|
||||
<n-form-item label="跨域地址" path="cors_origin">
|
||||
<n-input v-model:value="securityForm.cors_origin" placeholder="请输入跨域地址" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="12">
|
||||
<n-form-item label="JWT密钥" path="jwt_secret">
|
||||
<n-input v-model:value="securityForm.jwt_secret" type="password" placeholder="请输入JWT密钥" show-password-on="click" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="12">
|
||||
<n-form-item label="JWT有效期" path="jwt_expires_in">
|
||||
<n-input v-model:value="securityForm.jwt_expires_in" placeholder="如: 2h, 7d, 30m" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 游戏配置标签页 -->
|
||||
<n-tab-pane name="game" tab="游戏配置">
|
||||
<n-form
|
||||
ref="gameFormRef"
|
||||
:model="gameForm"
|
||||
label-placement="left"
|
||||
label-width="180px"
|
||||
:rules="gameRules"
|
||||
>
|
||||
<n-grid :cols="24" :x-gap="24">
|
||||
<n-gi :span="24">
|
||||
<n-form-item label="游戏服务端代理地址" path="game_server_proxy_url">
|
||||
<n-input v-model:value="gameForm.game_server_proxy_url" placeholder="请输入游戏服务端代理地址" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="24">
|
||||
<n-form-item label="游戏服务端PSK密钥" path="game_server_psk">
|
||||
<n-input v-model:value="gameForm.game_server_psk" type="password" placeholder="请输入游戏服务端PSK密钥" show-password-on="click" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 充值配置标签页 -->
|
||||
<n-tab-pane name="payment" tab="充值配置">
|
||||
<div class="development-notice">
|
||||
<n-result status="info" title="功能开发中" description="充值配置功能正在开发中,敬请期待">
|
||||
<template #icon>
|
||||
<n-icon :component="RiToolsLine" size="64" />
|
||||
</template>
|
||||
</n-result>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 邮件配置标签页 -->
|
||||
<n-tab-pane name="email" tab="邮件配置">
|
||||
<n-form
|
||||
ref="emailFormRef"
|
||||
:model="emailForm"
|
||||
label-placement="left"
|
||||
label-width="180px"
|
||||
:rules="emailRules"
|
||||
>
|
||||
<n-grid :cols="24" :x-gap="24">
|
||||
<n-gi :span="12">
|
||||
<n-form-item label="发件人邮箱" path="mail_from">
|
||||
<n-input v-model:value="emailForm.mail_from" placeholder="请输入发件人邮箱" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="12">
|
||||
<n-form-item label="SMTP服务器地址" path="mail_smtp_host">
|
||||
<n-input v-model:value="emailForm.mail_smtp_host" placeholder="请输入SMTP服务器地址" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="12">
|
||||
<n-form-item label="SMTP端口" path="mail_smtp_port">
|
||||
<n-input-number v-model:value="emailForm.mail_smtp_port" :min="1" :max="65535" style="width: 100%" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="12">
|
||||
<n-form-item label="SMTP用户名" path="mail_smtp_user">
|
||||
<n-input v-model:value="emailForm.mail_smtp_user" placeholder="请输入SMTP用户名" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="24">
|
||||
<n-form-item label="SMTP密码" path="mail_smtp_password">
|
||||
<n-input v-model:value="emailForm.mail_smtp_password" type="password" placeholder="请输入SMTP密码" show-password-on="click" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<!-- 邮件测试区域 -->
|
||||
<n-divider />
|
||||
<n-space vertical style="width: 100%">
|
||||
<n-alert type="info" title="邮件测试">
|
||||
配置完成后,可以发送测试邮件验证配置是否正确。
|
||||
</n-alert>
|
||||
<n-space>
|
||||
<n-input v-model:value="testEmailInput" placeholder="请输入测试邮箱地址" style="width: 300px" />
|
||||
<n-button type="primary" :loading="emailTestLoading" @click="handleTestEmail">
|
||||
发送测试邮件
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</n-form>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="handleReset">重置</n-button>
|
||||
<n-button type="primary" :loading="saveLoading" @click="handleSave">保存配置</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { NCard, NTabs, NTabPane, NForm, NFormItem, NInput, NInputNumber, NSelect, NSwitch, NGrid, NGi, NSpace, NButton, NDivider, NAlert, NResult, NIcon, useMessage } from 'naive-ui'
|
||||
import { RiToolsLine } from '@remixicon/vue'
|
||||
import { getAllConfigs, updateConfigs, testEmail as sendTestEmail } from '@/api/admin'
|
||||
import type { ConfigItem } from '@/types/api'
|
||||
import type { FormRules } from 'naive-ui'
|
||||
|
||||
// 消息提示
|
||||
const message = useMessage()
|
||||
|
||||
// 当前激活的标签页
|
||||
const activeTab = ref('basic')
|
||||
|
||||
// 基础配置表单
|
||||
const basicForm = reactive({
|
||||
backend_host: '',
|
||||
backend_port: 3000,
|
||||
log_level: 'info',
|
||||
login_captcha_enabled: true,
|
||||
player_service_enabled: true,
|
||||
player_service_close_msg: ''
|
||||
})
|
||||
|
||||
// 安全配置表单
|
||||
const securityForm = reactive({
|
||||
cors_origin: '',
|
||||
jwt_secret: '',
|
||||
jwt_expires_in: '2h'
|
||||
})
|
||||
|
||||
// 游戏配置表单
|
||||
const gameForm = reactive({
|
||||
game_server_proxy_url: '',
|
||||
game_server_psk: ''
|
||||
})
|
||||
|
||||
// 邮件配置表单
|
||||
const emailForm = reactive({
|
||||
mail_from: '',
|
||||
mail_smtp_host: '',
|
||||
mail_smtp_port: 587,
|
||||
mail_smtp_user: '',
|
||||
mail_smtp_password: ''
|
||||
})
|
||||
|
||||
// 表单引用
|
||||
const basicFormRef = ref()
|
||||
const securityFormRef = ref()
|
||||
const gameFormRef = ref()
|
||||
const emailFormRef = ref()
|
||||
|
||||
// 表单验证规则
|
||||
const basicRules: FormRules = {
|
||||
backend_host: { required: true, message: '请输入后端IP地址', trigger: 'blur' },
|
||||
backend_port: { required: true, type: 'number', message: '请输入后端端口', trigger: 'blur' },
|
||||
log_level: { required: true, message: '请选择日志级别', trigger: 'change' }
|
||||
}
|
||||
|
||||
const securityRules: FormRules = {
|
||||
cors_origin: { required: true, message: '请输入跨域地址', trigger: 'blur' },
|
||||
jwt_secret: { required: true, message: '请输入JWT密钥', trigger: 'blur' },
|
||||
jwt_expires_in: { required: true, message: '请输入JWT有效期', trigger: 'blur' }
|
||||
}
|
||||
|
||||
const gameRules: FormRules = {
|
||||
game_server_proxy_url: { required: true, message: '请输入游戏服务端代理地址', trigger: 'blur' },
|
||||
game_server_psk: { required: true, message: '请输入游戏服务端PSK密钥', trigger: 'blur' }
|
||||
}
|
||||
|
||||
const emailRules: FormRules = {
|
||||
mail_from: { required: true, message: '请输入发件人邮箱', trigger: 'blur' },
|
||||
mail_smtp_host: { required: true, message: '请输入SMTP服务器地址', trigger: 'blur' },
|
||||
mail_smtp_port: { required: true, type: 'number', message: '请输入SMTP端口', trigger: 'blur' },
|
||||
mail_smtp_user: { required: true, message: '请输入SMTP用户名', trigger: 'blur' },
|
||||
mail_smtp_password: { required: true, message: '请输入SMTP密码', trigger: 'blur' }
|
||||
}
|
||||
|
||||
// 日志级别选项
|
||||
const logLevelOptions = [
|
||||
{ label: 'info', value: 'info' },
|
||||
{ label: 'debug', value: 'debug' },
|
||||
{ label: 'error', value: 'error' }
|
||||
]
|
||||
|
||||
// 测试邮箱输入
|
||||
const testEmailInput = ref('')
|
||||
|
||||
// 加载状态
|
||||
const saveLoading = ref(false)
|
||||
const emailTestLoading = ref(false)
|
||||
|
||||
// 配置项映射
|
||||
const configMap: Record<string, any> = {
|
||||
// 基础配置
|
||||
backend_host: basicForm,
|
||||
backend_port: basicForm,
|
||||
log_level: basicForm,
|
||||
login_captcha_enabled: basicForm,
|
||||
player_service_enabled: basicForm,
|
||||
player_service_close_msg: basicForm,
|
||||
// 安全配置
|
||||
cors_origin: securityForm,
|
||||
jwt_secret: securityForm,
|
||||
jwt_expires_in: securityForm,
|
||||
// 游戏配置
|
||||
game_server_proxy_url: gameForm,
|
||||
game_server_psk: gameForm,
|
||||
// 邮件配置
|
||||
mail_from: emailForm,
|
||||
mail_smtp_host: emailForm,
|
||||
mail_smtp_port: emailForm,
|
||||
mail_smtp_user: emailForm,
|
||||
mail_smtp_password: emailForm
|
||||
}
|
||||
|
||||
// 获取配置列表
|
||||
const fetchConfigs = async () => {
|
||||
try {
|
||||
const response = await getAllConfigs()
|
||||
|
||||
if (response.success && response.data) {
|
||||
const groupedData = response.data as any
|
||||
|
||||
const allConfigs: ConfigItem[] = []
|
||||
Object.keys(groupedData).forEach(type => {
|
||||
if (Array.isArray(groupedData[type])) {
|
||||
allConfigs.push(...groupedData[type])
|
||||
}
|
||||
})
|
||||
|
||||
allConfigs.forEach((config: ConfigItem) => {
|
||||
const form = configMap[config.configKey]
|
||||
if (form) {
|
||||
const value = config.configValue
|
||||
if (typeof form[config.configKey] === 'boolean') {
|
||||
form[config.configKey] = value === 'true'
|
||||
} else if (typeof form[config.configKey] === 'number') {
|
||||
form[config.configKey] = Number(value)
|
||||
} else {
|
||||
form[config.configKey] = value
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
message.error(response.message || '获取配置失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
saveLoading.value = true
|
||||
|
||||
// 验证所有表单
|
||||
try {
|
||||
await basicFormRef.value?.validate()
|
||||
} catch (error) {
|
||||
message.error('请检查基础配置中的必填项')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await securityFormRef.value?.validate()
|
||||
} catch (error) {
|
||||
message.error('请检查安全配置中的必填项')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await gameFormRef.value?.validate()
|
||||
} catch (error) {
|
||||
message.error('请检查游戏配置中的必填项')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await emailFormRef.value?.validate()
|
||||
} catch (error) {
|
||||
message.error('请检查邮件配置中的必填项')
|
||||
return
|
||||
}
|
||||
|
||||
// 构建配置更新数据
|
||||
const configs = []
|
||||
for (const [key, form] of Object.entries(configMap)) {
|
||||
const value = form[key]
|
||||
configs.push({
|
||||
configKey: key,
|
||||
configValue: String(value)
|
||||
})
|
||||
}
|
||||
|
||||
const response = await updateConfigs(configs)
|
||||
|
||||
if (response.success) {
|
||||
message.success('配置保存成功')
|
||||
} else {
|
||||
message.error(response.message || '配置保存失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('保存配置异常:', error)
|
||||
message.error(error.message || '配置保存失败')
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 测试邮件
|
||||
const handleTestEmail = async () => {
|
||||
if (!testEmailInput.value) {
|
||||
message.warning('请输入测试邮箱地址')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
emailTestLoading.value = true
|
||||
const response = await sendTestEmail(testEmailInput.value)
|
||||
if (response.success) {
|
||||
message.success('测试邮件发送成功,请检查邮箱')
|
||||
} else {
|
||||
message.error(response.message || '测试邮件发送失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.response?.data?.message || '测试邮件发送失败')
|
||||
} finally {
|
||||
emailTestLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置配置
|
||||
const handleReset = async () => {
|
||||
await fetchConfigs()
|
||||
message.info('配置已重置')
|
||||
}
|
||||
|
||||
// 页面加载时获取配置
|
||||
onMounted(() => {
|
||||
fetchConfigs()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.system-config-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.config-card {
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.development-notice {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,20 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<n-card title="玩家登录" style="width: 400px;">
|
||||
<!-- 维护公告遮罩 -->
|
||||
<div v-if="!playerServiceEnabled" class="maintenance-overlay">
|
||||
<n-card title="系统维护公告" style="width: 500px; text-align: center;">
|
||||
<template #header-extra>
|
||||
<n-icon :component="RiAlertLine" size="32" color="#f0a020" />
|
||||
</template>
|
||||
<p style="font-size: 16px; color: #666;">{{ playerServiceCloseMsg }}</p>
|
||||
<template #footer>
|
||||
<n-button type="primary" @click="handleRefreshStatus">刷新状态</n-button>
|
||||
</template>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<n-card v-else title="玩家登录" style="width: 400px;">
|
||||
<n-form ref="formRef" :model="formValue" :rules="rules" size="large">
|
||||
<n-form-item path="username" label="用户名">
|
||||
<n-input v-model:value="formValue.username" placeholder="请输入用户名" />
|
||||
@@ -13,6 +27,16 @@
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="captchaEnabled" path="captchaCode" label="验证码">
|
||||
<n-space style="width: 100%">
|
||||
<n-input
|
||||
v-model:value="formValue.captchaCode"
|
||||
placeholder="请输入验证码"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
<div class="captcha-image" @click="handleRefreshCaptcha" v-html="captchaSvg"></div>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button type="primary" block @click="handleLogin" :loading="loading">
|
||||
登录
|
||||
@@ -24,10 +48,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { NCard, NForm, NFormItem, NInput, NButton, useMessage } from 'naive-ui'
|
||||
import { NCard, NForm, NFormItem, NInput, NButton, NSpace, NIcon, useMessage } from 'naive-ui'
|
||||
import { RiAlertLine } from '@remixicon/vue'
|
||||
import { usePlayerStore } from '@/stores/player'
|
||||
import { generateCaptcha, checkCaptchaEnabled, checkPlayerServiceStatus } from '@/api/player'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
@@ -35,10 +61,16 @@ const playerStore = usePlayerStore()
|
||||
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
const captchaEnabled = ref(false)
|
||||
const captchaSvg = ref('')
|
||||
const captchaId = ref('')
|
||||
const playerServiceEnabled = ref(true)
|
||||
const playerServiceCloseMsg = ref('玩家服务中心系统维护中')
|
||||
|
||||
const formValue = ref({
|
||||
username: '',
|
||||
password: ''
|
||||
password: '',
|
||||
captchaCode: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
@@ -51,6 +83,17 @@ const rules = {
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
trigger: 'blur'
|
||||
},
|
||||
captchaCode: {
|
||||
required: true,
|
||||
message: '请输入验证码',
|
||||
trigger: 'blur',
|
||||
validator: (_rule: any, value: string) => {
|
||||
if (captchaEnabled.value && !value) {
|
||||
return new Error('请输入验证码')
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,20 +102,82 @@ const handleLogin = async () => {
|
||||
await formRef.value?.validate()
|
||||
loading.value = true
|
||||
|
||||
const success = await playerStore.login(formValue.value.username, formValue.value.password)
|
||||
const success = await playerStore.login(
|
||||
formValue.value.username,
|
||||
formValue.value.password,
|
||||
captchaId.value,
|
||||
formValue.value.captchaCode
|
||||
)
|
||||
|
||||
if (success) {
|
||||
message.success('登录成功')
|
||||
router.push('/player/dashboard')
|
||||
} else {
|
||||
message.error('登录失败,请检查用户名和密码')
|
||||
if (captchaEnabled.value) {
|
||||
handleRefreshCaptcha()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('登录错误:', error)
|
||||
const errorMessage = error.response?.data?.message || error.message || '登录失败,请稍后重试'
|
||||
message.error(errorMessage)
|
||||
if (captchaEnabled.value) {
|
||||
handleRefreshCaptcha()
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefreshCaptcha = async () => {
|
||||
try {
|
||||
const response = await generateCaptcha()
|
||||
if (response.success && response.data) {
|
||||
captchaSvg.value = response.data.svg
|
||||
captchaId.value = response.data.captchaId
|
||||
formValue.value.captchaCode = ''
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取验证码失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const checkCaptchaStatus = async () => {
|
||||
try {
|
||||
const response = await checkCaptchaEnabled()
|
||||
if (response.success && response.data) {
|
||||
captchaEnabled.value = response.data.enabled
|
||||
if (captchaEnabled.value) {
|
||||
handleRefreshCaptcha()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查验证码状态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const checkServiceStatus = async () => {
|
||||
try {
|
||||
const response = await checkPlayerServiceStatus()
|
||||
if (response.success && response.data) {
|
||||
playerServiceEnabled.value = response.data.enabled
|
||||
playerServiceCloseMsg.value = response.data.closeMsg
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查玩家服务中心状态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefreshStatus = async () => {
|
||||
await checkServiceStatus()
|
||||
message.info('状态已刷新')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkCaptchaStatus()
|
||||
checkServiceStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -83,4 +188,38 @@ const handleLogin = async () => {
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.maintenance-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.captcha-image {
|
||||
width: 120px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.captcha-image:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.captcha-image :deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user