Compare commits

...

8 Commits

Author SHA1 Message Date
xiaomakuaiz 282dc63242
Merge f7c0fe273b into 35cd94e342 2025-11-07 17:06:33 +08:00
xiaomakuaiz 35cd94e342
Merge pull request #1484 from guanweiwang/pref/landing_drag
pref: 优化和bug修复
2025-11-07 17:06:15 +08:00
Gavan 0175624c84 pref: 删除无用代码,提取公共拖拽函数,修复 footer 2025-11-07 12:00:48 +08:00
xiaomakuaiz 3032384457
Merge pull request #1479 from xiaomakuaiz/fix/qa-modal-logo
Fix QA modal logo rendering
2025-11-06 23:01:16 +08:00
monkeycode-ai 50ed0c6794 Fix QA modal logo rendering
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
2025-11-06 22:44:18 +08:00
xiaomakuaiz 45238c3dfa
Merge pull request #1476 from guanweiwang/feature/qa_modal
feat: 优化问答弹窗,完善 ts 配置
2025-11-06 17:43:33 +08:00
Gavan ea87d5ef7e pref: 优化 ts 配置 2025-11-06 15:37:56 +08:00
Gavan b1aefd8cfd feat: ui调整 2025-11-06 11:46:37 +08:00
89 changed files with 1936 additions and 4235 deletions

View File

