first commit

This commit is contained in:
yuantao
2026-04-14 11:31:58 +08:00
commit 85574798f2
10 changed files with 3132 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.DS_Store
node_modules
/dist

66
README.md Normal file
View File

@@ -0,0 +1,66 @@
# iFlow Settings Editor
一个用于编辑 `C:\Users\MSI\.iflow\settings.json` 配置文件的桌面应用程序。
## 技术栈
- **Electron** - 桌面应用框架
- **Vue 3** - 前端框架 (组合式 API)
- **Vite** - 构建工具
- **@icon-park/vue-next** - 图标库
## 项目结构
```
settings-editor/
├── main.js # Electron 主进程
├── preload.js # 预加载脚本 (IPC 通信)
├── package.json # 项目配置
├── vite.config.js # Vite 配置
├── index.html # HTML 入口
└── src/
├── main.js # Vue 入口
└── App.vue # 主组件
```
## 快速开始
### 安装依赖
```bash
npm install
```
### 开发模式
```bash
npm run dev # 启动 Vite 开发服务器
npm run electron:dev # 同时运行 Electron (需先执行 npm run dev)
```
### 构建与运行
```bash
npm run build # 构建 Vue 应用到 dist 目录
npm start # 运行 Electron 应用
```
## 功能
- **常规设置**: 语言、主题、启动动画、检查点保存
- **API 配置**: 认证方式、API Key、Base URL、模型名称、搜索服务
- **MCP 服务器管理**: 添加、编辑、删除服务器配置
## 截图说明
应用采用 Windows 11 设计风格,包含:
- 自定义标题栏 (支持最小化/最大化/关闭)
- 侧边导航栏
- 表单编辑区域
- 底部状态栏
## 注意事项
- `webSecurity: false` 仅用于开发环境解决 CSP 问题
- 保存设置时会自动创建备份 (`settings.json.bak`)
- MCP 服务器参数每行一个,环境变量支持 JSON 格式

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self';">
<title>iFlow 设置编辑器</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

117
main.js Normal file
View File

@@ -0,0 +1,117 @@
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const path = require('path');
const fs = require('fs');
console.log('main.js loaded');
console.log('app.getPath("home"):', app.getPath('home'));
const SETTINGS_FILE = path.join(app.getPath('home'), '.iflow', 'settings.json');
console.log('SETTINGS_FILE:', SETTINGS_FILE);
let mainWindow;
const isDev = process.argv.includes('--dev');
function createWindow() {
console.log('Creating window...');
mainWindow = new BrowserWindow({
width: 1100,
height: 750,
minWidth: 900,
minHeight: 600,
backgroundColor: '#f3f3f3',
frame: false,
show: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
webSecurity: false
}
});
console.log('Loading index.html...');
if (isDev) {
mainWindow.loadURL('http://localhost:5173');
} else {
mainWindow.loadFile(path.join(__dirname, 'dist', 'index.html'));
}
console.log('index.html loading initiated');
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
console.error('Failed to load:', errorCode, errorDescription);
});
mainWindow.webContents.on('console-message', (event, level, message, line, sourceId) => {
console.log('Console [' + level + ']:', message);
});
mainWindow.once('ready-to-show', () => {
console.log('Window ready to show');
mainWindow.show();
});
mainWindow.on('closed', () => {
mainWindow = null;
});
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});
// Window controls
ipcMain.on('window-minimize', () => mainWindow.minimize());
ipcMain.on('window-maximize', () => {
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
} else {
mainWindow.maximize();
}
});
ipcMain.on('window-close', () => mainWindow.close());
ipcMain.handle('is-maximized', () => mainWindow.isMaximized());
// IPC Handlers
ipcMain.handle('load-settings', async () => {
try {
if (!fs.existsSync(SETTINGS_FILE)) {
return { success: false, error: 'File not found', data: null };
}
const data = fs.readFileSync(SETTINGS_FILE, 'utf-8');
const json = JSON.parse(data);
return { success: true, data: json };
} catch (error) {
return { success: false, error: error.message, data: null };
}
});
ipcMain.handle('save-settings', async (event, data) => {
try {
// Backup
if (fs.existsSync(SETTINGS_FILE)) {
const backupPath = SETTINGS_FILE + '.bak';
fs.copyFileSync(SETTINGS_FILE, backupPath);
}
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(data, null, 2), 'utf-8');
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});
ipcMain.handle('show-message', async (event, { type, title, message }) => {
return dialog.showMessageBox(mainWindow, { type, title, message });
});

