You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

979 lines
28 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<script setup lang="ts">
import { Close, Lock, Minus, User } from "@element-plus/icons-vue";
import { ElDialog, ElForm, ElMessage, ElMessageBox, ElProgress } from "element-plus";
import { getVersion } from "@tauri-apps/api/app";
import { writeText } from "@tauri-apps/api/clipboard";
import { listen } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api/tauri";
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
import { api } from "../api";
import { confirmNative, showErrorNative } from "../host/dialog";
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 ZYYUN_APT_SETUP_PROGRESS_EVENT = "zyyun-apt-setup-progress";
const aptSetupDialogVisible = ref(false);
const aptSetupPercent = ref(0);
const aptSetupHint = ref("");
const aptSetupFinished = ref(false);
const aptSetupProgressStatus = ref<"success" | "exception" | undefined>(undefined);
/** 默认紫云 apt 源行;可通过配置项 apt_source_deb_line 覆盖(见 ~/.config/com.ziyun.callclient/config.json */
const DEFAULT_APT_SOURCE_DEB_LINE =
"deb [arch=amd64 signed-by=/usr/share/keyrings/zyyun-archive-keyring.gpg] http://80.12.140.29:80/apt v10 main";
const aptSourceDebLine = ref(DEFAULT_APT_SOURCE_DEB_LINE);
function buildAptSourceSetupCommand(debLine: string): string {
const escaped = debLine.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
return `echo "${escaped}" | 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;
}
interface AptSetupProgressPayload {
percent: number;
message: 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,
/** 未选窗口前不写占位数字 0避免与「无效窗口」混淆且与 MainView `<=0` 判定一致 */
winUid: null,
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();
const callerWin = Number(callerInit.windowUid);
const resolvedWinUid =
Number.isFinite(callerWin) && callerWin > 0 ? callerWin : winUid;
const callerEmp = Number(callerInit.empUid);
const resolvedEmpUid = Number.isFinite(callerEmp) ? callerEmp : Number(sessionState.empUid ?? -1);
sessionState = {
...sessionState,
winUid: resolvedWinUid,
empUid: resolvedEmpUid,
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 源并 apt-get updateRust 通过事件推送进度。
*/
async function runZyyunAptSourceSetupWithProgress(debLine: string): Promise<void> {
aptSetupDialogVisible.value = true;
aptSetupFinished.value = false;
aptSetupPercent.value = 0;
aptSetupHint.value = "准备中…";
aptSetupProgressStatus.value = undefined;
const unlisten = await listen<AptSetupProgressPayload>(ZYYUN_APT_SETUP_PROGRESS_EVENT, (e) => {
const p = e.payload;
aptSetupPercent.value = Math.min(100, Math.max(0, Number(p.percent) || 0));
aptSetupHint.value = typeof p.message === "string" ? p.message : "";
});
const trimmed = debLine.trim();
const debPreview =
trimmed.length > 220 ? `${trimmed.slice(0, 220)}…(共 ${trimmed.length} 字符)` : trimmed;
await log(
"info",
`[apt-setup][前端] 即将 invoke setup_zyyun_apt_source, debLen=${trimmed.length}, debPreview=${debPreview}`,
);
try {
await invoke("setup_zyyun_apt_source", { debLine: trimmed });
aptSetupProgressStatus.value = "success";
await log("info", "[apt-setup][前端] setup_zyyun_apt_source invoke 成功结束");
} catch (error) {
aptSetupProgressStatus.value = "exception";
const message = error instanceof Error ? error.message : String(error);
await log("error", `[apt-setup][前端] setup_zyyun_apt_source invoke 失败: ${message}`);
throw error;
} finally {
unlisten();
aptSetupFinished.value = true;
}
}
/**
* 检查 apt 仓库中是否有新版本,并引导复制升级命令。
*/
async function handleCheckUpdate(): Promise<void> {
if (checkingUpdate.value) {
return;
}
checkingUpdate.value = true;
try {
await log(
"info",
`检查更新(前端): 开始, package=call-client, currentVersion=${appVersion.value}`,
);
const invokeCheck = (): Promise<AptUpdateCheckResult> =>
invoke<AptUpdateCheckResult>("check_apt_update", {
packageName: "call-client",
currentVersion: appVersion.value,
});
let result = await invokeCheck();
await log(
"info",
`检查更新(前端): 首次 check_apt_update 返回 installed=${result.installedVersion}, candidate=${result.candidateVersion}, sourceAvailable=${result.sourceAvailable}, hasUpdate=${result.hasUpdate}`,
);
if (!result.sourceAvailable) {
const cfg = await getAllConfig();
const alreadyConfigured = cfg.zyyun_apt_source_configured === true;
if (alreadyConfigured) {
await log(
"warn",
"检查更新: 已记录自动配置过 apt 源,但候选版本仍不可用,请人工排查网络或 GPG 密钥",
);
showMessage(
"warning",
"本机已自动配置过紫云软件源,但 apt 仍无法解析候选版本。请在终端执行 sudo apt update 后重试,或检查 /usr/share/keyrings/zyyun-archive-keyring.gpg 是否存在。",
);
return;
}
try {
await ElMessageBox.confirm(
"未检测到可用更新源。是否使用管理员权限自动写入紫云软件源并执行 apt 更新?(需要桌面环境的 pkexec 授权)",
"配置更新源",
{
type: "warning",
confirmButtonText: "开始配置",
cancelButtonText: "取消",
distinguishCancelAndClose: true,
},
);
} catch {
await log("info", "检查更新(前端): 用户在「配置更新源」对话框选择取消或关闭");
const setupCmd = buildAptSourceSetupCommand(aptSourceDebLine.value);
const body = [
"已取消自动配置。若需手动配置,可将下面一行写入软件源,并在终端执行复制出的命令:",
aptSourceDebLine.value,
"",
"点击「复制命令」后,在终端执行:",
setupCmd,
].join("\n");
const copy = await confirmNative({
title: "更新源未配置",
message: body,
okLabel: "复制命令",
cancelLabel: "关闭",
});
if (copy) {
await writeText(setupCmd);
showMessage("success", "更新源初始化命令已复制到剪贴板");
}
return;
}
await log("info", "检查更新(前端): 用户确认自动配置 apt 源,开始 runZyyunAptSourceSetupWithProgress");
await runZyyunAptSourceSetupWithProgress(aptSourceDebLine.value);
await mergeConfig({ zyyun_apt_source_configured: true });
await log("info", "已写入配置: zyyun_apt_source_configured=true");
result = await invokeCheck();
await log(
"info",
`检查更新(前端): 自动配置后再次 check_apt_update 返回 installed=${result.installedVersion}, candidate=${result.candidateVersion}, sourceAvailable=${result.sourceAvailable}, hasUpdate=${result.hasUpdate}`,
);
if (!result.sourceAvailable) {
await log("warn", "自动配置 apt 源后仍无法解析候选版本");
const setupCmd = buildAptSourceSetupCommand(aptSourceDebLine.value);
const body = [
"自动配置已完成,但 apt 仍无法解析候选版本。请检查网络、GPG 密钥,或在终端执行:",
setupCmd,
].join("\n");
const copy = await confirmNative({
title: "更新源仍不可用",
message: body,
okLabel: "复制命令",
cancelLabel: "关闭",
});
if (copy) {
await writeText(setupCmd);
showMessage("success", "命令已复制到剪贴板");
}
return;
}
}
if (!result.hasUpdate) {
await log("info", `检查更新提示: 当前已是最新版本, version=${result.installedVersion}`);
showMessage("success", `当前已是最新版本(${result.installedVersion}`);
return;
}
await log(
"info",
`检查更新提示: 检测到新版本, installed=${result.installedVersion}, candidate=${result.candidateVersion}`,
);
const body = [
`检测到新版本:${result.candidateVersion}`,
`当前版本:${result.installedVersion}`,
"",
"可选:",
"· 点击「应用内升级」将弹出管理员授权pkexec执行固定的 apt 更新与仅升级 call-client与终端命令等价包名写死在程序内。",
"· 点击「复制终端命令」可在终端自行执行 sudo。",
"",
"终端命令:",
result.updateCommand,
].join("\n");
try {
await ElMessageBox.confirm(body, "发现新版本", {
confirmButtonText: "应用内升级(需管理员密码)",
cancelButtonText: "复制终端命令",
distinguishCancelAndClose: true,
type: "info",
});
} catch (action: unknown) {
if (action === "cancel") {
await writeText(result.updateCommand);
await log("info", "检查更新(前端): 用户选择复制终端升级命令");
showMessage("success", "升级命令已复制到剪贴板");
} else {
await log(
"info",
`检查更新(前端): 关闭「发现新版本」或未应用内升级 action=${String(action)}`,
);
}
return;
}
await log(
"info",
"检查更新(前端): 用户选择应用内 apt 升级invoke upgrade_call_client_via_apt",
);
await invoke("upgrade_call_client_via_apt");
await log("info", "检查更新(前端): upgrade_call_client_via_apt 已成功返回");
try {
appVersion.value = await getVersion();
} catch {
// 忽略版本读取失败
}
showMessage(
"success",
`升级命令已执行完毕。界面显示版本:${appVersion.value}。若仍为旧版本,请重启本客户端后再试。`,
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
await showErrorNative(`检查更新失败:${message}`, "检查更新", {
logActions: "file",
});
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.apt_source_deb_line === "string" &&
config.apt_source_deb_line.trim() !== ""
) {
aptSourceDebLine.value = config.apt_source_deb_line.trim();
}
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>
<el-dialog
v-model="aptSetupDialogVisible"
title="正在配置更新源"
width="440px"
class="apt-setup-dialog"
:close-on-click-modal="false"
:close-on-press-escape="aptSetupFinished"
:show-close="aptSetupFinished"
>
<el-progress
:percentage="aptSetupPercent"
:status="aptSetupProgressStatus"
:stroke-width="16"
/>
<p class="apt-setup-hint">{{ aptSetupHint }}</p>
</el-dialog>
</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;
}
.apt-setup-hint {
margin-top: 12px;
font-size: 13px;
color: #606266;
line-height: 1.45;
}
:global(.narrow-window-message) {
min-width: 0 !important;
width: calc(100vw - 32px) !important;
left: 16px !important;
right: 16px !important;
}
</style>