feat(secrets-mcp): 条目页按 folder/type 筛选并发版 0.3.5
- entries 路由支持 ?folder=&type= 查询,与搜索层 SearchParams 对齐 - 条目列表页增加筛选表单与说明文案 - 版本 0.3.4 → 0.3.5,同步 Cargo.lock Made-with: Cursor
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1968,7 +1968,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "secrets-mcp"
|
name = "secrets-mcp"
|
||||||
version = "0.3.4"
|
version = "0.3.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"askama",
|
"askama",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "secrets-mcp"
|
name = "secrets-mcp"
|
||||||
version = "0.3.4"
|
version = "0.3.5"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ struct EntriesPageTemplate {
|
|||||||
total_count: i64,
|
total_count: i64,
|
||||||
shown_count: usize,
|
shown_count: usize,
|
||||||
limit: u32,
|
limit: u32,
|
||||||
|
filter_folder: String,
|
||||||
|
filter_type: String,
|
||||||
version: &'static str,
|
version: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +108,14 @@ struct EntryListItemView {
|
|||||||
/// Cap for HTML list (avoids loading unbounded rows into memory).
|
/// Cap for HTML list (avoids loading unbounded rows into memory).
|
||||||
const ENTRIES_PAGE_LIMIT: u32 = 5_000;
|
const ENTRIES_PAGE_LIMIT: u32 = 5_000;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct EntriesQuery {
|
||||||
|
folder: Option<String>,
|
||||||
|
/// URL query key is `type` (maps to DB column `entries.type`).
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
entry_type: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
// ── App state helpers ─────────────────────────────────────────────────────────
|
// ── App state helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn google_cfg(state: &AppState) -> Option<&OAuthConfig> {
|
fn google_cfg(state: &AppState) -> Option<&OAuthConfig> {
|
||||||
@@ -510,6 +520,7 @@ async fn dashboard(
|
|||||||
async fn entries_page(
|
async fn entries_page(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
session: Session,
|
session: Session,
|
||||||
|
Query(q): Query<EntriesQuery>,
|
||||||
) -> Result<Response, StatusCode> {
|
) -> Result<Response, StatusCode> {
|
||||||
let Some(user_id) = current_user_id(&session).await else {
|
let Some(user_id) = current_user_id(&session).await else {
|
||||||
return Ok(Redirect::to("/login").into_response());
|
return Ok(Redirect::to("/login").into_response());
|
||||||
@@ -523,9 +534,22 @@ async fn entries_page(
|
|||||||
None => return Ok(Redirect::to("/login").into_response()),
|
None => return Ok(Redirect::to("/login").into_response()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let folder_filter = q
|
||||||
|
.folder
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.trim())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
let type_filter = q
|
||||||
|
.entry_type
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.trim())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
let params = SearchParams {
|
let params = SearchParams {
|
||||||
folder: None,
|
folder: folder_filter.as_deref(),
|
||||||
entry_type: None,
|
entry_type: type_filter.as_deref(),
|
||||||
name: None,
|
name: None,
|
||||||
tags: &[],
|
tags: &[],
|
||||||
query: None,
|
query: None,
|
||||||
@@ -567,6 +591,8 @@ async fn entries_page(
|
|||||||
total_count,
|
total_count,
|
||||||
shown_count,
|
shown_count,
|
||||||
limit: ENTRIES_PAGE_LIMIT,
|
limit: ENTRIES_PAGE_LIMIT,
|
||||||
|
filter_folder: folder_filter.unwrap_or_default(),
|
||||||
|
filter_type: type_filter.unwrap_or_default(),
|
||||||
version: env!("CARGO_PKG_VERSION"),
|
version: env!("CARGO_PKG_VERSION"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,30 @@
|
|||||||
padding: 24px; width: 100%; max-width: 1280px; margin: 0 auto; }
|
padding: 24px; width: 100%; max-width: 1280px; margin: 0 auto; }
|
||||||
.card-title { font-size: 20px; font-weight: 600; margin-bottom: 8px; }
|
.card-title { font-size: 20px; font-weight: 600; margin-bottom: 8px; }
|
||||||
.card-subtitle { color: var(--text-muted); font-size: 13px; margin-bottom: 20px; }
|
.card-subtitle { color: var(--text-muted); font-size: 13px; margin-bottom: 20px; }
|
||||||
|
.filter-bar {
|
||||||
|
display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px 16px;
|
||||||
|
margin-bottom: 20px; padding: 16px; background: var(--bg); border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.filter-field { display: flex; flex-direction: column; gap: 6px; min-width: 140px; flex: 1; }
|
||||||
|
.filter-field label { font-size: 12px; color: var(--text-muted); font-weight: 500; }
|
||||||
|
.filter-field input {
|
||||||
|
background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
|
||||||
|
color: var(--text); padding: 8px 10px; font-size: 13px; font-family: 'JetBrains Mono', monospace;
|
||||||
|
outline: none; width: 100%;
|
||||||
|
}
|
||||||
|
.filter-field input:focus { border-color: var(--accent); }
|
||||||
|
.filter-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
||||||
|
.btn-filter {
|
||||||
|
padding: 8px 16px; border-radius: 6px; border: none; background: var(--accent); color: #0d1117;
|
||||||
|
font-size: 13px; font-weight: 600; cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-filter:hover { background: var(--accent-hover); }
|
||||||
|
.btn-clear {
|
||||||
|
padding: 8px 14px; border-radius: 6px; border: 1px solid var(--border); background: transparent;
|
||||||
|
color: var(--text-muted); font-size: 13px; text-decoration: none; cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-clear:hover { background: var(--surface2); color: var(--text); }
|
||||||
.empty { color: var(--text-muted); font-size: 14px; padding: 20px 0; }
|
.empty { color: var(--text-muted); font-size: 14px; padding: 20px 0; }
|
||||||
.table-wrap { overflow-x: auto; }
|
.table-wrap { overflow-x: auto; }
|
||||||
table { width: 100%; border-collapse: collapse; min-width: 720px; }
|
table { width: 100%; border-collapse: collapse; min-width: 720px; }
|
||||||
@@ -116,7 +140,22 @@
|
|||||||
<main class="main">
|
<main class="main">
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card-title">我的条目</div>
|
<div class="card-title">我的条目</div>
|
||||||
<div class="card-subtitle">共 <strong>{{ total_count }}</strong> 条记录;当前列表显示 <strong>{{ shown_count }}</strong> 条(按更新时间降序,单页最多 {{ limit }} 条)。不含密文字段。时间为浏览器本地时区。</div>
|
<div class="card-subtitle">在当前筛选条件下,共 <strong>{{ total_count }}</strong> 条记录;本页显示 <strong>{{ shown_count }}</strong> 条(按更新时间降序,单页最多 {{ limit }} 条)。不含密文字段。时间为浏览器本地时区。</div>
|
||||||
|
|
||||||
|
<form class="filter-bar" method="get" action="/entries">
|
||||||
|
<div class="filter-field">
|
||||||
|
<label for="filter-folder">Folder(精确匹配)</label>
|
||||||
|
<input id="filter-folder" name="folder" type="text" value="{{ filter_folder }}" placeholder="例如 refining" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="filter-field">
|
||||||
|
<label for="filter-type">Type(精确匹配)</label>
|
||||||
|
<input id="filter-type" name="type" type="text" value="{{ filter_type }}" placeholder="例如 server" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button type="submit" class="btn-filter">筛选</button>
|
||||||
|
<a href="/entries" class="btn-clear">清空</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
{% if entries.is_empty() %}
|
{% if entries.is_empty() %}
|
||||||
<div class="empty">暂无条目。</div>
|
<div class="empty">暂无条目。</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user