|
|
<script setup lang="ts">
|
|
|
import { Close, Lock, Minus, User } from "@element-plus/icons-vue";
|
|
|
import { ElForm, ElMessage, ElMessageBox } from "element-plus";
|
|
|
import { getVersion } from "@tauri-apps/api/app";
|
|
|
import { writeText } from "@tauri-apps/api/clipboard";
|
|
|
import { invoke } from "@tauri-apps/api/tauri";
|
|
|
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
|
|
|
import { api } from "../api";
|
|
|
import { getAllConfig, mergeConfig } from "../host/config";
|
|
|
import { log } from "../host/logger";
|
|
|
import { setSession } from "../host/session";
|
|
|
import type { SessionState } from "../host/types";
|
|
|
import {
|
|
|
closeWindow,
|
|
|
minimizeWindow,
|
|
|
openMainWindow,
|
|
|
startWindowDragging,
|
|
|
} from "../host/window";
|
|
|
|
|
|
/** el-form 需要稳定引用,勿使用每次渲染新建的 `{ username, password }` 对象 */
|
|
|
const loginForm = reactive({
|
|
|
username: "admin",
|
|
|
password: "admin",
|
|
|
});
|
|
|
const loginType = ref("NSFW");
|
|
|
const isLoading = ref(false);
|
|
|
const formRef = ref<InstanceType<typeof ElForm>>();
|
|
|
const isLoginSuccessed = ref(false);
|
|
|
const selectedWin = ref("");
|
|
|
const cachedWinKey = ref("");
|
|
|
const options = ref<Array<{ label: string; value: string }>>([]);
|
|
|
const appVersion = ref("0.1.0");
|
|
|
const checkingUpdate = ref(false);
|
|
|
const APT_SOURCE_ENTRY =
|
|
|
"deb [arch=amd64 signed-by=/usr/share/keyrings/zyyun-archive-keyring.gpg] http://80.12.140.29:80/apt v10 main";
|
|
|
const APT_SOURCE_SETUP_COMMAND = `echo "${APT_SOURCE_ENTRY}" | sudo tee /etc/apt/sources.list.d/zyyun.list && sudo apt update`;
|
|
|
|
|
|
interface AptUpdateCheckResult {
|
|
|
packageName: string;
|
|
|
currentVersion: string;
|
|
|
installedVersion: string;
|
|
|
candidateVersion: string;
|
|
|
hasUpdate: boolean;
|
|
|
sourceAvailable: boolean;
|
|
|
updateCommand: string;
|
|
|
}
|
|
|
|
|
|
let sessionState: SessionState = {
|
|
|
empUid: null,
|
|
|
winUid: null,
|
|
|
queueToken: null,
|
|
|
refreshToken: null,
|
|
|
};
|
|
|
|
|
|
const rules = {
|
|
|
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
|
|
|
password: [
|
|
|
{ required: true, message: "请输入密码", trigger: "blur" },
|
|
|
{ min: 5, message: "密码长度不能少于5位", trigger: "blur" },
|
|
|
],
|
|
|
};
|
|
|
|
|
|
const isFormValid = computed(
|
|
|
() => loginForm.username.trim() !== "" && loginForm.password.trim() !== "",
|
|
|
);
|
|
|
const isWindowSelected = computed(() => selectedWin.value !== "");
|
|
|
|
|
|
/**
|
|
|
* 在窄窗口中使用居中消息,避免提示框被裁剪。
|
|
|
*/
|
|
|
function showMessage(
|
|
|
type: "success" | "warning" | "error",
|
|
|
message: string,
|
|
|
): void {
|
|
|
ElMessage({
|
|
|
type,
|
|
|
message,
|
|
|
offset: 52,
|
|
|
grouping: true,
|
|
|
customClass: "narrow-window-message",
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 从窗口选择返回账号登录步骤。
|
|
|
*/
|
|
|
function backToUserForm(): void {
|
|
|
isLoginSuccessed.value = false;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 处理账号登录。
|
|
|
*/
|
|
|
async function handleLogin(): Promise<void> {
|
|
|
if (!isFormValid.value) {
|
|
|
showMessage("warning", "请输入用户名和密码");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const form = formRef.value;
|
|
|
if (!form) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
isLoading.value = true;
|
|
|
try {
|
|
|
await form.validate();
|
|
|
} catch {
|
|
|
isLoading.value = false;
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
const loginRes = await api.user.login({
|
|
|
loginMode: loginType.value,
|
|
|
clientType: "CALLER",
|
|
|
username: loginForm.username,
|
|
|
password: loginForm.password,
|
|
|
hallRegNum: "",
|
|
|
});
|
|
|
|
|
|
if (!loginRes?.queueToken) {
|
|
|
await log(
|
|
|
"warn",
|
|
|
`登录失败: 接口未返回有效 queueToken, user=${loginForm.username}`,
|
|
|
);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
sessionState = {
|
|
|
empUid: loginRes.operatorProfile.empUid,
|
|
|
winUid: 0,
|
|
|
queueToken: loginRes.queueToken,
|
|
|
refreshToken: loginRes.refreshToken,
|
|
|
};
|
|
|
|
|
|
await setSession(sessionState);
|
|
|
|
|
|
const winList = await api.window.list();
|
|
|
options.value = winList.windows.map((win) => ({
|
|
|
label: win.windowName,
|
|
|
value: win.windowUid.toString(),
|
|
|
}));
|
|
|
|
|
|
if (
|
|
|
cachedWinKey.value &&
|
|
|
options.value.some((item) => item.value === cachedWinKey.value)
|
|
|
) {
|
|
|
selectedWin.value = cachedWinKey.value;
|
|
|
}
|
|
|
|
|
|
isLoginSuccessed.value = true;
|
|
|
await log(
|
|
|
"info",
|
|
|
`登录成功: user=${loginForm.username}, empUid=${sessionState.empUid}`,
|
|
|
);
|
|
|
} catch (error) {
|
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
|
await log("error", `登录失败: user=${loginForm.username}, ${message}`);
|
|
|
} finally {
|
|
|
isLoading.value = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 处理窗口登录确认。
|
|
|
*/
|
|
|
async function handleWindowLogin(): Promise<void> {
|
|
|
isLoading.value = true;
|
|
|
try {
|
|
|
const winUid = Number.parseInt(selectedWin.value, 10);
|
|
|
if (!Number.isFinite(winUid) || winUid <= 0) {
|
|
|
showMessage("warning", "请选择有效窗口");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const selected = options.value.find(
|
|
|
(item) => item.value === selectedWin.value,
|
|
|
);
|
|
|
|
|
|
const initRes = await api.action.init({
|
|
|
empUid: Number(sessionState.empUid ?? -1),
|
|
|
windowUid: winUid,
|
|
|
});
|
|
|
|
|
|
const initSuccess =
|
|
|
((initRes as { data?: { success?: boolean } }).data?.success ??
|
|
|
(initRes as { success?: boolean }).success) === true;
|
|
|
|
|
|
if (!initSuccess) {
|
|
|
const initData = (initRes as { data?: { message?: string } }).data;
|
|
|
const msg =
|
|
|
(typeof initData?.message === "string" && initData.message.trim() !== ""
|
|
|
? initData.message
|
|
|
: null) ??
|
|
|
(typeof (initRes as { message?: string }).message === "string"
|
|
|
? (initRes as { message?: string }).message
|
|
|
: null) ??
|
|
|
"窗口初始化失败,请重试或更换窗口";
|
|
|
showMessage("error", String(msg));
|
|
|
await log("warn", `call-terminal/init 未成功: windowUid=${winUid}`);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
await mergeConfig({
|
|
|
last_username: loginForm.username.trim(),
|
|
|
selected_win_key: selectedWin.value,
|
|
|
selected_win_value: selected?.label ?? "",
|
|
|
selected_win_uid: winUid,
|
|
|
});
|
|
|
|
|
|
sessionState.winUid = winUid;
|
|
|
await setSession(sessionState);
|
|
|
|
|
|
// 登录完成后拉取 caller-init,并将返回 data 作为运行时缓存写入 session。
|
|
|
try {
|
|
|
const callerInit = await api.action.callerInit();
|
|
|
sessionState = {
|
|
|
...sessionState,
|
|
|
winUid: Number(callerInit.windowUid ?? winUid),
|
|
|
empUid: Number(callerInit.empUid ?? sessionState.empUid ?? -1),
|
|
|
windowName: String(callerInit.windowName ?? ""),
|
|
|
empName: String(callerInit.empName ?? ""),
|
|
|
autoCallEnabled: Boolean(callerInit.autoCallEnabled),
|
|
|
autoCallWaitSeconds: Number(callerInit.autoCallWaitSeconds ?? 0),
|
|
|
autoStartEnabled: Boolean(callerInit.autoStartEnabled),
|
|
|
autoStartWaitSeconds: Number(callerInit.autoStartWaitSeconds ?? 0),
|
|
|
callInHistoryEnabled: Boolean(callerInit.callInHistoryEnabled),
|
|
|
callNotifyForward: Number(callerInit.callNotifyForward ?? 0),
|
|
|
callNotifyMode: Number(callerInit.callNotifyMode ?? 0),
|
|
|
callNotifySmsUrl: String(callerInit.callNotifySmsUrl ?? ""),
|
|
|
callTransferEnabled: Boolean(callerInit.callTransferEnabled),
|
|
|
realnameCheckApi: String(callerInit.realnameCheckApi ?? ""),
|
|
|
rankWaitSeconds: Number(callerInit.rankWaitSeconds ?? 0),
|
|
|
ziyunServiceUrl: String(callerInit.ziyunServiceUrl ?? ""),
|
|
|
};
|
|
|
await setSession(sessionState);
|
|
|
await log(
|
|
|
"info",
|
|
|
`caller-init 缓存完成: windowUid=${sessionState.winUid}, empUid=${sessionState.empUid}`,
|
|
|
);
|
|
|
} catch (error) {
|
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
|
await log("warn", `caller-init 调用失败,继续使用本地会话: ${message}`);
|
|
|
}
|
|
|
|
|
|
await openMainWindow();
|
|
|
} catch (error) {
|
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
|
showMessage("error", message || "打开主窗口失败");
|
|
|
await log("error", `打开主窗口失败: ${message}`);
|
|
|
} finally {
|
|
|
isLoading.value = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function handleMinimizeClick(event: MouseEvent): Promise<void> {
|
|
|
event.preventDefault();
|
|
|
event.stopPropagation();
|
|
|
try {
|
|
|
await minimizeWindow();
|
|
|
} catch (error) {
|
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
|
showMessage("error", message || "最小化窗口失败");
|
|
|
await log("error", `最小化窗口失败: ${message}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function handleCloseClick(event: MouseEvent): Promise<void> {
|
|
|
event.preventDefault();
|
|
|
event.stopPropagation();
|
|
|
try {
|
|
|
await closeWindow();
|
|
|
} catch (error) {
|
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
|
showMessage("error", message || "关闭窗口失败");
|
|
|
await log("error", `关闭窗口失败: ${message}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function handleDragMouseDown(event: MouseEvent): Promise<void> {
|
|
|
if (event.button !== 0) {
|
|
|
return;
|
|
|
}
|
|
|
try {
|
|
|
await startWindowDragging();
|
|
|
} catch {
|
|
|
// 失败时仍可回退到 data-tauri-drag-region 行为。
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 处理回车键触发登录。
|
|
|
*/
|
|
|
function handleKeyPress(event: KeyboardEvent): void {
|
|
|
if (event.key === "Enter" && isFormValid.value && !isLoginSuccessed.value) {
|
|
|
void handleLogin();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 聚焦时选中输入内容。
|
|
|
*/
|
|
|
function handleInputFocus(event: Event): void {
|
|
|
const target = event.target as HTMLInputElement;
|
|
|
target.select();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 检查 apt 仓库中是否有新版本,并引导复制升级命令。
|
|
|
*/
|
|
|
async function handleCheckUpdate(): Promise<void> {
|
|
|
if (checkingUpdate.value) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
checkingUpdate.value = true;
|
|
|
try {
|
|
|
await log("info", `检查更新开始: package=call-client, currentVersion=${appVersion.value}`);
|
|
|
const result = await invoke<AptUpdateCheckResult>("check_apt_update", {
|
|
|
packageName: "call-client",
|
|
|
currentVersion: appVersion.value,
|
|
|
});
|
|
|
await log(
|
|
|
"info",
|
|
|
`检查更新结果: package=${result.packageName}, installed=${result.installedVersion}, candidate=${result.candidateVersion}, hasUpdate=${result.hasUpdate}, sourceAvailable=${result.sourceAvailable}`,
|
|
|
);
|
|
|
|
|
|
if (!result.sourceAvailable) {
|
|
|
await log("warn", `检查更新提示: 未检测到可用更新源, package=${result.packageName}`);
|
|
|
try {
|
|
|
await ElMessageBox.confirm(
|
|
|
[
|
|
|
"未检测到可用更新源,请先配置固定更新地址:",
|
|
|
APT_SOURCE_ENTRY,
|
|
|
"",
|
|
|
"点击“复制命令”后,在终端执行:",
|
|
|
APT_SOURCE_SETUP_COMMAND,
|
|
|
].join("\n"),
|
|
|
"更新源未配置",
|
|
|
{
|
|
|
confirmButtonText: "复制命令",
|
|
|
cancelButtonText: "关闭",
|
|
|
distinguishCancelAndClose: true,
|
|
|
type: "warning",
|
|
|
},
|
|
|
);
|
|
|
|
|
|
await writeText(APT_SOURCE_SETUP_COMMAND);
|
|
|
showMessage("success", "更新源初始化命令已复制到剪贴板");
|
|
|
} catch {
|
|
|
// 用户取消不提示错误。
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (!result.hasUpdate) {
|
|
|
await log("info", `检查更新提示: 当前已是最新版本, version=${result.installedVersion}`);
|
|
|
showMessage("success", `当前已是最新版本(${result.installedVersion})`);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
await log(
|
|
|
"info",
|
|
|
`检查更新提示: 检测到新版本, installed=${result.installedVersion}, candidate=${result.candidateVersion}`,
|
|
|
);
|
|
|
try {
|
|
|
await ElMessageBox.confirm(
|
|
|
[
|
|
|
`检测到新版本:${result.candidateVersion}`,
|
|
|
`当前版本:${result.installedVersion}`,
|
|
|
"",
|
|
|
"点击“复制命令”后,在终端执行升级:",
|
|
|
result.updateCommand,
|
|
|
].join("\n"),
|
|
|
"发现新版本",
|
|
|
{
|
|
|
confirmButtonText: "复制命令",
|
|
|
cancelButtonText: "关闭",
|
|
|
distinguishCancelAndClose: true,
|
|
|
type: "info",
|
|
|
},
|
|
|
);
|
|
|
|
|
|
await writeText(result.updateCommand);
|
|
|
showMessage("success", "升级命令已复制到剪贴板");
|
|
|
} catch {
|
|
|
// 用户取消不提示错误。
|
|
|
}
|
|
|
} catch (error) {
|
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
|
showMessage("error", `检查更新失败:${message}`);
|
|
|
await log("error", `检查更新失败: ${message}`);
|
|
|
} finally {
|
|
|
checkingUpdate.value = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
onMounted(async () => {
|
|
|
try {
|
|
|
appVersion.value = await getVersion();
|
|
|
} catch {
|
|
|
appVersion.value = "0.1.0";
|
|
|
}
|
|
|
|
|
|
const config = await getAllConfig();
|
|
|
if (typeof config.last_username === "string" && config.last_username.trim()) {
|
|
|
loginForm.username = config.last_username;
|
|
|
}
|
|
|
if (
|
|
|
typeof config.selected_win_key === "string" &&
|
|
|
config.selected_win_key.trim()
|
|
|
) {
|
|
|
cachedWinKey.value = config.selected_win_key;
|
|
|
selectedWin.value = config.selected_win_key;
|
|
|
}
|
|
|
window.addEventListener("keydown", handleKeyPress);
|
|
|
});
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
window.removeEventListener("keydown", handleKeyPress);
|
|
|
});
|
|
|
</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">
|
|
|
<div
|
|
|
class="login-header-drag"
|
|
|
data-tauri-drag-region
|
|
|
@dblclick.prevent.stop
|
|
|
@mousedown="handleDragMouseDown"
|
|
|
></div>
|
|
|
<div class="login-header-actions">
|
|
|
<button
|
|
|
class="control-button"
|
|
|
type="button"
|
|
|
@mousedown.stop
|
|
|
@click="handleMinimizeClick"
|
|
|
>
|
|
|
<el-icon class="control-icon">
|
|
|
<component :is="Minus" />
|
|
|
</el-icon>
|
|
|
</button>
|
|
|
<button
|
|
|
class="control-button"
|
|
|
type="button"
|
|
|
@mousedown.stop
|
|
|
@dblclick.prevent.stop
|
|
|
@click="handleCloseClick"
|
|
|
>
|
|
|
<el-icon class="control-icon">
|
|
|
<component :is="Close" />
|
|
|
</el-icon>
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="login-main">
|
|
|
<div
|
|
|
class="header-section"
|
|
|
data-tauri-drag-region
|
|
|
@dblclick.prevent.stop
|
|
|
@mousedown="handleDragMouseDown"
|
|
|
>
|
|
|
<div class="app-info" data-tauri-drag-region>
|
|
|
<h1 class="app-title" data-tauri-drag-region>紫云智慧大厅</h1>
|
|
|
<h2 class="app-subtitle" data-tauri-drag-region>呼叫客户端系统</h2>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="form-section">
|
|
|
<div class="form-wrapper">
|
|
|
<div v-show="!isLoginSuccessed" class="user-form">
|
|
|
<div class="form-header">
|
|
|
<p class="form-subtitle">请选择登录方式并输入账号信息</p>
|
|
|
</div>
|
|
|
|
|
|
<div class="type-selector">
|
|
|
<el-radio-group v-model="loginType" size="large">
|
|
|
<el-radio-button value="NSFW">综合管理平台账号</el-radio-button>
|
|
|
<el-radio-button value="QUEUE">自建账号</el-radio-button>
|
|
|
</el-radio-group>
|
|
|
</div>
|
|
|
|
|
|
<el-form
|
|
|
ref="formRef"
|
|
|
:model="loginForm"
|
|
|
:rules="rules"
|
|
|
class="login-form"
|
|
|
label-position="top"
|
|
|
@submit.prevent
|
|
|
>
|
|
|
<el-form-item label="账号" prop="username">
|
|
|
<el-input
|
|
|
v-model="loginForm.username"
|
|
|
:prefix-icon="User"
|
|
|
placeholder="请输入用户名/手机号"
|
|
|
size="large"
|
|
|
@focus="handleInputFocus"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="密码" prop="password">
|
|
|
<el-input
|
|
|
v-model="loginForm.password"
|
|
|
:prefix-icon="Lock"
|
|
|
type="password"
|
|
|
placeholder="请输入登录密码"
|
|
|
size="large"
|
|
|
show-password
|
|
|
@focus="handleInputFocus"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
|
|
|
<div class="form-actions">
|
|
|
<el-button
|
|
|
type="primary"
|
|
|
size="large"
|
|
|
:loading="isLoading"
|
|
|
:disabled="!isFormValid"
|
|
|
class="login-button"
|
|
|
@click="handleLogin"
|
|
|
>
|
|
|
{{ isLoading ? "登录中..." : "立即登录" }}
|
|
|
</el-button>
|
|
|
</div>
|
|
|
</el-form>
|
|
|
</div>
|
|
|
|
|
|
<div v-show="isLoginSuccessed" class="window-form">
|
|
|
<div class="form-header">
|
|
|
<p class="form-subtitle">请选择登录窗口</p>
|
|
|
</div>
|
|
|
<el-form>
|
|
|
<el-form-item class="window-select-item">
|
|
|
<el-select
|
|
|
v-model="selectedWin"
|
|
|
placeholder="请选择登录窗口"
|
|
|
size="large"
|
|
|
>
|
|
|
<el-option
|
|
|
v-for="item in options"
|
|
|
:key="item.value"
|
|
|
:label="item.label"
|
|
|
:value="item.value"
|
|
|
/>
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
<div class="form-actions">
|
|
|
<el-button
|
|
|
type="primary"
|
|
|
size="large"
|
|
|
:loading="isLoading"
|
|
|
:disabled="!isWindowSelected"
|
|
|
class="login-button"
|
|
|
@click="handleWindowLogin"
|
|
|
>
|
|
|
{{ isLoading ? "登录中..." : "立即登录" }}
|
|
|
</el-button>
|
|
|
</div>
|
|
|
<div class="form-actions">
|
|
|
<el-button
|
|
|
type="primary"
|
|
|
size="large"
|
|
|
class="login-button"
|
|
|
@click="backToUserForm"
|
|
|
>
|
|
|
返回
|
|
|
</el-button>
|
|
|
</div>
|
|
|
</el-form>
|
|
|
</div>
|
|
|
|
|
|
<div class="version-info">
|
|
|
<div class="version-row">
|
|
|
<div class="version">版本号:V{{ appVersion }}</div>
|
|
|
<el-button
|
|
|
text
|
|
|
type="primary"
|
|
|
size="small"
|
|
|
:loading="checkingUpdate"
|
|
|
@click="handleCheckUpdate"
|
|
|
>
|
|
|
检查更新
|
|
|
</el-button>
|
|
|
</div>
|
|
|
<div class="copyright">© 2023 紫云科技 版权所有</div>
|
|
|
</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, #004d99 0%, #003b7a 100%);
|
|
|
overflow: hidden;
|
|
|
position: relative;
|
|
|
border-radius: 5px;
|
|
|
padding: 0 16px;
|
|
|
}
|
|
|
|
|
|
.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: space-between;
|
|
|
align-items: center;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.login-header-drag {
|
|
|
flex: 1;
|
|
|
height: 100%;
|
|
|
}
|
|
|
|
|
|
.login-header-actions {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
}
|
|
|
|
|
|
.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;
|
|
|
cursor: pointer;
|
|
|
}
|
|
|
|
|
|
.control-button,
|
|
|
.control-button * {
|
|
|
cursor: pointer;
|
|
|
}
|
|
|
|
|
|
.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: 10px;
|
|
|
position: relative;
|
|
|
z-index: 2;
|
|
|
}
|
|
|
|
|
|
.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 22px;
|
|
|
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;
|
|
|
}
|
|
|
|
|
|
.type-selector {
|
|
|
margin-bottom: 25px;
|
|
|
|
|
|
:deep(.el-radio-group) {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
:deep(.el-radio-button) {
|
|
|
flex: 1;
|
|
|
}
|
|
|
|
|
|
:deep(.el-radio-button__inner) {
|
|
|
width: 100%;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.form-actions {
|
|
|
.login-button {
|
|
|
width: 100%;
|
|
|
margin-top: 10px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.window-select-item {
|
|
|
margin: 80px 0;
|
|
|
}
|
|
|
|
|
|
.version-info {
|
|
|
margin-top: 18px;
|
|
|
text-align: center;
|
|
|
color: #999;
|
|
|
font-size: 12px;
|
|
|
padding-top: 10px;
|
|
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
|
|
}
|
|
|
|
|
|
.version-row {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
}
|
|
|
|
|
|
.version {
|
|
|
margin-bottom: 0;
|
|
|
}
|
|
|
|
|
|
.copyright {
|
|
|
margin-top: 5px;
|
|
|
}
|
|
|
|
|
|
:global(.narrow-window-message) {
|
|
|
min-width: 0 !important;
|
|
|
width: calc(100vw - 32px) !important;
|
|
|
left: 16px !important;
|
|
|
right: 16px !important;
|
|
|
}
|
|
|
</style>
|