|
|
<script setup lang="ts">
|
|
|
import { Close, Minus } from "@element-plus/icons-vue";
|
|
|
import { ElMessage } from "element-plus";
|
|
|
import { computed, ref } from "vue";
|
|
|
import { useRouter } from "vue-router";
|
|
|
import { mergeConfig } from "../host/config";
|
|
|
import { log } from "../host/logger";
|
|
|
import { closeWindow, minimizeWindow } from "../host/window";
|
|
|
import { applyServerIpToHttp } from "../utils/service";
|
|
|
|
|
|
const router = useRouter();
|
|
|
const serverIp = ref("");
|
|
|
const saving = ref(false);
|
|
|
|
|
|
const isValid = computed(() => serverIp.value.trim().length > 0);
|
|
|
|
|
|
/**
|
|
|
* 在窄窗口中使用居中消息,避免提示框被裁剪。
|
|
|
*/
|
|
|
function showMessage(
|
|
|
type: "success" | "warning" | "error",
|
|
|
message: string,
|
|
|
): void {
|
|
|
ElMessage({
|
|
|
type,
|
|
|
message,
|
|
|
offset: 52,
|
|
|
grouping: true,
|
|
|
customClass: "narrow-window-message",
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 校验服务地址输入是否合法。
|
|
|
*/
|
|
|
function validateHostInput(raw: string): boolean {
|
|
|
const value = raw.trim();
|
|
|
if (!value) {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
if (value.startsWith("http://") || value.startsWith("https://")) {
|
|
|
try {
|
|
|
const url = new URL(value);
|
|
|
return Boolean(url);
|
|
|
} catch {
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return /^[\w.\-]+(?::\d+)?$/.test(value) || /^\d{1,3}(\.\d{1,3}){3}(?::\d+)?$/.test(value);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 保存服务地址并跳转登录页。
|
|
|
*/
|
|
|
async function handleSave(): Promise<void> {
|
|
|
const value = serverIp.value.trim();
|
|
|
if (!value) {
|
|
|
showMessage("warning", "请输入服务器 IP 或地址");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (!validateHostInput(value)) {
|
|
|
showMessage("warning", "地址格式不正确,例如:192.168.1.10 或 192.168.1.10:8845");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
saving.value = true;
|
|
|
|
|
|
try {
|
|
|
await mergeConfig({ server_ip: value });
|
|
|
applyServerIpToHttp(value);
|
|
|
await log("info", `已保存服务器地址: ${value}`);
|
|
|
showMessage("success", "保存成功");
|
|
|
await router.replace("/login");
|
|
|
} catch (error) {
|
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
|
showMessage("error", message || "保存失败");
|
|
|
await log("error", `保存服务器地址失败: ${message}`);
|
|
|
} finally {
|
|
|
saving.value = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function handleMinimizeClick(event: MouseEvent): void {
|
|
|
event.preventDefault();
|
|
|
event.stopPropagation();
|
|
|
void minimizeWindow();
|
|
|
}
|
|
|
|
|
|
function handleCloseClick(event: MouseEvent): void {
|
|
|
event.preventDefault();
|
|
|
event.stopPropagation();
|
|
|
void closeWindow();
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
<div class="login-container">
|
|
|
<div class="background-elements">
|
|
|
<div class="circle circle-1"></div>
|
|
|
<div class="circle circle-2"></div>
|
|
|
</div>
|
|
|
|
|
|
<div class="login-header drag-region">
|
|
|
<button class="control-button" type="button" @click="handleMinimizeClick">
|
|
|
<el-icon class="control-icon">
|
|
|
<component :is="Minus" />
|
|
|
</el-icon>
|
|
|
</button>
|
|
|
<button class="control-button" type="button" @click="handleCloseClick">
|
|
|
<el-icon class="control-icon">
|
|
|
<component :is="Close" />
|
|
|
</el-icon>
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
<div class="login-main">
|
|
|
<div class="header-section drag-region">
|
|
|
<div class="app-info">
|
|
|
<h1 class="app-title">服务地址</h1>
|
|
|
<h2 class="app-subtitle">请先配置服务器 IP 或地址</h2>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="form-section">
|
|
|
<div class="form-wrapper">
|
|
|
<div class="form-header">
|
|
|
<p class="form-subtitle">默认端口 8845;支持 host:port 或完整 http(s) 地址</p>
|
|
|
</div>
|
|
|
|
|
|
<el-input
|
|
|
v-model="serverIp"
|
|
|
size="large"
|
|
|
clearable
|
|
|
placeholder="例如 192.168.1.10"
|
|
|
@keyup.enter="handleSave"
|
|
|
/>
|
|
|
|
|
|
<div class="form-actions">
|
|
|
<el-button
|
|
|
type="primary"
|
|
|
size="large"
|
|
|
class="login-button"
|
|
|
:loading="saving"
|
|
|
:disabled="!isValid"
|
|
|
@click="handleSave"
|
|
|
>
|
|
|
保存并进入登录
|
|
|
</el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
.login-container {
|
|
|
width: 100vw;
|
|
|
height: 100vh;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
justify-content: flex-start;
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
overflow: hidden;
|
|
|
position: relative;
|
|
|
padding: 0 16px 20px;
|
|
|
}
|
|
|
|
|
|
.drag-region {
|
|
|
-webkit-app-region: drag;
|
|
|
}
|
|
|
|
|
|
.background-elements {
|
|
|
position: absolute;
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
pointer-events: none;
|
|
|
|
|
|
.circle {
|
|
|
position: absolute;
|
|
|
border-radius: 50%;
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
|
|
|
&.circle-1 {
|
|
|
width: 300px;
|
|
|
height: 300px;
|
|
|
top: -100px;
|
|
|
left: -100px;
|
|
|
}
|
|
|
|
|
|
&.circle-2 {
|
|
|
width: 200px;
|
|
|
height: 200px;
|
|
|
right: -80px;
|
|
|
bottom: -80px;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.login-header {
|
|
|
width: 100%;
|
|
|
height: 32px;
|
|
|
display: flex;
|
|
|
justify-content: flex-end;
|
|
|
align-items: center;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.control-button {
|
|
|
width: 32px;
|
|
|
height: 32px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
color: white;
|
|
|
border-radius: 4px;
|
|
|
-webkit-app-region: no-drag;
|
|
|
border: none;
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
.control-button:hover {
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
}
|
|
|
|
|
|
.control-icon {
|
|
|
font-size: 20px;
|
|
|
}
|
|
|
|
|
|
.login-main {
|
|
|
width: 100%;
|
|
|
max-width: 420px;
|
|
|
z-index: 1;
|
|
|
flex: 1;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
justify-content: center;
|
|
|
padding-bottom: 16px;
|
|
|
}
|
|
|
|
|
|
.header-section {
|
|
|
text-align: center;
|
|
|
margin-bottom: 30px;
|
|
|
}
|
|
|
|
|
|
.app-title {
|
|
|
font-size: 28px;
|
|
|
font-weight: 700;
|
|
|
color: white;
|
|
|
margin-bottom: 8px;
|
|
|
}
|
|
|
|
|
|
.app-subtitle {
|
|
|
font-size: 16px;
|
|
|
color: rgba(255, 255, 255, 0.9);
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
|
|
|
.form-wrapper {
|
|
|
background: rgba(255, 255, 255, 0.95);
|
|
|
border-radius: 5px;
|
|
|
padding: 28px 30px 26px;
|
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
|
|
}
|
|
|
|
|
|
.form-header {
|
|
|
text-align: center;
|
|
|
margin-bottom: 25px;
|
|
|
}
|
|
|
|
|
|
.form-subtitle {
|
|
|
color: #666;
|
|
|
font-size: 13px;
|
|
|
}
|
|
|
|
|
|
.form-actions {
|
|
|
margin-top: 24px;
|
|
|
}
|
|
|
|
|
|
.login-button {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
:global(.narrow-window-message) {
|
|
|
min-width: 0 !important;
|
|
|
width: calc(100vw - 32px) !important;
|
|
|
left: 16px !important;
|
|
|
right: 16px !important;
|
|
|
}
|
|
|
</style>
|