506 lines
20 KiB
Vue
506 lines
20 KiB
Vue
<script setup lang="ts">
|
||
import { Icon } from '@iconify/vue'
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
import { useRouter } from 'vue-router'
|
||
import { toast } from 'vue-sonner'
|
||
|
||
import { BasicPage } from '@/components/global-layout'
|
||
import api from '@/services/api'
|
||
|
||
const router = useRouter()
|
||
const { t } = useI18n()
|
||
|
||
interface Application {
|
||
id: number
|
||
name: string
|
||
}
|
||
|
||
const saving = ref(false)
|
||
const applications = ref<Application[]>([])
|
||
const fileInput = ref<HTMLInputElement | null>(null)
|
||
const selectedFile = ref<File | null>(null)
|
||
const uploading = ref(false)
|
||
|
||
const form = ref({
|
||
app_id: '',
|
||
key: '',
|
||
default_value: '',
|
||
var_type: 'string',
|
||
max_records: 0,
|
||
scope: 'app',
|
||
write_permission: 'admin',
|
||
description: '',
|
||
status: 'active',
|
||
})
|
||
|
||
async function fetchApplications() {
|
||
try {
|
||
const data = await api.get<{ applications: Application[] }>('/dev/applications')
|
||
applications.value = Array.isArray(data?.applications) ? data.applications : []
|
||
}
|
||
catch (error) {
|
||
console.error('获取应用列表失败:', error)
|
||
}
|
||
}
|
||
|
||
const selectedApplication = computed(() => {
|
||
if (form.value.app_id) {
|
||
return applications.value.find(app => String(app.id) === form.value.app_id)
|
||
}
|
||
return null
|
||
})
|
||
|
||
function handleFileSelect(event: Event) {
|
||
const target = event.target as HTMLInputElement
|
||
if (target.files && target.files[0]) {
|
||
selectedFile.value = target.files[0]
|
||
if (!form.value.key) {
|
||
form.value.key = selectedFile.value.name.replace(/\.[^/.]+$/, '')
|
||
}
|
||
}
|
||
}
|
||
|
||
function clearFile() {
|
||
selectedFile.value = null
|
||
if (fileInput.value) {
|
||
fileInput.value.value = ''
|
||
}
|
||
}
|
||
|
||
async function handleSave() {
|
||
if (!form.value.key) {
|
||
toast.error(t('admin.cloudVariables.create.keyRequired'))
|
||
return
|
||
}
|
||
if (!form.value.app_id) {
|
||
toast.error(t('admin.cloudVariables.create.appRequired'))
|
||
return
|
||
}
|
||
|
||
if (form.value.var_type === 'binary') {
|
||
if (!selectedFile.value) {
|
||
toast.error('请选择要上传的文件')
|
||
return
|
||
}
|
||
await uploadFile()
|
||
return
|
||
}
|
||
|
||
saving.value = true
|
||
try {
|
||
await api.post('/dev/cloud-variables', {
|
||
app_id: Number(form.value.app_id),
|
||
key: form.value.key,
|
||
default_value: form.value.default_value,
|
||
var_type: form.value.var_type,
|
||
max_records: form.value.max_records,
|
||
scope: form.value.scope,
|
||
write_permission: form.value.write_permission,
|
||
description: form.value.description,
|
||
status: form.value.status,
|
||
})
|
||
toast.success(t('admin.cloudVariables.create.success'))
|
||
router.push('/admin/cloud-variables')
|
||
}
|
||
catch (error: any) {
|
||
console.error('创建云端变量失败:', error)
|
||
toast.error(error.message || t('admin.cloudVariables.create.failed'))
|
||
}
|
||
finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
async function uploadFile() {
|
||
if (!selectedFile.value || !form.value.app_id || !form.value.key)
|
||
return
|
||
|
||
uploading.value = true
|
||
try {
|
||
const formData = new FormData()
|
||
formData.append('file', selectedFile.value)
|
||
formData.append('app_id', form.value.app_id)
|
||
formData.append('key', form.value.key)
|
||
formData.append('description', form.value.description)
|
||
formData.append('status', form.value.status)
|
||
formData.append('scope', form.value.scope)
|
||
formData.append('write_permission', form.value.write_permission)
|
||
|
||
await api.postFormData('/dev/cloud-variables/upload', formData)
|
||
toast.success(t('admin.cloudVariables.create.success'))
|
||
router.push('/admin/cloud-variables')
|
||
}
|
||
catch (error: any) {
|
||
console.error('上传文件失败:', error)
|
||
toast.error(error.message || '上传文件失败')
|
||
}
|
||
finally {
|
||
uploading.value = false
|
||
}
|
||
}
|
||
|
||
function formatFileSize(bytes: number): string {
|
||
if (bytes === 0)
|
||
return '0 B'
|
||
const k = 1024
|
||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchApplications()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<BasicPage
|
||
:title="t('admin.cloudVariables.addVariable')"
|
||
:description="t('admin.cloudVariables.createDescription')"
|
||
:breadcrumbs="[
|
||
{ title: t('admin.cloudVariables.title'), href: '/admin/cloud-variables' },
|
||
{ title: t('admin.cloudVariables.addVariable') },
|
||
]"
|
||
sticky
|
||
>
|
||
<div class="space-y-6">
|
||
<div class="grid gap-6 lg:grid-cols-3">
|
||
<div class="lg:col-span-2 space-y-6">
|
||
<UiCard>
|
||
<UiCardHeader>
|
||
<UiCardTitle class="flex items-center gap-2">
|
||
<Icon icon="lucide:settings-2" class="size-5" />
|
||
{{ t('admin.cloudVariables.create.config') }}
|
||
</UiCardTitle>
|
||
<UiCardDescription>{{ t('admin.cloudVariables.create.configDesc') }}</UiCardDescription>
|
||
</UiCardHeader>
|
||
<UiCardContent class="space-y-6">
|
||
<div class="space-y-2">
|
||
<UiLabel for="application">
|
||
{{ t('admin.cloudVariables.create.application') }}
|
||
</UiLabel>
|
||
<UiSelect v-model="form.app_id">
|
||
<UiSelectTrigger>
|
||
<UiSelectValue :placeholder="t('admin.cloudVariables.create.selectApp')" />
|
||
</UiSelectTrigger>
|
||
<UiSelectContent>
|
||
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
|
||
{{ app.name }}
|
||
</UiSelectItem>
|
||
</UiSelectContent>
|
||
</UiSelect>
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
<UiLabel for="varType">
|
||
{{ t('admin.cloudVariables.create.type') }}
|
||
</UiLabel>
|
||
<UiSelect id="varType" v-model="form.var_type">
|
||
<UiSelectTrigger>
|
||
<UiSelectValue :placeholder="t('admin.cloudVariables.create.selectType')" />
|
||
</UiSelectTrigger>
|
||
<UiSelectContent>
|
||
<UiSelectItem value="integer">
|
||
{{ t('admin.cloudVariables.types.integer') }}
|
||
</UiSelectItem>
|
||
<UiSelectItem value="decimal">
|
||
{{ t('admin.cloudVariables.types.decimal') }}
|
||
</UiSelectItem>
|
||
<UiSelectItem value="string">
|
||
{{ t('admin.cloudVariables.types.string') }}
|
||
</UiSelectItem>
|
||
<UiSelectItem value="binary">
|
||
二进制
|
||
</UiSelectItem>
|
||
<UiSelectItem value="stream">
|
||
记录
|
||
</UiSelectItem>
|
||
</UiSelectContent>
|
||
</UiSelect>
|
||
</div>
|
||
|
||
<div v-if="form.var_type === 'stream'" class="space-y-2">
|
||
<UiLabel for="maxRecords">
|
||
最大记录数
|
||
</UiLabel>
|
||
<UiInput
|
||
id="maxRecords"
|
||
v-model.number="form.max_records"
|
||
type="number"
|
||
min="0"
|
||
placeholder="0 = 不限制"
|
||
/>
|
||
<p class="text-xs text-muted-foreground">
|
||
超过限制时自动删除最旧的记录,0表示不限制
|
||
</p>
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
<UiLabel for="key">
|
||
{{ t('admin.cloudVariables.create.key') }}
|
||
</UiLabel>
|
||
<UiInput id="key" v-model="form.key" :placeholder="t('admin.cloudVariables.create.keyPlaceholder')" />
|
||
<p class="text-xs text-muted-foreground">
|
||
{{ t('admin.cloudVariables.create.keyHint') }}
|
||
</p>
|
||
</div>
|
||
|
||
<div v-if="form.var_type === 'binary'" class="space-y-2">
|
||
<UiLabel>选择文件</UiLabel>
|
||
<div v-if="!selectedFile" class="border-2 border-dashed rounded-lg p-6 text-center">
|
||
<input
|
||
ref="fileInput"
|
||
type="file"
|
||
class="hidden"
|
||
@change="handleFileSelect"
|
||
>
|
||
<Icon icon="lucide:upload" class="mx-auto h-12 w-12 text-muted-foreground mb-4" />
|
||
<p class="text-sm text-muted-foreground mb-2">
|
||
拖拽文件到此处或点击选择
|
||
</p>
|
||
<UiButton variant="outline" @click="fileInput?.click()">
|
||
选择文件
|
||
</UiButton>
|
||
</div>
|
||
<div v-else class="border rounded-lg p-4">
|
||
<div class="flex items-center justify-between">
|
||
<div class="flex items-center gap-3">
|
||
<Icon icon="lucide:file" class="h-8 w-8 text-muted-foreground" />
|
||
<div>
|
||
<p class="font-medium">
|
||
{{ selectedFile.name }}
|
||
</p>
|
||
<p class="text-sm text-muted-foreground">
|
||
{{ formatFileSize(selectedFile.size) }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<UiButton variant="ghost" size="icon" @click="clearFile">
|
||
<Icon icon="lucide:x" class="h-4 w-4" />
|
||
</UiButton>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="space-y-2">
|
||
<UiLabel for="defaultValue">
|
||
{{ t('admin.cloudVariables.create.defaultValue') }}
|
||
</UiLabel>
|
||
<UiTextarea id="defaultValue" v-model="form.default_value" :placeholder="t('admin.cloudVariables.create.defaultValuePlaceholder')" rows="3" />
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
<UiLabel for="description">
|
||
{{ t('admin.cloudVariables.create.description') }}
|
||
</UiLabel>
|
||
<UiInput id="description" v-model="form.description" :placeholder="t('admin.cloudVariables.create.descriptionPlaceholder')" />
|
||
</div>
|
||
</UiCardContent>
|
||
</UiCard>
|
||
|
||
<UiCard>
|
||
<UiCardHeader>
|
||
<UiCardTitle>{{ t('admin.cloudVariables.create.scopeTitle') }}</UiCardTitle>
|
||
<UiCardDescription>{{ t('admin.cloudVariables.create.scopeDesc') }}</UiCardDescription>
|
||
</UiCardHeader>
|
||
<UiCardContent>
|
||
<UiRadioGroup v-model="form.scope" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div
|
||
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
|
||
:class="form.scope === 'app' ? 'border-primary bg-primary/5' : 'hover:bg-accent'"
|
||
@click="form.scope = 'app'"
|
||
>
|
||
<UiRadioGroupItem value="app" class="mt-0.5" />
|
||
<div>
|
||
<div class="flex items-center gap-2">
|
||
<Icon icon="lucide:globe" class="size-4 text-primary" />
|
||
<p class="font-medium">
|
||
{{ t('admin.cloudVariables.create.appScope') }}
|
||
</p>
|
||
</div>
|
||
<p class="text-sm text-muted-foreground mt-1">
|
||
{{ t('admin.cloudVariables.create.appScopeDesc') }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
|
||
:class="form.scope === 'user' ? 'border-primary bg-primary/5' : 'hover:bg-accent'"
|
||
@click="form.scope = 'user'"
|
||
>
|
||
<UiRadioGroupItem value="user" class="mt-0.5" />
|
||
<div>
|
||
<div class="flex items-center gap-2">
|
||
<Icon icon="lucide:user" class="size-4 text-primary" />
|
||
<p class="font-medium">
|
||
{{ t('admin.cloudVariables.create.userScope') }}
|
||
</p>
|
||
</div>
|
||
<p class="text-sm text-muted-foreground mt-1">
|
||
{{ t('admin.cloudVariables.create.userScopeDesc') }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</UiRadioGroup>
|
||
</UiCardContent>
|
||
</UiCard>
|
||
|
||
<UiCard>
|
||
<UiCardHeader>
|
||
<UiCardTitle>{{ t('admin.cloudVariables.create.permissionTitle') }}</UiCardTitle>
|
||
<UiCardDescription>{{ t('admin.cloudVariables.create.permissionDesc') }}</UiCardDescription>
|
||
</UiCardHeader>
|
||
<UiCardContent>
|
||
<UiRadioGroup v-model="form.write_permission" class="grid grid-cols-1 gap-4">
|
||
<div
|
||
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
|
||
:class="form.write_permission === 'admin' ? 'border-primary bg-primary/5' : 'hover:bg-accent'"
|
||
@click="form.write_permission = 'admin'"
|
||
>
|
||
<UiRadioGroupItem value="admin" class="mt-0.5" />
|
||
<div>
|
||
<div class="flex items-center gap-2">
|
||
<Icon icon="lucide:lock" class="size-4 text-primary" />
|
||
<p class="font-medium">
|
||
{{ t('admin.cloudVariables.create.developerOnly') }}
|
||
</p>
|
||
</div>
|
||
<p class="text-sm text-muted-foreground mt-1">
|
||
{{ form.scope === 'app' ? t('admin.cloudVariables.create.developerOnlyAppDesc') : t('admin.cloudVariables.create.developerOnlyDesc') }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
|
||
:class="form.write_permission === 'user' ? 'border-primary bg-primary/5' : 'hover:bg-accent'"
|
||
@click="form.write_permission = 'user'"
|
||
>
|
||
<UiRadioGroupItem value="user" class="mt-0.5" />
|
||
<div>
|
||
<div class="flex items-center gap-2">
|
||
<Icon icon="lucide:unlock" class="size-4 text-primary" />
|
||
<p class="font-medium">
|
||
{{ t('admin.cloudVariables.create.userWritable') }}
|
||
</p>
|
||
</div>
|
||
<p class="text-sm text-muted-foreground mt-1">
|
||
{{ form.scope === 'app' ? t('admin.cloudVariables.create.userWritableAppDesc') : t('admin.cloudVariables.create.userWritableDesc') }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="form.var_type === 'stream'"
|
||
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
|
||
:class="form.write_permission === 'app_user' ? 'border-primary bg-primary/5' : 'hover:bg-accent'"
|
||
@click="form.write_permission = 'app_user'"
|
||
>
|
||
<UiRadioGroupItem value="app_user" class="mt-0.5" />
|
||
<div>
|
||
<div class="flex items-center gap-2">
|
||
<Icon icon="lucide:users" class="size-4 text-primary" />
|
||
<p class="font-medium">
|
||
应用用户可写
|
||
</p>
|
||
</div>
|
||
<p class="text-sm text-muted-foreground mt-1">
|
||
允许应用端用户通过API写入记录,适合订单、日志等场景
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</UiRadioGroup>
|
||
</UiCardContent>
|
||
</UiCard>
|
||
</div>
|
||
|
||
<div class="space-y-6">
|
||
<UiCard>
|
||
<UiCardHeader>
|
||
<UiCardTitle class="flex items-center gap-2">
|
||
<Icon icon="lucide:eye" class="size-5" />
|
||
{{ t('admin.cloudVariables.create.preview') }}
|
||
</UiCardTitle>
|
||
</UiCardHeader>
|
||
<UiCardContent class="space-y-4">
|
||
<div class="space-y-3">
|
||
<div class="flex justify-between text-sm">
|
||
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.application') }}</span>
|
||
<span>{{ selectedApplication?.name || '-' }}</span>
|
||
</div>
|
||
<div class="flex justify-between text-sm">
|
||
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.type') }}</span>
|
||
<span>{{ t(`.cloudVariables.types.${form.var_type}`) }}</span>
|
||
</div>
|
||
<div v-if="form.var_type === 'stream'" class="flex justify-between text-sm">
|
||
<span class="text-muted-foreground">最大记录数</span>
|
||
<span>{{ form.max_records || '不限制' }}</span>
|
||
</div>
|
||
<div class="flex justify-between text-sm">
|
||
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.key') }}</span>
|
||
<span>{{ form.key || '-' }}</span>
|
||
</div>
|
||
<div class="flex justify-between text-sm">
|
||
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.scope') }}</span>
|
||
<UiBadge variant="outline">
|
||
{{ form.scope === 'app' ? t('admin.cloudVariables.appScope') : t('admin.cloudVariables.userScope') }}
|
||
</UiBadge>
|
||
</div>
|
||
<div class="flex justify-between text-sm">
|
||
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.writePermission') }}</span>
|
||
<UiBadge variant="secondary">
|
||
{{ form.write_permission === 'admin' ? t('admin.cloudVariables.create.developerOnly') : form.write_permission === 'app_user' ? '应用用户可写' : t('admin.cloudVariables.create.userWritable') }}
|
||
</UiBadge>
|
||
</div>
|
||
<div class="flex justify-between text-sm items-center">
|
||
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.status') }}</span>
|
||
<UiSwitch
|
||
:checked="form.status === 'active'"
|
||
@update:checked="form.status = $event ? 'active' : 'inactive'"
|
||
/>
|
||
</div>
|
||
<div class="border-t pt-3 mt-3">
|
||
<div class="text-sm">
|
||
<div class="text-muted-foreground mb-2">
|
||
{{ t('admin.cloudVariables.create.defaultValue') }}
|
||
</div>
|
||
<div class="bg-muted p-2 rounded max-h-[100px] overflow-y-auto text-xs">
|
||
{{ form.default_value || '-' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</UiCardContent>
|
||
</UiCard>
|
||
|
||
<UiCard>
|
||
<UiCardContent class="pt-6">
|
||
<div class="flex flex-col gap-3">
|
||
<UiButton
|
||
class="w-full"
|
||
size="lg"
|
||
:disabled="(saving || uploading) || !form.app_id || !form.key || (form.var_type === 'binary' && !selectedFile)"
|
||
@click="handleSave"
|
||
>
|
||
<Icon v-if="saving || uploading" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
|
||
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
|
||
{{ uploading ? '上传中...' : t('admin.cloudVariables.create.submit') }}
|
||
</UiButton>
|
||
<UiButton
|
||
variant="outline"
|
||
class="w-full"
|
||
@click="router.back()"
|
||
>
|
||
{{ t('admin.cloudVariables.create.cancel') }}
|
||
</UiButton>
|
||
</div>
|
||
</UiCardContent>
|
||
</UiCard>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</BasicPage>
|
||
</template>
|