You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
234 lines
9.0 KiB
234 lines
9.0 KiB
<template>
|
|
<div>
|
|
<!-- 列表视图 -->
|
|
<div v-show="view === 'list'">
|
|
<div class="flex items-center justify-between mb-5">
|
|
<div>
|
|
<h2 class="text-lg font-semibold text-slate-800">全局环境配置</h2>
|
|
<p class="text-xs text-slate-400 mt-1">全局第三方服务配置(AI / 翻译等),按区域分组,所有应用共享。修改后各服务约 10 分钟内自动同步。</p>
|
|
</div>
|
|
<button class="btn btn-primary btn-sm text-white" @click="showForm()">+ 新增配置</button>
|
|
</div>
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<span class="text-sm text-slate-500">区域</span>
|
|
<select v-model.number="filterRegion" class="select select-bordered select-sm" @change="loadData">
|
|
<option v-for="r in REGIONS" :key="r.value" :value="r.value">{{ r.label }}</option>
|
|
</select>
|
|
<input v-model="keyword" class="input input-bordered input-sm w-48" placeholder="按分组/键搜索" />
|
|
</div>
|
|
<div class="bg-white rounded-xl shadow-sm border border-slate-100">
|
|
<div class="overflow-x-auto">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>键</th><th>值</th><th>描述</th><th class="text-right">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-if="loading">
|
|
<td colspan="4" class="text-center py-8"><span class="loading loading-spinner loading-sm"></span></td>
|
|
</tr>
|
|
<tr v-else-if="!filteredRows.length">
|
|
<td colspan="4" class="text-center text-slate-400 py-8">暂无配置</td>
|
|
</tr>
|
|
<template v-for="grp in groupedRows" :key="grp.key">
|
|
<tr class="bg-slate-100/80">
|
|
<td colspan="4" class="py-2">
|
|
<span class="font-semibold text-slate-600 text-sm">▸ {{ grp.label }}</span>
|
|
<span class="text-xs text-slate-400 ml-2">{{ grp.items.length }} 项</span>
|
|
</td>
|
|
</tr>
|
|
<tr v-for="c in grp.items" :key="c.id" class="hover">
|
|
<td class="font-mono text-sm font-medium">{{ c.key || '' }}</td>
|
|
<td class="text-sm text-slate-500 max-w-xs truncate" :title="c.value || ''">{{ c.value || '' }}</td>
|
|
<td class="text-sm text-slate-400">{{ c.description || '' }}</td>
|
|
<td class="text-right">
|
|
<div class="inline-flex gap-1">
|
|
<button class="btn btn-ghost btn-xs" @click="showForm(c)">编辑</button>
|
|
<button class="btn btn-ghost btn-xs text-red-500" @click="delCfg(c)">删除</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 表单视图 -->
|
|
<div v-show="view === 'form'">
|
|
<div class="flex items-center gap-3 mb-5">
|
|
<button class="btn btn-ghost btn-sm" @click="showList()">← 返回列表</button>
|
|
<h2 class="text-lg font-semibold text-slate-800">{{ isEditing ? '编辑配置' : '新增配置' }}</h2>
|
|
</div>
|
|
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-6 max-w-2xl">
|
|
<form @submit.prevent="saveCfg">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">区域 *</span></label>
|
|
<select v-model.number="form.region" class="select select-bordered w-full">
|
|
<option v-for="r in REGIONS" :key="r.value" :value="r.value">{{ r.label }}</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">分组 (group)</span></label>
|
|
<input v-model="form.group" type="text" class="input input-bordered w-full" placeholder="如 ai / translate / asr" />
|
|
</div>
|
|
</div>
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">键 (key) *</span></label>
|
|
<input v-model="form.key" type="text" class="input input-bordered w-full" required placeholder="如 openai_api_key" />
|
|
</div>
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">值 (value)</span></label>
|
|
<textarea v-model="form.value" class="textarea textarea-bordered w-full font-mono text-sm" rows="3"></textarea>
|
|
</div>
|
|
<div class="form-control mb-6">
|
|
<label class="label"><span class="label-text font-medium">描述</span></label>
|
|
<input v-model="form.description" type="text" class="input input-bordered w-full" />
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button type="button" class="btn btn-ghost" @click="showList()">取消</button>
|
|
<button type="submit" class="btn btn-primary text-white" :disabled="submitting">保存</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
import { useApi } from '~/composables/useApi'
|
|
import { useToast } from '~/composables/useToast'
|
|
|
|
definePageMeta({ title: '全局环境配置' })
|
|
|
|
// 区域枚举(与 pb.Region 一一对应),按区域分组管理,始终选定一个区域,不带「全部区域」
|
|
const REGIONS = [
|
|
{ value: 1, label: '中国' }, { value: 2, label: '美国' }, { value: 3, label: '东亚' },
|
|
{ value: 4, label: '东南亚' }, { value: 5, label: '南亚' }, { value: 6, label: '中东' },
|
|
{ value: 7, label: '欧洲' }, { value: 8, label: '俄罗斯' }, { value: 9, label: '非洲' },
|
|
{ value: 10, label: '南美' }, { value: 11, label: '北美' }, { value: 12, label: '大洋洲' },
|
|
{ value: 13, label: '巴西' }, { value: 14, label: '印度' },
|
|
]
|
|
|
|
interface GlobalConfig {
|
|
id: number
|
|
region: number
|
|
group: string
|
|
key: string
|
|
value: string
|
|
description: string
|
|
}
|
|
|
|
const { webApi } = useApi()
|
|
const { success, error } = useToast()
|
|
|
|
const view = ref<'list' | 'form'>('list')
|
|
const items = ref<GlobalConfig[]>([])
|
|
const loading = ref(false)
|
|
const submitting = ref(false)
|
|
const isEditing = ref(false)
|
|
const filterRegion = ref(1)
|
|
const keyword = ref('')
|
|
|
|
const form = reactive({ id: 0, region: 1, group: '', key: '', value: '', description: '' })
|
|
|
|
// 关键字过滤(按 group + key)
|
|
const filteredRows = computed(() => {
|
|
const kw = keyword.value.trim().toLowerCase()
|
|
if (!kw) return items.value
|
|
return items.value.filter((c) => ((c.group || '') + (c.key || '')).toLowerCase().includes(kw))
|
|
})
|
|
|
|
// 按 group 分组:相同组归在一起(未分组排最前)
|
|
const groupedRows = computed(() => {
|
|
const groups: Record<string, GlobalConfig[]> = {}
|
|
filteredRows.value.forEach((c) => {
|
|
const g = c.group || ''
|
|
;(groups[g] = groups[g] || []).push(c)
|
|
})
|
|
const names = Object.keys(groups).sort((a, b) => {
|
|
if (a === '') return -1
|
|
if (b === '') return 1
|
|
return a < b ? -1 : 1
|
|
})
|
|
return names.map((g) => ({ key: g, label: g || '未分组', items: groups[g] }))
|
|
})
|
|
|
|
async function loadData() {
|
|
loading.value = true
|
|
try {
|
|
const data = await webApi<{ items: GlobalConfig[] }>('getglobalconfigs', { region: filterRegion.value })
|
|
items.value = data?.items ?? []
|
|
} catch (e: any) {
|
|
error(e?.message ?? '获取配置失败')
|
|
items.value = []
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function showList() {
|
|
view.value = 'list'
|
|
loadData()
|
|
}
|
|
|
|
function showForm(cfg?: GlobalConfig) {
|
|
if (cfg) {
|
|
isEditing.value = true
|
|
form.id = cfg.id || 0
|
|
form.region = cfg.region || 1
|
|
form.group = cfg.group || ''
|
|
form.key = cfg.key || ''
|
|
form.value = cfg.value || ''
|
|
form.description = cfg.description || ''
|
|
} else {
|
|
isEditing.value = false
|
|
form.id = 0
|
|
// 新增时默认沿用当前筛选区域(回退到中国)
|
|
form.region = filterRegion.value > 0 ? filterRegion.value : 1
|
|
form.group = ''
|
|
form.key = ''
|
|
form.value = ''
|
|
form.description = ''
|
|
}
|
|
view.value = 'form'
|
|
}
|
|
|
|
async function saveCfg() {
|
|
submitting.value = true
|
|
try {
|
|
const data: Record<string, unknown> = {
|
|
region: Number(form.region) || 0,
|
|
group: form.group.trim(),
|
|
key: form.key.trim(),
|
|
value: form.value,
|
|
description: form.description.trim(),
|
|
}
|
|
if (isEditing.value) {
|
|
data.id = Number(form.id)
|
|
await webApi('updateglobalconfig', data)
|
|
} else {
|
|
await webApi('addglobalconfig', data)
|
|
}
|
|
success('保存成功')
|
|
showList()
|
|
} catch (e: any) {
|
|
error(e?.message ?? '保存失败')
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
function delCfg(c: GlobalConfig) {
|
|
if (!window.confirm('确定删除该配置项吗?')) return
|
|
webApi('delglobalconfig', { ids: [c.id] })
|
|
.then(() => { success('删除成功'); loadData() })
|
|
.catch((e: any) => error(e?.message ?? '删除失败'))
|
|
}
|
|
|
|
onMounted(loadData)
|
|
</script>
|
|
|