mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-09-19 16:38:31 +00:00
feat(web): 窄屏适配 — 侧栏转底部 tab bar + 各页响应式重排
此前只有一个 max-width:900px 断点(仅缩窄侧栏),手机上 210px 侧栏吃掉 半屏、卡片网格横向溢出、按钮被挤成细条。新增 720px / 380px 两档断点: App.vue(关键) - ≤720px:shell 转竖向,导航从侧栏抽出固定到屏幕底部(图标上/文字下), 品牌+身份+主题折进顶部窄条;tab 最小 44px 触控高度 - 内容区预留 env(safe-area-inset-bottom),避开 iOS 手势条 - ≤380px 再压一档:隐藏权限徽标、缩小字号 StatusView - card-grid 的 minmax(320px) 在 375px 屏溢出 → 单列 - KPI 由 auto-fit 改固定两列(auto-fit 窄屏退化单列使四卡拉长),≤380px 单列 - 转发卡 fwd-head 原一行塞 7~8 元素 → 路由占整行、按钮另起一行平分 - 分组标题按钮不再被 margin-left:auto 压扁 ClusterView - 环拓扑改竖向,节点卡占满宽,箭头旋转 90° 指下 - nodeKey(32 位 hex)允许 break-all,不再撑宽容器 - 日志 lg-detail 由 max-width:60%+nowrap 改整行完整显示 SettingsView / UsersView / CanvasView - binary-row、setting-row 竖排;表格卡标题行竖排、操作按钮平分 - 画布 toolbar 按钮换行平铺;vue-flow handle 12px→16px(触控可达) index.html - viewport 加 viewport-fit=cover,这是 env(safe-area-inset-*) 生效前提 验证:tsc --noEmit 通过、npm run build 通过、dist 已重新 embed, 三节点部署后环正常(pending=0,5 条转发 frpc 进程与 topology 归属一致)。
This commit is contained in:
@ -2,7 +2,7 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<title>webui4frpc</title>
|
||||
</head>
|
||||
|
||||
115
web/src/App.vue
115
web/src/App.vue
@ -363,4 +363,119 @@ const currentLabel = computed(() => nav.value.find((n) => n.key === view.value)?
|
||||
.breadcrumb { padding: 12px 16px 6px; }
|
||||
.content { padding: 8px 14px 32px; }
|
||||
}
|
||||
|
||||
/* ---------- narrow screens: sidebar becomes a bottom tab bar ----------
|
||||
窄屏(≤ 720px)下 210px 侧栅会吃掉手机一半宽度,改为:
|
||||
- shell 竖向排列,侧栅沉到底部变成固定 tab bar(图标在上文字在下)
|
||||
- 品牌区/身份区/主题切换折叠进顶部窄条,不占垂直空间
|
||||
- 内容区预留底部安全区 + tab bar 高度,避开 iOS 手势条
|
||||
- 每个 tab 至少 44px 高(触控目标下限) */
|
||||
@media (max-width: 720px) {
|
||||
.app-shell {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部窄条:logo + 身份 + 主题,横向排一行 */
|
||||
.sidebar {
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
order: -1;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
overflow: visible;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--w4f-line);
|
||||
padding-top: max(10px, env(safe-area-inset-top));
|
||||
}
|
||||
.sb-brand {
|
||||
padding: 0;
|
||||
gap: 8px;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.sb-logo { width: 30px; height: 30px; border-radius: 10px; }
|
||||
.sb-brand-text b { font-size: 14.5px; }
|
||||
.sb-brand-text small { display: none; }
|
||||
|
||||
/* 导航从侧栅里抽出来,固定到底部 */
|
||||
.sb-nav {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 40;
|
||||
flex: none;
|
||||
flex-direction: row;
|
||||
gap: 0;
|
||||
padding: 4px 4px max(4px, env(safe-area-inset-bottom));
|
||||
background: var(--w4f-card);
|
||||
border-top: 1px solid var(--w4f-line);
|
||||
backdrop-filter: blur(calc(var(--w4f-glass) * 0.9)) saturate(1.3);
|
||||
-webkit-backdrop-filter: blur(calc(var(--w4f-glass) * 0.9)) saturate(1.3);
|
||||
box-shadow: 0 -4px 20px rgb(0 0 0 / 6%);
|
||||
}
|
||||
.sb-i {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: 44px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
padding: 5px 2px;
|
||||
border-radius: 11px;
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
.sb-i .label {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sb-i.active {
|
||||
/* 底部 tab 不用渐变底,只高亮图标与文字(避免拥挤) */
|
||||
background: var(--w4f-primary-50);
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 身份与主题折进顶条右侧 */
|
||||
.sb-foot {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.sb-identity {
|
||||
margin-bottom: 0;
|
||||
padding: 5px 8px;
|
||||
font-size: 11.5px;
|
||||
max-width: 42vw;
|
||||
}
|
||||
.sb-identity .id-name { max-width: 14vw; }
|
||||
.sb-theme { margin-bottom: 0; }
|
||||
.sb-theme .sw { width: 22px; height: 22px; border-radius: 7px; }
|
||||
|
||||
.main { height: auto; flex: 1; min-height: 0; }
|
||||
.breadcrumb { padding: 10px 14px 4px; font-size: 14px; }
|
||||
.content {
|
||||
padding: 6px 12px 24px;
|
||||
/* 底部 tab bar(44px + padding)+ 安全区 */
|
||||
padding-bottom: calc(64px + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
|
||||
/* 极窄(≤ 380px,iPhone SE / 小屏安卓):进一步压缩顶条 */
|
||||
@media (max-width: 380px) {
|
||||
.sb-brand-text b { font-size: 13px; }
|
||||
.sb-identity { max-width: 38vw; padding: 4px 7px; }
|
||||
.sb-identity .id-level { display: none; }
|
||||
.sb-i { font-size: 9.5px; }
|
||||
.content { padding-left: 10px; padding-right: 10px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1053,4 +1053,47 @@ onBeforeUnmount(() => {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- narrow screens ----------
|
||||
画布本质上需要宽屏拖拽,窄屏下只做到“可用”:
|
||||
- toolbar 七个按钮在 375px 下会溢出,改成标题占行 + 按钮换行平铺
|
||||
- 画布高度给个下限,否则在短屏上只剩一条缝
|
||||
- handle 抬到 16px(手指命中 12px 圆点几乎不可能) */
|
||||
@media (max-width: 720px) {
|
||||
.canvas-editor { gap: 10px; }
|
||||
|
||||
.toolbar {
|
||||
padding: 11px 12px;
|
||||
row-gap: 8px;
|
||||
}
|
||||
.toolbar-left,
|
||||
.toolbar-right {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.toolbar-title {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
font-size: 15.5px;
|
||||
}
|
||||
.toolbar .btn {
|
||||
flex: 1 1 auto;
|
||||
min-height: 38px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
.save-hint { width: 100%; }
|
||||
|
||||
.flow-wrap { min-height: 62vh; }
|
||||
|
||||
/* 触控:连接点与边标签放大,手指才点得到 */
|
||||
:deep(.vue-flow__handle) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
:deep(.vue-flow__edge-label) {
|
||||
padding: 3px 9px;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -560,4 +560,78 @@ onBeforeUnmount(() => {
|
||||
/* dialog form */
|
||||
.form-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; label { width: 72px; font-size: 13px; color: $color-text-secondary; flex-shrink: 0; } }
|
||||
.form-hint { font-size: 12px; color: $color-text-muted; }
|
||||
|
||||
/* ---------- narrow screens ----------
|
||||
窄屏下的主要问题:
|
||||
- ring-node 的 min-width:168px 与环形箭头在 375px 下会横向溢出,且节点横排无意义
|
||||
- nodeKey 是 32 位 hex,单行放不下会撑宽容器
|
||||
- log-row 的 lg-detail max-width:60% + 不换行,窄屏几乎全被省略号吃掉
|
||||
- toolbar / 对话框表单同 StatusView 的问题 */
|
||||
@media (max-width: 720px) {
|
||||
.cluster-page { gap: 10px; }
|
||||
.section { padding: 14px 14px; }
|
||||
|
||||
.hero { padding: 14px 15px; gap: 12px; }
|
||||
.page-title { font-size: 16.5px; }
|
||||
|
||||
/* nodeKey 允许折行,不再撑宽容器 */
|
||||
.nk-value {
|
||||
font-size: 11px;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
max-width: 100%;
|
||||
}
|
||||
.nk-label { white-space: normal; }
|
||||
|
||||
.kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
||||
|
||||
/* 工具栏按钮平分整行 */
|
||||
.toolbar { padding: 11px 13px; gap: 8px; }
|
||||
.toolbar > .w4f-btn { flex: 1 1 auto; min-height: 40px; }
|
||||
.act-hint { width: 100%; }
|
||||
|
||||
/* 环拓扑改竖向:节点卡占整宽,箭头旋转 90° 指下 */
|
||||
.ring-line { flex-direction: column; align-items: stretch; gap: 6px; }
|
||||
.ring-node { min-width: 0; width: 100%; padding: 11px 12px; }
|
||||
.rn-id { font-size: 12.5px; word-break: break-all; }
|
||||
.ring-arrow {
|
||||
align-self: center;
|
||||
transform: rotate(90deg);
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 日志:详情另起一行完整显示,不再被省略号截掉 */
|
||||
.log-row { row-gap: 3px; font-size: 11.5px; }
|
||||
.lg-detail {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
.log-list { max-height: 260px; }
|
||||
|
||||
/* 任务/拓扑行:允许折行,归属另起一行 */
|
||||
.task-row, .topo-row { flex-wrap: wrap; row-gap: 4px; font-size: 12.5px; }
|
||||
.tp-owner { margin-left: 0; width: 100%; }
|
||||
|
||||
.title-actions { margin-left: 0; width: 100%; }
|
||||
.title-actions > .w4f-btn { flex: 1 1 0; min-height: 36px; }
|
||||
.section-title { flex-wrap: wrap; row-gap: 8px; }
|
||||
|
||||
/* 对话框表单:标签在上、输入在下 */
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 5px;
|
||||
label { width: auto; }
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
.kpis { grid-template-columns: minmax(0, 1fr); }
|
||||
.page-title { font-size: 15.5px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -321,4 +321,21 @@ onMounted(load)
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
/* ---------- narrow screens ----------
|
||||
binary-row 把输入框与安装按钮横排,窄屏下路径输入框被挤到只剩几十像素;
|
||||
setting-row 的 space-between 在窄屏也会把说明文字与开关挤在一起。 */
|
||||
@media (max-width: 720px) {
|
||||
.binary-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.binary-row .btn { min-height: 40px; justify-content: center; }
|
||||
|
||||
.setting-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -638,4 +638,91 @@ onBeforeUnmount(() => {
|
||||
.form-row { display: flex; align-items: center; gap: 10px; margin-bottom: 12px;
|
||||
label { width: 72px; font-size: 13px; color: $color-text-secondary; flex-shrink: 0; }
|
||||
}
|
||||
|
||||
/* ---------- narrow screens ----------
|
||||
窄屏下的主要问题:
|
||||
- card-grid 的 minmax(320px) 在 375px 屏上会溢出(加上内边距超宽)
|
||||
- topbar 的 space-between 把标题和按钮挤在一行
|
||||
- fwd-head 一行塞下 7~8 个元素(类型/点/路由/协议/分组/归属/按钮)
|
||||
- 分组标题的三个按钮 margin-left:auto 后被挤成细条
|
||||
- 触控目标(small 按钮)低于 44px */
|
||||
@media (max-width: 720px) {
|
||||
.status-page { gap: 10px; }
|
||||
|
||||
/* 标题与工具栏换行,按钮占整行平分 */
|
||||
.topbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.tb-right { width: 100%; }
|
||||
.tb-right > .w4f-btn { flex: 1 1 0; min-height: 40px; }
|
||||
|
||||
/* KPI 固定两列(auto-fit 在窄屏会变单列,四张卡拉得太长) */
|
||||
.kpis {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 卡片网格改单列:消除 320px 最小宽导致的横向溢出 */
|
||||
.card-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 分组标题:标题一行,操作按钮另起一行平分 */
|
||||
.section-title {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 8px;
|
||||
}
|
||||
.grp-actions {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
gap: 6px;
|
||||
}
|
||||
.grp-actions > .w4f-btn { flex: 1 1 0; min-height: 38px; font-size: 12px; }
|
||||
|
||||
/* 转发卡:路由占整行,其余属性换行,按钮另起一行 */
|
||||
.fwd-card { padding: 11px 12px; }
|
||||
.fwd-head { row-gap: 7px; }
|
||||
.fwd-route {
|
||||
order: -1;
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 2px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.fwd-owner { max-width: 100%; }
|
||||
.fwd-actions {
|
||||
width: 100%;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.fwd-actions > .w4f-btn { flex: 1 1 0; min-height: 38px; }
|
||||
|
||||
/* 远程节点卡:名称占行,操作按钮平分 */
|
||||
.node-card { padding: 14px 15px; }
|
||||
.nc-name { width: 100%; margin-right: 0; }
|
||||
.nc-actions { width: 100%; }
|
||||
.nc-actions > .w4f-btn { flex: 1 1 0; min-height: 38px; }
|
||||
.nc-cluster-note { max-width: 100%; }
|
||||
.meta-pill { max-width: 100%; }
|
||||
.np-err { max-width: 45%; }
|
||||
|
||||
/* 对话框表单:标签在上、输入在下(窄屏下 72px 标签挤死输入框) */
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 5px;
|
||||
label { width: auto; }
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
/* 极窄:KPI 也降为单列,否则数字会被压断 */
|
||||
.kpis { grid-template-columns: minmax(0, 1fr); }
|
||||
.fwd-route { font-size: 12.5px; }
|
||||
.page-title { font-size: 16px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -362,4 +362,37 @@ const onDeleteKey = async (k: ApiKey) => {
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ---------- narrow screens ----------
|
||||
两张 el-table 列实宽加起来 ~900px,窄屏必然需要横向滚动;
|
||||
Element Plus 自己会处理表内滚动,但卡片内边距与标题行需要压缩,
|
||||
否则“导出 CSV / 新建”两个按钮会把标题挤出容器。 */
|
||||
@media (max-width: 720px) {
|
||||
.users-view { gap: 14px; }
|
||||
.card { padding: 13px 13px; border-radius: 14px; }
|
||||
|
||||
.card-h {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 9px;
|
||||
}
|
||||
.card-h .h-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
/* Element 按钮在窄屏平分整行,并抬到 40px 触控高度 */
|
||||
.card-h .h-actions :deep(.el-button) {
|
||||
flex: 1 1 0;
|
||||
min-height: 40px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* 表格横向滚动时给个视觉提示,并保证不把卡片撑宽 */
|
||||
:deep(.el-table) {
|
||||
font-size: 12.5px;
|
||||
}
|
||||
:deep(.el-table__body-wrapper) {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user