feat(web): 隐藏原生设置、圆形视图切换、工作空间改列表、三视图标题统一

用户提出的四条,都基于实测数据而不是猜测:

1) 隐藏 Dashy 原生设置入口
   顶栏的 .config-launcher(齿轮+铅笔+调色板)与我们的 /admin/ 重复,
   连同 .options-trigger/.settings-outer 一起隐藏。

2) 视图切换图标改成圆形,紧贴自定义入口左侧
   原生切换按钮是侧栏底部的方形图标(.switch-view-buttons),已隐藏;
   改为右下角圆形悬浮按钮 .navi-view-switch,点开是「首页/极简/工作空间」
   菜单并标出当前视图。
   顺带修掉 2px 错位:管理入口是内容盒(48+2px 边框=50),视图按钮是
   边框盒(48),并排时差 2px;给管理入口补 box-sizing: border-box。

3) 工作空间「点了没反应」
   读 Dashy 自己的 Workspace 组件(assets/Workspace-*.js)得到结论:
   右侧面板就是一个 iframe,launchApp() 只有 target 不是 newtab 才把 URL
   灌进面板;我们全局 openingMethod: newtab,所以每次点击都走 window.open
   分支,this.url 恒为空 → 面板永远空白。
   就算灌进去也没用:Gitea 实测 X-Frame-Options: sameorigin 拒绝被嵌
   (Refused to display … in a frame)。
   所以改成:点左侧分组 → 右侧列出该分组应用卡片(数据取自
   /admin/api/sections),不依赖目标站点是否允许 iframe,首页仍保持新标签页。

4) 三视图标题割裂
   实测 /minimal 是 48px 居中 -1.92px,而 / 与 /workspace 是顶栏里 40px
   左对齐、无字距。现在三个视图共用同一条规则(clamp(1.75rem,4vw,2.75rem)、
   700、-0.035em、居中;移动端 clamp(1.6rem,8vw,2.2rem)),副标题也居中置灰。

另外:工作空间面板原本用 position: fixed 钉在视口中间(1364×654,上距顶栏
7px、下距搜索栏 8px),把页面切成「顶栏/大方块/底部」三段;改为正常文档流,
内容多时页面自然滚动。
This commit is contained in:
HomeDash Bot
2026-09-11 18:17:23 +08:00
parent c04d00d4de
commit da196abcb5
2 changed files with 605 additions and 51 deletions

View File

