Merge branch 'master' of https://gitea.51mars.com/pqc/new-demo
This commit is contained in:
commit
c37ba84ea3
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import { Viewer } from '@bytemd/react';
|
||||
import gfm from '@bytemd/plugin-gfm';
|
||||
import breaks from '@bytemd/plugin-breaks';
|
||||
import frontmatter from '@bytemd/plugin-frontmatter';
|
||||
import gemoji from '@bytemd/plugin-gemoji';
|
||||
import highlight from '@bytemd/plugin-highlight';
|
||||
import 'katex/dist/katex.css';
|
||||
import 'highlight.js/styles/default.css';
|
||||
import math from '@bytemd/plugin-math';
|
||||
import mediumZoom from '@bytemd/plugin-medium-zoom';
|
||||
import mermaid from '@bytemd/plugin-mermaid';
|
||||
// 引入基础css
|
||||
import 'bytemd/dist/index.min.css';
|
||||
import "./style.css"
|
||||
|
||||
const plugins = [
|
||||
gfm(),
|
||||
breaks(),
|
||||
frontmatter(),
|
||||
gemoji(),
|
||||
highlight(),
|
||||
math(),
|
||||
mediumZoom(),
|
||||
mermaid(),
|
||||
];
|
||||
|
||||
|
||||
const MdViewer = ({ md }: { md: string }) => {
|
||||
return (
|
||||
<Viewer
|
||||
value={md}
|
||||
plugins={plugins}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default MdViewer;
|
||||
|
|
@ -1,3 +1,11 @@
|
|||
.bytemd {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,25 +2,119 @@ import React from 'react';
|
|||
import Essays from './essays';
|
||||
import { useHomeContext } from './context/homeContext';
|
||||
import EssaysByLabel from './essays/byLabel';
|
||||
import Styles from './styles.module.less';
|
||||
import LabelList from './labels';
|
||||
import LabelsByCategory from './labels/byCategory';
|
||||
import useFeatures from '@project/hooks/useFeatures';
|
||||
import { DoubleRightOutlined } from '@ant-design/icons';
|
||||
|
||||
const FrontendBody = () => {
|
||||
const { selectedLabelId, selectedCategoryId, searchParam } =
|
||||
useHomeContext();
|
||||
const {
|
||||
selectedLabelId,
|
||||
selectedCategoryId,
|
||||
searchParam,
|
||||
setSearchParam,
|
||||
setSelectedCategoryId,
|
||||
setSelectedLabelId,
|
||||
} = useHomeContext();
|
||||
|
||||
const features = useFeatures();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!selectedLabelId ? (
|
||||
<Essays
|
||||
oakPath='essays'
|
||||
categoryId={selectedCategoryId!}
|
||||
searchParam={searchParam}
|
||||
></Essays>
|
||||
) : (
|
||||
<EssaysByLabel
|
||||
oakPath='essaysByLabel'
|
||||
oakId={selectedLabelId!}
|
||||
></EssaysByLabel>
|
||||
)}
|
||||
<div className={Styles.body}>
|
||||
<div className={Styles.left}>
|
||||
<div className={Styles.labels}>
|
||||
{!selectedCategoryId ? (
|
||||
<LabelList
|
||||
oakPath='labels'
|
||||
labelClassName={Styles.label}
|
||||
></LabelList>
|
||||
) : (
|
||||
<LabelsByCategory
|
||||
oakPath='labelsByCategory'
|
||||
oakId={selectedCategoryId!}
|
||||
labelClassName={Styles.label}
|
||||
></LabelsByCategory>
|
||||
)}
|
||||
<div className={Styles.blank}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={Styles.right}>
|
||||
<div className={Styles.topFilters}>
|
||||
{/* 如果什么都没筛选 */}
|
||||
{!selectedCategoryId &&
|
||||
!selectedLabelId &&
|
||||
!searchParam.searchText && (
|
||||
<div className={Styles.all}>
|
||||
{features.locales.t('allessay')}
|
||||
<DoubleRightOutlined />
|
||||
</div>
|
||||
)}
|
||||
{selectedCategoryId && (
|
||||
<div
|
||||
className={Styles.category}
|
||||
onClick={() => {
|
||||
if (selectedLabelId) {
|
||||
setSelectedLabelId(null);
|
||||
return;
|
||||
}
|
||||
setSelectedCategoryId(null);
|
||||
}}
|
||||
>
|
||||
{features.locales.t('essay:attr.category')}
|
||||
<DoubleRightOutlined />
|
||||
</div>
|
||||
)}
|
||||
{selectedLabelId && (
|
||||
<div
|
||||
className={Styles.label}
|
||||
onClick={() => {
|
||||
setSelectedLabelId(null);
|
||||
}}
|
||||
>
|
||||
{features.locales.t('label:name')}
|
||||
<DoubleRightOutlined />
|
||||
</div>
|
||||
)}
|
||||
{searchParam.searchText && (
|
||||
<div
|
||||
className={Styles.search}
|
||||
onClick={() => {
|
||||
setSearchParam({
|
||||
searchText: '',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className={Styles.content}>
|
||||
{/* 类型 */}
|
||||
<div className={Styles.type}>
|
||||
{features.locales.t(searchParam.searchType)}
|
||||
</div>
|
||||
{/* 关键字 */}
|
||||
<div className={Styles.keyword}>
|
||||
{searchParam.searchText}
|
||||
</div>
|
||||
</div>
|
||||
<DoubleRightOutlined />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={Styles.content}>
|
||||
{!selectedLabelId ? (
|
||||
<Essays
|
||||
oakPath='essays'
|
||||
categoryId={selectedCategoryId!}
|
||||
searchParam={searchParam}
|
||||
></Essays>
|
||||
) : (
|
||||
<EssaysByLabel
|
||||
oakPath='essaysByLabel'
|
||||
oakId={selectedLabelId!}
|
||||
categoryId={selectedCategoryId!}
|
||||
></EssaysByLabel>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import React from "react";
|
||||
|
||||
const FrontendFooter = () => {
|
||||
return <h1>Footer</h1>;
|
||||
}
|
||||
|
||||
export default FrontendFooter;
|
||||
|
|
@ -1,34 +1,13 @@
|
|||
import React from 'react';
|
||||
import LabelList from './labels';
|
||||
import Styles from './styles.module.less';
|
||||
import CategoryList from './categories';
|
||||
import { useHomeContext } from './context/homeContext';
|
||||
import LabelsByCategory from './labels/byCategory';
|
||||
|
||||
const FrontendHeader = () => {
|
||||
const { selectedCategoryId } = useHomeContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={Styles.categories}>
|
||||
<CategoryList oakPath='categories'></CategoryList>
|
||||
</div>
|
||||
<div className={Styles.labels}>
|
||||
{!selectedCategoryId ? (
|
||||
<LabelList
|
||||
oakPath='labels'
|
||||
labelClassName={Styles.label}
|
||||
></LabelList>
|
||||
) : (
|
||||
<LabelsByCategory
|
||||
oakPath='labelsByCategory'
|
||||
oakId={selectedCategoryId!}
|
||||
labelClassName={Styles.label}
|
||||
></LabelsByCategory>
|
||||
)}
|
||||
<div className={Styles.blank}></div>
|
||||
</div>
|
||||
</>
|
||||
<div className={Styles.categories}>
|
||||
<CategoryList oakPath='categories'></CategoryList>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,22 +5,16 @@ import Styles from './styles.module.less';
|
|||
|
||||
const CatrgoryItem = (props: {
|
||||
item: RowWithActions<EntityDict, 'category'>;
|
||||
navigateTo: (params: { url: string; oakId: string }) => void;
|
||||
t: (key: string) => string;
|
||||
onClick?: () => void;
|
||||
selected?: boolean;
|
||||
}) => {
|
||||
const { item, navigateTo, t, selected } = props;
|
||||
const { item, selected } = props;
|
||||
return (
|
||||
<div
|
||||
className={Styles.category}
|
||||
className={`${Styles.category} ${selected ? Styles.selected : ''}`}
|
||||
onClick={() => {
|
||||
props.onClick && props.onClick();
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: selected ? '#f0f0f0' : 'white',
|
||||
border: selected ? '1px solid #f0f0f0' : '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,4 +8,17 @@
|
|||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
height: 20px;
|
||||
border: 1px solid #e8e8e8;
|
||||
min-width: 50px;
|
||||
|
||||
&:hover {
|
||||
background: #e6f3ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
background: #57aeff;
|
||||
color: white;
|
||||
border: 1px solid #aed8ff;
|
||||
}
|
||||
|
|
@ -8,12 +8,7 @@
|
|||
|
||||
.title {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
margin-bottom: 10px;
|
||||
font-size: 20px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||
import { EntityDict } from '@project/oak-app-domain';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import { useHomeContext } from '../context/homeContext';
|
||||
import { Empty } from 'antd';
|
||||
import CatrgoryItem from './item';
|
||||
import Styles from './styles.module.less';
|
||||
|
||||
|
|
@ -17,22 +16,25 @@ const CategoryList = (
|
|||
>
|
||||
) => {
|
||||
const { list } = props.data;
|
||||
const { t, navigateTo } = props.methods;
|
||||
const { t } = props.methods;
|
||||
const { selectedCategoryId, setSelectedCategoryId } = useHomeContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
{list && list.length ? (
|
||||
<div className={Styles.list}>
|
||||
<div className={Styles.title}>
|
||||
<div
|
||||
className={Styles.title}
|
||||
onClick={() => {
|
||||
setSelectedCategoryId(null);
|
||||
}}
|
||||
>
|
||||
{t('categories')} ({list.length})
|
||||
</div>
|
||||
{list.map((item) => {
|
||||
return (
|
||||
<CatrgoryItem
|
||||
item={item}
|
||||
navigateTo={navigateTo}
|
||||
t={t}
|
||||
key={item.id}
|
||||
onClick={() => {
|
||||
if (selectedCategoryId === item.id) {
|
||||
|
|
@ -47,7 +49,6 @@ const CategoryList = (
|
|||
})}
|
||||
</div>
|
||||
) : (
|
||||
// <Empty description={t('common::noData')} />
|
||||
<div>{t('noCategaries')}</div>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { createContext } from 'react';
|
||||
|
||||
type SearchType = 'title' | 'content';
|
||||
export type SearchType = 'title' | 'content';
|
||||
|
||||
type HomeContextType = {
|
||||
// label selection
|
||||
|
|
@ -11,6 +11,8 @@ type HomeContextType = {
|
|||
selectedCategoryId: string | undefined;
|
||||
setSelectedCategoryId: (categoryId: string | null) => void;
|
||||
// search
|
||||
showSeachInput: boolean;
|
||||
setShowSeachInput: (show: boolean) => void;
|
||||
searchParam: {
|
||||
searchType: SearchType;
|
||||
searchText: string;
|
||||
|
|
@ -57,6 +59,14 @@ export const HomeProvider: React.FC<{
|
|||
};
|
||||
|
||||
// search
|
||||
const [showSeachInput, _setShowSeachInput] = React.useState(false);
|
||||
const setShowSeachInput = (show: boolean) => {
|
||||
!show &&
|
||||
setSearchParam({
|
||||
searchText: '',
|
||||
});
|
||||
_setShowSeachInput(show);
|
||||
};
|
||||
const [searchParam, _setSearchParam] = React.useState<{
|
||||
searchType: SearchType;
|
||||
searchText: string;
|
||||
|
|
@ -87,6 +97,8 @@ export const HomeProvider: React.FC<{
|
|||
setSelectedLabelId,
|
||||
selectedCategoryId,
|
||||
setSelectedCategoryId,
|
||||
showSeachInput,
|
||||
setShowSeachInput,
|
||||
searchParam,
|
||||
setSearchParam,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ export default OakComponent({
|
|||
},
|
||||
},
|
||||
formData({ data }) {
|
||||
const categoryId = this.props.categoryId;
|
||||
|
||||
return {
|
||||
list: data.essayLabels$label
|
||||
?.map((item) => {
|
||||
|
|
@ -39,7 +41,11 @@ export default OakComponent({
|
|||
...item,
|
||||
cover: '',
|
||||
};
|
||||
}),
|
||||
})
|
||||
// 只能在这里做筛选,不知道有没有更好的做法
|
||||
.filter(
|
||||
(item) => !categoryId || item.categoryId === categoryId
|
||||
),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
|||
import Styles from './styles.module.less';
|
||||
import dayjs from 'dayjs';
|
||||
import useFeatures from '@project/hooks/useFeatures';
|
||||
import { HighlightWords } from '@project/utils/dom-utils';
|
||||
import { SearchType } from '../../context/homeContext';
|
||||
|
||||
const EssayItem = (props: {
|
||||
item: RowWithActions<EntityDict, 'essay'> & {
|
||||
|
|
@ -11,8 +13,12 @@ const EssayItem = (props: {
|
|||
};
|
||||
navigateTo: (params: { url: string; oakId: string }) => void;
|
||||
t: (key: string) => string;
|
||||
searchParam?: {
|
||||
searchType: SearchType;
|
||||
searchText: string;
|
||||
};
|
||||
}) => {
|
||||
const { item, navigateTo, t } = props;
|
||||
const { item, navigateTo, t, searchParam } = props;
|
||||
const features = useFeatures();
|
||||
// features.extraFile.
|
||||
|
||||
|
|
@ -39,8 +45,34 @@ const EssayItem = (props: {
|
|||
)}
|
||||
</div>
|
||||
<div className={Styles.body}>
|
||||
<div className={Styles.title}>{item.title}</div>
|
||||
<div className={Styles.content}>{item.summary}</div>
|
||||
<div className={Styles.title}>
|
||||
{item.isTop && (
|
||||
<div className={Styles.top}>
|
||||
{t('common::top')}
|
||||
</div>
|
||||
)}
|
||||
{searchParam &&
|
||||
searchParam.searchType === 'title' ? (
|
||||
<HighlightWords
|
||||
string={item.title!}
|
||||
words={[searchParam.searchText]}
|
||||
></HighlightWords>
|
||||
) : (
|
||||
item.title
|
||||
)}
|
||||
</div>
|
||||
<div className={Styles.content}>
|
||||
{/* {item.summary} */}
|
||||
{searchParam &&
|
||||
searchParam.searchType === 'content' ? (
|
||||
<HighlightWords
|
||||
string={item.summary!}
|
||||
words={[searchParam.searchText]}
|
||||
></HighlightWords>
|
||||
) : (
|
||||
item.summary
|
||||
)}
|
||||
</div>
|
||||
{/* labels */}
|
||||
<div className={Styles.labels}>
|
||||
{item.essayLabels$essay?.map((essayLabel) => {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,14 @@
|
|||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
|
||||
.top {
|
||||
background-color: rgb(246, 190, 190);
|
||||
padding: 0 5px;
|
||||
margin-right: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { EntityDict } from '@project/oak-app-domain';
|
|||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import EssayItem from './item';
|
||||
import { Empty } from 'antd';
|
||||
import { SearchType } from '../context/homeContext';
|
||||
|
||||
const Essays = (
|
||||
props: WebComponentProps<
|
||||
|
|
@ -13,10 +14,14 @@ const Essays = (
|
|||
list: (RowWithActions<EntityDict, 'essay'> & {
|
||||
cover: string;
|
||||
})[];
|
||||
searchParam: {
|
||||
searchType: SearchType;
|
||||
searchText: string;
|
||||
};
|
||||
}
|
||||
>
|
||||
) => {
|
||||
const { list } = props.data;
|
||||
const { list, searchParam } = props.data;
|
||||
const { t, navigateTo } = props.methods;
|
||||
|
||||
return (
|
||||
|
|
@ -30,6 +35,7 @@ const Essays = (
|
|||
navigateTo={navigateTo}
|
||||
t={t}
|
||||
key={item.id}
|
||||
searchParam={searchParam}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const LabelsByCategory = (
|
|||
<>
|
||||
{list && (
|
||||
<div className={Styles.labels}>
|
||||
<div className={Styles.title}>{t('labelsByCategory')}</div>
|
||||
<div className={Styles.title}>{t('labelsByCategory') + `(${list.length})`}</div>
|
||||
<div className={Styles.list}>
|
||||
<LabelLists
|
||||
list={list}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export default OakComponent({
|
|||
formData({ data }) {
|
||||
return {
|
||||
list: data,
|
||||
count: data.length,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
.labels {
|
||||
width: 100%;
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.list {
|
||||
|
|
|
|||
|
|
@ -12,17 +12,18 @@ const LabelList = (
|
|||
{
|
||||
list: RowWithActions<EntityDict, 'label'>[];
|
||||
labelClassName?: string;
|
||||
count: number;
|
||||
}
|
||||
>
|
||||
) => {
|
||||
const { list, oakFullpath, labelClassName } = props.data;
|
||||
const { list, oakFullpath, labelClassName, count } = props.data;
|
||||
const { t } = props.methods;
|
||||
|
||||
return (
|
||||
<>
|
||||
{list && (
|
||||
<div className={Styles.labels}>
|
||||
<div className={Styles.title}>{t('labelsAll')}</div>
|
||||
<div className={Styles.title}>{t('labelsAll') + `(${count})`}</div>
|
||||
<div className={Styles.list}>
|
||||
<LabelLists
|
||||
list={list as EntityDict['label']['Schema'][]}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"title": "标题包含",
|
||||
"content": "内容包含",
|
||||
"allessay": "全部文章"
|
||||
}
|
||||
|
|
@ -1,21 +1,86 @@
|
|||
.labels {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
.body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 3fr;
|
||||
gap: 20px;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
|
||||
.label {
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
.left {
|
||||
// 最小宽度
|
||||
min-width: 300px;
|
||||
// 圆角阴影半透明
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
height: 100%;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
.labels {
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
overflow: auto;
|
||||
// 圆角边框背景
|
||||
border-radius: 10px;
|
||||
background: rgba(244, 244, 244, 0.8);
|
||||
|
||||
.label {
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.blank {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.blank {
|
||||
flex: 1;
|
||||
.right {
|
||||
// 圆角阴影半透明
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
.topFilters {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.category,
|
||||
.label,
|
||||
.search,
|
||||
.all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
.content {
|
||||
flex-direction: column;
|
||||
.type {
|
||||
font-size: 12px;
|
||||
}
|
||||
.keyword {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -116,6 +116,16 @@ const i18ns: I18n[] = [
|
|||
"tips": "请选择模式"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "aa6631e469935d0fb5c556e96dcc4379",
|
||||
namespace: "new-demo-p-frontend-home",
|
||||
language: "zh-CN",
|
||||
module: "new-demo",
|
||||
position: "src/pages/frontend/home",
|
||||
data: {
|
||||
"pageTitle": "首页"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "fd2b72a085371d814256e3e315beba4b",
|
||||
namespace: "new-demo-p-frontend-login",
|
||||
|
|
@ -232,6 +242,18 @@ const i18ns: I18n[] = [
|
|||
"labelsAll": "全部标签"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "4b10fe11458024730c7c9c6f4115f807",
|
||||
namespace: "new-demo-c-frontend-home",
|
||||
language: "zh-CN",
|
||||
module: "new-demo",
|
||||
position: "src/components/frontend/home",
|
||||
data: {
|
||||
"title": "标题包含",
|
||||
"content": "内容包含",
|
||||
"allessay": "全部文章"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "66807ab52114b7a250380d6a509aea19",
|
||||
namespace: "new-demo-l-common",
|
||||
|
|
@ -289,7 +311,8 @@ const i18ns: I18n[] = [
|
|||
"enter": "请输入",
|
||||
"change": "修改",
|
||||
"finish": "完成",
|
||||
"creator": "创建人"
|
||||
"creator": "创建人",
|
||||
"top": "置顶"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1904,7 +1927,11 @@ const i18ns: I18n[] = [
|
|||
"console": "控制台",
|
||||
"logout": "退出",
|
||||
"close": "关闭",
|
||||
"login": "登录"
|
||||
"login": "登录",
|
||||
"searchPlaceholder": "搜索文章",
|
||||
"title": "标题",
|
||||
"content": "内容",
|
||||
"search": "搜索"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1957,7 +1984,11 @@ const i18ns: I18n[] = [
|
|||
"console": "控制台",
|
||||
"logout": "退出",
|
||||
"close": "关闭",
|
||||
"login": "登录"
|
||||
"login": "登录",
|
||||
"searchPlaceholder": "搜索文章",
|
||||
"title": "标题",
|
||||
"content": "内容",
|
||||
"search": "搜索"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -49,5 +49,6 @@
|
|||
"enter": "请输入",
|
||||
"change": "修改",
|
||||
"finish": "完成",
|
||||
"creator": "创建人"
|
||||
"creator": "创建人",
|
||||
"top": "置顶"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,22 @@ export default OakComponent({
|
|||
isList: false,
|
||||
projection: essayProjection,
|
||||
formData({ data }) {
|
||||
const fileIndex = data?.extraFile$entity?.findIndex(
|
||||
(item) => item.tag1 === 'cover'
|
||||
);
|
||||
if (fileIndex !== undefined && fileIndex > -1) {
|
||||
const url = this.features.extraFile.getUrl(
|
||||
data!.extraFile$entity![fileIndex]
|
||||
);
|
||||
return {
|
||||
item: data,
|
||||
// 获取封面的url地址
|
||||
cover: url,
|
||||
};
|
||||
}
|
||||
return {
|
||||
item: data,
|
||||
cover: '',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
.essay {
|
||||
min-height: calc(100vh - 60px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.top {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
|
||||
.backgroundImage {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// 短边缩放,左右可以留白
|
||||
object-fit: contain;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.backgroundfilter {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
// 模糊效果
|
||||
filter: blur(10px);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 20%;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
// 翻转色
|
||||
mix-blend-mode: difference;
|
||||
padding: 10px;
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(5px);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.noCover {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 40px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
mix-blend-mode: difference;
|
||||
padding: 10px;
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(5px);
|
||||
filter: blur(2px);
|
||||
}
|
||||
}
|
||||
|
||||
.viewer {
|
||||
width: 60%; // 设置宽度为60%,这样左右各留20%
|
||||
min-width: 400px; // 最小宽度300px
|
||||
margin-left: auto; // 自动左边距
|
||||
margin-right: auto; // 自动右边距
|
||||
padding: 20px;
|
||||
// 背景色和边框样式
|
||||
background-color: #ffffff;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
z-index: 3;
|
||||
|
||||
.btns {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.blank {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: #fff;
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
.scrollTopButton {
|
||||
position: fixed;
|
||||
bottom: 100px;
|
||||
right: 60px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: opacity 0.3s;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { EntityDict } from '@project/oak-app-domain';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import React from 'react';
|
||||
import Styles from './styles.module.less';
|
||||
import MdViewer from '@project/components/common/byteMD/MdViewer';
|
||||
import FrontendFooter from '@project/components/frontend/home/FrontendFooter';
|
||||
import { Button } from 'antd';
|
||||
import { VerticalAlignTopOutlined } from '@ant-design/icons';
|
||||
|
||||
const EssayDetails = (
|
||||
props: WebComponentProps<
|
||||
|
|
@ -9,12 +14,83 @@ const EssayDetails = (
|
|||
false,
|
||||
{
|
||||
item: RowWithActions<EntityDict, 'essay'>;
|
||||
cover: string;
|
||||
}
|
||||
>
|
||||
) => {
|
||||
const { item, cover } = props.data;
|
||||
const [showScrollTop, setShowScrollTop] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (window.scrollY > 300) {
|
||||
setShowScrollTop(true);
|
||||
} else {
|
||||
setShowScrollTop(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Essay Details</h1>
|
||||
<div className={Styles.essay}>
|
||||
{item && (
|
||||
<>
|
||||
<div className={Styles.top}>
|
||||
{cover ? (
|
||||
<>
|
||||
<img
|
||||
className={Styles.backgroundfilter}
|
||||
src={cover}
|
||||
alt={item.title}
|
||||
></img>
|
||||
<img
|
||||
className={Styles.backgroundImage}
|
||||
src={cover}
|
||||
alt={item.title}
|
||||
></img>
|
||||
</>
|
||||
) : (
|
||||
<div className={Styles.noCover}>{item.title}</div>
|
||||
)}
|
||||
<div className={Styles.title}>{item.title}</div>
|
||||
</div>
|
||||
{/* viewer */}
|
||||
<div className={Styles.viewer}>
|
||||
{/* 操作按钮,返回 */}
|
||||
<div className={Styles.btns}>
|
||||
<Button
|
||||
className={Styles.back}
|
||||
onClick={() => window.history.back()}
|
||||
>
|
||||
返回
|
||||
</Button>
|
||||
</div>
|
||||
<MdViewer md={item.content!} />
|
||||
</div>
|
||||
<div className={Styles.blank}></div>
|
||||
</>
|
||||
)}
|
||||
<div className={Styles.footer}>
|
||||
<FrontendFooter />
|
||||
</div>
|
||||
{showScrollTop && (
|
||||
<button
|
||||
className={Styles.scrollTopButton}
|
||||
onClick={scrollToTop}
|
||||
>
|
||||
<VerticalAlignTopOutlined />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"pageTitle": "首页"
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
.body {
|
||||
padding-top: 60px;
|
||||
background-color: #f0f2f5;
|
||||
min-height: calc(100vh - 60px);
|
||||
display: flex;
|
||||
|
|
@ -13,9 +12,7 @@
|
|||
.content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
margin-top: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,20 @@ import React from 'react';
|
|||
import Styles from './styles.module.less';
|
||||
|
||||
import FrontendBody from '@project/components/frontend/home/FrontendBody';
|
||||
import { useHomeContext } from '@project/components/frontend/home/context/homeContext';
|
||||
import FrontendFooter from '@project/components/frontend/home/FrontendFooter';
|
||||
|
||||
const Home = () => {
|
||||
const { setShowSeachInput } = useHomeContext();
|
||||
|
||||
// 只有在这个页面的时候才会显示输入框,否则隐藏
|
||||
React.useEffect(() => {
|
||||
setShowSeachInput(true);
|
||||
return () => {
|
||||
setShowSeachInput(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={Styles.body}>
|
||||
<div className={Styles.header}>
|
||||
|
|
@ -14,7 +26,7 @@ const Home = () => {
|
|||
<FrontendBody />
|
||||
</div>
|
||||
<div className={Styles.footer}>
|
||||
<h1>Footer</h1>
|
||||
<FrontendFooter />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import React, { useMemo } from 'react';
|
||||
|
||||
export const HighlightWords = ({
|
||||
string,
|
||||
words,
|
||||
}: {
|
||||
string: string;
|
||||
words: string[];
|
||||
}) => {
|
||||
const highlightedElements = useMemo(() => {
|
||||
const reg = new RegExp(words.join('|'), 'g');
|
||||
const token = string.replace(reg, '#@$&#');
|
||||
const elements = token.split('#').map((x, index) =>
|
||||
x[0] === '@'
|
||||
? React.createElement(
|
||||
'mark',
|
||||
{
|
||||
key: index,
|
||||
className: 'highlight',
|
||||
},
|
||||
x.slice(1)
|
||||
)
|
||||
: x
|
||||
);
|
||||
return elements;
|
||||
}, [string, words]);
|
||||
|
||||
return React.createElement('span', null, ...highlightedElements);
|
||||
};
|
||||
|
|
@ -20,7 +20,7 @@ export default function render() {
|
|||
});
|
||||
}, []);
|
||||
|
||||
const { searchParam, setSearchParam } = useHomeContext();
|
||||
const { searchParam, setSearchParam, showSeachInput } = useHomeContext();
|
||||
|
||||
const selectBefore = (
|
||||
<Select
|
||||
|
|
@ -67,23 +67,27 @@ export default function render() {
|
|||
{features.locales.t('welcome')} {user?.name || user?.nickname}
|
||||
</div> */}
|
||||
</div>
|
||||
<div className={Styles.search} >
|
||||
<div className={Styles.search}>
|
||||
{/* 搜索框 */}
|
||||
<Space.Compact className={Styles.input}>
|
||||
<Input
|
||||
addonBefore={selectBefore}
|
||||
placeholder={features.locales.t('searchPlaceholder')}
|
||||
value={searchParam.searchText}
|
||||
onChange={(e) => {
|
||||
setSearchParam({
|
||||
searchText: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Button type='primary' className={Styles.btn}>
|
||||
{features.locales.t('search')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
{showSeachInput && (
|
||||
<Space.Compact className={Styles.input}>
|
||||
<Input
|
||||
addonBefore={selectBefore}
|
||||
placeholder={features.locales.t(
|
||||
'searchPlaceholder'
|
||||
)}
|
||||
value={searchParam.searchText}
|
||||
onChange={(e) => {
|
||||
setSearchParam({
|
||||
searchText: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Button type='primary' className={Styles.btn}>
|
||||
{features.locales.t('search')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
)}
|
||||
</div>
|
||||
<div className={Styles.right}>
|
||||
{user ? (
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
.header {
|
||||
height: 60px;
|
||||
// background-color: #6661f5;
|
||||
// 背景透明,模糊
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
z-index: 100;
|
||||
|
||||
// header固定在最上方
|
||||
position: fixed;
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
|
||||
.input {
|
||||
width: 30%;
|
||||
min-width: 300px;
|
||||
min-width: 500px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
.body {
|
||||
background-color: #f0f2f5;
|
||||
padding-top: 60px;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
Loading…
Reference in New Issue