t193 三修:每庫一頁+狀態時間軸+預設淺色(leo 六點回饋)

## ① logo 尺寸不對導致白邊
真兇:我寫死 height:22px **沒給 width:auto** ⇒ 容器一壓就變形、露出底板邊緣。
修:照 portal 側欄同一條 clamp(22px,5vw,28px) + width:auto + max-width:100%,
    並依 CIS「clear space = one chevron height」在四邊留白。
實測:logo 底板與品牌區落差 **0**(原本是明顯方塊)。

## ② 深色跟網頁不同/「它有淺色佈景?」
是——**portal 預設淺色**,深色由使用者切換並存 localStorage。
我卻硬跟系統走 ⇒ 兩邊當然不同。
修:改成與 portal 同一套(預設淺色+切換鈕+localStorage),
    按鈕規格也逐字對齊 portal 的 .btn/.btn3(padding/font-size/radius/border)。
實測淺色:品牌區(253,252,251)=Paper、側欄(242,241,237)=Canvas,與 CIS 一致。

## ③ 「開啟知識庫網頁」開到錯的庫
真兇:它在首頁、寫死取 accounts[0] ⇒ 有兩個庫必然開錯。
修:移到**各庫頁的上方**,開的就是那個庫。

## ④⑤ 30 個資料夾放不下/「加入資料夾」會加到哪個帳號?
修:**每個知識庫一個獨立分頁**(側邊欄動態列出,附資料夾數)。
    「加入資料夾」在庫頁裡,**作用對象就是那個庫**,不會加錯。

## ⑥ 首頁要顯示 status(看守/發現變化/萃取/上傳)
修:首頁改成**狀態時間軸**四步,進行中那步的圓點會呼吸。
    誠實邊界:collector 目前回報「整輪」而非逐檔階段,
    所以同步中時後三步一起標進行中,**不假裝有更細的進度**。