@ -6,21 +6,44 @@ html {
text-size-adjust: 100%;
}
/* 禁用移动端/Chromium 对可点击元素注入的蓝色点击闪烁。 */
html[data-theme] *,
html[data-theme] *::before,
html[data-theme] *::after {
-webkit-tap-highlight-color: transparent !important;
}
html[data-theme] body {
/* border-box 让 padding 计入 min-height:否则 min-height:100vh 与
padding-bottom 相加,短页面(极简/工作空间)必然多出一屏滚动。 */
box-sizing: border-box !important;
min-width: 0;
overflow-x: hidden;
/* 悬浮搜索栏固定在底部,给内容留出空间,避免遮挡页脚备案号。 */
padding-bottom: 84px !important;
/* 列向 flex + 页脚 margin-top:auto,把备案号顶到页面底部而非浮在中间。 */
display: flex !important;
flex-direction: column !important;
min-height: 100vh !important;
/* 备案号占据真正的页面最底部;悬浮控件在它上方,不再用底部 padding
把页脚整体抬高。 */
padding-bottom: 0 !important;
}
/* Dashy 的 #dashy 本身撑满 100vh;不扣掉留给悬浮搜索栏的高度,
短页面(极简视图/工作空间)就会凭空多出约 134px 的纵向滚动。 */
html[data-theme] #app {
width: 100% !important;
}
/* Dashy 的 #dashy 默认 min-height:100vh,短页面叠加页脚后必出滚动;
交回内容自然撑高,视口高度由 body 兜住。 */
html[data-theme] #dashy {
min-height: calc(100vh - 84px) !important;
min-height: auto !important;
}
/* Dashy's native filter/settings strip duplicates the portal search and breaks on phones. */
/* Dashy's native filter/settings strip duplicates the portal search and breaks on phones.
原生设置入口(齿轮/铅笔/调色板)与我们的 /admin/ 重复,统一隐藏。 */
html[data-theme] .home > .settings-outer,
html[data-theme] .config-launcher,
html[data-theme] .options-trigger,
html[data-theme] .settings-outer,
html[data-theme] .minimal-home .title-and-search > form {
display: none !important;
}
@ -98,7 +121,7 @@ html[data-theme] .navi-search-bar {
box-sizing: border-box !important;
position: fixed !important;
left: calc(16px + env(safe-area-inset-left, 0px)) !important;
bottom: calc(16px + env(safe-area-inset-bottom, 0px)) !important;
bottom: calc(76px + env(safe-area-inset-bottom, 0px)) !important;
margin: 0 !important;
display: flex !important;
align-items: center !important;
@ -144,20 +167,37 @@ html[data-theme] .navi-search-toggle {
border: 0 !important;
border-radius: 50% !important;
cursor: pointer;
transition: background .2s ease, color .2s ease, transform .2s cubic-bezier(.22, 1, .36, 1) !important;
appearance: none !important;
-webkit-appearance: none !important;
-webkit-tap-highlight-color: transparent;
transition: transform .28s cubic-bezier(.22, 1, .36, 1) !important;
}
html[data-theme] .navi-search-toggle:hover {
color: #0284c7 !important;
background: rgba(14, 165, 233, .12) !important;
transform: scale(1.06);
/* 点击与聚焦都不变蓝,只保留轻微缩放反馈。 */
html[data-theme] .navi-search-toggle:hover,
html[data-theme] .navi-search-toggle:focus,
html[data-theme] .navi-search-toggle:active {
color: #0f172a !important;
background: transparent !important;
outline: none !important;
}
@media (hover: hover) {
html[data-theme] .navi-search-toggle:hover {
transform: scale(1.05);
}
}
html[data-theme] .navi-search-toggle:active {
transform: scale(.96);
}
/* 右侧管理入口:Dashy 页面本身没有去 /admin/ 的链接 */
html[data-theme] .navi-admin-entry {
box-sizing: border-box !important;
position: fixed !important;
right: calc(16px + env(safe-area-inset-right, 0px)) !important;
bottom: calc(16px + env(safe-area-inset-bottom, 0px)) !important;
bottom: calc(76px + env(safe-area-inset-bottom, 0px)) !important;
width: 52px !important;
height: 52px !important;
display: grid !important;
@ -170,13 +210,26 @@ html[data-theme] .navi-admin-entry {
box-shadow: 0 14px 38px rgba(15, 23, 42, .16), inset 0 1px 0 rgba(255, 255, 255, .95) !important;
backdrop-filter: blur(22px) saturate(180%) !important;
-webkit-backdrop-filter: blur(22px) saturate(180%) !important;
transition: transform .22s cubic-bezier(.22, 1, .36, 1), color .2s ease, background .2s ease !important;
-webkit-tap-highlight-color: transparent !important;
transition: transform .22s cubic-bezier(.22, 1, .36, 1), background .2s ease !important;
}
html[data-theme] .navi-admin-entry:hover {
color: #0284c7 !important;
html[data-theme] .navi-admin-entry:hover,
html[data-theme] .navi-admin-entry:focus,
html[data-theme] .navi-admin-entry:active {
color: #334155 !important;
background: rgba(255, 255, 255, .94) !important;
transform: translateY(-1px) scale(1.05);
outline: none !important;
}
@media (hover: hover) {
html[data-theme] .navi-admin-entry:hover {
transform: translateY(-1px) scale(1.05);
}
}
html[data-theme] .navi-admin-entry:active {
transform: scale(.96);
}
html[data-theme] .navi-admin-entry svg {
@ -189,6 +242,114 @@ html[data-theme] body:has(.modal-overlay) > .navi-admin-entry {
pointer-events: none !important;
}
/* ── 视图切换:圆形悬浮按钮,紧贴管理入口左侧 ──
原生 .switch-view-buttons 是侧栏底部的方形图标,这里一并隐藏。 */
html[data-theme] nav.side-bar .switch-view-buttons {
display: none !important;
}
html[data-theme] .navi-view-switch {
position: fixed !important;
right: calc(78px + env(safe-area-inset-right, 0px)) !important;
bottom: calc(76px + env(safe-area-inset-bottom, 0px)) !important;
z-index: 900 !important;
}
html[data-theme] .navi-view-toggle {
box-sizing: border-box !important;
width: 52px !important;
height: 52px !important;
display: grid !important;
place-items: center !important;
padding: 0 !important;
color: #334155 !important;
background: rgba(255, 255, 255, .72) !important;
border: 1px solid rgba(255, 255, 255, .9) !important;
border-radius: 50% !important;
box-shadow: 0 14px 38px rgba(15, 23, 42, .16), inset 0 1px 0 rgba(255, 255, 255, .95) !important;
backdrop-filter: blur(22px) saturate(180%) !important;
-webkit-backdrop-filter: blur(22px) saturate(180%) !important;
-webkit-tap-highlight-color: transparent !important;
appearance: none !important;
-webkit-appearance: none !important;
outline: none !important;
cursor: pointer;
transition: transform .22s cubic-bezier(.22, 1, .36, 1), background .2s ease !important;
}
html[data-theme] .navi-view-toggle:hover,
html[data-theme] .navi-view-toggle:focus,
html[data-theme] .navi-view-toggle:active {
color: #334155 !important;
background: rgba(255, 255, 255, .94) !important;
}
@media (hover: hover) {
html[data-theme] .navi-view-toggle:hover {
transform: translateY(-1px) scale(1.05);
}
}
html[data-theme] .navi-view-toggle:active {
transform: scale(.96);
}
html[data-theme] .navi-view-toggle svg {
width: 22px !important;
height: 22px !important;
}
html[data-theme] .navi-view-menu {
position: absolute !important;
right: 0 !important;
bottom: 62px !important;
min-width: 134px !important;
display: flex !important;
flex-direction: column !important;
gap: 2px !important;
padding: 7px !important;
background: rgba(255, 255, 255, .92) !important;
border: 1px solid rgba(255, 255, 255, .95) !important;
border-radius: 16px !important;
box-shadow: 0 18px 40px rgba(15, 23, 42, .18), inset 0 1px 0 rgba(255, 255, 255, .95) !important;
backdrop-filter: blur(20px) saturate(180%) !important;
-webkit-backdrop-filter: blur(20px) saturate(180%) !important;
animation: navi-fade-up .22s cubic-bezier(.22, 1, .36, 1) both;
}
html[data-theme] .navi-view-menu[hidden] {
display: none !important;
}
html[data-theme] .navi-view-option {
padding: 9px 12px !important;
color: #334155 !important;
font-size: 13.5px !important;
text-decoration: none !important;
border-radius: 10px !important;
white-space: nowrap !important;
-webkit-tap-highlight-color: transparent !important;
}
html[data-theme] .navi-view-option:hover,
html[data-theme] .navi-view-option:focus,
html[data-theme] .navi-view-option:active {
color: #0f172a !important;
background: rgba(226, 232, 240, .7) !important;
outline: none !important;
}
html[data-theme] .navi-view-option.is-active {
color: #0f172a !important;
background: rgba(186, 230, 253, .55) !important;
font-weight: 600 !important;
}
html[data-theme] body:has(.modal-overlay) > .navi-view-switch {
visibility: hidden !important;
pointer-events: none !important;
}
html[data-theme] .navi-search-bar input {
min-width: 0 !important;
height: 42px !important;
@ -221,6 +382,8 @@ html[data-theme] .navi-search-submit {
border-radius: 14px !important;
box-shadow: 0 8px 20px rgba(37, 99, 235, .28) !important;
cursor: pointer;
-webkit-tap-highlight-color: transparent !important;
outline: none !important;
}
html[data-theme] .navi-search-toggle svg,
@ -229,6 +392,39 @@ html[data-theme] .navi-search-submit svg {
height: 20px;
}
/* 放大镜与搜索框同步形变:圆环、手柄退场,向左收起箭头进入。 */
html[data-theme] .navi-search-toggle svg {
overflow: visible;
}
html[data-theme] .navi-magnifier-ring,
html[data-theme] .navi-magnifier-handle,
html[data-theme] .navi-collapse-arrow {
transform-box: fill-box;
transform-origin: center;
transition: opacity .22s ease, transform .28s cubic-bezier(.22, 1, .36, 1) !important;
}
html[data-theme] .navi-collapse-arrow {
opacity: 0;
transform: translateX(5px) scale(.62);
}
html[data-theme] .navi-search-bar[data-state="open"] .navi-magnifier-ring {
opacity: 0;
transform: translateX(-4px) scale(.28);
}
html[data-theme] .navi-search-bar[data-state="open"] .navi-magnifier-handle {
opacity: 0;
transform: translate(-4px, -4px) rotate(-35deg) scale(.45);
}
html[data-theme] .navi-search-bar[data-state="open"] .navi-collapse-arrow {
opacity: 1;
transform: translateX(0) scale(1);
}
/* Minimal view is the phone/tablet layout used by Dashy. */
html[data-theme] .minimal-home {
box-sizing: border-box !important;
@ -244,13 +440,49 @@ html[data-theme] .minimal-home .title-and-search {
padding: 0 8px !important;
}
/* ── 顶部标题:三个视图统一(同字号/字重/字距/居中)──
此前 /minimal 是 48px 居中,而 / 与 /workspace 是顶栏里 40px 左对齐,
同一个站点的三个视图两套标题样式,很割裂。 */
html[data-theme] #dashy > header {
display: flex !important;
align-items: center !important;
justify-content: center !important;
padding: 10px 12px !important;
}
html[data-theme] #dashy > header .page-titles {
display: block !important;
width: auto !important;
max-width: 100% !important;
margin: 0 auto !important;
text-align: center !important;
}
html[data-theme] #dashy > header .page-titles .text {
display: block !important;
text-align: center !important;
}
html[data-theme] #dashy > header .page-titles .subtitle {
display: block !important;
margin: 6px 0 0 !important;
color: #64748b !important;
font-size: 13.5px !important;
font-weight: 400 !important;
letter-spacing: 0 !important;
text-align: center !important;
}
html[data-theme] #dashy > header .page-titles h1,
html[data-theme] .minimal-home .title-and-search h1 {
max-width: 100%;
margin: 0 !important;
color: #0f172a !important;
font-size: clamp(2rem, 8vw, 3rem) !important;
font-size: clamp(1.75rem, 4vw, 2.75rem) !important;
font-weight: 700 !important;
line-height: 1.08 !important;
letter-spacing: -.04em !important;
letter-spacing: -.035em !important;
text-align: center !important;
overflow-wrap: anywhere;
}
@ -331,8 +563,9 @@ html[data-theme] .minimal-section-inner a.item {
padding-top: 22px !important;
}
html[data-theme] #dashy > header .page-titles h1,
html[data-theme] .minimal-home .title-and-search h1 {
font-size: clamp(1.9rem, 9.4vw, 2.55rem) !important;
font-size: clamp(1.6rem, 8vw, 2.2rem) !important;
}
html[data-theme] .minimal-home > .item-group-container {
@ -410,7 +643,7 @@ html[data-theme] .minimal-section-inner a.item {
html[data-theme] .navi-search-bar {
left: calc(10px + env(safe-area-inset-left, 0px)) !important;
bottom: calc(12px + env(safe-area-inset-bottom, 0px)) !important;
bottom: calc(76px + env(safe-area-inset-bottom, 0px)) !important;
}
html[data-theme] .navi-search-bar[data-state="collapsed"] {
@ -428,19 +661,25 @@ html[data-theme] .minimal-section-inner a.item {
width: 48px !important;
height: 48px !important;
right: calc(10px + env(safe-area-inset-right, 0px)) !important;
bottom: calc(12px + env(safe-area-inset-bottom, 0px)) !important;
bottom: calc(76px + env(safe-area-inset-bottom, 0px)) !important;
}
html[data-theme] .navi-view-switch {
right: calc(68px + env(safe-area-inset-right, 0px)) !important;
}
html[data-theme] .navi-view-toggle {
width: 48px !important;
height: 48px !important;
}
html[data-theme] .navi-view-toggle svg {
width: 20px !important;
height: 20px !important;
}
html[data-theme] body {
padding-bottom: 78px !important;
}
html[data-theme] #dashy {
min-height: calc(100vh - 78px) !important;
}
html[data-theme] .navi-footer-host {
bottom: calc(78px + env(safe-area-inset-bottom, 0px)) !important;
padding-bottom: 0 !important;
}
}
@ -449,7 +688,7 @@ html[data-theme] .minimal-section-inner a.item {
内联样式里的 margin-bottom:80px(那是给旧的流式搜索栏让位的)。 */
html[data-theme] footer {
margin: 0 !important;
padding: 0 0 4px !important;
padding: 0 0 8px !important;
background: transparent !important;
}
@ -458,22 +697,13 @@ html[data-theme] footer > div {
padding: 0 !important;
}
/* 极简/工作空间视图补的备案号页脚:固定悬浮在搜索栏正上方。
它是「底部固定家具」的一部分,不能再参与文档流,否则会把页面撑高导致多余滚动。 */
/* 极简/工作空间视图补的备案号页脚:留在文档流里,靠 body 的 flex
把它顶到页面底部(贴着悬浮搜索栏上方),而不是 fixed 浮在内容中间。 */
html[data-theme] .navi-footer-host {
position: fixed !important;
left: 0 !important;
right: 0 !important;
bottom: calc(84px + env(safe-area-inset-bottom, 0px)) !important;
z-index: 890 !important;
margin-top: auto !important;
display: flex !important;
justify-content: center !important;
padding: 0 8px !important;
pointer-events: none !important;
}
html[data-theme] .navi-footer-host .navi-footer {
pointer-events: auto !important;
padding: 8px !important;
}
html[data-theme] .navi-footer {
@ -501,18 +731,132 @@ html[data-theme] .navi-footer a {
color: #64748b !important;
text-decoration: none !important;
border-bottom: 1px dashed rgba(100, 116, 139, .45) !important;
transition: color .2s ease, border-color .2s ease !important;
-webkit-tap-highlight-color: transparent !important;
}
html[data-theme] .navi-footer a:hover {
color: #0284c7 !important;
border-bottom-color: rgba(2, 132, 199, .6) !important;
html[data-theme] .navi-footer a:hover,
html[data-theme] .navi-footer a:focus,
html[data-theme] .navi-footer a:active {
color: #64748b !important;
border-bottom-color: rgba(100, 116, 139, .45) !important;
outline: none !important;
}
html[data-theme] .navi-footer-sep {
color: rgba(100, 116, 139, .45) !important;
}
/* ── 工作空间:右侧改为「分组应用列表」面板 ──
Dashy 原生面板是 iframe,目标站点多数用 X-Frame-Options 拒绝被嵌,
会呈现一片空白;这里改为直接展示我们自己的卡片列表。
面板走正常文档流(而不是 fixed 钉在中间),否则会把页面切成
「顶栏 / 大方块 / 底部」三段,看起来上下都被截断。 */
html[data-theme] .work-space .web-content,
html[data-theme] .work-space .multi-taking-view,
html[data-theme] .work-space .workspace-widget-view {
display: none !important;
}
html[data-theme] .work-space {
height: auto !important;
min-height: 0 !important;
}
html[data-theme] .navi-ws-pane {
box-sizing: border-box !important;
position: static !important;
display: flex !important;
flex-direction: column !important;
gap: 14px !important;
min-height: 340px !important;
margin: 18px 18px 14px 72px !important;
padding: 20px !important;
background: rgba(255, 255, 255, .5) !important;
border: 1px solid rgba(255, 255, 255, .82) !important;
border-radius: 20px !important;
box-shadow: 0 18px 44px rgba(15, 23, 42, .12), inset 0 1px 0 rgba(255, 255, 255, .92) !important;
backdrop-filter: blur(18px) saturate(180%) !important;
-webkit-backdrop-filter: blur(18px) saturate(180%) !important;
z-index: 1 !important;
}
html[data-theme] .navi-ws-title {
margin: 0 !important;
color: #0f172a !important;
font-size: 19px !important;
font-weight: 650 !important;
letter-spacing: -.01em !important;
}
html[data-theme] .navi-ws-hint {
color: #64748b !important;
font-size: 13.5px !important;
}
html[data-theme] .navi-ws-grid {
display: grid !important;
grid-template-columns: repeat(auto-fill, minmax(216px, 1fr)) !important;
gap: 12px !important;
}
html[data-theme] .navi-ws-card {
box-sizing: border-box !important;
display: flex !important;
flex-direction: column !important;
gap: 3px !important;
min-height: 78px !important;
padding: 14px 16px !important;
color: #0f172a !important;
text-decoration: none !important;
background: rgba(255, 255, 255, .68) !important;
border: 1px solid rgba(255, 255, 255, .9) !important;
border-radius: 14px !important;
box-shadow: 0 8px 20px rgba(15, 23, 42, .07) !important;
-webkit-tap-highlight-color: transparent !important;
transition: transform .22s cubic-bezier(.22, 1, .36, 1), box-shadow .22s ease !important;
}
@media (hover: hover) {
html[data-theme] .navi-ws-card:hover {
transform: translateY(-2px);
box-shadow: 0 14px 30px rgba(15, 23, 42, .12) !important;
}
}
html[data-theme] .navi-ws-card:active {
transform: scale(.99);
}
html[data-theme] .navi-ws-card-icon {
display: inline-flex !important;
align-items: center !important;
width: 26px !important;
height: 26px !important;
margin-bottom: 3px !important;
}
html[data-theme] .navi-ws-card-icon img {
width: 26px !important;
height: 26px !important;
object-fit: contain !important;
}
html[data-theme] .navi-ws-card-title {
font-size: 14.5px !important;
font-weight: 600 !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
html[data-theme] .navi-ws-card-url {
color: #64748b !important;
font-size: 12px !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
/* ── 工作空间:侧栏与内容区同样走玻璃圆角 ── */
html[data-theme] nav.side-bar {
background: rgba(255, 255, 255, .62) !important;
@ -585,6 +929,11 @@ html[data-theme] .navi-search-submit:hover {
box-shadow: 0 12px 26px rgba(37, 99, 235, .34) !important;
}
html[data-theme] .navi-search-submit:focus,
html[data-theme] .navi-search-submit:active {
outline: none !important;
}
html[data-theme] .navi-search-submit:active {
transform: translateY(0) scale(.96);
}

View File

@ -28,9 +28,12 @@
return el;
}
// 收起态是放大镜,展开时通过 transform/opacity 平滑形变为向左箭头,
// 让图标动效与搜索框的宽度动画保持一致。
const magnifierIcon = () => svg([
{ tag: 'circle', attrs: { cx: '11', cy: '11', r: '7.5' } },
{ tag: 'line', attrs: { x1: '20.5', y1: '20.5', x2: '16.2', y2: '16.2' } },
{ tag: 'circle', attrs: { class: 'navi-magnifier-ring', cx: '11', cy: '11', r: '7.5' } },
{ tag: 'line', attrs: { class: 'navi-magnifier-handle', x1: '20.5', y1: '20.5', x2: '16.2', y2: '16.2' } },
{ tag: 'polyline', attrs: { class: 'navi-collapse-arrow', points: '14.5 6.5 9 12 14.5 17.5' } },
], { size: '20' });
const arrowIcon = () => svg([
@ -134,6 +137,205 @@
document.body.append(link);
}
// 视图切换:Dashy 原生的切换按钮挤在侧栏底部、且是方形图标。
// 这里换成圆形悬浮按钮,紧挨管理入口左侧,点开是一组视图链接。
const VIEWS = [
{ path: '/', label: '首页' },
{ path: '/minimal', label: '极简' },
{ path: '/workspace', label: '工作空间' },
];
const viewIcon = () => svg([
{ tag: 'rect', attrs: { x: '3', y: '3.5', width: '18', height: '17', rx: '3.4' } },
{ tag: 'line', attrs: { x1: '9.4', y1: '3.5', x2: '9.4', y2: '20.5' } },
{ tag: 'line', attrs: { x1: '12.8', y1: '9.4', x2: '17.8', y2: '9.4' } },
{ tag: 'line', attrs: { x1: '12.8', y1: '14.2', x2: '17.8', y2: '14.2' } },
], { size: '20', width: '1.9' });
function currentView() {
const p = location.pathname.replace(/\/+$/, '') || '/';
if (p.endsWith('/minimal')) return '/minimal';
if (p.endsWith('/workspace')) return '/workspace';
return '/';
}
function mountViewSwitch() {
if (document.querySelector('.navi-view-switch')) return;
const wrap = document.createElement('div');
wrap.className = 'navi-view-switch';
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'navi-view-toggle';
btn.setAttribute('aria-label', '切换视图');
btn.setAttribute('aria-expanded', 'false');
btn.append(viewIcon());
const menu = document.createElement('div');
menu.className = 'navi-view-menu';
menu.hidden = true;
const active = currentView();
VIEWS.forEach((v) => {
const a = document.createElement('a');
a.className = 'navi-view-option';
a.href = v.path;
a.textContent = v.label;
if (v.path === active) {
a.classList.add('is-active');
a.setAttribute('aria-current', 'page');
}
menu.append(a);
});
const close = () => {
menu.hidden = true;
btn.setAttribute('aria-expanded', 'false');
};
btn.addEventListener('click', (evt) => {
evt.preventDefault();
evt.stopPropagation();
const open = menu.hidden;
menu.hidden = !open;
btn.setAttribute('aria-expanded', String(open));
});
document.addEventListener('click', (evt) => {
if (!menu.hidden && !wrap.contains(evt.target)) close();
});
document.addEventListener('keydown', (evt) => {
if (evt.key === 'Escape' && !menu.hidden) close();
});
wrap.append(btn, menu);
document.body.append(wrap);
}
// Dashy 的「工作空间」右侧面板是一个 iframe:侧栏点应用时把目标 URL
// 内嵌进去。但多数应用(Gitea、VanBlog 等)用 X-Frame-Options 拒绝被嵌,
// 面板就是一片空白(实测:Refused to display … 'sameorigin')。
// 所以这里改成:点左侧分组 → 右侧列出该分组的应用卡片,不依赖内嵌。
const WS_ICON_RE = /\.(png|svg|jpe?g|webp|gif|ico)$/i;
let wsSections = null;
let wsPending = false;
async function fetchWorkspaceSections() {
if (wsSections) return wsSections;
if (wsPending) return null;
wsPending = true;
try {
const res = await fetch('/admin/api/sections', { credentials: 'same-origin' });
if (!res.ok) return null;
const data = await res.json();
const list = Array.isArray(data) ? data : (data.sections || []);
// 与 Dashy 侧栏保持同序:剔除标记了 hideFromWorkspace 的分组。
wsSections = list.filter((s) => !(s && s.displayData && s.displayData.hideFromWorkspace));
return wsSections;
} catch (err) {
return null;
} finally {
wsPending = false;
}
}
function workspacePane() {
const host = document.querySelector('.work-space');
if (!host) return null;
let pane = host.querySelector('.navi-ws-pane');
if (!pane) {
pane = document.createElement('div');
pane.className = 'navi-ws-pane';
host.append(pane);
}
return pane;
}
function renderWorkspaceSection(section) {
const pane = workspacePane();
if (!pane || !section) return;
while (pane.firstChild) pane.removeChild(pane.firstChild);
const title = document.createElement('h2');
title.className = 'navi-ws-title';
title.textContent = section.name || '';
pane.append(title);
const items = (section.items || []).filter((it) => it && it.url);
if (!items.length) {
const empty = document.createElement('div');
empty.className = 'navi-ws-hint';
empty.textContent = '这个分组还没有项目';
pane.append(empty);
return;
}
const grid = document.createElement('div');
grid.className = 'navi-ws-grid';
items.forEach((it) => {
const card = document.createElement('a');
card.className = 'navi-ws-card';
card.href = it.url;
card.target = '_blank';
card.rel = 'noopener noreferrer';
const icon = String(it.icon || '');
if (WS_ICON_RE.test(icon)) {
const wrap = document.createElement('span');
wrap.className = 'navi-ws-card-icon';
const img = document.createElement('img');
img.src = '/item-icons/' + icon;
img.alt = '';
img.loading = 'lazy';
wrap.append(img);
card.append(wrap);
}
const name = document.createElement('span');
name.className = 'navi-ws-card-title';
name.textContent = it.title || it.url;
card.append(name);
const sub = document.createElement('span');
sub.className = 'navi-ws-card-url';
sub.textContent = String(it.description || '').trim()
|| it.url.replace(/^https?:\/\//, '').replace(/\/$/, '');
card.append(sub);
grid.append(card);
});
pane.append(grid);
}
function enableWorkspacePane() {
if (window.__naviWorkspacePane) return;
window.__naviWorkspacePane = true;
document.addEventListener('click', async (evt) => {
if (!/^\/workspace/.test(location.pathname)) return;
const target = evt.target;
const head = target && target.closest ? target.closest('.side-bar-item-container') : null;
if (!head) return;
const sections = await fetchWorkspaceSections();
if (!sections || !sections.length) return;
const heads = Array.from(document.querySelectorAll('nav.side-bar .side-bar-item-container'));
const idx = heads.indexOf(head);
if (idx >= 0 && sections[idx]) renderWorkspaceSection(sections[idx]);
});
}
// 工作空间初次打开时先把第一个分组的内容摆上,避免右侧一片空白。
let wsSeeded = false;
async function seedWorkspacePane() {
if (wsSeeded) return;
if (!/^\/workspace/.test(location.pathname)) return;
if (!document.querySelector('.work-space')) return;
wsSeeded = true;
const sections = await fetchWorkspaceSections();
if (sections && sections.length) renderWorkspaceSection(sections[0]);
}
// 极简视图(/minimal)与工作空间视图不渲染 Dashy 页脚,备案号会整段消失。
// 这里从只读的 pageinfo 取回页脚 HTML 补一份,保证每个视图都能看到备案信息。
let footerPending = false;
@ -166,6 +368,9 @@
function run() {
mountSearch();
mountAdminEntry();
mountViewSwitch();
enableWorkspacePane();
seedWorkspacePane();
hideInstallEntry();
mountFooter();
}