初始化2
This commit is contained in:
693
app/pages/developer/config/[id].vue
Normal file
693
app/pages/developer/config/[id].vue
Normal file
@@ -0,0 +1,693 @@
|
||||
<template>
|
||||
<div v-if="mounted" class="app-config-page">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<!-- 应用信息卡片 -->
|
||||
<div class="app-info-card">
|
||||
<!-- 左侧:返回 + 应用图标 + 基本信息 -->
|
||||
<div class="app-info-main">
|
||||
<a-button
|
||||
type="text"
|
||||
class="back-btn"
|
||||
@click="navigateTo('/developer/apps')"
|
||||
>
|
||||
<template #icon><LeftOutlined /></template>
|
||||
</a-button>
|
||||
|
||||
<div class="app-avatar">
|
||||
<img
|
||||
v-if="appInfo?.icon || appInfo?.logo"
|
||||
:src="appInfo?.icon || appInfo?.logo"
|
||||
class="app-avatar-img"
|
||||
/>
|
||||
<span v-else class="app-avatar-placeholder">
|
||||
{{ appInfo?.productName?.charAt(0)?.toUpperCase() || '?' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="app-meta">
|
||||
<div class="app-name-row">
|
||||
<span v-if="appInfo?.productName" class="app-name">{{ appInfo.productName }}</span>
|
||||
<span v-else-if="appInfoLoading" class="app-name-skeleton">
|
||||
<a-skeleton-input active size="small" style="width: 120px" />
|
||||
</span>
|
||||
<span v-else class="app-name app-name-unknown">未知应用</span>
|
||||
|
||||
<a-tag
|
||||
v-if="appTypeLabel"
|
||||
:color="appTypeColor"
|
||||
class="app-type-tag"
|
||||
>{{ appTypeLabel }}</a-tag>
|
||||
|
||||
<a-tag
|
||||
v-if="appInfo?.status !== undefined"
|
||||
:color="appStatusColor"
|
||||
class="app-status-tag"
|
||||
>{{ appStatusLabel }}</a-tag>
|
||||
</div>
|
||||
|
||||
<div class="app-sub-info">
|
||||
<span v-if="appInfo?.productCode" class="app-code">
|
||||
<CodeOutlined class="sub-icon" />
|
||||
{{ appInfo.productCode }}
|
||||
</span>
|
||||
<span v-if="appInfo?.domain" class="app-domain">
|
||||
<GlobalOutlined class="sub-icon" />
|
||||
{{ appInfo.domain }}
|
||||
</span>
|
||||
<span v-if="appInfo?.createTime" class="app-create-time">
|
||||
<ClockCircleOutlined class="sub-icon" />
|
||||
创建于 {{ appInfo.createTime?.slice(0, 10) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:页面说明 -->
|
||||
<div class="page-desc-block">
|
||||
<div class="page-title-text">应用配置</div>
|
||||
<div class="page-desc-text">管理应用的 API、回调、支付等配置</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置类型标签页 -->
|
||||
<a-tabs v-model:activeKey="activeType" class="config-tabs" @change="handleTypeChange">
|
||||
<a-tab-pane
|
||||
v-for="type in configTypes"
|
||||
:key="type.key"
|
||||
:tab="type.name"
|
||||
>
|
||||
<!-- 配置表单 -->
|
||||
<div v-if="!loading" class="config-content">
|
||||
<a-alert
|
||||
v-if="type.description"
|
||||
:message="type.description"
|
||||
type="info"
|
||||
show-icon
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<a-form
|
||||
:model="formData"
|
||||
layout="vertical"
|
||||
class="config-form"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col
|
||||
v-for="field in type.configs"
|
||||
:key="field.key"
|
||||
:span="field.type === 'textarea' ? 24 : 12"
|
||||
>
|
||||
<a-form-item
|
||||
:label="field.label"
|
||||
:name="field.key"
|
||||
:required="field.required"
|
||||
:rules="field.required ? [{ required: true, message: `请输入${field.label}` }] : []"
|
||||
>
|
||||
<!-- 密码输入 -->
|
||||
<a-input-password
|
||||
v-if="field.type === 'password'"
|
||||
v-model:value="formData[field.key]"
|
||||
:placeholder="field.placeholder"
|
||||
/>
|
||||
|
||||
<!-- 文本域 -->
|
||||
<a-textarea
|
||||
v-else-if="field.type === 'textarea'"
|
||||
v-model:value="formData[field.key]"
|
||||
:placeholder="field.placeholder"
|
||||
:rows="4"
|
||||
/>
|
||||
|
||||
<!-- 下拉选择 -->
|
||||
<a-select
|
||||
v-else-if="field.type === 'select'"
|
||||
v-model:value="formData[field.key]"
|
||||
:placeholder="field.placeholder"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="opt in field.options"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<!-- 开关 -->
|
||||
<a-switch
|
||||
v-else-if="field.type === 'switch'"
|
||||
v-model:checked="formData[field.key]"
|
||||
/>
|
||||
|
||||
<!-- JSON 编辑器 -->
|
||||
<a-textarea
|
||||
v-else-if="field.type === 'json'"
|
||||
v-model:value="formData[field.key]"
|
||||
:placeholder="field.placeholder"
|
||||
:rows="8"
|
||||
style="font-family: monospace"
|
||||
/>
|
||||
|
||||
<!-- 数字输入 -->
|
||||
<a-input-number
|
||||
v-else-if="field.type === 'number'"
|
||||
v-model:value="formData[field.key]"
|
||||
:placeholder="field.placeholder"
|
||||
style="width: 100%"
|
||||
/>
|
||||
|
||||
<!-- 普通输入 -->
|
||||
<a-input
|
||||
v-else
|
||||
v-model:value="formData[field.key]"
|
||||
:placeholder="field.placeholder"
|
||||
/>
|
||||
|
||||
<!-- 字段说明 -->
|
||||
<div v-if="field.description" class="field-desc">
|
||||
<InfoCircleOutlined class="field-desc-icon" />
|
||||
{{ field.description }}
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="form-actions">
|
||||
<a-space>
|
||||
<a-button @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
<a-button type="primary" :loading="saving" @click="handleSave">
|
||||
保存配置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<div v-else class="loading-wrap">
|
||||
<a-spin size="large" tip="加载中..." />
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { InfoCircleOutlined, LeftOutlined, CodeOutlined, GlobalOutlined, ClockCircleOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { getConfigsMap, listAppConfig, saveAppConfig, updateAppConfig } from '@/api/app/appConfig'
|
||||
import type { AppConfig, ConfigType, ConfigField } from '@/api/app/appConfig/model'
|
||||
import { getAppProduct } from '@/api/app/appProduct'
|
||||
import type { AppProduct } from '@/api/app/appProduct/model'
|
||||
|
||||
definePageMeta({ layout: 'developer' })
|
||||
useHead({ title: '应用配置 - 开发者中心' })
|
||||
|
||||
const route = useRoute()
|
||||
const mounted = ref(false)
|
||||
const productId = computed(() => {
|
||||
if (!mounted.value) return 0
|
||||
return parseInt(route.params.id as string || route.query.productId as string || '0')
|
||||
})
|
||||
|
||||
// ── 应用信息 ──────────────────────────────────────────
|
||||
const appInfo = ref<AppProduct | null>(null)
|
||||
const appInfoLoading = ref(false)
|
||||
|
||||
async function loadAppInfo(id: number) {
|
||||
if (!id) return
|
||||
appInfoLoading.value = true
|
||||
try {
|
||||
appInfo.value = await getAppProduct(id)
|
||||
// 同步页面 title
|
||||
if (appInfo.value?.productName) {
|
||||
useHead({ title: `${appInfo.value.productName} - 应用配置` })
|
||||
}
|
||||
} catch {
|
||||
// 加载应用信息失败不阻塞主流程,静默处理
|
||||
} finally {
|
||||
appInfoLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const APP_TYPE_MAP: Record<string, { label: string; color: string }> = {
|
||||
web: { label: 'Web 应用', color: 'blue' },
|
||||
miniprogram: { label: '小程序', color: 'green' },
|
||||
mobile: { label: '移动 App', color: 'purple' },
|
||||
api: { label: 'API 服务', color: 'orange' },
|
||||
internal: { label: '内部工具', color: 'default' },
|
||||
}
|
||||
|
||||
const appTypeLabel = computed(() => {
|
||||
const t = appInfo.value?.appType
|
||||
if (t === 10) return '网站'
|
||||
if (t === 20) return '微信小程序'
|
||||
if (t === 30) return '抖音小程序'
|
||||
if (t === 40) return '百度小程序'
|
||||
if (t === 50) return '支付宝小程序'
|
||||
if (t === 60) return 'Android APP'
|
||||
if (t === 70) return 'iOS APP'
|
||||
if (t === 80) return 'macOS 应用'
|
||||
if (t === 90) return 'Windows 应用'
|
||||
if (t === 100) return '插件'
|
||||
return t?.toString() || ''
|
||||
})
|
||||
|
||||
const appTypeColor = computed(() => {
|
||||
const t = appInfo.value?.appType
|
||||
if (t === 20 || t === 30 || t === 40 || t === 50) return 'green'
|
||||
if (t === 60 || t === 70 || t === 80 || t === 90) return 'purple'
|
||||
return 'blue'
|
||||
})
|
||||
|
||||
const APP_STATUS_MAP: Record<number, { label: string; color: string }> = {
|
||||
0: { label: '未开通', color: 'default' },
|
||||
1: { label: '运行中', color: 'success' },
|
||||
2: { label: '维护中', color: 'warning' },
|
||||
3: { label: '已关闭', color: 'error' },
|
||||
4: { label: '已欠费', color: 'error' },
|
||||
5: { label: '违规关停', color: 'error' },
|
||||
}
|
||||
|
||||
const appStatusLabel = computed(() => {
|
||||
const s = appInfo.value?.status
|
||||
return s !== undefined ? (APP_STATUS_MAP[s]?.label ?? `状态${s}`) : ''
|
||||
})
|
||||
|
||||
const appStatusColor = computed(() => {
|
||||
const s = appInfo.value?.status
|
||||
return s !== undefined ? (APP_STATUS_MAP[s]?.color ?? 'default') : 'default'
|
||||
})
|
||||
|
||||
// 配置类型定义
|
||||
const configTypes: ConfigType[] = [
|
||||
{
|
||||
key: 'api',
|
||||
name: 'API 配置',
|
||||
icon: '🔌',
|
||||
description: '配置应用的 API 基础信息,包括地址、超时等',
|
||||
configs: [
|
||||
{ key: 'api.baseUrl', label: 'API 基础地址', type: 'input', placeholder: 'https://api.example.com' },
|
||||
{ key: 'api.timeout', label: '请求超时(秒)', type: 'number', placeholder: '30', defaultValue: 30 },
|
||||
{ key: 'api.enableCache', label: '启用缓存', type: 'switch', defaultValue: true },
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'callback',
|
||||
name: '回调地址',
|
||||
icon: '🔔',
|
||||
description: '配置第三方平台的回调地址,用于接收异步通知',
|
||||
configs: [
|
||||
{ key: 'callback.url', label: '回调 URL', type: 'input', placeholder: 'https://yourdomain.com/callback', required: true },
|
||||
{ key: 'callback.secret', label: '回调密钥', type: 'password', placeholder: '用于验证回调签名' },
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'wechat',
|
||||
name: '微信配置',
|
||||
icon: '💬',
|
||||
description: '配置微信小程序或公众号的 AppID 和 AppSecret',
|
||||
configs: [
|
||||
{ key: 'wechat.appId', label: 'AppID', type: 'input', placeholder: 'wx1234567890abcdef', required: true },
|
||||
{ key: 'wechat.appSecret', label: 'AppSecret', type: 'password', placeholder: '微信小程序密钥' },
|
||||
{ key: 'wechat.type', label: '应用类型', type: 'select', defaultValue: 'miniprogram', options: [
|
||||
{ label: '小程序', value: 'miniprogram' },
|
||||
{ label: '公众号', value: 'mp' },
|
||||
{ label: '网页应用', value: 'web' },
|
||||
]},
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'payment',
|
||||
name: '支付配置',
|
||||
icon: '💰',
|
||||
description: '配置微信支付、支付宝等支付渠道',
|
||||
configs: [
|
||||
{ key: 'payment.enabled', label: '启用支付', type: 'switch', defaultValue: false },
|
||||
{ key: 'payment.provider', label: '支付渠道', type: 'select', defaultValue: 'wechat', options: [
|
||||
{ label: '微信支付', value: 'wechat' },
|
||||
{ label: '支付宝', value: 'alipay' },
|
||||
{ label: 'Stripe', value: 'stripe' },
|
||||
]},
|
||||
{ key: 'payment.mchId', label: '商户号', type: 'input', placeholder: '支付商户号' },
|
||||
{ key: 'payment.apiKey', label: 'API 密钥', type: 'password', placeholder: '支付 API 密钥' },
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'git',
|
||||
name: 'Git 仓库',
|
||||
icon: '🔧',
|
||||
description: '配置代码仓库地址,用于持续集成和部署',
|
||||
configs: [
|
||||
{ key: 'git.repository', label: '仓库地址', type: 'input', placeholder: 'https://github.com/user/repo.git' },
|
||||
{ key: 'git.branch', label: '默认分支', type: 'input', placeholder: 'main', defaultValue: 'main' },
|
||||
{ key: 'git.accessToken', label: '访问令牌', type: 'password', placeholder: 'Git 访问令牌' },
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
const activeType = ref('api')
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
|
||||
// 表单数据(根据当前配置类型动态生成)
|
||||
const formData = reactive<Record<string, any>>({})
|
||||
|
||||
// 原始数据(用于重置)
|
||||
let originalData: Record<string, any> = {}
|
||||
|
||||
// 已有配置映射(用于更新时获取 configId)
|
||||
const existingConfigs = ref<Map<string, number>>(new Map())
|
||||
|
||||
// 初始化表单数据
|
||||
function initForm() {
|
||||
const currentType = configTypes.find(t => t.key === activeType.value)
|
||||
if (!currentType) return
|
||||
|
||||
// 清空表单
|
||||
Object.keys(formData).forEach(key => delete formData[key])
|
||||
|
||||
// 设置默认值
|
||||
currentType.configs.forEach(field => {
|
||||
if (field.defaultValue !== undefined) {
|
||||
formData[field.key] = field.defaultValue
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 加载配置
|
||||
async function loadConfigs() {
|
||||
const id = parseInt(route.params.id as string || route.query.productId as string || '0')
|
||||
if (!id || id === 0) {
|
||||
message.warning('缺少应用 ID')
|
||||
navigateTo('/developer/apps')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const configs = await getConfigsMap(id)
|
||||
originalData = { ...configs }
|
||||
|
||||
// 合并配置到表单
|
||||
Object.keys(configs).forEach(key => {
|
||||
formData[key] = configs[key]
|
||||
})
|
||||
|
||||
// 加载配置列表以获取 configId
|
||||
const configList = await listAppConfig({ productId: id })
|
||||
existingConfigs.value = new Map()
|
||||
configList.forEach(config => {
|
||||
if (config.configId && config.configKey) {
|
||||
existingConfigs.value.set(config.configKey, config.configId)
|
||||
}
|
||||
})
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '加载配置失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换配置类型
|
||||
function handleTypeChange() {
|
||||
initForm()
|
||||
loadConfigs()
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
async function handleSave() {
|
||||
const currentType = configTypes.find(t => t.key === activeType.value)
|
||||
if (!currentType) return
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const id = parseInt(route.params.id as string || route.query.productId as string || '0')
|
||||
if (!id || id === 0) {
|
||||
message.error('缺少应用 ID')
|
||||
return
|
||||
}
|
||||
|
||||
const savePromises = currentType.configs.map(async (field) => {
|
||||
const value = formData[field.key]
|
||||
const configId = existingConfigs.value.get(field.key)
|
||||
|
||||
const config: AppConfig = {
|
||||
configId,
|
||||
productId: id,
|
||||
configKey: field.key,
|
||||
configValue: value !== undefined && value !== null ? String(value) : '',
|
||||
configType: activeType.value,
|
||||
isEncrypted: 0,
|
||||
isSecret: 0,
|
||||
description: field.label,
|
||||
sortNumber: field.type === 'textarea' ? 999 : 0,
|
||||
} as AppConfig
|
||||
|
||||
// 如果存在 configId 则更新,否则新增
|
||||
if (configId) {
|
||||
await updateAppConfig(config)
|
||||
} else {
|
||||
await saveAppConfig(config)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(savePromises)
|
||||
|
||||
message.success('配置保存成功')
|
||||
originalData = { ...formData }
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '保存配置失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
Object.keys(formData).forEach(key => {
|
||||
formData[key] = originalData[key]
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 标记页面已挂载
|
||||
mounted.value = true
|
||||
|
||||
// 检查是否有 productId 参数
|
||||
const paramId = route.params.id || route.query.productId
|
||||
if (!paramId) {
|
||||
navigateTo('/developer/apps')
|
||||
return
|
||||
}
|
||||
const id = parseInt(paramId as string || '0')
|
||||
loadAppInfo(id)
|
||||
initForm()
|
||||
loadConfigs()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-config-page {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* ── 页面头部 ─────────────────────────── */
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.app-info-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 12px;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
/* 左侧:返回按钮 + 图标 + 基本信息 */
|
||||
.app-info-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
flex-shrink: 0;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
padding: 0 6px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.back-btn:hover {
|
||||
background: #f0f5ff;
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
/* 应用头像 */
|
||||
.app-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.app-avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.app-avatar-placeholder {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* 应用 Meta 信息 */
|
||||
.app-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.app-name-unknown {
|
||||
color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.app-type-tag,
|
||||
.app-status-tag {
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
padding: 0 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 次级信息行 */
|
||||
.app-sub-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.app-code,
|
||||
.app-domain,
|
||||
.app-create-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-family: 'SFMono-Regular', Consolas, monospace;
|
||||
}
|
||||
|
||||
.app-domain,
|
||||
.app-create-time {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.sub-icon {
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 右侧:页面说明 */
|
||||
.page-desc-block {
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.page-title-text {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.page-desc-text {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.35);
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
/* ── 配置区域 ─────────────────────────── */
|
||||
.config-tabs {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.config-content {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.config-form {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.field-desc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.field-desc-icon {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.loading-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user