refactor&fix: 重构布局约束:

- 命名统一化,更有条理;
- 改用grid布局;
- 修复之前出现的宽度不一致、该缩不缩等问题;
This commit is contained in:
Ted 2026-06-23 03:53:41 +08:00
parent e2d54868a7
commit 589f696331
12 changed files with 150 additions and 144 deletions

View file

@ -1,26 +0,0 @@
import * as React from "react";
interface MasonryLayoutProps {
children: React.ReactNode[];
columns: number;
}
export const MasonryLayout = ({ children, columns }: MasonryLayoutProps) => {
const getColumns = (items: React.ReactNode[], colCount: number) => {
const cols: React.ReactNode[][] = Array.from({ length: colCount }, () => []);
items.forEach((item, idx) => {
cols[idx % colCount].push(item);
});
return cols;
};
const columnsData = getColumns(children, columns);
return (
<>
{columnsData.map((col, idx) => (
<div className="main-cards-column" key={idx}>{col}</div>
))}
</>
);
};

View file

@ -5,31 +5,38 @@ import { useCombineStore } from 'src/store';
export const useDashboardLayout = <T extends HTMLElement | null>(containerRef: RefObject<T>) => {
const cardsColumns = useCombineStore((state) => state.settings.cardsColumns);
const [showSidebar, setShowSidebar] = useState<'normal' | 'hide' | 'show'>(Platform.isMobile ? 'hide' : 'normal');
const [colCount, setColCount] = useState(1);
const [colCount, setColCount] = useState<'small' | 'one' | 'two'>('one');
const updateLayout = useCallback((width?: number) => {
if (Platform.isMobile) {
setShowSidebar('hide');
setColCount(1);
setColCount('small');
return;
}
const containerWidth = width ?? containerRef.current?.clientWidth ?? 0;
let containerWidth = width ?? containerRef.current?.clientWidth ?? 0;
if (containerWidth === 0) return;
containerWidth -= 16; // both padding 8
const siderbar = 296;
const padding = 6;
const card = 620;
const cardGap = 16;
const _showSidebar = containerWidth >= 920 ? 'normal' : 'hide'; // 920 是试验效果得来的
const _showSidebar = containerWidth >= (siderbar + padding + card) ? 'normal' : 'hide';
setShowSidebar(_showSidebar);
if (cardsColumns == 1) {
setColCount(1);
if (containerWidth < card) {
setColCount('small');
return;
}
if (cardsColumns == 1) {
setColCount('one');
return;
}
const mainWidth = containerWidth - (_showSidebar == 'normal' ? 400 : 0);
const cardWidth = 620;
const cardsPadding = 24;
const widthFor2Cols = cardWidth + cardsPadding + cardWidth;
const cnt = mainWidth >= widthFor2Cols ? 2 : 1;
const cnt = containerWidth >= (siderbar + padding + card + cardGap + card) ? 'two' : 'one';
setColCount(cnt);
}, [cardsColumns, containerRef]);

View file

@ -7,24 +7,19 @@ import CardNote from "./cards/CardNote";
import { Icon } from "src/components/Icon";
import Sidebar from "./sidebar/Sidebar";
import { DefaultFilterSchemeID } from "src/models/FilterScheme";
import { SidebarContent } from "./sidebar/SideBarContent";
import { HeaderView } from "./header/HeaderView";
import { ActiveFiltersView } from "./header/ActiveFiltersView";
import EmptyStateCard from "./cards/EmptyStateCard";
import { ViewSelectModal } from "./sidebar/viewScheme/ViewSelectModal";
import { useDashboardLayout } from 'src/hooks/useDashboardLayout';
import { useInfiniteScroll } from 'src/hooks/useInfiniteScroll';
import AddNoteView from "./header/AddNoteView";
import { i18n } from "src/utils/i18n";
import { useCombineStore, createDashboardLocalStore, DashboardStoreContext, useDashboardLocalStore } from "src/store";
import { SortFilesButton } from "./header/SortFilesButton";
import { MasonryLayout } from "src/components/MasonryLayout";
import { FileInfo } from "src/models/FileInfo";
export const CARD_DASHBOARD_VIEW_TYPE = "dashboard-view";
export class CardDashboard extends ItemView {
@ -168,55 +163,54 @@ const CardDashboardInner = ({ plugin }: { plugin: BanyanPlugin }) => {
modal.open();
};
const cardNodes = useMemo(() => displayFiles.map((f, index) => {
const cardNotes = useMemo(() => displayFiles.map((f, index) => {
const isLastCard = index === displayFiles.length - 1;
const isPinned = curScheme.pinned.includes(f.file.path);
return (
<div ref={isLastCard ? lastCardElementRef : null} key={f.file.path}>
<div ref={isLastCard ? lastCardElementRef : null} key={f.file.path} className={`card-note ${colCount == 'small' ? 'small' : ''}`}>
<CardNote fileInfo={f} isPinned={isPinned} editable={!Platform.isMobile && useCardNote2} />
</div>
);
}), [displayFiles, curScheme.pinned, lastCardElementRef, useCardNote2]);
}), [displayFiles, curScheme.pinned, lastCardElementRef, useCardNote2, colCount]);
const isNotEmpty = displayFiles.length > 0;
return (
<div className="dashboard-container" ref={dashboardRef}>
{showSidebar != 'normal' && <Sidebar visible={showSidebar == 'show'} onClose={() => setShowSidebar('hide')}><SidebarContent /></Sidebar>}
{showSidebar == 'normal' && <SidebarContent />}
<div className="main-container" ref={mainContainerRef}>
<HeaderView
showSidebar={showSidebar}
setShowSidebar={setShowSidebar}
curScheme={curScheme}
filterSchemes={filterSchemes}
setCurScheme={setCurScheme}
/>
{!Platform.isMobile && <div className="main-header-add-note-container"><AddNoteView app={app} plugin={plugin} onAdd={handleRefresh} /></div>}
<div className="main-subheader-container">
<div className="main-subheader-info">
<div className="refresh-btn" onClick={handleRefresh}>
<Icon name="refresh-ccw" />
<div className="dashboard" ref={dashboardRef}>
<Sidebar showSidebar={showSidebar} onClose={() => setShowSidebar('hide')} />
<div className="main" ref={mainContainerRef}>
<div className="header">
<HeaderView
showSidebar={showSidebar}
setShowSidebar={setShowSidebar}
curScheme={curScheme}
filterSchemes={filterSchemes}
setCurScheme={setCurScheme}
/>
{!Platform.isMobile && <div className="header-main-add-note-container"><AddNoteView app={app} plugin={plugin} onAdd={handleRefresh} /></div>}
<div className="header-sub-container">
<div className="header-sub-info">
<div className="refresh-btn" onClick={handleRefresh}>
<Icon name="refresh-ccw" />
</div>
<span className="header-sub-loaded-notes">{i18n.t('loaded_notes', { count: `${displayFiles.length}`, total: `${curSchemeNotesLength}` })}</span>
{isNotEmpty && !randomBrowse && <SortFilesButton />}
</div>
<div className="header-sub-btn-section">
{enableViewSchemes && curScheme.type != 'ViewScheme' && curScheme.id != DefaultFilterSchemeID && isNotEmpty && <button className="clickable-icon batch-add-button" onClick={handleBatchImportToView}>{i18n.t('batch_add_to_view')}</button>}
</div>
<span className="main-subheader-loaded-notes">{i18n.t('loaded_notes', { count: `${displayFiles.length}`, total: `${curSchemeNotesLength}` })}</span>
{cardNodes.length > 0 && !randomBrowse && <SortFilesButton />}
</div>
<div className="main-subheader-btn-section">
{enableViewSchemes && curScheme.type != 'ViewScheme' && curScheme.id != DefaultFilterSchemeID && cardNodes.length > 0 && <button className="clickable-icon batch-add-button" onClick={handleBatchImportToView}>{i18n.t('batch_add_to_view')}</button>}
</div>
<ActiveFiltersView curScheme={curScheme} colCount={colCount == 'two' ? 2 : 1} />
</div>
<ActiveFiltersView curScheme={curScheme} colCount={colCount} />
<div className="main-cards">
{cardNodes.length === 0 ? (
<div className={`cards ${colCount}`}>
{isNotEmpty ?
cardNotes :
<EmptyStateCard isSearch={curScheme.type == 'FilterScheme' && curScheme.id !== DefaultFilterSchemeID} />
) : (
<MasonryLayout columns={colCount}>
{cardNodes}
</MasonryLayout>
)}
}
</div>
{/* Add loading and end-of-list indicators here */}
<div className="main-cards-loading">
<div className="cards-loading">
{isLoading && <div>{i18n.t('loading_text')}</div>}
{!isLoading && displayFiles.length >= curSchemeNotesLength && cardNodes.length > 0 && <div>{i18n.t('reached_bottom')}</div>}
{!isLoading && displayFiles.length >= curSchemeNotesLength && isNotEmpty && <div>{i18n.t('reached_bottom')}</div>}
</div>
{/* 手机端悬浮添加按钮 */}
{Platform.isMobile && (

View file

@ -1,13 +1,24 @@
.card-note {
display: flex;
width: 620px;
}
.card-note.small {
width: 100%;
}
.card-note-container {
background: var(--background-secondary);
color: var(--text-normal);
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
position: relative;
box-shadow: 0 1px 6px 1px rgba(0, 0, 0, 0.16);
width: 620px;
}
.card-note.small .card-note-container {
width: 100%;
max-width: 620px;
}
.card-note-content {

View file

@ -8,7 +8,6 @@
border-radius: 16px;
box-shadow: 0 1px 6px 1px rgba(0, 0, 0, 0.16);
padding: 48px 24px;
max-width: 620px;
width: 100%;
}

View file

@ -1,42 +1,55 @@
/* --- 整个面板 --- */
.dashboard-container {
.dashboard {
display: flex;
flex-direction: row;
justify-content: center;
height: 100%;
overflow: hidden;
width: 100%;
max-width: 1590px; /* 16 + 296 + 6 + 620 + 16 + 620 + 16 */
margin: 0 auto;
}
/* --- 主内容区 --- */
.main-container {
.main {
width: fit-content;
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
scrollbar-width: none;
}
.main-container::-webkit-scrollbar {
.main::-webkit-scrollbar {
display: none;
}
/* 卡片内容区 */
.cards {
display: grid;
justify-content: start;
gap: 16px;
margin: 6px 4px 0 4px;
}
.cards.small {
grid-template-columns: 1fr;
}
.cards.one {
grid-template-columns: 620px;
max-width: 620px;
}
.cards.two {
grid-template-columns: repeat(2, 620px);
}
/* 桌面设备 */
@media (min-width: 768px) {
.main-container {
max-width: 100%;
min-width: 620px;
}
.main-cards {
display: flex;
gap: 16px;
flex: 1;
margin: 6px 0 0 4px;
}
.main-cards-column {
width: 100%;
min-width: 620px;
}
}
/* 移动设备 */
@ -45,19 +58,9 @@
.workspace-leaf-content[data-type="dashboard-view"] .view-content {
padding-bottom: 0 !important;
}
.main-container {
width: 100%;
}
.main-cards-column {
width: 100%;
min-width: calc(100vw - 24px);
padding: 0 4px;
}
}
.main-cards-loading {
.cards-loading {
text-align: center;
padding: 20px;
color: var(--text-muted);

View file

@ -1,15 +1,16 @@
import React from 'react';
import { Icon } from "src/components/Icon";
import { Searchbar } from "./searchbar/Searchbar";
import { DefaultFilterSchemeID, getDefaultFilterScheme } from "src/models/FilterScheme";
import { DefaultFilterSchemeID, FilterScheme, getDefaultFilterScheme } from "src/models/FilterScheme";
import { i18n } from "src/utils/i18n";
import { ViewScheme } from 'src/models/ViewScheme';
interface HeaderViewProps {
showSidebar: 'normal' | 'hide' | 'show';
setShowSidebar: (state: 'normal' | 'hide' | 'show') => void;
curScheme: any;
filterSchemes: any[];
setCurScheme: (scheme: any) => void;
curScheme: FilterScheme | ViewScheme;
filterSchemes: FilterScheme[];
setCurScheme: (scheme: FilterScheme | ViewScheme) => void;
}
export const HeaderView: React.FC<HeaderViewProps> = ({
@ -20,8 +21,8 @@ export const HeaderView: React.FC<HeaderViewProps> = ({
setCurScheme
}) => {
return (
<div className="main-header-container">
<div className="main-header-title">
<div className="header-main">
<div className="header-main-title">
<button
className={"clickable-icon " + (showSidebar == 'normal' ? "sidebar-toggle-button-hidden" : "sidebar-toggle-button-visible")}
onClick={() => setShowSidebar('show')}
@ -30,20 +31,20 @@ export const HeaderView: React.FC<HeaderViewProps> = ({
<Icon name="menu" />
</button>
{curScheme.id === DefaultFilterSchemeID && (
<div className="main-header-title-content">{curScheme.name}</div>
<div className="header-main-title-content">{curScheme.name}</div>
)}
{curScheme.id !== DefaultFilterSchemeID && (
<div className="main-header-title-container">
<div className="header-main-title-container">
<div
className="main-header-title-content main-header-title-content-clickable"
className="header-main-title-content header-main-title-content-clickable"
onClick={() => {
setCurScheme(getDefaultFilterScheme(filterSchemes));
}}
>
{getDefaultFilterScheme(filterSchemes).name}
</div>
<div className="main-header-title-separator">{'/'}</div>
<div className="main-header-title-content">{curScheme.name}</div>
<div className="header-main-title-separator">{'/'}</div>
<div className="header-main-title-content">{curScheme.name}</div>
</div>
)}
</div>

View file

@ -9,7 +9,6 @@
position: relative;
box-shadow: 0 1px 6px 1px rgba(0, 0, 0, 0.16);
width: 100%;
max-width: 620px;
border: 1px solid transparent;
}
@ -90,6 +89,8 @@
.add-note-send-button {
padding: 6px 15px;
width: 48px;
align-self: end;
}
.add-note-send-button:hover {

View file

@ -1,4 +1,10 @@
.main-header-container {
.header {
display: flex;
flex-direction: column;
width: 100%;
}
.header-main {
display: flex;
flex-direction: row;
justify-content: space-between;
@ -14,62 +20,61 @@
}
/* 主标题 */
.main-header-title {
.header-main-title {
display: flex;
align-items: center;
}
.main-header-title-container {
.header-main-title-container {
display: flex;
}
.main-header-add-note-container {
margin-top: 16px;
margin: 4px;
.header-main-add-note-container {
margin: 4px 4px 0 4px;
}
.main-header-title-container {
.header-main-title-container {
display: flex;
}
.main-header-title-content {
.header-main-title-content {
padding: 6px 8px;
color: var(--text-normal);
font-size: var(--font-text-size);
font-weight: var(--font-ui-semibold);
}
.main-header-title-separator {
.header-main-title-separator {
padding: 6px 4px;
color: var(--text-faint);
font-size: var(--font-small);
font-weight: var(--font-ui-semibold);
}
.main-header-title-content-clickable {
.header-main-title-content-clickable {
cursor: pointer;
}
.main-header-title-content-clickable:hover {
.header-main-title-content-clickable:hover {
background: var(--background-secondary);
border-radius: 6px;
}
/* 副标题 */
.main-subheader-container {
.header-sub-container {
margin-top: 0;
display: flex;
align-items: baseline;
justify-content: space-between;
}
.main-subheader-info {
.header-sub-info {
display: flex;
align-items: center;
}
.main-subheader-info .refresh-btn {
.header-sub-info .refresh-btn {
background-color: transparent;
padding: var(--size-2-2) var(--size-2-3);
cursor: pointer;
@ -77,14 +82,14 @@
}
@media (hover: hover) {
.main-subheader-info .refresh-btn:hover {
.header-sub-info .refresh-btn:hover {
background-color: var(--theme-color-translucent-015);
color: var(--icon-color-hover);
}
}
@media (hover: none) {
.main-subheader-info .refresh-btn:active {
.header-sub-info .refresh-btn:active {
background-color: var(--theme-color-translucent-015);
color: var(--icon-color-hover);
}
@ -92,13 +97,13 @@
.main-subheader-loaded-notes {
.header-sub-loaded-notes {
padding: 12px 6px;
color: var(--text-muted);
font-size: var(--font-smaller);
}
.main-subheader-btn-section {
.header-sub-btn-section {
display: flex;
gap: 8px;
}

View file

@ -20,7 +20,7 @@ export const SidebarContent = () => {
}
return (
<div className="sidebar-content-container">
<div className="sidebar-content">
<StatisticsInfo />
<Heatmap onCickDate={handleClickDate} />
<div className="sidebar-margin-top" />

View file

@ -1,13 +1,24 @@
import { useEffect } from "react";
import { SidebarContent } from "./SideBarContent";
interface SidebarProps {
showSidebar: 'normal' | 'hide' | 'show';
onClose: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ showSidebar, onClose }) => {
if (showSidebar == 'normal') return <SidebarContent />;
return <SidebarContainer visible={showSidebar == 'show'} onClose={onClose}><SidebarContent /></SidebarContainer>;
}
interface SidebarContainerProps {
visible: boolean;
onClose: () => void;
children?: React.ReactNode;
}
// 侧边栏组件,支持响应式隐藏与展开
const Sidebar: React.FC<SidebarProps> = ({ visible, onClose, children }) => {
const SidebarContainer: React.FC<SidebarContainerProps> = ({ visible, onClose, children }) => {
// 监听点击遮罩关闭
useEffect(() => {
if (!visible) return;

View file

@ -12,8 +12,8 @@
.sidebar-container {
position: absolute;
height: 100%;
background: var(--background-secondary, #222);
color: var(--text-normal, #fff);
background: var(--background-secondary);
color: var(--text-normal);
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.08);
z-index: 1001;
transition: left 0.25s cubic-bezier(.4, 0, .2, 1);
@ -43,7 +43,7 @@
/* 侧边栏内容 */
.sidebar-content-container {
.sidebar-content {
display: flex;
flex-direction: column;
padding: 0 16px 50px 16px;
@ -55,7 +55,7 @@
scrollbar-width: none;
}
.sidebar-content-container::-webkit-scrollbar {
.sidebar-content::-webkit-scrollbar {
display: none;
}