后端优化: - 新增 resolvePassword 函数,消除密码获取重复逻辑 - 新增 parseMongoOptions 函数,消除 Options 解析重复 - 新增 testConnectionByType 统一连接测试调用 - 重构 loadMongoDatabasesWithOptions 接收解析后参数 - 删除重复代码 37 行 前端优化: - 新增 useVisibleDatabases composable - 统一 visible_databases 解析和过滤逻辑 - 简化错误处理,移除 try-catch 包装 - 删除重复代码 22 行 代码质量: - 消除 6 处重复代码块 - 新增 5 个可复用函数 - 提升代码可维护性和可测试性
714 lines
20 KiB
Vue
714 lines
20 KiB
Vue
<template>
|
||
<a-modal
|
||
v-model:visible="visible"
|
||
title="数据库连接配置"
|
||
width="600px"
|
||
:body-style="{ padding: '16px 20px' }"
|
||
@cancel="handleCancel"
|
||
>
|
||
<!-- 错误提示区域 -->
|
||
<a-alert
|
||
v-if="errorMessage"
|
||
type="error"
|
||
show-icon
|
||
closable
|
||
@close="errorMessage = ''"
|
||
class="error-alert"
|
||
>
|
||
{{ errorMessage }}
|
||
</a-alert>
|
||
|
||
<a-form :model="form" :rules="rules" ref="formRef" layout="horizontal" :label-col-props="{ span: 6 }"
|
||
:wrapper-col-props="{ span: 18 }" size="small">
|
||
<a-form-item label="连接名称" field="name">
|
||
<a-input v-model="form.name" placeholder="请输入连接名称" size="small"/>
|
||
</a-form-item>
|
||
<a-form-item label="数据库类型" field="type">
|
||
<a-select v-model="form.type" placeholder="请选择数据库类型" @change="handleTypeChange" size="small">
|
||
<a-option value="mysql">MySQL</a-option>
|
||
<a-option value="redis">Redis</a-option>
|
||
<a-option value="mongo">MongoDB</a-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
<a-form-item label="主机地址" field="host">
|
||
<a-input v-model="form.host" placeholder="请输入主机地址" size="small"/>
|
||
</a-form-item>
|
||
<a-form-item label="端口" field="port">
|
||
<a-input-number v-model="form.port" :min="1" :max="65535" placeholder="请输入端口" style="width: 100%"
|
||
size="small"/>
|
||
</a-form-item>
|
||
<a-form-item label="用户名" field="username" v-if="form.type !== 'redis'">
|
||
<a-input v-model="form.username" placeholder="请输入用户名" size="small"/>
|
||
</a-form-item>
|
||
<a-form-item label="密码" field="password">
|
||
<div v-if="props.connectionId && !isPasswordChanged" class="password-display">
|
||
<a-input
|
||
value="已保存的密码"
|
||
disabled
|
||
class="password-input"
|
||
size="small"
|
||
/>
|
||
<a-button type="text" size="mini" @click="isPasswordChanged = true">
|
||
修改密码
|
||
</a-button>
|
||
</div>
|
||
<a-input-password
|
||
v-else
|
||
v-model="form.password"
|
||
:placeholder="getPasswordPlaceholder()"
|
||
size="small"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item :label="form.type === 'redis' ? '数据库编号' : '数据库名'" field="database">
|
||
<a-input v-model="form.database"
|
||
:placeholder="form.type === 'redis' ? 'Redis DB 编号 (0-15,默认为0)' : '可选,留空则连接所有数据库'"
|
||
:max-length="100"
|
||
size="small"/>
|
||
</a-form-item>
|
||
|
||
<!-- 数据库过滤选项(仅 MySQL 和 MongoDB) -->
|
||
<template v-if="form.type === 'mysql' || form.type === 'mongo'">
|
||
<a-form-item label="可见数据库" field="visibleDatabases">
|
||
<div class="database-list-container">
|
||
<!-- 顶部工具栏 -->
|
||
<div class="list-toolbar">
|
||
<a-button
|
||
type="outline"
|
||
size="small"
|
||
@click="loadAllDatabases"
|
||
:loading="loadingDatabases"
|
||
:disabled="!canLoadDatabases"
|
||
>
|
||
<template #icon>
|
||
<icon-refresh />
|
||
</template>
|
||
{{ allDatabases.length > 0 ? '重新加载' : '加载数据库列表' }}
|
||
</a-button>
|
||
|
||
<template v-if="allDatabases.length > 0">
|
||
<div class="toolbar-stats">
|
||
已选 {{ selectedDatabases.length }} / {{ allDatabases.length }}
|
||
</div>
|
||
<a-button size="small" @click="handleSelectAll(true)">全选</a-button>
|
||
<a-button size="small" @click="handleInvertSelection">反选</a-button>
|
||
<a-button size="small" @click="handleSelectAll(false)">清空</a-button>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<div v-if="!loadingDatabases && allDatabases.length === 0" class="list-empty">
|
||
<icon-storage />
|
||
<span>点击上方按钮加载数据库列表</span>
|
||
</div>
|
||
|
||
<!-- 数据库列表(复选框) -->
|
||
<div v-else class="database-checkbox-list">
|
||
<div
|
||
v-for="db in allDatabases"
|
||
:key="db"
|
||
class="database-checkbox-item"
|
||
>
|
||
<a-checkbox
|
||
:model-value="selectedDatabases.includes(db)"
|
||
:disabled="loadingDatabases"
|
||
@change="(checked: boolean) => toggleDatabase(db, checked)"
|
||
>
|
||
{{ db }}
|
||
</a-checkbox>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 提示信息 -->
|
||
<div v-if="allDatabases.length > 0" class="list-tip">
|
||
<icon-info-circle />
|
||
未选择任何数据库时,将展示所有数据库
|
||
</div>
|
||
</div>
|
||
</a-form-item>
|
||
</template>
|
||
|
||
<!-- MongoDB 专用选项 -->
|
||
<template v-if="form.type === 'mongo'">
|
||
<a-form-item label="认证数据库" field="options.authSource">
|
||
<a-input v-model="optionsForm.authSource" placeholder="留空则使用 admin" size="small"/>
|
||
<template #extra>
|
||
<span class="form-item-extra">MongoDB 用户所在的数据库,通常为 admin(可选)</span>
|
||
</template>
|
||
</a-form-item>
|
||
</template>
|
||
</a-form>
|
||
<template #footer>
|
||
<a-space size="small">
|
||
<a-button @click="handleTest" :loading="testing" size="small">测试连接</a-button>
|
||
<a-button @click="handleCancel" size="small">取消</a-button>
|
||
<a-button type="primary" @click="handleSubmit" :loading="saving" size="small">保存</a-button>
|
||
</a-space>
|
||
</template>
|
||
</a-modal>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import {reactive, ref, watch, computed, nextTick} from 'vue'
|
||
import {Message} from '@arco-design/web-vue'
|
||
import {
|
||
IconCheckCircle,
|
||
IconClose,
|
||
IconInfoCircle,
|
||
IconRefresh,
|
||
IconStorage
|
||
} from '@arco-design/web-vue/es/icon'
|
||
import {
|
||
ListDbConnections,
|
||
SaveDbConnection
|
||
} from '../../../wailsjs/wailsjs/go/main/App'
|
||
import { getConnectionFailedTip, getLoadFailedTip } from '@/utils/database-error'
|
||
import { useVisibleDatabases } from '@/composables/useVisibleDatabases'
|
||
|
||
// 使用 defineModel 简化 v-model:visible 双向绑定(Vue 3.5+)
|
||
const visible = defineModel('visible', { type: Boolean, default: false })
|
||
|
||
// 使用 TypeScript 泛型语法(Vue 3.5+)
|
||
const props = defineProps<{
|
||
connectionId?: number | null
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
success: []
|
||
}>()
|
||
|
||
const formRef = ref<any>(null)
|
||
const saving = ref(false)
|
||
const testing = ref(false)
|
||
const errorMessage = ref('')
|
||
// 是否修改密码(编辑模式下)
|
||
const isPasswordChanged = ref(false)
|
||
|
||
// 数据库过滤相关
|
||
const { parse: parseVisibleDatabases, filter: filterVisibleDatabases } = useVisibleDatabases()
|
||
const loadingDatabases = ref(false)
|
||
const allDatabases = ref<string[]>([])
|
||
const selectedDatabases = ref<string[]>([])
|
||
|
||
// 是否可以加载数据库列表
|
||
const canLoadDatabases = computed(() =>
|
||
!!(form.host && form.port && form.username && (form.password || props.connectionId))
|
||
)
|
||
|
||
const form = reactive({
|
||
name: '',
|
||
type: 'mysql',
|
||
host: '127.0.0.1',
|
||
port: 3306,
|
||
username: '',
|
||
password: '',
|
||
database: '',
|
||
options: '',
|
||
visibleDatabases: ''
|
||
})
|
||
|
||
// 选项表单(用于表单输入)
|
||
const optionsForm = reactive({
|
||
authSource: ''
|
||
})
|
||
|
||
// 将 options JSON 字符串解析为 optionsForm
|
||
const parseOptionsToForm = (optionsStr: string) => {
|
||
if (!optionsStr || optionsStr.trim() === '') {
|
||
optionsForm.authSource = ''
|
||
return
|
||
}
|
||
|
||
try {
|
||
const opts = JSON.parse(optionsStr)
|
||
optionsForm.authSource = opts.authSource || ''
|
||
// 认证机制使用自动检测,不需要从选项读取
|
||
} catch (error) {
|
||
console.warn('解析 Options JSON 失败:', error)
|
||
// 解析失败时,清空表单选项
|
||
optionsForm.authSource = ''
|
||
}
|
||
}
|
||
|
||
// 将 optionsForm 和 form.options 合并为 JSON 字符串
|
||
const mergeOptionsToJson = (): string => {
|
||
let customOptions: any = {}
|
||
|
||
// 先解析已有的 JSON(可能包含其他自定义选项)
|
||
if (form.options && form.options.trim() !== '') {
|
||
try {
|
||
customOptions = JSON.parse(form.options)
|
||
} catch (error) {
|
||
console.warn('解析自定义 Options JSON 失败:', error)
|
||
}
|
||
}
|
||
|
||
// 根据数据库类型合并表单选项(仅 MongoDB)
|
||
if (form.type === 'mongo') {
|
||
// 只有认证数据库不为空时才添加
|
||
if (optionsForm.authSource && optionsForm.authSource.trim() !== '') {
|
||
customOptions.authSource = optionsForm.authSource.trim()
|
||
}
|
||
// 认证机制使用自动检测,不需要添加到选项
|
||
}
|
||
|
||
// 如果没有任何选项,返回空字符串
|
||
if (Object.keys(customOptions).length === 0) {
|
||
return ''
|
||
}
|
||
|
||
return JSON.stringify(customOptions)
|
||
}
|
||
|
||
// 表单验证规则
|
||
const rules = {
|
||
name: [
|
||
{required: true, message: '请输入连接名称'},
|
||
{maxLength: 100, message: '连接名称长度不能超过100个字符'}
|
||
],
|
||
type: [{required: true, message: '请选择数据库类型'}],
|
||
host: [
|
||
{required: true, message: '请输入主机地址'},
|
||
{maxLength: 255, message: '主机地址长度不能超过255个字符'}
|
||
],
|
||
port: [
|
||
{required: true, message: '请输入端口'},
|
||
{
|
||
validator: (value, callback) => {
|
||
if (!value || value < 1 || value > 65535) {
|
||
callback('端口号必须在1-65535之间')
|
||
} else {
|
||
callback()
|
||
}
|
||
}
|
||
}
|
||
],
|
||
database: [
|
||
{
|
||
validator: (value, callback) => {
|
||
// MySQL 类型时数据库名为可选(允许为空)
|
||
// MongoDB 和 Redis 也为可选
|
||
callback()
|
||
}
|
||
}
|
||
]
|
||
}
|
||
|
||
// 获取密码输入框的占位符
|
||
const getPasswordPlaceholder = () => {
|
||
if (props.connectionId) return '请输入新密码'
|
||
const placeholders = { redis: '可选,留空则无密码连接', mongo: '可选,留空则无认证连接' }
|
||
return placeholders[form.type] || '请输入密码'
|
||
}
|
||
|
||
// 监听类型变化,设置默认端口、主机和用户名
|
||
const handleTypeChange = (type) => {
|
||
// 如果主机为空,设置默认值
|
||
if (!form.host || form.host.trim() === '') {
|
||
form.host = '127.0.0.1'
|
||
}
|
||
|
||
// 根据类型设置默认端口和用户名
|
||
switch (type) {
|
||
case 'mysql':
|
||
form.port = 3306
|
||
if (!form.username) {
|
||
form.username = 'root'
|
||
}
|
||
// 清空 MongoDB 专用选项
|
||
optionsForm.authSource = ''
|
||
form.options = ''
|
||
break
|
||
case 'redis':
|
||
form.port = 6379
|
||
form.username = '' // Redis 不需要用户名
|
||
if (!form.database) {
|
||
form.database = '0' // Redis 默认 DB 0
|
||
}
|
||
// 清空其他数据库的选项
|
||
optionsForm.authSource = ''
|
||
form.options = ''
|
||
break
|
||
case 'mongo':
|
||
case 'mongodb':
|
||
form.port = 27017
|
||
if (!form.username) {
|
||
form.username = 'admin' // MongoDB 常用默认用户名
|
||
}
|
||
break
|
||
}
|
||
|
||
// 类型变化时,同步更新 options JSON
|
||
form.options = mergeOptionsToJson()
|
||
}
|
||
|
||
// 加载连接详情
|
||
const loadConnection = async () => {
|
||
if (!props.connectionId) {
|
||
resetForm()
|
||
// 新建模式:不自动加载,等用户手动点击
|
||
return
|
||
}
|
||
|
||
isLoading.value = true
|
||
try {
|
||
if (!(window as any).go?.main?.App?.ListDbConnections) {
|
||
return
|
||
}
|
||
|
||
const connections = await (window as any).go.main.App.ListDbConnections()
|
||
const conn = connections.find(c => c.id === props.connectionId)
|
||
if (conn) {
|
||
form.name = conn.name
|
||
form.type = conn.type
|
||
form.host = conn.host || '127.0.0.1'
|
||
form.port = conn.port || (conn.type === 'mysql' ? 3306 : conn.type === 'redis' ? 6379 : 27017)
|
||
form.username = conn.username || ''
|
||
form.database = conn.database || ''
|
||
// 先解析 options 到表单
|
||
parseOptionsToForm(conn.options || '')
|
||
// 然后设置 form.options(这样不会触发 watch)
|
||
form.options = conn.options || ''
|
||
// 设置可见数据库
|
||
form.visibleDatabases = conn.visible_databases || ''
|
||
// 编辑模式下,默认不修改密码
|
||
form.password = ''
|
||
isPasswordChanged.value = false
|
||
|
||
// 恢复数据库选择
|
||
selectedDatabases.value = parseVisibleDatabases(conn.visible_databases || null)
|
||
|
||
// 编辑模式:自动加载数据库列表
|
||
nextTick(() => {
|
||
loadAllDatabases()
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error('加载连接详情失败:', error)
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 获取要使用的密码(编辑模式下未修改密码时为空)
|
||
const getPasswordToUse = () =>
|
||
(props.connectionId && !isPasswordChanged.value) ? '' : (form.password || '')
|
||
|
||
// 表单验证(返回 true 表示验证通过)
|
||
const validateForm = async () => {
|
||
if (!formRef.value) {
|
||
console.error('formRef 未初始化')
|
||
return false
|
||
}
|
||
errorMessage.value = ''
|
||
try {
|
||
await formRef.value.validate()
|
||
return true
|
||
} catch (error) {
|
||
const errorMsg = error?.fields?.[Object.keys(error.fields)[0]]?.[0]?.message || '请检查表单填写是否正确'
|
||
errorMessage.value = errorMsg
|
||
Message.warning(errorMsg)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 重置表单
|
||
const resetForm = () => {
|
||
form.name = ''
|
||
form.type = 'mysql'
|
||
form.host = '127.0.0.1'
|
||
form.port = 3306
|
||
form.username = 'root' // MySQL 默认用户名
|
||
form.password = ''
|
||
form.database = ''
|
||
form.options = ''
|
||
form.visibleDatabases = ''
|
||
optionsForm.authSource = ''
|
||
isPasswordChanged.value = false
|
||
loadingDatabases.value = false
|
||
allDatabases.value = []
|
||
selectedDatabases.value = []
|
||
}
|
||
|
||
// 测试连接(不保存数据)
|
||
const handleTest = async () => {
|
||
if (!(await validateForm())) return
|
||
|
||
// 检查 Go 后端是否可用
|
||
if (!(window as any).go?.main?.App) {
|
||
const msg = 'Go 后端未就绪,请确保应用已启动'
|
||
errorMessage.value = msg
|
||
Message.error(msg)
|
||
return
|
||
}
|
||
|
||
testing.value = true
|
||
try {
|
||
await (window as any).go.main.App.TestDbConnectionWithParams({
|
||
id: props.connectionId || 0,
|
||
type: form.type,
|
||
host: form.host,
|
||
port: form.port,
|
||
username: form.username || '',
|
||
password: getPasswordToUse(),
|
||
database: form.database || '',
|
||
options: mergeOptionsToJson()
|
||
})
|
||
Message.success('连接测试成功')
|
||
errorMessage.value = ''
|
||
} catch (error) {
|
||
const friendlyMsg = getConnectionFailedTip(error, form.type)
|
||
errorMessage.value = friendlyMsg
|
||
Message.error(friendlyMsg)
|
||
} finally {
|
||
testing.value = false
|
||
}
|
||
}
|
||
|
||
// 提交表单
|
||
const handleSubmit = async () => {
|
||
if (!(await validateForm())) return
|
||
|
||
// 检查 Go 后端是否可用
|
||
if (!(window as any).go?.main?.App) {
|
||
const msg = 'Go 后端未就绪,请确保应用已启动'
|
||
errorMessage.value = msg
|
||
Message.error(msg)
|
||
return
|
||
}
|
||
|
||
saving.value = true
|
||
try {
|
||
await (window as any).go.main.App.SaveDbConnection({
|
||
id: props.connectionId || 0,
|
||
name: form.name,
|
||
type: form.type,
|
||
host: form.host,
|
||
port: form.port,
|
||
username: form.username || '',
|
||
password: getPasswordToUse(),
|
||
database: form.database || '',
|
||
options: mergeOptionsToJson(),
|
||
visible_databases: form.visibleDatabases || ''
|
||
})
|
||
Message.success(props.connectionId ? '更新成功' : '保存成功')
|
||
errorMessage.value = ''
|
||
emit('success')
|
||
visible.value = false
|
||
} catch (error) {
|
||
const errorMsg = error?.message || error?.toString() || '未知错误'
|
||
errorMessage.value = '保存失败: ' + errorMsg
|
||
Message.error('保存失败: ' + errorMsg)
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
// 取消
|
||
const handleCancel = () => {
|
||
errorMessage.value = ''
|
||
visible.value = false
|
||
resetForm()
|
||
}
|
||
|
||
// 是否正在加载连接(用于避免加载时触发 watch)
|
||
const isLoading = ref(false)
|
||
|
||
// 监听 optionsForm 变化,自动同步到 form.options(仅 MongoDB)
|
||
watch(
|
||
() => [optionsForm.authSource, form.type],
|
||
() => {
|
||
// 如果正在加载,不触发更新
|
||
if (isLoading.value) {
|
||
return
|
||
}
|
||
// 仅 MongoDB 需要同步选项
|
||
if (visible.value && form.type === 'mongo') {
|
||
form.options = mergeOptionsToJson()
|
||
}
|
||
},
|
||
{ deep: true }
|
||
)
|
||
|
||
// 加载全部数据库列表
|
||
const loadAllDatabases = async () => {
|
||
if (!canLoadDatabases.value) {
|
||
Message.warning('请先填写连接信息')
|
||
return
|
||
}
|
||
|
||
loadingDatabases.value = true
|
||
try {
|
||
const databases = await (window as any).go.main.App.LoadAllDatabases({
|
||
id: props.connectionId || 0,
|
||
type: form.type,
|
||
host: form.host,
|
||
port: form.port,
|
||
username: form.username || '',
|
||
password: getPasswordToUse(),
|
||
database: form.database || '',
|
||
options: mergeOptionsToJson()
|
||
})
|
||
|
||
allDatabases.value = databases || []
|
||
|
||
// 从已保存的 visibleDatabases 中恢复选择(使用 composable)
|
||
selectedDatabases.value = filterVisibleDatabases(databases, form.visibleDatabases || null)
|
||
|
||
Message.success(`成功加载 ${databases.length} 个数据库`)
|
||
} catch (error) {
|
||
Message.error(getLoadFailedTip(error, 'databases'))
|
||
} finally {
|
||
loadingDatabases.value = false
|
||
}
|
||
}
|
||
|
||
// 切换单个数据库选择
|
||
const toggleDatabase = (dbName: string, checked: boolean) => {
|
||
const list = selectedDatabases.value
|
||
if (checked && !list.includes(dbName)) {
|
||
list.push(dbName)
|
||
} else if (!checked) {
|
||
selectedDatabases.value = list.filter(db => db !== dbName)
|
||
}
|
||
}
|
||
|
||
// 全选/全不选
|
||
const handleSelectAll = (selectAll: boolean) => {
|
||
if (selectAll) {
|
||
selectedDatabases.value = [...allDatabases.value]
|
||
} else {
|
||
selectedDatabases.value = []
|
||
}
|
||
}
|
||
|
||
// 反选
|
||
const handleInvertSelection = () => {
|
||
const selectedSet = new Set(selectedDatabases.value)
|
||
selectedDatabases.value = allDatabases.value.filter(db => !selectedSet.has(db))
|
||
}
|
||
|
||
// 监听数据库选择变化,同步到表单
|
||
watch(selectedDatabases, (newVal) => {
|
||
if (newVal.length === 0) {
|
||
form.visibleDatabases = ''
|
||
} else {
|
||
form.visibleDatabases = JSON.stringify(newVal)
|
||
}
|
||
}, { deep: true })
|
||
|
||
// 监听 visible 变化
|
||
watch(visible, (val) => {
|
||
if (val) {
|
||
errorMessage.value = ''
|
||
loadConnection()
|
||
} else {
|
||
errorMessage.value = ''
|
||
resetForm()
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.error-alert {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.password-display {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.password-input {
|
||
flex: 1;
|
||
}
|
||
|
||
.options-item {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.form-item-extra {
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
margin-top: 4px;
|
||
display: block;
|
||
}
|
||
|
||
.database-list-container {
|
||
width: 100%;
|
||
}
|
||
|
||
/* 顶部工具栏 */
|
||
.list-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.toolbar-stats {
|
||
font-size: 13px;
|
||
color: var(--color-text-2);
|
||
margin: 0 8px;
|
||
}
|
||
|
||
/* 空状态 */
|
||
.list-empty {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
padding: 40px 20px;
|
||
color: var(--color-text-3);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.list-empty svg {
|
||
font-size: 32px;
|
||
}
|
||
|
||
/* 复选框列表 */
|
||
.database-checkbox-list {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||
gap: 8px;
|
||
max-height: 240px;
|
||
overflow-y: auto;
|
||
padding: 12px;
|
||
background: var(--color-fill-2);
|
||
border: 1px solid var(--color-border-2);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.database-checkbox-item {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.database-checkbox-item :deep(.arco-checkbox) {
|
||
width: 100%;
|
||
font-size: 13px;
|
||
}
|
||
|
||
/* 底部提示 */
|
||
.list-tip {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-top: 8px;
|
||
padding: 8px 12px;
|
||
background: var(--color-fill-1);
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
}
|
||
|
||
.list-tip svg {
|
||
color: var(--color-text-3);
|
||
font-size: 14px;
|
||
}
|
||
</style>
|
||
|