mirror of https://github.com/chaitin/PandaWiki.git
Compare commits
No commits in common. "d630567a3c99cdd9be0135c9177432263b2624ed" and "1aa2855e00d237551b9e8c5c9f101a54d49ec5de" have entirely different histories.
d630567a3c
...
1aa2855e00
|
|
@ -385,14 +385,14 @@ const ThemeWrapper = ({ children }: { children: React.ReactNode }) => {
|
|||
const { appPreviewData } = useAppSelector(state => state.config);
|
||||
|
||||
const theme = useMemo(() => {
|
||||
const themeName =
|
||||
appPreviewData?.settings?.web_app_landing_theme?.name || 'blue';
|
||||
return createTheme(
|
||||
// @ts-expect-error themeOptions is not typed
|
||||
{
|
||||
...themeOptions[0],
|
||||
palette:
|
||||
THEME_TO_PALETTE[themeName]?.palette || THEME_TO_PALETTE.blue.palette,
|
||||
THEME_TO_PALETTE[
|
||||
appPreviewData?.settings?.web_app_landing_theme?.name || 'blue'
|
||||
].palette,
|
||||
},
|
||||
...themeOptions.slice(1),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -157,7 +157,6 @@ const ComponentBar = ({
|
|||
ref={setNodeRef}
|
||||
direction={'row'}
|
||||
sx={{
|
||||
flexShrink: 0,
|
||||
cursor: 'not-allowed',
|
||||
height: '40px',
|
||||
borderRadius: '6px',
|
||||
|
|
@ -269,9 +268,7 @@ const ComponentBar = ({
|
|||
<Stack sx={{ pr: '20px', marginTop: '15px' }}>
|
||||
<Select
|
||||
value={
|
||||
THEME_TO_PALETTE[
|
||||
appPreviewData.settings?.web_app_landing_theme?.name || 'blue'
|
||||
]?.value || 'blue'
|
||||
appPreviewData.settings?.web_app_landing_theme?.name || 'blue'
|
||||
}
|
||||
renderValue={value => {
|
||||
return THEME_TO_PALETTE[value]?.label;
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
arrayMove,
|
||||
rectSortingStrategy,
|
||||
SortableContext,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { Stack } from '@mui/material';
|
||||
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
|
||||
import Item, { type ItemType } from './Item';
|
||||
import SortableItem from './SortableItem';
|
||||
|
||||
interface DragListProps {
|
||||
data: ItemType[];
|
||||
onChange: (data: ItemType[]) => void;
|
||||
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
|
||||
|
||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
||||
setActiveId(event.active.id as string);
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (active.id !== over?.id) {
|
||||
const oldIndex = data.findIndex(item => item.id === active.id);
|
||||
const newIndex = data.findIndex(item => item.id === over!.id);
|
||||
const newData = arrayMove(data, oldIndex, newIndex);
|
||||
onChange(newData);
|
||||
}
|
||||
setActiveId(null);
|
||||
},
|
||||
[data, onChange],
|
||||
);
|
||||
|
||||
const handleDragCancel = useCallback(() => {
|
||||
setActiveId(null);
|
||||
}, []);
|
||||
|
||||
const handleRemove = useCallback(
|
||||
(id: string) => {
|
||||
const newData = data.filter(item => item.id !== id);
|
||||
onChange(newData);
|
||||
},
|
||||
[data, onChange],
|
||||
);
|
||||
|
||||
const handleUpdateItem = useCallback(
|
||||
(updatedItem: ItemType) => {
|
||||
const newData = data.map(item =>
|
||||
item.id === updatedItem.id ? updatedItem : item,
|
||||
);
|
||||
onChange(newData);
|
||||
},
|
||||
[data, onChange],
|
||||
);
|
||||
|
||||
if (data.length === 0) return null;
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragCancel={handleDragCancel}
|
||||
>
|
||||
<SortableContext
|
||||
items={data.map(item => item.id)}
|
||||
strategy={rectSortingStrategy}
|
||||
>
|
||||
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
|
||||
{data.map(item => (
|
||||
<SortableItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
item={item}
|
||||
handleRemove={handleRemove}
|
||||
handleUpdateItem={handleUpdateItem}
|
||||
setIsEdit={setIsEdit}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</SortableContext>
|
||||
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
|
||||
{activeId ? (
|
||||
<Item
|
||||
isDragging
|
||||
item={data.find(item => item.id === activeId)!}
|
||||
setIsEdit={setIsEdit}
|
||||
handleUpdateItem={handleUpdateItem}
|
||||
/>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default DragList;
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
import { Box, IconButton, Stack, TextField } from '@mui/material';
|
||||
import { Icon } from '@ctzhian/ui';
|
||||
import UploadFile from '@/components/UploadFile';
|
||||
import {
|
||||
CSSProperties,
|
||||
Dispatch,
|
||||
forwardRef,
|
||||
HTMLAttributes,
|
||||
SetStateAction,
|
||||
} from 'react';
|
||||
|
||||
export type ItemType = {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type ItemProps = Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> & {
|
||||
item: ItemType;
|
||||
withOpacity?: boolean;
|
||||
isDragging?: boolean;
|
||||
dragHandleProps?: React.HTMLAttributes<HTMLDivElement>;
|
||||
handleRemove?: (id: string) => void;
|
||||
handleUpdateItem?: (item: ItemType) => void;
|
||||
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
const Item = forwardRef<HTMLDivElement, ItemProps>(
|
||||
(
|
||||
{
|
||||
item,
|
||||
withOpacity,
|
||||
isDragging,
|
||||
style,
|
||||
dragHandleProps,
|
||||
handleRemove,
|
||||
handleUpdateItem,
|
||||
setIsEdit,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const inlineStyles: CSSProperties = {
|
||||
opacity: withOpacity ? '0.5' : '1',
|
||||
borderRadius: '10px',
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
backgroundColor: '#ffffff',
|
||||
width: '100%',
|
||||
...style,
|
||||
};
|
||||
return (
|
||||
<Box ref={ref} style={inlineStyles} {...props}>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'space-between'}
|
||||
gap={0.5}
|
||||
sx={{
|
||||
py: 1.5,
|
||||
px: 1,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: '10px',
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction={'column'}
|
||||
gap={'20px'}
|
||||
sx={{
|
||||
flex: 1,
|
||||
p: 1.5,
|
||||
}}
|
||||
>
|
||||
<UploadFile
|
||||
name='url'
|
||||
id={`${item.id}_image`}
|
||||
type='url'
|
||||
disabled={false}
|
||||
accept='image/*'
|
||||
width={160}
|
||||
height={140}
|
||||
value={item.url}
|
||||
onChange={(url: string) => {
|
||||
const updatedItem = { ...item, url: url };
|
||||
handleUpdateItem?.(updatedItem);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label='名称'
|
||||
slotProps={{
|
||||
inputLabel: {
|
||||
shrink: true,
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
height: '36px',
|
||||
'& .MuiOutlinedInput-root': {
|
||||
height: '36px',
|
||||
padding: '0 12px',
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: '8px 0',
|
||||
},
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
placeholder='请输入名称'
|
||||
variant='outlined'
|
||||
value={item.name}
|
||||
onChange={e => {
|
||||
const updatedItem = { ...item, name: e.target.value };
|
||||
handleUpdateItem?.(updatedItem);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
direction={'column'}
|
||||
sx={{ justifyContent: 'space-between', alignSelf: 'stretch' }}
|
||||
>
|
||||
<IconButton
|
||||
size='small'
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleRemove?.(item.id);
|
||||
}}
|
||||
sx={{
|
||||
color: 'text.tertiary',
|
||||
':hover': { color: 'error.main' },
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
}}
|
||||
>
|
||||
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size='small'
|
||||
sx={{
|
||||
cursor: 'grab',
|
||||
color: 'text.secondary',
|
||||
'&:hover': { color: 'primary.main' },
|
||||
}}
|
||||
{...(dragHandleProps as any)}
|
||||
>
|
||||
<Icon type='icon-drag' />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default Item;
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { FC } from 'react';
|
||||
import Item, { ItemProps } from './Item';
|
||||
|
||||
type SortableItemProps = ItemProps & {};
|
||||
|
||||
const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
|
||||
const {
|
||||
isDragging,
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
} = useSortable({ id: item.id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition: transition || undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<Item
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
withOpacity={isDragging}
|
||||
dragHandleProps={{
|
||||
...attributes,
|
||||
...listeners,
|
||||
}}
|
||||
item={item}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SortableItem;
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
||||
import { TextField } from '@mui/material';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import DragList from './DragList';
|
||||
import type { ConfigProps } from '../type';
|
||||
import { useAppSelector } from '@/store';
|
||||
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
||||
import { Empty } from '@ctzhian/ui';
|
||||
import { DEFAULT_DATA } from '../../../constants';
|
||||
import { findConfigById, handleLandingConfigs } from '../../../utils';
|
||||
|
||||
const Config = ({ setIsEdit, id }: ConfigProps) => {
|
||||
const { appPreviewData } = useAppSelector(state => state.config);
|
||||
const debouncedDispatch = useDebounceAppPreviewData();
|
||||
const { control, setValue, watch, reset, subscribe } = useForm<
|
||||
typeof DEFAULT_DATA.block_grid
|
||||
>({
|
||||
defaultValues: findConfigById(
|
||||
appPreviewData?.settings?.web_app_landing_configs || [],
|
||||
id,
|
||||
),
|
||||
});
|
||||
|
||||
const list = watch('list') || [];
|
||||
|
||||
const handleAddFeature = () => {
|
||||
const nextId = `${Date.now()}`;
|
||||
setValue('list', [...list, { id: nextId, name: '', url: '' }]);
|
||||
};
|
||||
|
||||
const handleListChange = (
|
||||
newList: (typeof DEFAULT_DATA.block_grid)['list'],
|
||||
) => {
|
||||
setValue('list', newList);
|
||||
setIsEdit(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reset(
|
||||
findConfigById(
|
||||
appPreviewData?.settings?.web_app_landing_configs || [],
|
||||
id,
|
||||
),
|
||||
{ keepDefaultValues: true },
|
||||
);
|
||||
}, [id, appPreviewData]);
|
||||
|
||||
useEffect(() => {
|
||||
const callback = subscribe({
|
||||
formState: {
|
||||
values: true,
|
||||
},
|
||||
callback: ({ values }) => {
|
||||
const previewData = {
|
||||
...appPreviewData,
|
||||
settings: {
|
||||
...appPreviewData?.settings,
|
||||
web_app_landing_configs: handleLandingConfigs({
|
||||
id,
|
||||
config: appPreviewData?.settings?.web_app_landing_configs || [],
|
||||
values,
|
||||
}),
|
||||
},
|
||||
};
|
||||
setIsEdit(true);
|
||||
debouncedDispatch(previewData);
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
callback();
|
||||
};
|
||||
}, [subscribe, id, appPreviewData]);
|
||||
|
||||
return (
|
||||
<StyledCommonWrapper>
|
||||
<CommonItem title='标题'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='title'
|
||||
render={({ field }) => (
|
||||
<TextField label='文字' {...field} placeholder='请输入' />
|
||||
)}
|
||||
/>
|
||||
</CommonItem>
|
||||
<CommonItem title='宫格列表' onAdd={handleAddFeature}>
|
||||
{list.length === 0 ? (
|
||||
<Empty />
|
||||
) : (
|
||||
<DragList
|
||||
data={list}
|
||||
onChange={handleListChange}
|
||||
setIsEdit={setIsEdit}
|
||||
/>
|
||||
)}
|
||||
</CommonItem>
|
||||
</StyledCommonWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Config;
|
||||
|
|
@ -107,7 +107,7 @@ const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
|
|||
)}
|
||||
/>
|
||||
</CommonItem> */}
|
||||
<CommonItem title='链接列表' onAdd={handleAddQuestion}>
|
||||
<CommonItem title='问题列表' onAdd={handleAddQuestion}>
|
||||
{list.length === 0 ? (
|
||||
<Empty />
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
arrayMove,
|
||||
rectSortingStrategy,
|
||||
SortableContext,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { Stack } from '@mui/material';
|
||||
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
|
||||
import Item, { ItemType } from './Item';
|
||||
import SortableItem from './SortableItem';
|
||||
|
||||
interface FaqDragListProps {
|
||||
data: ItemType[];
|
||||
onChange: (data: ItemType[]) => void;
|
||||
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const FaqDragList: FC<FaqDragListProps> = ({ data, onChange, setIsEdit }) => {
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
|
||||
|
||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
||||
setActiveId(event.active.id as string);
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (active.id !== over?.id) {
|
||||
const oldIndex = data.findIndex(item => item.id === active.id);
|
||||
const newIndex = data.findIndex(item => item.id === over!.id);
|
||||
const newData = arrayMove(data, oldIndex, newIndex);
|
||||
onChange(newData);
|
||||
}
|
||||
setActiveId(null);
|
||||
},
|
||||
[data, onChange],
|
||||
);
|
||||
|
||||
const handleDragCancel = useCallback(() => {
|
||||
setActiveId(null);
|
||||
}, []);
|
||||
|
||||
const handleRemove = useCallback(
|
||||
(id: string) => {
|
||||
const newData = data.filter(item => item.id !== id);
|
||||
onChange(newData);
|
||||
},
|
||||
[data, onChange],
|
||||
);
|
||||
|
||||
const handleUpdateItem = useCallback(
|
||||
(updatedItem: ItemType) => {
|
||||
const newData = data.map(item =>
|
||||
item.id === updatedItem.id ? updatedItem : item,
|
||||
);
|
||||
onChange(newData);
|
||||
},
|
||||
[data, onChange],
|
||||
);
|
||||
|
||||
if (data.length === 0) return null;
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragCancel={handleDragCancel}
|
||||
>
|
||||
<SortableContext
|
||||
items={data.map(item => item.id)}
|
||||
strategy={rectSortingStrategy}
|
||||
>
|
||||
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
|
||||
{data.map(item => (
|
||||
<SortableItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
item={item}
|
||||
handleRemove={handleRemove}
|
||||
handleUpdateItem={handleUpdateItem}
|
||||
setIsEdit={setIsEdit}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</SortableContext>
|
||||
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
|
||||
{activeId ? (
|
||||
<Item
|
||||
isDragging
|
||||
item={data.find(item => item.id === activeId)!}
|
||||
setIsEdit={setIsEdit}
|
||||
handleUpdateItem={handleUpdateItem}
|
||||
/>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default FaqDragList;
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
import { Box, IconButton, Stack, TextField } from '@mui/material';
|
||||
import { Icon } from '@ctzhian/ui';
|
||||
import {
|
||||
CSSProperties,
|
||||
Dispatch,
|
||||
forwardRef,
|
||||
HTMLAttributes,
|
||||
SetStateAction,
|
||||
} from 'react';
|
||||
|
||||
export type ItemType = {
|
||||
id: string;
|
||||
question: string;
|
||||
};
|
||||
|
||||
export type ItemProps = Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> & {
|
||||
item: ItemType;
|
||||
withOpacity?: boolean;
|
||||
isDragging?: boolean;
|
||||
dragHandleProps?: React.HTMLAttributes<HTMLDivElement>;
|
||||
handleRemove?: (id: string) => void;
|
||||
handleUpdateItem?: (item: ItemType) => void;
|
||||
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
const Item = forwardRef<HTMLDivElement, ItemProps>(
|
||||
(
|
||||
{
|
||||
item,
|
||||
withOpacity,
|
||||
isDragging,
|
||||
style,
|
||||
dragHandleProps,
|
||||
handleRemove,
|
||||
handleUpdateItem,
|
||||
setIsEdit,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const inlineStyles: CSSProperties = {
|
||||
opacity: withOpacity ? '0.5' : '1',
|
||||
borderRadius: '10px',
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
backgroundColor: '#ffffff',
|
||||
width: '100%',
|
||||
...style,
|
||||
};
|
||||
return (
|
||||
<Box ref={ref} style={inlineStyles} {...props}>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'space-between'}
|
||||
gap={0.5}
|
||||
sx={{
|
||||
py: 1.5,
|
||||
px: 1,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: '10px',
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction={'column'}
|
||||
gap={'20px'}
|
||||
sx={{
|
||||
flex: 1,
|
||||
p: 1.5,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label='问题'
|
||||
slotProps={{
|
||||
inputLabel: {
|
||||
shrink: true,
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
height: '36px',
|
||||
'& .MuiOutlinedInput-root': {
|
||||
height: '36px',
|
||||
padding: '0 12px',
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: '8px 0',
|
||||
},
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
placeholder='请输入问题'
|
||||
variant='outlined'
|
||||
value={item.question}
|
||||
onChange={e => {
|
||||
const updatedItem = { ...item, question: e.target.value };
|
||||
handleUpdateItem?.(updatedItem);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
direction={'column'}
|
||||
sx={{ justifyContent: 'space-between', alignSelf: 'stretch' }}
|
||||
>
|
||||
<IconButton
|
||||
size='small'
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleRemove?.(item.id);
|
||||
}}
|
||||
sx={{
|
||||
color: 'text.tertiary',
|
||||
':hover': { color: 'error.main' },
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
}}
|
||||
>
|
||||
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size='small'
|
||||
sx={{
|
||||
cursor: 'grab',
|
||||
color: 'text.secondary',
|
||||
'&:hover': { color: 'primary.main' },
|
||||
}}
|
||||
{...(dragHandleProps as any)}
|
||||
>
|
||||
<Icon type='icon-drag' />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default Item;
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { FC } from 'react';
|
||||
import Item, { ItemProps } from './Item';
|
||||
|
||||
type SortableItemProps = ItemProps & {};
|
||||
|
||||
const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
|
||||
const {
|
||||
isDragging,
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
} = useSortable({ id: item.id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition: transition || undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<Item
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
withOpacity={isDragging}
|
||||
dragHandleProps={{
|
||||
...attributes,
|
||||
...listeners,
|
||||
}}
|
||||
item={item}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SortableItem;
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
||||
import { TextField } from '@mui/material';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import FaqDragList from './DragList';
|
||||
import type { ConfigProps } from '../type';
|
||||
import { useAppSelector } from '@/store';
|
||||
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
||||
import { Empty } from '@ctzhian/ui';
|
||||
import { DEFAULT_DATA } from '../../../constants';
|
||||
import { findConfigById, handleLandingConfigs } from '../../../utils';
|
||||
|
||||
const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
|
||||
const { appPreviewData } = useAppSelector(state => state.config);
|
||||
const debouncedDispatch = useDebounceAppPreviewData();
|
||||
const { control, setValue, watch, reset, subscribe } = useForm<
|
||||
typeof DEFAULT_DATA.question
|
||||
>({
|
||||
defaultValues: findConfigById(
|
||||
appPreviewData?.settings?.web_app_landing_configs || [],
|
||||
id,
|
||||
),
|
||||
});
|
||||
|
||||
const list = watch('list') || [];
|
||||
|
||||
const handleAddQuestion = () => {
|
||||
const nextId = `${Date.now()}`;
|
||||
setValue('list', [...list, { id: nextId, question: '' }]);
|
||||
};
|
||||
|
||||
const handleListChange = (
|
||||
newList: (typeof DEFAULT_DATA.question)['list'],
|
||||
) => {
|
||||
setValue('list', newList);
|
||||
setIsEdit(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reset(
|
||||
findConfigById(
|
||||
appPreviewData?.settings?.web_app_landing_configs || [],
|
||||
id,
|
||||
),
|
||||
{ keepDefaultValues: true },
|
||||
);
|
||||
}, [id, appPreviewData]);
|
||||
|
||||
useEffect(() => {
|
||||
const callback = subscribe({
|
||||
formState: {
|
||||
values: true,
|
||||
},
|
||||
callback: ({ values }) => {
|
||||
const previewData = {
|
||||
...appPreviewData,
|
||||
settings: {
|
||||
...appPreviewData?.settings,
|
||||
web_app_landing_configs: handleLandingConfigs({
|
||||
id,
|
||||
config: appPreviewData?.settings?.web_app_landing_configs || [],
|
||||
values,
|
||||
}),
|
||||
},
|
||||
};
|
||||
setIsEdit(true);
|
||||
debouncedDispatch(previewData);
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
callback();
|
||||
};
|
||||
}, [subscribe, id, appPreviewData]);
|
||||
|
||||
return (
|
||||
<StyledCommonWrapper>
|
||||
<CommonItem title='标题'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='title'
|
||||
render={({ field }) => (
|
||||
<TextField label='文字' {...field} placeholder='请输入' />
|
||||
)}
|
||||
/>
|
||||
</CommonItem>
|
||||
|
||||
<CommonItem title='常见问题列表' onAdd={handleAddQuestion}>
|
||||
{list.length === 0 ? (
|
||||
<Empty />
|
||||
) : (
|
||||
<FaqDragList
|
||||
data={list}
|
||||
onChange={handleListChange}
|
||||
setIsEdit={setIsEdit}
|
||||
/>
|
||||
)}
|
||||
</CommonItem>
|
||||
</StyledCommonWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default FaqConfig;
|
||||
|
|
@ -5,6 +5,7 @@ import {
|
|||
IconJianyiwendang,
|
||||
IconChangjianwenti,
|
||||
IconLunbotu,
|
||||
IconShanchu,
|
||||
IconDanwenzi,
|
||||
IconShuzikapian,
|
||||
IconKehuanli,
|
||||
|
|
@ -12,8 +13,6 @@ import {
|
|||
IconZuotuyouzi,
|
||||
IconYoutuzuozi,
|
||||
IconKehupingjia,
|
||||
IconJiugongge,
|
||||
IconLianjiezu1,
|
||||
} from '@panda-wiki/icons';
|
||||
import { DomainRecommendNodeListResp } from '@/request/types';
|
||||
|
||||
|
|
@ -71,14 +70,6 @@ export const DEFAULT_DATA = {
|
|||
comment: string;
|
||||
}[],
|
||||
},
|
||||
block_grid: {
|
||||
title: '区块网格',
|
||||
list: [] as {
|
||||
id: string;
|
||||
url: string;
|
||||
name: string;
|
||||
}[],
|
||||
},
|
||||
banner: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
|
|
@ -114,18 +105,11 @@ export const DEFAULT_DATA = {
|
|||
}[],
|
||||
},
|
||||
faq: {
|
||||
title: '链接组',
|
||||
list: [] as {
|
||||
id: string;
|
||||
question: string;
|
||||
link: string;
|
||||
}[],
|
||||
},
|
||||
question: {
|
||||
title: '常见问题',
|
||||
list: [] as {
|
||||
id: string;
|
||||
question: string;
|
||||
link: string;
|
||||
}[],
|
||||
},
|
||||
};
|
||||
|
|
@ -192,7 +176,7 @@ export const COMPONENTS_MAP = {
|
|||
faq: {
|
||||
name: 'faq',
|
||||
title: '链接组',
|
||||
icon: IconLianjiezu1,
|
||||
icon: IconChangjianwenti,
|
||||
component: lazy(() => import('@panda-wiki/ui/faq')),
|
||||
config: lazy(() => import('./components/config/FaqConfig')),
|
||||
fixed: false,
|
||||
|
|
@ -278,26 +262,6 @@ export const COMPONENTS_MAP = {
|
|||
disabled: false,
|
||||
hidden: false,
|
||||
},
|
||||
block_grid: {
|
||||
name: 'block_grid',
|
||||
title: '区块网格',
|
||||
icon: IconJiugongge,
|
||||
component: lazy(() => import('@panda-wiki/ui/blockGrid')),
|
||||
config: lazy(() => import('./components/config/BlockGridConfig')),
|
||||
fixed: false,
|
||||
disabled: false,
|
||||
hidden: false,
|
||||
},
|
||||
question: {
|
||||
name: 'question',
|
||||
title: '常见问题',
|
||||
icon: IconChangjianwenti,
|
||||
component: lazy(() => import('@panda-wiki/ui/question')),
|
||||
config: lazy(() => import('./components/config/QuestionConfig')),
|
||||
fixed: false,
|
||||
disabled: false,
|
||||
hidden: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const TYPE_TO_CONFIG_LABEL = {
|
||||
|
|
@ -314,6 +278,4 @@ export const TYPE_TO_CONFIG_LABEL = {
|
|||
text_img: 'text_img_config',
|
||||
img_text: 'img_text_config',
|
||||
comment: 'comment_config',
|
||||
block_grid: 'block_grid_config',
|
||||
question: 'question_config',
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import { DEFAULT_DATA, TYPE_TO_CONFIG_LABEL } from './constants';
|
||||
import Logo from '@/assets/images/footer-logo.png';
|
||||
|
||||
const handleHeaderProps = (setting: any) => {
|
||||
return {
|
||||
title: setting.title,
|
||||
|
|
@ -156,20 +159,6 @@ const handleCommentProps = (config: any = {}) => {
|
|||
};
|
||||
};
|
||||
|
||||
const handleBlockGridProps = (config: any = {}) => {
|
||||
return {
|
||||
title: config.title || '区块网格',
|
||||
items: config.list || [],
|
||||
};
|
||||
};
|
||||
|
||||
const handleQuestionProps = (config: any = {}) => {
|
||||
return {
|
||||
title: config.title || '常见问题',
|
||||
items: config.list || [],
|
||||
};
|
||||
};
|
||||
|
||||
export const handleComponentProps = (
|
||||
type: string,
|
||||
id: string,
|
||||
|
|
@ -211,10 +200,6 @@ export const handleComponentProps = (
|
|||
return handleTextImgProps(config);
|
||||
case 'comment':
|
||||
return handleCommentProps(config);
|
||||
case 'block_grid':
|
||||
return handleBlockGridProps(config);
|
||||
case 'question':
|
||||
return handleQuestionProps(config);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -493,16 +493,6 @@ export interface DomainBatchMoveReq {
|
|||
parent_id?: string;
|
||||
}
|
||||
|
||||
export interface DomainBlockGridConfig {
|
||||
list?: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
url?: string;
|
||||
}[];
|
||||
title?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface DomainBrandGroup {
|
||||
links?: DomainLink[];
|
||||
name?: string;
|
||||
|
|
@ -1112,15 +1102,6 @@ export interface DomainProviderModelListItem {
|
|||
model?: string;
|
||||
}
|
||||
|
||||
export interface DomainQuestionConfig {
|
||||
list?: {
|
||||
id?: string;
|
||||
question?: string;
|
||||
}[];
|
||||
title?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface DomainRagInfo {
|
||||
message?: string;
|
||||
status?: ConstsNodeRagInfoStatus;
|
||||
|
|
@ -1285,7 +1266,6 @@ export interface DomainWebAppCustomSettings {
|
|||
export interface DomainWebAppLandingConfig {
|
||||
banner_config?: DomainBannerConfig;
|
||||
basic_doc_config?: DomainBasicDocConfig;
|
||||
block_grid_config?: DomainBlockGridConfig;
|
||||
carousel_config?: DomainCarouselConfig;
|
||||
case_config?: DomainCaseConfig;
|
||||
com_config_order?: string[];
|
||||
|
|
@ -1296,7 +1276,6 @@ export interface DomainWebAppLandingConfig {
|
|||
img_text_config?: DomainImgTextConfig;
|
||||
metrics_config?: DomainMetricsConfig;
|
||||
node_ids?: string[];
|
||||
question_config?: DomainQuestionConfig;
|
||||
simple_doc_config?: DomainSimpleDocConfig;
|
||||
text_config?: DomainTextConfig;
|
||||
text_img_config?: DomainTextImgConfig;
|
||||
|
|
@ -1306,7 +1285,6 @@ export interface DomainWebAppLandingConfig {
|
|||
export interface DomainWebAppLandingConfigResp {
|
||||
banner_config?: DomainBannerConfig;
|
||||
basic_doc_config?: DomainBasicDocConfig;
|
||||
block_grid_config?: DomainBlockGridConfig;
|
||||
carousel_config?: DomainCarouselConfig;
|
||||
case_config?: DomainCaseConfig;
|
||||
com_config_order?: string[];
|
||||
|
|
@ -1318,7 +1296,6 @@ export interface DomainWebAppLandingConfigResp {
|
|||
metrics_config?: DomainMetricsConfig;
|
||||
node_ids?: string[];
|
||||
nodes?: DomainRecommendNodeListResp[];
|
||||
question_config?: DomainQuestionConfig;
|
||||
simple_doc_config?: DomainSimpleDocConfig;
|
||||
text_config?: DomainTextConfig;
|
||||
text_img_config?: DomainTextImgConfig;
|
||||
|
|
|
|||
|
|
@ -19,9 +19,7 @@ const HomePage = () => {
|
|||
cssVariables: {
|
||||
cssVarPrefix: 'welcome',
|
||||
},
|
||||
palette:
|
||||
THEME_TO_PALETTE[themeMode]?.palette ||
|
||||
THEME_TO_PALETTE['blue'].palette,
|
||||
palette: THEME_TO_PALETTE[themeMode].palette,
|
||||
typography: {
|
||||
fontFamily: 'var(--font-gilory), PingFang SC, sans-serif',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -493,16 +493,6 @@ export interface DomainBatchMoveReq {
|
|||
parent_id?: string;
|
||||
}
|
||||
|
||||
export interface DomainBlockGridConfig {
|
||||
list?: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
url?: string;
|
||||
}[];
|
||||
title?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface DomainBrandGroup {
|
||||
links?: DomainLink[];
|
||||
name?: string;
|
||||
|
|
@ -1112,15 +1102,6 @@ export interface DomainProviderModelListItem {
|
|||
model?: string;
|
||||
}
|
||||
|
||||
export interface DomainQuestionConfig {
|
||||
list?: {
|
||||
id?: string;
|
||||
question?: string;
|
||||
}[];
|
||||
title?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface DomainRagInfo {
|
||||
message?: string;
|
||||
status?: ConstsNodeRagInfoStatus;
|
||||
|
|
@ -1285,7 +1266,6 @@ export interface DomainWebAppCustomSettings {
|
|||
export interface DomainWebAppLandingConfig {
|
||||
banner_config?: DomainBannerConfig;
|
||||
basic_doc_config?: DomainBasicDocConfig;
|
||||
block_grid_config?: DomainBlockGridConfig;
|
||||
carousel_config?: DomainCarouselConfig;
|
||||
case_config?: DomainCaseConfig;
|
||||
com_config_order?: string[];
|
||||
|
|
@ -1296,7 +1276,6 @@ export interface DomainWebAppLandingConfig {
|
|||
img_text_config?: DomainImgTextConfig;
|
||||
metrics_config?: DomainMetricsConfig;
|
||||
node_ids?: string[];
|
||||
question_config?: DomainQuestionConfig;
|
||||
simple_doc_config?: DomainSimpleDocConfig;
|
||||
text_config?: DomainTextConfig;
|
||||
text_img_config?: DomainTextImgConfig;
|
||||
|
|
@ -1306,7 +1285,6 @@ export interface DomainWebAppLandingConfig {
|
|||
export interface DomainWebAppLandingConfigResp {
|
||||
banner_config?: DomainBannerConfig;
|
||||
basic_doc_config?: DomainBasicDocConfig;
|
||||
block_grid_config?: DomainBlockGridConfig;
|
||||
carousel_config?: DomainCarouselConfig;
|
||||
case_config?: DomainCaseConfig;
|
||||
com_config_order?: string[];
|
||||
|
|
@ -1318,7 +1296,6 @@ export interface DomainWebAppLandingConfigResp {
|
|||
metrics_config?: DomainMetricsConfig;
|
||||
node_ids?: string[];
|
||||
nodes?: DomainRecommendNodeListResp[];
|
||||
question_config?: DomainQuestionConfig;
|
||||
simple_doc_config?: DomainSimpleDocConfig;
|
||||
text_config?: DomainTextConfig;
|
||||
text_img_config?: DomainTextImgConfig;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,42 @@
|
|||
'use client';
|
||||
|
||||
import { Banner } from '@panda-wiki/ui';
|
||||
import dynamic from 'next/dynamic';
|
||||
import {
|
||||
Banner,
|
||||
Faq,
|
||||
BasicDoc,
|
||||
DirDoc,
|
||||
SimpleDoc,
|
||||
Carousel,
|
||||
Text,
|
||||
Case,
|
||||
Metrics,
|
||||
Feature,
|
||||
ImgText,
|
||||
Comment,
|
||||
} from '@panda-wiki/ui';
|
||||
import { DomainRecommendNodeListResp } from '@/request/types';
|
||||
|
||||
import { useStore } from '@/provider';
|
||||
|
||||
const handleHeaderProps = (setting: any) => {
|
||||
return {
|
||||
title: setting.title,
|
||||
logo: setting.icon,
|
||||
btns: setting.btns,
|
||||
placeholder:
|
||||
setting.web_app_custom_style?.header_search_placeholder || '搜索...',
|
||||
};
|
||||
};
|
||||
|
||||
const handleFooterProps = (setting: any) => {
|
||||
return {
|
||||
footerSetting: setting.footer_settings,
|
||||
logo: setting.icon,
|
||||
showBrand: setting.web_app_custom_style?.show_brand_info || false,
|
||||
customStyle: setting.web_app_custom_style,
|
||||
};
|
||||
};
|
||||
|
||||
const handleFaqProps = (config: any = {}) => {
|
||||
return {
|
||||
title: config.title || '链接组',
|
||||
|
|
@ -142,40 +173,20 @@ const handleCommentProps = (config: any = {}) => {
|
|||
};
|
||||
};
|
||||
|
||||
const handleBlockGridProps = (config: any = {}) => {
|
||||
return {
|
||||
title: config.title || '区块网格',
|
||||
items: config.list || [],
|
||||
};
|
||||
};
|
||||
|
||||
const handleQuestionProps = (config: any = {}) => {
|
||||
return {
|
||||
title: config.title || '常见问题',
|
||||
items: config.list || [],
|
||||
};
|
||||
};
|
||||
|
||||
const componentMap = {
|
||||
banner: Banner,
|
||||
basic_doc: dynamic(() => import('@panda-wiki/ui').then(mod => mod.BasicDoc)),
|
||||
dir_doc: dynamic(() => import('@panda-wiki/ui').then(mod => mod.DirDoc)),
|
||||
simple_doc: dynamic(() =>
|
||||
import('@panda-wiki/ui').then(mod => mod.SimpleDoc),
|
||||
),
|
||||
carousel: dynamic(() => import('@panda-wiki/ui').then(mod => mod.Carousel)),
|
||||
faq: dynamic(() => import('@panda-wiki/ui').then(mod => mod.Faq)),
|
||||
text: dynamic(() => import('@panda-wiki/ui').then(mod => mod.Text)),
|
||||
case: dynamic(() => import('@panda-wiki/ui').then(mod => mod.Case)),
|
||||
metrics: dynamic(() => import('@panda-wiki/ui').then(mod => mod.Metrics)),
|
||||
feature: dynamic(() => import('@panda-wiki/ui').then(mod => mod.Feature)),
|
||||
text_img: dynamic(() => import('@panda-wiki/ui').then(mod => mod.ImgText)),
|
||||
img_text: dynamic(() => import('@panda-wiki/ui').then(mod => mod.ImgText)),
|
||||
comment: dynamic(() => import('@panda-wiki/ui').then(mod => mod.Comment)),
|
||||
block_grid: dynamic(() =>
|
||||
import('@panda-wiki/ui').then(mod => mod.BlockGrid),
|
||||
),
|
||||
question: dynamic(() => import('@panda-wiki/ui').then(mod => mod.Question)),
|
||||
basic_doc: BasicDoc,
|
||||
dir_doc: DirDoc,
|
||||
simple_doc: SimpleDoc,
|
||||
carousel: Carousel,
|
||||
faq: Faq,
|
||||
text: Text,
|
||||
case: Case,
|
||||
metrics: Metrics,
|
||||
feature: Feature,
|
||||
text_img: ImgText,
|
||||
img_text: ImgText,
|
||||
comment: Comment,
|
||||
} as const;
|
||||
|
||||
const Welcome = () => {
|
||||
|
|
@ -209,8 +220,6 @@ const Welcome = () => {
|
|||
text_img: 'text_img_config',
|
||||
img_text: 'img_text_config',
|
||||
comment: 'comment_config',
|
||||
block_grid: 'block_grid_config',
|
||||
question: 'question_config',
|
||||
} as const;
|
||||
|
||||
const handleComponentProps = (data: any) => {
|
||||
|
|
@ -234,6 +243,7 @@ const Welcome = () => {
|
|||
return {
|
||||
...handleBannerProps(config),
|
||||
onSearch: onBannerSearch,
|
||||
onQaClick: () => setQaModalOpen?.(true),
|
||||
btns: (config?.btns || []).map((item: any) => ({
|
||||
...item,
|
||||
href: item.href || '/node',
|
||||
|
|
@ -253,15 +263,6 @@ const Welcome = () => {
|
|||
return handleImgTextProps(config);
|
||||
case 'comment':
|
||||
return handleCommentProps(config);
|
||||
case 'block_grid':
|
||||
return handleBlockGridProps(config);
|
||||
case 'question':
|
||||
return {
|
||||
...handleQuestionProps(config),
|
||||
onSearch: (text: string) => {
|
||||
onBannerSearch(text, 'chat');
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
import React from 'react';
|
||||
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
|
||||
|
||||
const IconJiugongge = (props: SvgIconProps) => (
|
||||
<SvgIcon
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 1117 1024'
|
||||
{...props}
|
||||
>
|
||||
<path d='M1061.236364 0H55.854545C22.341818 0 0 22.341818 0 55.854545v893.672728c0 33.512727 22.341818 55.854545 55.854545 55.854545h1005.381819c33.512727 0 55.854545-22.341818 55.854545-55.854545V55.854545c0-33.512727-22.341818-55.854545-55.854545-55.854545zM111.709091 893.672727V111.709091h893.672727v781.963636H111.709091z'></path>
|
||||
<path d='M279.412364 392.424727h159.604363V232.866909H279.412364v159.557818z m199.493818 0h159.557818V232.866909h-159.557818v159.557818z m199.493818-159.557818v159.557818h159.557818V232.866909h-159.557818z m-398.987636 359.098182h159.604363v-159.650909H279.412364v159.650909z m199.493818 0h159.557818v-159.650909h-159.557818v159.650909z m199.493818 0h159.557818v-159.650909h-159.557818v159.650909z m-398.987636 199.447273h159.604363v-159.557819H279.412364v159.557819z m199.493818 0h159.557818v-159.557819h-159.557818v159.557819z m199.493818 0h159.557818v-159.557819h-159.557818v159.557819z'></path>
|
||||
</SvgIcon>
|
||||
);
|
||||
|
||||
IconJiugongge.displayName = 'icon-jiugongge';
|
||||
|
||||
export default IconJiugongge;
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import React from 'react';
|
||||
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
|
||||
|
||||
const IconLianjiezu1 = (props: SvgIconProps) => (
|
||||
<SvgIcon
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 1117 1024'
|
||||
{...props}
|
||||
>
|
||||
<path d='M1061.236364 0H55.854545C22.341818 0 0 22.341818 0 55.854545v893.672728c0 33.512727 22.341818 55.854545 55.854545 55.854545h1005.381819c33.512727 0 55.854545-22.341818 55.854545-55.854545V55.854545c0-33.512727-22.341818-55.854545-55.854545-55.854545zM111.709091 893.672727V111.709091h893.672727v781.963636H111.709091z'></path>
|
||||
<path d='M442.926545 644.096l8.145455 6.283636a174.08 174.08 0 0 0 85.504 35.560728l10.24 1.256727v70.376727l-12.8-1.256727a245.061818 245.061818 0 0 1-130.653091-54.272l-9.914182-8.098909 9.029818-9.122909 33.18691-33.419637 7.26109-7.307636z m230.958546 0l49.803636 49.803636-9.960727 8.145455a244.270545 244.270545 0 0 1-130.653091 54.272l-12.753454 1.256727v-70.376727l10.193454-1.256727a172.357818 172.357818 0 0 0 85.224727-35.514182l8.145455-6.330182z m130.234182-120.087273l-1.256728 12.753455a245.061818 245.061818 0 0 1-54.272 130.653091l-8.145454 9.960727-49.803636-49.803636 6.283636-8.098909c19.362909-24.948364 31.697455-54.225455 35.560727-85.271273l1.256727-10.24h70.376728z m-420.770909-0.232727l1.256727 10.193455c3.863273 31.278545 16.197818 60.509091 35.514182 85.224727l6.330182 8.145454-49.803637 49.803637-8.145454-9.960728a244.270545 244.270545 0 0 1-54.272-130.65309l-1.256728-12.753455h70.376728zM558.545455 372.363636c77.265455 0 139.636364 62.370909 139.636363 139.636364s-62.370909 139.636364-139.636363 139.636364-139.636364-62.370909-139.636364-139.636364 62.370909-139.636364 139.636364-139.636364z m-181.946182-25.460363l9.122909 9.029818 33.419636 33.186909 7.307637 7.261091-6.283637 8.145454a174.08 174.08 0 0 0-35.560727 85.504l-1.256727 10.24H312.971636l1.256728-12.8a244.270545 244.270545 0 0 1 54.272-130.65309l8.098909-9.914182z m363.892363 0l8.098909 9.914182c30.440727 37.236364 49.477818 82.385455 54.272 130.65309l1.256728 12.753455h-70.376728l-1.256727-10.193455a174.08 174.08 0 0 0-35.560727-85.504l-6.283636-8.145454 7.307636-7.261091 33.419636-33.186909 9.122909-9.029818z m-170.170181-80.523637l12.753454 1.303273a244.270545 244.270545 0 0 1 130.653091 54.272l9.914182 8.098909-9.029818 9.122909-33.186909 33.419637-7.261091 7.307636-8.145455-6.283636a174.08 174.08 0 0 0-85.504-35.560728l-10.24-1.256727V266.426182z m-23.552 0v70.423273l-10.193455 1.256727a174.08 174.08 0 0 0-85.504 35.560728l-8.145455 6.283636-7.26109-7.307636-33.18691-33.419637-9.029818-9.122909 9.914182-8.098909a244.270545 244.270545 0 0 1 130.653091-54.272l12.753455-1.256727z'></path>
|
||||
</SvgIcon>
|
||||
);
|
||||
|
||||
IconLianjiezu1.displayName = 'icon-lianjiezu1';
|
||||
|
||||
export default IconLianjiezu1;
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import React from 'react';
|
||||
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
|
||||
|
||||
const IconWenhao = (props: SvgIconProps) => (
|
||||
<SvgIcon
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 1024 1024'
|
||||
{...props}
|
||||
>
|
||||
<path d='M469.3504 768h85.2992v-85.3504H469.3504V768zM512 85.3504A426.8032 426.8032 0 0 0 85.3504 512c0 235.52 191.1296 426.6496 426.6496 426.6496S938.6496 747.52 938.6496 512 747.52 85.3504 512 85.3504z m0 768A341.8112 341.8112 0 0 1 170.6496 512 341.8112 341.8112 0 0 1 512 170.6496 341.8112 341.8112 0 0 1 853.3504 512 341.8112 341.8112 0 0 1 512 853.3504zM512 256a170.5984 170.5984 0 0 0-170.6496 170.6496h85.2992c0-46.8992 38.4-85.2992 85.3504-85.2992s85.3504 38.4 85.3504 85.2992c0 85.3504-128 74.7008-128 213.3504h85.2992c0-96 128-106.6496 128-213.3504A170.5984 170.5984 0 0 0 512 256z'></path>
|
||||
</SvgIcon>
|
||||
);
|
||||
|
||||
IconWenhao.displayName = 'icon-wenhao';
|
||||
|
||||
export default IconWenhao;
|
||||
|
|
@ -109,7 +109,6 @@ export { default as IconJichuwendang } from './IconJichuwendang';
|
|||
export { default as IconJina } from './IconJina';
|
||||
export { default as IconJinggao } from './IconJinggao';
|
||||
export { default as IconJinsousuo } from './IconJinsousuo';
|
||||
export { default as IconJiugongge } from './IconJiugongge';
|
||||
export { default as IconJushou } from './IconJushou';
|
||||
export { default as IconKefu } from './IconKefu';
|
||||
export { default as IconKehuanli } from './IconKehuanli';
|
||||
|
|
@ -121,7 +120,6 @@ export { default as IconLDAP } from './IconLDAP';
|
|||
export { default as IconLanyun } from './IconLanyun';
|
||||
export { default as IconLepton } from './IconLepton';
|
||||
export { default as IconLianjiezu } from './IconLianjiezu';
|
||||
export { default as IconLianjiezu1 } from './IconLianjiezu1';
|
||||
export { default as IconLingyiwanwu } from './IconLingyiwanwu';
|
||||
export { default as IconLmstudio } from './IconLmstudio';
|
||||
export { default as IconLogoGroq } from './IconLogoGroq';
|
||||
|
|
@ -203,7 +201,6 @@ export { default as IconWeibo1 } from './IconWeibo1';
|
|||
export { default as IconWeixingongzhonghao } from './IconWeixingongzhonghao';
|
||||
export { default as IconWeixingongzhonghaoDaiyanse } from './IconWeixingongzhonghaoDaiyanse';
|
||||
export { default as IconWendajiqiren } from './IconWendajiqiren';
|
||||
export { default as IconWenhao } from './IconWenhao';
|
||||
export { default as IconWenjian } from './IconWenjian';
|
||||
export { default as IconWenjianjia } from './IconWenjianjia';
|
||||
export { default as IconWenjianjiaKai } from './IconWenjianjiaKai';
|
||||
|
|
|
|||
|
|
@ -46,11 +46,11 @@ export const THEME_LIST = [
|
|||
value: 'darkDeepForest',
|
||||
palette: darkDeepForestPalette,
|
||||
},
|
||||
// {
|
||||
// label: '深邃黑',
|
||||
// value: 'white',
|
||||
// palette: whitePalette,
|
||||
// },
|
||||
{
|
||||
label: '深邃黑',
|
||||
value: 'white',
|
||||
palette: whitePalette,
|
||||
},
|
||||
{
|
||||
label: '电光蓝',
|
||||
value: 'electricBlue',
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ interface BannerProps {
|
|||
onSearch?: (value: string, type?: 'search' | 'chat') => void;
|
||||
onSearchSuggestions?: (query: string) => Promise<SearchSuggestion[]>;
|
||||
baseUrl?: string;
|
||||
onQaClick?: () => void;
|
||||
}
|
||||
|
||||
const Banner = React.memo(
|
||||
|
|
@ -144,6 +145,7 @@ const Banner = React.memo(
|
|||
onSearch,
|
||||
onSearchSuggestions,
|
||||
baseUrl = '',
|
||||
onQaClick,
|
||||
}: BannerProps) => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [suggestions, setSuggestions] = useState<SearchSuggestion[]>([]);
|
||||
|
|
|
|||
|
|
@ -4,10 +4,7 @@ import React from 'react';
|
|||
import { styled, Grid, Box, alpha } from '@mui/material';
|
||||
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
||||
import IconWenjian from '@panda-wiki/icons/IconWenjian';
|
||||
import {
|
||||
useFadeInText,
|
||||
useCardFadeInAnimation,
|
||||
} from '../hooks/useGsapAnimation';
|
||||
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
||||
|
||||
interface BasicDocProps {
|
||||
mobile?: boolean;
|
||||
|
|
@ -36,13 +33,9 @@ const StyledBasicDocItem = styled('div')(({ theme }) => ({
|
|||
transform: 'translateY(-5px)',
|
||||
boxShadow: `0px 10px 20px 0px ${alpha(theme.palette.text.primary, 0.1)}`,
|
||||
borderColor: theme.palette.primary.main,
|
||||
'.basic-doc-item-title': {
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
},
|
||||
width: '100%',
|
||||
cursor: 'pointer',
|
||||
opacity: 0,
|
||||
}));
|
||||
|
||||
const StyledBasicDocItemTitle = styled('h3')(({ theme }) => ({
|
||||
|
|
@ -81,7 +74,7 @@ const BasicDocItem: React.FC<{
|
|||
baseUrl: string;
|
||||
size: any;
|
||||
}> = React.memo(({ item, index, baseUrl, size }) => {
|
||||
const cardRef = useCardFadeInAnimation(0.2 + index * 0.1, 0.1);
|
||||
const cardRef = useCardAnimation(0.2 + index * 0.1, 0.1);
|
||||
|
||||
return (
|
||||
<Grid size={size} key={index}>
|
||||
|
|
@ -91,7 +84,7 @@ const BasicDocItem: React.FC<{
|
|||
window.open(`${baseUrl}/node/${item.id}`, '_blank');
|
||||
}}
|
||||
>
|
||||
<StyledBasicDocItemTitle className='basic-doc-item-title'>
|
||||
<StyledBasicDocItemTitle>
|
||||
{item.emoji ? (
|
||||
<Box>{item.emoji}</Box>
|
||||
) : (
|
||||
|
|
@ -100,6 +93,16 @@ const BasicDocItem: React.FC<{
|
|||
<StyledBasicDocItemName>{item.name}</StyledBasicDocItemName>
|
||||
</StyledBasicDocItemTitle>
|
||||
<StyledBasicDocItemSummary>{item.summary}</StyledBasicDocItemSummary>
|
||||
<Box
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
alignSelf: 'flex-end',
|
||||
}}
|
||||
>
|
||||
查看更多
|
||||
</Box>
|
||||
</StyledBasicDocItem>
|
||||
</Grid>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { styled, Grid, alpha, Stack } from '@mui/material';
|
||||
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
||||
import {
|
||||
useFadeInText,
|
||||
useCardFadeInAnimation,
|
||||
} from '../hooks/useGsapAnimation';
|
||||
|
||||
interface BlockGridProps {
|
||||
mobile?: boolean;
|
||||
title?: string;
|
||||
items?: {
|
||||
name: string;
|
||||
url: string;
|
||||
}[];
|
||||
}
|
||||
const StyledBlockGridItem = styled(Stack)(({ theme }) => ({
|
||||
aspectRatio: '1 / 1',
|
||||
position: 'relative',
|
||||
border: `1px solid ${alpha(theme.palette.text.primary, 0.15)}`,
|
||||
borderRadius: '10px',
|
||||
padding: theme.spacing(1),
|
||||
boxShadow: `0px 5px 20px 0px ${alpha(theme.palette.text.primary, 0.06)}`,
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
color: theme.palette.primary.main,
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: `0px 10px 20px 0px ${alpha(theme.palette.text.primary, 0.1)}`,
|
||||
},
|
||||
opacity: 0,
|
||||
}));
|
||||
|
||||
export const StyledBlockGridItemImgBox = styled('div')(({ theme }) => ({
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
}));
|
||||
|
||||
export const StyledBlockGridItemImg = styled('img')(({ theme }) => ({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
borderRadius: '10px',
|
||||
}));
|
||||
|
||||
const StyledBlockGridItemTitle = styled('div')(({ theme }) => ({
|
||||
position: 'absolute',
|
||||
bottom: '24px',
|
||||
left: '50%',
|
||||
maxWidth: 'calc(100% - 24px)',
|
||||
transform: 'translateX(-50%)',
|
||||
padding: theme.spacing(0.5, 1),
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
flexShrink: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
fontWeight: 700,
|
||||
color: theme.palette.background.default,
|
||||
backgroundColor: alpha(theme.palette.text.primary, 0.5),
|
||||
borderRadius: '6px',
|
||||
}));
|
||||
|
||||
// 单个卡片组件,带动画效果
|
||||
const BlockGridItem: React.FC<{
|
||||
item: {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
index: number;
|
||||
}> = React.memo(({ item, index }) => {
|
||||
const cardRef = useCardFadeInAnimation(0.2 + index * 0.1, 0.1);
|
||||
return (
|
||||
<StyledBlockGridItem ref={cardRef as React.Ref<HTMLDivElement>} gap={2}>
|
||||
<StyledBlockGridItemImgBox>
|
||||
<StyledBlockGridItemImg src={item.url} />
|
||||
</StyledBlockGridItemImgBox>
|
||||
|
||||
<StyledBlockGridItemTitle>{item.name}</StyledBlockGridItemTitle>
|
||||
</StyledBlockGridItem>
|
||||
);
|
||||
});
|
||||
|
||||
const BlockGrid: React.FC<BlockGridProps> = React.memo(
|
||||
({ title, items = [], mobile }) => {
|
||||
const size =
|
||||
typeof mobile === 'boolean'
|
||||
? mobile
|
||||
? 12
|
||||
: { xs: 12, md: 4 }
|
||||
: { xs: 12, md: 4 };
|
||||
|
||||
// 添加标题淡入动画
|
||||
const titleRef = useFadeInText(0.2, 0.1);
|
||||
|
||||
return (
|
||||
<StyledTopicBox>
|
||||
<StyledTopicTitle ref={titleRef}>{title}</StyledTopicTitle>
|
||||
<Grid container spacing={3} sx={{ width: '100%' }}>
|
||||
{items.map((item, index) => (
|
||||
<Grid size={size} key={index}>
|
||||
<BlockGridItem item={item} index={index} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</StyledTopicBox>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default BlockGrid;
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
.swiper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.swiper-slide {
|
||||
|
|
@ -8,11 +9,26 @@
|
|||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
/* .swiper-slide-prev {
|
||||
transform-origin: center right;
|
||||
opacity: 0.4;
|
||||
} */
|
||||
/* .swiper-slide-next {
|
||||
opacity: 0.4;
|
||||
} */
|
||||
|
||||
.swiper-slide img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* 居中激活项保持原尺寸 */
|
||||
/* .swiper-slide-active {
|
||||
transform: scale(1) translateZ(0) !important;
|
||||
z-index: 1;
|
||||
} */
|
||||
|
|
|
|||
|
|
@ -1,17 +1,9 @@
|
|||
import {
|
||||
CSSProperties,
|
||||
memo,
|
||||
useRef,
|
||||
useCallback,
|
||||
useState,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { styled, alpha, Tabs, Tab, Box, useTheme } from '@mui/material';
|
||||
import { CSSProperties, memo, useRef, useCallback, useState } from 'react';
|
||||
import { styled, alpha, Tabs, Tab, Box } from '@mui/material';
|
||||
import { StyledTopicTitle, StyledTopicBox } from '../component/styledCommon';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { useFadeInText } from '../hooks/useGsapAnimation';
|
||||
import { Swiper as SwiperType } from 'swiper';
|
||||
import { gsap } from 'gsap';
|
||||
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/pagination';
|
||||
|
|
@ -59,33 +51,6 @@ const StyledSwiperSlideImg = styled('img')(({ theme }) => ({
|
|||
borderRadius: '10px',
|
||||
}));
|
||||
|
||||
const StyledSwiperSlideDesc = styled('div')(({ theme }) => ({
|
||||
position: 'absolute',
|
||||
bottom: '24px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
padding: theme.spacing(0.5, 1),
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
color: theme.palette.background.default,
|
||||
borderRadius: '6px',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
zIndex: 0,
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: alpha(theme.palette.text.primary, 0.5),
|
||||
filter: 'blur(6px)',
|
||||
borderRadius: '12px',
|
||||
zIndex: -1,
|
||||
},
|
||||
}));
|
||||
|
||||
// 样式化的 Tabs 容器 - 浅灰色背景,圆角,阴影
|
||||
const StyledTabsContainer = styled(Box)(({ theme }) => ({
|
||||
maxWidth: '100%',
|
||||
|
|
@ -113,7 +78,7 @@ const StyledTabs = styled(Tabs)(({ theme }) => ({
|
|||
const StyledTab = styled(Tab)(({ theme }) => ({
|
||||
minHeight: 'auto',
|
||||
padding: theme.spacing(1, 2),
|
||||
borderRadius: '6px',
|
||||
borderRadius: '10px',
|
||||
backgroundColor: theme.palette.background.default,
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 14,
|
||||
|
|
@ -131,16 +96,11 @@ const StyledTab = styled(Tab)(({ theme }) => ({
|
|||
}));
|
||||
|
||||
const Carousel = ({ title, items }: CarouselProps) => {
|
||||
const theme = useTheme();
|
||||
// 添加标题淡入动画
|
||||
const titleRef = useFadeInText(0.2, 0.1);
|
||||
// 添加Swiper ref
|
||||
const swiperRef = useRef<SwiperType | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<string>(items[0]?.id || '');
|
||||
// 存储所有描述元素的 ref
|
||||
const descRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
// 存储动画时间线,用于清理
|
||||
const animationTimelines = useRef<gsap.core.Timeline[]>([]);
|
||||
|
||||
// 导航函数
|
||||
const handlePrev = useCallback(() => {
|
||||
|
|
@ -155,117 +115,7 @@ const Carousel = ({ title, items }: CarouselProps) => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
// 触发从左到右的文字出现动画(逐字符显示,容器逐渐撑大)
|
||||
const animateTextFromLeft = useCallback(
|
||||
(index: number) => {
|
||||
const descElement = descRefs.current[index];
|
||||
if (!descElement) return;
|
||||
|
||||
// 清理之前的动画
|
||||
animationTimelines.current.forEach(tl => tl.kill());
|
||||
animationTimelines.current = [];
|
||||
|
||||
const originalText = descElement.textContent || '';
|
||||
if (!originalText) return;
|
||||
|
||||
// 获取容器的 padding 值
|
||||
const computedStyle = window.getComputedStyle(descElement);
|
||||
const paddingLeft = parseFloat(computedStyle.paddingLeft) || 0;
|
||||
const paddingRight = parseFloat(computedStyle.paddingRight) || 0;
|
||||
const padding = paddingLeft + paddingRight;
|
||||
|
||||
// 将文字分割成字符
|
||||
const chars = Array.from(originalText);
|
||||
const charElements: HTMLSpanElement[] = [];
|
||||
|
||||
// 清空容器并创建字符元素(初始都隐藏)
|
||||
descElement.innerHTML = '';
|
||||
chars.forEach(char => {
|
||||
const span = document.createElement('span');
|
||||
span.textContent = char === ' ' ? '\u00A0' : char; // 空格用非断行空格
|
||||
span.style.opacity = '0';
|
||||
span.style.display = 'inline-block';
|
||||
descElement.appendChild(span);
|
||||
charElements.push(span);
|
||||
});
|
||||
|
||||
// 创建一个隐藏的测量容器来准确测量每个字符的宽度
|
||||
const measureContainer = document.createElement('div');
|
||||
measureContainer.style.position = 'absolute';
|
||||
measureContainer.style.visibility = 'hidden';
|
||||
measureContainer.style.whiteSpace = 'nowrap';
|
||||
measureContainer.style.fontSize = computedStyle.fontSize;
|
||||
measureContainer.style.fontWeight = computedStyle.fontWeight;
|
||||
measureContainer.style.fontFamily = computedStyle.fontFamily;
|
||||
document.body.appendChild(measureContainer);
|
||||
|
||||
// 测量每个字符的宽度
|
||||
const charWidths: number[] = [];
|
||||
charElements.forEach(span => {
|
||||
measureContainer.textContent = span.textContent;
|
||||
const charWidth = measureContainer.offsetWidth;
|
||||
charWidths.push(charWidth);
|
||||
});
|
||||
|
||||
document.body.removeChild(measureContainer);
|
||||
|
||||
// 设置容器初始状态(只有 padding,背景色透明)
|
||||
gsap.set(descElement, {
|
||||
width: padding,
|
||||
minWidth: padding,
|
||||
});
|
||||
|
||||
// 创建动画时间线,延迟 0.5 秒开始
|
||||
const tl = gsap.timeline({ delay: 0.5 });
|
||||
let currentWidth = padding;
|
||||
|
||||
// 背景色从透明逐渐加深(与第一个字符同时开始)
|
||||
tl.to(
|
||||
descElement,
|
||||
{
|
||||
duration: 0.4, // 背景色变化稍快一些,在文字显示过程中完成
|
||||
ease: 'power2.out',
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
// 逐个显示字符,同时增加容器宽度
|
||||
// 第一个字符在延迟后立即开始显示(时间位置 0)
|
||||
charElements.forEach((span, i) => {
|
||||
const charWidth = charWidths[i];
|
||||
currentWidth += charWidth;
|
||||
|
||||
// 同时显示字符和增加容器宽度
|
||||
// 第一个字符立即显示(i=0 时时间为 0),后续字符依次延迟
|
||||
tl.to(
|
||||
span,
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 0.08,
|
||||
ease: 'none',
|
||||
},
|
||||
i * 0.08,
|
||||
);
|
||||
|
||||
// 同时更新容器宽度
|
||||
tl.to(
|
||||
descElement,
|
||||
{
|
||||
width: currentWidth,
|
||||
duration: 0.08,
|
||||
ease: 'none',
|
||||
},
|
||||
i * 0.08,
|
||||
);
|
||||
});
|
||||
|
||||
// 保存动画时间线
|
||||
animationTimelines.current.push(tl);
|
||||
},
|
||||
[theme],
|
||||
);
|
||||
|
||||
// 监听 Swiper 切换,更新 activeTab 并触发动画
|
||||
// 监听 Swiper 切换,更新 activeTab
|
||||
const handleSlideChange = useCallback(
|
||||
(swiper: SwiperType) => {
|
||||
const activeIndex = swiper.activeIndex;
|
||||
|
|
@ -273,11 +123,9 @@ const Carousel = ({ title, items }: CarouselProps) => {
|
|||
const activeItem = items[activeIndex];
|
||||
if (activeItem) {
|
||||
setActiveTab(activeItem.id);
|
||||
// 触发当前幻灯片的文字动画
|
||||
animateTextFromLeft(activeIndex);
|
||||
}
|
||||
},
|
||||
[items, animateTextFromLeft],
|
||||
[items],
|
||||
);
|
||||
|
||||
// 当 activeTab 改变时,切换对应的 Swiper 卡片
|
||||
|
|
@ -287,34 +135,11 @@ const Carousel = ({ title, items }: CarouselProps) => {
|
|||
const targetIndex = items.findIndex(item => item.id === value);
|
||||
if (targetIndex !== -1 && swiperRef.current) {
|
||||
swiperRef.current.slideTo(targetIndex);
|
||||
// 触发切换后的文字动画
|
||||
setTimeout(() => {
|
||||
animateTextFromLeft(targetIndex);
|
||||
}, 300); // 等待切换动画完成
|
||||
}
|
||||
},
|
||||
[items, animateTextFromLeft],
|
||||
[items],
|
||||
);
|
||||
|
||||
// 初始加载时触发第一个幻灯片的动画
|
||||
useEffect(() => {
|
||||
if (items.length > 0 && descRefs.current[0]) {
|
||||
// 延迟执行,确保元素已经渲染
|
||||
const timer = setTimeout(() => {
|
||||
animateTextFromLeft(0);
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [items.length, animateTextFromLeft]);
|
||||
|
||||
// 组件卸载时清理所有动画
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
animationTimelines.current.forEach(tl => tl.kill());
|
||||
animationTimelines.current = [];
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 使用事件委托的方式处理点击事件
|
||||
const handleSlideClick = useCallback(
|
||||
(swiper: SwiperType, event: MouseEvent | TouchEvent | PointerEvent) => {
|
||||
|
|
@ -388,16 +213,9 @@ const Carousel = ({ title, items }: CarouselProps) => {
|
|||
modules={[Pagination, Autoplay]}
|
||||
className='mySwiper'
|
||||
>
|
||||
{items?.map((item, index) => (
|
||||
<SwiperSlide key={item.id} style={{ position: 'relative' }}>
|
||||
{items?.map(item => (
|
||||
<SwiperSlide key={item.id}>
|
||||
<StyledSwiperSlideImg src={item.url} alt={item.title} />
|
||||
<StyledSwiperSlideDesc
|
||||
ref={el => {
|
||||
descRefs.current[index] = el;
|
||||
}}
|
||||
>
|
||||
{item.desc}
|
||||
</StyledSwiperSlideDesc>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@
|
|||
import React from 'react';
|
||||
import { styled, Grid, alpha, Stack } from '@mui/material';
|
||||
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
||||
import {
|
||||
useFadeInText,
|
||||
useCardScaleAnimation,
|
||||
} from '../hooks/useGsapAnimation';
|
||||
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
||||
|
||||
interface CaseProps {
|
||||
mobile?: boolean;
|
||||
|
|
@ -30,8 +27,6 @@ const StyledCaseItem = styled('a')(({ theme }) => ({
|
|||
boxShadow: `0px 10px 20px 0px ${alpha(theme.palette.text.primary, 0.1)}`,
|
||||
},
|
||||
cursor: 'pointer',
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
}));
|
||||
|
||||
const StyledCaseItemTitle = styled('span')(({ theme }) => ({
|
||||
|
|
@ -45,10 +40,7 @@ const CaseItem: React.FC<{
|
|||
item: any;
|
||||
index: number;
|
||||
}> = React.memo(({ item, index }) => {
|
||||
const rand = Math.random();
|
||||
const cardRef = useCardScaleAnimation({
|
||||
duration: rand < 0.5 ? rand + 0.5 : rand,
|
||||
});
|
||||
const cardRef = useCardAnimation(0.2 + index * 0.1, 0.1);
|
||||
return (
|
||||
<StyledCaseItem
|
||||
ref={cardRef as React.Ref<HTMLAnchorElement>}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@
|
|||
import React from 'react';
|
||||
import { styled, Grid, alpha, Stack, Rating } from '@mui/material';
|
||||
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
||||
import {
|
||||
useFadeInText,
|
||||
useCardFadeInAnimation,
|
||||
} from '../hooks/useGsapAnimation';
|
||||
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
||||
|
||||
interface Props {
|
||||
mobile?: boolean;
|
||||
|
|
@ -25,13 +22,6 @@ const StyledItem = styled(Stack)(({ theme }) => ({
|
|||
boxShadow: `0px 5px 20px 0px ${alpha(theme.palette.text.primary, 0.06)}`,
|
||||
height: '100%',
|
||||
justifyContent: 'space-between',
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
color: theme.palette.primary.main,
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: `0px 10px 20px 0px ${alpha(theme.palette.text.primary, 0.1)}`,
|
||||
},
|
||||
opacity: 0,
|
||||
}));
|
||||
|
||||
const StyledItemSummary = styled('div')(({ theme }) => ({
|
||||
|
|
@ -68,7 +58,7 @@ const Item: React.FC<{
|
|||
};
|
||||
index: number;
|
||||
}> = React.memo(({ item, index }) => {
|
||||
const cardRef = useCardFadeInAnimation(0.2 + index * 0.1, 0.1);
|
||||
const cardRef = useCardAnimation(0.2 + index * 0.1, 0.1);
|
||||
return (
|
||||
<StyledItem ref={cardRef as React.Ref<HTMLDivElement>} gap={3}>
|
||||
<StyledItemSummary>{item.comment}</StyledItemSummary>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { decodeBase64 } from '../utils';
|
||||
export const DocWidth = {
|
||||
full: {
|
||||
label: '全屏',
|
||||
|
|
@ -13,6 +12,3 @@ export const DocWidth = {
|
|||
value: 720,
|
||||
},
|
||||
};
|
||||
|
||||
export const PROJECT_NAME =
|
||||
'5pys572R56uZ55SxIFBhbmRhV2lraSDmj5DkvpvmioDmnK/mlK/mjIE=';
|
||||
|
|
|
|||
|
|
@ -11,10 +11,7 @@ import {
|
|||
} from '../component/styledCommon';
|
||||
import { IconWenjianjia, IconWenjian } from '@panda-wiki/icons';
|
||||
import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
|
||||
import {
|
||||
useFadeInText,
|
||||
useCardFadeInAnimation,
|
||||
} from '../hooks/useGsapAnimation';
|
||||
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
||||
interface DirDocProps {
|
||||
mobile?: boolean;
|
||||
title?: string;
|
||||
|
|
@ -96,7 +93,7 @@ const DirDocItem: React.FC<{
|
|||
baseUrl: string;
|
||||
size: any;
|
||||
}> = React.memo(({ item, index, baseUrl, size }) => {
|
||||
const cardRef = useCardFadeInAnimation(0.2 + index * 0.1, 0.1);
|
||||
const cardRef = useCardAnimation(0.2 + index * 0.1, 0.1);
|
||||
|
||||
return (
|
||||
<Grid size={size} key={index}>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,7 @@ import React from 'react';
|
|||
import { styled, Grid, alpha } from '@mui/material';
|
||||
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
||||
import { IconLianjiezu } from '@panda-wiki/icons';
|
||||
import {
|
||||
useFadeInText,
|
||||
useCardFadeInAnimation,
|
||||
} from '../hooks/useGsapAnimation';
|
||||
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
||||
|
||||
interface FaqProps {
|
||||
mobile?: boolean;
|
||||
|
|
@ -38,7 +35,6 @@ const StyledFaqItem = styled('a')(({ theme }) => ({
|
|||
},
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
opacity: 0,
|
||||
}));
|
||||
|
||||
const StyledFaqItemTitle = styled('span')(({ theme }) => ({
|
||||
|
|
@ -52,7 +48,7 @@ const FaqItem: React.FC<{
|
|||
index: number;
|
||||
size: any;
|
||||
}> = React.memo(({ item, index, size }) => {
|
||||
const cardRef = useCardFadeInAnimation(0.2 + index * 0.1, 0.1);
|
||||
const cardRef = useCardAnimation(0.2 + index * 0.1, 0.1);
|
||||
|
||||
return (
|
||||
<Grid size={size} key={index}>
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@
|
|||
import React from 'react';
|
||||
import { styled, Grid, alpha, Stack } from '@mui/material';
|
||||
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
||||
import {
|
||||
useFadeInText,
|
||||
useCardFadeInAnimation,
|
||||
} from '../hooks/useGsapAnimation';
|
||||
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
||||
import { IconTips } from '@panda-wiki/icons';
|
||||
|
||||
interface FeatureProps {
|
||||
|
|
@ -14,7 +11,7 @@ interface FeatureProps {
|
|||
title?: string;
|
||||
items?: {
|
||||
name: string;
|
||||
desc: string;
|
||||
link: string;
|
||||
}[];
|
||||
}
|
||||
const StyledFeatureItem = styled(Stack)(({ theme }) => ({
|
||||
|
|
@ -23,12 +20,12 @@ const StyledFeatureItem = styled(Stack)(({ theme }) => ({
|
|||
padding: theme.spacing(2.5),
|
||||
boxShadow: `0px 5px 20px 0px ${alpha(theme.palette.text.primary, 0.06)}`,
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
color: theme.palette.primary.main,
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: `0px 10px 20px 0px ${alpha(theme.palette.text.primary, 0.1)}`,
|
||||
},
|
||||
opacity: 0,
|
||||
// '&:hover': {
|
||||
// color: theme.palette.primary.main,
|
||||
// borderColor: theme.palette.primary.main,
|
||||
// boxShadow: `0px 10px 20px 0px ${alpha(theme.palette.text.primary, 0.1)}`,
|
||||
// },
|
||||
// cursor: 'pointer',
|
||||
}));
|
||||
|
||||
export const StyledFeatureItemIcon = styled('div')(({ theme }) => ({
|
||||
|
|
@ -65,13 +62,10 @@ const StyledFeatureItemSummary = styled('div')(({ theme }) => ({
|
|||
|
||||
// 单个卡片组件,带动画效果
|
||||
const FeatureItem: React.FC<{
|
||||
item: {
|
||||
name: string;
|
||||
desc: string;
|
||||
};
|
||||
item: any;
|
||||
index: number;
|
||||
}> = React.memo(({ item, index }) => {
|
||||
const cardRef = useCardFadeInAnimation(0.2 + index * 0.1, 0.1);
|
||||
const cardRef = useCardAnimation(0.2 + index * 0.1, 0.1);
|
||||
return (
|
||||
<StyledFeatureItem
|
||||
ref={cardRef as React.Ref<HTMLDivElement>}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ import { useState } from 'react';
|
|||
import { IconDianhua, IconWeixingongzhonghao } from '@panda-wiki/icons';
|
||||
import Overlay from './Overlay';
|
||||
import { DocWidth } from '../constants';
|
||||
import { PROJECT_NAME } from '../constants';
|
||||
import { decodeBase64 } from '../utils';
|
||||
|
||||
interface DomainSocialMediaAccount {
|
||||
channel?: string;
|
||||
|
|
@ -342,7 +340,7 @@ const Footer = React.memo(
|
|||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Box>{decodeBase64(PROJECT_NAME)}</Box>
|
||||
<Box>本网站由 PandaWiki 提供技术支持</Box>
|
||||
<img src={logo} alt='PandaWiki' width={16} height={16} />
|
||||
</Stack>
|
||||
</Link>
|
||||
|
|
@ -779,7 +777,7 @@ const Footer = React.memo(
|
|||
},
|
||||
}}
|
||||
>
|
||||
<Box>{decodeBase64(PROJECT_NAME)}</Box>
|
||||
<Box>本网站由 PandaWiki 提供技术支持</Box>
|
||||
<img
|
||||
src={logo}
|
||||
alt='PandaWiki'
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ export const useTypewriterText = (
|
|||
};
|
||||
|
||||
// 卡片渐入动画 hook
|
||||
export const useCardFadeInAnimation = (
|
||||
export const useCardAnimation = (
|
||||
delay: number = 0,
|
||||
threshold: number = 0.1,
|
||||
) => {
|
||||
|
|
@ -308,6 +308,7 @@ export const useCardFadeInAnimation = (
|
|||
gsap.set(card, {
|
||||
opacity: 0,
|
||||
y: 50,
|
||||
// scale: 0.9,
|
||||
});
|
||||
|
||||
// 创建动画
|
||||
|
|
@ -316,6 +317,7 @@ export const useCardFadeInAnimation = (
|
|||
tl.to(card, {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
duration: 0.4,
|
||||
ease: 'back.out(1.4)',
|
||||
});
|
||||
|
|
@ -327,136 +329,3 @@ export const useCardFadeInAnimation = (
|
|||
|
||||
return cardRef;
|
||||
};
|
||||
|
||||
export const useCardScaleAnimation = ({
|
||||
delay = 0,
|
||||
threshold = 0.1,
|
||||
duration = 0.4,
|
||||
}: {
|
||||
delay?: number;
|
||||
threshold?: number;
|
||||
duration?: number;
|
||||
}) => {
|
||||
const cardRef = useRef<HTMLElement>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [hasAnimated, setHasAnimated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cardRef.current || hasAnimated) return;
|
||||
|
||||
const card = cardRef.current;
|
||||
|
||||
// 创建 Intersection Observer
|
||||
const observer = new IntersectionObserver(
|
||||
entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting && !hasAnimated) {
|
||||
setIsVisible(true);
|
||||
setHasAnimated(true);
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold,
|
||||
rootMargin: '0px 0px -50px 0px',
|
||||
},
|
||||
);
|
||||
|
||||
observer.observe(card);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [threshold, hasAnimated]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cardRef.current || !isVisible) return;
|
||||
|
||||
const card = cardRef.current;
|
||||
|
||||
// 设置初始状态
|
||||
gsap.set(card, {
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
});
|
||||
|
||||
// 创建动画
|
||||
const tl = gsap.timeline({ delay });
|
||||
|
||||
tl.to(card, {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration,
|
||||
});
|
||||
|
||||
return () => {
|
||||
tl.kill();
|
||||
};
|
||||
}, [isVisible, delay]);
|
||||
|
||||
return cardRef;
|
||||
};
|
||||
|
||||
export const useCardAnimation = ({
|
||||
delay = 0,
|
||||
threshold = 0.1,
|
||||
initial,
|
||||
to,
|
||||
}: {
|
||||
delay?: number;
|
||||
threshold?: number;
|
||||
initial: GSAPTweenVars;
|
||||
to: GSAPTweenVars;
|
||||
}) => {
|
||||
const cardRef = useRef<HTMLElement>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [hasAnimated, setHasAnimated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cardRef.current || hasAnimated) return;
|
||||
|
||||
const card = cardRef.current;
|
||||
|
||||
// 创建 Intersection Observer
|
||||
const observer = new IntersectionObserver(
|
||||
entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting && !hasAnimated) {
|
||||
setIsVisible(true);
|
||||
setHasAnimated(true);
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold,
|
||||
rootMargin: '0px 0px -50px 0px',
|
||||
},
|
||||
);
|
||||
|
||||
observer.observe(card);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [threshold, hasAnimated]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cardRef.current || !isVisible) return;
|
||||
|
||||
const card = cardRef.current;
|
||||
|
||||
// 设置初始状态
|
||||
gsap.set(card, initial);
|
||||
|
||||
// 创建动画
|
||||
const tl = gsap.timeline({ delay });
|
||||
|
||||
tl.to(card, to);
|
||||
|
||||
return () => {
|
||||
tl.kill();
|
||||
};
|
||||
}, [isVisible, delay]);
|
||||
|
||||
return cardRef;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { styled, alpha, Stack, Box } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { styled, Grid, alpha, Stack, Box } from '@mui/material';
|
||||
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
||||
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
||||
|
||||
|
|
@ -18,8 +18,8 @@ interface ImgTextProps {
|
|||
const StyledImgTextItem = styled(Stack)(({ theme }) => ({}));
|
||||
|
||||
export const StyledImgTextItemImg = styled('img')(({ theme }) => ({
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
maxWidth: 350,
|
||||
maxHeight: 350,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
|
|
@ -28,7 +28,7 @@ export const StyledImgTextItemImg = styled('img')(({ theme }) => ({
|
|||
}));
|
||||
|
||||
const StyledImgTextItemTitle = styled('h3')(({ theme }) => ({
|
||||
fontSize: 24,
|
||||
fontSize: 20,
|
||||
fontWeight: 700,
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
||||
|
|
@ -49,31 +49,14 @@ const ImgText: React.FC<ImgTextProps> = React.memo(
|
|||
: { xs: 12, md: 6 };
|
||||
|
||||
const titleRef = useFadeInText(0.2, 0.1);
|
||||
|
||||
const cardLeftAnimation = useMemo(
|
||||
() => ({
|
||||
initial: { opacity: 0, x: -250 },
|
||||
to: { opacity: 1, x: 0, duration: 0.6, ease: 'power2.out' },
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const cardRightAnimation = useMemo(
|
||||
() => ({
|
||||
initial: { opacity: 0, x: 250 },
|
||||
to: { opacity: 1, x: 0, duration: 0.6, ease: 'power2.out' },
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const cardLeftRef = useCardAnimation(cardLeftAnimation);
|
||||
const cardRightRef = useCardAnimation(cardRightAnimation);
|
||||
const cardRef = useCardAnimation(0.2, 0.1);
|
||||
|
||||
return (
|
||||
<StyledTopicBox>
|
||||
<StyledTopicTitle ref={titleRef}>{title}</StyledTopicTitle>
|
||||
<StyledImgTextItem
|
||||
gap={mobile ? 4 : { xs: 4, sm: 6, md: 16 }}
|
||||
ref={cardRef as React.Ref<HTMLDivElement>}
|
||||
gap={mobile ? 4 : { xs: 4, sm: 6, md: 38 }}
|
||||
direction={
|
||||
mobile
|
||||
? 'column-reverse'
|
||||
|
|
@ -86,46 +69,12 @@ const ImgText: React.FC<ImgTextProps> = React.memo(
|
|||
justifyContent='center'
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
<Box
|
||||
sx={{ width: '100%' }}
|
||||
ref={cardLeftRef as React.Ref<HTMLDivElement>}
|
||||
>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<StyledImgTextItemImg src={item.url} alt={item.name} />
|
||||
</Box>
|
||||
<Stack
|
||||
gap={1}
|
||||
sx={{ width: '100%' }}
|
||||
ref={cardRightRef as React.Ref<HTMLDivElement>}
|
||||
alignItems={
|
||||
mobile
|
||||
? 'flex-start'
|
||||
: direction === 'row'
|
||||
? 'flex-start'
|
||||
: 'flex-end'
|
||||
}
|
||||
>
|
||||
<StyledImgTextItemTitle
|
||||
sx={{
|
||||
textAlign: mobile
|
||||
? 'left'
|
||||
: direction === 'row'
|
||||
? 'left'
|
||||
: 'right',
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</StyledImgTextItemTitle>
|
||||
<StyledImgTextItemSummary
|
||||
sx={{
|
||||
textAlign: mobile
|
||||
? 'left'
|
||||
: direction === 'row'
|
||||
? 'left'
|
||||
: 'right',
|
||||
}}
|
||||
>
|
||||
{item.desc}
|
||||
</StyledImgTextItemSummary>
|
||||
<Stack gap={1} sx={{ width: '100%' }}>
|
||||
<StyledImgTextItemTitle>{item.name}</StyledImgTextItemTitle>
|
||||
<StyledImgTextItemSummary>{item.desc}</StyledImgTextItemSummary>
|
||||
</Stack>
|
||||
</StyledImgTextItem>
|
||||
</StyledTopicBox>
|
||||
|
|
|
|||
|
|
@ -14,14 +14,11 @@ export { default as Case } from './case';
|
|||
export { default as ImgText } from './imgText';
|
||||
export { default as Feature } from './feature';
|
||||
export { default as Comment } from './comment';
|
||||
export { default as Question } from './question';
|
||||
export { default as BlockGrid } from './blockGrid';
|
||||
|
||||
// 导出动画 hooks
|
||||
export {
|
||||
useTextAnimation,
|
||||
useFadeInText,
|
||||
useTypewriterText,
|
||||
useCardFadeInAnimation,
|
||||
useCardAnimation,
|
||||
} from './hooks/useGsapAnimation';
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@
|
|||
import React from 'react';
|
||||
import { styled, Grid, alpha, Stack } from '@mui/material';
|
||||
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
||||
import {
|
||||
useFadeInText,
|
||||
useCardFadeInAnimation,
|
||||
} from '../hooks/useGsapAnimation';
|
||||
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
||||
|
||||
interface MetricsProps {
|
||||
mobile?: boolean;
|
||||
|
|
@ -51,7 +48,7 @@ const MetricsItem: React.FC<{
|
|||
index: number;
|
||||
size: any;
|
||||
}> = React.memo(({ item, index, size }) => {
|
||||
const cardRef = useCardFadeInAnimation(0.2 + index * 0.1, 0.1);
|
||||
const cardRef = useCardAnimation(0.2 + index * 0.1, 0.1);
|
||||
|
||||
return (
|
||||
<Grid size={size} key={index}>
|
||||
|
|
@ -59,7 +56,6 @@ const MetricsItem: React.FC<{
|
|||
ref={cardRef as React.Ref<HTMLDivElement>}
|
||||
gap={1}
|
||||
alignItems='center'
|
||||
sx={{ opacity: 0 }}
|
||||
>
|
||||
<StyledMetricsItemNumber className='metrics-item-number'>
|
||||
{item.number}
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { styled, Stack, alpha } from '@mui/material';
|
||||
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
||||
import { IconWenhao } from '@panda-wiki/icons';
|
||||
import {
|
||||
useFadeInText,
|
||||
useCardFadeInAnimation,
|
||||
} from '../hooks/useGsapAnimation';
|
||||
|
||||
interface QuestionProps {
|
||||
mobile?: boolean;
|
||||
title?: string;
|
||||
onSearch: (question: string) => void;
|
||||
items?: {
|
||||
question: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
const StyledItem = styled('div')(({ theme }) => ({
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(2),
|
||||
color: theme.palette.text.primary,
|
||||
borderRadius: '10px',
|
||||
border: `1px solid ${alpha(theme.palette.text.primary, 0.1)}`,
|
||||
boxShadow: `0px 5px 20px 0px ${alpha(theme.palette.text.primary, 0.06)}`,
|
||||
padding: theme.spacing(3, 4),
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-5px)',
|
||||
color: theme.palette.primary.main,
|
||||
border: `1px solid ${alpha(theme.palette.primary.main, 0.5)}`,
|
||||
boxShadow: `0px 10px 20px 0px ${alpha(theme.palette.text.primary, 0.1)}`,
|
||||
},
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
opacity: 0,
|
||||
}));
|
||||
|
||||
const StyledItemTitle = styled('span')(({ theme }) => ({
|
||||
fontSize: 20,
|
||||
fontWeight: 400,
|
||||
}));
|
||||
|
||||
// 单个卡片组件,带动画效果
|
||||
const Item: React.FC<{
|
||||
item: {
|
||||
question: string;
|
||||
};
|
||||
onSearch: (question: string) => void;
|
||||
index: number;
|
||||
}> = React.memo(({ item, index, onSearch }) => {
|
||||
const cardRef = useCardFadeInAnimation(0.2 + index * 0.1, 0.1);
|
||||
|
||||
return (
|
||||
<StyledItem
|
||||
ref={cardRef as React.Ref<HTMLDivElement>}
|
||||
onClick={() => onSearch(item.question)}
|
||||
>
|
||||
<IconWenhao sx={{ color: 'primary.main', fontSize: 20 }} />
|
||||
<StyledItemTitle>{item.question}</StyledItemTitle>
|
||||
</StyledItem>
|
||||
);
|
||||
});
|
||||
|
||||
const Question: React.FC<QuestionProps> = React.memo(
|
||||
({ title, items = [], onSearch }) => {
|
||||
// 添加标题淡入动画
|
||||
const titleRef = useFadeInText(0.2, 0.1);
|
||||
|
||||
return (
|
||||
<StyledTopicBox>
|
||||
<StyledTopicTitle ref={titleRef}>{title}</StyledTopicTitle>
|
||||
<Stack gap={3} sx={{ width: '100%' }}>
|
||||
{items.map((item, index) => (
|
||||
<Item key={index} item={item} index={index} onSearch={onSearch} />
|
||||
))}
|
||||
</Stack>
|
||||
</StyledTopicBox>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default Question;
|
||||
|
|
@ -11,10 +11,7 @@ import {
|
|||
} from '../component/styledCommon';
|
||||
import IconWenjian from '@panda-wiki/icons/IconWenjian';
|
||||
import ArrowForwardIosRoundedIcon from '@mui/icons-material/ArrowForwardIosRounded';
|
||||
import {
|
||||
useFadeInText,
|
||||
useCardFadeInAnimation,
|
||||
} from '../hooks/useGsapAnimation';
|
||||
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
||||
|
||||
interface SimpleDocProps {
|
||||
mobile?: boolean;
|
||||
|
|
@ -65,7 +62,7 @@ const SimpleDocItem: React.FC<{
|
|||
baseUrl: string;
|
||||
size: any;
|
||||
}> = React.memo(({ item, index, baseUrl, size }) => {
|
||||
const cardRef = useCardFadeInAnimation(0.2 + index * 0.1, 0.1);
|
||||
const cardRef = useCardAnimation(0.2 + index * 0.1, 0.1);
|
||||
|
||||
return (
|
||||
<Grid size={size} key={index}>
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
export const decodeBase64 = (text: string) => {
|
||||
try {
|
||||
const buff = Buffer.from(text, 'base64');
|
||||
return buff.toString('utf-8');
|
||||
} catch (e) {
|
||||
// 客户端如果报错,退回到 atob
|
||||
if (typeof window !== 'undefined' && window.atob) {
|
||||
return window.atob(text);
|
||||
}
|
||||
// 处理解码失败的情况
|
||||
console.error('Base64 decoding failed:', e);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
|
@ -4,8 +4,7 @@ import { Box, Divider, Stack, Link, alpha } from '@mui/material';
|
|||
import { useState } from 'react';
|
||||
import { IconDianhua, IconWeixingongzhonghao } from '@panda-wiki/icons';
|
||||
import Overlay from './Overlay';
|
||||
import { decodeBase64 } from '../utils';
|
||||
import { PROJECT_NAME } from '../constants';
|
||||
import { DocWidth } from '../constants';
|
||||
|
||||
interface DomainSocialMediaAccount {
|
||||
channel?: string;
|
||||
|
|
@ -344,7 +343,7 @@ const Footer = React.memo(
|
|||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Box>{decodeBase64(PROJECT_NAME)}</Box>
|
||||
<Box>本网站由 PandaWiki 提供技术支持</Box>
|
||||
<img src={logo} alt='PandaWiki' width={16} height={16} />
|
||||
</Stack>
|
||||
</Link>
|
||||
|
|
@ -774,7 +773,7 @@ const Footer = React.memo(
|
|||
},
|
||||
}}
|
||||
>
|
||||
<Box>{decodeBase64(PROJECT_NAME)}</Box>
|
||||
<Box>本网站由 PandaWiki 提供技术支持</Box>
|
||||
<img src={logo} alt='PandaWiki' width={0} />
|
||||
</Stack>
|
||||
</Link>
|
||||
|
|
|
|||
Loading…
Reference in New Issue