2363
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "iflow-settings-editor",
"version": "1.0.0",
"description": "iFlow Settings Editor - Vue 3 + Electron",
"main": "main.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"start": "electron .",
"electron:dev": "concurrently \"vite\" \"electron . --dev\"",
"electron:start": "vite build && electron ."
},
"author": "iFlow",
"license": "MIT",
"devDependencies": {
"@icon-park/vue-next": "^1.4.2",
"@vitejs/plugin-vue": "^6.0.6",
"concurrently": "^8.2.2",
"electron": "^28.0.0",
"vite": "^8.0.8",
"vue": "^3.4.0"
},
"dependencies": {}
}

11
preload.js Normal file
View File

@@ -0,0 +1,11 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
loadSettings: () => ipcRenderer.invoke('load-settings'),
saveSettings: (data) => ipcRenderer.invoke('save-settings', data),
showMessage: (options) => ipcRenderer.invoke('show-message', options),
isMaximized: () => ipcRenderer.invoke('is-maximized'),
minimize: () => ipcRenderer.send('window-minimize'),
maximize: () => ipcRenderer.send('window-maximize'),
close: () => ipcRenderer.send('window-close')
});

514
src/App.vue Normal file
View File

@@ -0,0 +1,514 @@
<template>
<div class="app">
<div class="titlebar">
<div class="titlebar-left">
<div class="titlebar-icon"></div>
<span class="titlebar-title">iFlow Settings Editor</span>
</div>
<div class="titlebar-controls">
<button class="titlebar-btn" @click="minimize" title="最小化">
<svg viewBox="0 0 10 1"><line x1="0" y1="0.5" x2="10" y2="0.5"/></svg>
</button>
<button class="titlebar-btn" @click="maximize" title="最大化">
<svg viewBox="0 0 10 10"><rect x="0.5" y="0.5" width="9" height="9" stroke-width="1" stroke="currentColor" fill="none"/></svg>
</button>
<button class="titlebar-btn close" @click="close" title="关闭">
<svg viewBox="0 0 10 10"><line x1="0" y1="0" x2="10" y2="10"/><line x1="10" y1="0" x2="0" y2="10"/></svg>
</button>
</div>
</div>
<header class="header">
<div class="header-left">
<div class="header-icon"></div>
<span class="header-title">iFlow 设置编辑器</span>
<span class="header-subtitle">settings.json</span>
</div>
<div class="header-actions">
<button class="btn btn-secondary" @click="reloadSettings">
<Refresh size="14" />
重新加载
</button>
<button class="btn btn-primary" @click="saveSettings">
<Save size="14" />
保存更改
</button>
</div>
</header>
<main class="main">
<aside class="sidebar">
<div class="sidebar-section">
<div class="sidebar-title">常规</div>
<div class="nav-item" :class="{ active: currentSection === 'general' }" @click="showSection('general')">
<Config size="16" />
<span class="nav-item-text">基本设置</span>
</div>
<div class="nav-item" :class="{ active: currentSection === 'api' }" @click="showSection('api')">
<Key size="16" />
<span class="nav-item-text">API 配置</span>
</div>
</div>
<div class="sidebar-section">
<div class="sidebar-title">高级</div>
<div class="nav-item" :class="{ active: currentSection === 'mcp' }" @click="showSection('mcp')">
<Server size="16" />
<span class="nav-item-text">MCP 服务器</span>
<span class="nav-item-badge">{{ serverCount }}</span>
</div>
</div>
</aside>
<div class="content">
<section v-if="currentSection === 'general'">
<div class="content-header">
<h1 class="content-title">基本设置</h1>
<p class="content-desc">配置应用程序的常规选项</p>
</div>
<div class="card">
<div class="card-title">
<Globe size="16" />
语言与界面
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">语言</label>
<select class="form-select" v-model="settings.language">
<option value="zh-CN">简体中文</option>
<option value="en-US">English</option>
<option value="ja-JP">日本語</option>
</select>
</div>
<div class="form-group">
<label class="form-label">主题</label>
<select class="form-select" v-model="settings.theme">
<option value="Xcode">Xcode</option>
<option value="Dark">深色</option>
<option value="Light">浅色</option>
<option value="Solarized Dark">Solarized Dark</option>
</select>
</div>
</div>
</div>
<div class="card">
<div class="card-title">
<Setting size="16" />
其他设置
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">启动动画</label>
<select class="form-select" v-model="settings.bootAnimationShown">
<option :value="true">已显示</option>
<option :value="false">未显示</option>
</select>
</div>
<div class="form-group">
<label class="form-label">检查点保存</label>
<select class="form-select" v-model="settings.checkpointing.enabled">
<option :value="true">已启用</option>
<option :value="false">已禁用</option>
</select>
</div>
</div>
</div>
</section>
<section v-if="currentSection === 'api'">
<div class="content-header">
<h1 class="content-title">API 配置</h1>
<p class="content-desc">配置 AI 服务和搜索 API</p>
</div>
<div class="card">
<div class="card-title">
<Robot size="16" />
AI 模型
</div>
<div class="form-group">
<label class="form-label">认证方式</label>
<select class="form-select" v-model="settings.selectedAuthType">
<option value="iflow">iFlow</option>
<option value="api">API Key</option>
</select>
</div>
<div class="form-group">
<label class="form-label">API Key</label>
<input type="password" class="form-input" v-model="settings.apiKey" placeholder="sk-cp-XXXXX...">
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Base URL</label>
<input type="text" class="form-input" v-model="settings.baseUrl" placeholder="https://api.minimaxi.com/v1">
</div>
<div class="form-group">
<label class="form-label">模型名称</label>
<input type="text" class="form-input" v-model="settings.modelName" placeholder="MiniMax-M2.7">
</div>
</div>
</div>
<div class="card">
<div class="card-title">
<Search size="16" />
搜索服务
</div>
<div class="form-group">
<label class="form-label">搜索 API Key</label>
<input type="password" class="form-input" v-model="settings.searchApiKey" placeholder="sk-XXXXX...">
</div>
<div class="form-group">
<label class="form-label">CNA</label>
<input type="text" class="form-input" v-model="settings.cna" placeholder="CNA 标识">
</div>
</div>
</section>
<section v-if="currentSection === 'mcp'">
<div class="content-header">
<h1 class="content-title">MCP 服务器</h1>
<p class="content-desc">管理 Model Context Protocol 服务器配置</p>
</div>
<div class="form-group">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<label class="form-label" style="margin: 0;">服务器列表</label>
<button class="btn btn-primary" @click="addServer" style="padding: 6px 12px; font-size: 12px;">
<Add size="12" />
添加服务器
</button>
</div>
<div class="server-list">
<template v-if="serverCount > 0">
<div v-for="(config, name) in settings.mcpServers" :key="name"
class="server-item" :class="{ selected: currentServerName === name }"
@click="selectServer(name)">
<div class="server-info">
<div class="server-name">{{ name }}</div>
<div class="server-desc">{{ config.description || '无描述' }}</div>
</div>
<div class="server-status"></div>
</div>
</template>
<div v-else class="empty-state">
<Server size="48" class="empty-state-icon" />
<div class="empty-state-title">暂无 MCP 服务器</div>
<div class="empty-state-desc">点击上方按钮添加第一个服务器</div>
</div>
</div>
</div>
<div v-if="currentServer" class="card">
<div class="card-title">
<Edit size="16" />
编辑服务器
</div>
<div class="form-group">
<label class="form-label">名称</label>
<input type="text" class="form-input" id="serverName" :value="currentServerName" readonly>
</div>
<div class="form-group">
<label class="form-label">描述</label>
<input type="text" class="form-input" id="serverDescription" :value="currentServer.description || ''">
</div>
<div class="form-group">
<label class="form-label">命令</label>
<input type="text" class="form-input" id="serverCommand" :value="currentServer.command || ''">
</div>
<div class="form-group">
<label class="form-label">工作目录</label>
<input type="text" class="form-input" id="serverCwd" :value="currentServer.cwd || '.'">
</div>
<div class="form-group">
<label class="form-label">参数 (每行一个)</label>
<textarea class="form-textarea" id="serverArgs" rows="4">{{ serverArgsText }}</textarea>
</div>
<div class="form-group">
<label class="form-label">环境变量 (JSON 格式)</label>
<textarea class="form-textarea" id="serverEnv" rows="3">{{ serverEnvText }}</textarea>
</div>
<div style="margin-top: 16px;">
<button class="btn btn-danger" @click="deleteServer">
<Delete size="12" />
删除服务器
</button>
</div>
</div>
</section>
</div>
</main>
<footer class="footer">
<div class="footer-status">
<div class="footer-status-dot"></div>
<span>C:\Users\MSI\.iflow\settings.json</span>
</div>
<span :class="{ 'footer-modified': modified }">{{ modified ? '● 已修改' : '✓ 未修改' }}</span>
</footer>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, watch } from 'vue';
import { Refresh, Save, Config, Key, Server, Globe, Setting, Robot, Search, Add, Edit, Delete } from '@icon-park/vue-next';
const settings = ref({
language: 'zh-CN',
theme: 'Xcode',
bootAnimationShown: true,
checkpointing: { enabled: true },
mcpServers: {},
selectedAuthType: 'iflow',
apiKey: '',
baseUrl: '',
modelName: '',
searchApiKey: '',
cna: ''
});
const originalSettings = ref({});
const modified = ref(false);
const currentSection = ref('general');
const currentServerName = ref(null);
const isLoading = ref(true);
const loadSettings = async () => {
const result = await window.electronAPI.loadSettings();
if (result.success) {
const data = result.data;
if (!data.checkpointing) data.checkpointing = { enabled: true };
if (!data.mcpServers) data.mcpServers = {};
settings.value = data;
originalSettings.value = JSON.parse(JSON.stringify(data));
modified.value = false;
}
isLoading.value = false;
};
const saveSettings = async () => {
collectServerData();
const result = await window.electronAPI.saveSettings(settings.value);
if (result.success) {
originalSettings.value = JSON.parse(JSON.stringify(settings.value));
modified.value = false;
await window.electronAPI.showMessage({ type: 'info', title: '保存成功', message: '设置已保存到 settings.json' });
} else {
await window.electronAPI.showMessage({ type: 'error', title: '保存失败', message: `无法保存设置: ${result.error}` });
}
};
const reloadSettings = async () => {
if (modified.value && !confirm('当前有未保存的更改,确定要重新加载吗?')) return;
currentServerName.value = null;
await loadSettings();
};
watch(settings, () => { modified.value = true }, { deep: true });
const showSection = (section) => { currentSection.value = section; };
const serverCount = computed(() => settings.value.mcpServers ? Object.keys(settings.value.mcpServers).length : 0);
const selectServer = (name) => { currentServerName.value = name; };
const addServer = () => {
const name = prompt('请输入服务器名称:');
if (!name) return;
if (!settings.value.mcpServers) settings.value.mcpServers = {};
if (settings.value.mcpServers[name]) {
alert('服务器已存在');
return;
}
settings.value.mcpServers[name] = { command: 'npx', args: ['-y', 'package-name'] };
currentServerName.value = name;
};
const deleteServer = () => {
if (!currentServerName.value) return;
if (!confirm(`确定要删除服务器 "${currentServerName.value}" 吗?`)) return;
delete settings.value.mcpServers[currentServerName.value];
currentServerName.value = null;
};
const collectServerData = () => {
if (!currentServerName.value) return;
const el = document.getElementById('serverName');
if (el) {
const name = el.value.trim();
if (name !== currentServerName.value) {
delete settings.value.mcpServers[currentServerName.value];
currentServerName.value = name;
}
settings.value.mcpServers[name] = {
command: document.getElementById('serverCommand')?.value || '',
description: document.getElementById('serverDescription')?.value || '',
cwd: document.getElementById('serverCwd')?.value || '.',
args: (document.getElementById('serverArgs')?.value || '').split('\n').map(s => s.trim()).filter(s => s)
};
const envText = document.getElementById('serverEnv')?.value || '';
if (envText) {
try { settings.value.mcpServers[name].env = JSON.parse(envText); }
catch (e) { alert('环境变量 JSON 格式错误'); }
}
}
};
const currentServer = computed(() => {
if (!currentServerName.value || !settings.value.mcpServers) return null;
return settings.value.mcpServers[currentServerName.value];
});
const serverArgsText = computed(() => {
if (!currentServer.value) return '';
return (currentServer.value.args || []).join('\n');
});
const serverEnvText = computed(() => {
if (!currentServer.value || !currentServer.value.env) return '';
return JSON.stringify(currentServer.value.env, null, 2);
});
const minimize = () => window.electronAPI.minimize();
const maximize = () => window.electronAPI.maximize();
const close = () => window.electronAPI.close();
onMounted(() => { loadSettings(); });
</script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg-primary: #f3f3f3;
--bg-secondary: #ffffff;
--bg-tertiary: #f9f9f9;
--bg-hover: #e8e8e8;
--text-primary: #1a1a1a;
--text-secondary: #5a5a5a;
--text-tertiary: #8a8a8a;
--accent: #0078d4;
--accent-hover: #006cbd;
--accent-light: #d5e8ff;
--border: #e0e0e0;
--radius: 8px;
--radius-lg: 12px;
--shadow: rgba(0, 0, 0, 0.06);
}
body {
font-family: 'Segoe UI Variable', 'Segoe UI', sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
height: 100vh;
overflow: hidden;
user-select: none;
}
.app { display: flex; flex-direction: column; height: 100vh; }
.titlebar {
display: flex; align-items: center; justify-content: space-between;
height: 32px; background: var(--bg-secondary); border-bottom: 1px solid var(--border);
-webkit-app-region: drag;
}
.titlebar-left { display: flex; align-items: center; padding-left: 12px; gap: 8px; }
.titlebar-icon { width: 16px; height: 16px; background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%); border-radius: 4px; }
.titlebar-title { font-size: 12px; color: var(--text-secondary); }
.titlebar-controls { display: flex; -webkit-app-region: no-drag; }
.titlebar-btn {
width: 46px; height: 32px; display: flex; align-items: center; justify-content: center;
border: none; background: transparent; color: var(--text-primary); cursor: pointer; transition: background 0.1s;
}
.titlebar-btn:hover { background: var(--bg-hover); }
.titlebar-btn.close:hover { background: #e81123; color: white; }
.titlebar-btn svg { width: 10px; height: 10px; stroke: currentColor; stroke-width: 1; fill: none; }
.header {
display: flex; align-items: center; justify-content: space-between;
padding: 0 24px; height: 56px; background: var(--bg-secondary); border-bottom: 1px solid var(--border);
}
.header-left { display: flex; align-items: center; gap: 12px; }
.header-icon { width: 24px; height: 24px; background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%); border-radius: 6px; }
.header-title { font-size: 14px; font-weight: 600; }
.header-subtitle { font-size: 12px; color: var(--text-tertiary); margin-left: 8px; }
.header-actions { display: flex; gap: 8px; }
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
padding: 8px 16px; border: none; border-radius: var(--radius);
font-family: inherit; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.15s;
}
.btn-primary { background: var(--accent); color: white; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-secondary { background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border); }
.btn-secondary:hover { background: var(--bg-hover); }
.btn-danger { background: #d13438; color: white; }
.btn-danger:hover { background: #b52d30; }
.main { display: flex; flex: 1; overflow: hidden; }
.sidebar { width: 240px; background: var(--bg-secondary); border-right: 1px solid var(--border); padding: 8px; }
.sidebar-section { margin-bottom: 16px; }
.sidebar-title { font-size: 11px; font-weight: 600; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: 0.5px; padding: 8px 12px 4px; }
.nav-item {
display: flex; align-items: center; gap: 10px; padding: 10px 12px;
border-radius: var(--radius); cursor: pointer; transition: all 0.15s; color: var(--text-primary);
}
.nav-item:hover { background: var(--bg-hover); }
.nav-item.active { background: var(--accent-light); color: var(--accent); }
.nav-item-text { font-size: 13px; }
.nav-item-badge { margin-left: auto; background: var(--bg-tertiary); padding: 2px 8px; border-radius: 10px; font-size: 11px; color: var(--text-tertiary); }
.content { flex: 1; padding: 24px; overflow-y: auto; background: var(--bg-primary); }
.content-header { margin-bottom: 24px; }
.content-title { font-size: 24px; font-weight: 600; margin-bottom: 4px; }
.content-desc { font-size: 13px; color: var(--text-tertiary); }
.card { background: var(--bg-secondary); border-radius: var(--radius-lg); border: 1px solid var(--border); padding: 20px; margin-bottom: 16px; box-shadow: 0 2px 8px var(--shadow); }
.card-title { font-size: 14px; font-weight: 600; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
.form-group { margin-bottom: 16px; }
.form-group:last-child { margin-bottom: 0; }
.form-label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 6px; }
.form-input {
width: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius);
font-family: 'Cascadia Code', 'Consolas', monospace; font-size: 13px;
background: var(--bg-secondary); color: var(--text-primary); transition: all 0.15s;
}
.form-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-light); }
.form-input::placeholder { color: var(--text-tertiary); }
.form-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
.form-select {
width: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius);
font-family: inherit; font-size: 13px; background: var(--bg-secondary); color: var(--text-primary);
cursor: pointer; appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%235a5a5a' d='M2.5 4.5L6 8l3.5-3.5'/%3E%3C/svg%3E");
background-repeat: no-repeat; background-position: right 12px center; padding-right: 36px;
}
.form-select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-light); }
.form-textarea {
width: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius);
font-family: 'Cascadia Code', 'Consolas', monospace; font-size: 13px;
background: var(--bg-secondary); color: var(--text-primary); resize: vertical; min-height: 80px;
}
.form-textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-light); }
.server-list { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.server-item {
display: flex; align-items: center; padding: 12px 16px;
border-bottom: 1px solid var(--border); cursor: pointer; transition: all 0.15s;
}
.server-item:last-child { border-bottom: none; }
.server-item:hover { background: var(--bg-hover); }
.server-item.selected { background: var(--accent-light); }
.server-info { flex: 1; }
.server-name { font-size: 13px; font-weight: 500; }
.server-desc { font-size: 11px; color: var(--text-tertiary); margin-top: 2px; }
.server-status { width: 8px; height: 8px; border-radius: 50%; background: #6ccb5f; }
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px; text-align: center; }
.empty-state-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.4; color: var(--text-tertiary); }
.empty-state-title { font-size: 16px; font-weight: 500; margin-bottom: 8px; }
.empty-state-desc { font-size: 13px; color: var(--text-tertiary); margin-bottom: 20px; }
.footer { display: flex; align-items: center; justify-content: space-between; padding: 12px 24px; background: var(--bg-secondary); border-top: 1px solid var(--border); font-size: 12px; color: var(--text-tertiary); }
.footer-status { display: flex; align-items: center; gap: 6px; }
.footer-status-dot { width: 6px; height: 6px; border-radius: 50%; background: #6ccb5f; }
.footer-modified { color: var(--accent); }
.iconpark-icon { display: inline-block; vertical-align: middle; }
</style>

4
src/main.js Normal file
View File

@@ -0,0 +1,4 @@
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');

17
vite.config.js Normal file
View File

@@ -0,0 +1,17 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
export default defineConfig({
plugins: [vue()],
base: './',
build: {
outDir: 'dist',
emptyOutDir: true
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
});