Files
verify/frontend/src/pages/admin/cloud-variables/create.vue
T

506 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>