feat: 搜索框下滚动、本地服务卡片排序、拖拽末位修复
- 恢复内容区内部滚动:页头/搜索框固定,滚动区域从搜索框下方开始;侧栏跟随内容滚动,侧栏滚轮驱动内容区 - v22:本地服务以 card 行 local-service 参与排序;首页按卡片顺序渲染,后台可拖动排序 - 本地服务卡片后端禁止删除/改名/配置分组 - 修复向下拖动时插到目标之后,倒数第二项可拖到最后 - 验证:搜索框位置固定、侧栏 scrollTop 跟随、本地服务与倒数第二拖末位均落库、首页顺序一致
This commit is contained in:
+4
-3
@@ -100,7 +100,7 @@ README.md 运行、路由、库结构、登录说明
|
|||||||
| 分组管理 | 树形表格:新建根分组(左侧)、加子分组、编辑(名称/上级/图标/排序/状态)、删除;格式已下沉到条目,不再在分组上设置 |
|
| 分组管理 | 树形表格:新建根分组(左侧)、加子分组、编辑(名称/上级/图标/排序/状态)、删除;格式已下沉到条目,不再在分组上设置 |
|
||||||
| 内容 | 选分组以列表展示条目(选主分组时包含所有子分组);列:标题 / 格式 / 跳转链接 / 所属分组 / 操作;每条内容单独选格式(应用/海报/图文),同一分组可混合 |
|
| 内容 | 选分组以列表展示条目(选主分组时包含所有子分组);列:标题 / 格式 / 跳转链接 / 所属分组 / 操作;每条内容单独选格式(应用/海报/图文),同一分组可混合 |
|
||||||
| 侧边栏 | 顶部「侧栏卡片」控制本地管理员 / 本地服务显隐;下方「分组导航」逐项勾选;开关即保存 |
|
| 侧边栏 | 顶部「侧栏卡片」控制本地管理员 / 本地服务显隐;下方「分组导航」逐项勾选;开关即保存 |
|
||||||
| 首页卡片 | 左侧列表含只读「本地服务」表(不可删除)+ 自建卡片;右侧分组表格;卡片可拖动排序,表格内开关/条数即时保存 |
|
| 首页卡片 | 左侧列表含「本地服务」只读表(不可删除,但可拖动排序)+ 自建卡片;右侧分组表格;表格内开关/条数即时保存,标题用「保存」按钮 |
|
||||||
| 页头 | 启用开关、速度、条目、标签与色调 |
|
| 页头 | 启用开关、速度、条目、标签与色调 |
|
||||||
| 资料 | 头像 URL、名称、角色、简介、位置 |
|
| 资料 | 头像 URL、名称、角色、简介、位置 |
|
||||||
| 系统 | 修改密码、下载备份 |
|
| 系统 | 修改密码、下载备份 |
|
||||||
@@ -164,9 +164,10 @@ GET /api/backup SQLite 快照附件 需登录
|
|||||||
## 5. 数据库与迁移
|
## 5. 数据库与迁移
|
||||||
|
|
||||||
- 文件:开发态在 `target/debug/data/stash.db`;可用 `--root` / 数据目录覆盖(见 `config.rs`)。
|
- 文件:开发态在 `target/debug/data/stash.db`;可用 `--root` / 数据目录覆盖(见 `config.rs`)。
|
||||||
- 当前 `PRAGMA user_version = 21`;`schema::migrate` 逐级升级:0 建全量、15→16→17→18→19→20→21、更高版本拒绝启动。
|
- 当前 `PRAGMA user_version = 22`;`schema::migrate` 逐级升级:0 建全量、15→16→17→18→19→20→21→22、更高版本拒绝启动。
|
||||||
- 格式在 `entry.format`(apps / posters / articles),同组可混合;`node` 的旧 `view_mode` 仅保留兼容,不再决定渲染。
|
- 格式在 `entry.format`(apps / posters / articles),同组可混合;`node` 的旧 `view_mode` 仅保留兼容,不再决定渲染。
|
||||||
- `settings.sidebar.show_profile / show_service` 控制侧栏两张卡片显隐(默认 true);`card` 表只存自建卡片,首页「本地服务」卡片由 `/api/projects` 运行时生成,不落库、不可删。
|
- `settings.sidebar.show_profile / show_service` 控制侧栏两张卡片显隐(默认 true)。
|
||||||
|
- 本地服务是运行时卡片,但 `card` 表里有一行 `local-service` 只用于排序;内容来自 `/api/projects`,后端禁止删除/改名/配置分组。
|
||||||
- `node` 是统一分组树(根节点 `kind='section'`,子节点 `kind='group'` 可多层);`enabled` 控制启停;`icon` 可为空。
|
- `node` 是统一分组树(根节点 `kind='section'`,子节点 `kind='group'` 可多层);`enabled` 控制启停;`icon` 可为空。
|
||||||
- 主要表:`node`、`entry`、`entry_tag`、`section_block`、`admin`、`session`、`setting`、`audit`。
|
- 主要表:`node`、`entry`、`entry_tag`、`section_block`、`admin`、`session`、`setting`、`audit`。
|
||||||
- `entry.updated_at` 用于编辑乐观锁;`setting` 表 `key='site'` 存站点 JSON。
|
- `entry.updated_at` 用于编辑乐观锁;`setting` 表 `key='site'` 存站点 JSON。
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ cargo run -- --root ./sites --host 0.0.0.0 --port 3000
|
|||||||
### 登录与设置(v0.1.1 起)
|
### 登录与设置(v0.1.1 起)
|
||||||
|
|
||||||
- 登录是**页内弹窗**:点右上角用户图标;首次使用弹窗里是「设置密码 + 确认」(至少 8 位);
|
- 登录是**页内弹窗**:点右上角用户图标;首次使用弹窗里是「设置密码 + 确认」(至少 8 位);
|
||||||
- 登录后点「站点设置」进入 `/admin`:分组管理(树形表格,可建根/子分组、图标可空、排序权重、启停、删除)、内容(选分组以列表查看/编辑条目,每条可单独设格式,主分组含子分组)、侧边栏(侧栏卡片显隐 + 分组导航勾选)、首页卡片(本地服务只读表 + 左侧卡片列表 + 分组表格),以及页头滚动信息、资料与系统设置;
|
- 登录后点「站点设置」进入 `/admin`:分组管理(树形表格,可建根/子分组、图标可空、排序权重、启停、删除)、内容(选分组以列表查看/编辑条目,每条可单独设格式,主分组含子分组)、侧边栏(侧栏卡片显隐 + 分组导航勾选)、首页卡片(本地服务只读表 + 左侧卡片列表 + 分组表格,可拖动排序),以及页头滚动信息、资料与系统设置;
|
||||||
- 详情页的 `[编辑]` 只在登录后显示;保存后局部刷新,`updated_at` 乐观锁;
|
- 详情页的 `[编辑]` 只在登录后显示;保存后局部刷新,`updated_at` 乐观锁;
|
||||||
- `/admin` 的「系统」页可以改密码、下载整库备份(`VACUUM INTO` 快照);
|
- `/admin` 的「系统」页可以改密码、下载整库备份(`VACUUM INTO` 快照);
|
||||||
- 密码用 argon2id 哈希存在 `data/stash.db` 的 `admin` 表,会话是 HttpOnly + SameSite=Strict 的 Cookie(7 天);
|
- 密码用 argon2id 哈希存在 `data/stash.db` 的 `admin` 表,会话是 HttpOnly + SameSite=Strict 的 Cookie(7 天);
|
||||||
|
|||||||
@@ -147,6 +147,13 @@ DELETE /api/items/:id
|
|||||||
|
|
||||||
## 11. 实施记录
|
## 11. 实施记录
|
||||||
|
|
||||||
|
### P18(2026-10-31,搜索框下滚动、本地服务排序、拖拽末位修复)
|
||||||
|
|
||||||
|
- 前台恢复「内容区内部滚动」:页头与搜索框固定,滚动区域从搜索框下方开始;侧栏不再独立滚动,内容区滚动时同步设置侧栏 `scrollTop`,滚轮在侧栏上也驱动内容区;
|
||||||
|
- 本地服务卡片正式进入 `card` 表(v22,key=`local-service`,sort=-1),后台卡片列表可拖动排序,首页按顺序渲染;后端禁止删除、改名、配置分组;
|
||||||
|
- 修复拖拽排序:向下拖时插到目标之后,倒数第二项可以拖到最后一项;
|
||||||
|
- 验证:搜索框滚动前后位置不变;内容滚动 300px 时侧栏 `scrollTop` 跟随(受可滚高度限制);本地服务拖到末位、倒数第二拖到末位均落库;首页顺序与后台一致;删除本地服务返回 400。
|
||||||
|
|
||||||
### P17(2026-10-31,侧栏卡片显隐、本地服务卡片、同步滚动)
|
### P17(2026-10-31,侧栏卡片显隐、本地服务卡片、同步滚动)
|
||||||
|
|
||||||
- 侧边栏设置顶部新增「侧栏卡片」:本地管理员 / 本地服务各一个开关,即时保存到 `settings.sidebar.show_profile / show_service`;与下方分组导航分区显示;
|
- 侧边栏设置顶部新增「侧栏卡片」:本地管理员 / 本地服务各一个开关,即时保存到 `settings.sidebar.show_profile / show_service`;与下方分组导航分区显示;
|
||||||
|
|||||||
+31
-5
@@ -117,16 +117,23 @@ pub fn create(conn: &mut Connection, new: NewCard) -> Result<i64, StashError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(conn: &mut Connection, id: i64, patch: UpdateCard) -> Result<(), StashError> {
|
pub fn update(conn: &mut Connection, id: i64, patch: UpdateCard) -> Result<(), StashError> {
|
||||||
let current: Option<(String, i64)> = conn
|
let current: Option<(String, String, i64)> = conn
|
||||||
.query_row("SELECT title, sort FROM card WHERE id = ?1", [id], |row| {
|
.query_row(
|
||||||
Ok((row.get(0)?, row.get(1)?))
|
"SELECT key, title, sort FROM card WHERE id = ?1",
|
||||||
})
|
[id],
|
||||||
|
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
|
||||||
|
)
|
||||||
.optional()
|
.optional()
|
||||||
.map_err(map_error)?;
|
.map_err(map_error)?;
|
||||||
|
|
||||||
let Some((current_title, current_sort)) = current else {
|
let Some((current_key, current_title, current_sort)) = current else {
|
||||||
return Err(StashError::NotFound(format!("卡片不存在: {id}")));
|
return Err(StashError::NotFound(format!("卡片不存在: {id}")));
|
||||||
};
|
};
|
||||||
|
if current_key == "local-service" && patch.title.is_some() {
|
||||||
|
return Err(StashError::InvalidDirectory(
|
||||||
|
"本地服务卡片由系统生成,不能改名".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let title = match patch.title {
|
let title = match patch.title {
|
||||||
Some(title) => {
|
Some(title) => {
|
||||||
@@ -152,6 +159,15 @@ pub fn update(conn: &mut Connection, id: i64, patch: UpdateCard) -> Result<(), S
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(conn: &Connection, id: i64) -> Result<(), StashError> {
|
pub fn remove(conn: &Connection, id: i64) -> Result<(), StashError> {
|
||||||
|
let key: Option<String> = conn
|
||||||
|
.query_row("SELECT key FROM card WHERE id = ?1", [id], |row| row.get(0))
|
||||||
|
.optional()
|
||||||
|
.map_err(map_error)?;
|
||||||
|
if key.as_deref() == Some("local-service") {
|
||||||
|
return Err(StashError::InvalidDirectory(
|
||||||
|
"本地服务卡片由系统生成,不能删除".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
let removed = conn
|
let removed = conn
|
||||||
.execute("DELETE FROM card WHERE id = ?1", [id])
|
.execute("DELETE FROM card WHERE id = ?1", [id])
|
||||||
.map_err(map_error)?;
|
.map_err(map_error)?;
|
||||||
@@ -168,6 +184,16 @@ pub fn set_groups(
|
|||||||
id: i64,
|
id: i64,
|
||||||
groups: Vec<CardGroupInput>,
|
groups: Vec<CardGroupInput>,
|
||||||
) -> Result<(), StashError> {
|
) -> Result<(), StashError> {
|
||||||
|
let key: Option<String> = conn
|
||||||
|
.query_row("SELECT key FROM card WHERE id = ?1", [id], |row| row.get(0))
|
||||||
|
.optional()
|
||||||
|
.map_err(map_error)?;
|
||||||
|
if key.as_deref() == Some("local-service") {
|
||||||
|
return Err(StashError::InvalidDirectory(
|
||||||
|
"本地服务卡片由系统生成,不能配置分组".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let exists: i64 = conn
|
let exists: i64 = conn
|
||||||
.query_row("SELECT COUNT(*) FROM card WHERE id = ?1", [id], |row| row.get(0))
|
.query_row("SELECT COUNT(*) FROM card WHERE id = ?1", [id], |row| row.get(0))
|
||||||
.map_err(map_error)?;
|
.map_err(map_error)?;
|
||||||
|
|||||||
+14
-1
@@ -182,6 +182,9 @@ FROM section_block b
|
|||||||
JOIN node n ON n.key = b.node_key
|
JOIN node n ON n.key = b.node_key
|
||||||
LEFT JOIN node p ON p.key = n.parent_key
|
LEFT JOIN node p ON p.key = n.parent_key
|
||||||
JOIN card c ON c.key = 'card-' || COALESCE(p.key, n.key);
|
JOIN card c ON c.key = 'card-' || COALESCE(p.key, n.key);
|
||||||
|
|
||||||
|
-- 本地服务是运行时卡片,但用一行 card 记录它的位置,参与首页排序
|
||||||
|
INSERT OR IGNORE INTO card (key, title, sort) VALUES ('local-service', '本地服务', -1);
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
/// v16 → v17:条目加 `updated_at`(乐观锁),新增审计表。
|
/// v16 → v17:条目加 `updated_at`(乐观锁),新增审计表。
|
||||||
@@ -218,6 +221,11 @@ const V20_SQL: &str = r#"
|
|||||||
ALTER TABLE node ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1;
|
ALTER TABLE node ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1;
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
/// v21 → v22:本地服务卡片入库,用 card.sort 参与首页排序(内容仍来自运行时项目)。
|
||||||
|
const V22_SQL: &str = r#"
|
||||||
|
INSERT OR IGNORE INTO card (key, title, sort) VALUES ('local-service', '本地服务', -1);
|
||||||
|
"#;
|
||||||
|
|
||||||
/// v20 → v21:格式从分组下沉到条目;老数据按所属根分组的 view_mode 回填。
|
/// v20 → v21:格式从分组下沉到条目;老数据按所属根分组的 view_mode 回填。
|
||||||
const V21_SQL: &str = r#"
|
const V21_SQL: &str = r#"
|
||||||
ALTER TABLE entry ADD COLUMN format TEXT NOT NULL DEFAULT 'apps';
|
ALTER TABLE entry ADD COLUMN format TEXT NOT NULL DEFAULT 'apps';
|
||||||
@@ -294,7 +302,7 @@ pub struct SeedReport {
|
|||||||
///
|
///
|
||||||
/// 从 v16 起有了第一套迁移:`migrate` 会把老库逐级升上来,
|
/// 从 v16 起有了第一套迁移:`migrate` 会把老库逐级升上来,
|
||||||
/// 只有「比程序更新」或「没有迁移路径」的版本才拒绝启动。
|
/// 只有「比程序更新」或「没有迁移路径」的版本才拒绝启动。
|
||||||
pub const SCHEMA_VERSION: i64 = 21;
|
pub const SCHEMA_VERSION: i64 = 22;
|
||||||
|
|
||||||
fn current_version(conn: &Connection) -> Result<i64, StashError> {
|
fn current_version(conn: &Connection) -> Result<i64, StashError> {
|
||||||
conn.pragma_query_value(None, "user_version", |row| row.get(0))
|
conn.pragma_query_value(None, "user_version", |row| row.get(0))
|
||||||
@@ -359,6 +367,11 @@ pub fn migrate(conn: &Connection) -> Result<i64, StashError> {
|
|||||||
version = 21;
|
version = 21;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if version == 21 {
|
||||||
|
conn.execute_batch(V22_SQL).map_err(map_error)?;
|
||||||
|
version = 22;
|
||||||
|
}
|
||||||
|
|
||||||
conn.pragma_update(None, "user_version", version)
|
conn.pragma_update(None, "user_version", version)
|
||||||
.map_err(map_error)?;
|
.map_err(map_error)?;
|
||||||
|
|
||||||
|
|||||||
@@ -205,6 +205,39 @@ export default function AppNext() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const content = document.getElementById('content-scroll')
|
||||||
|
const sider = document.querySelector('aside')
|
||||||
|
if (!(content instanceof HTMLElement) || !(sider instanceof HTMLElement)) return
|
||||||
|
|
||||||
|
// 左右两栏共用一个滚动位置:侧栏没有独立滚动条,滚轮也驱动内容区
|
||||||
|
sider.id = 'sidebar-scroll'
|
||||||
|
sider.style.overflow = 'hidden'
|
||||||
|
let frame = 0
|
||||||
|
const sync = () => {
|
||||||
|
if (frame !== 0) return
|
||||||
|
frame = window.requestAnimationFrame(() => {
|
||||||
|
frame = 0
|
||||||
|
sider.scrollTop = content.scrollTop
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const onWheel = (event: WheelEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
content.scrollTop += event.deltaY
|
||||||
|
}
|
||||||
|
|
||||||
|
content.addEventListener('scroll', sync, { passive: true })
|
||||||
|
sider.addEventListener('wheel', onWheel, { passive: false })
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
content.removeEventListener('scroll', sync)
|
||||||
|
sider.removeEventListener('wheel', onWheel)
|
||||||
|
if (frame !== 0) window.cancelAnimationFrame(frame)
|
||||||
|
sider.id = ''
|
||||||
|
sider.style.overflow = ''
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const site: SiteSettings = settings?.settings ?? {
|
const site: SiteSettings = settings?.settings ?? {
|
||||||
header: { enabled: true, speed: 24, items: [] },
|
header: { enabled: true, speed: 24, items: [] },
|
||||||
sidebar: {
|
sidebar: {
|
||||||
@@ -311,7 +344,7 @@ export default function AppNext() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLocalService() {
|
function renderLocalService(card: CardItem) {
|
||||||
const items: AppItem[] = projects.map((project) => ({
|
const items: AppItem[] = projects.map((project) => ({
|
||||||
id: project.name,
|
id: project.name,
|
||||||
name: project.name,
|
name: project.name,
|
||||||
@@ -325,7 +358,7 @@ export default function AppNext() {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCard title="本地服务" count={items.length} barColor="#3858f6">
|
<SectionCard key={card.id} title="本地服务" count={items.length} barColor="#3858f6">
|
||||||
{items.length > 0 ? (
|
{items.length > 0 ? (
|
||||||
<AppGrid items={items} />
|
<AppGrid items={items} />
|
||||||
) : (
|
) : (
|
||||||
@@ -367,8 +400,9 @@ export default function AppNext() {
|
|||||||
<div className="home-view">
|
<div className="home-view">
|
||||||
{loading && <Empty text="正在读取内容库…" />}
|
{loading && <Empty text="正在读取内容库…" />}
|
||||||
{!loading && cards.length === 0 && <Empty text="后台还没有创建首页卡片" />}
|
{!loading && cards.length === 0 && <Empty text="后台还没有创建首页卡片" />}
|
||||||
{renderLocalService()}
|
{cards.map((card) =>
|
||||||
{cards.map(renderCard)}
|
card.key === 'local-service' ? renderLocalService(card) : renderCard(card),
|
||||||
|
)}
|
||||||
{keywordLower !== '' && cards.length > 0 && (
|
{keywordLower !== '' && cards.length > 0 && (
|
||||||
<Notice title={`搜索结果:${keyword.trim()}`}>在卡片与分组内按标题过滤。</Notice>
|
<Notice title={`搜索结果:${keyword.trim()}`}>在卡片与分组内按标题过滤。</Notice>
|
||||||
)}
|
)}
|
||||||
@@ -547,6 +581,9 @@ export default function AppNext() {
|
|||||||
siderBordered={false}
|
siderBordered={false}
|
||||||
contentPadding="none"
|
contentPadding="none"
|
||||||
contentOffset={88}
|
contentOffset={88}
|
||||||
|
height="screen"
|
||||||
|
scrollable
|
||||||
|
hideScrollbar
|
||||||
contentId="content-scroll"
|
contentId="content-scroll"
|
||||||
className="app"
|
className="app"
|
||||||
header={
|
header={
|
||||||
|
|||||||
@@ -1289,7 +1289,6 @@ function CardsPanel({
|
|||||||
}) {
|
}) {
|
||||||
const options = useMemo(() => flattenCardsGroups(groups), [groups])
|
const options = useMemo(() => flattenCardsGroups(groups), [groups])
|
||||||
const [activeId, setActiveId] = useState<number | null>(cards[0]?.id ?? null)
|
const [activeId, setActiveId] = useState<number | null>(cards[0]?.id ?? null)
|
||||||
const [view, setView] = useState<'local' | 'card'>('local')
|
|
||||||
const [pendingId, setPendingId] = useState<number | null>(null)
|
const [pendingId, setPendingId] = useState<number | null>(null)
|
||||||
const [newOpen, setNewOpen] = useState(false)
|
const [newOpen, setNewOpen] = useState(false)
|
||||||
const [newTitle, setNewTitle] = useState('')
|
const [newTitle, setNewTitle] = useState('')
|
||||||
@@ -1305,7 +1304,6 @@ function CardsPanel({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (pendingId !== null && cards.some((card) => card.id === pendingId)) {
|
if (pendingId !== null && cards.some((card) => card.id === pendingId)) {
|
||||||
setView('card')
|
|
||||||
setActiveId(pendingId)
|
setActiveId(pendingId)
|
||||||
setPendingId(null)
|
setPendingId(null)
|
||||||
return
|
return
|
||||||
@@ -1338,12 +1336,16 @@ function CardsPanel({
|
|||||||
setOverId(null)
|
setOverId(null)
|
||||||
if (from === null || from === targetId) return
|
if (from === null || from === targetId) return
|
||||||
|
|
||||||
const ids = cards.map((card) => card.id)
|
const original = cards.map((card) => card.id)
|
||||||
const start = ids.indexOf(from)
|
const fromIndex = original.indexOf(from)
|
||||||
if (start < 0) return
|
const targetIndex = original.indexOf(targetId)
|
||||||
ids.splice(start, 1)
|
if (fromIndex < 0 || targetIndex < 0) return
|
||||||
const to = ids.indexOf(targetId)
|
|
||||||
ids.splice(to, 0, from)
|
const ids = original.filter((id) => id !== from)
|
||||||
|
const targetAfter = ids.indexOf(targetId)
|
||||||
|
// 向下拖时插到目标之后,保证能拖到列表最后一项;向上拖时插到目标之前
|
||||||
|
const insertAt = fromIndex < targetIndex ? targetAfter + 1 : targetAfter
|
||||||
|
ids.splice(insertAt, 0, from)
|
||||||
|
|
||||||
setReordering(true)
|
setReordering(true)
|
||||||
const results = await Promise.all(ids.map((id, index) => updateCard(id, { sort: index })))
|
const results = await Promise.all(ids.map((id, index) => updateCard(id, { sort: index })))
|
||||||
@@ -1378,15 +1380,6 @@ function CardsPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="cards-side-list">
|
<div className="cards-side-list">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`cards-side-item${view === 'local' ? ' is-active' : ''}`}
|
|
||||||
onClick={() => setView('local')}
|
|
||||||
>
|
|
||||||
<span className="cards-side-title">本地服务</span>
|
|
||||||
<span className="cards-side-count">{projects.length}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{cards.map((card) => (
|
{cards.map((card) => (
|
||||||
<button
|
<button
|
||||||
key={card.id}
|
key={card.id}
|
||||||
@@ -1394,16 +1387,13 @@ function CardsPanel({
|
|||||||
draggable={!reordering}
|
draggable={!reordering}
|
||||||
className={[
|
className={[
|
||||||
'cards-side-item',
|
'cards-side-item',
|
||||||
view === 'card' && active?.id === card.id ? 'is-active' : '',
|
active?.id === card.id ? 'is-active' : '',
|
||||||
dragId === card.id ? 'is-dragging' : '',
|
dragId === card.id ? 'is-dragging' : '',
|
||||||
overId === card.id && dragId !== null && dragId !== card.id ? 'is-over' : '',
|
overId === card.id && dragId !== null && dragId !== card.id ? 'is-over' : '',
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
onClick={() => {
|
onClick={() => setActiveId(card.id)}
|
||||||
setView('card')
|
|
||||||
setActiveId(card.id)
|
|
||||||
}}
|
|
||||||
onDragStart={(event) => {
|
onDragStart={(event) => {
|
||||||
setDragId(card.id)
|
setDragId(card.id)
|
||||||
event.dataTransfer.effectAllowed = 'move'
|
event.dataTransfer.effectAllowed = 'move'
|
||||||
@@ -1426,7 +1416,9 @@ function CardsPanel({
|
|||||||
⋮⋮
|
⋮⋮
|
||||||
</span>
|
</span>
|
||||||
<span className="cards-side-title">{card.title}</span>
|
<span className="cards-side-title">{card.title}</span>
|
||||||
<span className="cards-side-count">{card.groups.length}</span>
|
<span className="cards-side-count">
|
||||||
|
{card.key === 'local-service' ? projects.length : card.groups.length}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
{cards.length === 0 && (
|
{cards.length === 0 && (
|
||||||
@@ -1444,10 +1436,10 @@ function CardsPanel({
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div className="cards-main">
|
<div className="cards-main">
|
||||||
{view === 'local' ? (
|
{active === undefined ? (
|
||||||
<LocalServiceTable projects={projects} />
|
|
||||||
) : active === undefined ? (
|
|
||||||
<Text tone="muted">新建一张卡片开始配置。</Text>
|
<Text tone="muted">新建一张卡片开始配置。</Text>
|
||||||
|
) : active.key === 'local-service' ? (
|
||||||
|
<LocalServiceTable projects={projects} />
|
||||||
) : (
|
) : (
|
||||||
<CardTable
|
<CardTable
|
||||||
key={active.id}
|
key={active.id}
|
||||||
|
|||||||
Reference in New Issue
Block a user