mirror of
https://github.com/gl-inet/glkvm-cloud.git
synced 2026-10-04 12:41:42 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8703f91ebf | |||
| 1c473458cf | |||
| c6c09dbae1 | |||
| 9258aa5f43 | |||
| 8994cccb25 | |||
| 60994b9513 |
@@ -67,6 +67,7 @@ We provide **two** ways to install GLKVM Cloud:
|
||||
#### A) One-line installer (recommended, x86_64/amd64)
|
||||
|
||||
> **Note:** The one-line installer is **Docker-based**. It automates Docker/Compose setup, pulls images, renders configs from templates, and starts services for you.
|
||||
>
|
||||
> **Platform:** currently supports **x86_64 (amd64)** only.
|
||||
|
||||
Run **as root**:
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
#### A) 一键安装脚本(推荐,仅支持 x86_64 / amd64)
|
||||
|
||||
> **注意:** 一键安装脚本基于 **Docker**。它会自动完成 Docker / Docker Compose 的安装、拉取镜像、根据模板渲染配置文件,并启动所有服务。
|
||||
>
|
||||
> **平台支持:** 当前仅支持 **x86_64(amd64)** 平台。
|
||||
|
||||
使用 **root 权限** 运行以下命令安装 GLKVM 轻量云:
|
||||
@@ -78,6 +79,7 @@
|
||||
#### B) 使用 Docker 手动安装
|
||||
|
||||
> 完整参考文档请查看:[`docker-compose/README-CN.md`](https://github.com/gl-inet/glkvm-cloud/blob/main/docker-compose/README-CN.md)
|
||||
>
|
||||
> 平台支持: 同时支持 x86_64(amd64) 与 arm64(AArch64) 平台。
|
||||
|
||||
|
||||
|
||||
@@ -432,9 +432,10 @@ func (srv *RttyServer) ListenAPI() error {
|
||||
|
||||
r.GET("/auth-config", func(c *gin.Context) {
|
||||
authConfig := gin.H{
|
||||
"ldapEnabled": cfg.LdapEnabled,
|
||||
"legacyPassword": cfg.Password != "",
|
||||
"oidcEnabled": cfg.OIDCEnabled,
|
||||
"ldapEnabled": cfg.LdapEnabled,
|
||||
"legacyPassword": cfg.Password != "",
|
||||
"oidcEnabled": cfg.OIDCEnabled,
|
||||
"kvmCloudVersion": KVMCloudVersion,
|
||||
}
|
||||
c.JSON(http.StatusOK, authConfig)
|
||||
})
|
||||
|
||||
@@ -84,6 +84,13 @@ type Config struct {
|
||||
// =====================================================
|
||||
// Enable proxy mode (app is behind Nginx/Traefik/Caddy/Cloudflare)
|
||||
ReverseProxyEnabled bool
|
||||
|
||||
// =====================================================
|
||||
// Device Remote Access
|
||||
// =====================================================
|
||||
// Host[:port] used to generate device remote access address:
|
||||
// <deviceId>.<DEVICE_ENDPOINT_HOST>
|
||||
DeviceEndpointHost string
|
||||
}
|
||||
|
||||
// docker mode fixed path for reading certificate
|
||||
@@ -268,6 +275,24 @@ func parseYamlCfg(cfg *Config, conf string) error {
|
||||
}
|
||||
}
|
||||
|
||||
if v := strings.TrimSpace(os.Getenv("DEVICE_ENDPOINT_HOST")); v != "" {
|
||||
cleaned := v
|
||||
// 1. Remove scheme if present (http:// or https://)
|
||||
if idx := strings.Index(cleaned, "://"); idx != -1 {
|
||||
cleaned = cleaned[idx+3:]
|
||||
}
|
||||
|
||||
// 2. Remove path/query/fragment if present
|
||||
// Keep only host[:port]
|
||||
if idx := strings.IndexAny(cleaned, "/?#"); idx != -1 {
|
||||
cleaned = cleaned[:idx]
|
||||
}
|
||||
|
||||
// 3. Final trim
|
||||
cleaned = strings.TrimSpace(cleaned)
|
||||
cfg.DeviceEndpointHost = cleaned
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,29 @@ COTURN_IMAGE=coturn/coturn:edge-alpine-arm64v8
|
||||
# https://github.com/gl-inet/glkvm-cloud/blob/main/docker-compose/nginx-reverse-proxy-example.conf
|
||||
REVERSE_PROXY_ENABLED=false
|
||||
|
||||
# =====================================================
|
||||
# Device Remote Access Domain (Reverse Proxy Mode Only)
|
||||
# =====================================================
|
||||
# This option is used to generate the Remote Control URL for devices when
|
||||
# running behind a reverse proxy.
|
||||
#
|
||||
# Effective ONLY when:
|
||||
# REVERSE_PROXY_ENABLED=true
|
||||
#
|
||||
# When set, GLKVM Cloud will generate device access addresses as:
|
||||
# https://<deviceId>.<DEVICE_ENDPOINT_HOST>/... (scheme is taken from X-Forwarded-Proto)
|
||||
#
|
||||
# Examples:
|
||||
# DEVICE_ENDPOINT_HOST=kvm.example.com
|
||||
# DEVICE_ENDPOINT_HOST=kvm.example.com:443
|
||||
#
|
||||
# Notes:
|
||||
# - Do NOT include scheme (http:// or https://)
|
||||
# - Do NOT include path (/xxx)
|
||||
#
|
||||
# Leave empty to derive the host/port from X-Forwarded-* headers (auto-detect).
|
||||
DEVICE_ENDPOINT_HOST=
|
||||
|
||||
# GLKVM access IP seen by devices/users.
|
||||
# Leave empty to auto-detect at container start.
|
||||
GLKVM_ACCESS_IP=
|
||||
|
||||
@@ -22,7 +22,30 @@ COTURN_IMAGE=coturn/coturn:edge-alpine
|
||||
# https://github.com/gl-inet/glkvm-cloud/blob/main/docker-compose/nginx-reverse-proxy-example.conf
|
||||
REVERSE_PROXY_ENABLED=false
|
||||
|
||||
# GLKVM access IP seen by devices/users.
|
||||
# =====================================================
|
||||
# Device Remote Access Domain (Reverse Proxy Mode Only)
|
||||
# =====================================================
|
||||
# This option is used to generate the Remote Control URL for devices when
|
||||
# running behind a reverse proxy.
|
||||
#
|
||||
# Effective ONLY when:
|
||||
# REVERSE_PROXY_ENABLED=true
|
||||
#
|
||||
# When set, GLKVM Cloud will generate device access addresses as:
|
||||
# https://<deviceId>.<DEVICE_ENDPOINT_HOST>/... (scheme is taken from X-Forwarded-Proto)
|
||||
#
|
||||
# Examples:
|
||||
# DEVICE_ENDPOINT_HOST=kvm.example.com
|
||||
# DEVICE_ENDPOINT_HOST=kvm.example.com:443
|
||||
#
|
||||
# Notes:
|
||||
# - Do NOT include scheme (http:// or https://)
|
||||
# - Do NOT include path (/xxx)
|
||||
#
|
||||
# Leave empty to derive the host/port from X-Forwarded-* headers (auto-detect).
|
||||
DEVICE_ENDPOINT_HOST=
|
||||
|
||||
GLKVM access IP seen by devices/users.
|
||||
# Leave empty to auto-detect at container start.
|
||||
GLKVM_ACCESS_IP=
|
||||
|
||||
|
||||
+34
-17
@@ -63,27 +63,24 @@ cd glkvm-cloud/docker-compose/
|
||||
- `OIDC_ALLOWED_USERNAMES`:允许的用户名列表(可选)
|
||||
- `OIDC_ALLOWED_GROUPS`:允许的用户组列表(可选)
|
||||
|
||||
#### **反向代理模式(可选)**
|
||||
#### 反向代理模式(可选)
|
||||
|
||||
```env
|
||||
# 启用反向代理模式(例如在 GLKVM Cloud 前使用 Nginx)
|
||||
# 启用后,TLS 由反向代理终止,GLKVM Cloud 内部使用明文 HTTP
|
||||
REVERSE_PROXY_ENABLED=false
|
||||
```
|
||||
|
||||
当 `REVERSE_PROXY_ENABLED` 设置为 `true` 时,GLKVM Cloud 将运行在 **反向代理(如 Nginx)之后**:
|
||||
启用后(`REVERSE_PROXY_ENABLED=true`):
|
||||
|
||||
- HTTPS 证书由反向代理管理(而不是由 GLKVM Cloud 本身管理)
|
||||
- GLKVM Cloud 内部以明文 HTTP 方式监听
|
||||
- 同一个 HTTPS 端口可同时用于:
|
||||
- 访问 GLKVM Cloud Web 管理界面
|
||||
- 访问远程 KVM 设备
|
||||
- GLKVM Cloud 运行在反向代理(如 Nginx)之后
|
||||
- TLS 由反向代理终止,GLKVM Cloud 内部使用 HTTP
|
||||
- Web UI 与设备远程访问可共用同一个 HTTPS 端口(通常为 443)
|
||||
|
||||
例如,在正确配置 Nginx 的情况下:
|
||||
|
||||
##### 必需的反向代理请求头
|
||||
|
||||
反向代理必须转发以下请求头,否则可能生成包含内部端口(如 `:10443`)的访问地址:
|
||||
|
||||
```nginx
|
||||
# 转发原始的主机名、协议、端口以及客户端 IP
|
||||
# 在反向代理模式下,这些 Header 是必须的
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
@@ -92,14 +89,34 @@ proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
```
|
||||
|
||||
你可以通过以下地址访问:
|
||||
##### 设备远程访问域名(可选)
|
||||
|
||||
```text
|
||||
https://www.example.com → GLKVM Cloud 管理界面
|
||||
https://<device_id>.example.com → 远程设备访问
|
||||
```env
|
||||
DEVICE_ENDPOINT_HOST=
|
||||
```
|
||||
|
||||
- **仅在** `REVERSE_PROXY_ENABLED=true` 时生效
|
||||
- 用于指定设备远程访问使用的域名
|
||||
- 生成的设备访问地址格式为:
|
||||
|
||||
```text
|
||||
https://<deviceId>.<DEVICE_ENDPOINT_HOST>/
|
||||
```
|
||||
|
||||
**说明:**
|
||||
|
||||
- 不需要包含 `http(s)://` 或路径
|
||||
- 可与 Web UI 域名不同
|
||||
- 留空时,将从 `X-Forwarded-*` 请求头自动推导
|
||||
|
||||
**示例:**
|
||||
|
||||
```text
|
||||
https://www.example.com → Web UI
|
||||
https://<deviceId>.kvm.example.com → 设备远程访问
|
||||
DEVICE_ENDPOINT_HOST=kvm.example.com
|
||||
```
|
||||
|
||||
这两个地址可以共用 **同一个 HTTPS 端口(443)**,由反向代理根据访问的域名进行路由区分。
|
||||
|
||||
⚠️ **注意:所有配置均需在 `.env` 中完成,不需要修改 `docker-compose.yml`、模板或脚本。**
|
||||
|
||||
|
||||
+60
-37
@@ -62,43 +62,66 @@
|
||||
- `OIDC_ALLOWED_USERNAMES`: comma-separated list of allowed usernames (`preferred_username` or `name`) (optional)
|
||||
- `OIDC_ALLOWED_GROUPS`: comma-separated list of allowed OIDC groups (optional)
|
||||
|
||||
**Reverse Proxy Mode (Optional)**
|
||||
|
||||
```env
|
||||
# Enable reverse proxy mode (e.g. Nginx in front of GLKVM Cloud).
|
||||
# When enabled, TLS is terminated by the reverse proxy and GLKVM Cloud runs in plain HTTP.
|
||||
REVERSE_PROXY_ENABLED=false
|
||||
```
|
||||
|
||||
When `REVERSE_PROXY_ENABLED` is set to `true`, GLKVM Cloud is designed to run **behind a reverse proxy** such as Nginx:
|
||||
|
||||
- HTTPS certificates are managed by the reverse proxy (not by GLKVM Cloud itself)
|
||||
- GLKVM Cloud listens on plain HTTP internally
|
||||
- The same HTTPS port can be used for both:
|
||||
- Accessing the GLKVM Cloud web UI
|
||||
- Accessing remote KVM devices
|
||||
|
||||
For example, with proper Nginx configuration,
|
||||
|
||||
```nginx
|
||||
# Forward original host, scheme, port and client IP
|
||||
# These headers are required when running GLKVM Cloud behind a reverse proxy.
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
```
|
||||
|
||||
you can use:
|
||||
|
||||
```text
|
||||
https://www.example.com → GLKVM Cloud web interface
|
||||
https://<device_id>.example.com → Remote device access
|
||||
```
|
||||
|
||||
Both addresses can share the **same HTTPS port (443)**, while routing is handled by the reverse proxy based on the domain name.
|
||||
|
||||
#### Reverse Proxy Mode (Optional)
|
||||
|
||||
```env
|
||||
REVERSE_PROXY_ENABLED=false
|
||||
```
|
||||
|
||||
When enabled (`REVERSE_PROXY_ENABLED=true`):
|
||||
|
||||
- GLKVM Cloud runs behind a reverse proxy (e.g. Nginx)
|
||||
- TLS is terminated at the reverse proxy; GLKVM Cloud uses plain HTTP internally
|
||||
- The Web UI and remote device access can share the same HTTPS port (usually 443)
|
||||
|
||||
|
||||
##### Required Reverse Proxy Headers
|
||||
|
||||
The reverse proxy **must** forward the following headers; otherwise, GLKVM Cloud may generate URLs containing internal ports (e.g. `:10443`):
|
||||
|
||||
```nginx
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
```
|
||||
|
||||
|
||||
##### Device Remote Access Domain (Optional)
|
||||
|
||||
```env
|
||||
DEVICE_ENDPOINT_HOST=
|
||||
```
|
||||
|
||||
- **Effective only when** `REVERSE_PROXY_ENABLED=true`
|
||||
- Used to specify the domain for device remote access
|
||||
- Device access URLs are generated as:
|
||||
|
||||
```text
|
||||
https://<deviceId>.<DEVICE_ENDPOINT_HOST>/
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
|
||||
- Do not include the scheme (`http://` or `https://`)
|
||||
- Do not include any path
|
||||
- The domain may differ from the Web UI domain
|
||||
- If left empty, the host/port will be derived from `X-Forwarded-*` headers
|
||||
|
||||
**Example:**
|
||||
|
||||
```text
|
||||
https://www.example.com → Web UI
|
||||
https://<deviceId>.kvm.example.com → Device remote access
|
||||
DEVICE_ENDPOINT_HOST=kvm.example.com
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
⚠️ **Note:** All configuration should be done in the `.env` file.
|
||||
You don’t need to modify `docker-compose.yml`, templates, or scripts directly.
|
||||
|
||||
@@ -53,6 +53,9 @@ services:
|
||||
|
||||
# ---- Reverse Proxy ----
|
||||
REVERSE_PROXY_ENABLED: ${REVERSE_PROXY_ENABLED:-false}
|
||||
|
||||
# ---- Device Endpoint Host ----
|
||||
DEVICE_ENDPOINT_HOST: ${DEVICE_ENDPOINT_HOST:-}
|
||||
volumes:
|
||||
- ./templates/rttys.conf.template:/tpl/rttys.conf.tmpl:ro
|
||||
- ./scripts/docker-entrypoint.sh:/docker-entrypoint.sh:ro
|
||||
|
||||
@@ -372,6 +372,12 @@ func httpProxyRedirect(srv *RttyServer, c *gin.Context, group string) {
|
||||
rawHost, xfHost, xfProto, xfPort, xRealIP, xFF,
|
||||
)
|
||||
|
||||
// -------------------------------------------------
|
||||
// Proxy mode:
|
||||
// 1) If DEVICE_ENDPOINT_HOST is configured, use it directly
|
||||
// 2) Otherwise, fallback to forwarded-header logic
|
||||
// -------------------------------------------------
|
||||
|
||||
// 0) scheme: follow reverse proxy
|
||||
scheme := ""
|
||||
if v := strings.TrimSpace(c.GetHeader("X-Forwarded-Proto")); v != "" {
|
||||
@@ -382,44 +388,50 @@ func httpProxyRedirect(srv *RttyServer, c *gin.Context, group string) {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
// 1) external port: prefer the one user actually accessed
|
||||
port := ""
|
||||
if fp := strings.TrimSpace(c.GetHeader("X-Forwarded-Port")); fp != "" {
|
||||
port = strings.TrimSpace(strings.Split(fp, ",")[0])
|
||||
} else if fh := strings.TrimSpace(c.GetHeader("X-Forwarded-Host")); fh != "" {
|
||||
fh = strings.TrimSpace(strings.Split(fh, ",")[0])
|
||||
if _, p, err := net.SplitHostPort(fh); err == nil && p != "" {
|
||||
// [A] Prefer explicit DEVICE_ENDPOINT_HOST if set
|
||||
if v := strings.TrimSpace(cfg.DeviceEndpointHost); v != "" {
|
||||
endpoint := v // already normalized when reading env: host[:port] only
|
||||
|
||||
baseHost := endpoint
|
||||
port := ""
|
||||
if h, p, err := net.SplitHostPort(endpoint); err == nil {
|
||||
baseHost = h
|
||||
port = p
|
||||
}
|
||||
}
|
||||
log.Info().Msgf("port: %s", port)
|
||||
|
||||
// 3) Build host: in proxy mode redirect domain to be redirHost
|
||||
hostPort := redirHost
|
||||
if port != "" {
|
||||
// avoid adding default ports
|
||||
if (scheme == "https" && port != "443") || (scheme == "http" && port != "80") {
|
||||
hostPort = net.JoinHostPort(redirHost, port)
|
||||
// Build device host: <deviceId>.<baseHost>
|
||||
// NOTE: DEVICE_ENDPOINT_HOST is a base domain (host[:port]) for device access,
|
||||
baseHost = strings.TrimSuffix(strings.TrimSpace(baseHost), ".")
|
||||
deviceHost := devid
|
||||
if baseHost != "" {
|
||||
deviceHost = devid + "." + baseHost
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Path: use the current request path
|
||||
redirectPath := c.Request.URL.Path
|
||||
if redirectPath == "" {
|
||||
redirectPath = "/"
|
||||
}
|
||||
hostPort := joinHostPortIfNeeded(deviceHost, scheme, port)
|
||||
|
||||
u := &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: hostPort,
|
||||
Path: redirectPath,
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("sid", sid)
|
||||
u.RawQuery = q.Encode()
|
||||
redirectPath := c.Request.URL.Path
|
||||
location = buildRedirectLocation(scheme, hostPort, redirectPath, sid)
|
||||
log.Info().Msgf("Using domain redirect (proxy mode, DEVICE_ENDPOINT_HOST): %s", location)
|
||||
} else {
|
||||
// 1) external port: prefer the one user actually accessed
|
||||
port := ""
|
||||
if fp := strings.TrimSpace(c.GetHeader("X-Forwarded-Port")); fp != "" {
|
||||
port = strings.TrimSpace(strings.Split(fp, ",")[0])
|
||||
} else if fh := strings.TrimSpace(c.GetHeader("X-Forwarded-Host")); fh != "" {
|
||||
fh = strings.TrimSpace(strings.Split(fh, ",")[0])
|
||||
if _, p, err := net.SplitHostPort(fh); err == nil && p != "" {
|
||||
port = p
|
||||
}
|
||||
}
|
||||
log.Info().Msgf("port: %s", port)
|
||||
|
||||
location = u.String()
|
||||
log.Info().Msgf("Using domain redirect (proxy mode): %s", location)
|
||||
// 3) Build host: in proxy mode redirect domain to be redirHost
|
||||
hostPort := joinHostPortIfNeeded(redirHost, scheme, port)
|
||||
|
||||
redirectPath := c.Request.URL.Path
|
||||
location = buildRedirectLocation(scheme, hostPort, redirectPath, sid)
|
||||
log.Info().Msgf("Using domain redirect (proxy mode): %s", location)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,3 +740,29 @@ func buildRedirectHost(hostname, devid string) string {
|
||||
return devid + "." + suffix
|
||||
}
|
||||
}
|
||||
|
||||
func joinHostPortIfNeeded(host, scheme, port string) string {
|
||||
if port == "" {
|
||||
return host
|
||||
}
|
||||
// avoid adding default ports
|
||||
if (scheme == "https" && port == "443") || (scheme == "http" && port == "80") {
|
||||
return host
|
||||
}
|
||||
return net.JoinHostPort(host, port)
|
||||
}
|
||||
|
||||
func buildRedirectLocation(scheme, hostPort, path, sid string) string {
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
u := &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: hostPort,
|
||||
Path: path,
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("sid", sid)
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String()
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import (
|
||||
)
|
||||
|
||||
const RttysVersion = "5.2.0"
|
||||
const KVMCloudVersion = "v1.7.0"
|
||||
|
||||
var (
|
||||
GitCommit = ""
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* @Author: LPY
|
||||
* @Date: 2025-06-09 09:29:48
|
||||
* @LastEditors: LPY
|
||||
* @LastEditTime: 2025-07-21 10:04:25
|
||||
* @FilePath: /kvm-cloud-frontend/src/components/base/baseWhitePage.vue
|
||||
* @LastEditTime: 2026-01-05 14:30:56
|
||||
* @FilePath: \glkvm-cloud\ui\src\components\base\baseWhitePage.vue
|
||||
* @Description: 基础白名单页。
|
||||
-->
|
||||
<template>
|
||||
@@ -13,6 +13,7 @@
|
||||
<img src="@/assets/svg/logo.svg" height="20">
|
||||
</div>
|
||||
<div class="base-white-page-header-right">
|
||||
<BaseText style="margin-right: 24px;">{{ appStore.state.version || '--' }}</BaseText>
|
||||
<BaseDropdownSelect :value="currentLang" :options="languageOptions" @update:value="changeLang">
|
||||
<div class="language-box flex">
|
||||
<BaseSvg name="gl-icon-language-regular" style="margin-right: 8px;font-size: 16px;"></BaseSvg>
|
||||
@@ -25,13 +26,13 @@
|
||||
|
||||
<div class="base-white-page-footer">
|
||||
<!-- 步骤条 -->
|
||||
<div v-if="route.query.bindToken" class="base-white-page-step">
|
||||
<!-- <div v-if="route.query.bindToken" class="base-white-page-step">
|
||||
<BaseStep
|
||||
v-model:value="useUserStore().bindingStep"
|
||||
:items="[{title: $t('login.accountSetup')}, {title: $t('login.deviceSetup')}]"
|
||||
titlePosition="bottom"
|
||||
/>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="base-white-page-content">
|
||||
<slot></slot>
|
||||
@@ -54,19 +55,17 @@
|
||||
<script setup lang="ts">
|
||||
import useLanguage from '@/hooks/useLanguage'
|
||||
import { languageOptions, Languages } from 'gl-web-main'
|
||||
import BaseStep from './baseStep.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
import { BaseDropdownSelect } from 'gl-web-main/components'
|
||||
import { isForeignEnv } from '@/utils'
|
||||
|
||||
const route = useRoute()
|
||||
import { useAppStore } from '@/stores/modules/app'
|
||||
|
||||
const { currentLang, currentLangLabel } = useLanguage()
|
||||
|
||||
const changeLang = (key: Languages) => {
|
||||
useLanguage().setLanguage(key)
|
||||
}
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -85,6 +84,8 @@ const changeLang = (key: Languages) => {
|
||||
border-bottom: 1px solid var(--gl-color-line-divider1);
|
||||
|
||||
.base-white-page-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.language-box {
|
||||
color: var(--gl-color-text-level2);
|
||||
user-select: none;
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* @Author: LPY
|
||||
* @Date: 2025-05-30 10:18:18
|
||||
* @LastEditors: LPY
|
||||
* @LastEditTime: 2025-06-25 10:03:27
|
||||
* @FilePath: /kvm-cloud-frontend/src/hooks/useLocalStorage.ts
|
||||
* @LastEditTime: 2026-01-05 14:22:12
|
||||
* @FilePath: \glkvm-cloud\ui\src\hooks\useLocalStorage.ts
|
||||
* @Description: 存储hook
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
@@ -18,6 +18,8 @@ export enum LocalStorageKeys {
|
||||
TWO_FACTOR_INFO_KEY = 'two-factor-info',
|
||||
/** 侧边栏手动控制展开收缩状态 */
|
||||
SIDEBAR_MANUAL_CONTROL_KEY = 'sidebar-manual-control',
|
||||
/** 版本号 */
|
||||
VERSION = 'version',
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* @Author: LPY
|
||||
* @Date: 2025-05-30 09:37:06
|
||||
* @LastEditors: LPY
|
||||
* @LastEditTime: 2025-06-13 15:55:51
|
||||
* @FilePath: /kvm-cloud-frontend/src/stores/modules/app.ts
|
||||
* @LastEditTime: 2026-01-05 14:35:02
|
||||
* @FilePath: \glkvm-cloud\ui\src\stores\modules\app.ts
|
||||
* @Description: app相关状态存储
|
||||
*/
|
||||
import { LocalStorageKeys, useLocalStorage } from '@/hooks/useLocalStorage'
|
||||
@@ -24,6 +24,8 @@ export const useAppStore = defineStore('appGlobal', () => {
|
||||
const state = reactive({
|
||||
/** 当前的主题模式 */
|
||||
themeMode: getThemeFromStorage(),
|
||||
/** 版本号 */
|
||||
version: (useLocalStorage(LocalStorageKeys.VERSION).getValue() as string)?.toUpperCase() || 'V1.0.0',
|
||||
})
|
||||
|
||||
/** 获取主题模式 */
|
||||
@@ -114,6 +116,11 @@ export const useAppStore = defineStore('appGlobal', () => {
|
||||
sidebar.manualSetting = false
|
||||
}
|
||||
|
||||
/** 设置版本号 */
|
||||
const setVersion = (version: string) =>{
|
||||
state.version = version.toUpperCase()
|
||||
useLocalStorage(LocalStorageKeys.VERSION).setValue(version)
|
||||
}
|
||||
|
||||
return { antdTheme, setThemeMode, state, sidebar, isCollapse, manualToggleSidebar, autoCloseSidebar, autoOpenSidebar, resetManualSetting }
|
||||
return { antdTheme, setThemeMode, state, sidebar, isCollapse, manualToggleSidebar, autoCloseSidebar, autoOpenSidebar, resetManualSetting, setVersion }
|
||||
})
|
||||
@@ -2,8 +2,8 @@
|
||||
* @Author: LPY
|
||||
* @Date: 2025-05-30 15:21:14
|
||||
* @LastEditors: LPY
|
||||
* @LastEditTime: 2025-08-26 17:57:09
|
||||
* @FilePath: \glkvm-cloud\web-ui\src\views\layout\layHeader\layHeader.vue
|
||||
* @LastEditTime: 2026-01-05 14:32:56
|
||||
* @FilePath: \glkvm-cloud\ui\src\views\layout\layHeader\layHeader.vue
|
||||
* @Description: 顶部集成页
|
||||
-->
|
||||
<template>
|
||||
@@ -12,6 +12,8 @@
|
||||
<img src="@/assets/svg/logo.svg" height="20">
|
||||
</div>
|
||||
<div class="lay-header-right">
|
||||
<!-- version -->
|
||||
<BaseText style="margin-right: 24px;">{{ appStore.state.version || '--' }}</BaseText>
|
||||
<!-- github -->
|
||||
<ATooltip>
|
||||
<template #title>{{ githubLink }}</template>
|
||||
@@ -38,9 +40,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/stores/modules/app'
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
// github链接
|
||||
const githubLink = 'https://github.com/gl-inet/glkvm-cloud'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: LPY
|
||||
* @Date: 2025-05-30 10:48:43
|
||||
* @LastEditors: LPY
|
||||
* @LastEditTime: 2025-11-12 17:01:59
|
||||
* @LastEditTime: 2026-01-05 14:25:45
|
||||
* @FilePath: \glkvm-cloud\ui\src\views\login\loginPage.vue
|
||||
* @Description: 登录页面
|
||||
-->
|
||||
@@ -103,6 +103,7 @@ import { useRouter } from 'vue-router'
|
||||
import { LoginParams, AuthConfig } from '@/models/user'
|
||||
import { message, Input, Form } from 'ant-design-vue'
|
||||
import { reqAuthConfig } from '@/api/user'
|
||||
import { useAppStore } from '@/stores/modules/app'
|
||||
|
||||
const AInput = Input
|
||||
const AForm = Form
|
||||
@@ -153,7 +154,7 @@ onMounted(async () => {
|
||||
// 提取配置数据 (Extract config data)
|
||||
const configData = response?.info || response?.data?.info || response?.data || response
|
||||
authConfig.value = configData
|
||||
|
||||
useAppStore().setVersion(configData.kvmCloudVersion)
|
||||
} catch (error) {
|
||||
console.error('Failed to load auth config:', error)
|
||||
// 回退 - 无LDAP可用 (Fallback - no LDAP available)
|
||||
|
||||
Reference in New Issue
Block a user