@ -13,26 +13,53 @@ import {
arrayMove,
rectSortingStrategy,
SortableContext,
SortingStrategy,
} from '@dnd-kit/sortable';
import { Stack } from '@mui/material';
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
import Item from './Item';
import SortableItem from './SortableItem';
import { Stack, SxProps, Theme } from '@mui/material';
import {
ComponentType,
CSSProperties,
Dispatch,
SetStateAction,
useCallback,
useState,
} from 'react';
type ItemType = {
id: string;
text: string;
type: 'contained' | 'outlined' | 'text';
href: string;
};
interface DragListProps {
data: ItemType[];
onChange: (data: ItemType[]) => void;
export interface DragListProps<T extends { id?: string | null }> {
data: T[];
onChange: (data: T[]) => void;
setIsEdit: Dispatch<SetStateAction<boolean>>;
SortableItemComponent: ComponentType<{
id: string;
item: T;
handleRemove: (id: string) => void;
handleUpdateItem: (item: T) => void;
setIsEdit: Dispatch<SetStateAction<boolean>>;
}>;
ItemComponent: ComponentType<{
isDragging?: boolean;
item: T;
style?: CSSProperties;
setIsEdit: Dispatch<SetStateAction<boolean>>;
handleUpdateItem?: (item: T) => void;
}>;
containerSx?: SxProps<Theme>;
sortingStrategy?: SortingStrategy;
direction?: 'row' | 'column';
gap?: number;
}
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
function DragList<T extends { id?: string | null }>({
data,
onChange,
setIsEdit,
SortableItemComponent,
ItemComponent,
containerSx,
sortingStrategy = rectSortingStrategy,
direction = 'row',
gap = 2,
}: DragListProps<T>) {
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
@ -44,8 +71,8 @@ const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
(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 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);
}
@ -60,21 +87,16 @@ const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
const handleRemove = useCallback(
(id: string) => {
const newData = data.filter(item => item.id !== id);
const newData = data.filter(item => (item.id || '') !== id);
onChange(newData);
},
[data, onChange],
);
const handleUpdateItem = useCallback(
(updatedItem: {
id: string;
text: string;
type: 'contained' | 'outlined' | 'text';
href: string;
}) => {
(updatedItem: T) => {
const newData = data.map(item =>
item.id === updatedItem.id ? updatedItem : item,
(item.id || '') === (updatedItem.id || '') ? updatedItem : item,
);
onChange(newData);
},
@ -92,14 +114,19 @@ const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
onDragCancel={handleDragCancel}
>
<SortableContext
items={data.map(item => item.id)}
strategy={rectSortingStrategy}
items={data.map(item => item.id || '')}
strategy={sortingStrategy}
>
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
<Stack
direction={direction}
flexWrap={'wrap'}
gap={gap}
sx={containerSx}
>
{data.map(item => (
<SortableItem
key={item.id}
id={item.id}
<SortableItemComponent
key={item.id || ''}
id={item.id || ''}
item={item}
handleRemove={handleRemove}
handleUpdateItem={handleUpdateItem}
@ -110,9 +137,9 @@ const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
</SortableContext>
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
{activeId ? (
<Item
<ItemComponent
isDragging
item={data.find(item => item.id === activeId)!}
item={data.find(item => (item.id || '') === activeId)!}
setIsEdit={setIsEdit}
handleUpdateItem={handleUpdateItem}
/>
@ -120,6 +147,6 @@ const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
</DragOverlay>
</DndContext>
);
};
}
export default DragList;

View File

@ -0,0 +1,59 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { ComponentType } from 'react';
export interface SortableItemProps<T extends { id?: string | null }> {
id: string;
item: T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ItemComponent: ComponentType<any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}
function SortableItem<T extends { id?: string | null }>({
id,
item,
ItemComponent,
...rest
}: SortableItemProps<T>) {
const {
isDragging,
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition || undefined,
};
return (
<ItemComponent
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createSortableItem(ItemComponent: ComponentType<any>) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const WrappedComponent = (props: any) => (
<SortableItem {...props} ItemComponent={ItemComponent} />
);
WrappedComponent.displayName = `SortableItem(${ItemComponent.displayName || ItemComponent.name || 'Component'})`;
return WrappedComponent;
}
export default SortableItem;

View File

@ -0,0 +1,5 @@
export { default as DragList } from './DragList';
export type { DragListProps } from './DragList';
export { default as SortableItem, createSortableItem } from './SortableItem';
export type { SortableItemProps } from './SortableItem';

View File

@ -0,0 +1,141 @@
import { Box, IconButton, Stack, TextField } from '@mui/material';
import { Icon } from '@ctzhian/ui';
import {
CSSProperties,
Dispatch,
forwardRef,
HTMLAttributes,
SetStateAction,
} from 'react';
type HotSearchItem = {
id: string;
text: string;
};
export type HotSearchItemProps = Omit<
HTMLAttributes<HTMLDivElement>,
'onChange'
> & {
item: HotSearchItem;
withOpacity?: boolean;
isDragging?: boolean;
dragHandleProps?: React.HTMLAttributes<HTMLDivElement>;
handleRemove?: (id: string) => void;
handleUpdateItem?: (item: HotSearchItem) => void;
setIsEdit: Dispatch<SetStateAction<boolean>>;
};
const HotSearchItem = forwardRef<HTMLDivElement, HotSearchItemProps>(
(
{
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.text}
onChange={e => {
const updatedItem = { ...item, text: 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 HotSearchItem;

View File

@ -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 FaqSortableItemProps = ItemProps & {};
const FaqSortableItem: FC<FaqSortableItemProps> = ({ 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 FaqSortableItem;

View File

@ -1,18 +1,21 @@
import React, { useState, useEffect } from 'react';
import { TextField, Chip, Autocomplete, Box } from '@mui/material';
import React, { useEffect, useRef } from 'react';
import { TextField } from '@mui/material';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import type { ConfigProps } from '../type';
import { useForm, Controller } from 'react-hook-form';
import { useAppSelector } from '@/store';
import DragList from './DragList';
import DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import HotSearchItem from './HotSearchItem';
import UploadFile from '@/components/UploadFile';
import { DEFAULT_DATA } from '../../../constants';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
import { handleLandingConfigs, findConfigById } from '../../../utils';
import { Empty } from '@ctzhian/ui';
const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
const { appPreviewData } = useAppSelector(state => state.config);
const [inputValue, setInputValue] = useState('');
const { control, watch, setValue, subscribe } = useForm<
typeof DEFAULT_DATA.banner
>({
@ -24,6 +27,49 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
const debouncedDispatch = useDebounceAppPreviewData();
const btns = watch('btns') || [];
const hotSearch = watch('hot_search') || [];
// 使用 ref 来维护稳定的 ID 映射
const idMapRef = useRef<Map<number, string>>(new Map());
// 将string[]转换为对象数组用于显示,保持 ID 稳定
const hotSearchList = Array.isArray(hotSearch)
? hotSearch.map((text, index) => {
// 如果该索引没有 ID生成一个新的
if (!idMapRef.current.has(index)) {
idMapRef.current.set(
index,
`${Date.now()}-${index}-${Math.random()}`,
);
}
return {
id: idMapRef.current.get(index)!,
text: String(text),
};
})
: [];
// 清理不再使用的 ID并确保所有索引都有 ID
useEffect(() => {
const currentIndexes = new Set(hotSearch.map((_, index) => index));
// 清理不存在的索引
const keysToDelete: number[] = [];
idMapRef.current.forEach((_, key) => {
if (!currentIndexes.has(key)) {
keysToDelete.push(key);
}
});
keysToDelete.forEach(key => idMapRef.current.delete(key));
// 确保每个索引都有 ID
hotSearch.forEach((_, index) => {
if (!idMapRef.current.has(index)) {
idMapRef.current.set(index, `${Date.now()}-${index}-${Math.random()}`);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hotSearch.length]);
const handleAddButton = () => {
const nextId = `${Date.now()}`;
@ -33,6 +79,31 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
]);
};
const handleAddHotSearch = () => {
const newIndex = hotSearch.length;
const nextId = `${Date.now()}-${newIndex}-${Math.random()}`;
idMapRef.current.set(newIndex, nextId);
// 转换回string[]格式
setValue('hot_search', [...hotSearch, '']);
setIsEdit(true);
};
const handleHotSearchChange = (newList: { id: string; text: string }[]) => {
// 重建 ID 映射关系
const newIdMap = new Map<number, string>();
newList.forEach((item, index) => {
newIdMap.set(index, item.id);
});
idMapRef.current = newIdMap;
// 转换回string[]格式
setValue(
'hot_search',
newList.map(item => item.text),
);
setIsEdit(true);
};
useEffect(() => {
const callback = subscribe({
formState: {
@ -59,6 +130,7 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
return () => {
callback();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [subscribe]);
return (
@ -132,60 +204,20 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
render={({ field }) => <TextField {...field} placeholder='请输入' />}
/>
</CommonItem>
<CommonItem title='热门搜索'>
<Controller
control={control}
name='hot_search'
render={({ field }) => (
<Autocomplete
{...field}
value={field.value || []}
multiple
freeSolo
fullWidth
options={[]}
inputValue={inputValue}
onInputChange={(_, newInputValue) => setInputValue(newInputValue)}
onChange={(_, newValue) => {
setIsEdit(true);
const newValues = [...new Set(newValue as string[])];
field.onChange(newValues);
}}
onBlur={() => {
setIsEdit(true);
const trimmedValue = inputValue.trim();
if (trimmedValue && !field.value?.includes(trimmedValue)) {
field.onChange([...(field.value || []), trimmedValue]);
}
setInputValue('');
}}
renderValue={(value, getTagProps) => {
return value.map((option, index: number) => {
return (
<Chip
variant='outlined'
size='small'
label={
<Box sx={{ fontSize: '12px' }}>
{option as React.ReactNode}
</Box>
}
{...getTagProps({ index })}
key={index}
/>
);
});
}}
renderInput={params => (
<TextField
{...params}
placeholder='回车确认,填写下一个热门搜索'
variant='outlined'
/>
)}
/>
)}
/>
<CommonItem title='热门搜索' onAdd={handleAddHotSearch}>
{hotSearchList.length === 0 ? (
<Empty />
) : (
<DragList
data={hotSearchList}
onChange={handleHotSearchChange}
setIsEdit={setIsEdit}
SortableItemComponent={sortableProps => (
<SortableItem {...sortableProps} ItemComponent={HotSearchItem} />
)}
ItemComponent={HotSearchItem}
/>
)}
</CommonItem>
<CommonItem title='主按钮' onAdd={handleAddButton}>
<DragList
@ -195,6 +227,10 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
setIsEdit(true);
}}
setIsEdit={setIsEdit}
SortableItemComponent={sortableProps => (
<SortableItem {...sortableProps} ItemComponent={Item} />
)}
ItemComponent={Item}
/>
</CommonItem>
</StyledCommonWrapper>

View File

@ -1,114 +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 from './Item';
import { DomainRecommendNodeListResp } from '@/request/types';
import SortableItem from './SortableItem';
interface DragListProps {
data: DomainRecommendNodeListResp[];
onChange: (data: DomainRecommendNodeListResp[]) => 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: DomainRecommendNodeListResp) => {
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;

View File

@ -1,38 +0,0 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import FaqItem, { ItemProps } from './Item';
type SortableItemProps = ItemProps & {};
const FaqSortableItem: 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 (
<FaqItem
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
};
export default FaqSortableItem;

View File

@ -2,7 +2,9 @@ import React, { useEffect, useState } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
import { Controller, useForm } from 'react-hook-form';
import BasicDocDragList from './DragList';
import DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import type { ConfigProps } from '../type';
import { Empty } from '@ctzhian/ui';
import { useAppSelector } from '@/store';
@ -116,13 +118,17 @@ const BasicDocConfig = ({ setIsEdit, id }: ConfigProps) => {
{nodes.length === 0 ? (
<Empty />
) : (
<BasicDocDragList
<DragList
data={nodes}
onChange={value => {
setIsEdit(true);
setValue('nodes', value);
}}
setIsEdit={setIsEdit}
SortableItemComponent={sortableProps => (
<SortableItem {...sortableProps} ItemComponent={Item} />
)}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -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;

View File

@ -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;

View File

@ -2,7 +2,9 @@ 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 DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -91,6 +93,10 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={sortableProps => (
<SortableItem {...sortableProps} ItemComponent={Item} />
)}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -1,120 +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 from './Item';
import FaqSortableItem from './SortableItem';
type ItemType = {
id: string;
title: string;
url: string;
desc: string;
};
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 => (
<FaqSortableItem
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;

View File

@ -1,38 +0,0 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import FaqItem, { ItemProps } from './Item';
type FaqSortableItemProps = ItemProps & {};
const FaqSortableItem: FC<FaqSortableItemProps> = ({ item, ...rest }) => {
const {
isDragging,
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id: item.id });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition || undefined,
};
return (
<FaqItem
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
};
export default FaqSortableItem;

View File

@ -2,7 +2,9 @@ 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 DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -121,6 +123,10 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={sortableProps => (
<SortableItem {...sortableProps} ItemComponent={Item} />
)}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -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;

View File

@ -1,38 +0,0 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import FaqItem, { ItemTypeProps } from './Item';
type SortableItemProps = ItemTypeProps & {};
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 (
<FaqItem
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
};
export default SortableItem;

View File

@ -2,7 +2,9 @@ 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 DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -90,6 +92,10 @@ const CaseConfig = ({ setIsEdit, id }: ConfigProps) => {
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={sortableProps => (
<SortableItem {...sortableProps} ItemComponent={Item} />
)}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -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;

View File

@ -1,38 +0,0 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import FaqItem, { ItemTypeProps } from './Item';
type SortableItemProps = ItemTypeProps & {};
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 (
<FaqItem
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
};
export default SortableItem;

View File

@ -2,7 +2,9 @@ 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 DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -93,6 +95,10 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={sortableProps => (
<SortableItem {...sortableProps} ItemComponent={Item} />
)}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -1,114 +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 from './Item';
import { DomainRecommendNodeListResp } from '@/request/types';
import SortableItem from './SortableItem';
interface DragListProps {
data: DomainRecommendNodeListResp[];
onChange: (data: DomainRecommendNodeListResp[]) => 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: DomainRecommendNodeListResp) => {
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;

View File

@ -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;

View File

@ -2,7 +2,9 @@ import React, { useEffect, useState } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
import { Controller, useForm } from 'react-hook-form';
import BasicDocDragList from './DragList';
import DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import { Empty } from '@ctzhian/ui';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
@ -117,13 +119,17 @@ const DirDocConfig = ({ setIsEdit, id }: ConfigProps) => {
{nodes.length === 0 ? (
<Empty />
) : (
<BasicDocDragList
<DragList
data={nodes}
onChange={value => {
setIsEdit(true);
setValue('nodes', value);
}}
setIsEdit={setIsEdit}
SortableItemComponent={sortableProps => (
<SortableItem {...sortableProps} ItemComponent={Item} />
)}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -1,119 +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 FaqItem from './FaqItem';
import FaqSortableItem from './FaqSortableItem';
type FaqItemType = {
id: string;
question: string;
link: string;
};
interface FaqDragListProps {
data: FaqItemType[];
onChange: (data: FaqItemType[]) => 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: FaqItemType) => {
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 => (
<FaqSortableItem
key={item.id}
id={item.id}
item={item}
handleRemove={handleRemove}
handleUpdateItem={handleUpdateItem}
setIsEdit={setIsEdit}
/>
))}
</Stack>
</SortableContext>
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
{activeId ? (
<FaqItem
isDragging
item={data.find(item => item.id === activeId)!}
setIsEdit={setIsEdit}
handleUpdateItem={handleUpdateItem}
/>
) : null}
</DragOverlay>
</DndContext>
);
};
export default FaqDragList;

View File

@ -1,38 +0,0 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import FaqItem, { FaqItemProps } from './FaqItem';
type FaqSortableItemProps = FaqItemProps & {};
const FaqSortableItem: FC<FaqSortableItemProps> = ({ item, ...rest }) => {
const {
isDragging,
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id: item.id });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition || undefined,
};
return (
<FaqItem
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
};
export default FaqSortableItem;

View File

@ -2,13 +2,14 @@ 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 './FaqDragList';
import DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import FaqItem from './Item';
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 ColorPickerField from '../../components/ColorPickerField';
import { findConfigById, handleLandingConfigs } from '../../../utils';
const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
@ -111,10 +112,14 @@ const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
{list.length === 0 ? (
<Empty />
) : (
<FaqDragList
<DragList
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={sortableProps => (
<SortableItem {...sortableProps} ItemComponent={FaqItem} />
)}
ItemComponent={FaqItem}
/>
)}
</CommonItem>

View File

@ -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;

View File

@ -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;

View File

@ -2,7 +2,9 @@ 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 DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -89,6 +91,10 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={sortableProps => (
<SortableItem {...sortableProps} ItemComponent={Item} />
)}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -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;

View File

@ -1,38 +0,0 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import FaqItem, { ItemTypeProps } from './Item';
type SortableItemProps = ItemTypeProps & {};
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 (
<FaqItem
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
};
export default SortableItem;

View File

@ -2,7 +2,9 @@ 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 DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -90,6 +92,10 @@ const MetricsConfig = ({ setIsEdit, id }: ConfigProps) => {
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={sortableProps => (
<SortableItem {...sortableProps} ItemComponent={Item} />
)}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -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;

View File

@ -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;

View File

@ -2,7 +2,9 @@ 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 DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -88,10 +90,14 @@ const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
{list.length === 0 ? (
<Empty />
) : (
<FaqDragList
<DragList
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={sortableProps => (
<SortableItem {...sortableProps} ItemComponent={Item} />
)}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -1,114 +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 from './Item';
import { DomainRecommendNodeListResp } from '@/request/types';
import SortableItem from './SortableItem';
interface DragListProps {
data: DomainRecommendNodeListResp[];
onChange: (data: DomainRecommendNodeListResp[]) => 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: DomainRecommendNodeListResp) => {
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;

View File

@ -1,38 +0,0 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import FaqItem, { ItemProps } from './Item';
type SortableItemProps = ItemProps & {};
const FaqSortableItem: 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 (
<FaqItem
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
};
export default FaqSortableItem;

View File

@ -2,7 +2,9 @@ import React, { useEffect, useState } 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 DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import { Empty } from '@ctzhian/ui';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
@ -124,6 +126,10 @@ const SimpleDocConfigConfig = ({ setIsEdit, id }: ConfigProps) => {
setValue('nodes', value);
}}
setIsEdit={setIsEdit}
SortableItemComponent={sortableProps => (
<SortableItem {...sortableProps} ItemComponent={Item} />
)}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -3,7 +3,6 @@ import { useURLSearchParams } from '@/hooks';
import { ConstsUserRole } from '@/request/types';
import { useAppDispatch, useAppSelector } from '@/store';
import { setKbC, setKbId } from '@/store/slices/config';
import custom from '@/themes/custom';
import { Ellipsis, Icon, message } from '@ctzhian/ui';
import {
Box,
@ -117,7 +116,7 @@ const KBSelect = () => {
borderRadius: '5px',
bgcolor: 'background.paper3',
'&:hover': {
bgcolor: custom.selectedMenuItemBgColor,
bgcolor: 'rgba(50,72,242,0.1)',
},
}}
fullWidth

View File

@ -1,6 +0,0 @@
import custom from './custom';
import dark from './dark';
import light from './light';
export { custom, dark, light };
export type ThemeColor = typeof light;

View File

@ -1,4 +0,0 @@
export default {
selectPopupBoxShadow: '0px 10px 20px 0px rgba(54,59,76,0.2)',
selectedMenuItemBgColor: 'rgba(50,72,242,0.1)',
};

View File

@ -1,91 +0,0 @@
const dark = {
cssVariables: true,
primary: {
main: '#fdfdfd',
contrastText: '#000',
},
secondary: {
main: '#2196F3',
lighter: '#D6E4FF',
light: '#84A9FF',
dark: '#1939B7',
darker: '#091A7A',
contrastText: '#fff',
},
info: {
main: '#1890FF',
lighter: '#D0F2FF',
light: '#74CAFF',
dark: '#0C53B7',
darker: '#04297A',
contrastText: '#fff',
},
success: {
main: '#00DF98',
lighter: '#E9FCD4',
light: '#AAF27F',
dark: '#229A16',
darker: '#08660D',
contrastText: 'rgba(0,0,0,0.7)',
},
warning: {
main: '#F7B500',
lighter: '#FFF7CD',
light: '#FFE16A',
dark: '#B78103',
darker: '#7A4F01',
contrastText: 'rgba(0,0,0,0.7)',
},
neutral: {
main: '#1A1A1A',
contrastText: 'rgba(255, 255, 255, 0.60)',
},
error: {
main: '#D93940',
lighter: '#FFE7D9',
light: '#FFA48D',
dark: '#B72136',
darker: '#7A0C2E',
contrastText: '#fff',
},
text: {
primary: '#fff',
secondary: 'rgba(255,255,255,0.7)',
auxiliary: 'rgba(255,255,255,0.5)',
disabled: 'rgba(255,255,255,0.26)',
slave: 'rgba(255,255,255,0.05)',
inverseAuxiliary: 'rgba(0,0,0,0.5)',
inverseDisabled: 'rgba(0,0,0,0.15)',
},
divider: '#ededed',
background: {
paper: '#18181b',
paper2: '#060608',
paper3: '#27272a',
default: 'rgba(255,255,255,0.6)',
disabled: 'rgba(15,15,15,0.8)',
chip: 'rgba(145,147,171,0.16)',
circle: '#3B476A',
focus: '#542996',
},
common: {},
shadows: 'transparent',
table: {
head: {
backgroundColor: '#484848',
color: '#fff',
},
row: {
backgroundColor: 'transparent',
hoverColor: 'rgba(48, 58, 70, 0.4)',
},
cell: {
borderColor: '#484848',
},
},
charts: {
color: ['#7267EF', '#36B37E'],
},
};
export default dark;

View File

@ -1,95 +0,0 @@
const light = {
cssVariables: true,
primary: {
main: '#3248F2',
contrastText: '#fff',
lighter: '#E6E8EC',
},
secondary: {
main: '#3366FF',
lighter: '#D6E4FF',
light: '#84A9FF',
dark: '#1939B7',
darker: '#091A7A',
contrastText: '#fff',
},
info: {
main: '#0063FF',
lighter: '#D0F2FF',
light: '#74CAFF',
dark: '#0C53B7',
darker: '#04297A',
contrastText: '#fff',
},
success: {
main: '#82DDAF',
lighter: '#E9FCD4',
light: '#AAF27F',
mainShadow: '#36B37E',
dark: '#229A16',
darker: '#08660D',
contrastText: 'rgba(0,0,0,0.7)',
},
warning: {
main: '#FEA145',
lighter: '#FFF7CD',
light: '#FFE16A',
shadow: 'rgba(255, 171, 0, 0.15)',
dark: '#B78103',
darker: '#7A4F01',
contrastText: 'rgba(0,0,0,0.7)',
},
neutral: {
main: '#FFFFFF',
contrastText: 'rgba(0, 0, 0, 0.60)',
},
error: {
main: '#FE4545',
lighter: '#FFE7D9',
light: '#FFA48D',
shadow: 'rgba(255, 86, 48, 0.15)',
dark: '#B72136',
darker: '#7A0C2E',
contrastText: '#FFFFFF',
},
divider: '#ECEEF1',
text: {
primary: '#21222D',
secondary: 'rgba(33,34,35,0.7)',
auxiliary: 'rgba(33,34,35,0.5)',
slave: 'rgba(33,34,35,0.3)',
disabled: 'rgba(33,34,35,0.2)',
inverse: '#FFFFFF',
inverseAuxiliary: 'rgba(255,255,255,0.5)',
inverseDisabled: 'rgba(255,255,255,0.15)',
},
background: {
paper: '#FFFFFF',
paper2: '#F1F2F8',
paper3: '#F8F9FA',
default: '#FFFFFF',
chip: '#FFFFFF',
circle: '#E6E8EC',
hover: 'rgba(243, 244, 245, 0.5)',
},
shadows: 'rgba(68, 80 ,91, 0.1)',
table: {
head: {
height: '50px',
backgroundColor: '#FFFFFF',
color: '#000',
},
row: {
hoverColor: '#F8F9FA',
},
cell: {
height: '72px',
borderColor: '#ECEEF1',
},
},
charts: {
color: ['#673AB7', '#36B37E'],
},
};
export default light;

View File

@ -1,460 +0,0 @@
import { addOpacityToColor } from '@/utils';
import { custom, ThemeColor } from './color';
declare module '@mui/material/styles' {}
declare module '@mui/material/styles' {
interface TypeBackground {
paper0?: string;
paper2?: string;
chip?: string;
circle?: string;
hover?: string;
focus?: string;
disabled?: string;
}
}
const componentStyleOverrides = (theme: ThemeColor) => {
return {
MuiTabs: {
styleOverrides: {
root: {
borderRadius: '10px !important',
overflow: 'hidden',
minHeight: '36px',
height: '36px',
padding: '0px !important',
},
indicator: {
borderRadius: '0px !important',
overflow: 'hidden',
backgroundColor: '#21222D !important',
},
},
},
MuiTab: {
styleOverrides: {
root: {
borderRadius: '0px !important',
fontWeight: 'normal',
fontSize: '14px !important',
lineHeight: '34px',
padding: '0 16px !important',
},
},
},
MuiFormLabel: {
styleOverrides: {
asterisk: {
color: theme.error.main,
},
},
},
MuiButton: {
styleOverrides: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
root: ({ ownerState }: { ownerState: any }) => {
return {
height: '36px',
fontSize: 14,
lineHeight: '36px',
paddingLeft: '16px',
paddingRight: '16px',
boxShadow: 'none',
transition: 'all 0.2s ease-in-out',
borderRadius: '10px',
fontWeight: '400',
...(ownerState.variant === 'contained' && {
color: theme.text.inverse,
backgroundColor: theme.text.primary,
}),
...(ownerState.variant === 'text' && {}),
...(ownerState.variant === 'outlined' && {
color: theme.text.primary,
border: `1px solid ${theme.text.primary}`,
}),
...(ownerState.disabled === true && {
cursor: 'not-allowed !important',
}),
...(ownerState.size === 'small' && {
height: '32px',
lineHeight: '32px',
}),
'&:hover': {
boxShadow: 'none',
...(ownerState.variant === 'contained' && {
backgroundColor: addOpacityToColor(theme.text.primary, 0.9),
}),
...(ownerState.variant === 'text' && {
backgroundColor: theme.background.paper3,
}),
...(ownerState.variant === 'outlined' && {
backgroundColor: theme.background.paper3,
}),
...(ownerState.color === 'neutral' && {
color: theme.text.primary,
}),
},
};
},
startIcon: {
marginLeft: 0,
marginRight: 8,
'>*:nth-of-type(1)': {
fontSize: 14,
},
},
},
},
MuiTooltip: {
styleOverrides: {
tooltip: {
borderRadius: '10px',
maxWidth: '600px',
padding: '8px 16px',
backgroundColor: theme.text.primary,
fontSize: '12px',
lineHeight: '20px',
color: theme.primary.contrastText,
},
arrow: {
color: theme.text.primary,
},
},
},
MuiFormHelperText: {
styleOverrides: {
root: {
color: theme.error.main,
},
},
},
MuiFormControl: {
styleOverrides: {
root: {
'.MuiFormLabel-asterisk': {
color: theme.error.main,
},
},
},
},
MuiFormControlLabel: {
styleOverrides: {
root: {
marginLeft: '0 !important',
},
},
},
MuiTableBody: {
styleOverrides: {
root: {
'.MuiTableRow-root:hover': {
'.MuiTableCell-root:not(.cx-table-empty-td)': {
backgroundColor: theme.table.row.hoverColor,
overflowX: 'hidden',
'.primary-color': {
color: theme.primary.main,
},
'.no-title-url': {
color: `${theme.primary.main} !important`,
},
'.error-color': {
opacity: 1,
},
},
},
},
},
},
MuiCheckbox: {
styleOverrides: {
root: {
padding: 0,
svg: {
fontSize: '18px',
},
},
},
},
MuiTableCell: {
styleOverrides: {
root: {
background: theme.background.paper,
lineHeight: 1.5,
height: theme.table.cell.height,
fontSize: '14px',
paddingTop: '16px !important',
paddingBottom: '16px !important',
paddingLeft: 0,
'&:first-of-type': {
paddingLeft: '0px',
},
'&:not(:first-of-type)': {
paddingLeft: '0px',
},
'.MuiCheckbox-root': {
color: '#CCCCCC',
svg: {
fontSize: '16px',
},
'&.Mui-checked': {
color: theme.text.primary,
},
},
},
head: {
backgroundColor: theme.background.paper3,
color: theme.table.head.color,
fontSize: '12px',
height: theme.table.head.height,
paddingTop: '0 !important',
paddingBottom: '0 !important',
borderSpacing: '12px',
zIndex: 100,
},
body: {
borderBottom: '1px dashed',
borderColor: theme.table.cell.borderColor,
borderSpacing: '12px',
},
},
},
MuiPopover: {
styleOverrides: {
paper: {
borderRadius: '10px',
boxShadow: custom.selectPopupBoxShadow,
},
},
},
MuiMenu: {
styleOverrides: {
paper: {
padding: '4px',
borderRadius: '10px',
backgroundColor: theme.background.paper,
boxShadow: custom.selectPopupBoxShadow,
},
list: {
paddingTop: '0px !important',
paddingBottom: '0px !important',
},
},
defaultProps: {
elevation: 0,
},
},
MuiMenuItem: {
styleOverrides: {
root: {
height: '40px',
borderRadius: '5px',
':hover': {
backgroundColor: theme.background.paper3,
},
'&.Mui-selected': {
fontWeight: '500',
backgroundColor: `${custom.selectedMenuItemBgColor} !important`,
color: theme.primary.main,
},
},
},
},
MuiPaper: {
defaultProps: {
elevation: 1,
},
styleOverrides: {
root: ({ ownerState }: { ownerState: { elevation?: number } }) => {
return {
...(ownerState.elevation === 0 && {
backgroundColor: theme.background.paper2,
}),
...(ownerState.elevation === 2 && {
backgroundColor: theme.background.paper3,
}),
backgroundImage: 'none',
};
},
},
},
MuiChip: {
styleOverrides: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
root: ({ ownerState }: { ownerState: any }) => {
return {
height: '24px',
lineHeight: '24px',
borderRadius: '8px',
'.MuiChip-label': {
padding: '0 8px 0 4px',
},
...(ownerState.color === 'default' && {
backgroundColor: theme.background.chip,
borderColor: theme.text.disabled,
'.Mui-focusVisible': {
backgroundColor: theme.background.chip,
},
}),
...(ownerState.color === 'error' && {
backgroundColor: addOpacityToColor(theme.error.main, 0.1),
}),
};
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
label: ({ ownerState }: { ownerState: any }) => {
return {
padding: '0 14px',
fontSize: '14px',
lineHeight: '24px',
...(ownerState.color === 'default' && {
color: theme.text.primary,
}),
};
},
deleteIcon: {
fontSize: '14px',
color: theme.text.disabled,
},
},
},
MuiAppBar: {
defaultProps: {
elevation: 1,
},
},
MuiDialog: {
styleOverrides: {
root: {
'h2.MuiTypography-root button': {
marginRight: '2px',
},
'.MuiDialogActions-root': {
paddingTop: '24px',
button: {
width: '88px',
height: '36px !important',
},
'.MuiButton-text': {
width: 'auto',
minWidth: 'auto',
color: `${theme.text.primary} !important`,
},
},
},
container: {
height: '100vh',
bgcolor: theme.text.secondary,
backdropFilter: 'blur(5px)',
},
paper: {
pb: 1,
border: '1px solid',
borderColor: theme.divider,
borderRadius: '10px',
backgroundColor: theme.background.paper,
textarea: {
borderRadius: '8px 8px 0 8px',
},
},
},
},
MuiDialogTitle: {
styleOverrides: {
root: {
paddingTop: '24px',
'> button': {
top: '20px',
},
},
},
},
MuiAlert: {
styleOverrides: {
root: {
lineHeight: '22px',
paddingTop: '1px',
paddingBottom: '1px',
borderRadius: '10px',
boxShadow: 'none',
},
icon: {
padding: '10px 0',
},
standardInfo: {
backgroundColor: addOpacityToColor(theme.primary.main, 0.1),
color: theme.text.primary,
},
},
},
MuiRadio: {
styleOverrides: {
root: {
padding: 0,
marginRight: '8px',
},
},
},
MuiTextField: {
styleOverrides: {
root: {
label: {
color: theme.text.secondary,
},
'label.Mui-focused': {
color: theme.text.primary,
},
'& .MuiInputBase-input::placeholder': {
fontSize: '12px',
},
},
},
},
MuiInputBase: {
styleOverrides: {
root: {
borderRadius: '10px !important',
backgroundColor: theme.background.paper3,
'.MuiOutlinedInput-notchedOutline': {
borderColor: `${theme.background.paper3} !important`,
borderWidth: '1px !important',
},
'&.Mui-focused': {
'.MuiOutlinedInput-notchedOutline': {
borderColor: `${theme.text.primary} !important`,
borderWidth: '1px !important',
},
},
'&:hover': {
'.MuiOutlinedInput-notchedOutline': {
borderColor: `${theme.text.primary} !important`,
borderWidth: '1px !important',
},
},
input: {
height: '19px',
'&.Mui-disabled': {
color: `${theme.text.secondary} !important`,
WebkitTextFillColor: `${theme.text.secondary} !important`,
},
},
},
},
},
MuiSelect: {
styleOverrides: {
root: {
height: '36px',
borderRadius: '10px !important',
backgroundColor: theme.background.paper3,
},
select: {
paddingRight: '0 !important',
},
},
},
};
};
export default componentStyleOverrides;

View File

@ -1,4 +1,6 @@
/// <reference types="vite/client" />
/// <reference types="@panda-wiki/themes/types" />
declare module 'swiper/css' {
const content: string;
export default content;

View File

@ -1,27 +1,20 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
/* Vite + React */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"incremental": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,

View File

@ -1,21 +1,14 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"incremental": true,
/* Vite */
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,

View File

@ -1,3 +1,5 @@
/// <reference types="@panda-wiki/themes/types" />
declare module '@cap.js/widget' {
interface CapOptions {
apiEndpoint: string;

View File

@ -25,7 +25,6 @@
"highlight.js": "^11.11.1",
"html-react-parser": "^5.2.5",
"html-to-image": "^1.11.13",
"import-in-the-middle": "^1.14.2",
"js-cookie": "^3.0.5",
"katex": "^0.16.22",
"markdown-it": "13.0.1",
@ -42,7 +41,6 @@
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"require-in-the-middle": "^7.5.2",
"uuid": "^11.1.0"
},
"devDependencies": {

View File

@ -70,7 +70,6 @@ const Layout = async ({
const [kbDetailResolve, authInfoResolve] = await Promise.allSettled([
getShareV1AppWebInfo(),
// @ts-ignore
getShareProV1AuthInfo({}),
]);

View File

@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import dayjs from 'dayjs';
import { ChunkResultItem } from '@/assets/type';
import Logo from '@/assets/images/logo.png';
import aiLoading from '@/assets/images/ai-loading.gif';
import { getShareV1ConversationDetail } from '@/request/ShareConversation';
import { message } from '@ctzhian/ui';
@ -25,21 +26,33 @@ import MarkDown2 from '@/components/markdown2';
import { postShareV1ChatFeedback } from '@/request/ShareChat';
import { copyText } from '@/utils';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { Box, IconButton, Stack, Typography, Tooltip } from '@mui/material';
import {
Box,
Button,
IconButton,
Stack,
Typography,
Tooltip,
alpha,
} from '@mui/material';
import 'dayjs/locale/zh-cn';
import relativeTime from 'dayjs/plugin/relativeTime';
import ChatLoading from '../../views/chat/ChatLoading';
import { IconTupian, IconFasong } from '@panda-wiki/icons';
import {
IconTupian,
IconFasong,
IconXingxing,
IconXinduihua,
} from '@panda-wiki/icons';
import CloseIcon from '@mui/icons-material/Close';
import Image from 'next/image';
import {
StyledMainContainer,
StyledConversationContainer,
StyledConversationItem,
StyledAccordion,
StyledAccordionSummary,
StyledAccordionDetails,
StyledQuestionText,
StyledUserBubble,
StyledAiBubble,
StyledAiBubbleContent,
StyledChunkAccordion,
StyledChunkAccordionSummary,
StyledChunkAccordionDetails,
@ -57,8 +70,9 @@ import {
StyledActionButtonStack,
StyledFuzzySuggestionsStack,
StyledFuzzySuggestionItem,
StyledHotSearchStack,
StyledHotSearchItem,
StyledHotSearchContainer,
StyledHotSearchColumn,
StyledHotSearchColumnItem,
} from './StyledComponents';
export interface ConversationItem {
@ -91,7 +105,13 @@ const LoadingContent = ({
return (
<Stack direction='row' alignItems='center' gap={1} sx={{ pb: 1 }}>
<Image src={aiLoading} alt='ai-loading' width={20} height={20} />
<Typography variant='body2' sx={{ fontSize: 12, color: 'text.tertiary' }}>
<Typography
variant='body2'
sx={theme => ({
fontSize: 12,
color: alpha(theme.palette.text.primary, 0.5),
})}
>
{AnswerStatus[thinking]}
</Typography>
</Stack>
@ -141,6 +161,10 @@ const AiQaContent: React.FC<{
});
const onReset = () => {
if (loading) {
handleSearchAbort();
}
handleSearch(true);
setConversationId('');
setConversation([]);
setFullAnswer('');
@ -156,9 +180,9 @@ const AiQaContent: React.FC<{
setNonce('');
};
const handleSearch = () => {
const handleSearch = (reset: boolean = false) => {
if (input.length > 0) {
onSearch(input);
onSearch(input, reset);
setInput('');
// 清理图片URL
uploadedImages.forEach(img => {
@ -663,156 +687,296 @@ const AiQaContent: React.FC<{
return (
<StyledMainContainer className={palette.mode === 'dark' ? 'md-dark' : ''}>
{/* 无对话时显示欢迎界面 */}
{conversation.length === 0 && (
<Box
sx={{
flex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
pb: 5,
}}
>
{/* Logo区域 */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, my: 8 }}>
<Image
src={kbDetail?.settings?.icon || Logo.src}
alt='logo'
width={46}
height={46}
unoptimized
style={{
objectFit: 'contain',
}}
/>
<Typography
variant='h6'
sx={{ fontSize: 32, color: 'text.primary', fontWeight: 700 }}
>
{kbDetail?.settings?.title}
</Typography>
</Box>
{/* 热门搜索区域 */}
{hotSearch.length > 0 && (
<Box sx={{ width: '100%' }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
mb: 2,
}}
>
<Typography
sx={{
fontSize: 12,
fontWeight: 500,
color: 'primary.main',
display: 'flex',
alignItems: 'center',
gap: 0.5,
}}
>
<IconXingxing sx={{ fontSize: 14 }} />
?
</Typography>
</Box>
{/* 热门搜索列表 - 两列布局 */}
<StyledHotSearchContainer>
{/* 左列 */}
<StyledHotSearchColumn>
{hotSearch
.filter((_, index) => index % 2 === 0)
.map((suggestion, index) => (
<StyledHotSearchColumnItem
key={index * 2}
onClick={() => onSuggestionClick(suggestion)}
>
{suggestion}
</StyledHotSearchColumnItem>
))}
</StyledHotSearchColumn>
{/* 右列 */}
<StyledHotSearchColumn>
{hotSearch
.filter((_, index) => index % 2 === 1)
.map((suggestion, index) => (
<StyledHotSearchColumnItem
key={index * 2 + 1}
onClick={() => onSuggestionClick(suggestion)}
>
{suggestion}
</StyledHotSearchColumnItem>
))}
</StyledHotSearchColumn>
</StyledHotSearchContainer>
</Box>
)}
</Box>
)}
{/* 有对话时显示对话历史 */}
<StyledConversationContainer
direction='column'
gap={2}
className='conversation-container'
sx={{
mb: conversation?.length > 0 ? 2 : 0,
display: conversation.length > 0 ? 'flex' : 'none',
}}
>
{conversation.map((item, index) => (
<StyledConversationItem key={index}>
<StyledAccordion key={index} defaultExpanded={true}>
<StyledAccordionSummary
expandIcon={<ExpandMoreIcon sx={{ fontSize: 18 }} />}
>
<StyledQuestionText>{item.q}</StyledQuestionText>
</StyledAccordionSummary>
<StyledAccordionDetails>
{item.chunk_result.length > 0 && (
<StyledChunkAccordion defaultExpanded>
<StyledChunkAccordionSummary
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
{/* 用户问题气泡 - 右对齐 */}
<StyledUserBubble>{item.q}</StyledUserBubble>
{/* AI回答气泡 - 左对齐 */}
<StyledAiBubble>
{/* 搜索结果 */}
{item.chunk_result.length > 0 && (
<StyledChunkAccordion defaultExpanded>
<StyledChunkAccordionSummary
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
>
<Typography
variant='body2'
sx={theme => ({
fontSize: 12,
color: alpha(theme.palette.text.primary, 0.5),
})}
>
{item.chunk_result.length}
</Typography>
</StyledChunkAccordionSummary>
<StyledChunkAccordionDetails>
<Stack gap={1}>
{item.chunk_result.map((chunk, chunkIndex) => (
<StyledChunkItem key={chunkIndex}>
<Typography
variant='body2'
className='hover-primary'
sx={theme => ({
fontSize: 12,
color: alpha(theme.palette.text.primary, 0.5),
})}
onClick={() => {
window.open(`/node/${chunk.node_id}`, '_blank');
}}
>
{chunk.name}
</Typography>
</StyledChunkItem>
))}
</Stack>
</StyledChunkAccordionDetails>
</StyledChunkAccordion>
)}
{/* 加载状态 */}
{index === conversation.length - 1 && loading && (
<LoadingContent thinking={thinking} />
)}
{/* 思考过程 */}
{!!item.thinking_content && (
<StyledThinkingAccordion defaultExpanded>
<StyledThinkingAccordionSummary
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
>
<Stack direction='row' alignItems='center' gap={1}>
{thinking === 2 && index === conversation.length - 1 && (
<Image
src={aiLoading}
alt='ai-loading'
width={20}
height={20}
/>
)}
<Typography
variant='body2'
sx={{ fontSize: 12, color: 'text.tertiary' }}
sx={theme => ({
fontSize: 12,
color: alpha(theme.palette.text.primary, 0.5),
})}
>
{item.chunk_result.length}
{thinking === 2 && index === conversation.length - 1
? '思考中...'
: '已思考'}
</Typography>
</StyledChunkAccordionSummary>
</Stack>
</StyledThinkingAccordionSummary>
<StyledChunkAccordionDetails>
<Stack gap={1}>
{item.chunk_result.map((chunk, index) => (
<StyledChunkItem key={index}>
<Typography
variant='body2'
className='hover-primary'
sx={{ fontSize: 12, color: 'text.tertiary' }}
onClick={() => {
window.open(`/node/${chunk.node_id}`, '_blank');
}}
>
{chunk.name}
</Typography>
</StyledChunkItem>
))}
</Stack>
</StyledChunkAccordionDetails>
</StyledChunkAccordion>
)}
{index === conversation.length - 1 && loading && (
<>
<LoadingContent thinking={thinking} />
</>
)}
{!!item.thinking_content && (
<StyledThinkingAccordion defaultExpanded>
<StyledThinkingAccordionSummary
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
>
<Stack direction='row' alignItems='center' gap={1}>
{thinking === 2 && (
<Image
src={aiLoading}
alt='ai-loading'
width={20}
height={20}
/>
)}
<Typography
variant='body2'
sx={{ fontSize: 12, color: 'text.tertiary' }}
>
{thinking === 2 ? '思考中...' : '已思考'}
</Typography>
</Stack>
</StyledThinkingAccordionSummary>
<StyledThinkingAccordionDetails>
<MarkDown2
content={item.thinking_content || ''}
autoScroll={false}
/>
</StyledThinkingAccordionDetails>
</StyledThinkingAccordion>
)}
<StyledThinkingAccordionDetails>
<MarkDown2
content={item.thinking_content || ''}
autoScroll={false}
/>
</StyledThinkingAccordionDetails>
</StyledThinkingAccordion>
)}
{/* AI回答内容 */}
<StyledAiBubbleContent>
{item.source === 'history' ? (
<MarkDown content={item.a} />
) : (
<MarkDown2 content={item.a} autoScroll={false} />
)}
</StyledAccordionDetails>
</StyledAccordion>
{(index !== conversation.length - 1 || !loading) && (
<StyledActionStack
direction={mobile ? 'column' : 'row'}
alignItems={mobile ? 'flex-start' : 'center'}
justifyContent='space-between'
gap={mobile ? 1 : 3}
>
<Stack direction='row' gap={3} alignItems='center'>
<span> {dayjs(item.update_time).fromNow()}</span>
</StyledAiBubbleContent>
<IconCopy
sx={{ cursor: 'pointer' }}
onClick={() => {
copyText(item.a);
}}
/>
{/* 操作按钮 */}
{(index !== conversation.length - 1 || !loading) && (
<StyledActionStack
direction={mobile ? 'column' : 'row'}
alignItems={mobile ? 'flex-start' : 'center'}
justifyContent='space-between'
gap={mobile ? 1 : 3}
>
<Stack direction='row' gap={3} alignItems='center'>
<span> {dayjs(item.update_time).fromNow()}</span>
{isFeedbackEnabled && item.source === 'chat' && (
<>
{item.score === 1 && (
<IconZaned sx={{ cursor: 'pointer' }} />
)}
{item.score !== 1 && (
<IconZan
sx={{ cursor: 'pointer' }}
onClick={() => {
if (item.score === 0)
handleScore(item.message_id, 1);
}}
/>
)}
{item.score !== -1 && (
<IconCai
sx={{ cursor: 'pointer' }}
onClick={() => {
if (item.score === 0) {
setConversationItem(item);
setOpen(true);
}
}}
/>
)}
{item.score === -1 && (
<IconCaied sx={{ cursor: 'pointer' }} />
)}
</>
)}
</Stack>
</StyledActionStack>
)}
<IconCopy
sx={{ cursor: 'pointer' }}
onClick={() => {
copyText(item.a);
}}
/>
{isFeedbackEnabled && item.source === 'chat' && (
<>
{item.score === 1 && (
<IconZaned sx={{ cursor: 'pointer' }} />
)}
{item.score !== 1 && (
<IconZan
sx={{ cursor: 'pointer' }}
onClick={() => {
if (item.score === 0)
handleScore(item.message_id, 1);
}}
/>
)}
{item.score !== -1 && (
<IconCai
sx={{ cursor: 'pointer' }}
onClick={() => {
if (item.score === 0) {
setConversationItem(item);
setOpen(true);
}
}}
/>
)}
{item.score === -1 && (
<IconCaied sx={{ cursor: 'pointer' }} />
)}
</>
)}
</Stack>
</StyledActionStack>
)}
</StyledAiBubble>
</StyledConversationItem>
))}
</StyledConversationContainer>
{conversation.length > 0 && (
<Button
variant='contained'
sx={theme => ({
textTransform: 'none',
minWidth: 'auto',
px: 3.5,
py: '2px',
gap: 0.5,
fontSize: 12,
backgroundColor: 'background.default',
color: 'text.primary',
boxShadow: `0px 1px 2px 0px ${alpha(theme.palette.text.primary, 0.06)}`,
border: '1px solid',
borderColor: alpha(theme.palette.text.primary, 0.1),
cursor: 'pointer',
'&:hover': {
boxShadow: `0px 1px 2px 0px ${alpha(theme.palette.text.primary, 0.06)}`,
borderColor: 'primary.main',
color: 'primary.main',
},
mb: 2,
})}
onClick={onReset}
>
<IconXinduihua sx={{ fontSize: 14 }} />
</Button>
)}
<StyledInputContainer>
<StyledInputWrapper>
{/* 多张图片预览 */}
@ -932,12 +1096,6 @@ const AiQaContent: React.FC<{
</StyledActionButtonStack>
</StyledInputWrapper>
</StyledInputContainer>
<Feedback
open={open}
onClose={() => setOpen(false)}
onSubmit={handleScore}
data={conversationItem}
/>
{/* 模糊搜索建议列表 */}
{showFuzzySuggestions &&
fuzzySuggestions.length > 0 &&
@ -954,29 +1112,12 @@ const AiQaContent: React.FC<{
</StyledFuzzySuggestionsStack>
)}
{/* 原始搜索建议列表 - 只在没有模糊搜索建议时显示 */}
{!showFuzzySuggestions &&
hotSearch.length > 0 &&
conversation.length === 0 && (
<StyledHotSearchStack gap={1}>
{hotSearch.map((suggestion, index) => (
<StyledHotSearchItem
key={index}
onClick={() => onSuggestionClick(suggestion)}
>
<Typography
variant='body2'
sx={{
fontSize: 14,
flex: 1,
}}
>
{suggestion}
</Typography>
</StyledHotSearchItem>
))}
</StyledHotSearchStack>
)}
<Feedback
open={open}
onClose={() => setOpen(false)}
onSubmit={handleScore}
data={conversationItem}
/>
</StyledMainContainer>
);
};

View File

@ -8,7 +8,11 @@ import {
Typography,
Stack,
CircularProgress,
alpha,
Skeleton,
styled,
} from '@mui/material';
import Logo from '@/assets/images/logo.png';
import noDocImage from '@/assets/images/no-doc.png';
import Image from 'next/image';
import { IconJinsousuo, IconFasong, IconMianbaoxie } from '@panda-wiki/icons';
@ -16,7 +20,51 @@ import { postShareV1ChatSearch } from '@/request/ShareChatSearch';
import { DomainNodeContentChunkSSE } from '@/request/types';
import { message } from '@ctzhian/ui';
import { IconWenjian } from '@panda-wiki/icons';
import { useStore } from '@/provider';
const StyledSearchResultItem = styled(Stack)(({ theme }) => ({
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
borderBottom: '1px dashed',
borderColor: alpha(theme.palette.text.primary, 0.1),
},
'&::after': {
content: '""',
position: 'absolute',
bottom: 0,
left: 0,
width: '100%',
borderBottom: '1px dashed',
borderColor: alpha(theme.palette.text.primary, 0.1),
},
padding: theme.spacing(2),
borderRadius: '8px',
cursor: 'pointer',
transition: 'all 0.2s ease-in-out',
'&:hover': {
backgroundColor: alpha(theme.palette.text.primary, 0.02),
'.hover-primary': {
color: 'primary.main',
},
},
}));
const SearchDocSkeleton = () => {
return (
<StyledSearchResultItem>
<Stack gap={1}>
<Skeleton variant='rounded' height={16} width={200} />
<Skeleton variant='rounded' height={22} width={400} />
<Skeleton variant='rounded' height={16} width={500} />
</Stack>
</StyledSearchResultItem>
);
};
interface SearchDocContentProps {
inputRef: React.RefObject<HTMLInputElement | null>;
placeholder: string;
@ -26,6 +74,7 @@ const SearchDocContent: React.FC<SearchDocContentProps> = ({
inputRef,
placeholder,
}) => {
const { kbDetail } = useStore();
// 模糊搜索相关状态
const [fuzzySuggestions, setFuzzySuggestions] = useState<string[]>([]);
const [showFuzzySuggestions, setShowFuzzySuggestions] = useState(false);
@ -148,6 +197,30 @@ const SearchDocContent: React.FC<SearchDocContentProps> = ({
return (
<Box>
<Stack
direction='row'
alignItems='center'
justifyContent='center'
gap={2}
sx={{ mb: 3, mt: 1 }}
>
<Image
src={kbDetail?.settings?.icon || Logo.src}
alt='logo'
width={46}
height={46}
unoptimized
style={{
objectFit: 'contain',
}}
/>
<Typography
variant='h6'
sx={{ fontSize: 32, color: 'text.primary', fontWeight: 700 }}
>
{kbDetail?.settings?.title}
</Typography>
</Stack>
{/* 搜索输入框 */}
<TextField
ref={inputRef}
@ -157,26 +230,27 @@ const SearchDocContent: React.FC<SearchDocContentProps> = ({
onKeyDown={handleKeyDown}
fullWidth
autoFocus
sx={{
sx={theme => ({
boxShadow: `0px 20px 40px 0px ${alpha(theme.palette.text.primary, 0.06)}`,
borderRadius: 2,
'& .MuiInputBase-root': {
fontSize: 16,
bgcolor: 'background.paper3',
backgroundColor: theme.palette.background.default,
'& fieldset': {
borderColor: 'background.paper3',
borderColor: alpha(theme.palette.text.primary, 0.1),
},
'&:hover fieldset': {
borderColor: 'primary.main',
},
'&.Mui-focused fieldset': {
borderColor: 'primary.main',
borderColor: `${theme.palette.primary.main} !important`,
borderWidth: 1,
},
},
'& .MuiInputBase-input': {
py: 1.5,
},
}}
})}
slotProps={{
input: {
startAdornment: (
@ -264,31 +338,15 @@ const SearchDocContent: React.FC<SearchDocContentProps> = ({
</Typography>
{/* 搜索结果列表 */}
<Stack sx={{ overflow: 'auto', maxHeight: 'calc(100vh - 284px)' }}>
<Stack sx={{ overflow: 'auto', maxHeight: 'calc(100vh - 334px)' }}>
{searchResults.map((result, index) => (
<Stack
<StyledSearchResultItem
direction='row'
justifyContent='space-between'
alignItems='center'
key={result.node_id}
gap={2}
onClick={() => handleSearchResultClick(result)}
sx={{
p: 2,
borderRadius: '8px',
borderBottom:
index !== searchResults.length - 1 ? 'none' : '1px dashed',
borderTop: '1px dashed',
borderColor: 'divider',
cursor: 'pointer',
transition: 'all 0.2s ease-in-out',
'&:hover': {
bgcolor: 'background.paper3',
'.hover-primary': {
color: 'primary.main',
},
},
}}
>
<Stack sx={{ flex: 1, width: 0 }} gap={0.5}>
{/* 路径 */}
@ -342,7 +400,7 @@ const SearchDocContent: React.FC<SearchDocContentProps> = ({
</Typography>
</Stack>
<IconMianbaoxie sx={{ fontSize: 12 }} />
</Stack>
</StyledSearchResultItem>
))}
</Stack>
</Box>
@ -359,11 +417,11 @@ const SearchDocContent: React.FC<SearchDocContentProps> = ({
{/* 搜索中状态 */}
{isSearching && (
<Box sx={{ mt: 2, textAlign: 'center' }}>
<Typography variant='body2' sx={{ color: 'text.tertiary' }}>
...
</Typography>
</Box>
<Stack sx={{ mt: 2 }}>
{[...Array(3)].map((_, index) => (
<SearchDocSkeleton key={index} />
))}
</Stack>
)}
</Box>
);

View File

@ -18,7 +18,7 @@ export const StyledMainContainer = styled(Box)(() => ({
}));
export const StyledConversationContainer = styled(Stack)(() => ({
maxHeight: 'calc(100vh - 334px)',
maxHeight: 'calc(100vh - 332px)',
overflow: 'auto',
scrollbarWidth: 'none',
msOverflowStyle: 'none',
@ -27,10 +27,38 @@ export const StyledConversationContainer = styled(Stack)(() => ({
},
}));
export const StyledConversationItem = styled(Box)(() => ({}));
export const StyledConversationItem = styled(Box)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
}));
// 聊天气泡相关组件
export const StyledUserBubble = styled(Box)(({ theme }) => ({
alignSelf: 'flex-end',
maxWidth: '75%',
padding: theme.spacing(1, 2),
borderRadius: '10px 10px 0px 10px',
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
fontSize: 14,
wordBreak: 'break-word',
}));
export const StyledAiBubble = styled(Box)(({ theme }) => ({
alignSelf: 'flex-start',
maxWidth: '85%',
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(3),
}));
export const StyledAiBubbleContent = styled(Box)(() => ({
wordBreak: 'break-word',
}));
// 对话相关组件
export const StyledAccordion = styled(Accordion)(({ theme }) => ({
export const StyledAccordion = styled(Accordion)(() => ({
padding: 0,
border: 'none',
'&:before': {
@ -71,7 +99,6 @@ export const StyledChunkAccordion = styled(Accordion)(({ theme }) => ({
background: 'transparent',
border: 'none',
padding: 0,
paddingBottom: theme.spacing(2),
}));
export const StyledChunkAccordionSummary = styled(AccordionSummary)(
@ -142,8 +169,7 @@ export const StyledThinkingAccordionDetails = styled(AccordionDetails)(
// 操作区域组件
export const StyledActionStack = styled(Stack)(({ theme }) => ({
fontSize: 12,
color: alpha(theme.palette.text.primary, 0.75),
marginTop: theme.spacing(2),
color: alpha(theme.palette.text.primary, 0.35),
}));
// 输入区域组件
@ -165,6 +191,7 @@ export const StyledInputWrapper = styled(Stack)(({ theme }) => ({
alignItems: 'flex-end',
gap: theme.spacing(2),
backgroundColor: theme.palette.background.default,
boxShadow: `0px 20px 40px 0px ${alpha(theme.palette.text.primary, 0.06)}`,
transition: 'border-color 0.2s ease-in-out',
'&:hover': {
borderColor: theme.palette.primary.main,
@ -282,3 +309,36 @@ export const StyledHotSearchItem = styled(Box)(({ theme }) => ({
alignItems: 'center',
width: 'auto',
}));
// 热门搜索容器
export const StyledHotSearchContainer = styled(Box)(({ theme }) => ({
display: 'flex',
gap: theme.spacing(2),
}));
// 热门搜索列
export const StyledHotSearchColumn = styled(Box)(({ theme }) => ({
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(1),
paddingLeft: theme.spacing(2),
borderLeft: `1px solid ${alpha(theme.palette.text.primary, 0.06)}`,
}));
// 热门搜索列项目
export const StyledHotSearchColumnItem = styled(Box)(({ theme }) => ({
paddingRight: theme.spacing(2),
borderRadius: '10px',
cursor: 'pointer',
transition: 'all 0.2s',
backgroundColor: 'transparent',
color: theme.palette.text.secondary,
fontSize: 12,
fontWeight: 400,
display: 'flex',
alignItems: 'center',
'&:hover': {
color: theme.palette.primary.main,
},
}));

View File

@ -1,286 +0,0 @@
import React from 'react';
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Stack,
Typography,
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import Image from 'next/image';
import dayjs from 'dayjs';
import { ConversationItem as ConversationItemType } from '../types';
import { ChunkResultItem } from '@/assets/type';
import MarkDown from '@/components/markdown';
import MarkDown2 from '@/components/markdown2';
import {
IconCai,
IconCaied,
IconCopy,
IconZan,
IconZaned,
} from '@/components/icons';
import { copyText } from '@/utils';
import aiLoading from '@/assets/images/ai-loading.gif';
import { LoadingContent } from './LoadingContent';
import { AnswerStatusType } from '../constants';
interface ConversationItemProps {
item: ConversationItemType;
index: number;
isLast: boolean;
loading: boolean;
thinking: AnswerStatusType;
thinkingContent: string;
answer: string;
chunkResult: ChunkResultItem[];
isChunkResult: boolean;
isThinking: boolean;
mobile: boolean;
themeMode: string;
isFeedbackEnabled: boolean;
onScoreChange: (message_id: string, score: number) => void;
onFeedbackOpen: (item: ConversationItemType) => void;
}
export const ConversationItemComponent: React.FC<ConversationItemProps> = ({
item,
index,
isLast,
loading,
thinking,
thinkingContent,
answer,
chunkResult,
isChunkResult,
isThinking,
mobile,
themeMode,
isFeedbackEnabled,
onScoreChange,
onFeedbackOpen,
}) => {
const displayChunkResult = isLast ? chunkResult : item.chunk_result;
const displayThinkingContent = isLast
? thinkingContent
: item.thinking_content;
const displayAnswer = isLast ? item.a || answer || '' : item.a;
return (
<Box>
<Accordion
defaultExpanded={true}
sx={{
bgcolor:
themeMode === 'dark' ? 'background.default' : 'background.paper3',
}}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon sx={{ fontSize: 24 }} />}
sx={{ userSelect: 'text' }}
>
<Box
sx={{
fontWeight: '700',
lineHeight: '24px',
wordBreak: 'break-all',
}}
>
{item.q}
</Box>
</AccordionSummary>
<AccordionDetails sx={{ pt: 2 }}>
{/* Chunk结果展示 */}
{displayChunkResult.length > 0 && (
<Accordion
sx={{
bgcolor: 'transparent',
border: 'none',
p: 0,
pb: 2,
}}
defaultExpanded
>
<AccordionSummary
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
sx={{
justifyContent: 'flex-start',
gap: 2,
'.MuiAccordionSummary-content': {
flexGrow: 0,
},
}}
>
<Typography
variant='body2'
sx={{ fontSize: 12, color: 'text.tertiary' }}
>
{displayChunkResult.length}
</Typography>
</AccordionSummary>
<AccordionDetails
sx={{
pt: 0,
pl: 2,
borderTop: 'none',
borderLeft: '1px solid',
borderColor: 'divider',
}}
>
<Stack gap={1}>
{displayChunkResult.map((chunk, idx) => (
<Box
key={idx}
sx={{
cursor: 'pointer',
'&:hover': {
'.hover-primary': {
color: 'primary.main',
},
},
}}
>
<Typography
variant='body2'
className='hover-primary'
sx={{ fontSize: 12, color: 'text.tertiary' }}
onClick={() => {
window.open(`/node/${chunk.node_id}`, '_blank');
}}
>
{chunk.name}
</Typography>
</Box>
))}
</Stack>
</AccordionDetails>
</Accordion>
)}
{/* 加载状态 */}
{isLast && loading && !isChunkResult && !thinkingContent && (
<LoadingContent thinking={thinking} />
)}
{/* 思考内容展示 */}
{displayThinkingContent && (
<Accordion
sx={{
bgcolor: 'transparent',
border: 'none',
p: 0,
pb: 2,
'&:before': {
content: '""',
height: 0,
},
}}
defaultExpanded
>
<AccordionSummary
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
sx={{
justifyContent: 'flex-start',
gap: 2,
'.MuiAccordionSummary-content': {
flexGrow: 0,
},
}}
>
<Stack direction='row' alignItems='center' gap={1}>
{isThinking && (
<Image
src={aiLoading}
alt='ai-loading'
width={20}
height={20}
/>
)}
<Typography
variant='body2'
sx={{ fontSize: 12, color: 'text.tertiary' }}
>
{isThinking ? '思考中...' : '已思考'}
</Typography>
</Stack>
</AccordionSummary>
<AccordionDetails
sx={{
pt: 0,
pl: 2,
borderTop: 'none',
borderLeft: '1px solid',
borderColor: 'divider',
'.markdown-body': {
opacity: 0.75,
fontSize: 12,
},
}}
>
<MarkDown2 content={displayThinkingContent} />
</AccordionDetails>
</Accordion>
)}
{/* 答案展示 */}
{item.source === 'history' ? (
<MarkDown content={item.a} />
) : (
<MarkDown2 content={displayAnswer} />
)}
</AccordionDetails>
</Accordion>
{/* 反馈区域 */}
{(!isLast || !loading) && (
<Stack
direction={mobile ? 'column' : 'row'}
alignItems={mobile ? 'flex-start' : 'center'}
justifyContent='space-between'
gap={mobile ? 1 : 3}
sx={{
fontSize: 12,
color: 'text.tertiary',
mt: 2,
}}
>
<Stack direction='row' gap={3} alignItems='center'>
<span> {dayjs(item.update_time).fromNow()}</span>
<IconCopy
sx={{ cursor: 'pointer' }}
onClick={() => copyText(item.a)}
/>
{isFeedbackEnabled && item.source === 'chat' && (
<>
{item.score === 1 && <IconZaned sx={{ cursor: 'pointer' }} />}
{item.score !== 1 && (
<IconZan
sx={{ cursor: 'pointer' }}
onClick={() => {
if (item.score === 0) onScoreChange(item.message_id, 1);
}}
/>
)}
{item.score !== -1 && (
<IconCai
sx={{ cursor: 'pointer' }}
onClick={() => {
if (item.score === 0) onFeedbackOpen(item);
}}
/>
)}
{item.score === -1 && <IconCaied sx={{ cursor: 'pointer' }} />}
</>
)}
</Stack>
</Stack>
)}
</Box>
);
};

View File

@ -1,219 +0,0 @@
import React from 'react';
import { Box, IconButton, Stack, TextField, Tooltip } from '@mui/material';
import { IconTupian, IconFasong } from '@panda-wiki/icons';
import CloseIcon from '@mui/icons-material/Close';
import Image from 'next/image';
import ChatLoading from '@/views/chat/ChatLoading';
import { UploadedImage } from '../types';
import { AnswerStatusType } from '../constants';
interface InputAreaProps {
input: string;
loading: boolean;
thinking: AnswerStatusType;
placeholder: string;
uploadedImages: UploadedImage[];
fileInputRef: React.RefObject<HTMLInputElement | null>;
inputRef: React.RefObject<HTMLInputElement | null>;
onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onInputFocus: () => void;
onInputBlur: () => void;
onPaste: (e: React.ClipboardEvent<HTMLDivElement>) => void;
onSearch: () => void;
onImageUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
onRemoveImage: (id: string) => void;
onSearchAbort: () => void;
setThinking: (value: AnswerStatusType) => void;
}
export const InputArea: React.FC<InputAreaProps> = ({
input,
loading,
thinking,
placeholder,
uploadedImages,
fileInputRef,
inputRef,
onInputChange,
onInputFocus,
onInputBlur,
onPaste,
onSearch,
onImageUpload,
onRemoveImage,
onSearchAbort,
setThinking,
}) => {
return (
<Stack
sx={{
px: 1.5,
py: 1,
borderRadius: '10px',
border: '1px solid',
borderColor: 'background.paper3',
display: 'flex',
alignItems: 'flex-end',
gap: 2,
bgcolor: 'background.paper3',
transition: 'border-color 0.2s ease-in-out',
'&:hover': {
borderColor: 'primary.main',
},
'&:focus-within': {
borderColor: 'dark.main',
},
}}
>
{uploadedImages.length > 0 && (
<Stack
direction='row'
flexWrap='wrap'
gap={1}
sx={{ width: '100%', zIndex: 1 }}
>
{uploadedImages.map(image => (
<Box
key={image.id}
sx={{
position: 'relative',
borderRadius: '8px',
overflow: 'hidden',
border: '1px solid',
borderColor: 'divider',
}}
>
<Image
src={image.url}
alt='uploaded'
width={40}
height={40}
style={{ objectFit: 'cover' }}
/>
<IconButton
size='small'
onClick={() => onRemoveImage(image.id)}
sx={{
position: 'absolute',
top: 2,
right: 2,
width: 16,
height: 16,
bgcolor: 'background.paper',
border: '1px solid',
borderColor: 'divider',
transition: 'opacity 0.2s',
'&:hover': {
bgcolor: 'background.paper',
},
}}
>
<CloseIcon sx={{ fontSize: 10 }} />
</IconButton>
</Box>
))}
</Stack>
)}
<TextField
fullWidth
multiline
rows={2}
disabled={loading}
ref={inputRef}
sx={{
bgcolor: 'background.paper3',
'.MuiInputBase-root': {
p: 0,
overflow: 'hidden',
height: '52px !important',
},
textarea: {
borderRadius: 0,
'&::-webkit-scrollbar': {
display: 'none',
},
scrollbarWidth: 'none',
msOverflowStyle: 'none',
p: '2px',
},
fieldset: {
border: 'none',
},
}}
size='small'
value={input}
onChange={onInputChange}
onFocus={onInputFocus}
onBlur={onInputBlur}
onPaste={onPaste}
onKeyDown={e => {
const isComposing =
e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229;
if (
e.key === 'Enter' &&
!e.shiftKey &&
input.length > 0 &&
!isComposing
) {
e.preventDefault();
onSearch();
}
}}
placeholder={placeholder}
autoComplete='off'
/>
<Stack
direction='row'
alignItems='center'
justifyContent='space-between'
sx={{ width: '100%' }}
>
<input
ref={fileInputRef}
type='file'
accept='image/*'
multiple
style={{ display: 'none' }}
onChange={onImageUpload}
/>
<Tooltip title='敬请期待'>
<IconButton size='small' disabled={loading} sx={{ flexShrink: 0 }}>
<IconTupian sx={{ fontSize: 20, color: 'text.secondary' }} />
</IconButton>
</Tooltip>
<Box sx={{ fontSize: 12, flexShrink: 0, cursor: 'pointer' }}>
{loading ? (
<ChatLoading
thinking={thinking}
onClick={() => {
setThinking(4);
onSearchAbort();
}}
/>
) : (
<IconButton
size='small'
onClick={() => {
if (input.length > 0) {
onSearchAbort();
setThinking(1);
onSearch();
}
}}
>
<IconFasong
sx={{
fontSize: 16,
color: input.length > 0 ? 'primary.main' : 'text.disabled',
}}
/>
</IconButton>
)}
</Box>
</Stack>
</Stack>
);
};

View File

@ -1,22 +0,0 @@
import React from 'react';
import { Stack, Typography } from '@mui/material';
import Image from 'next/image';
import aiLoading from '@/assets/images/ai-loading.gif';
import { AnswerStatus, AnswerStatusType } from '../constants';
interface LoadingContentProps {
thinking: AnswerStatusType;
}
export const LoadingContent: React.FC<LoadingContentProps> = ({ thinking }) => {
if (thinking === 4) return null;
return (
<Stack direction='row' alignItems='center' gap={1} sx={{ pb: 1 }}>
<Image src={aiLoading} alt='ai-loading' width={20} height={20} />
<Typography variant='body2' sx={{ fontSize: 12, color: 'text.tertiary' }}>
{AnswerStatus[thinking]}
</Typography>
</Stack>
);
};

View File

@ -1,97 +0,0 @@
import React from 'react';
import { Box, Stack, Typography } from '@mui/material';
interface SuggestionListProps {
hotSearch: string[];
fuzzySuggestions: string[];
showFuzzySuggestions: boolean;
input: string;
onSuggestionClick: (text: string) => void;
highlightMatch: (text: string, query: string) => React.ReactNode;
}
export const SuggestionList: React.FC<SuggestionListProps> = ({
hotSearch,
fuzzySuggestions,
showFuzzySuggestions,
input,
onSuggestionClick,
highlightMatch,
}) => {
// 模糊搜索建议
if (showFuzzySuggestions && fuzzySuggestions.length > 0) {
return (
<Stack
sx={{
mt: 1,
position: 'relative',
zIndex: 1000,
}}
gap={0.5}
>
{fuzzySuggestions.map((suggestion, index) => (
<Box
key={index}
onClick={() => onSuggestionClick(suggestion)}
sx={{
py: 1,
px: 2,
borderRadius: '6px',
cursor: 'pointer',
transition: 'all 0.2s',
bgcolor: 'transparent',
color: 'text.primary',
'&:hover': {
bgcolor: 'action.hover',
},
display: 'flex',
alignItems: 'center',
width: 'auto',
fontSize: 14,
fontWeight: 400,
}}
>
{highlightMatch(suggestion, input)}
</Box>
))}
</Stack>
);
}
// 热门搜索建议
if (!showFuzzySuggestions && hotSearch.length > 0) {
return (
<Stack sx={{ mt: 2 }} gap={1}>
{hotSearch.map((suggestion, index) => (
<Box
key={index}
onClick={() => onSuggestionClick(suggestion)}
sx={{
py: '6px',
px: 2,
mb: 1,
borderRadius: '10px',
cursor: 'pointer',
transition: 'all 0.2s',
bgcolor: '#F8F9FA',
color: 'text.secondary',
'&:hover': {
color: 'primary.main',
},
alignSelf: 'flex-start',
display: 'inline-flex',
alignItems: 'center',
width: 'auto',
}}
>
<Typography variant='body2' sx={{ fontSize: 14, flex: 1 }}>
{suggestion}
</Typography>
</Box>
))}
</Stack>
);
}
return null;
};

View File

@ -1,4 +0,0 @@
export { LoadingContent } from './LoadingContent';
export { InputArea } from './InputArea';
export { SuggestionList } from './SuggestionList';
export { ConversationItemComponent } from './ConversationItem';

View File

@ -1,6 +1,5 @@
'use client';
import React, { useState, useEffect, useRef, useMemo } from 'react';
import Logo from '@/assets/images/logo.png';
import { IconZhinengwenda, IconJinsousuo } from '@panda-wiki/icons';
import { useSearchParams } from 'next/navigation';
import {
@ -11,8 +10,10 @@ import {
Stack,
lighten,
alpha,
styled,
Tabs,
Tab,
} from '@mui/material';
import { CusTabs } from '@ctzhian/ui';
import AiQaContent from './AiQaContent';
import SearchDocContent from './SearchDocContent';
import { useStore } from '@/provider';
@ -32,6 +33,48 @@ interface QaModalProps {
defaultSuggestions?: SearchSuggestion[];
}
const StyledTabs = styled(Tabs)(({ theme }) => ({
minHeight: 'auto',
position: 'relative',
borderRadius: '10px',
padding: theme.spacing(0.5),
border: `1px solid ${alpha(theme.palette.text.primary, 0.1)}`,
'& .MuiTabs-indicator': {
height: '100%',
borderRadius: '8px',
backgroundColor: theme.palette.primary.main,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
zIndex: 0,
},
'& .MuiTabs-flexContainer': {
gap: theme.spacing(0.5),
position: 'relative',
zIndex: 1,
},
}));
// 样式化的 Tab 组件 - 白色背景,圆角,深灰色文字
const StyledTab = styled(Tab)(({ theme }) => ({
minHeight: 'auto',
padding: theme.spacing(0.75, 2),
borderRadius: '6px',
backgroundColor: 'transparent',
fontSize: 12,
fontWeight: 400,
textTransform: 'none',
transition: 'color 0.3s ease-in-out',
position: 'relative',
zIndex: 1,
lineHeight: 1,
'&:hover': {
color: theme.palette.text.primary,
},
'&.Mui-selected': {
color: theme.palette.primary.contrastText,
fontWeight: 500,
},
}));
const QaModal: React.FC<QaModalProps> = () => {
const { qaModalOpen, setQaModalOpen, kbDetail, mobile } = useStore();
const [searchMode, setSearchMode] = useState<'chat' | 'search'>('chat');
@ -69,6 +112,14 @@ const QaModal: React.FC<QaModalProps> = () => {
}
}, [qaModalOpen, searchMode]);
useEffect(() => {
if (!qaModalOpen) {
setTimeout(() => {
setSearchMode('chat');
}, 300);
}
}, [qaModalOpen]);
useEffect(() => {
const cid = searchParams.get('cid');
const ask = searchParams.get('ask');
@ -92,7 +143,6 @@ const QaModal: React.FC<QaModalProps> = () => {
sx={theme => ({
display: 'flex',
flexDirection: 'column',
gap: 2,
flex: 1,
maxWidth: 800,
maxHeight: '100%',
@ -101,87 +151,81 @@ const QaModal: React.FC<QaModalProps> = () => {
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.12)',
overflow: 'hidden',
outline: 'none',
'& .Mui-selected': {
color: `${theme.palette.background.default} !important`,
},
pb: 2,
})}
onClick={e => e.stopPropagation()}
>
{/* 头部区域 */}
{/* 顶部标签栏 */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
mx: 2,
py: 1.5,
borderBottom: '1px solid',
borderColor: 'divider',
px: 2,
pt: 2,
pb: 2.5,
}}
>
{/* Logo */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<img
src={kbDetail?.settings?.icon || Logo.src}
style={{
width: 24,
height: 24,
}}
/>
<Typography
variant='h6'
sx={{ fontSize: 16, color: 'text.primary' }}
>
{kbDetail?.settings?.title}
</Typography>
</Box>
<CusTabs
size='small'
list={[
{
label: (
<Stack direction='row' gap={1} alignItems='center'>
<IconZhinengwenda sx={{ fontSize: 16 }} />
{!mobile && <span></span>}
</Stack>
),
value: 'chat',
},
{
label: (
<Stack direction='row' gap={1} alignItems='center'>
<IconJinsousuo sx={{ fontSize: 16 }} />
{!mobile && <span></span>}
</Stack>
),
value: 'search',
},
]}
<StyledTabs
value={searchMode}
onChange={value => setSearchMode(value as 'chat' | 'search')}
/>
onChange={(_, value) => {
setSearchMode(value as 'chat' | 'search');
}}
variant='scrollable'
scrollButtons={false}
>
<StyledTab
label={
<Stack direction='row' gap={0.5} alignItems='center'>
<IconZhinengwenda sx={{ fontSize: 16 }} />
{!mobile && <span></span>}
</Stack>
}
value='chat'
/>
<StyledTab
label={
<Stack direction='row' gap={0.5} alignItems='center'>
<IconJinsousuo sx={{ fontSize: 16 }} />
{!mobile && <span></span>}
</Stack>
}
value='search'
/>
</StyledTabs>
{/* Esc按钮 */}
{!mobile && (
<Button
variant='outlined'
color='primary'
onClick={onClose}
size='small'
sx={theme => ({
minWidth: 'auto',
px: 1.5,
py: 0.5,
px: 1,
py: '1px',
fontSize: 12,
fontWeight: 500,
textTransform: 'none',
color: 'text.secondary',
borderColor: alpha(theme.palette.text.primary, 0.1),
})}
>
Esc
</Button>
)}
</Box>
{/* 主内容区域 - 根据模式切换 */}
<Box sx={{ px: 2, display: searchMode === 'chat' ? 'block' : 'none' }}>
<Box
sx={{
px: 3,
flex: 1,
display: searchMode === 'chat' ? 'flex' : 'none',
flexDirection: 'column',
}}
>
<AiQaContent
hotSearch={hotSearch}
placeholder={placeholder}
@ -189,15 +233,21 @@ const QaModal: React.FC<QaModalProps> = () => {
/>
</Box>
<Box
sx={{ px: 2, display: searchMode === 'search' ? 'block' : 'none' }}
sx={{
px: 3,
flex: 1,
display: searchMode === 'search' ? 'flex' : 'none',
flexDirection: 'column',
}}
>
<SearchDocContent inputRef={inputRef} placeholder={placeholder} />
</Box>
{/* 底部AI生成提示 */}
<Box
sx={{
px: 3,
py: kbDetail?.settings?.disclaimer_settings?.content ? 2 : 1,
pt: kbDetail?.settings?.disclaimer_settings?.content ? 2 : 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',

View File

@ -125,7 +125,6 @@ const DocContent = ({
);
useEffect(() => {
// @ts-ignore
window.CAP_CUSTOM_WASM_URL =
window.location.origin + '/cap@0.0.6/cap_wasm.min.js';
}, []);

View File

@ -1,18 +1,13 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
/* Next.js */
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"

View File

@ -0,0 +1,17 @@
import React from 'react';
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
const IconChuangjian = (props: SvgIconProps) => (
<SvgIcon
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 1024 1024'
{...props}
>
<path d='M512.36408889 55.18222223C259.83431111 55.18222223 54.38577778 260.59662223 54.38577778 513.12640001S259.83431111 971.09333334 512.36408889 971.09333334s457.96693333-205.44853333 457.96693333-457.96693333S764.88248889 55.18222223 512.36408889 55.18222223z m0 846.81386666c-214.41422222 0-388.84693333-174.43271111-388.84693334-388.84693333s174.43271111-388.86968889 388.84693334-388.86968889S901.19964445 298.66666667 901.19964445 513.12640001 726.76693333 901.96195556 512.36408889 901.96195556z'></path>
<path d='M546.92977778 291.25973334h-69.13137778v187.30097778H290.49742222v69.12h187.30097778v187.30097777h69.13137778V547.68071112h187.2896v-69.12H546.92977778V291.25973334z'></path>
</SvgIcon>
);
IconChuangjian.displayName = 'icon-chuangjian';
export default IconChuangjian;

View File

@ -0,0 +1,16 @@
import React from 'react';
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
const IconFabu = (props: SvgIconProps) => (
<SvgIcon
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 1024 1024'
{...props}
>
<path d='M766.1 880.9L539.5 769.8c-16.9-8.3-24-28.7-15.7-45.7 4-8.1 11-14.3 19.5-17.3 8.6-2.9 17.9-2.4 26.1 1.6l185.8 91.2 80.2-549.1-361.2 433.4v201.4c0 18.8-15.3 34.1-34.1 34.1S406 904.1 406 885.3V672.7c0-1.8 0.1-3.5 0.4-5.3 0.8-6.6 3.5-12.9 7.7-18L754.4 241 208.8 524.1l128.6 66.7c16.9 8.9 23.6 29.5 15.3 46.6-3.8 7.9-10.7 13.9-19 16.8-8.3 2.9-17.4 2.3-25.3-1.5l-186-96.8c-7.1-3.7-12.7-9.7-15.9-17-4.2-7.9-5.1-17.2-2.5-25.7 2.6-8.6 8.5-15.8 16.5-20l749.6-388.9c5-2.6 10.6-3.9 16.2-3.9 16.7-0.3 31.1 11.5 34.1 27.9 1.1 5.5 0.9 11.1-0.6 16.5L816.1 854.9c-2.2 15.4-14.3 26.8-28.9 29-7.2 1.3-14.6 0.3-21.1-3z m0 0'></path>
</SvgIcon>
);
IconFabu.displayName = 'icon-fabu';
export default IconFabu;

View File

@ -0,0 +1,18 @@
import React from 'react';
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
const IconXinduihua = (props: SvgIconProps) => (
<SvgIcon
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 1024 1024'
{...props}
>
<path d='M333.184 987.456a52.928 52.928 0 0 1-18.688-3.712 47.424 47.424 0 0 1-16.064-10.56 44.544 44.544 0 0 1-10.624-15.36 45.76 45.76 0 0 1-4.032-18.304l-1.088-96.896a256.896 256.896 0 0 1-82.304-27.456 239.808 239.808 0 0 1-36.16-24.128 219.008 219.008 0 0 1-31.488-29.632 265.728 265.728 0 0 1-25.6-34.752 240.384 240.384 0 0 1-30.336-79.744 237.44 237.44 0 0 1-3.648-42.368V346.24c0-15.68 1.472-31.04 4.352-46.4 3.328-15.36 8.064-30.4 13.952-44.992a248.32 248.32 0 0 1 52.992-77.184A242.432 242.432 0 0 1 269.504 112.64a230.4 230.4 0 0 1 47.552-4.736H486.4a44.48 44.48 0 0 1 30.72 12.416 42.176 42.176 0 0 1 0 59.584 43.712 43.712 0 0 1-30.72 12.48H317.056c-10.56 0-20.8 0.704-30.72 2.88-10.24 1.856-20.096 4.8-29.632 8.832a139.904 139.904 0 0 0-27.392 14.208 169.344 169.344 0 0 0-23.808 19.072 171.584 171.584 0 0 0-19.712 23.36 171.008 171.008 0 0 0-14.656 26.688 155.264 155.264 0 0 0-11.712 58.88v258.24c0 10.24 0.768 20.096 2.944 30.336 2.176 9.856 5.12 19.776 9.152 29.248a148.48 148.48 0 0 0 34.752 50.816 155.52 155.52 0 0 0 51.904 33.664c9.536 4.032 19.776 6.976 30.016 9.152 10.24 1.856 20.48 2.944 31.104 2.944a48.896 48.896 0 0 1 34.688 13.888 50.88 50.88 0 0 1 11.008 15.744c2.56 5.824 3.648 12.032 4.032 18.24l0.32 59.648 108.992-77.888c27.84-19.776 58.88-29.632 93.248-29.632h134.976c10.624 0 20.864-1.088 30.72-2.944 10.24-1.856 20.096-4.736 29.632-8.768 9.472-4.032 18.624-8.768 27.392-14.272 8.448-5.504 16.512-12.096 23.808-19.008 7.296-7.296 13.888-14.976 19.712-23.424a145.344 145.344 0 0 0 23.424-55.552c2.176-9.92 2.944-19.776 2.944-30.016V473.216a40.896 40.896 0 0 1 12.8-29.632 44.48 44.48 0 0 1 47.168-9.152c5.12 1.92 9.856 5.12 13.888 9.152a40.896 40.896 0 0 1 12.8 29.632V606.72a233.92 233.92 0 0 1-41.344 132.416 221.184 221.184 0 0 1-30.336 36.16 262.208 262.208 0 0 1-36.928 29.632 229.952 229.952 0 0 1-42.048 21.952 251.712 251.712 0 0 1-93.632 18.304H599.744c-33.984 0-65.088 9.856-92.864 29.632L362.432 977.92a49.92 49.92 0 0 1-29.248 9.536z'></path>
<path d='M902.592 188.16h-237.44a42.176 42.176 0 0 0 0 84.352h237.44a42.176 42.176 0 1 0 0-84.352z'></path>
<path d='M827.008 116.288a43.2 43.2 0 0 0-86.336 0v228.096a43.2 43.2 0 0 0 86.4 0V116.288z'></path>
</SvgIcon>
);
IconXinduihua.displayName = 'icon-xinduihua';
export default IconXinduihua;

View File

@ -40,6 +40,7 @@ export { default as IconChakan } from './IconChakan';
export { default as IconChangjianwenti } from './IconChangjianwenti';
export { default as IconChatgpt } from './IconChatgpt';
export { default as IconChilun } from './IconChilun';
export { default as IconChuangjian } from './IconChuangjian';
export { default as IconCohere } from './IconCohere';
export { default as IconCorrection } from './IconCorrection';
export { default as IconDJzhinengzhaiyao } from './IconDJzhinengzhaiyao';
@ -71,6 +72,7 @@ export { default as IconDuihao } from './IconDuihao';
export { default as IconDuihao1 } from './IconDuihao1';
export { default as IconDuihualishi1 } from './IconDuihualishi1';
export { default as IconExcel1 } from './IconExcel1';
export { default as IconFabu } from './IconFabu';
export { default as IconFankui } from './IconFankui';
export { default as IconFankuiwenti } from './IconFankuiwenti';
export { default as IconFasong } from './IconFasong';
@ -216,6 +218,7 @@ export { default as IconXiala1 } from './IconXiala1';
export { default as IconXialaCopy } from './IconXialaCopy';
export { default as IconXiaohongshu } from './IconXiaohongshu';
export { default as IconXiaohongshuHui } from './IconXiaohongshuHui';
export { default as IconXinduihua } from './IconXinduihua';
export { default as IconXinference } from './IconXinference';
export { default as IconXingxing } from './IconXingxing';
export { default as IconYanzhengma } from './IconYanzhengma';

View File

@ -0,0 +1,22 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* Icons */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"allowImportingTsExtensions": true,
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": ["src", "scripts"],
"exclude": ["node_modules", "dist"]
}

View File

@ -7,7 +7,8 @@
"types": "./theme.d.ts",
"exports": {
".": "./src/index.ts",
"./*": "./src/*.ts"
"./*": "./src/*.ts",
"./types": "./theme.d.ts"
},
"keywords": [],
"author": "",

View File

@ -1,4 +1,3 @@
/// <reference path="../theme.d.ts" />
import { PaletteOptions } from '@mui/material';
const blackPalette: PaletteOptions = {

View File

@ -1,4 +1,3 @@
/// <reference path="../theme.d.ts" />
import { PaletteOptions } from '@mui/material';
const bluePalette: PaletteOptions = {

View File

@ -1,4 +1,3 @@
/// <reference path="../theme.d.ts" />
import { PaletteOptions } from '@mui/material';
const darkPalette: PaletteOptions = {

View File

@ -1,4 +1,3 @@
/// <reference path="../theme.d.ts" />
import { PaletteOptions } from '@mui/material';
const darkDeepForestPalette: PaletteOptions = {

View File

@ -1,4 +1,3 @@
/// <reference path="../theme.d.ts" />
import { PaletteOptions } from '@mui/material';
const blackPalette: PaletteOptions = {

View File

@ -1,4 +1,3 @@
/// <reference path="../theme.d.ts" />
import { PaletteOptions } from '@mui/material';
const deepTealPalette: PaletteOptions = {

View File

@ -1,4 +1,3 @@
/// <reference path="../theme.d.ts" />
import { PaletteOptions } from '@mui/material';
const electricBluePalette: PaletteOptions = {

View File

@ -1,4 +1,3 @@
/// <reference path="../theme.d.ts" />
import { PaletteOptions } from '@mui/material';
const greenPalette: PaletteOptions = {

View File

@ -1,4 +1,3 @@
/// <reference path="../theme.d.ts" />
import { PaletteOptions } from '@mui/material';
const lightPalette: PaletteOptions = {

View File

@ -1,4 +1,3 @@
/// <reference path="../theme.d.ts" />
import { PaletteOptions } from '@mui/material';
const orangePalette: PaletteOptions = {

View File

@ -1,4 +1,3 @@
/// <reference path="../theme.d.ts" />
import { PaletteOptions } from '@mui/material';
const bluePalette: PaletteOptions = {

View File

@ -1,4 +1,3 @@
/// <reference path="../theme.d.ts" />
import { PaletteOptions } from '@mui/material';
const redPalette: PaletteOptions = {

View File

@ -0,0 +1,21 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* Themes */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"target": "ES2020",
"lib": ["ES2020"],
"allowImportingTsExtensions": true,
"moduleDetection": "force",
"esModuleInterop": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true
},
"include": ["src", "theme.d.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@ -1,3 +1,5 @@
/// <reference types="@panda-wiki/themes/types" />
declare module '*.png' {
const value: string;
export default value;

View File

@ -97,199 +97,198 @@ const Footer = React.memo(
},
})}
>
{showBrand && (
<Box
pt={
customStyle?.footer_show_intro
<Box
pt={
customStyle?.footer_show_intro
? 5
: (footerSetting?.brand_groups?.length || 0) > 0
? 5
: (footerSetting?.brand_groups?.length || 0) > 0
? 5
: 0
}
>
{customStyle?.footer_show_intro !== false && (
<Box sx={{ mb: 3 }}>
<Stack direction={'row'} alignItems={'center'} gap={1}>
{footerSetting?.brand_logo && (
<img
src={footerSetting.brand_logo}
alt='PandaWiki'
height={24}
/>
)}
<Box
sx={{
fontWeight: 'bold',
lineHeight: '32px',
fontSize: 24,
color: 'white',
}}
>
{footerSetting?.brand_name}
</Box>
</Stack>
{footerSetting?.brand_desc && (
<Box
sx={{
fontSize: 12,
lineHeight: '26px',
mt: 2,
color: 'rgba(255, 255, 255, 0.70)',
}}
>
{footerSetting.brand_desc}
</Box>
: 0
}
>
{customStyle?.footer_show_intro !== false && (
<Box sx={{ mb: 3 }}>
<Stack direction={'row'} alignItems={'center'} gap={1}>
{footerSetting?.brand_logo && (
<img
src={footerSetting.brand_logo}
alt='PandaWiki'
height={24}
/>
)}
<Stack direction={'column'} gap={2.5} mt={2}>
{customStyle?.social_media_accounts?.map(
(account, index) => {
return (
<Stack
direction={'row'}
key={index}
sx={theme => ({
position: 'relative',
color: alpha(theme.palette.text.primary, 0.7),
})}
gap={1}
onClick={() => {
setCurOverlayType(account.channel || '');
if (account.channel === 'phone') {
setPhoneData({
phone: account.phone || '',
text: account.text || '',
});
setOpen(true);
}
if (account.channel === 'wechat_oa') {
setWechatData({
src: account.icon || '',
text: account.text || '',
});
setOpen(true);
}
}}
>
{account.channel === 'wechat_oa' && (
<IconWeixingongzhonghao
sx={{ fontSize: '20px', color: 'inherit' }}
/>
)}
{account.channel === 'phone' && (
<IconDianhua
sx={{ fontSize: '20px', color: 'inherit' }}
></IconDianhua>
)}
<Box
sx={{
lineHeight: '24px',
fontSize: '14px',
color: 'inherit',
}}
>
{account.text}
</Box>
{account.channel === 'wechat_oa' &&
(account?.text || account?.icon) && (
<Stack
direction={'column'}
alignItems={'center'}
bgcolor={'#fff'}
p={1.5}
sx={theme => ({
position: 'absolute',
bottom: '100%',
transform: 'translateY(-10px)',
left: 0,
boxShadow:
' 0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
gap={1}
display={'none'}
zIndex={999}
>
{account.icon && (
<img
src={account.icon}
width={83}
height={83}
></img>
)}
{account.text && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: '#21222D',
maxWidth: '83px',
textAlign: 'center',
}}
>
{account.text}
</Box>
)}
</Stack>
)}
</Stack>
);
},
)}
</Stack>
</Box>
)}
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{footerSetting?.brand_groups?.map((group, idx) => (
<Stack
gap={1}
key={group.name}
<Box
sx={{
fontSize: 14,
lineHeight: '22px',
width: 'calc(50% - 8px)',
...(idx > 1 && {
mt: 1,
}),
'& a:hover': {
color: 'primary.main',
},
fontWeight: 'bold',
lineHeight: '32px',
fontSize: 24,
color: 'white',
}}
>
{footerSetting?.brand_name}
</Box>
</Stack>
{footerSetting?.brand_desc && (
<Box
sx={{
fontSize: 12,
lineHeight: '26px',
mt: 2,
color: 'rgba(255, 255, 255, 0.70)',
}}
>
{footerSetting.brand_desc}
</Box>
)}
<Stack direction={'column'} gap={2.5} mt={2}>
{customStyle?.social_media_accounts?.map(
(account, index) => {
return (
<Stack
direction={'row'}
key={index}
sx={theme => ({
position: 'relative',
color: alpha(theme.palette.text.primary, 0.7),
})}
gap={1}
onClick={() => {
setCurOverlayType(account.channel || '');
if (account.channel === 'phone') {
setPhoneData({
phone: account.phone || '',
text: account.text || '',
});
setOpen(true);
}
if (account.channel === 'wechat_oa') {
setWechatData({
src: account.icon || '',
text: account.text || '',
});
setOpen(true);
}
}}
>
{account.channel === 'wechat_oa' && (
<IconWeixingongzhonghao
sx={{ fontSize: '20px', color: 'inherit' }}
/>
)}
{account.channel === 'phone' && (
<IconDianhua
sx={{ fontSize: '20px', color: 'inherit' }}
></IconDianhua>
)}
<Box
sx={{
lineHeight: '24px',
fontSize: '14px',
color: 'inherit',
}}
>
{account.text}
</Box>
{account.channel === 'wechat_oa' &&
(account?.text || account?.icon) && (
<Stack
direction={'column'}
alignItems={'center'}
bgcolor={'#fff'}
p={1.5}
sx={theme => ({
position: 'absolute',
bottom: '100%',
transform: 'translateY(-10px)',
left: 0,
boxShadow:
' 0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
gap={1}
display={'none'}
zIndex={999}
>
{account.icon && (
<img
src={account.icon}
width={83}
height={83}
></img>
)}
{account.text && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: '#21222D',
maxWidth: '83px',
textAlign: 'center',
}}
>
{account.text}
</Box>
)}
</Stack>
)}
</Stack>
);
},
)}
</Stack>
</Box>
)}
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{footerSetting?.brand_groups?.map((group, idx) => (
<Stack
gap={1}
key={group.name}
sx={{
fontSize: 14,
lineHeight: '22px',
width: 'calc(50% - 8px)',
...(idx > 1 && {
mt: 1,
}),
'& a:hover': {
color: 'primary.main',
},
}}
>
<Box
sx={{
fontSize: 16,
lineHeight: '24px',
mb: 1,
color: '#ffffff',
}}
>
{group.name}
</Box>
{group.links?.map(link => (
<Box
sx={{
fontSize: 16,
lineHeight: '24px',
mb: 1,
color: '#ffffff',
}}
sx={theme => ({
color: alpha(theme.palette.text.primary, 0.5),
})}
key={link.name}
>
{group.name}
</Box>
{group.links?.map(link => (
<Box
sx={theme => ({
color: alpha(theme.palette.text.primary, 0.5),
})}
<Link
href={link?.url || ''}
target='_blank'
key={link.name}
>
<Link
href={link?.url || ''}
target='_blank'
key={link.name}
>
{link.name}
</Link>
</Box>
))}
</Stack>
))}
</Stack>
</Box>
)}
{link.name}
</Link>
</Box>
))}
</Stack>
))}
</Stack>
</Box>
{!(
customStyle?.footer_show_intro === false &&
footerSetting?.brand_groups?.length === 0
@ -478,241 +477,239 @@ const Footer = React.memo(
// }),
}}
>
{showBrand && (
<Box
py={
customStyle?.footer_show_intro
<Box
py={
customStyle?.footer_show_intro
? 6
: (footerSetting?.brand_groups?.length || 0) > 0
? 6
: (footerSetting?.brand_groups?.length || 0) > 0
? 6
: 0
: 0
}
>
<Stack
direction={'row'}
gap={10}
justifyContent={
customStyle?.footer_show_intro === false
? 'center'
: 'flex-start'
}
>
<Stack
direction={'row'}
gap={10}
justifyContent={
customStyle?.footer_show_intro === false
? 'center'
: 'flex-start'
}
>
{customStyle?.footer_show_intro !== false && (
<Stack
direction={'column'}
sx={{ width: '30%', minWidth: 200 }}
gap={3}
>
<Stack direction={'row'} alignItems={'center'} gap={1}>
{footerSetting?.brand_logo && (
<img
src={footerSetting.brand_logo}
alt='PandaWiki'
height={36}
/>
)}
<Box
sx={{
fontWeight: 'bold',
lineHeight: '32px',
fontSize: 24,
color: 'text.primary',
}}
>
{footerSetting?.brand_name}
</Box>
</Stack>
{footerSetting?.brand_desc && (
<Box
sx={theme => ({
fontSize: 14,
lineHeight: '26px',
color: alpha(theme.palette.text.primary, 0.7),
})}
>
{footerSetting.brand_desc}
</Box>
)}
<Stack direction={'column'} gap={'26px'}>
{customStyle?.social_media_accounts?.map(
(account, index) => {
return (
<Stack
direction={'row'}
key={index}
sx={theme => ({
position: 'relative',
'&:hover': {
color: theme.palette.primary.main,
},
'&:hover .popup': {
display: 'flex !important',
},
color: alpha(theme.palette.text.primary, 0.7),
cursor: 'default',
})}
gap={1}
>
{account.channel === 'wechat_oa' && (
<IconWeixingongzhonghao
sx={{ fontSize: '20px', color: 'inherit' }}
></IconWeixingongzhonghao>
)}
{account.channel === 'phone' && (
<IconDianhua
sx={{ fontSize: '20px', color: 'inherit' }}
></IconDianhua>
)}
<Box
sx={{
lineHeight: '24px',
fontSize: '14px',
color: 'inherit',
}}
>
{account.text}
</Box>
{account.channel === 'wechat_oa' &&
(account?.text || account?.icon) && (
<Stack
className={'popup'}
direction={'column'}
alignItems={'center'}
bgcolor={'#fff'}
p={1.5}
sx={theme => ({
position: 'absolute',
top: '40px',
left: 0,
boxShadow:
' 0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
gap={1}
display={'none'}
zIndex={999}
>
{account.icon && (
<img
src={account.icon}
width={120}
height={120}
></img>
)}
{account.text && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: 'text.primary',
maxWidth: '120px',
textAlign: 'center',
}}
>
{account.text}
</Box>
)}
</Stack>
)}
{account.channel === 'phone' &&
account?.phone && (
<Stack
className={'popup'}
bgcolor={'#fff'}
px={1.5}
py={1}
sx={theme => ({
position: 'absolute',
bottom: '100%',
transform: 'translateY(-10px)',
left: 0,
boxShadow:
'0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
display={'none'}
zIndex={999}
>
{account.phone && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: 'text.primary',
textAlign: 'center',
}}
>
{account.phone}
</Box>
)}
</Stack>
)}
</Stack>
);
},
)}
</Stack>
</Stack>
)}
{customStyle?.footer_show_intro !== false && (
<Stack
direction={'row'}
width={'100%'}
justifyContent={'flex-start'}
flexWrap='wrap'
direction={'column'}
sx={{ width: '30%', minWidth: 200 }}
gap={3}
>
{footerSetting?.brand_groups?.map(group => (
<Stack
gap={1.5}
key={group.name}
<Stack direction={'row'} alignItems={'center'} gap={1}>
{footerSetting?.brand_logo && (
<img
src={footerSetting.brand_logo}
alt='PandaWiki'
height={36}
/>
)}
<Box
sx={{
flex: '0 0 33.33%',
fontSize: 14,
lineHeight: '22px',
minWidth: '100px',
'& a:hover': {
color: 'primary.main',
},
fontWeight: 'bold',
lineHeight: '32px',
fontSize: 24,
color: 'text.primary',
}}
>
{footerSetting?.brand_name}
</Box>
</Stack>
{footerSetting?.brand_desc && (
<Box
sx={theme => ({
fontSize: 14,
lineHeight: '26px',
color: alpha(theme.palette.text.primary, 0.7),
})}
>
{footerSetting.brand_desc}
</Box>
)}
<Stack direction={'column'} gap={'26px'}>
{customStyle?.social_media_accounts?.map(
(account, index) => {
return (
<Stack
direction={'row'}
key={index}
sx={theme => ({
position: 'relative',
'&:hover': {
color: theme.palette.primary.main,
},
'&:hover .popup': {
display: 'flex !important',
},
color: alpha(theme.palette.text.primary, 0.7),
cursor: 'default',
})}
gap={1}
>
{account.channel === 'wechat_oa' && (
<IconWeixingongzhonghao
sx={{ fontSize: '20px', color: 'inherit' }}
></IconWeixingongzhonghao>
)}
{account.channel === 'phone' && (
<IconDianhua
sx={{ fontSize: '20px', color: 'inherit' }}
></IconDianhua>
)}
<Box
sx={{
lineHeight: '24px',
fontSize: '14px',
color: 'inherit',
}}
>
{account.text}
</Box>
{account.channel === 'wechat_oa' &&
(account?.text || account?.icon) && (
<Stack
className={'popup'}
direction={'column'}
alignItems={'center'}
bgcolor={'#fff'}
p={1.5}
sx={theme => ({
position: 'absolute',
top: '40px',
left: 0,
boxShadow:
' 0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
gap={1}
display={'none'}
zIndex={999}
>
{account.icon && (
<img
src={account.icon}
width={120}
height={120}
></img>
)}
{account.text && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: 'text.primary',
maxWidth: '120px',
textAlign: 'center',
}}
>
{account.text}
</Box>
)}
</Stack>
)}
{account.channel === 'phone' && account?.phone && (
<Stack
className={'popup'}
bgcolor={'#fff'}
px={1.5}
py={1}
sx={theme => ({
position: 'absolute',
bottom: '100%',
transform: 'translateY(-10px)',
left: 0,
boxShadow:
'0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
display={'none'}
zIndex={999}
>
{account.phone && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: 'text.primary',
textAlign: 'center',
}}
>
{account.phone}
</Box>
)}
</Stack>
)}
</Stack>
);
},
)}
</Stack>
</Stack>
)}
<Stack
direction={'row'}
width={'100%'}
justifyContent={'flex-start'}
flexWrap='wrap'
>
{footerSetting?.brand_groups?.map(group => (
<Stack
gap={1.5}
key={group.name}
sx={{
flex: '0 0 33.33%',
fontSize: 14,
lineHeight: '22px',
minWidth: '100px',
'& a:hover': {
color: 'primary.main',
},
}}
>
<Box
sx={{
fontSize: 16,
lineHeight: '24px',
mb: 1,
color: 'text.primary',
}}
>
{group.name}
</Box>
{group.links?.map(link => (
<Box
sx={{
fontSize: 16,
lineHeight: '24px',
mb: 1,
color: 'text.primary',
}}
sx={theme => ({
color: alpha(theme.palette.text.primary, 0.5),
})}
key={link.name}
>
{group.name}
</Box>
{group.links?.map(link => (
<Box
sx={theme => ({
color: alpha(theme.palette.text.primary, 0.5),
})}
<Link
href={link?.url || ''}
target='_blank'
key={link.name}
>
<Link
href={link?.url || ''}
target='_blank'
key={link.name}
>
{link.name}
</Link>
</Box>
))}
</Stack>
))}
</Stack>
{link.name}
</Link>
</Box>
))}
</Stack>
))}
</Stack>
</Box>
)}
</Stack>
</Box>
{!(
customStyle?.footer_show_intro === false &&
footerSetting?.brand_groups?.length === 0

View File

@ -94,197 +94,196 @@ const Footer = React.memo(
bgcolor: alpha(theme.palette.text.primary, 0.05),
})}
>
{showBrand && (
<Box
pt={
customStyle?.footer_show_intro
<Box
pt={
customStyle?.footer_show_intro
? 5
: (footerSetting?.brand_groups?.length || 0) > 0
? 5
: (footerSetting?.brand_groups?.length || 0) > 0
? 5
: 0
}
>
{customStyle?.footer_show_intro !== false && (
<Box sx={{ mb: 3 }}>
<Stack direction={'row'} alignItems={'center'} gap={1}>
{footerSetting?.brand_logo && (
<img
src={footerSetting.brand_logo}
alt='PandaWiki'
height={24}
/>
)}
<Box
sx={{
fontWeight: 'bold',
lineHeight: '32px',
fontSize: 24,
color: 'text.primary',
}}
>
{footerSetting?.brand_name}
</Box>
</Stack>
{footerSetting?.brand_desc && (
<Box
sx={theme => ({
fontSize: 12,
lineHeight: '26px',
mt: 2,
color: alpha(theme.palette.text.primary, 0.7),
})}
>
{footerSetting.brand_desc}
</Box>
: 0
}
>
{customStyle?.footer_show_intro !== false && (
<Box sx={{ mb: 3 }}>
<Stack direction={'row'} alignItems={'center'} gap={1}>
{footerSetting?.brand_logo && (
<img
src={footerSetting.brand_logo}
alt='PandaWiki'
height={24}
/>
)}
<Stack direction={'column'} gap={2.5} mt={2}>
{customStyle?.social_media_accounts?.map(
(account, index) => {
return (
<Stack
direction={'row'}
key={index}
sx={theme => ({
position: 'relative',
color: alpha(theme.palette.text.primary, 0.7),
})}
gap={1}
onClick={() => {
setCurOverlayType(account.channel || '');
if (account.channel === 'phone') {
setPhoneData({
phone: account.phone || '',
text: account.text || '',
});
setOpen(true);
}
if (account.channel === 'wechat_oa') {
setWechatData({
src: account.icon || '',
text: account.text || '',
});
setOpen(true);
}
}}
>
{account.channel === 'wechat_oa' && (
<IconWeixingongzhonghao
sx={{ fontSize: '20px', color: 'inherit' }}
/>
)}
{account.channel === 'phone' && (
<IconDianhua
sx={{ fontSize: '20px', color: 'inherit' }}
></IconDianhua>
)}
<Box
sx={{
lineHeight: '24px',
fontSize: '12px',
color: 'inherit',
}}
>
{account.text}
</Box>
{account.channel === 'wechat_oa' &&
(account?.text || account?.icon) && (
<Stack
direction={'column'}
alignItems={'center'}
p={1.5}
sx={theme => ({
position: 'absolute',
top: '40px',
left: 0,
boxShadow:
' 0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
gap={1}
display={'none'}
zIndex={999}
>
{account.icon && (
<img
src={account.icon}
width={83}
height={83}
></img>
)}
{account.text && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: 'text.primary',
maxWidth: '83px',
textAlign: 'center',
}}
>
{account.text}
</Box>
)}
</Stack>
)}
</Stack>
);
},
)}
</Stack>
</Box>
)}
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{footerSetting?.brand_groups?.map((group, idx) => (
<Stack
gap={1}
key={group.name}
<Box
sx={{
fontSize: 14,
lineHeight: '22px',
width: 'calc(50% - 8px)',
...(idx > 1 && {
mt: 1,
}),
'& a:hover': {
color: 'primary.main',
},
fontWeight: 'bold',
lineHeight: '32px',
fontSize: 24,
color: 'text.primary',
}}
>
{footerSetting?.brand_name}
</Box>
</Stack>
{footerSetting?.brand_desc && (
<Box
sx={theme => ({
fontSize: 12,
lineHeight: '26px',
mt: 2,
color: alpha(theme.palette.text.primary, 0.7),
})}
>
{footerSetting.brand_desc}
</Box>
)}
<Stack direction={'column'} gap={2.5} mt={2}>
{customStyle?.social_media_accounts?.map(
(account, index) => {
return (
<Stack
direction={'row'}
key={index}
sx={theme => ({
position: 'relative',
color: alpha(theme.palette.text.primary, 0.7),
})}
gap={1}
onClick={() => {
setCurOverlayType(account.channel || '');
if (account.channel === 'phone') {
setPhoneData({
phone: account.phone || '',
text: account.text || '',
});
setOpen(true);
}
if (account.channel === 'wechat_oa') {
setWechatData({
src: account.icon || '',
text: account.text || '',
});
setOpen(true);
}
}}
>
{account.channel === 'wechat_oa' && (
<IconWeixingongzhonghao
sx={{ fontSize: '20px', color: 'inherit' }}
/>
)}
{account.channel === 'phone' && (
<IconDianhua
sx={{ fontSize: '20px', color: 'inherit' }}
></IconDianhua>
)}
<Box
sx={{
lineHeight: '24px',
fontSize: '12px',
color: 'inherit',
}}
>
{account.text}
</Box>
{account.channel === 'wechat_oa' &&
(account?.text || account?.icon) && (
<Stack
direction={'column'}
alignItems={'center'}
p={1.5}
sx={theme => ({
position: 'absolute',
top: '40px',
left: 0,
boxShadow:
' 0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
gap={1}
display={'none'}
zIndex={999}
>
{account.icon && (
<img
src={account.icon}
width={83}
height={83}
></img>
)}
{account.text && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: 'text.primary',
maxWidth: '83px',
textAlign: 'center',
}}
>
{account.text}
</Box>
)}
</Stack>
)}
</Stack>
);
},
)}
</Stack>
</Box>
)}
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{footerSetting?.brand_groups?.map((group, idx) => (
<Stack
gap={1}
key={group.name}
sx={{
fontSize: 14,
lineHeight: '22px',
width: 'calc(50% - 8px)',
...(idx > 1 && {
mt: 1,
}),
'& a:hover': {
color: 'primary.main',
},
}}
>
<Box
sx={{
fontSize: 14,
lineHeight: '24px',
mb: 1,
color: 'text.primary',
}}
>
{group.name}
</Box>
{group.links?.map(link => (
<Box
sx={{
fontSize: 14,
lineHeight: '24px',
mb: 1,
color: 'text.primary',
}}
sx={theme => ({
color: alpha(theme.palette.text.primary, 0.5),
})}
key={link.name}
>
{group.name}
</Box>
{group.links?.map(link => (
<Box
sx={theme => ({
color: alpha(theme.palette.text.primary, 0.5),
})}
<Link
href={link?.url || ''}
target='_blank'
key={link.name}
>
<Link
href={link?.url || ''}
target='_blank'
key={link.name}
>
{link.name}
</Link>
</Box>
))}
</Stack>
))}
</Stack>
</Box>
)}
{link.name}
</Link>
</Box>
))}
</Stack>
))}
</Stack>
</Box>
{!(
customStyle?.footer_show_intro === false &&
footerSetting?.brand_groups?.length === 0
@ -456,239 +455,237 @@ const Footer = React.memo(
width: '100%',
}}
>
{showBrand && (
<Box
py={
customStyle?.footer_show_intro
<Box
py={
customStyle?.footer_show_intro
? 6
: (footerSetting?.brand_groups?.length || 0) > 0
? 6
: (footerSetting?.brand_groups?.length || 0) > 0
? 6
: 0
: 0
}
>
<Stack
direction={'row'}
gap={10}
justifyContent={
customStyle?.footer_show_intro === false
? 'center'
: 'flex-start'
}
>
<Stack
direction={'row'}
gap={10}
justifyContent={
customStyle?.footer_show_intro === false
? 'center'
: 'flex-start'
}
>
{customStyle?.footer_show_intro !== false && (
<Stack
direction={'column'}
sx={{ width: '30%', minWidth: 200 }}
gap={3}
>
<Stack direction={'row'} alignItems={'center'} gap={1}>
{footerSetting?.brand_logo && (
<img
src={footerSetting.brand_logo}
alt='PandaWiki'
height={36}
/>
)}
<Box
sx={{
fontWeight: 'bold',
lineHeight: '32px',
fontSize: 24,
}}
>
{footerSetting?.brand_name}
</Box>
</Stack>
{footerSetting?.brand_desc && (
<Box
sx={theme => ({
fontSize: 14,
lineHeight: '26px',
color: alpha(theme.palette.text.primary, 0.7),
})}
>
{footerSetting.brand_desc}
</Box>
)}
<Stack direction={'column'} gap={'26px'}>
{customStyle?.social_media_accounts?.map(
(account, index) => {
return (
<Stack
direction={'row'}
key={index}
alignItems='center'
sx={theme => ({
position: 'relative',
'&:hover': {
color: theme.palette.primary.main,
},
'&:hover .popup': {
display: 'flex !important',
},
color: alpha(theme.palette.text.primary, 0.7),
cursor: 'default',
})}
gap={1}
>
{account.channel === 'wechat_oa' && (
<IconWeixingongzhonghao
sx={{ fontSize: '18px', color: 'inherit' }}
></IconWeixingongzhonghao>
)}
{account.channel === 'phone' && (
<IconDianhua
sx={{ fontSize: '16px', color: 'inherit' }}
></IconDianhua>
)}
<Box
sx={{
lineHeight: '24px',
fontSize: '14px',
color: 'inherit',
}}
>
{account.text}
</Box>
{account.channel === 'wechat_oa' &&
(account?.text || account?.icon) && (
<Stack
className={'popup'}
direction={'column'}
alignItems={'center'}
p={1.5}
sx={theme => ({
position: 'absolute',
bottom: '100%',
transform: 'translateY(-10px)',
left: 0,
boxShadow:
' 0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
gap={1}
display={'none'}
zIndex={999}
>
{account.icon && (
<img
src={account.icon}
width={120}
height={120}
></img>
)}
{account.text && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: 'text.primary',
maxWidth: '120px',
textAlign: 'center',
}}
>
{account.text}
</Box>
)}
</Stack>
)}
{account.channel === 'phone' &&
account?.phone && (
<Stack
className={'popup'}
px={1.5}
py={1}
sx={theme => ({
position: 'absolute',
bottom: '100%',
transform: 'translateY(-10px)',
left: 0,
boxShadow:
'0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
display={'none'}
zIndex={999}
>
{account.phone && (
<Box
sx={{
fontSize: '14px',
lineHeight: '16px',
color: 'text.primary',
textAlign: 'center',
}}
>
{account.phone}
</Box>
)}
</Stack>
)}
</Stack>
);
},
)}
</Stack>
</Stack>
)}
{customStyle?.footer_show_intro !== false && (
<Stack
direction={'row'}
width={'100%'}
justifyContent={'flex-start'}
flexWrap='wrap'
direction={'column'}
sx={{ width: '30%', minWidth: 200 }}
gap={3}
>
{footerSetting?.brand_groups?.map(group => (
<Stack
gap={1.5}
key={group.name}
<Stack direction={'row'} alignItems={'center'} gap={1}>
{footerSetting?.brand_logo && (
<img
src={footerSetting.brand_logo}
alt='PandaWiki'
height={36}
/>
)}
<Box
sx={{
flex: '0 0 33.33%',
fontSize: 14,
lineHeight: '22px',
minWidth: '100px',
'& a:hover': {
color: 'primary.main',
},
fontWeight: 'bold',
lineHeight: '32px',
fontSize: 24,
}}
>
{footerSetting?.brand_name}
</Box>
</Stack>
{footerSetting?.brand_desc && (
<Box
sx={theme => ({
fontSize: 14,
lineHeight: '26px',
color: alpha(theme.palette.text.primary, 0.7),
})}
>
{footerSetting.brand_desc}
</Box>
)}
<Stack direction={'column'} gap={'26px'}>
{customStyle?.social_media_accounts?.map(
(account, index) => {
return (
<Stack
direction={'row'}
key={index}
alignItems='center'
sx={theme => ({
position: 'relative',
'&:hover': {
color: theme.palette.primary.main,
},
'&:hover .popup': {
display: 'flex !important',
},
color: alpha(theme.palette.text.primary, 0.7),
cursor: 'default',
})}
gap={1}
>
{account.channel === 'wechat_oa' && (
<IconWeixingongzhonghao
sx={{ fontSize: '18px', color: 'inherit' }}
></IconWeixingongzhonghao>
)}
{account.channel === 'phone' && (
<IconDianhua
sx={{ fontSize: '16px', color: 'inherit' }}
></IconDianhua>
)}
<Box
sx={{
lineHeight: '24px',
fontSize: '14px',
color: 'inherit',
}}
>
{account.text}
</Box>
{account.channel === 'wechat_oa' &&
(account?.text || account?.icon) && (
<Stack
className={'popup'}
direction={'column'}
alignItems={'center'}
p={1.5}
sx={theme => ({
position: 'absolute',
bottom: '100%',
transform: 'translateY(-10px)',
left: 0,
boxShadow:
' 0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
gap={1}
display={'none'}
zIndex={999}
>
{account.icon && (
<img
src={account.icon}
width={120}
height={120}
></img>
)}
{account.text && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: 'text.primary',
maxWidth: '120px',
textAlign: 'center',
}}
>
{account.text}
</Box>
)}
</Stack>
)}
{account.channel === 'phone' && account?.phone && (
<Stack
className={'popup'}
px={1.5}
py={1}
sx={theme => ({
position: 'absolute',
bottom: '100%',
transform: 'translateY(-10px)',
left: 0,
boxShadow:
'0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
display={'none'}
zIndex={999}
>
{account.phone && (
<Box
sx={{
fontSize: '14px',
lineHeight: '16px',
color: 'text.primary',
textAlign: 'center',
}}
>
{account.phone}
</Box>
)}
</Stack>
)}
</Stack>
);
},
)}
</Stack>
</Stack>
)}
<Stack
direction={'row'}
width={'100%'}
justifyContent={'flex-start'}
flexWrap='wrap'
>
{footerSetting?.brand_groups?.map(group => (
<Stack
gap={1.5}
key={group.name}
sx={{
flex: '0 0 33.33%',
fontSize: 14,
lineHeight: '22px',
minWidth: '100px',
'& a:hover': {
color: 'primary.main',
},
}}
>
<Box
sx={{
fontSize: 16,
lineHeight: '24px',
mb: 1,
}}
>
{group.name}
</Box>
{group.links?.map(link => (
<Box
sx={{
fontSize: 16,
lineHeight: '24px',
mb: 1,
}}
sx={theme => ({
color: alpha(theme.palette.text.primary, 0.5),
})}
key={link.name}
>
{group.name}
</Box>
{group.links?.map(link => (
<Box
sx={theme => ({
color: alpha(theme.palette.text.primary, 0.5),
})}
<Link
href={link?.url || ''}
target='_blank'
key={link.name}
>
<Link
href={link?.url || ''}
target='_blank'
key={link.name}
>
{link.name}
</Link>
</Box>
))}
</Stack>
))}
</Stack>
{link.name}
</Link>
</Box>
))}
</Stack>
))}
</Stack>
</Box>
)}
</Stack>
</Box>
{!(
customStyle?.footer_show_intro === false &&
footerSetting?.brand_groups?.length === 0

View File

@ -25,6 +25,7 @@ const StyledButton = styled(Button)(({ theme }) => ({
borderRadius: '6px',
boxShadow: '0px 1px 2px 0px rgba(145,158,171,0.16)',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
}));
// 检测平台类型
@ -246,12 +247,7 @@ const Header = React.memo(
>
{ctrlKShortcut}
</Box>
<StyledButton
variant='contained'
// @ts-ignore
color='light'
sx={{}}
>
<StyledButton variant='contained'>
</StyledButton>
</Stack>

View File

@ -1,30 +1,23 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* UI */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"incremental": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"resolveJsonModule": true,
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,

View File

@ -262,9 +262,6 @@ importers:
html-to-image:
specifier: ^1.11.13
version: 1.11.13
import-in-the-middle:
specifier: ^1.14.2
version: 1.15.0
js-cookie:
specifier: ^3.0.5
version: 3.0.5
@ -313,9 +310,6 @@ importers:
remark-math:
specifier: ^6.0.0
version: 6.0.0
require-in-the-middle:
specifier: ^7.5.2
version: 7.5.2
uuid:
specifier: ^11.1.0
version: 11.1.0

13
web/tsconfig.base.json Normal file
View File

@ -0,0 +1,13 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
/* */
"incremental": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"isolatedModules": true,
"module": "ESNext",
"moduleResolution": "bundler"
}
}