feat quickly
This commit is contained in:
143
Tools/quickly/frontend/src/views/Home.vue
Normal file
143
Tools/quickly/frontend/src/views/Home.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<script lang="ts" setup>
|
||||
import {reactive} from 'vue'
|
||||
|
||||
const features = reactive([
|
||||
{
|
||||
title: '更新 Common',
|
||||
description: '批量更新多个项目中 Common 库的版本,支持自定义 Go Module Name,自动执行 go get 和 go mod tidy 命令。',
|
||||
icon: '🔄',
|
||||
path: '/update-common'
|
||||
},
|
||||
{
|
||||
title: 'MySQL Model',
|
||||
description: '执行 gen.ps1 脚本生成 model 和 query 文件,自动替换 import 路径,支持多数据库配置管理。',
|
||||
icon: '🗄️',
|
||||
path: '/mysql-model'
|
||||
},
|
||||
{
|
||||
title: 'Swagger',
|
||||
description: '配置 Swagger 目录,自动扫描所有 .swagger.json 文件,启动本地 HTTP 服务查看 API 文档。',
|
||||
icon: '📚',
|
||||
path: '/swagger'
|
||||
},
|
||||
{
|
||||
title: '设置',
|
||||
description: '自定义主题、语言、通知等应用设置,管理全局配置。',
|
||||
icon: '⚙️',
|
||||
path: '/settings'
|
||||
}
|
||||
])
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="home-container">
|
||||
<div class="welcome-section">
|
||||
<h1 class="welcome-title">欢迎使用 Quickly</h1>
|
||||
<p class="welcome-subtitle">高效开发工具,提升工作效率</p>
|
||||
</div>
|
||||
|
||||
<div class="features-section">
|
||||
<h2 class="section-title">功能介绍</h2>
|
||||
<div class="features-grid">
|
||||
<el-card v-for="(feature, index) in features" :key="index" class="feature-card">
|
||||
<div class="feature-icon">{{ feature.icon }}</div>
|
||||
<h3 class="feature-title">{{ feature.title }}</h3>
|
||||
<p class="feature-description">{{ feature.description }}</p>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-container {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
text-align: center;
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 10px 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.features-section {
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0 0 30px 0;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
text-align: center;
|
||||
transition: transform 0.3s, box-shadow 0.3s;
|
||||
cursor: pointer;
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.feature-description {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
padding: 20px 0;
|
||||
}
|
||||
</style>
|
||||
431
Tools/quickly/frontend/src/views/MySQLModel.vue
Normal file
431
Tools/quickly/frontend/src/views/MySQLModel.vue
Normal file
@@ -0,0 +1,431 @@
|
||||
<script lang="ts" setup>
|
||||
import {onMounted, reactive, ref} from 'vue'
|
||||
import {ElMessage} from 'element-plus'
|
||||
import {
|
||||
AddDatabaseConfig,
|
||||
CheckGenPs1Exists,
|
||||
ExecuteGenPs1,
|
||||
GetSettings,
|
||||
ReadGoModModule,
|
||||
RemoveDatabaseConfig,
|
||||
SaveSettings,
|
||||
SelectDirectory,
|
||||
SelectFile,
|
||||
UpdateDatabaseConfig
|
||||
} from '../../wailsjs/go/backend/App'
|
||||
|
||||
interface DatabaseConfig {
|
||||
name: string
|
||||
targetPath: string
|
||||
modelPackagePath: string
|
||||
}
|
||||
|
||||
interface AppSettings {
|
||||
theme: string
|
||||
language: string
|
||||
notifications: boolean
|
||||
autoStart: boolean
|
||||
mysqlModelPath: string
|
||||
defaultQueryPackagePath: string
|
||||
databases: DatabaseConfig[]
|
||||
}
|
||||
|
||||
const settings = reactive<AppSettings>({
|
||||
theme: 'light',
|
||||
language: 'zh-CN',
|
||||
notifications: true,
|
||||
autoStart: false,
|
||||
mysqlModelPath: '',
|
||||
defaultQueryPackagePath: '',
|
||||
databases: []
|
||||
})
|
||||
|
||||
const dbForm = reactive<DatabaseConfig>({
|
||||
name: '',
|
||||
targetPath: '',
|
||||
modelPackagePath: ''
|
||||
})
|
||||
|
||||
const scriptForm = reactive({
|
||||
selectedDbName: '',
|
||||
output: ''
|
||||
})
|
||||
|
||||
const isExecuting = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const editingIndex = ref(-1)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const loadedSettings = await GetSettings() as any
|
||||
settings.theme = loadedSettings.theme
|
||||
settings.language = loadedSettings.language
|
||||
settings.notifications = loadedSettings.notifications
|
||||
settings.autoStart = loadedSettings.autoStart
|
||||
settings.mysqlModelPath = loadedSettings.mysqlModelPath
|
||||
settings.defaultQueryPackagePath = loadedSettings.defaultQueryPackagePath || ''
|
||||
if (loadedSettings.databases) {
|
||||
settings.databases = loadedSettings.databases
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error)
|
||||
}
|
||||
})
|
||||
|
||||
async function validatePath(path: string): Promise<boolean> {
|
||||
if (!path) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const exists = await CheckGenPs1Exists(path)
|
||||
if (!exists) {
|
||||
ElMessage.warning('所选文件不是有效的 gen.ps1 文件')
|
||||
}
|
||||
return exists
|
||||
} catch (error) {
|
||||
console.error('Failed to check gen.ps1:', error)
|
||||
ElMessage.error('检查文件时出错')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function selectMysqlModelPath() {
|
||||
try {
|
||||
const path = await SelectFile('选择 gen.ps1 文件', settings.mysqlModelPath, '*.ps1')
|
||||
if (path) {
|
||||
const isValid = await validatePath(path)
|
||||
if (isValid) {
|
||||
settings.mysqlModelPath = path
|
||||
|
||||
try {
|
||||
const modulePath = await ReadGoModModule(path)
|
||||
if (modulePath) {
|
||||
settings.defaultQueryPackagePath = modulePath
|
||||
ElMessage.success(`已自动读取 go.mod 中的 module: ${modulePath}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to read go.mod:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to select file:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePathChange() {
|
||||
if (settings.mysqlModelPath) {
|
||||
await validatePath(settings.mysqlModelPath)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
if (!settings.mysqlModelPath) {
|
||||
ElMessage.warning('请先设置 MySQL Model 路径')
|
||||
return
|
||||
}
|
||||
|
||||
const isValid = await validatePath(settings.mysqlModelPath)
|
||||
if (!isValid) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const currentSettings = await GetSettings() as any
|
||||
currentSettings.mysqlModelPath = settings.mysqlModelPath
|
||||
currentSettings.defaultQueryPackagePath = settings.defaultQueryPackagePath
|
||||
currentSettings.databases = settings.databases
|
||||
await SaveSettings(currentSettings)
|
||||
ElMessage.success('设置保存成功')
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error)
|
||||
ElMessage.error('设置保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function selectTargetPath() {
|
||||
try {
|
||||
const path = await SelectDirectory('选择生成文件目标路径', dbForm.targetPath)
|
||||
if (path) {
|
||||
dbForm.targetPath = path
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to select directory:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function openAddDialog() {
|
||||
editingIndex.value = -1
|
||||
dbForm.name = ''
|
||||
dbForm.targetPath = ''
|
||||
dbForm.modelPackagePath = ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEditDialog(index: number) {
|
||||
editingIndex.value = index
|
||||
const db = settings.databases[index]
|
||||
dbForm.name = db.name
|
||||
dbForm.targetPath = db.targetPath
|
||||
dbForm.modelPackagePath = db.modelPackagePath || ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleAddDb() {
|
||||
if (!dbForm.name) {
|
||||
ElMessage.warning('请输入数据库名称')
|
||||
return
|
||||
}
|
||||
|
||||
if (!dbForm.targetPath) {
|
||||
ElMessage.warning('请选择目标路径')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (editingIndex.value === -1) {
|
||||
await AddDatabaseConfig(dbForm.name, dbForm.targetPath, dbForm.modelPackagePath)
|
||||
ElMessage.success('添加成功')
|
||||
} else {
|
||||
await UpdateDatabaseConfig(settings.databases[editingIndex.value].name, dbForm.name, dbForm.targetPath, dbForm.modelPackagePath)
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
|
||||
const loadedSettings = await GetSettings() as any
|
||||
settings.theme = loadedSettings.theme
|
||||
settings.language = loadedSettings.language
|
||||
settings.notifications = loadedSettings.notifications
|
||||
settings.autoStart = loadedSettings.autoStart
|
||||
settings.mysqlModelPath = loadedSettings.mysqlModelPath
|
||||
settings.defaultQueryPackagePath = loadedSettings.defaultQueryPackagePath || ''
|
||||
if (loadedSettings.databases) {
|
||||
settings.databases = loadedSettings.databases
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save database config:', error)
|
||||
ElMessage.error('保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteDb(index: number) {
|
||||
const db = settings.databases[index]
|
||||
try {
|
||||
await RemoveDatabaseConfig(db.name)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
const loadedSettings = await GetSettings() as any
|
||||
settings.theme = loadedSettings.theme
|
||||
settings.language = loadedSettings.language
|
||||
settings.notifications = loadedSettings.notifications
|
||||
settings.autoStart = loadedSettings.autoStart
|
||||
settings.mysqlModelPath = loadedSettings.mysqlModelPath
|
||||
if (loadedSettings.databases) {
|
||||
settings.databases = loadedSettings.databases
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete database config:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function executeScript() {
|
||||
if (!settings.mysqlModelPath) {
|
||||
ElMessage.warning('请先设置 MySQL Model 路径')
|
||||
return
|
||||
}
|
||||
|
||||
if (!scriptForm.selectedDbName) {
|
||||
ElMessage.warning('请选择要执行的数据库')
|
||||
return
|
||||
}
|
||||
|
||||
const isValid = await validatePath(settings.mysqlModelPath)
|
||||
if (!isValid) {
|
||||
return
|
||||
}
|
||||
|
||||
const selectedDb = settings.databases.find(db => db.name === scriptForm.selectedDbName)
|
||||
if (!selectedDb) {
|
||||
ElMessage.warning('未找到数据库配置')
|
||||
return
|
||||
}
|
||||
|
||||
isExecuting.value = true
|
||||
scriptForm.output = '正在执行脚本...'
|
||||
|
||||
try {
|
||||
const output = await ExecuteGenPs1(settings.mysqlModelPath, scriptForm.selectedDbName, selectedDb.targetPath, selectedDb.modelPackagePath || '')
|
||||
scriptForm.output = output + '\n\n文件已复制到目标路径: ' + selectedDb.targetPath
|
||||
ElMessage.success('脚本执行成功')
|
||||
} catch (error) {
|
||||
console.error('Failed to execute script:', error)
|
||||
scriptForm.output = `执行失败: ${error}`
|
||||
ElMessage.error('脚本执行失败')
|
||||
} finally {
|
||||
isExecuting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mysql-model-container">
|
||||
<el-card class="script-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>生成 model&query 文件</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form label-width="140px" class="script-form">
|
||||
<el-form-item label="选择数据库">
|
||||
<el-select
|
||||
v-model="scriptForm.selectedDbName"
|
||||
placeholder="请选择要执行的数据库"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="db in settings.databases"
|
||||
:key="db.name"
|
||||
:label="db.name"
|
||||
:value="db.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="executeScript"
|
||||
:loading="isExecuting"
|
||||
>
|
||||
执行脚本
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="执行输出">
|
||||
<el-input
|
||||
v-model="scriptForm.output"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
placeholder="脚本执行结果将显示在这里"
|
||||
readonly
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mysql-model-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>MySQL Model 设置</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form label-width="140px" class="settings-form">
|
||||
<el-form-item label="gen.ps1 路径">
|
||||
<el-input
|
||||
v-model="settings.mysqlModelPath"
|
||||
placeholder="请输入或选择 gen.ps1 文件路径"
|
||||
clearable
|
||||
@change="handlePathChange"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="selectMysqlModelPath">选择文件</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Query包名">
|
||||
<el-input
|
||||
v-model="settings.defaultQueryPackagePath"
|
||||
placeholder="请输入默认 Query 包名,例如:git.hlsq.asia/mmorpg/service-user/internal/dao/model"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveSettings">保存设置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="database-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>数据库配置管理</span>
|
||||
<el-button type="primary" size="small" @click="openAddDialog">添加配置</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="settings.databases" style="width: 100%">
|
||||
<el-table-column prop="name" label="数据库名称"/>
|
||||
<el-table-column prop="targetPath" label="目标路径" show-overflow-tooltip/>
|
||||
<el-table-column prop="modelPackagePath" label="Query包名" show-overflow-tooltip/>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="openEditDialog(scope.$index)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDeleteDb(scope.$index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="editingIndex === -1 ? '添加数据库配置' : '编辑数据库配置'"
|
||||
width="500px"
|
||||
>
|
||||
<el-form label-width="120px">
|
||||
<el-form-item label="数据库名称">
|
||||
<el-input
|
||||
v-model="dbForm.name"
|
||||
placeholder="请输入数据库名称,例如:user_db"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标路径">
|
||||
<el-input
|
||||
v-model="dbForm.targetPath"
|
||||
placeholder="请输入或选择生成文件的目标路径"
|
||||
clearable
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="selectTargetPath">选择文件夹</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Query包名">
|
||||
<el-input
|
||||
v-model="dbForm.modelPackagePath"
|
||||
placeholder="请输入package name,例如:git.hlsq.asia/mmorpg/service-user/internal/dao/model"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleAddDb">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mysql-model-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.mysql-model-card,
|
||||
.database-card,
|
||||
.script-card {
|
||||
max-width: 900px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.settings-form,
|
||||
.script-form {
|
||||
padding: 20px 0;
|
||||
}
|
||||
</style>
|
||||
114
Tools/quickly/frontend/src/views/Settings.vue
Normal file
114
Tools/quickly/frontend/src/views/Settings.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<script lang="ts" setup>
|
||||
import {onMounted, reactive} from 'vue'
|
||||
import {ElMessage} from 'element-plus'
|
||||
import {GetSettings, SaveSettings} from '../../wailsjs/go/backend/App'
|
||||
|
||||
interface DatabaseConfig {
|
||||
name: string
|
||||
targetPath: string
|
||||
modelPackagePath: string
|
||||
}
|
||||
|
||||
interface AppSettings {
|
||||
theme: string
|
||||
language: string
|
||||
notifications: boolean
|
||||
autoStart: boolean
|
||||
databases?: DatabaseConfig[]
|
||||
}
|
||||
|
||||
const settings = reactive<AppSettings>({
|
||||
theme: 'light',
|
||||
language: 'zh-CN',
|
||||
notifications: true,
|
||||
autoStart: false
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const loadedSettings = await GetSettings() as any
|
||||
settings.theme = loadedSettings.theme
|
||||
settings.language = loadedSettings.language
|
||||
settings.notifications = loadedSettings.notifications
|
||||
settings.autoStart = loadedSettings.autoStart
|
||||
if (loadedSettings.databases) {
|
||||
settings.databases = loadedSettings.databases
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error)
|
||||
}
|
||||
})
|
||||
|
||||
async function saveSettings() {
|
||||
try {
|
||||
const currentSettings = await GetSettings() as any
|
||||
currentSettings.theme = settings.theme
|
||||
currentSettings.language = settings.language
|
||||
currentSettings.notifications = settings.notifications
|
||||
currentSettings.autoStart = settings.autoStart
|
||||
await SaveSettings(currentSettings)
|
||||
ElMessage.success('设置保存成功')
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error)
|
||||
ElMessage.error('设置保存失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-container">
|
||||
<el-card class="settings-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>设置</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form label-width="120px" class="settings-form">
|
||||
<el-form-item label="主题">
|
||||
<el-select v-model="settings.theme" placeholder="选择主题">
|
||||
<el-option label="浅色" value="light"/>
|
||||
<el-option label="深色" value="dark"/>
|
||||
<el-option label="跟随系统" value="auto"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="语言">
|
||||
<el-select v-model="settings.language" placeholder="选择语言">
|
||||
<el-option label="简体中文" value="zh-CN"/>
|
||||
<el-option label="English" value="en-US"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="通知">
|
||||
<el-switch v-model="settings.notifications"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="开机自启">
|
||||
<el-switch v-model="settings.autoStart"/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveSettings">保存设置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settings-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
max-width: 600px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
padding: 20px 0;
|
||||
}
|
||||
</style>
|
||||
349
Tools/quickly/frontend/src/views/Swagger.vue
Normal file
349
Tools/quickly/frontend/src/views/Swagger.vue
Normal file
@@ -0,0 +1,349 @@
|
||||
<script lang="ts" setup>
|
||||
import {onMounted, reactive, ref} from 'vue'
|
||||
import {ElMessage} from 'element-plus'
|
||||
import {
|
||||
GetSettings,
|
||||
GetSwaggerFiles,
|
||||
GetSwaggerServerURL,
|
||||
IsSwaggerServerRunning,
|
||||
ReadSwaggerFile,
|
||||
SaveSettings,
|
||||
SelectDirectory,
|
||||
StartSwaggerServer,
|
||||
StopSwaggerServer
|
||||
} from '../../wailsjs/go/backend/App'
|
||||
|
||||
interface SwaggerFile {
|
||||
name: string
|
||||
path: string
|
||||
size: number
|
||||
modifiedTime: string
|
||||
}
|
||||
|
||||
interface AppSettings {
|
||||
theme: string
|
||||
language: string
|
||||
notifications: boolean
|
||||
autoStart: boolean
|
||||
swaggerDir: string
|
||||
}
|
||||
|
||||
const settings = reactive<AppSettings>({
|
||||
theme: 'light',
|
||||
language: 'zh-CN',
|
||||
notifications: true,
|
||||
autoStart: false,
|
||||
swaggerDir: ''
|
||||
})
|
||||
|
||||
const swaggerFiles = ref<SwaggerFile[]>([])
|
||||
const selectedFile = ref<SwaggerFile | null>(null)
|
||||
const selectedSwaggerFile = ref<SwaggerFile | null>(null)
|
||||
const fileContent = ref('')
|
||||
const isLoading = ref(false)
|
||||
const serverURL = ref('')
|
||||
const isServerRunning = ref(false)
|
||||
|
||||
async function selectSwaggerDir() {
|
||||
try {
|
||||
const path = await SelectDirectory('选择 Swagger 目录', settings.swaggerDir)
|
||||
if (path) {
|
||||
settings.swaggerDir = path
|
||||
await loadSwaggerFiles()
|
||||
await saveSettings()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to select directory:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSwaggerFiles() {
|
||||
if (!settings.swaggerDir) {
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const files = await GetSwaggerFiles(settings.swaggerDir)
|
||||
swaggerFiles.value = files
|
||||
} catch (error) {
|
||||
console.error('Failed to load swagger files:', error)
|
||||
ElMessage.error('加载 Swagger 文件失败')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
try {
|
||||
const currentSettings = await GetSettings() as any
|
||||
currentSettings.swaggerDir = settings.swaggerDir
|
||||
await SaveSettings(currentSettings)
|
||||
ElMessage.success('设置保存成功')
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error)
|
||||
ElMessage.error('设置保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function viewFile(file: SwaggerFile) {
|
||||
selectedFile.value = file
|
||||
isLoading.value = true
|
||||
try {
|
||||
const content = await ReadSwaggerFile(file.path)
|
||||
fileContent.value = content
|
||||
} catch (error) {
|
||||
console.error('Failed to read swagger file:', error)
|
||||
ElMessage.error('读取文件失败')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) {
|
||||
return bytes + ' B'
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return (bytes / 1024).toFixed(2) + ' KB'
|
||||
} else {
|
||||
return (bytes / (1024 * 1024)).toFixed(2) + ' MB'
|
||||
}
|
||||
}
|
||||
|
||||
async function startSwaggerServer() {
|
||||
if (!settings.swaggerDir) {
|
||||
ElMessage.warning('请先配置 Swagger 目录')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const url = await StartSwaggerServer(settings.swaggerDir)
|
||||
serverURL.value = url
|
||||
isServerRunning.value = true
|
||||
if (swaggerFiles.value.length > 0) {
|
||||
selectedSwaggerFile.value = swaggerFiles.value[0]
|
||||
}
|
||||
ElMessage.success(`Swagger 服务已启动: ${url}`)
|
||||
} catch (error) {
|
||||
console.error('Failed to start swagger server:', error)
|
||||
ElMessage.error('启动 Swagger 服务失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function stopSwaggerServer() {
|
||||
try {
|
||||
await StopSwaggerServer()
|
||||
serverURL.value = ''
|
||||
isServerRunning.value = false
|
||||
ElMessage.success('Swagger 服务已停止')
|
||||
} catch (error) {
|
||||
console.error('Failed to stop swagger server:', error)
|
||||
ElMessage.error('停止 Swagger 服务失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function checkServerStatus() {
|
||||
try {
|
||||
const running = await IsSwaggerServerRunning()
|
||||
isServerRunning.value = running
|
||||
if (running) {
|
||||
const url = await GetSwaggerServerURL()
|
||||
serverURL.value = url
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check server status:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function openSwaggerURL() {
|
||||
if (serverURL.value) {
|
||||
window.open(serverURL.value, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
function updateServerURL() {
|
||||
if (serverURL.value && selectedSwaggerFile.value) {
|
||||
const baseUrl = serverURL.value.split('?')[0]
|
||||
serverURL.value = `${baseUrl}?file=${encodeURIComponent(selectedSwaggerFile.value.name)}`
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const loadedSettings = await GetSettings() as any
|
||||
settings.theme = loadedSettings.theme
|
||||
settings.language = loadedSettings.language
|
||||
settings.notifications = loadedSettings.notifications
|
||||
settings.autoStart = loadedSettings.autoStart
|
||||
settings.swaggerDir = loadedSettings.swaggerDir || ''
|
||||
if (settings.swaggerDir) {
|
||||
await loadSwaggerFiles()
|
||||
}
|
||||
await checkServerStatus()
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="swagger-container">
|
||||
<el-card class="settings-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>Swagger 设置</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form label-width="140px" class="settings-form">
|
||||
<el-form-item label="Swagger 目录">
|
||||
<el-input
|
||||
v-model="settings.swaggerDir"
|
||||
placeholder="请输入或选择 Swagger 文件目录"
|
||||
clearable
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="selectSwaggerDir">选择文件夹</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Swagger 服务">
|
||||
<div class="server-controls">
|
||||
<el-button
|
||||
v-if="!isServerRunning"
|
||||
type="primary"
|
||||
@click="startSwaggerServer"
|
||||
>
|
||||
启动服务
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="danger"
|
||||
@click="stopSwaggerServer"
|
||||
>
|
||||
停止服务
|
||||
</el-button>
|
||||
<el-select
|
||||
v-if="isServerRunning && swaggerFiles.length > 1"
|
||||
v-model="selectedSwaggerFile"
|
||||
placeholder="选择 Swagger 文件"
|
||||
class="file-select"
|
||||
@change="updateServerURL"
|
||||
>
|
||||
<el-option
|
||||
v-for="file in swaggerFiles"
|
||||
:key="file.name"
|
||||
:label="file.name"
|
||||
:value="file"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-if="serverURL"
|
||||
v-model="serverURL"
|
||||
readonly
|
||||
class="server-url-input"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="openSwaggerURL">打开</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="files-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>Swagger 文件列表</span>
|
||||
<el-button type="primary" size="small" @click="loadSwaggerFiles" :loading="isLoading">刷新</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="swaggerFiles" style="width: 100%" v-loading="isLoading">
|
||||
<el-table-column prop="name" label="文件名" show-overflow-tooltip/>
|
||||
<el-table-column prop="size" label="大小" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatFileSize(scope.row.size) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="modifiedTime" label="修改时间" width="180"/>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="viewFile(scope.row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="swaggerFiles.length === 0 && !isLoading" description="暂无 Swagger 文件"/>
|
||||
</el-card>
|
||||
|
||||
<el-card class="content-card" v-if="selectedFile">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ selectedFile.name }}</span>
|
||||
<el-button size="small" @click="selectedFile = null">关闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="file-content" v-loading="isLoading">
|
||||
<pre>{{ fileContent }}</pre>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.swagger-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.files-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.server-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.server-url-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-content {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.file-content pre {
|
||||
margin: 0;
|
||||
padding: 15px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
327
Tools/quickly/frontend/src/views/UpdateCommon.vue
Normal file
327
Tools/quickly/frontend/src/views/UpdateCommon.vue
Normal file
@@ -0,0 +1,327 @@
|
||||
<script lang="ts" setup>
|
||||
import {onMounted, reactive, ref} from 'vue'
|
||||
import {ElMessage} from 'element-plus'
|
||||
import {
|
||||
SelectDirectory,
|
||||
GetSettings,
|
||||
AddProjectConfig,
|
||||
RemoveProjectConfig,
|
||||
UpdateProjectConfig,
|
||||
UpdateServiceCommon,
|
||||
SaveSettings
|
||||
} from '../../wailsjs/go/backend/App'
|
||||
|
||||
interface ProjectConfig {
|
||||
name: string
|
||||
path: string
|
||||
}
|
||||
|
||||
interface AppSettings {
|
||||
theme: string
|
||||
language: string
|
||||
notifications: boolean
|
||||
autoStart: boolean
|
||||
modelBasePath: string
|
||||
databases?: any[]
|
||||
projects?: ProjectConfig[]
|
||||
}
|
||||
|
||||
const settings = reactive<AppSettings>({
|
||||
theme: 'light',
|
||||
language: 'zh-CN',
|
||||
notifications: true,
|
||||
autoStart: false,
|
||||
modelBasePath: '',
|
||||
projects: []
|
||||
})
|
||||
|
||||
const projectForm = reactive<ProjectConfig>({
|
||||
name: '',
|
||||
path: ''
|
||||
})
|
||||
|
||||
const updateForm = reactive({
|
||||
commitId: '',
|
||||
selectedProjects: [] as string[]
|
||||
})
|
||||
|
||||
const isUpdating = ref(false)
|
||||
const output = ref('')
|
||||
const projectDialogVisible = ref(false)
|
||||
const editingProjectIndex = ref(-1)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const loadedSettings = await GetSettings() as any
|
||||
settings.theme = loadedSettings.theme
|
||||
settings.language = loadedSettings.language
|
||||
settings.notifications = loadedSettings.notifications
|
||||
settings.autoStart = loadedSettings.autoStart
|
||||
settings.modelBasePath = loadedSettings.modelBasePath || ''
|
||||
if (loadedSettings.projects) {
|
||||
settings.projects = loadedSettings.projects
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error)
|
||||
}
|
||||
})
|
||||
|
||||
async function selectProjectPath() {
|
||||
try {
|
||||
const path = await SelectDirectory('选择项目路径', projectForm.path)
|
||||
if (path) {
|
||||
projectForm.path = path
|
||||
const pathParts = path.replace(/\\/g, '/').split('/')
|
||||
projectForm.name = pathParts[pathParts.length - 1] || ''
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to select directory:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function openAddProjectDialog() {
|
||||
editingProjectIndex.value = -1
|
||||
projectForm.name = ''
|
||||
projectForm.path = ''
|
||||
projectDialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEditProjectDialog(index: number) {
|
||||
editingProjectIndex.value = index
|
||||
const project = settings.projects![index]
|
||||
projectForm.name = project.name
|
||||
projectForm.path = project.path
|
||||
projectDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleAddProject() {
|
||||
if (!projectForm.name) {
|
||||
ElMessage.warning('请输入项目名称')
|
||||
return
|
||||
}
|
||||
|
||||
if (!projectForm.path) {
|
||||
ElMessage.warning('请选择项目路径')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (editingProjectIndex.value === -1) {
|
||||
await AddProjectConfig(projectForm.name, projectForm.path)
|
||||
ElMessage.success('添加成功')
|
||||
} else {
|
||||
await UpdateProjectConfig(settings.projects![editingProjectIndex.value].name, projectForm.name, projectForm.path)
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
projectDialogVisible.value = false
|
||||
|
||||
const loadedSettings = await GetSettings() as any
|
||||
settings.theme = loadedSettings.theme
|
||||
settings.language = loadedSettings.language
|
||||
settings.notifications = loadedSettings.notifications
|
||||
settings.autoStart = loadedSettings.autoStart
|
||||
settings.modelBasePath = loadedSettings.modelBasePath || ''
|
||||
if (loadedSettings.projects) {
|
||||
settings.projects = loadedSettings.projects
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save project config:', error)
|
||||
ElMessage.error('保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteProject(index: number) {
|
||||
const project = settings.projects![index]
|
||||
try {
|
||||
await RemoveProjectConfig(project.name)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
const loadedSettings = await GetSettings() as any
|
||||
settings.theme = loadedSettings.theme
|
||||
settings.language = loadedSettings.language
|
||||
settings.notifications = loadedSettings.notifications
|
||||
settings.autoStart = loadedSettings.autoStart
|
||||
settings.modelBasePath = loadedSettings.modelBasePath || ''
|
||||
if (loadedSettings.projects) {
|
||||
settings.projects = loadedSettings.projects
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete project config:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function updateCommonVersion() {
|
||||
if (!updateForm.commitId) {
|
||||
ElMessage.warning('请输入 Commit ID')
|
||||
return
|
||||
}
|
||||
|
||||
if (updateForm.selectedProjects.length === 0) {
|
||||
ElMessage.warning('请至少选择一个项目')
|
||||
return
|
||||
}
|
||||
|
||||
isUpdating.value = true
|
||||
output.value = '正在更新 Common 版本...\n'
|
||||
|
||||
try {
|
||||
const result = await UpdateServiceCommon(updateForm.commitId, updateForm.selectedProjects)
|
||||
output.value = result
|
||||
} catch (error) {
|
||||
console.error('Failed to update common version:', error)
|
||||
output.value = `更新失败: ${error}`
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
try {
|
||||
const currentSettings = await GetSettings() as any
|
||||
currentSettings.modelBasePath = settings.modelBasePath
|
||||
currentSettings.projects = settings.projects
|
||||
await SaveSettings(currentSettings)
|
||||
ElMessage.success('设置保存成功')
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error)
|
||||
ElMessage.error('设置保存失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="update-common-container">
|
||||
<el-card class="update-version-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>更新 Common 版本</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form label-width="140px" class="settings-form">
|
||||
<el-form-item label="Commit ID">
|
||||
<el-input
|
||||
v-model="updateForm.commitId"
|
||||
placeholder="请输入 Commit ID"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="选择项目">
|
||||
<el-checkbox-group v-model="updateForm.selectedProjects">
|
||||
<el-checkbox
|
||||
v-for="project in settings.projects"
|
||||
:key="project.name"
|
||||
:label="project.name"
|
||||
>
|
||||
{{ project.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="updateCommonVersion"
|
||||
:loading="isUpdating"
|
||||
>
|
||||
更新 Common 版本
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="执行输出">
|
||||
<el-input
|
||||
v-model="output"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
placeholder="更新结果将显示在这里"
|
||||
readonly
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="project-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>项目配置管理</span>
|
||||
<el-button type="primary" size="small" @click="openAddProjectDialog">添加配置</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-form label-width="140px" class="settings-form">
|
||||
<el-form-item label="Go Module Name">
|
||||
<el-input
|
||||
v-model="settings.modelBasePath"
|
||||
placeholder="请输入 Go Module Name,例如:git.hlsq.asia/mmorpg/service-common"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveSettings">保存设置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table :data="settings.projects" style="width: 100%">
|
||||
<el-table-column prop="name" label="项目名称"/>
|
||||
<el-table-column prop="path" label="项目路径" show-overflow-tooltip/>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="openEditProjectDialog(scope.$index)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDeleteProject(scope.$index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="projectDialogVisible"
|
||||
:title="editingProjectIndex === -1 ? '添加项目配置' : '编辑项目配置'"
|
||||
width="500px"
|
||||
>
|
||||
<el-form label-width="120px">
|
||||
<el-form-item label="项目名称">
|
||||
<el-input
|
||||
v-model="projectForm.name"
|
||||
placeholder="自动从路径提取,也可手动修改"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目路径">
|
||||
<el-input
|
||||
v-model="projectForm.path"
|
||||
placeholder="请输入或选择项目路径(需包含 go.mod 文件)"
|
||||
clearable
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="selectProjectPath">选择文件夹</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="projectDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleAddProject">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.update-common-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.update-common-card,
|
||||
.project-card,
|
||||
.update-version-card {
|
||||
max-width: 900px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
padding: 20px 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user