index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. <template>
  2. <div class="server-config-container">
  3. <!-- 顶部可拖动标题栏 -->
  4. <div class="drag-region">
  5. <div class="window-controls">
  6. <button class="window-btn minimize" @click="handleMinimize" title="最小化">
  7. <svg viewBox="0 0 12 12"><rect y="5" width="12" height="2" fill="currentColor"/></svg>
  8. </button>
  9. <button class="window-btn maximize" @click="handleMaximize" :title="isMaximized ? '还原' : '最大化'">
  10. <svg v-if="isMaximized" viewBox="0 0 12 12">
  11. <rect x="1.5" y="3" width="7" height="7" stroke="currentColor" stroke-width="1.5" fill="none"/>
  12. <path d="M3.5 3V1.5H11V9H9.5" stroke="currentColor" stroke-width="1.5" fill="none"/>
  13. </svg>
  14. <svg v-else viewBox="0 0 12 12">
  15. <rect x="1" y="1" width="10" height="10" stroke="currentColor" stroke-width="1.5" fill="none"/>
  16. </svg>
  17. </button>
  18. <button class="window-btn close" @click="handleClose" title="关闭">
  19. <svg viewBox="0 0 12 12">
  20. <path d="M1 1L11 11M1 11L11 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
  21. </svg>
  22. </button>
  23. </div>
  24. </div>
  25. <!-- 背景装饰 -->
  26. <div class="bg-decoration">
  27. <div class="circle circle-1"></div>
  28. <div class="circle circle-2"></div>
  29. <div class="circle circle-3"></div>
  30. </div>
  31. <div class="config-card">
  32. <div class="config-header">
  33. <div class="logo">
  34. <el-icon><Setting /></el-icon>
  35. </div>
  36. <h1>服务器配置</h1>
  37. <p>配置后端服务器地址以连接系统</p>
  38. </div>
  39. <!-- 服务器列表 -->
  40. <div class="server-list" v-if="serverStore.servers.length > 0">
  41. <div
  42. v-for="server in serverStore.servers"
  43. :key="server.id"
  44. class="server-item"
  45. :class="{ active: server.id === serverStore.currentServerId }"
  46. @click="selectServer(server.id)"
  47. >
  48. <div class="server-info">
  49. <div class="server-name">{{ server.name }}</div>
  50. <div class="server-url">{{ server.url }}</div>
  51. </div>
  52. <div class="server-actions">
  53. <el-tag v-if="server.id === serverStore.currentServerId" type="success" size="small">
  54. 当前使用
  55. </el-tag>
  56. <el-button
  57. type="danger"
  58. link
  59. size="small"
  60. @click.stop="deleteServer(server.id)"
  61. >
  62. 删除
  63. </el-button>
  64. </div>
  65. </div>
  66. </div>
  67. <el-divider v-if="serverStore.servers.length > 0" />
  68. <!-- 添加服务器表单 -->
  69. <el-form
  70. ref="formRef"
  71. :model="form"
  72. :rules="rules"
  73. label-position="top"
  74. class="config-form"
  75. >
  76. <el-form-item label="服务器名称" prop="name">
  77. <el-input
  78. v-model="form.name"
  79. placeholder="例如:本地服务器"
  80. size="large"
  81. :prefix-icon="Connection"
  82. />
  83. </el-form-item>
  84. <el-form-item label="服务器地址" prop="url">
  85. <div class="url-input-row">
  86. <el-input
  87. v-model="form.url"
  88. placeholder="例如:http://localhost:3000"
  89. size="large"
  90. :prefix-icon="Link"
  91. />
  92. <el-button @click="testConnection" :loading="testing" class="test-btn">
  93. 测试连接
  94. </el-button>
  95. </div>
  96. </el-form-item>
  97. <el-form-item class="checkbox-row">
  98. <el-checkbox v-model="form.isDefault">设为默认服务器</el-checkbox>
  99. </el-form-item>
  100. <el-form-item class="action-row">
  101. <el-button type="primary" @click="addServer" :loading="loading" class="primary-btn">
  102. 添加服务器
  103. </el-button>
  104. <el-button v-if="serverStore.isConfigured" @click="goBack" class="back-btn">
  105. 返回
  106. </el-button>
  107. </el-form-item>
  108. </el-form>
  109. <div class="connection-status" v-if="connectionResult !== null">
  110. <el-alert
  111. :type="connectionResult ? 'success' : 'error'"
  112. :title="connectionResult ? '连接成功' : '连接失败'"
  113. :description="connectionResult ? '服务器响应正常' : '无法连接到服务器,请检查地址是否正确'"
  114. show-icon
  115. />
  116. </div>
  117. </div>
  118. </div>
  119. </template>
  120. <script setup lang="ts">
  121. import { ref, reactive, onMounted } from 'vue';
  122. import { useRouter } from 'vue-router';
  123. import { Setting, Connection, Link } from '@element-plus/icons-vue';
  124. import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
  125. import { useServerStore } from '@/stores/server';
  126. import { useAuthStore } from '@/stores/auth';
  127. const router = useRouter();
  128. const serverStore = useServerStore();
  129. const authStore = useAuthStore();
  130. const formRef = ref<FormInstance>();
  131. const loading = ref(false);
  132. const testing = ref(false);
  133. const connectionResult = ref<boolean | null>(null);
  134. const isMaximized = ref(false);
  135. // 窗口控制
  136. function handleMinimize() {
  137. window.electronAPI?.minimizeWindow?.();
  138. }
  139. function handleMaximize() {
  140. window.electronAPI?.maximizeWindow?.();
  141. }
  142. function handleClose() {
  143. window.electronAPI?.closeWindow?.();
  144. }
  145. // 监听窗口最大化状态
  146. onMounted(async () => {
  147. if (window.electronAPI?.isMaximized) {
  148. isMaximized.value = await window.electronAPI.isMaximized();
  149. }
  150. window.electronAPI?.onMaximizedChange?.((maximized: boolean) => {
  151. isMaximized.value = maximized;
  152. });
  153. });
  154. const form = reactive({
  155. name: '',
  156. url: '',
  157. isDefault: true,
  158. });
  159. const rules: FormRules = {
  160. name: [{ required: true, message: '请输入服务器名称', trigger: 'blur' }],
  161. url: [
  162. { required: true, message: '请输入服务器地址', trigger: 'blur' },
  163. { pattern: /^https?:\/\/.+/, message: '请输入有效的 URL', trigger: 'blur' },
  164. ],
  165. };
  166. async function testConnection() {
  167. if (!form.url) {
  168. ElMessage.warning('请先输入服务器地址');
  169. return;
  170. }
  171. testing.value = true;
  172. connectionResult.value = null;
  173. try {
  174. const result = await serverStore.checkConnection(form.url);
  175. connectionResult.value = result;
  176. if (result) {
  177. ElMessage.success('连接成功');
  178. } else {
  179. ElMessage.error('连接失败');
  180. }
  181. } catch {
  182. connectionResult.value = false;
  183. ElMessage.error('连接失败');
  184. } finally {
  185. testing.value = false;
  186. }
  187. }
  188. async function addServer() {
  189. if (!formRef.value) return;
  190. const valid = await formRef.value.validate().catch(() => false);
  191. if (!valid) return;
  192. loading.value = true;
  193. try {
  194. // 先测试连接
  195. const connected = await serverStore.checkConnection(form.url);
  196. if (!connected) {
  197. ElMessage.warning('服务器连接失败,仍要添加吗?');
  198. }
  199. serverStore.addServer({
  200. name: form.name,
  201. url: form.url.replace(/\/$/, ''), // 移除末尾斜杠
  202. isDefault: form.isDefault,
  203. });
  204. ElMessage.success('服务器添加成功');
  205. // 清空表单
  206. form.name = '';
  207. form.url = '';
  208. form.isDefault = false;
  209. connectionResult.value = null;
  210. // 如果已登录且切换了服务器,清除登录状态
  211. if (authStore.isAuthenticated) {
  212. authStore.clearTokens();
  213. }
  214. } finally {
  215. loading.value = false;
  216. }
  217. }
  218. function selectServer(id: string) {
  219. if (id === serverStore.currentServerId) return;
  220. serverStore.setCurrentServer(id);
  221. // 切换服务器后清除登录状态
  222. authStore.clearTokens();
  223. ElMessage.success('已切换服务器');
  224. }
  225. async function deleteServer(id: string) {
  226. try {
  227. await ElMessageBox.confirm('确定要删除这个服务器配置吗?', '提示', {
  228. confirmButtonText: '确定',
  229. cancelButtonText: '取消',
  230. type: 'warning',
  231. });
  232. serverStore.removeServer(id);
  233. ElMessage.success('已删除');
  234. } catch {
  235. // 取消
  236. }
  237. }
  238. function goBack() {
  239. if (authStore.isAuthenticated) {
  240. router.push('/');
  241. } else {
  242. router.push('/login');
  243. }
  244. }
  245. </script>
  246. <style lang="scss" scoped>
  247. @use '@/styles/variables.scss' as *;
  248. .server-config-container {
  249. min-height: 100vh;
  250. display: flex;
  251. align-items: center;
  252. justify-content: center;
  253. background: linear-gradient(135deg, #f5f7fa 0%, #e4e8eb 100%);
  254. position: relative;
  255. overflow: hidden;
  256. padding: 20px;
  257. }
  258. // 顶部可拖动区域
  259. .drag-region {
  260. position: fixed;
  261. top: 0;
  262. left: 0;
  263. right: 0;
  264. height: 32px;
  265. -webkit-app-region: drag;
  266. z-index: 999;
  267. }
  268. // 窗口控制按钮
  269. .window-controls {
  270. position: absolute;
  271. top: 0;
  272. right: 0;
  273. display: flex;
  274. -webkit-app-region: no-drag;
  275. .window-btn {
  276. width: 46px;
  277. height: 32px;
  278. border: none;
  279. background: transparent;
  280. display: flex;
  281. align-items: center;
  282. justify-content: center;
  283. cursor: pointer;
  284. transition: background 0.15s;
  285. color: $text-secondary;
  286. svg {
  287. width: 12px;
  288. height: 12px;
  289. }
  290. &:hover {
  291. background: rgba(0, 0, 0, 0.06);
  292. }
  293. &.close:hover {
  294. background: #e81123;
  295. color: #fff;
  296. }
  297. }
  298. }
  299. // 背景装饰
  300. .bg-decoration {
  301. position: absolute;
  302. inset: 0;
  303. overflow: hidden;
  304. pointer-events: none;
  305. .circle {
  306. position: absolute;
  307. border-radius: 50%;
  308. opacity: 0.5;
  309. }
  310. .circle-1 {
  311. width: 400px;
  312. height: 400px;
  313. background: linear-gradient(135deg, rgba(79, 140, 255, 0.2), rgba(79, 140, 255, 0.05));
  314. top: -100px;
  315. right: -100px;
  316. }
  317. .circle-2 {
  318. width: 300px;
  319. height: 300px;
  320. background: linear-gradient(135deg, rgba(250, 112, 154, 0.15), rgba(254, 225, 64, 0.1));
  321. bottom: -80px;
  322. left: -80px;
  323. }
  324. .circle-3 {
  325. width: 200px;
  326. height: 200px;
  327. background: linear-gradient(135deg, rgba(102, 126, 234, 0.15), rgba(118, 75, 162, 0.1));
  328. top: 50%;
  329. left: 10%;
  330. transform: translateY(-50%);
  331. }
  332. }
  333. .config-card {
  334. width: 480px;
  335. max-width: 100%;
  336. padding: 48px 40px;
  337. background: #fff;
  338. border-radius: $radius-xl;
  339. box-shadow: $shadow-lg;
  340. position: relative;
  341. z-index: 1;
  342. border: 1px solid rgba(255, 255, 255, 0.8);
  343. }
  344. .config-header {
  345. text-align: center;
  346. margin-bottom: 32px;
  347. .logo {
  348. width: 64px;
  349. height: 64px;
  350. margin: 0 auto 20px;
  351. border-radius: $radius-lg;
  352. background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
  353. display: flex;
  354. align-items: center;
  355. justify-content: center;
  356. box-shadow: 0 8px 24px rgba(79, 172, 254, 0.3);
  357. .el-icon {
  358. font-size: 32px;
  359. color: #fff;
  360. }
  361. }
  362. h1 {
  363. margin: 0 0 12px;
  364. font-size: 24px;
  365. font-weight: 700;
  366. color: $text-primary;
  367. }
  368. p {
  369. margin: 0;
  370. color: $text-secondary;
  371. font-size: 14px;
  372. }
  373. }
  374. .server-list {
  375. margin-bottom: 16px;
  376. .server-item {
  377. display: flex;
  378. align-items: center;
  379. justify-content: space-between;
  380. padding: 14px 18px;
  381. border: 1px solid $border-light;
  382. border-radius: $radius-base;
  383. margin-bottom: 10px;
  384. cursor: pointer;
  385. transition: all 0.2s;
  386. background: $bg-light;
  387. &:hover {
  388. border-color: $primary-color;
  389. background: #fff;
  390. }
  391. &.active {
  392. border-color: $primary-color;
  393. background: rgba($primary-color, 0.05);
  394. box-shadow: 0 0 0 3px rgba($primary-color, 0.08);
  395. }
  396. .server-info {
  397. .server-name {
  398. font-weight: 600;
  399. color: $text-primary;
  400. font-size: 15px;
  401. }
  402. .server-url {
  403. font-size: 12px;
  404. color: $text-secondary;
  405. margin-top: 4px;
  406. }
  407. }
  408. .server-actions {
  409. display: flex;
  410. align-items: center;
  411. gap: 8px;
  412. }
  413. }
  414. }
  415. .config-form {
  416. :deep(.el-form-item__label) {
  417. font-weight: 500;
  418. color: $text-primary;
  419. }
  420. :deep(.el-input__wrapper) {
  421. border-radius: $radius-base;
  422. box-shadow: 0 0 0 1px $border-light inset;
  423. transition: all 0.2s;
  424. &:hover {
  425. box-shadow: 0 0 0 1px $primary-color inset;
  426. }
  427. &.is-focus {
  428. box-shadow: 0 0 0 1px $primary-color inset, 0 0 0 3px rgba($primary-color, 0.1);
  429. }
  430. }
  431. :deep(.el-input__inner) {
  432. height: 44px;
  433. font-size: 15px;
  434. }
  435. :deep(.el-input__prefix) {
  436. color: $text-secondary;
  437. }
  438. // URL输入行样式
  439. .url-input-row {
  440. display: flex;
  441. gap: 12px;
  442. width: 100%;
  443. .el-input {
  444. flex: 1;
  445. }
  446. .test-btn {
  447. height: 44px;
  448. padding: 0 20px;
  449. border-radius: $radius-base;
  450. background: $bg-base;
  451. border: 1px solid $border-light;
  452. color: $primary-color;
  453. font-weight: 500;
  454. white-space: nowrap;
  455. &:hover {
  456. background: $primary-color-light;
  457. border-color: $primary-color;
  458. }
  459. }
  460. }
  461. .checkbox-row {
  462. margin-bottom: 24px;
  463. :deep(.el-checkbox__label) {
  464. color: $text-secondary;
  465. }
  466. }
  467. .action-row {
  468. margin-bottom: 0;
  469. .primary-btn {
  470. height: 44px;
  471. padding: 0 28px;
  472. font-size: 15px;
  473. font-weight: 600;
  474. border-radius: $radius-base;
  475. background: linear-gradient(135deg, $primary-color 0%, #3a7bd5 100%);
  476. border: none;
  477. box-shadow: 0 4px 16px rgba($primary-color, 0.3);
  478. transition: all 0.3s;
  479. &:hover {
  480. transform: translateY(-1px);
  481. box-shadow: 0 6px 20px rgba($primary-color, 0.4);
  482. }
  483. &:active {
  484. transform: translateY(0);
  485. }
  486. }
  487. .back-btn {
  488. height: 44px;
  489. padding: 0 28px;
  490. font-size: 15px;
  491. font-weight: 500;
  492. border-radius: $radius-base;
  493. border: 1px solid $border-base;
  494. background: #fff;
  495. color: $text-regular;
  496. transition: all 0.2s;
  497. &:hover {
  498. border-color: $primary-color;
  499. color: $primary-color;
  500. }
  501. }
  502. }
  503. }
  504. :deep(.el-divider) {
  505. margin: 24px 0;
  506. border-color: $border-lighter;
  507. }
  508. .connection-status {
  509. margin-top: 20px;
  510. :deep(.el-alert) {
  511. border-radius: $radius-base;
  512. }
  513. }
  514. </style>