refactor: 重构 Settings 页面
- 移除独立的 Settings.vue 主文件 - 合并到 knowledge 模块统一管理 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,6 @@ import { ElMessage } from 'element-plus'
|
||||
import { useModelSettings } from './settings/useModelSettings'
|
||||
import FormDialog from '@/components/FormDialog.vue'
|
||||
import './settings/settings.css'
|
||||
import './settings/settings-parsing.css'
|
||||
import './settings/modelSettings.css'
|
||||
|
||||
// 当前选中的设置菜单
|
||||
@@ -48,25 +47,8 @@ const menuItems = [
|
||||
{ key: 'members', label: 'Members', icon: 'fa-users' },
|
||||
{ key: 'notifications', label: 'Notifications', icon: 'fa-bell' },
|
||||
{ key: 'modelSettings', label: 'Model Settings', icon: 'fa-brain' },
|
||||
{ key: 'parsing', label: 'Parsing', icon: 'fa-code' },
|
||||
{ key: 'storage', label: 'Storage', icon: 'fa-database' },
|
||||
]
|
||||
|
||||
// Model Options by Provider (for Parsing settings)
|
||||
const modelOptionsByProvider: Record<string, { value: string; label: string }[]> = {
|
||||
'OpenAI': [
|
||||
{ value: 'gpt-4o', label: 'GPT-4o' },
|
||||
{ value: 'gpt-4o-mini', label: 'GPT-4o Mini' },
|
||||
{ value: 'gpt-4-turbo', label: 'GPT-4 Turbo' },
|
||||
{ value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo' },
|
||||
],
|
||||
'Ollama': [
|
||||
{ value: 'llama3', label: 'Llama 3' },
|
||||
{ value: 'mistral', label: 'Mistral' },
|
||||
{ value: 'codellama', label: 'CodeLlama' },
|
||||
],
|
||||
}
|
||||
|
||||
// General 设置表单
|
||||
const generalForm = ref({
|
||||
name: 'Alex Smith',
|
||||
@@ -100,73 +82,6 @@ const saveChanges = () => {
|
||||
const showChangePassword = () => {
|
||||
ElMessage.info('Password change dialog would open here')
|
||||
}
|
||||
|
||||
// Parsing 配置表单
|
||||
const parsingForm = ref({
|
||||
// LLM Provider
|
||||
provider: 'OpenAI',
|
||||
model: 'gpt-4o',
|
||||
apiKey: '',
|
||||
apiEndpoint: '',
|
||||
// 通用配置
|
||||
maxWorkers: 5,
|
||||
maxRetries: 3,
|
||||
requestTimeout: 60,
|
||||
enableProxy: false,
|
||||
httpProxyUrl: '',
|
||||
// 文本解析
|
||||
enableMultimodal: true,
|
||||
visionModel: 'gpt-4o',
|
||||
imageUnderstandingPrice: 0.00125,
|
||||
enableJsonMode: false,
|
||||
jsonModeModel: 'gpt-4o',
|
||||
// 文件解析
|
||||
enableFileParsing: true,
|
||||
enableTableRecognition: true,
|
||||
enableFormulaRecognition: true,
|
||||
ocrLanguage: 'en',
|
||||
})
|
||||
|
||||
// Vision Model 选项
|
||||
const visionModelOptions = [
|
||||
{ value: 'gpt-4o', label: 'GPT-4o' },
|
||||
{ value: 'gpt-4o-mini', label: 'GPT-4o Mini' },
|
||||
{ value: 'claude-3-5-sonnet', label: 'Claude 3.5 Sonnet' },
|
||||
{ value: 'claude-3-haiku', label: 'Claude 3 Haiku' },
|
||||
{ value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro' },
|
||||
]
|
||||
|
||||
// OCR 语言选项
|
||||
const ocrLanguageOptions = [
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'zh', label: 'Chinese' },
|
||||
{ value: 'ja', label: 'Japanese' },
|
||||
{ value: 'ko', label: 'Korean' },
|
||||
{ value: 'auto', label: 'Auto Detect' },
|
||||
]
|
||||
|
||||
// 保存 Parsing 设置
|
||||
const saveParsingSettings = () => {
|
||||
ElMessage.success('Parsing settings saved successfully')
|
||||
}
|
||||
|
||||
// Storage 配置表单
|
||||
const storageForm = ref({
|
||||
storageType: 'local',
|
||||
// Local 存储
|
||||
localPath: './storage',
|
||||
// MinIO 存储
|
||||
minioEndpoint: 'localhost:9000',
|
||||
minioAccessKey: '',
|
||||
minioSecretKey: '',
|
||||
minioBucket: 'x-agents',
|
||||
minioUseSSL: false,
|
||||
})
|
||||
|
||||
// 保存 Storage 设置
|
||||
const saveStorageSettings = () => {
|
||||
ElMessage.success('Storage settings saved successfully')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -244,186 +159,6 @@ const saveStorageSettings = () => {
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Parsing 设置 -->
|
||||
<div v-if="activeMenu === 'parsing'" class="settings-section">
|
||||
<h2 class="section-title">Parsing</h2>
|
||||
<p class="section-desc">Configure parsing settings</p>
|
||||
|
||||
<!-- LLM Provider -->
|
||||
<div class="config-card">
|
||||
<h3 class="config-title">LLM Provider</h3>
|
||||
<el-form label-position="top" class="settings-form">
|
||||
<div class="form-row">
|
||||
<el-form-item label="Provider" class="flex-1">
|
||||
<el-select v-model="parsingForm.provider" placeholder="Select provider">
|
||||
<el-option v-for="p in providerOptions" :key="p.value" :label="p.label" :value="p.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Model" class="flex-1">
|
||||
<el-select v-model="parsingForm.model" placeholder="Select model">
|
||||
<el-option v-for="m in modelOptionsByProvider[parsingForm.provider]" :key="m.value" :label="m.label" :value="m.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="API Key">
|
||||
<el-input v-model="parsingForm.apiKey" type="password" placeholder="Enter API key" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="API Endpoint">
|
||||
<el-input v-model="parsingForm.apiEndpoint" placeholder="Enter API endpoint" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- Parsing Configuration -->
|
||||
<div class="config-card">
|
||||
<h3 class="config-title">Parsing Configuration</h3>
|
||||
|
||||
<!-- 通用配置 -->
|
||||
<div class="config-section">
|
||||
<h4 class="config-subtitle">General</h4>
|
||||
<el-form label-position="top" class="settings-form">
|
||||
<div class="form-row">
|
||||
<el-form-item label="Max Workers" class="flex-1">
|
||||
<el-input-number v-model="parsingForm.maxWorkers" :min="1" :max="100" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Max Retries" class="flex-1">
|
||||
<el-input-number v-model="parsingForm.maxRetries" :min="0" :max="10" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Request Timeout (seconds)" class="flex-1">
|
||||
<el-input-number v-model="parsingForm.requestTimeout" :min="10" :max="600" controls-position="right" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<el-form-item label="Enable HTTP Proxy" class="flex-1">
|
||||
<el-switch v-model="parsingForm.enableProxy" class="parsing-switch" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="parsingForm.enableProxy" label="HTTP Proxy URL" class="flex-1">
|
||||
<el-input v-model="parsingForm.httpProxyUrl" placeholder="http://proxy:8080" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 文本解析 -->
|
||||
<div class="config-section">
|
||||
<h4 class="config-subtitle">Text Parsing</h4>
|
||||
<el-form label-position="top" class="settings-form">
|
||||
<div class="form-row">
|
||||
<el-form-item label="Enable Multimodal" class="flex-1">
|
||||
<el-switch v-model="parsingForm.enableMultimodal" class="parsing-switch" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="parsingForm.enableMultimodal" label="Vision Model" class="flex-1">
|
||||
<el-select v-model="parsingForm.visionModel" placeholder="Select model">
|
||||
<el-option v-for="m in visionModelOptions" :key="m.value" :label="m.label" :value="m.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="parsingForm.enableMultimodal" label="Image Understanding Price" class="flex-1">
|
||||
<el-input v-model="parsingForm.imageUnderstandingPrice" placeholder="0.00125">
|
||||
<template #append>$/1k tokens</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<el-form-item label="Enable JSON Mode" class="flex-1">
|
||||
<el-switch v-model="parsingForm.enableJsonMode" class="parsing-switch" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="parsingForm.enableJsonMode" label="JSON Mode Model" class="flex-1">
|
||||
<el-select v-model="parsingForm.jsonModeModel" placeholder="Select model">
|
||||
<el-option v-for="m in visionModelOptions" :key="m.value" :label="m.label" :value="m.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 文件解析 -->
|
||||
<div class="config-section">
|
||||
<h4 class="config-subtitle">File Parsing</h4>
|
||||
<el-form label-position="top" class="settings-form">
|
||||
<div class="form-row">
|
||||
<el-form-item label="Enable File Parsing" class="flex-1">
|
||||
<el-switch v-model="parsingForm.enableFileParsing" class="parsing-switch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Enable Table Recognition" class="flex-1">
|
||||
<el-switch v-model="parsingForm.enableTableRecognition" class="parsing-switch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Enable Formula Recognition" class="flex-1">
|
||||
<el-switch v-model="parsingForm.enableFormulaRecognition" class="parsing-switch" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="OCR Language">
|
||||
<el-select v-model="parsingForm.ocrLanguage" placeholder="Select language">
|
||||
<el-option v-for="lang in ocrLanguageOptions" :key="lang.value" :label="lang.label" :value="lang.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="form-actions mt-6">
|
||||
<el-button type="primary" @click="saveParsingSettings">Save</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Storage 设置 -->
|
||||
<div v-if="activeMenu === 'storage'" class="settings-section">
|
||||
<h2 class="section-title">Storage</h2>
|
||||
<p class="section-desc">Configure storage settings</p>
|
||||
|
||||
<!-- Storage Type -->
|
||||
<div class="config-card">
|
||||
<h3 class="config-title">Storage Type</h3>
|
||||
<el-form label-position="top" class="settings-form">
|
||||
<el-form-item label="Storage Type">
|
||||
<el-select v-model="storageForm.storageType" placeholder="Select storage type">
|
||||
<el-option label="Local" value="local" />
|
||||
<el-option label="MinIO" value="minio" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- Local Storage -->
|
||||
<div v-if="storageForm.storageType === 'local'" class="config-card">
|
||||
<h3 class="config-title">Local Storage</h3>
|
||||
<el-form label-position="top" class="settings-form">
|
||||
<el-form-item label="Storage Path">
|
||||
<el-input v-model="storageForm.localPath" placeholder="Enter local storage path" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- MinIO Storage -->
|
||||
<div v-if="storageForm.storageType === 'minio'" class="config-card">
|
||||
<h3 class="config-title">MinIO Storage</h3>
|
||||
<el-form label-position="top" class="settings-form">
|
||||
<el-form-item label="Endpoint">
|
||||
<el-input v-model="storageForm.minioEndpoint" placeholder="e.g., localhost:9000" />
|
||||
</el-form-item>
|
||||
<div class="form-row">
|
||||
<el-form-item label="Access Key" class="flex-1">
|
||||
<el-input v-model="storageForm.minioAccessKey" placeholder="Enter access key" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Secret Key" class="flex-1">
|
||||
<el-input v-model="storageForm.minioSecretKey" type="password" placeholder="Enter secret key" show-password />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="Bucket">
|
||||
<el-input v-model="storageForm.minioBucket" placeholder="Enter bucket name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Use SSL">
|
||||
<el-switch v-model="storageForm.minioUseSSL" class="parsing-switch" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="form-actions mt-6">
|
||||
<el-button type="primary" @click="saveStorageSettings">Save</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Members 设置 -->
|
||||
<div v-if="activeMenu === 'members'" class="settings-section">
|
||||
<h2 class="section-title">Members</h2>
|
||||
@@ -700,41 +435,3 @@ const saveStorageSettings = () => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* Model Settings dialog - 最高优先级覆盖 */
|
||||
|
||||
/* 输入框 */
|
||||
.add-model-dialog .el-input__wrapper {
|
||||
background-color: #171922 !important;
|
||||
}
|
||||
|
||||
.add-model-dialog .el-input__wrapper input {
|
||||
color: #ffffff !important;
|
||||
-webkit-text-fill-color: #ffffff !important;
|
||||
}
|
||||
|
||||
.add-model-dialog .el-input__wrapper input::placeholder {
|
||||
color: #9ca3af !important;
|
||||
-webkit-text-fill-color: #9ca3af !important;
|
||||
}
|
||||
|
||||
/* 选择框 */
|
||||
.add-model-dialog .el-select__wrapper {
|
||||
background-color: #171922 !important;
|
||||
}
|
||||
|
||||
.add-model-dialog .el-select__wrapper .el-select__selected-item {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.add-model-dialog .el-select__wrapper input::placeholder {
|
||||
color: #9ca3af !important;
|
||||
-webkit-text-fill-color: #9ca3af !important;
|
||||
}
|
||||
|
||||
/* 未选中时的占位符文字 */
|
||||
.add-model-dialog .el-select__placeholder {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
/* Parsing 和 Storage 配置样式 */
|
||||
|
||||
/* 配置卡片 */
|
||||
.config-card {
|
||||
background-color: #0d0d12;
|
||||
border: 1px solid #252530;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.config-card:hover {
|
||||
border-color: #353545;
|
||||
}
|
||||
|
||||
.config-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #252530;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.config-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.config-subtitle {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 添加模型按钮 */
|
||||
.add-model-btn {
|
||||
background-color: #f97316;
|
||||
border-color: #f97316;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.add-model-btn:hover {
|
||||
background-color: #ea580c;
|
||||
border-color: #ea580c;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.btn-icon {
|
||||
padding: 6px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.25s ease;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background-color: #1e1e28;
|
||||
}
|
||||
|
||||
.btn-icon i {
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-icon:hover i {
|
||||
color: #f97316 !important;
|
||||
}
|
||||
|
||||
/* 弹窗样式 */
|
||||
.add-model-dialog :deep(.el-dialog) {
|
||||
background-color: #16161e;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #252530;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
animation: dialogFadeIn 0.25s ease;
|
||||
}
|
||||
|
||||
@keyframes dialogFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-dialog__header) {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #252530;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-dialog__title) {
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-dialog__headerbtn) {
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-dialog__headerbtn .el-dialog__close) {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-dialog__headerbtn:hover .el-dialog__close) {
|
||||
color: #f97316;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-dialog__body) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-dialog__footer) {
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #252530;
|
||||
}
|
||||
|
||||
/* 弹窗描述 */
|
||||
.dialog-desc {
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 弹窗底部按钮 */
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 测试连接按钮 */
|
||||
.test-btn {
|
||||
background-color: #1e1e28;
|
||||
border: 1px solid #3a3a4a;
|
||||
color: #d1d5db;
|
||||
transition: all 0.25s ease;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.test-btn:hover {
|
||||
background-color: #2a2a3a;
|
||||
border-color: #4a4a5a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 连接状态 */
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.connection-status.success {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.connection-status.error {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* 弹窗表单 */
|
||||
.add-model-dialog :deep(.el-form-item__label) {
|
||||
color: #d1d5db;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-input__wrapper) {
|
||||
background-color: #171922;
|
||||
border: 1px solid #4b5563;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-input__inner) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-input__inner::placeholder) {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-select .el-input__wrapper) {
|
||||
background-color: #171922;
|
||||
border: 1px solid #4b5563;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-select .el-input__inner) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-select .el-input__inner::placeholder) {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-select-dropdown) {
|
||||
background-color: #1a1a24;
|
||||
border: 1px solid #2a2a3a;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-select-dropdown__item) {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-select-dropdown__item.hover),
|
||||
.add-model-dialog :deep(.el-select-dropdown__item:hover) {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-select-dropdown__item.selected) {
|
||||
color: #f97316;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-button--primary) {
|
||||
background-color: #f97316;
|
||||
border-color: #f97316;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-button--primary:hover) {
|
||||
background-color: #ea580c;
|
||||
border-color: #ea580c;
|
||||
}
|
||||
|
||||
/* API Endpoint 字段 */
|
||||
.api-endpoint-field {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.api-endpoint-field .el-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-spinner {
|
||||
text-align: center;
|
||||
padding: 32px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.loading-spinner i {
|
||||
font-size: 24px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user