Use 比特骆驼 logo for PWA/brand marks, remove install button, docs Ubuntu 22.04 only.
Co-authored-by: Cursor <cursoragent@cursor.com>
@@ -8,6 +8,15 @@
|
|||||||
|
|
||||||
仓库:<https://git.bz121.com/dekun/eth_hedge_sim.git>
|
仓库:<https://git.bz121.com/dekun/eth_hedge_sim.git>
|
||||||
|
|
||||||
|
## 运行环境
|
||||||
|
|
||||||
|
| 项 | 约定 |
|
||||||
|
|----|------|
|
||||||
|
| 操作系统 | **Ubuntu 22.04** LTS |
|
||||||
|
| 进程管理 | PM2(`eth-hedge-api`) |
|
||||||
|
| 安装目录 | `/opt/eth_hedge_sim` |
|
||||||
|
| 端口 | **5155** |
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
|
|
||||||
- [开发方案](docs/开发方案.md)
|
- [开发方案](docs/开发方案.md)
|
||||||
@@ -21,11 +30,10 @@
|
|||||||
- 反代:[`https://dc.hyf2.cc`](https://dc.hyf2.cc) → 本机 `5155`(PM2: `eth-hedge-api`)
|
- 反代:[`https://dc.hyf2.cc`](https://dc.hyf2.cc) → 本机 `5155`(PM2: `eth-hedge-api`)
|
||||||
- 登录账号:见服务器 `/opt/eth_hedge_sim/.env` 的 `AUTH_USERNAME` / `AUTH_PASSWORD`
|
- 登录账号:见服务器 `/opt/eth_hedge_sim/.env` 的 `AUTH_USERNAME` / `AUTH_PASSWORD`
|
||||||
- 可在「系统设置」修改用户名/密码;前端固定同源 API,无 API 地址配置项
|
- 可在「系统设置」修改用户名/密码;前端固定同源 API,无 API 地址配置项
|
||||||
- 支持电脑 / 平板安装为独立应用(PWA)
|
|
||||||
|
|
||||||
## 一键部署 / 更新(禁止 scp 传代码)
|
## 一键部署 / 更新(禁止 scp 传代码)
|
||||||
|
|
||||||
服务器目录:`/opt/eth_hedge_sim`
|
目标机:**Ubuntu 22.04**,目录 `/opt/eth_hedge_sim`。
|
||||||
更新方式:**只允许 `git pull`**,然后构建并 `pm2 startOrReload` 本项目进程。
|
更新方式:**只允许 `git pull`**,然后构建并 `pm2 startOrReload` 本项目进程。
|
||||||
|
|
||||||
### 推荐:curl 一键管理器(菜单)
|
### 推荐:curl 一键管理器(菜单)
|
||||||
@@ -47,30 +55,33 @@ curl -fsSL https://git.bz121.com/dekun/eth_hedge_sim/raw/branch/main/deploy/mana
|
|||||||
bash /opt/eth_hedge_sim/deploy/manage.sh
|
bash /opt/eth_hedge_sim/deploy/manage.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### 本机触发远程更新
|
### 从开发机触发远程更新(可选)
|
||||||
|
|
||||||
|
在能 SSH 到 Ubuntu 服务器的机器上:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install paramiko
|
pip install paramiko
|
||||||
set DEPLOY_PASS=*** # Windows PowerShell: $env:DEPLOY_PASS='***'
|
export DEPLOY_PASS='***'
|
||||||
python scripts/deploy_remote.py
|
python scripts/deploy_remote.py
|
||||||
```
|
```
|
||||||
|
|
||||||
PM2 进程名:`eth-hedge-api`(端口 **5155**)。禁止 `pm2 restart all`。
|
PM2 进程名:`eth-hedge-api`(端口 **5155**)。禁止 `pm2 restart all`。
|
||||||
|
|
||||||
## 本地开发
|
## 服务器上本地开发 / 调试(Ubuntu 22.04)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m venv .venv
|
cd /opt/eth_hedge_sim
|
||||||
.venv\Scripts\activate
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
copy .env.example .env
|
cp .env.example .env
|
||||||
|
|
||||||
python scripts/smoke_market.py
|
python scripts/smoke_market.py
|
||||||
cd backend
|
cd backend
|
||||||
uvicorn app.main:app --host 0.0.0.0 --port 5155 --reload
|
uvicorn app.main:app --host 0.0.0.0 --port 5155 --reload
|
||||||
|
|
||||||
# 另开终端
|
# 另开终端
|
||||||
cd frontend
|
cd /opt/eth_hedge_sim/frontend
|
||||||
npm ci
|
npm ci
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
@@ -81,3 +92,4 @@ npm run dev
|
|||||||
- 永续市价;期权只吃买卖一;滑点 = 1×手续费
|
- 永续市价;期权只吃买卖一;滑点 = 1×手续费
|
||||||
- 仓位:永续 1 ETH,期权 2 ETH 名义
|
- 仓位:永续 1 ETH,期权 2 ETH 名义
|
||||||
- 部署更新只用 git pull,不用 scp
|
- 部署更新只用 git pull,不用 scp
|
||||||
|
- 部署环境固定为 Ubuntu 22.04
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# deploy/lib/common.sh — 比特骆驼自动化对冲系统 部署公共函数
|
# deploy/lib/common.sh — 比特骆驼自动化对冲系统 部署公共函数
|
||||||
|
# 目标系统: Ubuntu 22.04 LTS
|
||||||
set -e
|
set -e
|
||||||
set -u
|
set -u
|
||||||
if [ -n "${BASH_VERSION:-}" ]; then
|
if [ -n "${BASH_VERSION:-}" ]; then
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# 比特骆驼自动化对冲系统 部署管理器(工程 eth_hedge_sim)
|
# 比特骆驼自动化对冲系统 部署管理器(工程 eth_hedge_sim)
|
||||||
|
# 部署环境: Ubuntu 22.04 LTS
|
||||||
#
|
#
|
||||||
# 新服务器(免克隆):
|
# 新服务器(免克隆):
|
||||||
# curl -fsSL https://git.bz121.com/dekun/eth_hedge_sim/raw/branch/main/deploy/manage.sh | bash
|
# curl -fsSL https://git.bz121.com/dekun/eth_hedge_sim/raw/branch/main/deploy/manage.sh | bash
|
||||||
|
|||||||
@@ -191,6 +191,8 @@ frontend/src/components/
|
|||||||
|
|
||||||
## 4. 部署相关文件
|
## 4. 部署相关文件
|
||||||
|
|
||||||
|
目标环境:**Ubuntu 22.04** + PM2。目录默认 `/opt/eth_hedge_sim`。
|
||||||
|
|
||||||
### 4.1 `deploy/ecosystem.config.cjs`(示意)
|
### 4.1 `deploy/ecosystem.config.cjs`(示意)
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -252,7 +254,7 @@ module.exports = {
|
|||||||
|
|
||||||
| 允许 | 禁止 |
|
| 允许 | 禁止 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 复制 OKX 行情订阅、签名、深度解析到本仓库 | 修改 `C:\Users\dekun\Desktop\crypto_monitor` 下任何文件 |
|
| 复制 OKX 行情订阅、签名、深度解析到本仓库 | 修改现网 `crypto_monitor` 仓库内任何文件 |
|
||||||
| 复制「买一流动性检查」思路后重写 | 把本项目塞进现网 monorepo 一起 PM2 |
|
| 复制「买一流动性检查」思路后重写 | 把本项目塞进现网 monorepo 一起 PM2 |
|
||||||
| 新建本仓库的 `.env` | 默认使用现网交易 Key 做模拟(模拟不需要交易权限) |
|
| 新建本仓库的 `.env` | 默认使用现网交易 Key 做模拟(模拟不需要交易权限) |
|
||||||
|
|
||||||
|
|||||||
@@ -43,13 +43,13 @@
|
|||||||
| 阶段 | 形态 | 说明 |
|
| 阶段 | 形态 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 现在(内部) | Git + `deploy/manage.sh` + PM2 | 已具备,继续打磨成「可复制安装」 |
|
| 现在(内部) | Git + `deploy/manage.sh` + PM2 | 已具备,继续打磨成「可复制安装」 |
|
||||||
| 商用 v1 | **一键部署脚本** + 环境检查 + 健康检查 | 客户 Ubuntu 机器上跑通 |
|
| 商用 v1 | **一键部署脚本** + 环境检查 + 健康检查 | 客户 **Ubuntu 22.04** 机器上跑通 |
|
||||||
| 商用 v2 | **Docker Compose**(可选) | 降低环境差异;与脚本二选一或并存 |
|
| 商用 v2 | **Docker Compose**(可选) | 降低环境差异;与脚本二选一或并存 |
|
||||||
| 商用 v3 | 离线包 / 镜像导出 | 内网客户、无法拉 Git 的场景 |
|
| 商用 v3 | 离线包 / 镜像导出 | 内网客户、无法拉 Git 的场景 |
|
||||||
|
|
||||||
### 2.2 客户侧必备条件
|
### 2.2 客户侧必备条件
|
||||||
|
|
||||||
- 一台独立 Linux 主机(建议独立 IP)
|
- 一台独立 **Ubuntu 22.04** 主机(建议独立 IP)
|
||||||
- 出网访问目标交易所 API(OKX 等)
|
- 出网访问目标交易所 API(OKX 等)
|
||||||
- 域名或 IP + HTTPS(可用反代)
|
- 域名或 IP + HTTPS(可用反代)
|
||||||
- 客户自备交易所 API Key(只读行情 / 实盘交易按档位要求)
|
- 客户自备交易所 API Key(只读行情 / 实盘交易按档位要求)
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
|----|------|
|
|----|------|
|
||||||
| 产品名 | **比特骆驼自动化对冲系统** |
|
| 产品名 | **比特骆驼自动化对冲系统** |
|
||||||
| 工程名 / 目录名 | `eth_hedge_sim` |
|
| 工程名 / 目录名 | `eth_hedge_sim` |
|
||||||
| 本机路径 | `C:\Users\dekun\Desktop\新建文件夹\eth_hedge_sim` |
|
| 部署操作系统 | **Ubuntu 22.04** LTS |
|
||||||
| 云服务器 | **单独一台 Ubuntu**(或同机不同目录/不同 PM2 进程名,且与现网隔离) |
|
| 云服务器 | **单独一台 Ubuntu 22.04**(或同机不同目录/不同 PM2 进程名,且与现网隔离) |
|
||||||
| 部署 | PM2 |
|
| 部署 | PM2 |
|
||||||
| 行情 | OKX **实盘只读** API(REST + WebSocket) |
|
| 行情 | OKX **实盘只读** API(REST + WebSocket) |
|
||||||
| 成交(默认) | **本地模拟撮合 + 本地虚拟资金**(非 OKX 模拟盘) |
|
| 成交(默认) | **本地模拟撮合 + 本地虚拟资金**(非 OKX 模拟盘) |
|
||||||
@@ -150,7 +150,7 @@ OKX 实盘只读行情 (WS/REST)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. 部署(Ubuntu + PM2)
|
## 6. 部署(Ubuntu 22.04 + PM2)
|
||||||
|
|
||||||
### 6.1 建议目录
|
### 6.1 建议目录
|
||||||
|
|
||||||
@@ -221,4 +221,4 @@ TZ=Asia/Shanghai
|
|||||||
|
|
||||||
## 9. 一句话
|
## 9. 一句话
|
||||||
|
|
||||||
**独立仓 `eth_hedge_sim`(产品:比特骆驼自动化对冲系统):OKX/币安真行情只读 + 本地虚拟资金撮合(永续市价、期权只买吃买卖一、滑点=1×手续费)+ 四页前端 + Ubuntu/PM2 单独部署;可抄现网思路,但不改现网代码、不共用现网进程。**
|
**独立仓 `eth_hedge_sim`(产品:比特骆驼自动化对冲系统):OKX/币安真行情只读 + 本地虚拟资金撮合(永续市价、期权只买吃买卖一、滑点=1×手续费)+ 四页前端 + Ubuntu 22.04 / PM2 单独部署;可抄现网思路,但不改现网代码、不共用现网进程。**
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 210 B After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 571 KiB |
@@ -1,7 +1,6 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { NavLink, Navigate, Route, Routes } from "react-router-dom";
|
import { NavLink, Navigate, Route, Routes } from "react-router-dom";
|
||||||
import { clearSession, getToken, getUsername } from "./api/client";
|
import { clearSession, getToken, getUsername } from "./api/client";
|
||||||
import InstallBadge from "./components/InstallBadge";
|
|
||||||
import LoginPage from "./pages/Login";
|
import LoginPage from "./pages/Login";
|
||||||
import PlanPage from "./pages/Plan";
|
import PlanPage from "./pages/Plan";
|
||||||
import TradesPage from "./pages/Trades";
|
import TradesPage from "./pages/Trades";
|
||||||
@@ -14,8 +13,16 @@ function Shell({ children }: { children: ReactNode }) {
|
|||||||
<div className="app-shell">
|
<div className="app-shell">
|
||||||
<header className="topnav">
|
<header className="topnav">
|
||||||
<div className="topnav-side left">
|
<div className="topnav-side left">
|
||||||
<div className="brand">比特骆驼自动化对冲系统</div>
|
<div className="brand">
|
||||||
<InstallBadge />
|
<img
|
||||||
|
className="brand-mark"
|
||||||
|
src="/icons/brand.png"
|
||||||
|
alt=""
|
||||||
|
width={28}
|
||||||
|
height={28}
|
||||||
|
/>
|
||||||
|
<span>比特骆驼自动化对冲系统</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav className="topnav-center">
|
<nav className="topnav-center">
|
||||||
<NavLink to="/plan" className={({ isActive }) => (isActive ? "active" : "")}>
|
<NavLink to="/plan" className={({ isActive }) => (isActive ? "active" : "")}>
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import {
|
|
||||||
BeforeInstallPromptEvent,
|
|
||||||
isAppleTouchDevice,
|
|
||||||
isStandaloneDisplay,
|
|
||||||
} from "../pwa/install";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Desktop/tablet install affordance:
|
|
||||||
* - Chrome/Edge/Android: native install via beforeinstallprompt
|
|
||||||
* - iPad/iOS: tip to Add to Home Screen
|
|
||||||
* - Already installed: show 已安装标识
|
|
||||||
*/
|
|
||||||
export default function InstallBadge() {
|
|
||||||
const [standalone, setStandalone] = useState(isStandaloneDisplay);
|
|
||||||
const [deferred, setDeferred] = useState<BeforeInstallPromptEvent | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
const [tipOpen, setTipOpen] = useState(false);
|
|
||||||
const [busy, setBusy] = useState(false);
|
|
||||||
const apple = isAppleTouchDevice();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const mq = window.matchMedia("(display-mode: standalone)");
|
|
||||||
const sync = () => setStandalone(isStandaloneDisplay());
|
|
||||||
mq.addEventListener?.("change", sync);
|
|
||||||
sync();
|
|
||||||
|
|
||||||
const onBip = (e: Event) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setDeferred(e as BeforeInstallPromptEvent);
|
|
||||||
};
|
|
||||||
const onInstalled = () => {
|
|
||||||
setDeferred(null);
|
|
||||||
setStandalone(true);
|
|
||||||
setTipOpen(false);
|
|
||||||
};
|
|
||||||
window.addEventListener("beforeinstallprompt", onBip);
|
|
||||||
window.addEventListener("appinstalled", onInstalled);
|
|
||||||
return () => {
|
|
||||||
mq.removeEventListener?.("change", sync);
|
|
||||||
window.removeEventListener("beforeinstallprompt", onBip);
|
|
||||||
window.removeEventListener("appinstalled", onInstalled);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (standalone) {
|
|
||||||
return (
|
|
||||||
<span className="install-badge installed" title="已安装为独立应用">
|
|
||||||
已安装
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const canNativeInstall = Boolean(deferred);
|
|
||||||
|
|
||||||
async function onInstall() {
|
|
||||||
if (!deferred) {
|
|
||||||
setTipOpen((v) => !v);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setBusy(true);
|
|
||||||
try {
|
|
||||||
await deferred.prompt();
|
|
||||||
await deferred.userChoice;
|
|
||||||
setDeferred(null);
|
|
||||||
} finally {
|
|
||||||
setBusy(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="install-wrap">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn ghost install-btn"
|
|
||||||
disabled={busy}
|
|
||||||
onClick={() => void onInstall()}
|
|
||||||
title={
|
|
||||||
canNativeInstall
|
|
||||||
? "安装到电脑或平板"
|
|
||||||
: apple
|
|
||||||
? "添加到主屏幕(平板/手机)"
|
|
||||||
: "安装说明"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{canNativeInstall ? (busy ? "安装中…" : "安装应用") : "安装应用"}
|
|
||||||
</button>
|
|
||||||
{tipOpen && !canNativeInstall ? (
|
|
||||||
<div className="install-tip" role="dialog" aria-label="安装说明">
|
|
||||||
{apple ? (
|
|
||||||
<>
|
|
||||||
<p>iPad / iPhone:用 Safari 打开本站,点分享 →「添加到主屏幕」。</p>
|
|
||||||
<p className="muted">安装后从主屏幕打开即为独立应用窗口。</p>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<p>
|
|
||||||
电脑:Chrome / Edge 地址栏右侧「安装」图标,或菜单 → 安装应用。
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
安卓平板:浏览器菜单 →「安装应用」/「添加到主屏幕」。
|
|
||||||
</p>
|
|
||||||
<p className="muted">需 HTTPS(或本机 localhost)且已加载完成。</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn ghost"
|
|
||||||
onClick={() => setTipOpen(false)}
|
|
||||||
>
|
|
||||||
关闭
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { FormEvent, useState } from "react";
|
import { FormEvent, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { login, setSession } from "../api/client";
|
import { login, setSession } from "../api/client";
|
||||||
import InstallBadge from "../components/InstallBadge";
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const nav = useNavigate();
|
const nav = useNavigate();
|
||||||
@@ -29,8 +28,14 @@ export default function LoginPage() {
|
|||||||
<div className="login-wrap">
|
<div className="login-wrap">
|
||||||
<form className="login-box" onSubmit={onSubmit}>
|
<form className="login-box" onSubmit={onSubmit}>
|
||||||
<div className="login-title-row">
|
<div className="login-title-row">
|
||||||
|
<img
|
||||||
|
className="login-brand-mark"
|
||||||
|
src="/icons/brand.png"
|
||||||
|
alt="比特骆驼"
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
/>
|
||||||
<h1>比特骆驼自动化对冲系统</h1>
|
<h1>比特骆驼自动化对冲系统</h1>
|
||||||
<InstallBadge />
|
|
||||||
</div>
|
</div>
|
||||||
{err ? <div className="err">{err}</div> : null}
|
{err ? <div className="err">{err}</div> : null}
|
||||||
<div className="field">
|
<div className="field">
|
||||||
|
|||||||
@@ -81,70 +81,35 @@ input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.install-wrap {
|
.brand-mark,
|
||||||
position: relative;
|
.login-brand-mark {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.install-btn {
|
.login-brand-mark {
|
||||||
font-size: 12px;
|
border-radius: 10px;
|
||||||
padding: 4px 10px;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.install-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
padding: 3px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.install-badge.installed {
|
|
||||||
color: #0b0e11;
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.install-tip {
|
|
||||||
position: absolute;
|
|
||||||
top: calc(100% + 8px);
|
|
||||||
left: 0;
|
|
||||||
z-index: 30;
|
|
||||||
width: min(320px, calc(100vw - 24px));
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--bg-elev);
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
|
||||||
}
|
|
||||||
|
|
||||||
.install-tip p {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.install-tip p.muted,
|
|
||||||
.install-tip .muted {
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.topnav-side.left .brand {
|
.topnav-side.left .brand span {
|
||||||
max-width: 11em;
|
max-width: 9em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,9 +371,8 @@ input {
|
|||||||
|
|
||||||
.login-title-row {
|
.login-title-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: 12px;
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||