兩支機械閘全過(check-cis.sh 14 項/check-render.sh 深淺色皆驗)。
style.css 仍是 0 個 hex(顏色全走 var)。
This commit is contained in:
2026-08-04 23:35:49 +08:00
parent 3d13e5a54e
commit 751ee74f19
4 changed files with 224 additions and 100 deletions
+97 -77
View File
@@ -1,8 +1,11 @@
// Arcrun 桌面 App — 前端(t193
//
// 版面leo 08-04 指定附 Google Drive 截圖):
// 「可以完全模仿 gdrive,**左方側邊欄,在右方替換頁面**」
// ⇒ 設定不再開 popup,全部是右側的頁;只有「確認刪除」才用覆蓋層。
// 版面leo 08-04 指定附 Google Drive 截圖):左側邊欄、右側換頁。
// 🔴 08-04 二輪回饋(本次處理):
// ③「開啟知識庫網頁」不該在首頁(會開到錯的庫)⇒ 移到**各庫頁的上方**
// ④ 一個庫 30 個資料夾放不下 ⇒ **每個知識庫一個獨立分頁**
// ⑤「加入資料夾」會加到哪個帳號?⇒ 在庫頁裡加,**作用對象就是那個庫**,不會加錯
// ⑥ 首頁要顯示 status:看守/發現變化/萃取/上傳 ⇒ **狀態時間軸**
import './arcrun-cis.css'; // 共用底層(色票/字體/紋理)——唯一真相源在 arcrun-cis/css/
import './style.css'; // 本 App 的版面
@@ -11,73 +14,96 @@ const $ = (id) => document.getElementById(id);
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
// 跟隨系統深淺色(CIS 兩套色票都在 arcrun-cis.css 定義好了)
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const applyTheme = () =>
document.documentElement.setAttribute('data-theme', mq.matches ? 'dark' : 'light');
mq.addEventListener('change', applyTheme);
applyTheme();
// 主題:**與 portal 同一套規則**——預設淺色,使用者切換後存 localStorage
// (leo 問「它有淺色佈景?」⇒ 是,portal 預設就是淺色)
const THEME_KEY = 'arcrun_app_theme';
function applyTheme(t) {
document.documentElement.setAttribute('data-theme', t);
try { localStorage.setItem(THEME_KEY, t); } catch (e) {}
const b = $('themeBtn'); if (b) b.textContent = t === 'dark' ? '☀' : '☾';
}
applyTheme((() => { try { return localStorage.getItem(THEME_KEY) === 'dark' ? 'dark' : 'light'; } catch (e) { return 'light'; } })());
$('themeBtn').onclick = () =>
applyTheme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
let state = null;
let page = 'home';
let updateInfo = null; // { current, latest, available, notes, staged }
let page = 'home'; // 'home' | 'ai' | 'update' | 'lib:<idx>'
let updateInfo = null;
// ── 覆蓋層(只給必須打斷的確認)──
function openSheet(html, wire) {
$('sheet').innerHTML = html; $('overlay').classList.add('on'); if (wire) wire();
}
function openSheet(html, wire) { $('sheet').innerHTML = html; $('overlay').classList.add('on'); if (wire) wire(); }
function closeSheet() { $('overlay').classList.remove('on'); }
$('overlay').addEventListener('click', (e) => { if (e.target.id === 'overlay') closeSheet(); });
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeSheet(); });
// ── 側邊欄換頁 ──
document.querySelectorAll('#side .nav').forEach((el) => {
el.onclick = () => {
page = el.dataset.page;
document.querySelectorAll('#side .nav').forEach((n) => n.classList.toggle('on', n === el));
renderPage();
};
});
// ── 側邊欄:每個知識庫一項(leo:「每個帳號有獨立的一個頁面」)──
function renderNav() {
const accs = (state && state.accounts) || [];
$('nav').innerHTML = `
<div class="nav" data-p="home"><span class="ic">◫</span><span class="nm">首頁</span></div>
${accs.length ? `<div class="sec">知識庫</div>` : ''}
${accs.map((a, i) => `
<div class="nav" data-p="lib:${i}">
<span class="ic">▤</span><span class="nm">${esc(a.name)}</span>
<span class="cnt">${(a.folders || []).length}</span>
</div>`).join('')}
<div class="sec">設定</div>
<div class="nav" data-p="ai"><span class="ic">✧</span><span class="nm">AI 設定</span></div>
<div class="nav" data-p="update"><span class="ic">↧</span><span class="nm">版本與更新</span></div>`;
$('nav').querySelectorAll('.nav').forEach((el) => {
el.classList.toggle('on', el.dataset.p === page);
el.onclick = () => { page = el.dataset.p; renderNav(); renderPage(); };
});
}
// ── 各頁 ──
// ── 首頁:狀態時間軸(leo:「看守、發現變化、萃取、上傳… 不同 status 在哪裡顯示?」)──
function pageHome(s) {
const folders = (s.accounts || []).reduce((n, a) => n + (a.folders || []).length, 0);
if (!s.accounts || !s.accounts.length) return onboarding();
const st = s.steps || [];
return `
<div class="card">
<h3>目前狀況</h3>
<div class="kv" style="margin-top:10px">
<div><div class="big-num">${s.accounts.length}</div><div class="k">個知識庫</div></div>
<div><div class="big-num">${folders}</div><div class="k">個資料夾在看守</div></div>
<div><div class="big-num">${s.extractedOK || 0}</div><div class="k">份已整理</div></div>
<h3>現在的狀態</h3>
<div class="steps">
${st.map((x) => `
<div class="step ${esc(x.state)}">
<span class="dot"></span>
<span class="t">${esc(x.title)}</span>
<span class="m">${esc(x.meta || '')}</span>
</div>`).join('')}
</div>
</div>
<div class="card">
<h3>快速動作</h3>
<div class="acts">
<button id="qAdd">加入資料夾</button>
<button id="qPortal">開啟知識庫網頁</button>
<button id="qDocs">使用說明</button>
<h3>總計</h3>
<div class="kv" style="margin-top:10px">
<div><div class="big-num">${s.accounts.length}</div><div class="k">個知識庫</div></div>
<div><div class="big-num">${s.accounts.reduce((n,a)=>n+(a.folders||[]).length,0)}</div><div class="k">個資料夾在看守</div></div>
<div><div class="big-num">${s.extractedOK || 0}</div><div class="k">份已整理</div></div>
</div>
</div>`;
}
function pageFolders(s) {
if (!s.accounts || !s.accounts.length) return onboarding();
return (s.accounts).map((a) => `
<div class="acct"><span class="name">${esc(a.name)}</span><span class="host">${esc(a.host)}</span></div>
// ── 各庫頁:動作全部作用在這個庫(不會加錯帳號)──
function pageLib(s, idx) {
const a = s.accounts[idx];
if (!a) return `<div class="empty"><div class="t">找不到這個知識庫</div></div>`;
const portal = 'https://' + a.host.replace('arcrun-cypher-executor.', 'arcrun-rag-ui.') + '/portal/';
return `
<div class="libhead">
<div class="g">
<div class="nm">${esc(a.name)}</div>
<div class="host">${esc(a.host)}</div>
</div>
<button data-portal="${esc(portal)}">開啟知識庫網頁</button>
<button class="primary" data-addto="${idx}">加入資料夾</button>
</div>
${(a.folders || []).map((f) => `
<div class="folder">
<span class="path" title="${esc(f.path)}">${esc(f.path)}</span>
<span class="tag">自動同步中</span>
<button class="ghost" data-rm="${esc(f.path)}" data-acc="${f.accIdx}">移除</button>
</div>`).join('')
|| `<div class="folder"><span class="path dim">還沒有資料夾</span></div>`}
`).join('') + `
<div class="acts">
<button class="primary" id="fAdd">加入資料夾</button>
<button id="fAcct">新增知識庫帳號</button>
</div>`;
|| `<div class="empty"><div class="t">這個知識庫還沒有資料夾</div>
<div class="d">按右上的「加入資料夾」,選一個要自動整理的資料夾。</div></div>`}`;
}
function pageAI(s) {
@@ -100,11 +126,9 @@ function pageAI(s) {
</div>`;
}
// leo:「檢查更新連到 docs 的檢查更新,不正確,**是直接在這裡進行檢查更新**,
// 應該是跳到一個頁面顯示**現在版本與最新版本**,然後**下載可以進行安裝**」
function pageUpdate(s) {
const u = updateInfo;
const latest = u ? (u.latest || '查詢中…') : '查詢中…';
const latest = u ? (u.latest || '查詢中…') : '按「檢查更新」查詢';
let action = `<button class="primary" id="uCheck">檢查更新</button>`;
let note = '';
if (u && u.staged) {
@@ -149,32 +173,30 @@ function onboarding() {
</div>`;
}
// ── 渲染 ──
function renderPage() {
if (!state) return;
const el = $('page');
el.innerHTML = page === 'folders' ? pageFolders(state)
: page === 'ai' ? pageAI(state)
: page === 'update' ? pageUpdate(state)
: pageHome(state);
let html;
if (page.startsWith('lib:')) html = pageLib(state, Number(page.slice(4)));
else if (page === 'ai') html = pageAI(state);
else if (page === 'update') html = pageUpdate(state);
else html = pageHome(state);
// 每個庫頁底下都給「新增知識庫帳號」入口
if (page === 'home' && state.accounts && state.accounts.length) {
html += `<div class="acts"><button id="hAcct">新增知識庫帳號</button></div>`;
}
$('page').innerHTML = html;
wire();
}
function wire() {
const on = (id, fn) => { const e = $(id); if (e) e.onclick = fn; };
on('qAdd', addFolder); on('fAdd', addFolder);
on('qDocs', () => go.OpenURL('https://rag.arcrun.dev/docs/'));
on('uDocs', () => go.OpenURL('https://rag.arcrun.dev/docs/use/update/'));
on('qPortal', () => {
const a = state.accounts && state.accounts[0];
if (a) go.OpenURL('https://' + a.host.replace('arcrun-cypher-executor.', 'arcrun-rag-ui.') + '/portal/');
});
on('fAcct', showConnect); on('obConnect', showConnect);
on('uDocs', () => go.OpenURL('https://rag.arcrun.dev/docs/'));
on('hAcct', showConnect); on('obConnect', showConnect);
on('obInstall', () => go.OpenURL('https://install.arcrun.dev/'));
on('aiSave', saveAI);
on('uCheck', checkUpdate);
on('uDownload', downloadUpdate);
on('uApply', applyUpdate);
on('uCheck', checkUpdate); on('uDownload', downloadUpdate); on('uApply', applyUpdate);
document.querySelectorAll('[data-portal]').forEach((b) => { b.onclick = () => go.OpenURL(b.dataset.portal); });
document.querySelectorAll('[data-addto]').forEach((b) => { b.onclick = () => addFolder(Number(b.dataset.addto)); });
document.querySelectorAll('[data-rm]').forEach((b) => {
b.onclick = () => confirmRemove(Number(b.dataset.acc), b.dataset.rm);
});
@@ -182,28 +204,27 @@ function wire() {
function render(s) {
const first = !state;
const navChanged = state && JSON.stringify((state.accounts||[]).map(a=>[a.name,(a.folders||[]).length]))
!== JSON.stringify((s.accounts||[]).map(a=>[a.name,(a.folders||[]).length]));
state = s;
$('ver').textContent = s.version ? s.version : '';
$('ver').textContent = s.version || '';
$('statusBig').textContent = s.statusBig;
$('statusBig').classList.toggle('syncing', !!s.syncing);
$('statusSub').textContent = s.statusSub;
// 只有狀態列每秒更新;頁面內容不重繪,避免打斷輸入
if (first) renderPage();
if (first || navChanged) { renderNav(); renderPage(); }
else if (page === 'home') renderPage(); // 首頁的狀態時間軸要跟著跳
}
async function tick() {
try { render(await go.GetState()); } catch (e) { /* 後端還沒起來 */ }
}
async function tick() { try { render(await go.GetState()); } catch (e) {} }
// ── 動作 ──
$('btnSync').onclick = async () => { await go.SyncNow(); tick(); };
async function addFolder() {
if (!state || !state.accounts.length) { showConnect(); return; }
async function addFolder(accIdx) {
const p = await go.PickFolder();
if (!p) return;
await go.AddFolder(0, p);
state = await go.GetState(); renderPage();
await go.AddFolder(accIdx, p);
state = await go.GetState(); renderNav(); renderPage();
}
function confirmRemove(accIdx, path) {
@@ -215,7 +236,7 @@ function confirmRemove(accIdx, path) {
$('c1').onclick = closeSheet;
$('c2').onclick = async () => {
await go.RemoveFolder(accIdx, path); closeSheet();
state = await go.GetState(); renderPage();
state = await go.GetState(); renderNav(); renderPage();
};
});
}
@@ -236,7 +257,7 @@ function showConnect() {
$('c2').onclick = async () => {
try {
await go.Connect($('u').value.trim(), $('e').value.trim(), $('p').value);
closeSheet(); state = await go.GetState(); renderPage();
closeSheet(); state = await go.GetState(); renderNav(); renderPage();
} catch (ex) { $('err').textContent = String(ex); $('err').style.display = 'block'; }
};
});
@@ -250,7 +271,6 @@ async function saveAI() {
} catch (ex) { $('aiErr').textContent = String(ex); $('aiErr').style.display = 'block'; }
}
// 更新:**在 App 裡完成**leo 指定),不是丟去 docs
async function checkUpdate() {
updateInfo = { latest: '查詢中…' }; renderPage();
try { updateInfo = await go.CheckUpdate(); } catch (ex) { updateInfo = { err: String(ex) }; }