Last active: a year ago
import { ReactComponent as EmptyIcon } from '@/projects/editor/assets/images/media-library/empty.svg';
import useScrollLoading from '@/projects/viewer/hooks/useScrollLoading';
import { useAppSelector } from '@/store';
import { floderList } from '@/utils/api/assets-manage';
import {
getMediaList,
getPublicFolder,
getPublicMediaList,
MediaListDatum,
PublicFolderData,
} from '@/utils/fetchers';
import { LoadingOutlined } from '@ant-design/icons';
import { useRequest } from 'ahooks';
import { Segmented } from 'antd';
import { SegmentedValue } from 'antd/lib/segmented';
import {
lazy,
Suspense,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import Scrollbars from 'react-custom-scrollbars-2';
import { useImmer } from 'use-immer';
import { assetsTypeVaild, segmentOptions } from '.';
import * as S from './PersonalLibrary.styles';
const FolderItem = lazy(() => import('./FolderItem'));
const AssetItem = lazy(() => import('./AssetsItem'));
/**
* 个人素材库
*
* 文件夹在素材前方渲染
* 素材为滚动加载
*/
const PersonalLibrary = ({ isPublic }: { isPublic: boolean }) => {
const library = useAppSelector((s) => s.ui.showMediaLibrary);
// 文件夹状态
const [folder, setFolder] = useImmer({
pid: 0,
type: 'all' as const,
// 用于切换公共素材时刷新
reload: 0,
});
const { data: folders, loading: folderLoading } = useRequest(
() => {
return isPublic
? getPublicFolder()
: floderList({ pid: folder.pid, type: folder.type });
},
{
refreshDeps: [folder],
onSuccess: (folders) => {
isPublic && setPublicFolders(folders);
},
}
);
// 当前点击进入的文件夹 面包屑
const [curFolder, setCurFolder] = useImmer<{ name: string; id: number }[]>(
[]
);
// 当前进入的公共素材文件夹
const [publicFolders, setPublicFolders] = useState<
PublicFolderData[] | undefined
>([]);
/**
* 点击文件夹操作
* 返回根目录 进入文件夹 返回上一级
*
* 如果是公共文件夹,则操作略有不同
*/
const handleFolder =
(type: 'root' | 'enter' | 'leave') =>
(f?: { id: number; pid: number; name: string }, i?: number) => {
if (loading || folderLoading) return;
const handleAssets = () => {
if (f == null) throw new Error('Enter a folder without id');
!isPublic &&
setFolder((d) => {
d.pid = f.id;
});
setPage((d) => {
d.page = 1;
if (isPublic) {
d.typeID = f.id;
} else {
d.folderID = f.id;
}
});
cleanup.current = true;
};
const map = {
root: () => {
if (isPublic) {
setPublicFolders(folders);
} else {
setFolder((d) => {
d.pid = 0;
});
}
setCurFolder([]);
},
enter: () => {
if (f == null) throw new Error('Enter a folder without id');
if (isPublic) {
const children = folders?.find((folder) => folder.id === f.id);
setPublicFolders(children?.children);
}
setCurFolder((d) => {
d.push({ name: f.name, id: f.id });
});
},
leave: () => {
if (i == null) throw new Error('Leave folder without index');
if (f == null) throw new Error('Enter a folder without id');
if (isPublic) {
const children = folders?.find((folder) => folder.id === f.id);
setPublicFolders(children?.children);
}
setCurFolder((d) => d.slice(0, i + 1));
},
};
map[type]();
handleAssets();
};
// 素材状态
const [page, setPage] = useImmer<{
pageCount: number;
page: number;
// type: number;
folderID: number | undefined;
materialType: number | undefined;
typeID: number | undefined;
reload: number;
}>({
pageCount: 50,
page: 1,
// type: 1,
// 个人素材文件夹 ID
folderID: undefined,
// 与嵌入内容不同 这里切换的是 material type
// 公共素材没有 material_type
materialType: undefined,
// 公共素材分类(文件夹)ID
typeID: undefined,
reload: 0,
});
// 当前列表,用于滚动加载
const [list, setList] = useImmer<MediaListDatum[]>([]);
// 是否清空滚动
const cleanup = useRef(false);
// 切换公共素材时,清空滚动加载,重制参数
useEffect(() => {
cleanup.current = true;
setPage((d) => {
d.materialType = isPublic ? undefined : materialType.current;
d.folderID = undefined;
d.page = 1;
d.reload++;
});
setFolder((d) => {
d.pid = 0;
d.reload++;
});
setCurFolder([]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isPublic]);
const { data, loading } = useRequest(
() =>
isPublic
? getPublicMediaList({
page: page.page,
page_count: page.pageCount,
type_id: page.typeID,
})
: getMediaList({
page_count: page.pageCount,
page: page.page,
// type: page.type,
floder_id: page.folderID,
material_type: page.materialType,
}),
{
refreshDeps: [page],
onSuccess(data) {
if (typeof data === 'string') return;
// 如果是进入文件夹或从文件夹返回,则清空滚动
if (cleanup.current) {
setList(data.data);
cleanup.current = false;
} else {
setList((d) => [...d, ...data.data]);
}
},
}
);
const [condition, setCondition] = useState({
loading: loading || folderLoading,
string: typeof data === 'string',
len: typeof data !== 'string' && data?.current_page === data?.last_page,
hasData: !list.length,
});
// 等待素材缩略图(列表渲染)加载完成再更新 option
const handleLoad = useCallback(() => {
setCondition({
loading: loading || folderLoading,
string: typeof data === 'string',
len: typeof data !== 'string' && data?.current_page === data?.last_page,
hasData: !list.length,
});
}, [data, folderLoading, list.length, loading, setCondition]);
const { ref } = useScrollLoading(() => {
setPage((d) => {
d.page++;
});
}, condition);
// 切换素材类型,重新素材列表
const materialType = useRef(undefined);
const handleChange = async (v: SegmentedValue) => {
const target = Number(v);
if (!assetsTypeVaild(target)) return;
cleanup.current = true;
const type = target === 0 ? undefined : target;
materialType.current = type;
setPage((d) => {
d.materialType = type;
d.page = 1;
});
setCurrent([]);
};
// 素材选择状态 asset.id
const [current, setCurrent] = useImmer<number | number[]>(
library.multiSelect ? [] : -1
);
const handleSelect = (a: MediaListDatum) => {
setCurrent((d) => {
if (Array.isArray(d)) {
const already = d.findIndex((c) => c === a.id);
if (!~already) {
d.push(a.id);
return;
}
d.splice(already, 1);
} else {
d = a.id;
}
});
};
return (
<S.Body>
{/* 与嵌入内容不同 这里切换的是 material type */}
<Segmented
style={{
overflow: 'hidden',
...(isPublic ? { maxHeight: 0, padding: 0 } : { maxHeight: 32 }),
}}
block
options={segmentOptions}
onChange={handleChange}
/>
<S.Bread>
<S.BreadItem
onClick={() => handleFolder('root')({ id: 0, pid: 0, name: '' })}
>
根目录
</S.BreadItem>
{curFolder.map((f, i) => (
<S.BreadList
key={f.id}
onClick={() => handleFolder('leave')({ ...f, pid: 0 }, i)}
>
<S.Slash>/</S.Slash>
<S.BreadItem>{f.name}</S.BreadItem>
</S.BreadList>
))}
</S.Bread>
<S.Body>
<Scrollbars style={{ flex: 1 }}>
<S.Wrapper>
<Suspense fallback>
{/* 文件夹 */}
{(isPublic ? publicFolders : folders)?.map((f, i) => (
<FolderItem
key={`${f.id}-${i}`}
name={f.name}
onClick={() => handleFolder('enter')(f)}
/>
))}
</Suspense>
<Suspense fallback={<></>}>
{/* 素材 */}
{list.map((a, i) => (
<AssetItem
key={`${a.id}-${i}`}
asset={a}
onLoad={handleLoad}
checked={
Array.isArray(current)
? current.includes(a.id)
: current === a.id
}
onClick={() => handleSelect(a)}
/>
))}
{/* 滚动加载锚点 */}
{loading || folderLoading || (
<div style={{ height: 1 }} ref={ref}></div>
)}
{(loading || folderLoading) && (
<>
<LoadingOutlined />
<div>加载中</div>
</>
)}
</Suspense>
</S.Wrapper>
{list.length ? (
void 0
) : (
<div className="empty">
<EmptyIcon />
<div className="tip">暂无素材</div>
</div>
)}
</Scrollbars>
<S.Loading
load={loading || folderLoading}
onTransitionEnd={(e) =>
loading || ((e.target as HTMLDivElement).style.display = 'none')
}
>
<LoadingOutlined />
<div>加载中</div>
</S.Loading>
</S.Body>
</S.Body>
);
};
export default PersonalLibrary;