Files
nl-blogs/client/src/pages/admin/Settings.vue

566 lines
20 KiB
Vue
Raw Normal View History

2026-01-15 13:51:44 +08:00
<template>
2026-01-16 11:06:59 +08:00
<div class="w-full animate-reveal">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-serif italic text-white">系统配置管理</h1>
2026-01-15 13:51:44 +08:00
</div>
2026-01-19 16:14:08 +08:00
<!-- Tab切换 -->
<div class="mb-6 border-b border-white/10">
<div class="flex gap-4">
<button
@click="activeTab = 'settings'"
:class="[
'px-4 py-2 text-sm font-medium transition-colors border-b-2',
activeTab === 'settings'
? 'text-art-accent border-art-accent'
: 'text-art-muted border-transparent hover:text-white'
]"
>
基础配置
</button>
<button
@click="activeTab = 'oss'"
:class="[
'px-4 py-2 text-sm font-medium transition-colors border-b-2',
activeTab === 'oss'
? 'text-art-accent border-art-accent'
: 'text-art-muted border-transparent hover:text-white'
]"
>
OSS配置
</button>
</div>
</div>
<!-- 基础配置Tab -->
<div v-if="activeTab === 'settings'">
2026-01-15 13:51:44 +08:00
2026-01-16 11:06:59 +08:00
<div class="admin-card overflow-hidden">
<div class="overflow-x-auto">
<table class="admin-table">
<thead>
<tr>
<th class="w-1/4">键名</th>
<th class="w-1/3">值</th>
<th>描述</th>
<th class="text-right w-32">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="setting in settings" :key="setting.id" class="group transition-colors duration-200">
<td class="font-mono text-xs text-art-accent">{{ setting.keyName }}</td>
<td class="max-w-xs truncate text-white/80" :title="setting.value">{{ setting.value }}</td>
<td class="text-art-muted text-sm">{{ setting.description }}</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<button @click="editSetting(setting)" class="admin-btn-secondary py-1 px-3 text-xs">
编辑
</button>
<button @click="deleteSetting(setting.keyName)" class="admin-btn-danger py-1 px-3 text-xs">
删除
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="settings.length === 0" class="p-16 text-center">
<div class="w-16 h-16 bg-white/5 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl">⚙️</div>
<h3 class="text-white font-medium mb-2">暂无配置</h3>
</div>
2026-01-15 13:51:44 +08:00
</div>
<!-- Add/Edit Modal -->
2026-01-16 11:06:59 +08:00
<div v-if="showModal" class="fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="absolute inset-0 bg-black/80 backdrop-blur-sm transition-opacity" @click="closeModal"></div>
<div class="admin-card w-full max-w-lg relative z-10 flex flex-col max-h-[90vh] shadow-2xl animate-reveal">
<div class="flex items-center justify-between p-6 border-b border-white/5">
<h2 class="text-lg font-medium text-white">{{ editingSetting ? '编辑配置' : '新增配置' }}</h2>
<button @click="closeModal" class="text-white/40 hover:text-white transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
2026-01-15 13:51:44 +08:00
</div>
2026-01-16 11:06:59 +08:00
<div class="p-6 overflow-y-auto">
<form @submit.prevent="saveSetting" class="space-y-4">
<div class="space-y-2">
<label for="keyName" class="block text-xs font-medium text-art-muted uppercase tracking-wider">键名</label>
2026-01-15 13:51:44 +08:00
<input
type="text"
id="keyName"
v-model="form.keyName"
:disabled="editingSetting"
required
2026-01-16 11:06:59 +08:00
class="admin-input font-mono disabled:opacity-50 disabled:cursor-not-allowed"
placeholder="例如: site_title"
2026-01-15 13:51:44 +08:00
>
</div>
2026-01-16 11:06:59 +08:00
<div class="space-y-2">
<label for="value" class="block text-xs font-medium text-art-muted uppercase tracking-wider">值</label>
2026-01-15 13:51:44 +08:00
<input
type="text"
id="value"
v-model="form.value"
required
2026-01-16 11:06:59 +08:00
class="admin-input"
placeholder="配置项的值"
2026-01-15 13:51:44 +08:00
>
</div>
2026-01-16 11:06:59 +08:00
<div class="space-y-2">
<label for="description" class="block text-xs font-medium text-art-muted uppercase tracking-wider">描述</label>
2026-01-15 13:51:44 +08:00
<textarea
id="description"
v-model="form.description"
rows="3"
2026-01-16 11:06:59 +08:00
class="admin-input resize-none"
placeholder="该配置项的用途说明"
2026-01-15 13:51:44 +08:00
></textarea>
</div>
2026-01-16 11:06:59 +08:00
<div class="pt-4 flex justify-end gap-3">
<button type="button" @click="closeModal" class="admin-btn-secondary">取消</button>
<button type="submit" class="admin-btn-primary">保存</button>
2026-01-15 13:51:44 +08:00
</div>
</form>
</div>
</div>
</div>
2026-01-19 16:14:08 +08:00
</div>
<!-- OSS配置Tab -->
<div v-if="activeTab === 'oss'">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-medium text-white">OSS存储配置</h2>
<button @click="openOSSModal" class="admin-btn-primary">
+ 新增OSS配置
</button>
</div>
<div class="admin-card overflow-hidden">
<div class="overflow-x-auto">
<table class="admin-table">
<thead>
<tr>
<th class="w-1/6">名称</th>
<th class="w-1/6">存储类型</th>
<th class="w-1/6">Bucket</th>
<th class="w-1/6">区域</th>
<th class="w-1/6">域名</th>
<th class="w-1/12">状态</th>
<th class="text-right w-32">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="config in ossConfigs" :key="config.id" class="group transition-colors duration-200">
<td class="font-medium text-white">{{ config.name }}</td>
<td class="text-art-muted">
<span class="px-2 py-1 text-xs rounded bg-white/5">
{{ getStorageTypeLabel(config.storageType) }}
</span>
</td>
<td class="text-art-muted text-sm">{{ config.bucket || '-' }}</td>
<td class="text-art-muted text-sm">{{ config.region || '-' }}</td>
<td class="text-art-muted text-sm max-w-xs truncate" :title="config.domain">{{ config.domain || '-' }}</td>
<td>
<span
:class="[
'px-2 py-1 text-xs rounded',
config.isActive === 1
? 'bg-green-500/20 text-green-400'
: 'bg-white/5 text-art-muted'
]"
>
{{ config.isActive === 1 ? '已启用' : '已停用' }}
</span>
</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<button @click="editOSSConfig(config)" class="admin-btn-secondary py-1 px-3 text-xs">
编辑
</button>
<button @click="deleteOSSConfig(config.id)" class="admin-btn-danger py-1 px-3 text-xs">
删除
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="ossConfigs.length === 0" class="p-16 text-center">
<div class="w-16 h-16 bg-white/5 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl">☁️</div>
<h3 class="text-white font-medium mb-2">暂无OSS配置</h3>
<p class="text-art-muted text-sm mb-4">点击上方按钮添加OSS配置</p>
</div>
</div>
<!-- OSS配置编辑Modal -->
<div v-if="showOSSModal" class="fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="absolute inset-0 bg-black/80 backdrop-blur-sm transition-opacity" @click="closeOSSModal"></div>
<div class="admin-card w-full max-w-2xl relative z-10 flex flex-col max-h-[90vh] shadow-2xl animate-reveal">
<div class="flex items-center justify-between p-6 border-b border-white/5">
<h2 class="text-lg font-medium text-white">{{ editingOSSConfig ? '编辑OSS配置' : '新增OSS配置' }}</h2>
<button @click="closeOSSModal" class="text-white/40 hover:text-white transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</div>
<div class="p-6 overflow-y-auto">
<form @submit.prevent="saveOSSConfig" class="space-y-4">
<div class="space-y-2">
<label for="ossName" class="block text-xs font-medium text-art-muted uppercase tracking-wider">配置名称</label>
<input
type="text"
id="ossName"
v-model="ossForm.name"
required
class="admin-input"
placeholder="例如: 腾讯云CDN"
>
</div>
<div class="space-y-2">
<label for="storageType" class="block text-xs font-medium text-art-muted uppercase tracking-wider">存储类型</label>
<CustomSelect
v-model="ossForm.storageType"
:options="storageTypeOptions"
placeholder="选择存储类型"
/>
</div>
<div class="space-y-2">
<label for="accessKey" class="block text-xs font-medium text-art-muted uppercase tracking-wider">Access Key</label>
<input
type="text"
id="accessKey"
v-model="ossForm.accessKey"
required
class="admin-input font-mono"
placeholder="访问密钥ID"
>
</div>
<div class="space-y-2">
<label for="secretKey" class="block text-xs font-medium text-art-muted uppercase tracking-wider">Secret Key</label>
<input
type="password"
id="secretKey"
v-model="ossForm.secretKey"
required
class="admin-input font-mono"
placeholder="访问密钥Secret"
>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<label for="bucket" class="block text-xs font-medium text-art-muted uppercase tracking-wider">Bucket</label>
<input
type="text"
id="bucket"
v-model="ossForm.bucket"
class="admin-input"
placeholder="存储桶名称"
>
</div>
<div class="space-y-2">
<label for="region" class="block text-xs font-medium text-art-muted uppercase tracking-wider">区域</label>
<input
type="text"
id="region"
v-model="ossForm.region"
class="admin-input"
placeholder="例如: ap-beijing"
>
</div>
</div>
<div class="space-y-2">
<label for="domain" class="block text-xs font-medium text-art-muted uppercase tracking-wider">访问域名</label>
<input
type="text"
id="domain"
v-model="ossForm.domain"
class="admin-input"
placeholder="例如: https://cdn.example.com"
>
</div>
<div class="space-y-2">
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
v-model="ossForm.isActive"
:true-value="1"
:false-value="0"
class="w-4 h-4 rounded border-white/20 bg-white/5 text-art-accent focus:ring-art-accent"
>
<span class="text-sm text-art-muted">启用此配置</span>
</label>
</div>
<div class="pt-4 flex justify-end gap-3">
<button type="button" @click="closeOSSModal" class="admin-btn-secondary">取消</button>
<button type="submit" class="admin-btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
</div>
2026-01-15 13:51:44 +08:00
</div>
</template>
<script setup lang="ts">
2026-01-19 16:14:08 +08:00
import { ref, onMounted, computed } from 'vue'
2026-01-15 13:51:44 +08:00
import { getSettings, createSetting, updateSetting, deleteSetting as deleteSettingApi, Setting } from '../../services/api'
2026-01-19 16:14:08 +08:00
import { getOSSConfigs, createOSSConfig, updateOSSConfig, deleteOSSConfig as deleteOSSConfigApi, OSSConfig } from '../../services/api'
2026-01-15 13:51:44 +08:00
import { useToast } from '../../composables/useToast'
2026-01-19 16:14:08 +08:00
import CustomSelect from '../../components/CustomSelect.vue'
2026-01-15 13:51:44 +08:00
const toast = useToast()
const settings = ref<Setting[]>([])
const showModal = ref(false)
const editingSetting = ref(false)
2026-01-19 16:14:08 +08:00
// Tab管理
const activeTab = ref<'settings' | 'oss'>('settings')
// OSS配置管理
const ossConfigs = ref<OSSConfig[]>([])
const showOSSModal = ref(false)
const editingOSSConfig = ref(false)
const ossForm = ref({
id: 0,
name: '',
storageType: '',
accessKey: '',
secretKey: '',
bucket: '',
region: '',
domain: '',
isActive: 0 as number
})
// 存储类型选项
const storageTypeOptions = [
{ value: 'local', label: '本地存储' },
{ value: 'qcloud', label: '腾讯云COS' },
{ value: 'aliyun', label: '阿里云OSS' },
{ value: 'qiniu', label: '七牛云' }
]
// 获取存储类型标签
const getStorageTypeLabel = (type: string): string => {
const option = storageTypeOptions.find(opt => opt.value === type)
return option ? option.label : type
}
2026-01-15 13:51:44 +08:00
const form = ref({
id: 0,
keyName: '',
value: '',
description: ''
})
const fetchSettings = async () => {
try {
settings.value = await getSettings()
} catch (error) {
console.error('Error fetching settings:', error)
2026-01-16 11:06:59 +08:00
toast.showToast('获取系统配置失败', 'error')
2026-01-15 13:51:44 +08:00
}
}
const editSetting = (setting: Setting) => {
editingSetting.value = true
form.value = {
id: setting.id,
keyName: setting.keyName,
value: setting.value,
description: setting.description
}
showModal.value = true
}
const saveSetting = async () => {
try {
if (editingSetting.value) {
await updateSetting(form.value)
2026-01-16 11:06:59 +08:00
toast.showToast('配置更新成功', 'success')
2026-01-15 13:51:44 +08:00
} else {
await createSetting({
keyName: form.value.keyName,
value: form.value.value,
description: form.value.description
})
2026-01-16 11:06:59 +08:00
toast.showToast('配置创建成功', 'success')
2026-01-15 13:51:44 +08:00
}
closeModal()
fetchSettings()
} catch (error) {
console.error('Error saving setting:', error)
2026-01-16 11:06:59 +08:00
toast.showToast(editingSetting.value ? '更新配置失败' : '创建配置失败', 'error')
2026-01-15 13:51:44 +08:00
}
}
const deleteSetting = async (keyName: string) => {
if (confirm(`确定要删除配置项 "${keyName}" 吗?`)) {
try {
await deleteSettingApi(keyName)
2026-01-16 11:06:59 +08:00
toast.showToast('配置删除成功', 'success')
2026-01-15 13:51:44 +08:00
fetchSettings()
} catch (error) {
console.error('Error deleting setting:', error)
2026-01-16 11:06:59 +08:00
toast.showToast('删除配置失败', 'error')
2026-01-15 13:51:44 +08:00
}
}
}
const closeModal = () => {
showModal.value = false
editingSetting.value = false
form.value = {
id: 0,
keyName: '',
value: '',
description: ''
}
}
2026-01-19 16:14:08 +08:00
// OSS配置相关函数
const fetchOSSConfigs = async () => {
try {
ossConfigs.value = await getOSSConfigs()
} catch (error) {
console.error('Error fetching OSS configs:', error)
toast.showToast('获取OSS配置失败', 'error')
}
}
const openOSSModal = () => {
editingOSSConfig.value = false
ossForm.value = {
id: 0,
name: '',
storageType: '',
accessKey: '',
secretKey: '',
bucket: '',
region: '',
domain: '',
isActive: 0
}
showOSSModal.value = true
}
const editOSSConfig = (config: OSSConfig) => {
editingOSSConfig.value = true
ossForm.value = {
id: config.id,
name: config.name,
storageType: config.storageType,
accessKey: config.accessKey === '***' ? '' : config.accessKey, // 如果是加密值,清空让用户重新输入
secretKey: config.secretKey === '***' ? '' : config.secretKey,
bucket: config.bucket,
region: config.region,
domain: config.domain,
isActive: config.isActive
}
showOSSModal.value = true
}
const saveOSSConfig = async () => {
try {
if (editingOSSConfig.value) {
// 更新时,如果密钥是空的,不发送密钥字段
const updateData: any = {
name: ossForm.value.name,
storageType: ossForm.value.storageType,
bucket: ossForm.value.bucket,
region: ossForm.value.region,
domain: ossForm.value.domain,
isActive: ossForm.value.isActive
}
// 只有用户输入了新密钥时才更新
if (ossForm.value.accessKey && ossForm.value.accessKey !== '***') {
updateData.accessKey = ossForm.value.accessKey
}
if (ossForm.value.secretKey && ossForm.value.secretKey !== '***') {
updateData.secretKey = ossForm.value.secretKey
}
await updateOSSConfig(ossForm.value.id, updateData)
toast.showToast('OSS配置更新成功', 'success')
} else {
await createOSSConfig({
name: ossForm.value.name,
storageType: ossForm.value.storageType,
accessKey: ossForm.value.accessKey,
secretKey: ossForm.value.secretKey,
bucket: ossForm.value.bucket,
region: ossForm.value.region,
domain: ossForm.value.domain,
isActive: ossForm.value.isActive,
createdAt: '',
updatedAt: ''
})
toast.showToast('OSS配置创建成功', 'success')
}
closeOSSModal()
fetchOSSConfigs()
} catch (error) {
console.error('Error saving OSS config:', error)
toast.showToast(editingOSSConfig.value ? '更新OSS配置失败' : '创建OSS配置失败', 'error')
}
}
const deleteOSSConfig = async (id: number) => {
if (confirm(`确定要删除这个OSS配置吗?`)) {
try {
await deleteOSSConfigApi(id)
toast.showToast('OSS配置删除成功', 'success')
fetchOSSConfigs()
} catch (error) {
console.error('Error deleting OSS config:', error)
toast.showToast('删除OSS配置失败', 'error')
}
}
}
const closeOSSModal = () => {
showOSSModal.value = false
editingOSSConfig.value = false
ossForm.value = {
id: 0,
name: '',
storageType: '',
accessKey: '',
secretKey: '',
bucket: '',
region: '',
domain: '',
isActive: 0
}
}
2026-01-15 13:51:44 +08:00
onMounted(() => {
fetchSettings()
2026-01-19 16:14:08 +08:00
fetchOSSConfigs()
2026-01-15 13:51:44 +08:00
})
</script>
<style scoped>
2026-01-16 11:06:59 +08:00
/* Scoped styles removed */
</style>