mirror of https://github.com/chaitin/PandaWiki.git
Compare commits
23 Commits
0624cfc340
...
bec070786d
| Author | SHA1 | Date |
|---|---|---|
|
|
bec070786d | |
|
|
0aeda02985 | |
|
|
d630567a3c | |
|
|
20a0a4ded4 | |
|
|
38383b983d | |
|
|
030a8ac25d | |
|
|
24d1ed1bcd | |
|
|
487db8e944 | |
|
|
2638fcdc0c | |
|
|
1aa2855e00 | |
|
|
fd81e83807 | |
|
|
f121494416 | |
|
|
b04aa2d472 | |
|
|
69bf9cbf0e | |
|
|
940282a521 | |
|
|
171cc6c632 | |
|
|
78e5e1d70d | |
|
|
c5151ee7fe | |
|
|
c7f764199e | |
|
|
5588a46752 | |
|
|
5fba15654f | |
|
|
028b872349 | |
|
|
02d17cb48f |
|
|
@ -4976,6 +4976,34 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.BlockGridConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"list": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.BrandGroup": {
|
"domain.BrandGroup": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -6762,6 +6790,31 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.QuestionConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"list": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"question": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.RagInfo": {
|
"domain.RagInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -7242,6 +7295,9 @@ const docTemplate = `{
|
||||||
"basic_doc_config": {
|
"basic_doc_config": {
|
||||||
"$ref": "#/definitions/domain.BasicDocConfig"
|
"$ref": "#/definitions/domain.BasicDocConfig"
|
||||||
},
|
},
|
||||||
|
"block_grid_config": {
|
||||||
|
"$ref": "#/definitions/domain.BlockGridConfig"
|
||||||
|
},
|
||||||
"carousel_config": {
|
"carousel_config": {
|
||||||
"$ref": "#/definitions/domain.CarouselConfig"
|
"$ref": "#/definitions/domain.CarouselConfig"
|
||||||
},
|
},
|
||||||
|
|
@ -7278,6 +7334,9 @@ const docTemplate = `{
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"question_config": {
|
||||||
|
"$ref": "#/definitions/domain.QuestionConfig"
|
||||||
|
},
|
||||||
"simple_doc_config": {
|
"simple_doc_config": {
|
||||||
"$ref": "#/definitions/domain.SimpleDocConfig"
|
"$ref": "#/definitions/domain.SimpleDocConfig"
|
||||||
},
|
},
|
||||||
|
|
@ -7301,6 +7360,9 @@ const docTemplate = `{
|
||||||
"basic_doc_config": {
|
"basic_doc_config": {
|
||||||
"$ref": "#/definitions/domain.BasicDocConfig"
|
"$ref": "#/definitions/domain.BasicDocConfig"
|
||||||
},
|
},
|
||||||
|
"block_grid_config": {
|
||||||
|
"$ref": "#/definitions/domain.BlockGridConfig"
|
||||||
|
},
|
||||||
"carousel_config": {
|
"carousel_config": {
|
||||||
"$ref": "#/definitions/domain.CarouselConfig"
|
"$ref": "#/definitions/domain.CarouselConfig"
|
||||||
},
|
},
|
||||||
|
|
@ -7343,6 +7405,9 @@ const docTemplate = `{
|
||||||
"$ref": "#/definitions/domain.RecommendNodeListResp"
|
"$ref": "#/definitions/domain.RecommendNodeListResp"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"question_config": {
|
||||||
|
"$ref": "#/definitions/domain.QuestionConfig"
|
||||||
|
},
|
||||||
"simple_doc_config": {
|
"simple_doc_config": {
|
||||||
"$ref": "#/definitions/domain.SimpleDocConfig"
|
"$ref": "#/definitions/domain.SimpleDocConfig"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4969,6 +4969,34 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.BlockGridConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"list": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.BrandGroup": {
|
"domain.BrandGroup": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -6755,6 +6783,31 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.QuestionConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"list": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"question": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.RagInfo": {
|
"domain.RagInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -7235,6 +7288,9 @@
|
||||||
"basic_doc_config": {
|
"basic_doc_config": {
|
||||||
"$ref": "#/definitions/domain.BasicDocConfig"
|
"$ref": "#/definitions/domain.BasicDocConfig"
|
||||||
},
|
},
|
||||||
|
"block_grid_config": {
|
||||||
|
"$ref": "#/definitions/domain.BlockGridConfig"
|
||||||
|
},
|
||||||
"carousel_config": {
|
"carousel_config": {
|
||||||
"$ref": "#/definitions/domain.CarouselConfig"
|
"$ref": "#/definitions/domain.CarouselConfig"
|
||||||
},
|
},
|
||||||
|
|
@ -7271,6 +7327,9 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"question_config": {
|
||||||
|
"$ref": "#/definitions/domain.QuestionConfig"
|
||||||
|
},
|
||||||
"simple_doc_config": {
|
"simple_doc_config": {
|
||||||
"$ref": "#/definitions/domain.SimpleDocConfig"
|
"$ref": "#/definitions/domain.SimpleDocConfig"
|
||||||
},
|
},
|
||||||
|
|
@ -7294,6 +7353,9 @@
|
||||||
"basic_doc_config": {
|
"basic_doc_config": {
|
||||||
"$ref": "#/definitions/domain.BasicDocConfig"
|
"$ref": "#/definitions/domain.BasicDocConfig"
|
||||||
},
|
},
|
||||||
|
"block_grid_config": {
|
||||||
|
"$ref": "#/definitions/domain.BlockGridConfig"
|
||||||
|
},
|
||||||
"carousel_config": {
|
"carousel_config": {
|
||||||
"$ref": "#/definitions/domain.CarouselConfig"
|
"$ref": "#/definitions/domain.CarouselConfig"
|
||||||
},
|
},
|
||||||
|
|
@ -7336,6 +7398,9 @@
|
||||||
"$ref": "#/definitions/domain.RecommendNodeListResp"
|
"$ref": "#/definitions/domain.RecommendNodeListResp"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"question_config": {
|
||||||
|
"$ref": "#/definitions/domain.QuestionConfig"
|
||||||
|
},
|
||||||
"simple_doc_config": {
|
"simple_doc_config": {
|
||||||
"$ref": "#/definitions/domain.SimpleDocConfig"
|
"$ref": "#/definitions/domain.SimpleDocConfig"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -833,6 +833,24 @@ definitions:
|
||||||
- ids
|
- ids
|
||||||
- kb_id
|
- kb_id
|
||||||
type: object
|
type: object
|
||||||
|
domain.BlockGridConfig:
|
||||||
|
properties:
|
||||||
|
list:
|
||||||
|
items:
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
domain.BrandGroup:
|
domain.BrandGroup:
|
||||||
properties:
|
properties:
|
||||||
links:
|
links:
|
||||||
|
|
@ -1993,6 +2011,22 @@ definitions:
|
||||||
model:
|
model:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
domain.QuestionConfig:
|
||||||
|
properties:
|
||||||
|
list:
|
||||||
|
items:
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
question:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
domain.RagInfo:
|
domain.RagInfo:
|
||||||
properties:
|
properties:
|
||||||
message:
|
message:
|
||||||
|
|
@ -2309,6 +2343,8 @@ definitions:
|
||||||
$ref: '#/definitions/domain.BannerConfig'
|
$ref: '#/definitions/domain.BannerConfig'
|
||||||
basic_doc_config:
|
basic_doc_config:
|
||||||
$ref: '#/definitions/domain.BasicDocConfig'
|
$ref: '#/definitions/domain.BasicDocConfig'
|
||||||
|
block_grid_config:
|
||||||
|
$ref: '#/definitions/domain.BlockGridConfig'
|
||||||
carousel_config:
|
carousel_config:
|
||||||
$ref: '#/definitions/domain.CarouselConfig'
|
$ref: '#/definitions/domain.CarouselConfig'
|
||||||
case_config:
|
case_config:
|
||||||
|
|
@ -2333,6 +2369,8 @@ definitions:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
question_config:
|
||||||
|
$ref: '#/definitions/domain.QuestionConfig'
|
||||||
simple_doc_config:
|
simple_doc_config:
|
||||||
$ref: '#/definitions/domain.SimpleDocConfig'
|
$ref: '#/definitions/domain.SimpleDocConfig'
|
||||||
text_config:
|
text_config:
|
||||||
|
|
@ -2348,6 +2386,8 @@ definitions:
|
||||||
$ref: '#/definitions/domain.BannerConfig'
|
$ref: '#/definitions/domain.BannerConfig'
|
||||||
basic_doc_config:
|
basic_doc_config:
|
||||||
$ref: '#/definitions/domain.BasicDocConfig'
|
$ref: '#/definitions/domain.BasicDocConfig'
|
||||||
|
block_grid_config:
|
||||||
|
$ref: '#/definitions/domain.BlockGridConfig'
|
||||||
carousel_config:
|
carousel_config:
|
||||||
$ref: '#/definitions/domain.CarouselConfig'
|
$ref: '#/definitions/domain.CarouselConfig'
|
||||||
case_config:
|
case_config:
|
||||||
|
|
@ -2376,6 +2416,8 @@ definitions:
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/domain.RecommendNodeListResp'
|
$ref: '#/definitions/domain.RecommendNodeListResp'
|
||||||
type: array
|
type: array
|
||||||
|
question_config:
|
||||||
|
$ref: '#/definitions/domain.QuestionConfig'
|
||||||
simple_doc_config:
|
simple_doc_config:
|
||||||
$ref: '#/definitions/domain.SimpleDocConfig'
|
$ref: '#/definitions/domain.SimpleDocConfig'
|
||||||
text_config:
|
text_config:
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,23 @@ type TextImgConfig struct {
|
||||||
Desc string `json:"desc"`
|
Desc string `json:"desc"`
|
||||||
} `json:"item"`
|
} `json:"item"`
|
||||||
}
|
}
|
||||||
|
type QuestionConfig struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
List []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Question string `json:"question"`
|
||||||
|
} `json:"list"`
|
||||||
|
}
|
||||||
|
type BlockGridConfig struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
List []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
} `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
type WebAppLandingConfig struct {
|
type WebAppLandingConfig struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
|
@ -310,6 +327,8 @@ type WebAppLandingConfig struct {
|
||||||
FeatureConfig *FeatureConfig `json:"feature_config,omitempty"`
|
FeatureConfig *FeatureConfig `json:"feature_config,omitempty"`
|
||||||
ImgTextConfig *ImgTextConfig `json:"img_text_config,omitempty"`
|
ImgTextConfig *ImgTextConfig `json:"img_text_config,omitempty"`
|
||||||
TextImgConfig *TextImgConfig `json:"text_img_config,omitempty"`
|
TextImgConfig *TextImgConfig `json:"text_img_config,omitempty"`
|
||||||
|
QuestionConfig *QuestionConfig `json:"question_config,omitempty"`
|
||||||
|
BlockGridConfig *BlockGridConfig `json:"block_grid_config,omitempty"`
|
||||||
ComConfigOrder []string `json:"com_config_order"`
|
ComConfigOrder []string `json:"com_config_order"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -526,6 +545,8 @@ type WebAppLandingConfigResp struct {
|
||||||
FeatureConfig *FeatureConfig `json:"feature_config,omitempty"`
|
FeatureConfig *FeatureConfig `json:"feature_config,omitempty"`
|
||||||
ImgTextConfig *ImgTextConfig `json:"img_text_config,omitempty"`
|
ImgTextConfig *ImgTextConfig `json:"img_text_config,omitempty"`
|
||||||
TextImgConfig *TextImgConfig `json:"text_img_config,omitempty"`
|
TextImgConfig *TextImgConfig `json:"text_img_config,omitempty"`
|
||||||
|
QuestionConfig *QuestionConfig `json:"question_config,omitempty"`
|
||||||
|
BlockGridConfig *BlockGridConfig `json:"block_grid_config,omitempty"`
|
||||||
ComConfigOrder []string `json:"com_config_order"`
|
ComConfigOrder []string `json:"com_config_order"`
|
||||||
NodeIds []string `json:"node_ids"`
|
NodeIds []string `json:"node_ids"`
|
||||||
Nodes []*RecommendNodeListResp `json:"nodes" gorm:"-"`
|
Nodes []*RecommendNodeListResp `json:"nodes" gorm:"-"`
|
||||||
|
|
|
||||||
|
|
@ -422,6 +422,8 @@ func (u *AppUsecase) GetAppDetailByKBIDAndAppType(ctx context.Context, kbID stri
|
||||||
FeatureConfig: app.Settings.WebAppLandingConfigs[i].FeatureConfig,
|
FeatureConfig: app.Settings.WebAppLandingConfigs[i].FeatureConfig,
|
||||||
ImgTextConfig: app.Settings.WebAppLandingConfigs[i].ImgTextConfig,
|
ImgTextConfig: app.Settings.WebAppLandingConfigs[i].ImgTextConfig,
|
||||||
TextImgConfig: app.Settings.WebAppLandingConfigs[i].TextImgConfig,
|
TextImgConfig: app.Settings.WebAppLandingConfigs[i].TextImgConfig,
|
||||||
|
QuestionConfig: app.Settings.WebAppLandingConfigs[i].QuestionConfig,
|
||||||
|
BlockGridConfig: app.Settings.WebAppLandingConfigs[i].BlockGridConfig,
|
||||||
ComConfigOrder: app.Settings.WebAppLandingConfigs[i].ComConfigOrder,
|
ComConfigOrder: app.Settings.WebAppLandingConfigs[i].ComConfigOrder,
|
||||||
NodeIds: app.Settings.WebAppLandingConfigs[i].NodeIds,
|
NodeIds: app.Settings.WebAppLandingConfigs[i].NodeIds,
|
||||||
}
|
}
|
||||||
|
|
@ -552,6 +554,8 @@ func (u *AppUsecase) ShareGetWebAppInfo(ctx context.Context, kbID string, authId
|
||||||
ImgTextConfig: app.Settings.WebAppLandingConfigs[i].ImgTextConfig,
|
ImgTextConfig: app.Settings.WebAppLandingConfigs[i].ImgTextConfig,
|
||||||
TextImgConfig: app.Settings.WebAppLandingConfigs[i].TextImgConfig,
|
TextImgConfig: app.Settings.WebAppLandingConfigs[i].TextImgConfig,
|
||||||
MetricsConfig: app.Settings.WebAppLandingConfigs[i].MetricsConfig,
|
MetricsConfig: app.Settings.WebAppLandingConfigs[i].MetricsConfig,
|
||||||
|
QuestionConfig: app.Settings.WebAppLandingConfigs[i].QuestionConfig,
|
||||||
|
BlockGridConfig: app.Settings.WebAppLandingConfigs[i].BlockGridConfig,
|
||||||
ComConfigOrder: app.Settings.WebAppLandingConfigs[i].ComConfigOrder,
|
ComConfigOrder: app.Settings.WebAppLandingConfigs[i].ComConfigOrder,
|
||||||
NodeIds: app.Settings.WebAppLandingConfigs[i].NodeIds,
|
NodeIds: app.Settings.WebAppLandingConfigs[i].NodeIds,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,6 @@
|
||||||
"@emoji-mart/data": "^1.2.1",
|
"@emoji-mart/data": "^1.2.1",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
"@reduxjs/toolkit": "^2.5.0",
|
"@reduxjs/toolkit": "^2.5.0",
|
||||||
"@tiptap/extension-collaboration": "^3.3.0",
|
|
||||||
"@tiptap/extension-collaboration-caret": "^3.3.0",
|
|
||||||
"ace-builds": "^1.43.4",
|
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"echarts": "^5.6.0",
|
"echarts": "^5.6.0",
|
||||||
|
|
@ -32,9 +29,9 @@
|
||||||
"lottie-react": "^2.4.1",
|
"lottie-react": "^2.4.1",
|
||||||
"lowlight": "^3.3.0",
|
"lowlight": "^3.3.0",
|
||||||
"prosemirror-state": "^1.4.3",
|
"prosemirror-state": "^1.4.3",
|
||||||
"react-ace": "^14.0.1",
|
|
||||||
"react-color-palette": "^7.3.1",
|
"react-color-palette": "^7.3.1",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
|
"react-diff-viewer": "^3.1.1",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-image-crop": "^11.0.10",
|
"react-image-crop": "^11.0.10",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
|
|
|
||||||
|
|
@ -385,14 +385,14 @@ const ThemeWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||||
const { appPreviewData } = useAppSelector(state => state.config);
|
const { appPreviewData } = useAppSelector(state => state.config);
|
||||||
|
|
||||||
const theme = useMemo(() => {
|
const theme = useMemo(() => {
|
||||||
|
const themeName =
|
||||||
|
appPreviewData?.settings?.web_app_landing_theme?.name || 'blue';
|
||||||
return createTheme(
|
return createTheme(
|
||||||
// @ts-expect-error themeOptions is not typed
|
// @ts-expect-error themeOptions is not typed
|
||||||
{
|
{
|
||||||
...themeOptions[0],
|
...themeOptions[0],
|
||||||
palette:
|
palette:
|
||||||
THEME_TO_PALETTE[
|
THEME_TO_PALETTE[themeName]?.palette || THEME_TO_PALETTE.blue.palette,
|
||||||
appPreviewData?.settings?.web_app_landing_theme?.name || 'blue'
|
|
||||||
].palette,
|
|
||||||
},
|
},
|
||||||
...themeOptions.slice(1),
|
...themeOptions.slice(1),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,7 @@ const ComponentBar = ({
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
direction={'row'}
|
direction={'row'}
|
||||||
sx={{
|
sx={{
|
||||||
|
flexShrink: 0,
|
||||||
cursor: 'not-allowed',
|
cursor: 'not-allowed',
|
||||||
height: '40px',
|
height: '40px',
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
|
|
@ -268,7 +269,9 @@ const ComponentBar = ({
|
||||||
<Stack sx={{ pr: '20px', marginTop: '15px' }}>
|
<Stack sx={{ pr: '20px', marginTop: '15px' }}>
|
||||||
<Select
|
<Select
|
||||||
value={
|
value={
|
||||||
appPreviewData.settings?.web_app_landing_theme?.name || 'blue'
|
THEME_TO_PALETTE[
|
||||||
|
appPreviewData.settings?.web_app_landing_theme?.name || 'blue'
|
||||||
|
]?.value || 'blue'
|
||||||
}
|
}
|
||||||
renderValue={value => {
|
renderValue={value => {
|
||||||
return THEME_TO_PALETTE[value]?.label;
|
return THEME_TO_PALETTE[value]?.label;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
import { Box, IconButton, Stack, TextField } from '@mui/material';
|
||||||
|
import { Icon } from '@ctzhian/ui';
|
||||||
|
import UploadFile from '@/components/UploadFile';
|
||||||
|
import {
|
||||||
|
CSSProperties,
|
||||||
|
Dispatch,
|
||||||
|
forwardRef,
|
||||||
|
HTMLAttributes,
|
||||||
|
SetStateAction,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
export type ItemType = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ItemProps = Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> & {
|
||||||
|
item: ItemType;
|
||||||
|
withOpacity?: boolean;
|
||||||
|
isDragging?: boolean;
|
||||||
|
dragHandleProps?: React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
handleRemove?: (id: string) => void;
|
||||||
|
handleUpdateItem?: (item: ItemType) => void;
|
||||||
|
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Item = forwardRef<HTMLDivElement, ItemProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
item,
|
||||||
|
withOpacity,
|
||||||
|
isDragging,
|
||||||
|
style,
|
||||||
|
dragHandleProps,
|
||||||
|
handleRemove,
|
||||||
|
handleUpdateItem,
|
||||||
|
setIsEdit,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const inlineStyles: CSSProperties = {
|
||||||
|
opacity: withOpacity ? '0.5' : '1',
|
||||||
|
borderRadius: '10px',
|
||||||
|
cursor: isDragging ? 'grabbing' : 'grab',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
width: '100%',
|
||||||
|
...style,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Box ref={ref} style={inlineStyles} {...props}>
|
||||||
|
<Stack
|
||||||
|
direction={'row'}
|
||||||
|
alignItems={'center'}
|
||||||
|
justifyContent={'space-between'}
|
||||||
|
gap={0.5}
|
||||||
|
sx={{
|
||||||
|
py: 1.5,
|
||||||
|
px: 1,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
borderRadius: '10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction={'column'}
|
||||||
|
gap={'20px'}
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
p: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UploadFile
|
||||||
|
name='url'
|
||||||
|
id={`${item.id}_image`}
|
||||||
|
type='url'
|
||||||
|
disabled={false}
|
||||||
|
accept='image/*'
|
||||||
|
width={160}
|
||||||
|
height={140}
|
||||||
|
value={item.url}
|
||||||
|
onChange={(url: string) => {
|
||||||
|
const updatedItem = { ...item, url: url };
|
||||||
|
handleUpdateItem?.(updatedItem);
|
||||||
|
setIsEdit(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label='名称'
|
||||||
|
slotProps={{
|
||||||
|
inputLabel: {
|
||||||
|
shrink: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
height: '36px',
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
height: '36px',
|
||||||
|
padding: '0 12px',
|
||||||
|
'& .MuiOutlinedInput-input': {
|
||||||
|
padding: '8px 0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
placeholder='请输入名称'
|
||||||
|
variant='outlined'
|
||||||
|
value={item.name}
|
||||||
|
onChange={e => {
|
||||||
|
const updatedItem = { ...item, name: e.target.value };
|
||||||
|
handleUpdateItem?.(updatedItem);
|
||||||
|
setIsEdit(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction={'column'}
|
||||||
|
sx={{ justifyContent: 'space-between', alignSelf: 'stretch' }}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
size='small'
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRemove?.(item.id);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
color: 'text.tertiary',
|
||||||
|
':hover': { color: 'error.main' },
|
||||||
|
width: '28px',
|
||||||
|
height: '28px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size='small'
|
||||||
|
sx={{
|
||||||
|
cursor: 'grab',
|
||||||
|
color: 'text.secondary',
|
||||||
|
'&:hover': { color: 'primary.main' },
|
||||||
|
}}
|
||||||
|
{...(dragHandleProps as any)}
|
||||||
|
>
|
||||||
|
<Icon type='icon-drag' />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Item;
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
||||||
|
import { TextField } from '@mui/material';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import DragList from './DragList';
|
||||||
|
import type { ConfigProps } from '../type';
|
||||||
|
import { useAppSelector } from '@/store';
|
||||||
|
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
||||||
|
import { Empty } from '@ctzhian/ui';
|
||||||
|
import { DEFAULT_DATA } from '../../../constants';
|
||||||
|
import { findConfigById, handleLandingConfigs } from '../../../utils';
|
||||||
|
|
||||||
|
const Config = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
|
const { appPreviewData } = useAppSelector(state => state.config);
|
||||||
|
const debouncedDispatch = useDebounceAppPreviewData();
|
||||||
|
const { control, setValue, watch, reset, subscribe } = useForm<
|
||||||
|
typeof DEFAULT_DATA.block_grid
|
||||||
|
>({
|
||||||
|
defaultValues: findConfigById(
|
||||||
|
appPreviewData?.settings?.web_app_landing_configs || [],
|
||||||
|
id,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = watch('list') || [];
|
||||||
|
|
||||||
|
const handleAddFeature = () => {
|
||||||
|
const nextId = `${Date.now()}`;
|
||||||
|
setValue('list', [...list, { id: nextId, name: '', url: '' }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleListChange = (
|
||||||
|
newList: (typeof DEFAULT_DATA.block_grid)['list'],
|
||||||
|
) => {
|
||||||
|
setValue('list', newList);
|
||||||
|
setIsEdit(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(
|
||||||
|
findConfigById(
|
||||||
|
appPreviewData?.settings?.web_app_landing_configs || [],
|
||||||
|
id,
|
||||||
|
),
|
||||||
|
{ keepDefaultValues: true },
|
||||||
|
);
|
||||||
|
}, [id, appPreviewData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const callback = subscribe({
|
||||||
|
formState: {
|
||||||
|
values: true,
|
||||||
|
},
|
||||||
|
callback: ({ values }) => {
|
||||||
|
const previewData = {
|
||||||
|
...appPreviewData,
|
||||||
|
settings: {
|
||||||
|
...appPreviewData?.settings,
|
||||||
|
web_app_landing_configs: handleLandingConfigs({
|
||||||
|
id,
|
||||||
|
config: appPreviewData?.settings?.web_app_landing_configs || [],
|
||||||
|
values,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setIsEdit(true);
|
||||||
|
debouncedDispatch(previewData);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
}, [subscribe, id, appPreviewData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledCommonWrapper>
|
||||||
|
<CommonItem title='标题'>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name='title'
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField label='文字' {...field} placeholder='请输入' />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommonItem>
|
||||||
|
<CommonItem title='宫格列表' onAdd={handleAddFeature}>
|
||||||
|
{list.length === 0 ? (
|
||||||
|
<Empty />
|
||||||
|
) : (
|
||||||
|
<DragList
|
||||||
|
data={list}
|
||||||
|
onChange={handleListChange}
|
||||||
|
setIsEdit={setIsEdit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CommonItem>
|
||||||
|
</StyledCommonWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Config;
|
||||||
|
|
@ -107,7 +107,7 @@ const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</CommonItem> */}
|
</CommonItem> */}
|
||||||
<CommonItem title='问题列表' onAdd={handleAddQuestion}>
|
<CommonItem title='链接列表' onAdd={handleAddQuestion}>
|
||||||
{list.length === 0 ? (
|
{list.length === 0 ? (
|
||||||
<Empty />
|
<Empty />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { Box, IconButton, Stack, TextField } from '@mui/material';
|
||||||
|
import { Icon } from '@ctzhian/ui';
|
||||||
|
import {
|
||||||
|
CSSProperties,
|
||||||
|
Dispatch,
|
||||||
|
forwardRef,
|
||||||
|
HTMLAttributes,
|
||||||
|
SetStateAction,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
export type ItemType = {
|
||||||
|
id: string;
|
||||||
|
question: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ItemProps = Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> & {
|
||||||
|
item: ItemType;
|
||||||
|
withOpacity?: boolean;
|
||||||
|
isDragging?: boolean;
|
||||||
|
dragHandleProps?: React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
handleRemove?: (id: string) => void;
|
||||||
|
handleUpdateItem?: (item: ItemType) => void;
|
||||||
|
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Item = forwardRef<HTMLDivElement, ItemProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
item,
|
||||||
|
withOpacity,
|
||||||
|
isDragging,
|
||||||
|
style,
|
||||||
|
dragHandleProps,
|
||||||
|
handleRemove,
|
||||||
|
handleUpdateItem,
|
||||||
|
setIsEdit,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const inlineStyles: CSSProperties = {
|
||||||
|
opacity: withOpacity ? '0.5' : '1',
|
||||||
|
borderRadius: '10px',
|
||||||
|
cursor: isDragging ? 'grabbing' : 'grab',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
width: '100%',
|
||||||
|
...style,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Box ref={ref} style={inlineStyles} {...props}>
|
||||||
|
<Stack
|
||||||
|
direction={'row'}
|
||||||
|
alignItems={'center'}
|
||||||
|
justifyContent={'space-between'}
|
||||||
|
gap={0.5}
|
||||||
|
sx={{
|
||||||
|
py: 1.5,
|
||||||
|
px: 1,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
borderRadius: '10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction={'column'}
|
||||||
|
gap={'20px'}
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
p: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
label='问题'
|
||||||
|
slotProps={{
|
||||||
|
inputLabel: {
|
||||||
|
shrink: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
height: '36px',
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
height: '36px',
|
||||||
|
padding: '0 12px',
|
||||||
|
'& .MuiOutlinedInput-input': {
|
||||||
|
padding: '8px 0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
placeholder='请输入问题'
|
||||||
|
variant='outlined'
|
||||||
|
value={item.question}
|
||||||
|
onChange={e => {
|
||||||
|
const updatedItem = { ...item, question: e.target.value };
|
||||||
|
handleUpdateItem?.(updatedItem);
|
||||||
|
setIsEdit(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction={'column'}
|
||||||
|
sx={{ justifyContent: 'space-between', alignSelf: 'stretch' }}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
size='small'
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRemove?.(item.id);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
color: 'text.tertiary',
|
||||||
|
':hover': { color: 'error.main' },
|
||||||
|
width: '28px',
|
||||||
|
height: '28px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size='small'
|
||||||
|
sx={{
|
||||||
|
cursor: 'grab',
|
||||||
|
color: 'text.secondary',
|
||||||
|
'&:hover': { color: 'primary.main' },
|
||||||
|
}}
|
||||||
|
{...(dragHandleProps as any)}
|
||||||
|
>
|
||||||
|
<Icon type='icon-drag' />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Item;
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
||||||
|
import { TextField } from '@mui/material';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import FaqDragList from './DragList';
|
||||||
|
import type { ConfigProps } from '../type';
|
||||||
|
import { useAppSelector } from '@/store';
|
||||||
|
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
||||||
|
import { Empty } from '@ctzhian/ui';
|
||||||
|
import { DEFAULT_DATA } from '../../../constants';
|
||||||
|
import { findConfigById, handleLandingConfigs } from '../../../utils';
|
||||||
|
|
||||||
|
const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
|
const { appPreviewData } = useAppSelector(state => state.config);
|
||||||
|
const debouncedDispatch = useDebounceAppPreviewData();
|
||||||
|
const { control, setValue, watch, reset, subscribe } = useForm<
|
||||||
|
typeof DEFAULT_DATA.question
|
||||||
|
>({
|
||||||
|
defaultValues: findConfigById(
|
||||||
|
appPreviewData?.settings?.web_app_landing_configs || [],
|
||||||
|
id,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = watch('list') || [];
|
||||||
|
|
||||||
|
const handleAddQuestion = () => {
|
||||||
|
const nextId = `${Date.now()}`;
|
||||||
|
setValue('list', [...list, { id: nextId, question: '' }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleListChange = (
|
||||||
|
newList: (typeof DEFAULT_DATA.question)['list'],
|
||||||
|
) => {
|
||||||
|
setValue('list', newList);
|
||||||
|
setIsEdit(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(
|
||||||
|
findConfigById(
|
||||||
|
appPreviewData?.settings?.web_app_landing_configs || [],
|
||||||
|
id,
|
||||||
|
),
|
||||||
|
{ keepDefaultValues: true },
|
||||||
|
);
|
||||||
|
}, [id, appPreviewData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const callback = subscribe({
|
||||||
|
formState: {
|
||||||
|
values: true,
|
||||||
|
},
|
||||||
|
callback: ({ values }) => {
|
||||||
|
const previewData = {
|
||||||
|
...appPreviewData,
|
||||||
|
settings: {
|
||||||
|
...appPreviewData?.settings,
|
||||||
|
web_app_landing_configs: handleLandingConfigs({
|
||||||
|
id,
|
||||||
|
config: appPreviewData?.settings?.web_app_landing_configs || [],
|
||||||
|
values,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setIsEdit(true);
|
||||||
|
debouncedDispatch(previewData);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
}, [subscribe, id, appPreviewData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledCommonWrapper>
|
||||||
|
<CommonItem title='标题'>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name='title'
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField label='文字' {...field} placeholder='请输入' />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommonItem>
|
||||||
|
|
||||||
|
<CommonItem title='常见问题列表' onAdd={handleAddQuestion}>
|
||||||
|
{list.length === 0 ? (
|
||||||
|
<Empty />
|
||||||
|
) : (
|
||||||
|
<FaqDragList
|
||||||
|
data={list}
|
||||||
|
onChange={handleListChange}
|
||||||
|
setIsEdit={setIsEdit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CommonItem>
|
||||||
|
</StyledCommonWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FaqConfig;
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
IconJianyiwendang,
|
IconJianyiwendang,
|
||||||
IconChangjianwenti,
|
IconChangjianwenti,
|
||||||
IconLunbotu,
|
IconLunbotu,
|
||||||
IconShanchu,
|
|
||||||
IconDanwenzi,
|
IconDanwenzi,
|
||||||
IconShuzikapian,
|
IconShuzikapian,
|
||||||
IconKehuanli,
|
IconKehuanli,
|
||||||
|
|
@ -13,6 +12,8 @@ import {
|
||||||
IconZuotuyouzi,
|
IconZuotuyouzi,
|
||||||
IconYoutuzuozi,
|
IconYoutuzuozi,
|
||||||
IconKehupingjia,
|
IconKehupingjia,
|
||||||
|
IconJiugongge,
|
||||||
|
IconLianjiezu1,
|
||||||
} from '@panda-wiki/icons';
|
} from '@panda-wiki/icons';
|
||||||
import { DomainRecommendNodeListResp } from '@/request/types';
|
import { DomainRecommendNodeListResp } from '@/request/types';
|
||||||
|
|
||||||
|
|
@ -70,6 +71,14 @@ export const DEFAULT_DATA = {
|
||||||
comment: string;
|
comment: string;
|
||||||
}[],
|
}[],
|
||||||
},
|
},
|
||||||
|
block_grid: {
|
||||||
|
title: '区块网格',
|
||||||
|
list: [] as {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
}[],
|
||||||
|
},
|
||||||
banner: {
|
banner: {
|
||||||
title: '',
|
title: '',
|
||||||
subtitle: '',
|
subtitle: '',
|
||||||
|
|
@ -105,13 +114,20 @@ export const DEFAULT_DATA = {
|
||||||
}[],
|
}[],
|
||||||
},
|
},
|
||||||
faq: {
|
faq: {
|
||||||
title: '常见问题',
|
title: '链接组',
|
||||||
list: [] as {
|
list: [] as {
|
||||||
id: string;
|
id: string;
|
||||||
question: string;
|
question: string;
|
||||||
link: string;
|
link: string;
|
||||||
}[],
|
}[],
|
||||||
},
|
},
|
||||||
|
question: {
|
||||||
|
title: '常见问题',
|
||||||
|
list: [] as {
|
||||||
|
id: string;
|
||||||
|
question: string;
|
||||||
|
}[],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const COMPONENTS_MAP = {
|
export const COMPONENTS_MAP = {
|
||||||
|
|
@ -176,7 +192,7 @@ export const COMPONENTS_MAP = {
|
||||||
faq: {
|
faq: {
|
||||||
name: 'faq',
|
name: 'faq',
|
||||||
title: '链接组',
|
title: '链接组',
|
||||||
icon: IconChangjianwenti,
|
icon: IconLianjiezu1,
|
||||||
component: lazy(() => import('@panda-wiki/ui/faq')),
|
component: lazy(() => import('@panda-wiki/ui/faq')),
|
||||||
config: lazy(() => import('./components/config/FaqConfig')),
|
config: lazy(() => import('./components/config/FaqConfig')),
|
||||||
fixed: false,
|
fixed: false,
|
||||||
|
|
@ -262,6 +278,26 @@ export const COMPONENTS_MAP = {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
},
|
},
|
||||||
|
block_grid: {
|
||||||
|
name: 'block_grid',
|
||||||
|
title: '区块网格',
|
||||||
|
icon: IconJiugongge,
|
||||||
|
component: lazy(() => import('@panda-wiki/ui/blockGrid')),
|
||||||
|
config: lazy(() => import('./components/config/BlockGridConfig')),
|
||||||
|
fixed: false,
|
||||||
|
disabled: false,
|
||||||
|
hidden: false,
|
||||||
|
},
|
||||||
|
question: {
|
||||||
|
name: 'question',
|
||||||
|
title: '常见问题',
|
||||||
|
icon: IconChangjianwenti,
|
||||||
|
component: lazy(() => import('@panda-wiki/ui/question')),
|
||||||
|
config: lazy(() => import('./components/config/QuestionConfig')),
|
||||||
|
fixed: false,
|
||||||
|
disabled: false,
|
||||||
|
hidden: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TYPE_TO_CONFIG_LABEL = {
|
export const TYPE_TO_CONFIG_LABEL = {
|
||||||
|
|
@ -278,4 +314,6 @@ export const TYPE_TO_CONFIG_LABEL = {
|
||||||
text_img: 'text_img_config',
|
text_img: 'text_img_config',
|
||||||
img_text: 'img_text_config',
|
img_text: 'img_text_config',
|
||||||
comment: 'comment_config',
|
comment: 'comment_config',
|
||||||
|
block_grid: 'block_grid_config',
|
||||||
|
question: 'question_config',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
import { DEFAULT_DATA, TYPE_TO_CONFIG_LABEL } from './constants';
|
|
||||||
import Logo from '@/assets/images/footer-logo.png';
|
|
||||||
|
|
||||||
const handleHeaderProps = (setting: any) => {
|
const handleHeaderProps = (setting: any) => {
|
||||||
return {
|
return {
|
||||||
title: setting.title,
|
title: setting.title,
|
||||||
|
|
@ -159,6 +156,20 @@ const handleCommentProps = (config: any = {}) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBlockGridProps = (config: any = {}) => {
|
||||||
|
return {
|
||||||
|
title: config.title || '区块网格',
|
||||||
|
items: config.list || [],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuestionProps = (config: any = {}) => {
|
||||||
|
return {
|
||||||
|
title: config.title || '常见问题',
|
||||||
|
items: config.list || [],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const handleComponentProps = (
|
export const handleComponentProps = (
|
||||||
type: string,
|
type: string,
|
||||||
id: string,
|
id: string,
|
||||||
|
|
@ -200,6 +211,10 @@ export const handleComponentProps = (
|
||||||
return handleTextImgProps(config);
|
return handleTextImgProps(config);
|
||||||
case 'comment':
|
case 'comment':
|
||||||
return handleCommentProps(config);
|
return handleCommentProps(config);
|
||||||
|
case 'block_grid':
|
||||||
|
return handleBlockGridProps(config);
|
||||||
|
case 'question':
|
||||||
|
return handleQuestionProps(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
import {
|
||||||
|
ConstsContributeStatus,
|
||||||
|
ConstsContributeType,
|
||||||
|
getApiProV1ContributeDetail,
|
||||||
|
GithubComChaitinPandaWikiProApiContributeV1ContributeDetailResp,
|
||||||
|
GithubComChaitinPandaWikiProApiContributeV1ContributeItem,
|
||||||
|
} from '@/request/pro';
|
||||||
|
import { useAppSelector } from '@/store';
|
||||||
|
import { Modal } from '@ctzhian/ui';
|
||||||
|
import { Box, Button, Stack } from '@mui/material';
|
||||||
|
import { IconWenjian } from '@panda-wiki/icons';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import ReactDiffViewer from 'react-diff-viewer';
|
||||||
|
|
||||||
|
type MarkdownPreviewModalProps = {
|
||||||
|
open: boolean;
|
||||||
|
row: GithubComChaitinPandaWikiProApiContributeV1ContributeItem | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onAccept: () => void;
|
||||||
|
onReject: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MarkdownPreviewModal = ({
|
||||||
|
open,
|
||||||
|
row,
|
||||||
|
onClose,
|
||||||
|
onAccept,
|
||||||
|
onReject,
|
||||||
|
}: MarkdownPreviewModalProps) => {
|
||||||
|
const { kb_id = '' } = useAppSelector(state => state.config);
|
||||||
|
const [data, setData] =
|
||||||
|
useState<GithubComChaitinPandaWikiProApiContributeV1ContributeDetailResp | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && row) {
|
||||||
|
getApiProV1ContributeDetail({ id: row.id!, kb_id }).then(res => {
|
||||||
|
setData(res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [open, row, kb_id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onCancel={onClose}
|
||||||
|
width={'1200px'}
|
||||||
|
sx={{
|
||||||
|
'.MuiDialogContent-root': {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
<Stack direction='row' alignItems='center' gap={2}>
|
||||||
|
<Box>
|
||||||
|
来自 {row?.auth_name || '匿名用户'} 的
|
||||||
|
{row?.type === ConstsContributeType.ContributeTypeAdd
|
||||||
|
? '新增'
|
||||||
|
: '修改'}
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ fontSize: 14, color: 'text.auxiliary', fontWeight: 400 }}>
|
||||||
|
{dayjs(row?.created_at).fromNow()}
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
row?.status === ConstsContributeStatus.ContributeStatusPending ||
|
||||||
|
row?.status === ConstsContributeStatus.ContributeStatusRejected ? (
|
||||||
|
<Stack
|
||||||
|
direction='row'
|
||||||
|
gap={1}
|
||||||
|
justifyContent='flex-end'
|
||||||
|
sx={{ p: 3, pt: 0 }}
|
||||||
|
>
|
||||||
|
{row?.status === ConstsContributeStatus.ContributeStatusPending ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
variant='outlined'
|
||||||
|
color='error'
|
||||||
|
onClick={onReject}
|
||||||
|
>
|
||||||
|
拒绝
|
||||||
|
</Button>
|
||||||
|
<Button size='small' variant='contained' onClick={onAccept}>
|
||||||
|
采纳
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button onClick={onClose} size='small' variant='contained'>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack direction='row'>
|
||||||
|
<Stack
|
||||||
|
spacing={2}
|
||||||
|
sx={{
|
||||||
|
overflow: 'auto',
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction='row'
|
||||||
|
gap={1}
|
||||||
|
sx={{ bgcolor: 'background.paper2', p: 1, borderRadius: '10px' }}
|
||||||
|
>
|
||||||
|
<Box sx={{ fontSize: 14, fontWeight: 'bold', flexShrink: 0 }}>
|
||||||
|
提交说明:
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ fontSize: 14, color: 'text.tertiary' }}>
|
||||||
|
{data?.reason || '-'}
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
direction='row'
|
||||||
|
alignItems='center'
|
||||||
|
gap={1}
|
||||||
|
sx={{ fontSize: 24, fontWeight: 700, pb: 2 }}
|
||||||
|
>
|
||||||
|
<IconWenjian /> {row?.node_name || '-'}
|
||||||
|
</Stack>
|
||||||
|
<Box sx={{ overflowY: 'auto', maxHeight: 'calc(100vh - 400px)' }}>
|
||||||
|
<ReactDiffViewer
|
||||||
|
oldValue={data?.original_node?.content || ''}
|
||||||
|
newValue={data?.content || ''}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarkdownPreviewModal;
|
||||||
|
|
@ -1,26 +1,27 @@
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { styled } from '@mui/material/styles';
|
|
||||||
import Logo from '@/assets/images/logo.png';
|
import Logo from '@/assets/images/logo.png';
|
||||||
import { Box, Chip, Stack, TextField } from '@mui/material';
|
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import { tableSx } from '@/constant/styles';
|
import { tableSx } from '@/constant/styles';
|
||||||
import dayjs from 'dayjs';
|
import { Ellipsis, message, Modal, Table } from '@ctzhian/ui';
|
||||||
import { Table, Ellipsis, message, Modal } from '@ctzhian/ui';
|
|
||||||
import type { ColumnType } from '@ctzhian/ui/dist/Table';
|
import type { ColumnType } from '@ctzhian/ui/dist/Table';
|
||||||
|
import { Box, Chip, Stack, TextField } from '@mui/material';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import DocModal from './DocModal';
|
import DocModal from './DocModal';
|
||||||
|
|
||||||
|
import { useURLSearchParams } from '@/hooks';
|
||||||
import {
|
import {
|
||||||
getApiProV1ContributeList,
|
getApiProV1ContributeList,
|
||||||
postApiProV1ContributeAudit,
|
postApiProV1ContributeAudit,
|
||||||
} from '@/request/pro/Contribute';
|
} from '@/request/pro/Contribute';
|
||||||
import {
|
import {
|
||||||
GithubComChaitinPandaWikiProApiContributeV1ContributeItem,
|
|
||||||
ConstsContributeStatus,
|
ConstsContributeStatus,
|
||||||
ConstsContributeType,
|
ConstsContributeType,
|
||||||
|
GithubComChaitinPandaWikiProApiContributeV1ContributeItem,
|
||||||
} from '@/request/pro/types';
|
} from '@/request/pro/types';
|
||||||
import { useURLSearchParams } from '@/hooks';
|
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import ContributePreviewModal from './ContributePreviewModal';
|
import ContributePreviewModal from './ContributePreviewModal';
|
||||||
|
import MarkdownPreviewModal from './MarkdownPreviewModal';
|
||||||
|
|
||||||
const StyledSearchRow = styled(Stack)(({ theme }) => ({
|
const StyledSearchRow = styled(Stack)(({ theme }) => ({
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
|
|
@ -365,13 +366,23 @@ export default function ContributionPage() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ContributePreviewModal
|
{previewRow?.meta?.content_type === 'md' ? (
|
||||||
open={open}
|
<MarkdownPreviewModal
|
||||||
row={previewRow}
|
open={open}
|
||||||
onClose={closeDialog}
|
row={previewRow}
|
||||||
onAccept={handleAccept}
|
onClose={closeDialog}
|
||||||
onReject={handleReject}
|
onAccept={handleAccept}
|
||||||
/>
|
onReject={handleReject}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ContributePreviewModal
|
||||||
|
open={open}
|
||||||
|
row={previewRow}
|
||||||
|
onClose={closeDialog}
|
||||||
|
onAccept={handleAccept}
|
||||||
|
onReject={handleReject}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<DocModal
|
<DocModal
|
||||||
open={docModalOpen}
|
open={docModalOpen}
|
||||||
onClose={() => setDocModalOpen(false)}
|
onClose={() => setDocModalOpen(false)}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import { DomainNodeListItemResp, V1NodeDetailResp } from '@/request/types';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import { addOpacityToColor } from '@/utils';
|
import { addOpacityToColor } from '@/utils';
|
||||||
import { convertToTree } from '@/utils/drag';
|
import { convertToTree } from '@/utils/drag';
|
||||||
import { filterEmptyFolders } from '@/utils/tree';
|
|
||||||
import { Ellipsis, Icon } from '@ctzhian/ui';
|
import { Ellipsis, Icon } from '@ctzhian/ui';
|
||||||
import { alpha, Box, IconButton, Stack, useTheme } from '@mui/material';
|
import { alpha, Box, IconButton, Stack, useTheme } from '@mui/material';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
@ -66,7 +65,7 @@ const Catalog = ({ curNode, setCatalogOpen }: CatalogProps) => {
|
||||||
kb_id: kb_id || localStorage.getItem('kb_id') || '',
|
kb_id: kb_id || localStorage.getItem('kb_id') || '',
|
||||||
};
|
};
|
||||||
getApiV1NodeList(params).then(res => {
|
getApiV1NodeList(params).then(res => {
|
||||||
const v = filterEmptyFolders(convertToTree(res || []));
|
const v = convertToTree(res || []);
|
||||||
setData(v);
|
setData(v);
|
||||||
// 计算当前文档的所有父级文件夹,并默认展开
|
// 计算当前文档的所有父级文件夹,并默认展开
|
||||||
try {
|
try {
|
||||||
|
|
@ -110,6 +109,75 @@ const Catalog = ({ curNode, setCatalogOpen }: CatalogProps) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderAdd = (parentId: string) => {
|
||||||
|
return (
|
||||||
|
<Box sx={{ flexShrink: 0 }} onClick={e => e.stopPropagation()}>
|
||||||
|
<Cascader
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'left',
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'left',
|
||||||
|
}}
|
||||||
|
context={
|
||||||
|
<IconButton>
|
||||||
|
<Icon
|
||||||
|
className='catalog-folder-add-icon'
|
||||||
|
type='icon-icon_tool_close'
|
||||||
|
sx={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: 'action.selected',
|
||||||
|
transform: 'rotate(45deg)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
list={Object.entries(ImportContentWays).map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
label: (
|
||||||
|
<Box key={key}>
|
||||||
|
<Stack
|
||||||
|
direction={'row'}
|
||||||
|
alignItems={'center'}
|
||||||
|
gap={1}
|
||||||
|
sx={{
|
||||||
|
fontSize: 14,
|
||||||
|
px: 2,
|
||||||
|
lineHeight: '40px',
|
||||||
|
height: 40,
|
||||||
|
width: 180,
|
||||||
|
borderRadius: '5px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
':hover': {
|
||||||
|
bgcolor: addOpacityToColor(
|
||||||
|
theme.palette.primary.main,
|
||||||
|
0.1,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={() => value.onClick(parentId)}
|
||||||
|
>
|
||||||
|
{value.label}
|
||||||
|
</Stack>
|
||||||
|
{key === 'OfflineFile' && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
borderTop: '1px solid',
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
my: 0.5,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderTree = (items: ITreeItem[], pl = 2.5, depth = 1) => {
|
const renderTree = (items: ITreeItem[], pl = 2.5, depth = 1) => {
|
||||||
const sortedItems = [...items].sort(
|
const sortedItems = [...items].sort(
|
||||||
(a, b) => (a.order ?? 0) - (b.order ?? 0),
|
(a, b) => (a.order ?? 0) - (b.order ?? 0),
|
||||||
|
|
@ -210,72 +278,7 @@ const Catalog = ({ curNode, setCatalogOpen }: CatalogProps) => {
|
||||||
MD
|
MD
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{item.type === 1 && (
|
{item.type === 1 && renderAdd(item.id)}
|
||||||
<Box sx={{ flexShrink: 0 }} onClick={e => e.stopPropagation()}>
|
|
||||||
<Cascader
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'left',
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'left',
|
|
||||||
}}
|
|
||||||
context={
|
|
||||||
<IconButton>
|
|
||||||
<Icon
|
|
||||||
className='catalog-folder-add-icon'
|
|
||||||
type='icon-icon_tool_close'
|
|
||||||
sx={{
|
|
||||||
fontSize: 16,
|
|
||||||
color: 'action.selected',
|
|
||||||
transform: 'rotate(45deg)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
list={Object.entries(ImportContentWays).map(([key, value]) => ({
|
|
||||||
key,
|
|
||||||
label: (
|
|
||||||
<Box key={key}>
|
|
||||||
<Stack
|
|
||||||
direction={'row'}
|
|
||||||
alignItems={'center'}
|
|
||||||
gap={1}
|
|
||||||
sx={{
|
|
||||||
fontSize: 14,
|
|
||||||
px: 2,
|
|
||||||
lineHeight: '40px',
|
|
||||||
height: 40,
|
|
||||||
width: 180,
|
|
||||||
borderRadius: '5px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
':hover': {
|
|
||||||
bgcolor: addOpacityToColor(
|
|
||||||
theme.palette.primary.main,
|
|
||||||
0.1,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
onClick={() => value.onClick(item.id)}
|
|
||||||
>
|
|
||||||
{value.label}
|
|
||||||
</Stack>
|
|
||||||
{key === 'OfflineFile' && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
borderTop: '1px solid',
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
my: 0.5,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
{item.children &&
|
{item.children &&
|
||||||
item.children.length > 0 &&
|
item.children.length > 0 &&
|
||||||
|
|
@ -341,16 +344,24 @@ const Catalog = ({ curNode, setCatalogOpen }: CatalogProps) => {
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Box
|
<Stack
|
||||||
sx={{
|
direction={'row'}
|
||||||
px: 2,
|
alignItems={'center'}
|
||||||
fontSize: 14,
|
justifyContent={'space-between'}
|
||||||
fontWeight: 'bold',
|
sx={{ pr: 1 }}
|
||||||
color: 'text.tertiary',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
目录
|
<Box
|
||||||
</Box>
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'text.tertiary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
目录
|
||||||
|
</Box>
|
||||||
|
{renderAdd('')}
|
||||||
|
</Stack>
|
||||||
<Stack
|
<Stack
|
||||||
sx={{
|
sx={{
|
||||||
my: 1,
|
my: 1,
|
||||||
|
|
@ -368,38 +379,59 @@ const Catalog = ({ curNode, setCatalogOpen }: CatalogProps) => {
|
||||||
open={customDocOpen}
|
open={customDocOpen}
|
||||||
parentId={opraParentId}
|
parentId={opraParentId}
|
||||||
onCreated={node => {
|
onCreated={node => {
|
||||||
// 复用工具方法:findItemDeep / setProperty
|
if (opraParentId) {
|
||||||
setData(prev => {
|
// 复用工具方法:findItemDeep / setProperty
|
||||||
const parent = findItemDeep(prev, opraParentId);
|
setData(prev => {
|
||||||
if (!parent) return prev;
|
const parent = findItemDeep(prev, opraParentId);
|
||||||
const children = (parent.children as ITreeItem[] | undefined) ?? [];
|
if (!parent) return prev;
|
||||||
const lastOrder = children.length
|
const children =
|
||||||
? (children[children.length - 1].order ?? children.length - 1)
|
(parent.children as ITreeItem[] | undefined) ?? [];
|
||||||
: -1;
|
const lastOrder = children.length
|
||||||
|
? (children[children.length - 1].order ?? children.length - 1)
|
||||||
|
: -1;
|
||||||
|
const newChild: ITreeItem = {
|
||||||
|
id: node.id,
|
||||||
|
name: node.name,
|
||||||
|
content_type: node.content_type,
|
||||||
|
type: node.type,
|
||||||
|
emoji: node.emoji,
|
||||||
|
parentId: parent.id,
|
||||||
|
level: (parent.level ?? 0) + 1,
|
||||||
|
order: lastOrder + 1,
|
||||||
|
status: 1,
|
||||||
|
children: node.type === 1 ? [] : undefined,
|
||||||
|
};
|
||||||
|
const next = setProperty(prev, opraParentId, 'children', val => [
|
||||||
|
...((val as ITreeItem[] | undefined) ?? []),
|
||||||
|
newChild,
|
||||||
|
]) as ITreeItem[];
|
||||||
|
return [...next];
|
||||||
|
});
|
||||||
|
// 展开父级,确保新项可见
|
||||||
|
setExpandedFolders(prev => {
|
||||||
|
const ns = new Set(prev);
|
||||||
|
if (opraParentId) ns.add(opraParentId);
|
||||||
|
return ns;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
const newChild: ITreeItem = {
|
const newChild: ITreeItem = {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
name: node.name,
|
name: node.name,
|
||||||
content_type: node.content_type,
|
content_type: node.content_type,
|
||||||
type: node.type,
|
type: node.type,
|
||||||
emoji: node.emoji,
|
emoji: node.emoji,
|
||||||
parentId: parent.id,
|
parentId: '',
|
||||||
level: (parent.level ?? 0) + 1,
|
level: 1,
|
||||||
order: lastOrder + 1,
|
order: data.length
|
||||||
|
? (data[data.length - 1].order ?? data.length - 1)
|
||||||
|
: -1,
|
||||||
status: 1,
|
status: 1,
|
||||||
children: node.type === 1 ? [] : undefined,
|
children: node.type === 1 ? [] : undefined,
|
||||||
};
|
};
|
||||||
const next = setProperty(prev, opraParentId, 'children', val => [
|
setData(prev => {
|
||||||
...((val as ITreeItem[] | undefined) ?? []),
|
return [...prev, newChild];
|
||||||
newChild,
|
});
|
||||||
]) as ITreeItem[];
|
}
|
||||||
return [...next];
|
|
||||||
});
|
|
||||||
// 展开父级,确保新项可见
|
|
||||||
setExpandedFolders(prev => {
|
|
||||||
const ns = new Set(prev);
|
|
||||||
if (opraParentId) ns.add(opraParentId);
|
|
||||||
return ns;
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
onClose={() => setCustomDocOpen(false)}
|
onClose={() => setCustomDocOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { Editor, UseTiptapReturn } from '@ctzhian/tiptap';
|
import {
|
||||||
import { alpha, Box, Divider, Stack, useTheme } from '@mui/material';
|
EditorMarkdown,
|
||||||
import { useState } from 'react';
|
MarkdownEditorRef,
|
||||||
import AceEditor from 'react-ace';
|
UseTiptapReturn,
|
||||||
|
} from '@ctzhian/tiptap';
|
||||||
import 'ace-builds/src-noconflict/ext-language_tools';
|
import { Box } from '@mui/material';
|
||||||
import 'ace-builds/src-noconflict/mode-markdown';
|
import { forwardRef } from 'react';
|
||||||
import 'ace-builds/src-noconflict/theme-github';
|
|
||||||
|
|
||||||
interface MarkdownEditorProps {
|
interface MarkdownEditorProps {
|
||||||
editor: UseTiptapReturn['editor'];
|
editor: UseTiptapReturn['editor'];
|
||||||
|
|
@ -14,132 +13,29 @@ interface MarkdownEditorProps {
|
||||||
header: React.ReactNode;
|
header: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MarkdownEditor = ({
|
const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>(
|
||||||
editor,
|
({ editor, value, onChange, header }, ref) => {
|
||||||
value,
|
return (
|
||||||
onChange,
|
<Box
|
||||||
header,
|
|
||||||
}: MarkdownEditorProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const [displayMode, setDisplayMode] = useState<'edit' | 'preview' | 'split'>(
|
|
||||||
'split',
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
mt: '56px',
|
|
||||||
px: 10,
|
|
||||||
pt: 4,
|
|
||||||
flex: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{}}>{header}</Box>
|
|
||||||
<Stack
|
|
||||||
direction={'row'}
|
|
||||||
alignItems={'stretch'}
|
|
||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
mt: '56px',
|
||||||
|
px: 10,
|
||||||
|
pt: 4,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
border: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack
|
<Box sx={{}}>{header}</Box>
|
||||||
direction='row'
|
<EditorMarkdown
|
||||||
sx={{
|
editor={editor}
|
||||||
position: 'absolute',
|
value={value}
|
||||||
p: 0.5,
|
onAceChange={onChange}
|
||||||
top: -32,
|
height='calc(100vh - 340px)'
|
||||||
left: -1,
|
/>
|
||||||
border: '1px solid',
|
</Box>
|
||||||
borderColor: 'divider',
|
);
|
||||||
borderBottom: 'none',
|
},
|
||||||
borderRadius: '4px 4px 0 0',
|
);
|
||||||
fontSize: 12,
|
|
||||||
color: 'text.tertiary',
|
MarkdownEditor.displayName = 'MarkdownEditor';
|
||||||
'.md-display-mode-active': {
|
|
||||||
color: 'primary.main',
|
|
||||||
bgcolor: alpha(theme.palette.primary.main, 0.1),
|
|
||||||
},
|
|
||||||
'& :hover:not(.md-display-mode-active)': {
|
|
||||||
borderRadius: '4px',
|
|
||||||
bgcolor: 'background.paper3',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
className={displayMode === 'split' ? 'md-display-mode-active' : ''}
|
|
||||||
sx={{ px: 1, py: 0.25, cursor: 'pointer', borderRadius: '4px' }}
|
|
||||||
onClick={() => setDisplayMode('split')}
|
|
||||||
>
|
|
||||||
分屏模式
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
className={displayMode === 'edit' ? 'md-display-mode-active' : ''}
|
|
||||||
sx={{ px: 1, py: 0.25, cursor: 'pointer', borderRadius: '4px' }}
|
|
||||||
onClick={() => setDisplayMode('edit')}
|
|
||||||
>
|
|
||||||
编辑模式
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
className={
|
|
||||||
displayMode === 'preview' ? 'md-display-mode-active' : ''
|
|
||||||
}
|
|
||||||
sx={{ px: 1, py: 0.25, cursor: 'pointer', borderRadius: '4px' }}
|
|
||||||
onClick={() => setDisplayMode('preview')}
|
|
||||||
>
|
|
||||||
预览模式
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
{['edit', 'split'].includes(displayMode) && (
|
|
||||||
<Stack
|
|
||||||
direction='column'
|
|
||||||
sx={{
|
|
||||||
flex: 1,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AceEditor
|
|
||||||
mode='markdown'
|
|
||||||
theme='twilight'
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
name='project-doc-editor'
|
|
||||||
wrapEnabled={true}
|
|
||||||
showPrintMargin={false}
|
|
||||||
fontSize={16}
|
|
||||||
editorProps={{ $blockScrolling: true }}
|
|
||||||
setOptions={{
|
|
||||||
enableBasicAutocompletion: true,
|
|
||||||
enableLiveAutocompletion: true,
|
|
||||||
showLineNumbers: true,
|
|
||||||
tabSize: 2,
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: 'calc(100vh - 56px)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
{displayMode === 'split' && <Divider orientation='vertical' flexItem />}
|
|
||||||
{['split', 'preview'].includes(displayMode) && (
|
|
||||||
<Box
|
|
||||||
id='markdown-preview-container'
|
|
||||||
sx={{
|
|
||||||
overflowY: 'scroll',
|
|
||||||
flex: 1,
|
|
||||||
p: 2,
|
|
||||||
height: 'calc(100vh - 56px)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Editor editor={editor} />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MarkdownEditor;
|
export default MarkdownEditor;
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ interface TocProps {
|
||||||
setFixed: (fixed: boolean) => void;
|
setFixed: (fixed: boolean) => void;
|
||||||
setShowSummary: (showSummary: boolean) => void;
|
setShowSummary: (showSummary: boolean) => void;
|
||||||
isMarkdown: boolean;
|
isMarkdown: boolean;
|
||||||
|
scrollToHeading?: (headingText: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeadingIcon = [
|
const HeadingIcon = [
|
||||||
|
|
@ -34,7 +35,13 @@ const HeadingSx = [
|
||||||
{ fontSize: 14, fontWeight: 400, color: 'text.disabled' },
|
{ fontSize: 14, fontWeight: 400, color: 'text.disabled' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const Toc = ({ headings, fixed, setFixed, isMarkdown }: TocProps) => {
|
const Toc = ({
|
||||||
|
headings,
|
||||||
|
fixed,
|
||||||
|
setFixed,
|
||||||
|
isMarkdown,
|
||||||
|
scrollToHeading,
|
||||||
|
}: TocProps) => {
|
||||||
const storageTocOpen = localStorage.getItem('toc-open');
|
const storageTocOpen = localStorage.getItem('toc-open');
|
||||||
const [open, setOpen] = useState(!!storageTocOpen);
|
const [open, setOpen] = useState(!!storageTocOpen);
|
||||||
const levels = Array.from(
|
const levels = Array.from(
|
||||||
|
|
@ -191,6 +198,10 @@ const Toc = ({ headings, fixed, setFixed, isMarkdown }: TocProps) => {
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// 同时滚动 AceEditor
|
||||||
|
if (scrollToHeading) {
|
||||||
|
scrollToHeading(it.textContent);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 在富文本编辑器模式下,滚动整个窗口
|
// 在富文本编辑器模式下,滚动整个窗口
|
||||||
const offset = 100;
|
const offset = 100;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,13 @@ import Emoji from '@/components/Emoji';
|
||||||
import { postApiV1CreationTabComplete, putApiV1NodeDetail } from '@/request';
|
import { postApiV1CreationTabComplete, putApiV1NodeDetail } from '@/request';
|
||||||
import { V1NodeDetailResp } from '@/request/types';
|
import { V1NodeDetailResp } from '@/request/types';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import { TocList, useTiptap, UseTiptapReturn } from '@ctzhian/tiptap';
|
import {
|
||||||
|
EditorMarkdown,
|
||||||
|
MarkdownEditorRef,
|
||||||
|
TocList,
|
||||||
|
useTiptap,
|
||||||
|
UseTiptapReturn,
|
||||||
|
} from '@ctzhian/tiptap';
|
||||||
import { Icon, message } from '@ctzhian/ui';
|
import { Icon, message } from '@ctzhian/ui';
|
||||||
import { Box, Stack, TextField, Tooltip } from '@mui/material';
|
import { Box, Stack, TextField, Tooltip } from '@mui/material';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
@ -19,7 +25,6 @@ import { WrapContext } from '..';
|
||||||
import AIGenerate from './AIGenerate';
|
import AIGenerate from './AIGenerate';
|
||||||
import FullTextEditor from './FullTextEditor';
|
import FullTextEditor from './FullTextEditor';
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import MarkdownEditor from './MarkdownEditor';
|
|
||||||
import Summary from './Summary';
|
import Summary from './Summary';
|
||||||
import Toc from './Toc';
|
import Toc from './Toc';
|
||||||
import Toolbar from './Toolbar';
|
import Toolbar from './Toolbar';
|
||||||
|
|
@ -43,6 +48,8 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const markdownEditorRef = useRef<MarkdownEditorRef>(null);
|
||||||
|
|
||||||
const isMarkdown = useMemo(() => {
|
const isMarkdown = useMemo(() => {
|
||||||
return defaultDetail.meta?.content_type === 'md';
|
return defaultDetail.meta?.content_type === 'md';
|
||||||
}, [defaultDetail.meta?.content_type]);
|
}, [defaultDetail.meta?.content_type]);
|
||||||
|
|
@ -185,7 +192,7 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
|
||||||
editable: !isMarkdown,
|
editable: !isMarkdown,
|
||||||
contentType: isMarkdown ? 'markdown' : 'html',
|
contentType: isMarkdown ? 'markdown' : 'html',
|
||||||
immediatelyRender: true,
|
immediatelyRender: true,
|
||||||
content: defaultDetail.content || '',
|
content: defaultDetail.content,
|
||||||
exclude: ['invisibleCharacters', 'youtube', 'mention'],
|
exclude: ['invisibleCharacters', 'youtube', 'mention'],
|
||||||
onCreate: ({ editor: tiptapEditor }) => {
|
onCreate: ({ editor: tiptapEditor }) => {
|
||||||
const characterCount = (
|
const characterCount = (
|
||||||
|
|
@ -584,17 +591,27 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ ...(fixedToc && { display: 'flex' }) }}>
|
<Box sx={{ ...(fixedToc && { display: 'flex' }) }}>
|
||||||
{isMarkdown ? (
|
{isMarkdown ? (
|
||||||
<MarkdownEditor
|
<Box
|
||||||
editor={editorRef.editor}
|
sx={{
|
||||||
value={nodeDetail?.content || ''}
|
mt: '56px',
|
||||||
onChange={value => {
|
px: 10,
|
||||||
updateDetail({
|
pt: 4,
|
||||||
content: value,
|
flex: 1,
|
||||||
});
|
|
||||||
editorRef.setContent(value);
|
|
||||||
}}
|
}}
|
||||||
header={renderEditorTitleEmojiSummary()}
|
>
|
||||||
/>
|
<Box sx={{}}>{renderEditorTitleEmojiSummary()}</Box>
|
||||||
|
<EditorMarkdown
|
||||||
|
ref={markdownEditorRef}
|
||||||
|
editor={editorRef.editor}
|
||||||
|
value={nodeDetail?.content || ''}
|
||||||
|
onAceChange={value => {
|
||||||
|
updateDetail({
|
||||||
|
content: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
height='calc(100vh - 340px)'
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<FullTextEditor
|
<FullTextEditor
|
||||||
editor={editorRef.editor}
|
editor={editorRef.editor}
|
||||||
|
|
@ -609,6 +626,12 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
|
||||||
isMarkdown={isMarkdown}
|
isMarkdown={isMarkdown}
|
||||||
setFixed={setFixedToc}
|
setFixed={setFixedToc}
|
||||||
setShowSummary={setShowSummary}
|
setShowSummary={setShowSummary}
|
||||||
|
scrollToHeading={
|
||||||
|
isMarkdown
|
||||||
|
? headingText =>
|
||||||
|
markdownEditorRef.current?.scrollToHeading(headingText)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<AIGenerate
|
<AIGenerate
|
||||||
open={aiGenerateOpen}
|
open={aiGenerateOpen}
|
||||||
|
|
|
||||||
|
|
@ -376,6 +376,7 @@ export interface GithubComChaitinPandaWikiProApiContributeV1ContributeListResp {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GithubComChaitinPandaWikiProApiContributeV1NodeMeta {
|
export interface GithubComChaitinPandaWikiProApiContributeV1NodeMeta {
|
||||||
|
content_type?: string;
|
||||||
doc_width?: string;
|
doc_width?: string;
|
||||||
emoji?: string;
|
emoji?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -492,6 +493,7 @@ export type GithubComChaitinPandaWikiProApiShareV1OAuthCallbackResp = Record<
|
||||||
export interface GithubComChaitinPandaWikiProApiShareV1SubmitContributeReq {
|
export interface GithubComChaitinPandaWikiProApiShareV1SubmitContributeReq {
|
||||||
captcha_token: string;
|
captcha_token: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
|
content_type: "html" | "md";
|
||||||
emoji?: string;
|
emoji?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
node_id?: string;
|
node_id?: string;
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,25 @@ export enum ConstsSourceType {
|
||||||
SourceTypeOpenAIAPI = "openai_api",
|
SourceTypeOpenAIAPI = "openai_api",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ConstsNodeRagInfoStatus {
|
||||||
|
/** 等待基础处理 */
|
||||||
|
NodeRagStatusBasicPending = "BASIC_PENDING",
|
||||||
|
/** 正在进行基础处理(文本分割、向量化等) */
|
||||||
|
NodeRagStatusBasicRunning = "BASIC_RUNNING",
|
||||||
|
/** 基础处理失败 */
|
||||||
|
NodeRagStatusBasicFailed = "BASIC_FAILED",
|
||||||
|
/** 基础处理成功 */
|
||||||
|
NodeRagStatusBasicSucceeded = "BASIC_SUCCEEDED",
|
||||||
|
/** 基础处理完成,等待增强处理 */
|
||||||
|
NodeRagStatusEnhancePending = "ENHANCE_PENDING",
|
||||||
|
/** 正在进行增强处理(关键词提取等) */
|
||||||
|
NodeRagStatusEnhanceRunning = "ENHANCE_RUNNING",
|
||||||
|
/** 增强处理失败 */
|
||||||
|
NodeRagStatusEnhanceFailed = "ENHANCE_FAILED",
|
||||||
|
/** 增强处理成功 */
|
||||||
|
NodeRagStatusEnhanceSucceeded = "ENHANCE_SUCCEEDED",
|
||||||
|
}
|
||||||
|
|
||||||
export enum ConstsNodePermName {
|
export enum ConstsNodePermName {
|
||||||
/** 导航内可见 */
|
/** 导航内可见 */
|
||||||
NodePermNameVisible = "visible",
|
NodePermNameVisible = "visible",
|
||||||
|
|
@ -474,6 +493,16 @@ export interface DomainBatchMoveReq {
|
||||||
parent_id?: string;
|
parent_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DomainBlockGridConfig {
|
||||||
|
list?: {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
url?: string;
|
||||||
|
}[];
|
||||||
|
title?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DomainBrandGroup {
|
export interface DomainBrandGroup {
|
||||||
links?: DomainLink[];
|
links?: DomainLink[];
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
@ -937,6 +966,7 @@ export interface DomainNodeListItemResp {
|
||||||
parent_id?: string;
|
parent_id?: string;
|
||||||
permissions?: DomainNodePermissions;
|
permissions?: DomainNodePermissions;
|
||||||
position?: number;
|
position?: number;
|
||||||
|
rag_info?: DomainRagInfo;
|
||||||
status?: DomainNodeStatus;
|
status?: DomainNodeStatus;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
type?: DomainNodeType;
|
type?: DomainNodeType;
|
||||||
|
|
@ -1082,6 +1112,20 @@ export interface DomainProviderModelListItem {
|
||||||
model?: string;
|
model?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DomainQuestionConfig {
|
||||||
|
list?: {
|
||||||
|
id?: string;
|
||||||
|
question?: string;
|
||||||
|
}[];
|
||||||
|
title?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DomainRagInfo {
|
||||||
|
message?: string;
|
||||||
|
status?: ConstsNodeRagInfoStatus;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DomainRecommendNodeListResp {
|
export interface DomainRecommendNodeListResp {
|
||||||
emoji?: string;
|
emoji?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
@ -1241,6 +1285,7 @@ export interface DomainWebAppCustomSettings {
|
||||||
export interface DomainWebAppLandingConfig {
|
export interface DomainWebAppLandingConfig {
|
||||||
banner_config?: DomainBannerConfig;
|
banner_config?: DomainBannerConfig;
|
||||||
basic_doc_config?: DomainBasicDocConfig;
|
basic_doc_config?: DomainBasicDocConfig;
|
||||||
|
block_grid_config?: DomainBlockGridConfig;
|
||||||
carousel_config?: DomainCarouselConfig;
|
carousel_config?: DomainCarouselConfig;
|
||||||
case_config?: DomainCaseConfig;
|
case_config?: DomainCaseConfig;
|
||||||
com_config_order?: string[];
|
com_config_order?: string[];
|
||||||
|
|
@ -1251,6 +1296,7 @@ export interface DomainWebAppLandingConfig {
|
||||||
img_text_config?: DomainImgTextConfig;
|
img_text_config?: DomainImgTextConfig;
|
||||||
metrics_config?: DomainMetricsConfig;
|
metrics_config?: DomainMetricsConfig;
|
||||||
node_ids?: string[];
|
node_ids?: string[];
|
||||||
|
question_config?: DomainQuestionConfig;
|
||||||
simple_doc_config?: DomainSimpleDocConfig;
|
simple_doc_config?: DomainSimpleDocConfig;
|
||||||
text_config?: DomainTextConfig;
|
text_config?: DomainTextConfig;
|
||||||
text_img_config?: DomainTextImgConfig;
|
text_img_config?: DomainTextImgConfig;
|
||||||
|
|
@ -1260,6 +1306,7 @@ export interface DomainWebAppLandingConfig {
|
||||||
export interface DomainWebAppLandingConfigResp {
|
export interface DomainWebAppLandingConfigResp {
|
||||||
banner_config?: DomainBannerConfig;
|
banner_config?: DomainBannerConfig;
|
||||||
basic_doc_config?: DomainBasicDocConfig;
|
basic_doc_config?: DomainBasicDocConfig;
|
||||||
|
block_grid_config?: DomainBlockGridConfig;
|
||||||
carousel_config?: DomainCarouselConfig;
|
carousel_config?: DomainCarouselConfig;
|
||||||
case_config?: DomainCaseConfig;
|
case_config?: DomainCaseConfig;
|
||||||
com_config_order?: string[];
|
com_config_order?: string[];
|
||||||
|
|
@ -1271,6 +1318,7 @@ export interface DomainWebAppLandingConfigResp {
|
||||||
metrics_config?: DomainMetricsConfig;
|
metrics_config?: DomainMetricsConfig;
|
||||||
node_ids?: string[];
|
node_ids?: string[];
|
||||||
nodes?: DomainRecommendNodeListResp[];
|
nodes?: DomainRecommendNodeListResp[];
|
||||||
|
question_config?: DomainQuestionConfig;
|
||||||
simple_doc_config?: DomainSimpleDocConfig;
|
simple_doc_config?: DomainSimpleDocConfig;
|
||||||
text_config?: DomainTextConfig;
|
text_config?: DomainTextConfig;
|
||||||
text_img_config?: DomainTextImgConfig;
|
text_img_config?: DomainTextImgConfig;
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ const HomePage = () => {
|
||||||
cssVariables: {
|
cssVariables: {
|
||||||
cssVarPrefix: 'welcome',
|
cssVarPrefix: 'welcome',
|
||||||
},
|
},
|
||||||
palette: THEME_TO_PALETTE[themeMode].palette,
|
palette:
|
||||||
|
THEME_TO_PALETTE[themeMode]?.palette ||
|
||||||
|
THEME_TO_PALETTE['blue'].palette,
|
||||||
typography: {
|
typography: {
|
||||||
fontFamily: 'var(--font-gilory), PingFang SC, sans-serif',
|
fontFamily: 'var(--font-gilory), PingFang SC, sans-serif',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,61 @@
|
||||||
--inline-code-color: #ff502c;
|
--inline-code-color: #ff502c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md-dark .markdown-body {
|
||||||
|
/* 语法高亮颜色 - 暗色主题 */
|
||||||
|
--color-prettylights-syntax-comment: #8b949e;
|
||||||
|
--color-prettylights-syntax-constant: #79c0ff;
|
||||||
|
--color-prettylights-syntax-entity: #d2a8ff;
|
||||||
|
--color-prettylights-syntax-storage-modifier-import: #ff7b72;
|
||||||
|
--color-prettylights-syntax-entity-tag: #7ee787;
|
||||||
|
--color-prettylights-syntax-keyword: #ff7b72;
|
||||||
|
--color-prettylights-syntax-string: #a5d6ff;
|
||||||
|
--color-prettylights-syntax-variable: #ffa657;
|
||||||
|
--color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
|
||||||
|
--color-prettylights-syntax-invalid-illegal-text: #f0f6fc;
|
||||||
|
--color-prettylights-syntax-invalid-illegal-bg: #f85149;
|
||||||
|
--color-prettylights-syntax-carriage-return-text: #f0f6fc;
|
||||||
|
--color-prettylights-syntax-carriage-return-bg: #f85149;
|
||||||
|
--color-prettylights-syntax-string-regexp: #7ee787;
|
||||||
|
--color-prettylights-syntax-markup-list: #d2a8ff;
|
||||||
|
--color-prettylights-syntax-markup-heading: #1f6feb;
|
||||||
|
--color-prettylights-syntax-markup-italic: #ff7b72;
|
||||||
|
--color-prettylights-syntax-markup-bold: #ff7b72;
|
||||||
|
--color-prettylights-syntax-markup-deleted-text: #ffdcd7;
|
||||||
|
--color-prettylights-syntax-markup-deleted-bg: #67060c;
|
||||||
|
--color-prettylights-syntax-markup-inserted-text: #aff5b4;
|
||||||
|
--color-prettylights-syntax-markup-inserted-bg: #033a16;
|
||||||
|
--color-prettylights-syntax-markup-changed-text: #ffdfb6;
|
||||||
|
--color-prettylights-syntax-markup-changed-bg: #5a1e02;
|
||||||
|
--color-prettylights-syntax-markup-ignored-text: #c9d1d9;
|
||||||
|
--color-prettylights-syntax-markup-ignored-bg: #1158c7;
|
||||||
|
--color-prettylights-syntax-meta-diff-range: #d2a8ff;
|
||||||
|
--color-prettylights-syntax-brackethighlighter-angle: #8b949e;
|
||||||
|
--color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;
|
||||||
|
--color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
|
||||||
|
|
||||||
|
/* 基础颜色 - 暗色主题 */
|
||||||
|
--color-fg-default: #ffffff;
|
||||||
|
--color-fg-h: #ffffff;
|
||||||
|
--color-fg-muted: rgba(255, 255, 255, 0.7);
|
||||||
|
--color-fg-subtle: rgba(255, 255, 255, 0.5);
|
||||||
|
--color-canvas-default: #141923;
|
||||||
|
--color-canvas-subtle: #202531;
|
||||||
|
--color-border-default: #525770;
|
||||||
|
--color-border-muted: #525770;
|
||||||
|
--color-neutral-muted: rgba(110, 118, 129, 0.4);
|
||||||
|
--color-accent-fg: #6e73fe;
|
||||||
|
--color-accent-emphasis: #6e73fe;
|
||||||
|
--color-attention-subtle: rgba(187, 128, 9, 0.15);
|
||||||
|
--color-danger-fg: #f64e54;
|
||||||
|
--color-primary-main: #6e73fe;
|
||||||
|
|
||||||
|
/* 代码块颜色 - 暗色主题 */
|
||||||
|
--code-bg: #141923;
|
||||||
|
--code-color: #ffffff;
|
||||||
|
--inline-code-bg: rgba(255, 255, 255, 0.1);
|
||||||
|
--inline-code-color: #ff7b72;
|
||||||
|
}
|
||||||
/* 暗色主题变量 */
|
/* 暗色主题变量 */
|
||||||
.markdown-body.md-dark {
|
.markdown-body.md-dark {
|
||||||
/* 语法高亮颜色 - 暗色主题 */
|
/* 语法高亮颜色 - 暗色主题 */
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { message } from '@ctzhian/ui';
|
||||||
import Feedback from '@/components/feedback';
|
import Feedback from '@/components/feedback';
|
||||||
import { handleThinkingContent } from './utils';
|
import { handleThinkingContent } from './utils';
|
||||||
import { useSmartScroll } from '@/hooks';
|
import { useSmartScroll } from '@/hooks';
|
||||||
|
import { useTheme } from '@mui/material';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IconCai,
|
IconCai,
|
||||||
|
|
@ -107,7 +108,7 @@ const AiQaContent: React.FC<{
|
||||||
content: string;
|
content: string;
|
||||||
chunk_result: ChunkResultItem;
|
chunk_result: ChunkResultItem;
|
||||||
}> | null>(null);
|
}> | null>(null);
|
||||||
|
const { palette } = useTheme();
|
||||||
const messageIdRef = useRef('');
|
const messageIdRef = useRef('');
|
||||||
const [fullAnswer, setFullAnswer] = useState<string>('');
|
const [fullAnswer, setFullAnswer] = useState<string>('');
|
||||||
const [conversation, setConversation] = useState<ConversationItem[]>([]);
|
const [conversation, setConversation] = useState<ConversationItem[]>([]);
|
||||||
|
|
@ -661,7 +662,7 @@ const AiQaContent: React.FC<{
|
||||||
}, [conversation]);
|
}, [conversation]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledMainContainer>
|
<StyledMainContainer className={palette.mode === 'dark' ? 'md-dark' : ''}>
|
||||||
<StyledConversationContainer
|
<StyledConversationContainer
|
||||||
direction='column'
|
direction='column'
|
||||||
gap={2}
|
gap={2}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,8 @@ export const StyledAccordion = styled(Accordion)(({ theme }) => ({
|
||||||
content: '""',
|
content: '""',
|
||||||
height: 0,
|
height: 0,
|
||||||
},
|
},
|
||||||
backgroundColor: theme.palette.background.default,
|
background: 'transparent',
|
||||||
|
backgroundImage: 'none',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
|
export const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
|
||||||
|
|
@ -66,7 +67,8 @@ export const StyledQuestionText = styled(Box)(() => ({
|
||||||
|
|
||||||
// 搜索结果相关组件
|
// 搜索结果相关组件
|
||||||
export const StyledChunkAccordion = styled(Accordion)(({ theme }) => ({
|
export const StyledChunkAccordion = styled(Accordion)(({ theme }) => ({
|
||||||
backgroundColor: 'transparent',
|
backgroundImage: 'none',
|
||||||
|
background: 'transparent',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
padding: 0,
|
padding: 0,
|
||||||
paddingBottom: theme.spacing(2),
|
paddingBottom: theme.spacing(2),
|
||||||
|
|
@ -140,7 +142,7 @@ export const StyledThinkingAccordionDetails = styled(AccordionDetails)(
|
||||||
// 操作区域组件
|
// 操作区域组件
|
||||||
export const StyledActionStack = styled(Stack)(({ theme }) => ({
|
export const StyledActionStack = styled(Stack)(({ theme }) => ({
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: theme.palette.text.tertiary,
|
color: alpha(theme.palette.text.primary, 0.75),
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,15 @@ import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||||
import Logo from '@/assets/images/logo.png';
|
import Logo from '@/assets/images/logo.png';
|
||||||
import { IconZhinengwenda, IconJinsousuo } from '@panda-wiki/icons';
|
import { IconZhinengwenda, IconJinsousuo } from '@panda-wiki/icons';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { Box, Button, Typography, Modal, Stack, lighten } from '@mui/material';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
Modal,
|
||||||
|
Stack,
|
||||||
|
lighten,
|
||||||
|
alpha,
|
||||||
|
} from '@mui/material';
|
||||||
import { CusTabs } from '@ctzhian/ui';
|
import { CusTabs } from '@ctzhian/ui';
|
||||||
import AiQaContent from './AiQaContent';
|
import AiQaContent from './AiQaContent';
|
||||||
import SearchDocContent from './SearchDocContent';
|
import SearchDocContent from './SearchDocContent';
|
||||||
|
|
@ -159,19 +167,14 @@ const QaModal: React.FC<QaModalProps> = () => {
|
||||||
<Button
|
<Button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
size='small'
|
size='small'
|
||||||
sx={{
|
sx={theme => ({
|
||||||
minWidth: 'auto',
|
minWidth: 'auto',
|
||||||
px: 1.5,
|
px: 1.5,
|
||||||
py: 0.5,
|
py: 0.5,
|
||||||
bgcolor: 'background.paper3',
|
|
||||||
color: 'text.tertiary',
|
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
'&:hover': {
|
})}
|
||||||
bgcolor: 'grey.200',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Esc
|
Esc
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,86 +1,128 @@
|
||||||
'use client';
|
'use client';
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { useStore } from '@/provider';
|
import { useStore } from '@/provider';
|
||||||
import { Stack, Tooltip, Fab, Zoom } from '@mui/material';
|
import { Modal } from '@ctzhian/ui';
|
||||||
import { usePathname, useParams } from 'next/navigation';
|
|
||||||
import MenuIcon from '@mui/icons-material/Menu';
|
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
|
import {
|
||||||
|
Fab,
|
||||||
|
FormControlLabel,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
Stack,
|
||||||
|
Tooltip,
|
||||||
|
Zoom,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useParams, usePathname } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
const DocFab = () => {
|
const DocFab = () => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { id: docId } = useParams() || {};
|
const { id: docId } = useParams() || {};
|
||||||
const { kbDetail, mobile } = useStore();
|
const { kbDetail, mobile } = useStore();
|
||||||
const [showActions, setShowActions] = useState(false);
|
const [showActions, setShowActions] = useState(false);
|
||||||
|
const [contentType, setContentType] = useState<'html' | 'md'>('html');
|
||||||
|
const [openSelectContentTypeModal, setOpenSelectContentTypeModal] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
if (mobile) return null;
|
if (mobile) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<>
|
||||||
gap={1}
|
<Modal
|
||||||
sx={{
|
title='新建文档类型'
|
||||||
position: 'fixed',
|
open={openSelectContentTypeModal}
|
||||||
bottom: 70,
|
onCancel={() => {
|
||||||
right: 16,
|
setOpenSelectContentTypeModal(false);
|
||||||
zIndex: 10000,
|
setContentType('html');
|
||||||
}}
|
}}
|
||||||
onMouseLeave={() => setShowActions(false)}
|
onOk={() => {
|
||||||
>
|
setOpenSelectContentTypeModal(false);
|
||||||
{kbDetail?.settings.contribute_settings?.is_enable && (
|
window.open(`/editor?contentType=${contentType}`, '_blank');
|
||||||
<>
|
}}
|
||||||
<Zoom
|
>
|
||||||
in={showActions}
|
<RadioGroup
|
||||||
style={{ transitionDelay: showActions ? '100ms' : '0ms' }}
|
value={contentType}
|
||||||
>
|
onChange={e => setContentType(e.target.value as 'html' | 'md')}
|
||||||
<Tooltip title='创建文档' placement='left' arrow>
|
>
|
||||||
<Fab
|
<FormControlLabel
|
||||||
color='primary'
|
value='html'
|
||||||
size='small'
|
control={<Radio size='small' />}
|
||||||
onClick={() => {
|
label='富文本'
|
||||||
window.open(`/editor`, '_blank');
|
/>
|
||||||
}}
|
<FormControlLabel
|
||||||
>
|
value='md'
|
||||||
<AddIcon />
|
control={<Radio size='small' />}
|
||||||
</Fab>
|
label='Markdown'
|
||||||
</Tooltip>
|
/>
|
||||||
</Zoom>
|
</RadioGroup>
|
||||||
{pathname.startsWith('/node/') && (
|
</Modal>
|
||||||
|
<Stack
|
||||||
|
gap={1}
|
||||||
|
sx={{
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: 70,
|
||||||
|
right: 16,
|
||||||
|
zIndex: 10000,
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => setShowActions(false)}
|
||||||
|
>
|
||||||
|
{kbDetail?.settings.contribute_settings?.is_enable && (
|
||||||
|
<>
|
||||||
<Zoom
|
<Zoom
|
||||||
in={showActions}
|
in={showActions}
|
||||||
style={{ transitionDelay: showActions ? '40ms' : '0ms' }}
|
style={{ transitionDelay: showActions ? '100ms' : '0ms' }}
|
||||||
>
|
>
|
||||||
<Tooltip title='编辑文档' placement='left' arrow>
|
<Tooltip title='创建文档' placement='left' arrow>
|
||||||
<Fab
|
<Fab
|
||||||
color='primary'
|
color='primary'
|
||||||
size='small'
|
size='small'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.open(`/editor/${docId}`, '_blank');
|
setOpenSelectContentTypeModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EditIcon />
|
<AddIcon />
|
||||||
</Fab>
|
</Fab>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Zoom>
|
</Zoom>
|
||||||
)}
|
{pathname.startsWith('/node/') && (
|
||||||
<Fab
|
<Zoom
|
||||||
size='small'
|
in={showActions}
|
||||||
sx={{
|
style={{ transitionDelay: showActions ? '40ms' : '0ms' }}
|
||||||
backgroundColor: 'background.paper2',
|
>
|
||||||
color: 'text.secondary',
|
<Tooltip title='编辑文档' placement='left' arrow>
|
||||||
'&:hover': { backgroundColor: 'background.paper2' },
|
<Fab
|
||||||
}}
|
color='primary'
|
||||||
onMouseEnter={() => setShowActions(true)}
|
size='small'
|
||||||
>
|
onClick={() => {
|
||||||
<MenuIcon
|
window.open(`/editor/${docId}`, '_blank');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</Fab>
|
||||||
|
</Tooltip>
|
||||||
|
</Zoom>
|
||||||
|
)}
|
||||||
|
<Fab
|
||||||
|
size='small'
|
||||||
sx={{
|
sx={{
|
||||||
transition: 'transform 200ms',
|
backgroundColor: 'background.paper2',
|
||||||
transform: showActions ? 'rotate(90deg)' : 'rotate(0deg)',
|
color: 'text.secondary',
|
||||||
|
'&:hover': { backgroundColor: 'background.paper2' },
|
||||||
}}
|
}}
|
||||||
/>
|
onMouseEnter={() => setShowActions(true)}
|
||||||
</Fab>
|
>
|
||||||
</>
|
<MenuIcon
|
||||||
)}
|
sx={{
|
||||||
</Stack>
|
transition: 'transform 200ms',
|
||||||
|
transform: showActions ? 'rotate(90deg)' : 'rotate(0deg)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Fab>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -376,6 +376,7 @@ export interface GithubComChaitinPandaWikiProApiContributeV1ContributeListResp {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GithubComChaitinPandaWikiProApiContributeV1NodeMeta {
|
export interface GithubComChaitinPandaWikiProApiContributeV1NodeMeta {
|
||||||
|
content_type?: string;
|
||||||
doc_width?: string;
|
doc_width?: string;
|
||||||
emoji?: string;
|
emoji?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -492,6 +493,7 @@ export type GithubComChaitinPandaWikiProApiShareV1OAuthCallbackResp = Record<
|
||||||
export interface GithubComChaitinPandaWikiProApiShareV1SubmitContributeReq {
|
export interface GithubComChaitinPandaWikiProApiShareV1SubmitContributeReq {
|
||||||
captcha_token: string;
|
captcha_token: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
|
content_type: "html" | "md";
|
||||||
emoji?: string;
|
emoji?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
node_id?: string;
|
node_id?: string;
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,25 @@ export enum ConstsSourceType {
|
||||||
SourceTypeOpenAIAPI = "openai_api",
|
SourceTypeOpenAIAPI = "openai_api",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ConstsNodeRagInfoStatus {
|
||||||
|
/** 等待基础处理 */
|
||||||
|
NodeRagStatusBasicPending = "BASIC_PENDING",
|
||||||
|
/** 正在进行基础处理(文本分割、向量化等) */
|
||||||
|
NodeRagStatusBasicRunning = "BASIC_RUNNING",
|
||||||
|
/** 基础处理失败 */
|
||||||
|
NodeRagStatusBasicFailed = "BASIC_FAILED",
|
||||||
|
/** 基础处理成功 */
|
||||||
|
NodeRagStatusBasicSucceeded = "BASIC_SUCCEEDED",
|
||||||
|
/** 基础处理完成,等待增强处理 */
|
||||||
|
NodeRagStatusEnhancePending = "ENHANCE_PENDING",
|
||||||
|
/** 正在进行增强处理(关键词提取等) */
|
||||||
|
NodeRagStatusEnhanceRunning = "ENHANCE_RUNNING",
|
||||||
|
/** 增强处理失败 */
|
||||||
|
NodeRagStatusEnhanceFailed = "ENHANCE_FAILED",
|
||||||
|
/** 增强处理成功 */
|
||||||
|
NodeRagStatusEnhanceSucceeded = "ENHANCE_SUCCEEDED",
|
||||||
|
}
|
||||||
|
|
||||||
export enum ConstsNodePermName {
|
export enum ConstsNodePermName {
|
||||||
/** 导航内可见 */
|
/** 导航内可见 */
|
||||||
NodePermNameVisible = "visible",
|
NodePermNameVisible = "visible",
|
||||||
|
|
@ -474,6 +493,16 @@ export interface DomainBatchMoveReq {
|
||||||
parent_id?: string;
|
parent_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DomainBlockGridConfig {
|
||||||
|
list?: {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
url?: string;
|
||||||
|
}[];
|
||||||
|
title?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DomainBrandGroup {
|
export interface DomainBrandGroup {
|
||||||
links?: DomainLink[];
|
links?: DomainLink[];
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
@ -937,6 +966,7 @@ export interface DomainNodeListItemResp {
|
||||||
parent_id?: string;
|
parent_id?: string;
|
||||||
permissions?: DomainNodePermissions;
|
permissions?: DomainNodePermissions;
|
||||||
position?: number;
|
position?: number;
|
||||||
|
rag_info?: DomainRagInfo;
|
||||||
status?: DomainNodeStatus;
|
status?: DomainNodeStatus;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
type?: DomainNodeType;
|
type?: DomainNodeType;
|
||||||
|
|
@ -1082,6 +1112,20 @@ export interface DomainProviderModelListItem {
|
||||||
model?: string;
|
model?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DomainQuestionConfig {
|
||||||
|
list?: {
|
||||||
|
id?: string;
|
||||||
|
question?: string;
|
||||||
|
}[];
|
||||||
|
title?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DomainRagInfo {
|
||||||
|
message?: string;
|
||||||
|
status?: ConstsNodeRagInfoStatus;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DomainRecommendNodeListResp {
|
export interface DomainRecommendNodeListResp {
|
||||||
emoji?: string;
|
emoji?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
@ -1241,6 +1285,7 @@ export interface DomainWebAppCustomSettings {
|
||||||
export interface DomainWebAppLandingConfig {
|
export interface DomainWebAppLandingConfig {
|
||||||
banner_config?: DomainBannerConfig;
|
banner_config?: DomainBannerConfig;
|
||||||
basic_doc_config?: DomainBasicDocConfig;
|
basic_doc_config?: DomainBasicDocConfig;
|
||||||
|
block_grid_config?: DomainBlockGridConfig;
|
||||||
carousel_config?: DomainCarouselConfig;
|
carousel_config?: DomainCarouselConfig;
|
||||||
case_config?: DomainCaseConfig;
|
case_config?: DomainCaseConfig;
|
||||||
com_config_order?: string[];
|
com_config_order?: string[];
|
||||||
|
|
@ -1251,6 +1296,7 @@ export interface DomainWebAppLandingConfig {
|
||||||
img_text_config?: DomainImgTextConfig;
|
img_text_config?: DomainImgTextConfig;
|
||||||
metrics_config?: DomainMetricsConfig;
|
metrics_config?: DomainMetricsConfig;
|
||||||
node_ids?: string[];
|
node_ids?: string[];
|
||||||
|
question_config?: DomainQuestionConfig;
|
||||||
simple_doc_config?: DomainSimpleDocConfig;
|
simple_doc_config?: DomainSimpleDocConfig;
|
||||||
text_config?: DomainTextConfig;
|
text_config?: DomainTextConfig;
|
||||||
text_img_config?: DomainTextImgConfig;
|
text_img_config?: DomainTextImgConfig;
|
||||||
|
|
@ -1260,6 +1306,7 @@ export interface DomainWebAppLandingConfig {
|
||||||
export interface DomainWebAppLandingConfigResp {
|
export interface DomainWebAppLandingConfigResp {
|
||||||
banner_config?: DomainBannerConfig;
|
banner_config?: DomainBannerConfig;
|
||||||
basic_doc_config?: DomainBasicDocConfig;
|
basic_doc_config?: DomainBasicDocConfig;
|
||||||
|
block_grid_config?: DomainBlockGridConfig;
|
||||||
carousel_config?: DomainCarouselConfig;
|
carousel_config?: DomainCarouselConfig;
|
||||||
case_config?: DomainCaseConfig;
|
case_config?: DomainCaseConfig;
|
||||||
com_config_order?: string[];
|
com_config_order?: string[];
|
||||||
|
|
@ -1271,6 +1318,7 @@ export interface DomainWebAppLandingConfigResp {
|
||||||
metrics_config?: DomainMetricsConfig;
|
metrics_config?: DomainMetricsConfig;
|
||||||
node_ids?: string[];
|
node_ids?: string[];
|
||||||
nodes?: DomainRecommendNodeListResp[];
|
nodes?: DomainRecommendNodeListResp[];
|
||||||
|
question_config?: DomainQuestionConfig;
|
||||||
simple_doc_config?: DomainSimpleDocConfig;
|
simple_doc_config?: DomainSimpleDocConfig;
|
||||||
text_config?: DomainTextConfig;
|
text_config?: DomainTextConfig;
|
||||||
text_img_config?: DomainTextImgConfig;
|
text_img_config?: DomainTextImgConfig;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { Box, Drawer, IconButton, Stack } from '@mui/material';
|
|
||||||
import {
|
import {
|
||||||
H1Icon,
|
H1Icon,
|
||||||
H2Icon,
|
H2Icon,
|
||||||
|
|
@ -9,12 +8,15 @@ import {
|
||||||
TocList,
|
TocList,
|
||||||
} from '@ctzhian/tiptap';
|
} from '@ctzhian/tiptap';
|
||||||
import { Ellipsis, Icon } from '@ctzhian/ui';
|
import { Ellipsis, Icon } from '@ctzhian/ui';
|
||||||
|
import { Box, Drawer, IconButton, Stack } from '@mui/material';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
interface TocProps {
|
interface TocProps {
|
||||||
headings: TocList;
|
headings: TocList;
|
||||||
fixed: boolean;
|
fixed: boolean;
|
||||||
setFixed: (fixed: boolean) => void;
|
setFixed: (fixed: boolean) => void;
|
||||||
|
isMarkdown: boolean;
|
||||||
|
scrollToHeading?: (headingText: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeadingIcon = [
|
const HeadingIcon = [
|
||||||
|
|
@ -32,7 +34,13 @@ const HeadingSx = [
|
||||||
{ fontSize: 14, fontWeight: 400, color: 'text.disabled' },
|
{ fontSize: 14, fontWeight: 400, color: 'text.disabled' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const Toc = ({ headings, fixed, setFixed }: TocProps) => {
|
const Toc = ({
|
||||||
|
headings,
|
||||||
|
fixed,
|
||||||
|
setFixed,
|
||||||
|
scrollToHeading,
|
||||||
|
isMarkdown,
|
||||||
|
}: TocProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const levels = Array.from(
|
const levels = Array.from(
|
||||||
new Set(headings.map(it => it.level).sort((a, b) => a - b)),
|
new Set(headings.map(it => it.level).sort((a, b) => a - b)),
|
||||||
|
|
@ -164,15 +172,40 @@ const Toc = ({ headings, fixed, setFixed }: TocProps) => {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const element = document.getElementById(it.id);
|
const element = document.getElementById(it.id);
|
||||||
if (element) {
|
if (element) {
|
||||||
const offset = 100;
|
if (isMarkdown) {
|
||||||
const elementPosition =
|
const container = document.getElementById(
|
||||||
element.getBoundingClientRect().top;
|
'markdown-preview-container',
|
||||||
const offsetPosition =
|
);
|
||||||
elementPosition + window.pageYOffset - offset;
|
if (container) {
|
||||||
window.scrollTo({
|
const containerRect =
|
||||||
top: offsetPosition,
|
container.getBoundingClientRect();
|
||||||
behavior: 'smooth',
|
const elementRect = element.getBoundingClientRect();
|
||||||
});
|
const offset = 20; // 顶部偏移
|
||||||
|
const scrollTop =
|
||||||
|
container.scrollTop +
|
||||||
|
elementRect.top -
|
||||||
|
containerRect.top -
|
||||||
|
offset;
|
||||||
|
container.scrollTo({
|
||||||
|
top: scrollTop,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 同时滚动 AceEditor
|
||||||
|
if (scrollToHeading) {
|
||||||
|
scrollToHeading(it.textContent);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const offset = 100;
|
||||||
|
const elementPosition =
|
||||||
|
element.getBoundingClientRect().top;
|
||||||
|
const offsetPosition =
|
||||||
|
elementPosition + window.pageYOffset - offset;
|
||||||
|
window.scrollTo({
|
||||||
|
top: offsetPosition,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,20 @@
|
||||||
import Emoji from '@/components/emoji';
|
import Emoji from '@/components/emoji';
|
||||||
import { postShareProV1FileUploadWithProgress } from '@/request/pro/otherCustomer';
|
import { postShareProV1FileUploadWithProgress } from '@/request/pro/otherCustomer';
|
||||||
import { V1NodeDetailResp } from '@/request/types';
|
import { V1NodeDetailResp } from '@/request/types';
|
||||||
import { Editor, TocList, useTiptap, UseTiptapReturn } from '@ctzhian/tiptap';
|
import {
|
||||||
|
Editor,
|
||||||
|
EditorMarkdown,
|
||||||
|
MarkdownEditorRef,
|
||||||
|
TocList,
|
||||||
|
useTiptap,
|
||||||
|
UseTiptapReturn,
|
||||||
|
} from '@ctzhian/tiptap';
|
||||||
import { message } from '@ctzhian/ui';
|
import { message } from '@ctzhian/ui';
|
||||||
import { Box, Stack, TextField } from '@mui/material';
|
import { Box, Stack, TextField } from '@mui/material';
|
||||||
import { IconAShijian2, IconZiti } from '@panda-wiki/icons';
|
import { IconAShijian2, IconZiti } from '@panda-wiki/icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams, useSearchParams } from 'next/navigation';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useWrapContext } from '..';
|
import { useWrapContext } from '..';
|
||||||
import AIGenerate from './AIGenerate';
|
import AIGenerate from './AIGenerate';
|
||||||
import ConfirmModal from './ConfirmModal';
|
import ConfirmModal from './ConfirmModal';
|
||||||
|
|
@ -21,9 +28,12 @@ interface WrapProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
|
const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
|
||||||
const { catalogOpen, nodeDetail, setNodeDetail, onSave } = useWrapContext();
|
const searchParams = useSearchParams();
|
||||||
|
const contentType = searchParams.get('contentType') || 'html';
|
||||||
|
const { nodeDetail, setNodeDetail, onSave } = useWrapContext();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
|
const markdownEditorRef = useRef<MarkdownEditorRef>(null);
|
||||||
const [characterCount, setCharacterCount] = useState(0);
|
const [characterCount, setCharacterCount] = useState(0);
|
||||||
const [headings, setHeadings] = useState<TocList>([]);
|
const [headings, setHeadings] = useState<TocList>([]);
|
||||||
const [fixedToc, setFixedToc] = useState(false);
|
const [fixedToc, setFixedToc] = useState(false);
|
||||||
|
|
@ -31,6 +41,14 @@ const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
|
||||||
const [aiGenerateOpen, setAiGenerateOpen] = useState(false);
|
const [aiGenerateOpen, setAiGenerateOpen] = useState(false);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
|
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const isMarkdown = useMemo(() => {
|
||||||
|
if (!id && contentType === 'md') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return defaultDetail.meta?.content_type === 'md';
|
||||||
|
}, [defaultDetail.meta?.content_type, contentType]);
|
||||||
|
|
||||||
const updateDetail = (value: V1NodeDetailResp) => {
|
const updateDetail = (value: V1NodeDetailResp) => {
|
||||||
setNodeDetail({
|
setNodeDetail({
|
||||||
...nodeDetail,
|
...nodeDetail,
|
||||||
|
|
@ -73,8 +91,9 @@ const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const editorRef = useTiptap({
|
const editorRef = useTiptap({
|
||||||
editable: true,
|
|
||||||
immediatelyRender: false,
|
immediatelyRender: false,
|
||||||
|
editable: !isMarkdown,
|
||||||
|
contentType: isMarkdown ? 'markdown' : 'html',
|
||||||
content: defaultDetail?.content || '',
|
content: defaultDetail?.content || '',
|
||||||
exclude: ['invisibleCharacters', 'youtube', 'mention'],
|
exclude: ['invisibleCharacters', 'youtube', 'mention'],
|
||||||
onCreate: ({ editor: tiptapEditor }) => {
|
onCreate: ({ editor: tiptapEditor }) => {
|
||||||
|
|
@ -106,17 +125,33 @@ const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
|
||||||
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
|
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (editorRef && editorRef.editor) {
|
if (editorRef && editorRef.editor) {
|
||||||
const html = editorRef.getHTML();
|
const value = editorRef.getContent();
|
||||||
updateDetail({
|
updateDetail({
|
||||||
content: html,
|
content: value,
|
||||||
});
|
});
|
||||||
setConfirmModalOpen(true);
|
setTimeout(() => {
|
||||||
|
if (checkRequiredFields()) {
|
||||||
|
setConfirmModalOpen(true);
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[editorRef, onSave],
|
[editorRef],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const checkRequiredFields = useCallback(() => {
|
||||||
|
if (!nodeDetail?.name?.trim()) {
|
||||||
|
message.error('请先输入文档名称');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!nodeDetail?.content?.trim()) {
|
||||||
|
message.error('请先输入文档内容');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}, [nodeDetail]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener('keydown', handleGlobalSave);
|
document.addEventListener('keydown', handleGlobalSave);
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -148,10 +183,14 @@ const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
|
||||||
detail={nodeDetail!}
|
detail={nodeDetail!}
|
||||||
updateDetail={updateDetail}
|
updateDetail={updateDetail}
|
||||||
handleSave={async () => {
|
handleSave={async () => {
|
||||||
setConfirmModalOpen(true);
|
if (checkRequiredFields()) {
|
||||||
|
setConfirmModalOpen(true);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Toolbar editorRef={editorRef} handleAiGenerate={handleAiGenerate} />
|
{!isMarkdown && (
|
||||||
|
<Toolbar editorRef={editorRef} handleAiGenerate={handleAiGenerate} />
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -163,8 +202,8 @@ const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: `calc(100vw - 160px - ${fixedToc ? 292 : 0}px)`,
|
width: `calc(100vw - 160px - ${fixedToc ? 292 : 0}px)`,
|
||||||
p: '72px 80px 150px',
|
p: isMarkdown ? '72px 80px' : '72px 80px 150px',
|
||||||
mt: '102px',
|
mt: isMarkdown ? '56px' : '102px',
|
||||||
mx: 'auto',
|
mx: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -253,24 +292,52 @@ const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
|
||||||
{characterCount} 字
|
{characterCount} 字
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
{editorRef.editor && (
|
||||||
<Box
|
<Box sx={{ ...(fixedToc && { display: 'flex' }) }}>
|
||||||
sx={{
|
{isMarkdown ? (
|
||||||
wordBreak: 'break-all',
|
<EditorMarkdown
|
||||||
'.tiptap.ProseMirror': {
|
ref={markdownEditorRef}
|
||||||
overflowX: 'hidden',
|
editor={editorRef.editor}
|
||||||
minHeight: 'calc(100vh - 102px - 48px)',
|
value={nodeDetail?.content || defaultDetail?.content || ''}
|
||||||
},
|
onAceChange={value => {
|
||||||
'.tableWrapper': {
|
updateDetail({
|
||||||
width: '100%',
|
content: value,
|
||||||
overflowX: 'auto',
|
});
|
||||||
},
|
}}
|
||||||
}}
|
height='calc(100vh - 340px)'
|
||||||
>
|
/>
|
||||||
{editorRef.editor && <Editor editor={editorRef.editor} />}
|
) : (
|
||||||
</Box>
|
<Box
|
||||||
|
sx={{
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
'.tiptap.ProseMirror': {
|
||||||
|
overflowX: 'hidden',
|
||||||
|
minHeight: 'calc(100vh - 102px - 48px)',
|
||||||
|
},
|
||||||
|
'.tableWrapper': {
|
||||||
|
width: '100%',
|
||||||
|
overflowX: 'auto',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Editor editor={editorRef.editor} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Toc headings={headings} fixed={fixedToc} setFixed={setFixedToc} />
|
<Toc
|
||||||
|
headings={headings}
|
||||||
|
fixed={fixedToc}
|
||||||
|
setFixed={setFixedToc}
|
||||||
|
isMarkdown={isMarkdown}
|
||||||
|
scrollToHeading={
|
||||||
|
isMarkdown
|
||||||
|
? headingText =>
|
||||||
|
markdownEditorRef.current?.scrollToHeading(headingText)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<AIGenerate
|
<AIGenerate
|
||||||
|
|
@ -284,11 +351,11 @@ const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
|
||||||
open={confirmModalOpen}
|
open={confirmModalOpen}
|
||||||
onCancel={() => setConfirmModalOpen(false)}
|
onCancel={() => setConfirmModalOpen(false)}
|
||||||
onOk={async (reason: string, token: string) => {
|
onOk={async (reason: string, token: string) => {
|
||||||
const value = editorRef.getHTML();
|
const value = editorRef.getContent();
|
||||||
updateDetail({
|
updateDetail({
|
||||||
content: value,
|
content: value,
|
||||||
});
|
});
|
||||||
await onSave(value, reason, token);
|
await onSave(value, reason, token, isMarkdown ? 'md' : 'html');
|
||||||
setConfirmModalOpen(false);
|
setConfirmModalOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { createContext, useContext } from 'react';
|
|
||||||
import { postShareProV1ContributeSubmit } from '@/request/pro/ShareContribute';
|
import { postShareProV1ContributeSubmit } from '@/request/pro/ShareContribute';
|
||||||
import { V1NodeDetailResp } from '@/request/types';
|
import { V1NodeDetailResp } from '@/request/types';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
|
||||||
import { Box, Stack, useMediaQuery } from '@mui/material';
|
|
||||||
import { message } from '@ctzhian/ui';
|
import { message } from '@ctzhian/ui';
|
||||||
import { useEffect, useState } from 'react';
|
import { Box, Stack, useMediaQuery } from '@mui/material';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import { createContext, useContext, useEffect, useState } from 'react';
|
||||||
import Edit from './edit';
|
import Edit from './edit';
|
||||||
|
|
||||||
export interface WrapContext {
|
export interface WrapContext {
|
||||||
|
|
@ -13,7 +12,12 @@ export interface WrapContext {
|
||||||
setCatalogOpen: (open: boolean) => void;
|
setCatalogOpen: (open: boolean) => void;
|
||||||
nodeDetail: V1NodeDetailResp | null;
|
nodeDetail: V1NodeDetailResp | null;
|
||||||
setNodeDetail: (detail: V1NodeDetailResp) => void;
|
setNodeDetail: (detail: V1NodeDetailResp) => void;
|
||||||
onSave: (content: string, reason: string, token: string) => void;
|
onSave: (
|
||||||
|
content: string,
|
||||||
|
reason: string,
|
||||||
|
token: string,
|
||||||
|
contentType?: 'html' | 'md',
|
||||||
|
) => void;
|
||||||
saveLoading: boolean;
|
saveLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +45,12 @@ const DocEditor = () => {
|
||||||
);
|
);
|
||||||
const [catalogOpen, setCatalogOpen] = useState(true);
|
const [catalogOpen, setCatalogOpen] = useState(true);
|
||||||
|
|
||||||
const onSave = (content: string, reason: string, token: string) => {
|
const onSave = (
|
||||||
|
content: string,
|
||||||
|
reason: string,
|
||||||
|
token: string,
|
||||||
|
contentType?: 'html' | 'md',
|
||||||
|
) => {
|
||||||
setSaveLoading(true);
|
setSaveLoading(true);
|
||||||
return postShareProV1ContributeSubmit({
|
return postShareProV1ContributeSubmit({
|
||||||
node_id: id ? id[0] : undefined,
|
node_id: id ? id[0] : undefined,
|
||||||
|
|
@ -51,6 +60,7 @@ const DocEditor = () => {
|
||||||
reason,
|
reason,
|
||||||
emoji: nodeDetail?.meta?.emoji,
|
emoji: nodeDetail?.meta?.emoji,
|
||||||
captcha_token: token,
|
captcha_token: token,
|
||||||
|
content_type: contentType || 'html',
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
message.success('保存成功, 即将关闭页面');
|
message.success('保存成功, 即将关闭页面');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,11 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { Banner } from '@panda-wiki/ui';
|
||||||
Banner,
|
import dynamic from 'next/dynamic';
|
||||||
Faq,
|
|
||||||
BasicDoc,
|
|
||||||
DirDoc,
|
|
||||||
SimpleDoc,
|
|
||||||
Carousel,
|
|
||||||
Text,
|
|
||||||
Case,
|
|
||||||
Metrics,
|
|
||||||
Feature,
|
|
||||||
ImgText,
|
|
||||||
Comment,
|
|
||||||
} from '@panda-wiki/ui';
|
|
||||||
import { DomainRecommendNodeListResp } from '@/request/types';
|
import { DomainRecommendNodeListResp } from '@/request/types';
|
||||||
|
|
||||||
import { useStore } from '@/provider';
|
import { useStore } from '@/provider';
|
||||||
|
|
||||||
const handleHeaderProps = (setting: any) => {
|
|
||||||
return {
|
|
||||||
title: setting.title,
|
|
||||||
logo: setting.icon,
|
|
||||||
btns: setting.btns,
|
|
||||||
placeholder:
|
|
||||||
setting.web_app_custom_style?.header_search_placeholder || '搜索...',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFooterProps = (setting: any) => {
|
|
||||||
return {
|
|
||||||
footerSetting: setting.footer_settings,
|
|
||||||
logo: setting.icon,
|
|
||||||
showBrand: setting.web_app_custom_style?.show_brand_info || false,
|
|
||||||
customStyle: setting.web_app_custom_style,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFaqProps = (config: any = {}) => {
|
const handleFaqProps = (config: any = {}) => {
|
||||||
return {
|
return {
|
||||||
title: config.title || '链接组',
|
title: config.title || '链接组',
|
||||||
|
|
@ -173,20 +142,40 @@ const handleCommentProps = (config: any = {}) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBlockGridProps = (config: any = {}) => {
|
||||||
|
return {
|
||||||
|
title: config.title || '区块网格',
|
||||||
|
items: config.list || [],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuestionProps = (config: any = {}) => {
|
||||||
|
return {
|
||||||
|
title: config.title || '常见问题',
|
||||||
|
items: config.list || [],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
banner: Banner,
|
banner: Banner,
|
||||||
basic_doc: BasicDoc,
|
basic_doc: dynamic(() => import('@panda-wiki/ui').then(mod => mod.BasicDoc)),
|
||||||
dir_doc: DirDoc,
|
dir_doc: dynamic(() => import('@panda-wiki/ui').then(mod => mod.DirDoc)),
|
||||||
simple_doc: SimpleDoc,
|
simple_doc: dynamic(() =>
|
||||||
carousel: Carousel,
|
import('@panda-wiki/ui').then(mod => mod.SimpleDoc),
|
||||||
faq: Faq,
|
),
|
||||||
text: Text,
|
carousel: dynamic(() => import('@panda-wiki/ui').then(mod => mod.Carousel)),
|
||||||
case: Case,
|
faq: dynamic(() => import('@panda-wiki/ui').then(mod => mod.Faq)),
|
||||||
metrics: Metrics,
|
text: dynamic(() => import('@panda-wiki/ui').then(mod => mod.Text)),
|
||||||
feature: Feature,
|
case: dynamic(() => import('@panda-wiki/ui').then(mod => mod.Case)),
|
||||||
text_img: ImgText,
|
metrics: dynamic(() => import('@panda-wiki/ui').then(mod => mod.Metrics)),
|
||||||
img_text: ImgText,
|
feature: dynamic(() => import('@panda-wiki/ui').then(mod => mod.Feature)),
|
||||||
comment: Comment,
|
text_img: dynamic(() => import('@panda-wiki/ui').then(mod => mod.ImgText)),
|
||||||
|
img_text: dynamic(() => import('@panda-wiki/ui').then(mod => mod.ImgText)),
|
||||||
|
comment: dynamic(() => import('@panda-wiki/ui').then(mod => mod.Comment)),
|
||||||
|
block_grid: dynamic(() =>
|
||||||
|
import('@panda-wiki/ui').then(mod => mod.BlockGrid),
|
||||||
|
),
|
||||||
|
question: dynamic(() => import('@panda-wiki/ui').then(mod => mod.Question)),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const Welcome = () => {
|
const Welcome = () => {
|
||||||
|
|
@ -220,6 +209,8 @@ const Welcome = () => {
|
||||||
text_img: 'text_img_config',
|
text_img: 'text_img_config',
|
||||||
img_text: 'img_text_config',
|
img_text: 'img_text_config',
|
||||||
comment: 'comment_config',
|
comment: 'comment_config',
|
||||||
|
block_grid: 'block_grid_config',
|
||||||
|
question: 'question_config',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const handleComponentProps = (data: any) => {
|
const handleComponentProps = (data: any) => {
|
||||||
|
|
@ -243,7 +234,6 @@ const Welcome = () => {
|
||||||
return {
|
return {
|
||||||
...handleBannerProps(config),
|
...handleBannerProps(config),
|
||||||
onSearch: onBannerSearch,
|
onSearch: onBannerSearch,
|
||||||
onQaClick: () => setQaModalOpen?.(true),
|
|
||||||
btns: (config?.btns || []).map((item: any) => ({
|
btns: (config?.btns || []).map((item: any) => ({
|
||||||
...item,
|
...item,
|
||||||
href: item.href || '/node',
|
href: item.href || '/node',
|
||||||
|
|
@ -263,6 +253,15 @@ const Welcome = () => {
|
||||||
return handleImgTextProps(config);
|
return handleImgTextProps(config);
|
||||||
case 'comment':
|
case 'comment':
|
||||||
return handleCommentProps(config);
|
return handleCommentProps(config);
|
||||||
|
case 'block_grid':
|
||||||
|
return handleBlockGridProps(config);
|
||||||
|
case 'question':
|
||||||
|
return {
|
||||||
|
...handleQuestionProps(config),
|
||||||
|
onSearch: (text: string) => {
|
||||||
|
onBannerSearch(text, 'chat');
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"packageManager": "pnpm@10.12.1",
|
"packageManager": "pnpm@10.12.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ctzhian/tiptap": "^1.10.3",
|
"@ctzhian/tiptap": "^1.11.4",
|
||||||
"@ctzhian/ui": "^7.0.5",
|
"@ctzhian/ui": "^7.0.5",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React from 'react';
|
||||||
|
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
|
||||||
|
|
||||||
|
const IconJiugongge = (props: SvgIconProps) => (
|
||||||
|
<SvgIcon
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 1117 1024'
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d='M1061.236364 0H55.854545C22.341818 0 0 22.341818 0 55.854545v893.672728c0 33.512727 22.341818 55.854545 55.854545 55.854545h1005.381819c33.512727 0 55.854545-22.341818 55.854545-55.854545V55.854545c0-33.512727-22.341818-55.854545-55.854545-55.854545zM111.709091 893.672727V111.709091h893.672727v781.963636H111.709091z'></path>
|
||||||
|
<path d='M279.412364 392.424727h159.604363V232.866909H279.412364v159.557818z m199.493818 0h159.557818V232.866909h-159.557818v159.557818z m199.493818-159.557818v159.557818h159.557818V232.866909h-159.557818z m-398.987636 359.098182h159.604363v-159.650909H279.412364v159.650909z m199.493818 0h159.557818v-159.650909h-159.557818v159.650909z m199.493818 0h159.557818v-159.650909h-159.557818v159.650909z m-398.987636 199.447273h159.604363v-159.557819H279.412364v159.557819z m199.493818 0h159.557818v-159.557819h-159.557818v159.557819z m199.493818 0h159.557818v-159.557819h-159.557818v159.557819z'></path>
|
||||||
|
</SvgIcon>
|
||||||
|
);
|
||||||
|
|
||||||
|
IconJiugongge.displayName = 'icon-jiugongge';
|
||||||
|
|
||||||
|
export default IconJiugongge;
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React from 'react';
|
||||||
|
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
|
||||||
|
|
||||||
|
const IconLianjiezu1 = (props: SvgIconProps) => (
|
||||||
|
<SvgIcon
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 1117 1024'
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d='M1061.236364 0H55.854545C22.341818 0 0 22.341818 0 55.854545v893.672728c0 33.512727 22.341818 55.854545 55.854545 55.854545h1005.381819c33.512727 0 55.854545-22.341818 55.854545-55.854545V55.854545c0-33.512727-22.341818-55.854545-55.854545-55.854545zM111.709091 893.672727V111.709091h893.672727v781.963636H111.709091z'></path>
|
||||||
|
<path d='M442.926545 644.096l8.145455 6.283636a174.08 174.08 0 0 0 85.504 35.560728l10.24 1.256727v70.376727l-12.8-1.256727a245.061818 245.061818 0 0 1-130.653091-54.272l-9.914182-8.098909 9.029818-9.122909 33.18691-33.419637 7.26109-7.307636z m230.958546 0l49.803636 49.803636-9.960727 8.145455a244.270545 244.270545 0 0 1-130.653091 54.272l-12.753454 1.256727v-70.376727l10.193454-1.256727a172.357818 172.357818 0 0 0 85.224727-35.514182l8.145455-6.330182z m130.234182-120.087273l-1.256728 12.753455a245.061818 245.061818 0 0 1-54.272 130.653091l-8.145454 9.960727-49.803636-49.803636 6.283636-8.098909c19.362909-24.948364 31.697455-54.225455 35.560727-85.271273l1.256727-10.24h70.376728z m-420.770909-0.232727l1.256727 10.193455c3.863273 31.278545 16.197818 60.509091 35.514182 85.224727l6.330182 8.145454-49.803637 49.803637-8.145454-9.960728a244.270545 244.270545 0 0 1-54.272-130.65309l-1.256728-12.753455h70.376728zM558.545455 372.363636c77.265455 0 139.636364 62.370909 139.636363 139.636364s-62.370909 139.636364-139.636363 139.636364-139.636364-62.370909-139.636364-139.636364 62.370909-139.636364 139.636364-139.636364z m-181.946182-25.460363l9.122909 9.029818 33.419636 33.186909 7.307637 7.261091-6.283637 8.145454a174.08 174.08 0 0 0-35.560727 85.504l-1.256727 10.24H312.971636l1.256728-12.8a244.270545 244.270545 0 0 1 54.272-130.65309l8.098909-9.914182z m363.892363 0l8.098909 9.914182c30.440727 37.236364 49.477818 82.385455 54.272 130.65309l1.256728 12.753455h-70.376728l-1.256727-10.193455a174.08 174.08 0 0 0-35.560727-85.504l-6.283636-8.145454 7.307636-7.261091 33.419636-33.186909 9.122909-9.029818z m-170.170181-80.523637l12.753454 1.303273a244.270545 244.270545 0 0 1 130.653091 54.272l9.914182 8.098909-9.029818 9.122909-33.186909 33.419637-7.261091 7.307636-8.145455-6.283636a174.08 174.08 0 0 0-85.504-35.560728l-10.24-1.256727V266.426182z m-23.552 0v70.423273l-10.193455 1.256727a174.08 174.08 0 0 0-85.504 35.560728l-8.145455 6.283636-7.26109-7.307636-33.18691-33.419637-9.029818-9.122909 9.914182-8.098909a244.270545 244.270545 0 0 1 130.653091-54.272l12.753455-1.256727z'></path>
|
||||||
|
</SvgIcon>
|
||||||
|
);
|
||||||
|
|
||||||
|
IconLianjiezu1.displayName = 'icon-lianjiezu1';
|
||||||
|
|
||||||
|
export default IconLianjiezu1;
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
|
||||||
|
|
||||||
|
const IconWenhao = (props: SvgIconProps) => (
|
||||||
|
<SvgIcon
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 1024 1024'
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d='M469.3504 768h85.2992v-85.3504H469.3504V768zM512 85.3504A426.8032 426.8032 0 0 0 85.3504 512c0 235.52 191.1296 426.6496 426.6496 426.6496S938.6496 747.52 938.6496 512 747.52 85.3504 512 85.3504z m0 768A341.8112 341.8112 0 0 1 170.6496 512 341.8112 341.8112 0 0 1 512 170.6496 341.8112 341.8112 0 0 1 853.3504 512 341.8112 341.8112 0 0 1 512 853.3504zM512 256a170.5984 170.5984 0 0 0-170.6496 170.6496h85.2992c0-46.8992 38.4-85.2992 85.3504-85.2992s85.3504 38.4 85.3504 85.2992c0 85.3504-128 74.7008-128 213.3504h85.2992c0-96 128-106.6496 128-213.3504A170.5984 170.5984 0 0 0 512 256z'></path>
|
||||||
|
</SvgIcon>
|
||||||
|
);
|
||||||
|
|
||||||
|
IconWenhao.displayName = 'icon-wenhao';
|
||||||
|
|
||||||
|
export default IconWenhao;
|
||||||
|
|
@ -109,6 +109,7 @@ export { default as IconJichuwendang } from './IconJichuwendang';
|
||||||
export { default as IconJina } from './IconJina';
|
export { default as IconJina } from './IconJina';
|
||||||
export { default as IconJinggao } from './IconJinggao';
|
export { default as IconJinggao } from './IconJinggao';
|
||||||
export { default as IconJinsousuo } from './IconJinsousuo';
|
export { default as IconJinsousuo } from './IconJinsousuo';
|
||||||
|
export { default as IconJiugongge } from './IconJiugongge';
|
||||||
export { default as IconJushou } from './IconJushou';
|
export { default as IconJushou } from './IconJushou';
|
||||||
export { default as IconKefu } from './IconKefu';
|
export { default as IconKefu } from './IconKefu';
|
||||||
export { default as IconKehuanli } from './IconKehuanli';
|
export { default as IconKehuanli } from './IconKehuanli';
|
||||||
|
|
@ -120,6 +121,7 @@ export { default as IconLDAP } from './IconLDAP';
|
||||||
export { default as IconLanyun } from './IconLanyun';
|
export { default as IconLanyun } from './IconLanyun';
|
||||||
export { default as IconLepton } from './IconLepton';
|
export { default as IconLepton } from './IconLepton';
|
||||||
export { default as IconLianjiezu } from './IconLianjiezu';
|
export { default as IconLianjiezu } from './IconLianjiezu';
|
||||||
|
export { default as IconLianjiezu1 } from './IconLianjiezu1';
|
||||||
export { default as IconLingyiwanwu } from './IconLingyiwanwu';
|
export { default as IconLingyiwanwu } from './IconLingyiwanwu';
|
||||||
export { default as IconLmstudio } from './IconLmstudio';
|
export { default as IconLmstudio } from './IconLmstudio';
|
||||||
export { default as IconLogoGroq } from './IconLogoGroq';
|
export { default as IconLogoGroq } from './IconLogoGroq';
|
||||||
|
|
@ -201,6 +203,7 @@ export { default as IconWeibo1 } from './IconWeibo1';
|
||||||
export { default as IconWeixingongzhonghao } from './IconWeixingongzhonghao';
|
export { default as IconWeixingongzhonghao } from './IconWeixingongzhonghao';
|
||||||
export { default as IconWeixingongzhonghaoDaiyanse } from './IconWeixingongzhonghaoDaiyanse';
|
export { default as IconWeixingongzhonghaoDaiyanse } from './IconWeixingongzhonghaoDaiyanse';
|
||||||
export { default as IconWendajiqiren } from './IconWendajiqiren';
|
export { default as IconWendajiqiren } from './IconWendajiqiren';
|
||||||
|
export { default as IconWenhao } from './IconWenhao';
|
||||||
export { default as IconWenjian } from './IconWenjian';
|
export { default as IconWenjian } from './IconWenjian';
|
||||||
export { default as IconWenjianjia } from './IconWenjianjia';
|
export { default as IconWenjianjia } from './IconWenjianjia';
|
||||||
export { default as IconWenjianjiaKai } from './IconWenjianjiaKai';
|
export { default as IconWenjianjiaKai } from './IconWenjianjiaKai';
|
||||||
|
|
|
||||||
|
|
@ -46,11 +46,11 @@ export const THEME_LIST = [
|
||||||
value: 'darkDeepForest',
|
value: 'darkDeepForest',
|
||||||
palette: darkDeepForestPalette,
|
palette: darkDeepForestPalette,
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
label: '深邃黑',
|
// label: '深邃黑',
|
||||||
value: 'white',
|
// value: 'white',
|
||||||
palette: whitePalette,
|
// palette: whitePalette,
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
label: '电光蓝',
|
label: '电光蓝',
|
||||||
value: 'electricBlue',
|
value: 'electricBlue',
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,6 @@ interface BannerProps {
|
||||||
onSearch?: (value: string, type?: 'search' | 'chat') => void;
|
onSearch?: (value: string, type?: 'search' | 'chat') => void;
|
||||||
onSearchSuggestions?: (query: string) => Promise<SearchSuggestion[]>;
|
onSearchSuggestions?: (query: string) => Promise<SearchSuggestion[]>;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
onQaClick?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Banner = React.memo(
|
const Banner = React.memo(
|
||||||
|
|
@ -145,7 +144,6 @@ const Banner = React.memo(
|
||||||
onSearch,
|
onSearch,
|
||||||
onSearchSuggestions,
|
onSearchSuggestions,
|
||||||
baseUrl = '',
|
baseUrl = '',
|
||||||
onQaClick,
|
|
||||||
}: BannerProps) => {
|
}: BannerProps) => {
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [suggestions, setSuggestions] = useState<SearchSuggestion[]>([]);
|
const [suggestions, setSuggestions] = useState<SearchSuggestion[]>([]);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ import React from 'react';
|
||||||
import { styled, Grid, Box, alpha } from '@mui/material';
|
import { styled, Grid, Box, alpha } from '@mui/material';
|
||||||
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
||||||
import IconWenjian from '@panda-wiki/icons/IconWenjian';
|
import IconWenjian from '@panda-wiki/icons/IconWenjian';
|
||||||
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
import {
|
||||||
|
useFadeInText,
|
||||||
|
useCardFadeInAnimation,
|
||||||
|
} from '../hooks/useGsapAnimation';
|
||||||
|
|
||||||
interface BasicDocProps {
|
interface BasicDocProps {
|
||||||
mobile?: boolean;
|
mobile?: boolean;
|
||||||
|
|
@ -33,9 +36,13 @@ const StyledBasicDocItem = styled('div')(({ theme }) => ({
|
||||||
transform: 'translateY(-5px)',
|
transform: 'translateY(-5px)',
|
||||||
boxShadow: `0px 10px 20px 0px ${alpha(theme.palette.text.primary, 0.1)}`,
|
boxShadow: `0px 10px 20px 0px ${alpha(theme.palette.text.primary, 0.1)}`,
|
||||||
borderColor: theme.palette.primary.main,
|
borderColor: theme.palette.primary.main,
|
||||||
|
'.basic-doc-item-title': {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
width: '100%',
|
width: '100%',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
opacity: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledBasicDocItemTitle = styled('h3')(({ theme }) => ({
|
const StyledBasicDocItemTitle = styled('h3')(({ theme }) => ({
|
||||||
|
|
@ -74,7 +81,7 @@ const BasicDocItem: React.FC<{
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
size: any;
|
size: any;
|
||||||
}> = React.memo(({ item, index, baseUrl, size }) => {
|
}> = React.memo(({ item, index, baseUrl, size }) => {
|
||||||
const cardRef = useCardAnimation(0.2 + index * 0.1, 0.1);
|
const cardRef = useCardFadeInAnimation(0.2 + index * 0.1, 0.1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid size={size} key={index}>
|
<Grid size={size} key={index}>
|
||||||
|
|
@ -84,7 +91,7 @@ const BasicDocItem: React.FC<{
|
||||||
window.open(`${baseUrl}/node/${item.id}`, '_blank');
|
window.open(`${baseUrl}/node/${item.id}`, '_blank');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StyledBasicDocItemTitle>
|
<StyledBasicDocItemTitle className='basic-doc-item-title'>
|
||||||
{item.emoji ? (
|
{item.emoji ? (
|
||||||
<Box>{item.emoji}</Box>
|
<Box>{item.emoji}</Box>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -93,16 +100,6 @@ const BasicDocItem: React.FC<{
|
||||||
<StyledBasicDocItemName>{item.name}</StyledBasicDocItemName>
|
<StyledBasicDocItemName>{item.name}</StyledBasicDocItemName>
|
||||||
</StyledBasicDocItemTitle>
|
</StyledBasicDocItemTitle>
|
||||||
<StyledBasicDocItemSummary>{item.summary}</StyledBasicDocItemSummary>
|
<StyledBasicDocItemSummary>{item.summary}</StyledBasicDocItemSummary>
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
color: 'primary.main',
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 400,
|
|
||||||
alignSelf: 'flex-end',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
查看更多
|
|
||||||
</Box>
|
|
||||||
</StyledBasicDocItem>
|
</StyledBasicDocItem>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { styled, Grid, alpha, Stack } from '@mui/material';
|
||||||
|
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
||||||
|
import {
|
||||||
|
useFadeInText,
|
||||||
|
useCardFadeInAnimation,
|
||||||
|
} from '../hooks/useGsapAnimation';
|
||||||
|
|
||||||
|
interface BlockGridProps {
|
||||||
|
mobile?: boolean;
|
||||||
|
title?: string;
|
||||||
|
items?: {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
const StyledBlockGridItem = styled(Stack)(({ theme }) => ({
|
||||||
|
aspectRatio: '1 / 1',
|
||||||
|
position: 'relative',
|
||||||
|
border: `1px solid ${alpha(theme.palette.text.primary, 0.15)}`,
|
||||||
|
borderRadius: '10px',
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
boxShadow: `0px 5px 20px 0px ${alpha(theme.palette.text.primary, 0.06)}`,
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
'&:hover': {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
borderColor: theme.palette.primary.main,
|
||||||
|
boxShadow: `0px 10px 20px 0px ${alpha(theme.palette.text.primary, 0.1)}`,
|
||||||
|
},
|
||||||
|
opacity: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledBlockGridItemImgBox = styled('div')(({ theme }) => ({
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledBlockGridItemImg = styled('img')(({ theme }) => ({
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover',
|
||||||
|
borderRadius: '10px',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledBlockGridItemTitle = styled('div')(({ theme }) => ({
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '24px',
|
||||||
|
left: '50%',
|
||||||
|
maxWidth: 'calc(100% - 24px)',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
padding: theme.spacing(0.5, 1),
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
flexShrink: 0,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
fontSize: 14,
|
||||||
|
textAlign: 'center',
|
||||||
|
fontWeight: 700,
|
||||||
|
color: theme.palette.background.default,
|
||||||
|
backgroundColor: alpha(theme.palette.text.primary, 0.5),
|
||||||
|
borderRadius: '6px',
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 单个卡片组件,带动画效果
|
||||||
|
const BlockGridItem: React.FC<{
|
||||||
|
item: {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
index: number;
|
||||||
|
}> = React.memo(({ item, index }) => {
|
||||||
|
const cardRef = useCardFadeInAnimation(0.2 + index * 0.1, 0.1);
|
||||||
|
return (
|
||||||
|
<StyledBlockGridItem ref={cardRef as React.Ref<HTMLDivElement>} gap={2}>
|
||||||
|
<StyledBlockGridItemImgBox>
|
||||||
|
<StyledBlockGridItemImg src={item.url} />
|
||||||
|
</StyledBlockGridItemImgBox>
|
||||||
|
|
||||||
|
<StyledBlockGridItemTitle>{item.name}</StyledBlockGridItemTitle>
|
||||||
|
</StyledBlockGridItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const BlockGrid: React.FC<BlockGridProps> = React.memo(
|
||||||
|
({ title, items = [], mobile }) => {
|
||||||
|
const size =
|
||||||
|
typeof mobile === 'boolean'
|
||||||
|
? mobile
|
||||||
|
? 12
|
||||||
|
: { xs: 12, md: 4 }
|
||||||
|
: { xs: 12, md: 4 };
|
||||||
|
|
||||||
|
// 添加标题淡入动画
|
||||||
|
const titleRef = useFadeInText(0.2, 0.1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledTopicBox>
|
||||||
|
<StyledTopicTitle ref={titleRef}>{title}</StyledTopicTitle>
|
||||||
|
<Grid container spacing={3} sx={{ width: '100%' }}>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<Grid size={size} key={index}>
|
||||||
|
<BlockGridItem item={item} index={index} />
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</StyledTopicBox>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default BlockGrid;
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
.swiper {
|
.swiper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.swiper-slide {
|
.swiper-slide {
|
||||||
|
|
@ -9,26 +8,11 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding-bottom: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .swiper-slide-prev {
|
|
||||||
transform-origin: center right;
|
|
||||||
opacity: 0.4;
|
|
||||||
} */
|
|
||||||
/* .swiper-slide-next {
|
|
||||||
opacity: 0.4;
|
|
||||||
} */
|
|
||||||
|
|
||||||
.swiper-slide img {
|
.swiper-slide img {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 居中激活项保持原尺寸 */
|
|
||||||
/* .swiper-slide-active {
|
|
||||||
transform: scale(1) translateZ(0) !important;
|
|
||||||
z-index: 1;
|
|
||||||
} */
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
import { CSSProperties, memo, useRef, useCallback, useState } from 'react';
|
import {
|
||||||
import { styled, alpha, Tabs, Tab, Box } from '@mui/material';
|
CSSProperties,
|
||||||
|
memo,
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
} from 'react';
|
||||||
|
import { styled, alpha, Tabs, Tab, Box, useTheme } from '@mui/material';
|
||||||
import { StyledTopicTitle, StyledTopicBox } from '../component/styledCommon';
|
import { StyledTopicTitle, StyledTopicBox } from '../component/styledCommon';
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
import { useFadeInText } from '../hooks/useGsapAnimation';
|
import { useFadeInText } from '../hooks/useGsapAnimation';
|
||||||
import { Swiper as SwiperType } from 'swiper';
|
import { Swiper as SwiperType } from 'swiper';
|
||||||
|
import { gsap } from 'gsap';
|
||||||
|
|
||||||
import 'swiper/css';
|
import 'swiper/css';
|
||||||
import 'swiper/css/pagination';
|
import 'swiper/css/pagination';
|
||||||
|
|
@ -51,6 +59,33 @@ const StyledSwiperSlideImg = styled('img')(({ theme }) => ({
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledSwiperSlideDesc = styled('div')(({ theme }) => ({
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '24px',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
padding: theme.spacing(0.5, 1),
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 400,
|
||||||
|
color: theme.palette.background.default,
|
||||||
|
borderRadius: '6px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
zIndex: 0,
|
||||||
|
'&::before': {
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: alpha(theme.palette.text.primary, 0.5),
|
||||||
|
filter: 'blur(6px)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
zIndex: -1,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
// 样式化的 Tabs 容器 - 浅灰色背景,圆角,阴影
|
// 样式化的 Tabs 容器 - 浅灰色背景,圆角,阴影
|
||||||
const StyledTabsContainer = styled(Box)(({ theme }) => ({
|
const StyledTabsContainer = styled(Box)(({ theme }) => ({
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
|
|
@ -78,7 +113,7 @@ const StyledTabs = styled(Tabs)(({ theme }) => ({
|
||||||
const StyledTab = styled(Tab)(({ theme }) => ({
|
const StyledTab = styled(Tab)(({ theme }) => ({
|
||||||
minHeight: 'auto',
|
minHeight: 'auto',
|
||||||
padding: theme.spacing(1, 2),
|
padding: theme.spacing(1, 2),
|
||||||
borderRadius: '10px',
|
borderRadius: '6px',
|
||||||
backgroundColor: theme.palette.background.default,
|
backgroundColor: theme.palette.background.default,
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
|
@ -96,11 +131,16 @@ const StyledTab = styled(Tab)(({ theme }) => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const Carousel = ({ title, items }: CarouselProps) => {
|
const Carousel = ({ title, items }: CarouselProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
// 添加标题淡入动画
|
// 添加标题淡入动画
|
||||||
const titleRef = useFadeInText(0.2, 0.1);
|
const titleRef = useFadeInText(0.2, 0.1);
|
||||||
// 添加Swiper ref
|
// 添加Swiper ref
|
||||||
const swiperRef = useRef<SwiperType | null>(null);
|
const swiperRef = useRef<SwiperType | null>(null);
|
||||||
const [activeTab, setActiveTab] = useState<string>(items[0]?.id || '');
|
const [activeTab, setActiveTab] = useState<string>(items[0]?.id || '');
|
||||||
|
// 存储所有描述元素的 ref
|
||||||
|
const descRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||||
|
// 存储动画时间线,用于清理
|
||||||
|
const animationTimelines = useRef<gsap.core.Timeline[]>([]);
|
||||||
|
|
||||||
// 导航函数
|
// 导航函数
|
||||||
const handlePrev = useCallback(() => {
|
const handlePrev = useCallback(() => {
|
||||||
|
|
@ -115,7 +155,117 @@ const Carousel = ({ title, items }: CarouselProps) => {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 监听 Swiper 切换,更新 activeTab
|
// 触发从左到右的文字出现动画(逐字符显示,容器逐渐撑大)
|
||||||
|
const animateTextFromLeft = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
const descElement = descRefs.current[index];
|
||||||
|
if (!descElement) return;
|
||||||
|
|
||||||
|
// 清理之前的动画
|
||||||
|
animationTimelines.current.forEach(tl => tl.kill());
|
||||||
|
animationTimelines.current = [];
|
||||||
|
|
||||||
|
const originalText = descElement.textContent || '';
|
||||||
|
if (!originalText) return;
|
||||||
|
|
||||||
|
// 获取容器的 padding 值
|
||||||
|
const computedStyle = window.getComputedStyle(descElement);
|
||||||
|
const paddingLeft = parseFloat(computedStyle.paddingLeft) || 0;
|
||||||
|
const paddingRight = parseFloat(computedStyle.paddingRight) || 0;
|
||||||
|
const padding = paddingLeft + paddingRight;
|
||||||
|
|
||||||
|
// 将文字分割成字符
|
||||||
|
const chars = Array.from(originalText);
|
||||||
|
const charElements: HTMLSpanElement[] = [];
|
||||||
|
|
||||||
|
// 清空容器并创建字符元素(初始都隐藏)
|
||||||
|
descElement.innerHTML = '';
|
||||||
|
chars.forEach(char => {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.textContent = char === ' ' ? '\u00A0' : char; // 空格用非断行空格
|
||||||
|
span.style.opacity = '0';
|
||||||
|
span.style.display = 'inline-block';
|
||||||
|
descElement.appendChild(span);
|
||||||
|
charElements.push(span);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建一个隐藏的测量容器来准确测量每个字符的宽度
|
||||||
|
const measureContainer = document.createElement('div');
|
||||||
|
measureContainer.style.position = 'absolute';
|
||||||
|
measureContainer.style.visibility = 'hidden';
|
||||||
|
measureContainer.style.whiteSpace = 'nowrap';
|
||||||
|
measureContainer.style.fontSize = computedStyle.fontSize;
|
||||||
|
measureContainer.style.fontWeight = computedStyle.fontWeight;
|
||||||
|
measureContainer.style.fontFamily = computedStyle.fontFamily;
|
||||||
|
document.body.appendChild(measureContainer);
|
||||||
|
|
||||||
|
// 测量每个字符的宽度
|
||||||
|
const charWidths: number[] = [];
|
||||||
|
charElements.forEach(span => {
|
||||||
|
measureContainer.textContent = span.textContent;
|
||||||
|
const charWidth = measureContainer.offsetWidth;
|
||||||
|
charWidths.push(charWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.removeChild(measureContainer);
|
||||||
|
|
||||||
|
// 设置容器初始状态(只有 padding,背景色透明)
|
||||||
|
gsap.set(descElement, {
|
||||||
|
width: padding,
|
||||||
|
minWidth: padding,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建动画时间线,延迟 0.5 秒开始
|
||||||
|
const tl = gsap.timeline({ delay: 0.5 });
|
||||||
|
let currentWidth = padding;
|
||||||
|
|
||||||
|
// 背景色从透明逐渐加深(与第一个字符同时开始)
|
||||||
|
tl.to(
|
||||||
|
descElement,
|
||||||
|
{
|
||||||
|
duration: 0.4, // 背景色变化稍快一些,在文字显示过程中完成
|
||||||
|
ease: 'power2.out',
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 逐个显示字符,同时增加容器宽度
|
||||||
|
// 第一个字符在延迟后立即开始显示(时间位置 0)
|
||||||
|
charElements.forEach((span, i) => {
|
||||||
|
const charWidth = charWidths[i];
|
||||||
|
currentWidth += charWidth;
|
||||||
|
|
||||||
|
// 同时显示字符和增加容器宽度
|
||||||
|
// 第一个字符立即显示(i=0 时时间为 0),后续字符依次延迟
|
||||||
|
tl.to(
|
||||||
|
span,
|
||||||
|
{
|
||||||
|
opacity: 1,
|
||||||
|
duration: 0.08,
|
||||||
|
ease: 'none',
|
||||||
|
},
|
||||||
|
i * 0.08,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 同时更新容器宽度
|
||||||
|
tl.to(
|
||||||
|
descElement,
|
||||||
|
{
|
||||||
|
width: currentWidth,
|
||||||
|
duration: 0.08,
|
||||||
|
ease: 'none',
|
||||||
|
},
|
||||||
|
i * 0.08,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存动画时间线
|
||||||
|
animationTimelines.current.push(tl);
|
||||||
|
},
|
||||||
|
[theme],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听 Swiper 切换,更新 activeTab 并触发动画
|
||||||
const handleSlideChange = useCallback(
|
const handleSlideChange = useCallback(
|
||||||
(swiper: SwiperType) => {
|
(swiper: SwiperType) => {
|
||||||
const activeIndex = swiper.activeIndex;
|
const activeIndex = swiper.activeIndex;
|
||||||
|
|
@ -123,9 +273,11 @@ const Carousel = ({ title, items }: CarouselProps) => {
|
||||||
const activeItem = items[activeIndex];
|
const activeItem = items[activeIndex];
|
||||||
if (activeItem) {
|
if (activeItem) {
|
||||||
setActiveTab(activeItem.id);
|
setActiveTab(activeItem.id);
|
||||||
|
// 触发当前幻灯片的文字动画
|
||||||
|
animateTextFromLeft(activeIndex);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[items],
|
[items, animateTextFromLeft],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 当 activeTab 改变时,切换对应的 Swiper 卡片
|
// 当 activeTab 改变时,切换对应的 Swiper 卡片
|
||||||
|
|
@ -135,11 +287,34 @@ const Carousel = ({ title, items }: CarouselProps) => {
|
||||||
const targetIndex = items.findIndex(item => item.id === value);
|
const targetIndex = items.findIndex(item => item.id === value);
|
||||||
if (targetIndex !== -1 && swiperRef.current) {
|
if (targetIndex !== -1 && swiperRef.current) {
|
||||||
swiperRef.current.slideTo(targetIndex);
|
swiperRef.current.slideTo(targetIndex);
|
||||||
|
// 触发切换后的文字动画
|
||||||
|
setTimeout(() => {
|
||||||
|
animateTextFromLeft(targetIndex);
|
||||||
|
}, 300); // 等待切换动画完成
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[items],
|
[items, animateTextFromLeft],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 初始加载时触发第一个幻灯片的动画
|
||||||
|
useEffect(() => {
|
||||||
|
if (items.length > 0 && descRefs.current[0]) {
|
||||||
|
// 延迟执行,确保元素已经渲染
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
animateTextFromLeft(0);
|
||||||
|
}, 100);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [items.length, animateTextFromLeft]);
|
||||||
|
|
||||||
|
// 组件卸载时清理所有动画
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
animationTimelines.current.forEach(tl => tl.kill());
|
||||||
|
animationTimelines.current = [];
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 使用事件委托的方式处理点击事件
|
// 使用事件委托的方式处理点击事件
|
||||||
const handleSlideClick = useCallback(
|
const handleSlideClick = useCallback(
|
||||||
(swiper: SwiperType, event: MouseEvent | TouchEvent | PointerEvent) => {
|
(swiper: SwiperType, event: MouseEvent | TouchEvent | PointerEvent) => {
|
||||||
|
|
@ -213,9 +388,16 @@ const Carousel = ({ title, items }: CarouselProps) => {
|
||||||
modules={[Pagination, Autoplay]}
|
modules={[Pagination, Autoplay]}
|
||||||
className='mySwiper'
|
className='mySwiper'
|
||||||
>
|
>
|
||||||
{items?.map(item => (
|
{items?.map((item, index) => (
|
||||||
<SwiperSlide key={item.id}>
|
<SwiperSlide key={item.id} style={{ position: 'relative' }}>
|
||||||
<StyledSwiperSlideImg src={item.url} alt={item.title} />
|
<StyledSwiperSlideImg src={item.url} alt={item.title} />
|
||||||
|
<StyledSwiperSlideDesc
|
||||||
|
ref={el => {
|
||||||
|
descRefs.current[index] = el;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.desc}
|
||||||
|
</StyledSwiperSlideDesc>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
</Swiper>
|
</Swiper>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { styled, Grid, alpha, Stack } from '@mui/material';
|
import { styled, Grid, alpha, Stack } from '@mui/material';
|
||||||
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
||||||
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
import {
|
||||||
|
useFadeInText,
|
||||||
|
useCardScaleAnimation,
|
||||||
|
} from '../hooks/useGsapAnimation';
|
||||||
|
|
||||||
interface CaseProps {
|
interface CaseProps {
|
||||||
mobile?: boolean;
|
mobile?: boolean;
|
||||||
|
|
@ -27,6 +30,8 @@ const StyledCaseItem = styled('a')(({ theme }) => ({
|
||||||
boxShadow: `0px 10px 20px 0px ${alpha(theme.palette.text.primary, 0.1)}`,
|
boxShadow: `0px 10px 20px 0px ${alpha(theme.palette.text.primary, 0.1)}`,
|
||||||
},
|
},
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
opacity: 0,
|
||||||
|
scale: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledCaseItemTitle = styled('span')(({ theme }) => ({
|
const StyledCaseItemTitle = styled('span')(({ theme }) => ({
|
||||||
|
|
@ -40,7 +45,10 @@ const CaseItem: React.FC<{
|
||||||
item: any;
|
item: any;
|
||||||
index: number;
|
index: number;
|
||||||
}> = React.memo(({ item, index }) => {
|
}> = React.memo(({ item, index }) => {
|
||||||
const cardRef = useCardAnimation(0.2 + index * 0.1, 0.1);
|
const rand = Math.random();
|
||||||
|
const cardRef = useCardScaleAnimation({
|
||||||
|
duration: rand < 0.5 ? rand + 0.5 : rand,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<StyledCaseItem
|
<StyledCaseItem
|
||||||
ref={cardRef as React.Ref<HTMLAnchorElement>}
|
ref={cardRef as React.Ref<HTMLAnchorElement>}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { styled, Grid, alpha, Stack, Rating } from '@mui/material';
|
import { styled, Grid, alpha, Stack, Rating } from '@mui/material';
|
||||||
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
||||||
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
import {
|
||||||
|
useFadeInText,
|
||||||
|
useCardFadeInAnimation,
|
||||||
|
} from '../hooks/useGsapAnimation';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mobile?: boolean;
|
mobile?: boolean;
|
||||||
|
|
@ -22,6 +25,13 @@ const StyledItem = styled(Stack)(({ theme }) => ({
|
||||||
boxShadow: `0px 5px 20px 0px ${alpha(theme.palette.text.primary, 0.06)}`,
|
boxShadow: `0px 5px 20px 0px ${alpha(theme.palette.text.primary, 0.06)}`,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
'&:hover': {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
borderColor: theme.palette.primary.main,
|
||||||
|
boxShadow: `0px 10px 20px 0px ${alpha(theme.palette.text.primary, 0.1)}`,
|
||||||
|
},
|
||||||
|
opacity: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledItemSummary = styled('div')(({ theme }) => ({
|
const StyledItemSummary = styled('div')(({ theme }) => ({
|
||||||
|
|
@ -58,7 +68,7 @@ const Item: React.FC<{
|
||||||
};
|
};
|
||||||
index: number;
|
index: number;
|
||||||
}> = React.memo(({ item, index }) => {
|
}> = React.memo(({ item, index }) => {
|
||||||
const cardRef = useCardAnimation(0.2 + index * 0.1, 0.1);
|
const cardRef = useCardFadeInAnimation(0.2 + index * 0.1, 0.1);
|
||||||
return (
|
return (
|
||||||
<StyledItem ref={cardRef as React.Ref<HTMLDivElement>} gap={3}>
|
<StyledItem ref={cardRef as React.Ref<HTMLDivElement>} gap={3}>
|
||||||
<StyledItemSummary>{item.comment}</StyledItemSummary>
|
<StyledItemSummary>{item.comment}</StyledItemSummary>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { decodeBase64 } from '../utils';
|
||||||
export const DocWidth = {
|
export const DocWidth = {
|
||||||
full: {
|
full: {
|
||||||
label: '全屏',
|
label: '全屏',
|
||||||
|
|
@ -12,3 +13,6 @@ export const DocWidth = {
|
||||||
value: 720,
|
value: 720,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PROJECT_NAME =
|
||||||
|
'5pys572R56uZ55SxIFBhbmRhV2lraSDmj5DkvpvmioDmnK/mlK/mjIE=';
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,10 @@ import {
|
||||||
} from '../component/styledCommon';
|
} from '../component/styledCommon';
|
||||||
import { IconWenjianjia, IconWenjian } from '@panda-wiki/icons';
|
import { IconWenjianjia, IconWenjian } from '@panda-wiki/icons';
|
||||||
import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
|
import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
|
||||||
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
import {
|
||||||
|
useFadeInText,
|
||||||
|
useCardFadeInAnimation,
|
||||||
|
} from '../hooks/useGsapAnimation';
|
||||||
interface DirDocProps {
|
interface DirDocProps {
|
||||||
mobile?: boolean;
|
mobile?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
@ -93,7 +96,7 @@ const DirDocItem: React.FC<{
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
size: any;
|
size: any;
|
||||||
}> = React.memo(({ item, index, baseUrl, size }) => {
|
}> = React.memo(({ item, index, baseUrl, size }) => {
|
||||||
const cardRef = useCardAnimation(0.2 + index * 0.1, 0.1);
|
const cardRef = useCardFadeInAnimation(0.2 + index * 0.1, 0.1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid size={size} key={index}>
|
<Grid size={size} key={index}>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ import React from 'react';
|
||||||
import { styled, Grid, alpha } from '@mui/material';
|
import { styled, Grid, alpha } from '@mui/material';
|
||||||
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
||||||
import { IconLianjiezu } from '@panda-wiki/icons';
|
import { IconLianjiezu } from '@panda-wiki/icons';
|
||||||
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
import {
|
||||||
|
useFadeInText,
|
||||||
|
useCardFadeInAnimation,
|
||||||
|
} from '../hooks/useGsapAnimation';
|
||||||
|
|
||||||
interface FaqProps {
|
interface FaqProps {
|
||||||
mobile?: boolean;
|
mobile?: boolean;
|
||||||
|
|
@ -35,6 +38,7 @@ const StyledFaqItem = styled('a')(({ theme }) => ({
|
||||||
},
|
},
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
|
opacity: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledFaqItemTitle = styled('span')(({ theme }) => ({
|
const StyledFaqItemTitle = styled('span')(({ theme }) => ({
|
||||||
|
|
@ -48,7 +52,7 @@ const FaqItem: React.FC<{
|
||||||
index: number;
|
index: number;
|
||||||
size: any;
|
size: any;
|
||||||
}> = React.memo(({ item, index, size }) => {
|
}> = React.memo(({ item, index, size }) => {
|
||||||
const cardRef = useCardAnimation(0.2 + index * 0.1, 0.1);
|
const cardRef = useCardFadeInAnimation(0.2 + index * 0.1, 0.1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid size={size} key={index}>
|
<Grid size={size} key={index}>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { styled, Grid, alpha, Stack } from '@mui/material';
|
import { styled, Grid, alpha, Stack } from '@mui/material';
|
||||||
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
||||||
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
import {
|
||||||
|
useFadeInText,
|
||||||
|
useCardFadeInAnimation,
|
||||||
|
} from '../hooks/useGsapAnimation';
|
||||||
import { IconTips } from '@panda-wiki/icons';
|
import { IconTips } from '@panda-wiki/icons';
|
||||||
|
|
||||||
interface FeatureProps {
|
interface FeatureProps {
|
||||||
|
|
@ -11,7 +14,7 @@ interface FeatureProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
items?: {
|
items?: {
|
||||||
name: string;
|
name: string;
|
||||||
link: string;
|
desc: string;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
const StyledFeatureItem = styled(Stack)(({ theme }) => ({
|
const StyledFeatureItem = styled(Stack)(({ theme }) => ({
|
||||||
|
|
@ -20,12 +23,12 @@ const StyledFeatureItem = styled(Stack)(({ theme }) => ({
|
||||||
padding: theme.spacing(2.5),
|
padding: theme.spacing(2.5),
|
||||||
boxShadow: `0px 5px 20px 0px ${alpha(theme.palette.text.primary, 0.06)}`,
|
boxShadow: `0px 5px 20px 0px ${alpha(theme.palette.text.primary, 0.06)}`,
|
||||||
transition: 'all 0.2s ease',
|
transition: 'all 0.2s ease',
|
||||||
// '&:hover': {
|
'&:hover': {
|
||||||
// color: theme.palette.primary.main,
|
color: theme.palette.primary.main,
|
||||||
// borderColor: theme.palette.primary.main,
|
borderColor: theme.palette.primary.main,
|
||||||
// boxShadow: `0px 10px 20px 0px ${alpha(theme.palette.text.primary, 0.1)}`,
|
boxShadow: `0px 10px 20px 0px ${alpha(theme.palette.text.primary, 0.1)}`,
|
||||||
// },
|
},
|
||||||
// cursor: 'pointer',
|
opacity: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const StyledFeatureItemIcon = styled('div')(({ theme }) => ({
|
export const StyledFeatureItemIcon = styled('div')(({ theme }) => ({
|
||||||
|
|
@ -62,10 +65,13 @@ const StyledFeatureItemSummary = styled('div')(({ theme }) => ({
|
||||||
|
|
||||||
// 单个卡片组件,带动画效果
|
// 单个卡片组件,带动画效果
|
||||||
const FeatureItem: React.FC<{
|
const FeatureItem: React.FC<{
|
||||||
item: any;
|
item: {
|
||||||
|
name: string;
|
||||||
|
desc: string;
|
||||||
|
};
|
||||||
index: number;
|
index: number;
|
||||||
}> = React.memo(({ item, index }) => {
|
}> = React.memo(({ item, index }) => {
|
||||||
const cardRef = useCardAnimation(0.2 + index * 0.1, 0.1);
|
const cardRef = useCardFadeInAnimation(0.2 + index * 0.1, 0.1);
|
||||||
return (
|
return (
|
||||||
<StyledFeatureItem
|
<StyledFeatureItem
|
||||||
ref={cardRef as React.Ref<HTMLDivElement>}
|
ref={cardRef as React.Ref<HTMLDivElement>}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import { useState } from 'react';
|
||||||
import { IconDianhua, IconWeixingongzhonghao } from '@panda-wiki/icons';
|
import { IconDianhua, IconWeixingongzhonghao } from '@panda-wiki/icons';
|
||||||
import Overlay from './Overlay';
|
import Overlay from './Overlay';
|
||||||
import { DocWidth } from '../constants';
|
import { DocWidth } from '../constants';
|
||||||
|
import { PROJECT_NAME } from '../constants';
|
||||||
|
import { decodeBase64 } from '../utils';
|
||||||
|
|
||||||
interface DomainSocialMediaAccount {
|
interface DomainSocialMediaAccount {
|
||||||
channel?: string;
|
channel?: string;
|
||||||
|
|
@ -340,7 +342,7 @@ const Footer = React.memo(
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>本网站由 PandaWiki 提供技术支持</Box>
|
<Box>{decodeBase64(PROJECT_NAME)}</Box>
|
||||||
<img src={logo} alt='PandaWiki' width={16} height={16} />
|
<img src={logo} alt='PandaWiki' width={16} height={16} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -777,7 +779,7 @@ const Footer = React.memo(
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>本网站由 PandaWiki 提供技术支持</Box>
|
<Box>{decodeBase64(PROJECT_NAME)}</Box>
|
||||||
<img
|
<img
|
||||||
src={logo}
|
src={logo}
|
||||||
alt='PandaWiki'
|
alt='PandaWiki'
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,7 @@ export const useTypewriterText = (
|
||||||
};
|
};
|
||||||
|
|
||||||
// 卡片渐入动画 hook
|
// 卡片渐入动画 hook
|
||||||
export const useCardAnimation = (
|
export const useCardFadeInAnimation = (
|
||||||
delay: number = 0,
|
delay: number = 0,
|
||||||
threshold: number = 0.1,
|
threshold: number = 0.1,
|
||||||
) => {
|
) => {
|
||||||
|
|
@ -308,7 +308,6 @@ export const useCardAnimation = (
|
||||||
gsap.set(card, {
|
gsap.set(card, {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
y: 50,
|
y: 50,
|
||||||
// scale: 0.9,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 创建动画
|
// 创建动画
|
||||||
|
|
@ -317,7 +316,6 @@ export const useCardAnimation = (
|
||||||
tl.to(card, {
|
tl.to(card, {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
scale: 1,
|
|
||||||
duration: 0.4,
|
duration: 0.4,
|
||||||
ease: 'back.out(1.4)',
|
ease: 'back.out(1.4)',
|
||||||
});
|
});
|
||||||
|
|
@ -329,3 +327,136 @@ export const useCardAnimation = (
|
||||||
|
|
||||||
return cardRef;
|
return cardRef;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCardScaleAnimation = ({
|
||||||
|
delay = 0,
|
||||||
|
threshold = 0.1,
|
||||||
|
duration = 0.4,
|
||||||
|
}: {
|
||||||
|
delay?: number;
|
||||||
|
threshold?: number;
|
||||||
|
duration?: number;
|
||||||
|
}) => {
|
||||||
|
const cardRef = useRef<HTMLElement>(null);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const [hasAnimated, setHasAnimated] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cardRef.current || hasAnimated) return;
|
||||||
|
|
||||||
|
const card = cardRef.current;
|
||||||
|
|
||||||
|
// 创建 Intersection Observer
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
entries => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting && !hasAnimated) {
|
||||||
|
setIsVisible(true);
|
||||||
|
setHasAnimated(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
threshold,
|
||||||
|
rootMargin: '0px 0px -50px 0px',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(card);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [threshold, hasAnimated]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cardRef.current || !isVisible) return;
|
||||||
|
|
||||||
|
const card = cardRef.current;
|
||||||
|
|
||||||
|
// 设置初始状态
|
||||||
|
gsap.set(card, {
|
||||||
|
opacity: 0,
|
||||||
|
scale: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建动画
|
||||||
|
const tl = gsap.timeline({ delay });
|
||||||
|
|
||||||
|
tl.to(card, {
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
duration,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
tl.kill();
|
||||||
|
};
|
||||||
|
}, [isVisible, delay]);
|
||||||
|
|
||||||
|
return cardRef;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCardAnimation = ({
|
||||||
|
delay = 0,
|
||||||
|
threshold = 0.1,
|
||||||
|
initial,
|
||||||
|
to,
|
||||||
|
}: {
|
||||||
|
delay?: number;
|
||||||
|
threshold?: number;
|
||||||
|
initial: GSAPTweenVars;
|
||||||
|
to: GSAPTweenVars;
|
||||||
|
}) => {
|
||||||
|
const cardRef = useRef<HTMLElement>(null);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const [hasAnimated, setHasAnimated] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cardRef.current || hasAnimated) return;
|
||||||
|
|
||||||
|
const card = cardRef.current;
|
||||||
|
|
||||||
|
// 创建 Intersection Observer
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
entries => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting && !hasAnimated) {
|
||||||
|
setIsVisible(true);
|
||||||
|
setHasAnimated(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
threshold,
|
||||||
|
rootMargin: '0px 0px -50px 0px',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(card);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [threshold, hasAnimated]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cardRef.current || !isVisible) return;
|
||||||
|
|
||||||
|
const card = cardRef.current;
|
||||||
|
|
||||||
|
// 设置初始状态
|
||||||
|
gsap.set(card, initial);
|
||||||
|
|
||||||
|
// 创建动画
|
||||||
|
const tl = gsap.timeline({ delay });
|
||||||
|
|
||||||
|
tl.to(card, to);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
tl.kill();
|
||||||
|
};
|
||||||
|
}, [isVisible, delay]);
|
||||||
|
|
||||||
|
return cardRef;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { styled, Grid, alpha, Stack, Box } from '@mui/material';
|
import { styled, alpha, Stack, Box } from '@mui/material';
|
||||||
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
||||||
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
||||||
|
|
||||||
|
|
@ -18,8 +18,8 @@ interface ImgTextProps {
|
||||||
const StyledImgTextItem = styled(Stack)(({ theme }) => ({}));
|
const StyledImgTextItem = styled(Stack)(({ theme }) => ({}));
|
||||||
|
|
||||||
export const StyledImgTextItemImg = styled('img')(({ theme }) => ({
|
export const StyledImgTextItemImg = styled('img')(({ theme }) => ({
|
||||||
maxWidth: 350,
|
maxWidth: '100%',
|
||||||
maxHeight: 350,
|
maxHeight: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
objectFit: 'cover',
|
objectFit: 'cover',
|
||||||
|
|
@ -28,7 +28,7 @@ export const StyledImgTextItemImg = styled('img')(({ theme }) => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledImgTextItemTitle = styled('h3')(({ theme }) => ({
|
const StyledImgTextItemTitle = styled('h3')(({ theme }) => ({
|
||||||
fontSize: 20,
|
fontSize: 24,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
}));
|
}));
|
||||||
|
|
@ -49,14 +49,31 @@ const ImgText: React.FC<ImgTextProps> = React.memo(
|
||||||
: { xs: 12, md: 6 };
|
: { xs: 12, md: 6 };
|
||||||
|
|
||||||
const titleRef = useFadeInText(0.2, 0.1);
|
const titleRef = useFadeInText(0.2, 0.1);
|
||||||
const cardRef = useCardAnimation(0.2, 0.1);
|
|
||||||
|
const cardLeftAnimation = useMemo(
|
||||||
|
() => ({
|
||||||
|
initial: { opacity: 0, x: -250 },
|
||||||
|
to: { opacity: 1, x: 0, duration: 0.6, ease: 'power2.out' },
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const cardRightAnimation = useMemo(
|
||||||
|
() => ({
|
||||||
|
initial: { opacity: 0, x: 250 },
|
||||||
|
to: { opacity: 1, x: 0, duration: 0.6, ease: 'power2.out' },
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const cardLeftRef = useCardAnimation(cardLeftAnimation);
|
||||||
|
const cardRightRef = useCardAnimation(cardRightAnimation);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTopicBox>
|
<StyledTopicBox>
|
||||||
<StyledTopicTitle ref={titleRef}>{title}</StyledTopicTitle>
|
<StyledTopicTitle ref={titleRef}>{title}</StyledTopicTitle>
|
||||||
<StyledImgTextItem
|
<StyledImgTextItem
|
||||||
ref={cardRef as React.Ref<HTMLDivElement>}
|
gap={mobile ? 4 : { xs: 4, sm: 6, md: 16 }}
|
||||||
gap={mobile ? 4 : { xs: 4, sm: 6, md: 38 }}
|
|
||||||
direction={
|
direction={
|
||||||
mobile
|
mobile
|
||||||
? 'column-reverse'
|
? 'column-reverse'
|
||||||
|
|
@ -69,12 +86,46 @@ const ImgText: React.FC<ImgTextProps> = React.memo(
|
||||||
justifyContent='center'
|
justifyContent='center'
|
||||||
sx={{ width: '100%' }}
|
sx={{ width: '100%' }}
|
||||||
>
|
>
|
||||||
<Box sx={{ width: '100%' }}>
|
<Box
|
||||||
|
sx={{ width: '100%' }}
|
||||||
|
ref={cardLeftRef as React.Ref<HTMLDivElement>}
|
||||||
|
>
|
||||||
<StyledImgTextItemImg src={item.url} alt={item.name} />
|
<StyledImgTextItemImg src={item.url} alt={item.name} />
|
||||||
</Box>
|
</Box>
|
||||||
<Stack gap={1} sx={{ width: '100%' }}>
|
<Stack
|
||||||
<StyledImgTextItemTitle>{item.name}</StyledImgTextItemTitle>
|
gap={1}
|
||||||
<StyledImgTextItemSummary>{item.desc}</StyledImgTextItemSummary>
|
sx={{ width: '100%' }}
|
||||||
|
ref={cardRightRef as React.Ref<HTMLDivElement>}
|
||||||
|
alignItems={
|
||||||
|
mobile
|
||||||
|
? 'flex-start'
|
||||||
|
: direction === 'row'
|
||||||
|
? 'flex-start'
|
||||||
|
: 'flex-end'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledImgTextItemTitle
|
||||||
|
sx={{
|
||||||
|
textAlign: mobile
|
||||||
|
? 'left'
|
||||||
|
: direction === 'row'
|
||||||
|
? 'left'
|
||||||
|
: 'right',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</StyledImgTextItemTitle>
|
||||||
|
<StyledImgTextItemSummary
|
||||||
|
sx={{
|
||||||
|
textAlign: mobile
|
||||||
|
? 'left'
|
||||||
|
: direction === 'row'
|
||||||
|
? 'left'
|
||||||
|
: 'right',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.desc}
|
||||||
|
</StyledImgTextItemSummary>
|
||||||
</Stack>
|
</Stack>
|
||||||
</StyledImgTextItem>
|
</StyledImgTextItem>
|
||||||
</StyledTopicBox>
|
</StyledTopicBox>
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,14 @@ export { default as Case } from './case';
|
||||||
export { default as ImgText } from './imgText';
|
export { default as ImgText } from './imgText';
|
||||||
export { default as Feature } from './feature';
|
export { default as Feature } from './feature';
|
||||||
export { default as Comment } from './comment';
|
export { default as Comment } from './comment';
|
||||||
|
export { default as Question } from './question';
|
||||||
|
export { default as BlockGrid } from './blockGrid';
|
||||||
|
|
||||||
// 导出动画 hooks
|
// 导出动画 hooks
|
||||||
export {
|
export {
|
||||||
useTextAnimation,
|
useTextAnimation,
|
||||||
useFadeInText,
|
useFadeInText,
|
||||||
useTypewriterText,
|
useTypewriterText,
|
||||||
|
useCardFadeInAnimation,
|
||||||
useCardAnimation,
|
useCardAnimation,
|
||||||
} from './hooks/useGsapAnimation';
|
} from './hooks/useGsapAnimation';
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { styled, Grid, alpha, Stack } from '@mui/material';
|
import { styled, Grid, alpha, Stack } from '@mui/material';
|
||||||
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
||||||
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
import {
|
||||||
|
useFadeInText,
|
||||||
|
useCardFadeInAnimation,
|
||||||
|
} from '../hooks/useGsapAnimation';
|
||||||
|
|
||||||
interface MetricsProps {
|
interface MetricsProps {
|
||||||
mobile?: boolean;
|
mobile?: boolean;
|
||||||
|
|
@ -48,7 +51,7 @@ const MetricsItem: React.FC<{
|
||||||
index: number;
|
index: number;
|
||||||
size: any;
|
size: any;
|
||||||
}> = React.memo(({ item, index, size }) => {
|
}> = React.memo(({ item, index, size }) => {
|
||||||
const cardRef = useCardAnimation(0.2 + index * 0.1, 0.1);
|
const cardRef = useCardFadeInAnimation(0.2 + index * 0.1, 0.1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid size={size} key={index}>
|
<Grid size={size} key={index}>
|
||||||
|
|
@ -56,6 +59,7 @@ const MetricsItem: React.FC<{
|
||||||
ref={cardRef as React.Ref<HTMLDivElement>}
|
ref={cardRef as React.Ref<HTMLDivElement>}
|
||||||
gap={1}
|
gap={1}
|
||||||
alignItems='center'
|
alignItems='center'
|
||||||
|
sx={{ opacity: 0 }}
|
||||||
>
|
>
|
||||||
<StyledMetricsItemNumber className='metrics-item-number'>
|
<StyledMetricsItemNumber className='metrics-item-number'>
|
||||||
{item.number}
|
{item.number}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { styled, Stack, alpha } from '@mui/material';
|
||||||
|
import { StyledTopicBox, StyledTopicTitle } from '../component/styledCommon';
|
||||||
|
import { IconWenhao } from '@panda-wiki/icons';
|
||||||
|
import {
|
||||||
|
useFadeInText,
|
||||||
|
useCardFadeInAnimation,
|
||||||
|
} from '../hooks/useGsapAnimation';
|
||||||
|
|
||||||
|
interface QuestionProps {
|
||||||
|
mobile?: boolean;
|
||||||
|
title?: string;
|
||||||
|
onSearch: (question: string) => void;
|
||||||
|
items?: {
|
||||||
|
question: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledItem = styled('div')(({ theme }) => ({
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
borderRadius: '10px',
|
||||||
|
border: `1px solid ${alpha(theme.palette.text.primary, 0.1)}`,
|
||||||
|
boxShadow: `0px 5px 20px 0px ${alpha(theme.palette.text.primary, 0.06)}`,
|
||||||
|
padding: theme.spacing(3, 4),
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'translateY(-5px)',
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
border: `1px solid ${alpha(theme.palette.primary.main, 0.5)}`,
|
||||||
|
boxShadow: `0px 10px 20px 0px ${alpha(theme.palette.text.primary, 0.1)}`,
|
||||||
|
},
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
cursor: 'pointer',
|
||||||
|
opacity: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledItemTitle = styled('span')(({ theme }) => ({
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 400,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 单个卡片组件,带动画效果
|
||||||
|
const Item: React.FC<{
|
||||||
|
item: {
|
||||||
|
question: string;
|
||||||
|
};
|
||||||
|
onSearch: (question: string) => void;
|
||||||
|
index: number;
|
||||||
|
}> = React.memo(({ item, index, onSearch }) => {
|
||||||
|
const cardRef = useCardFadeInAnimation(0.2 + index * 0.1, 0.1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledItem
|
||||||
|
ref={cardRef as React.Ref<HTMLDivElement>}
|
||||||
|
onClick={() => onSearch(item.question)}
|
||||||
|
>
|
||||||
|
<IconWenhao sx={{ color: 'primary.main', fontSize: 20 }} />
|
||||||
|
<StyledItemTitle>{item.question}</StyledItemTitle>
|
||||||
|
</StyledItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const Question: React.FC<QuestionProps> = React.memo(
|
||||||
|
({ title, items = [], onSearch }) => {
|
||||||
|
// 添加标题淡入动画
|
||||||
|
const titleRef = useFadeInText(0.2, 0.1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledTopicBox>
|
||||||
|
<StyledTopicTitle ref={titleRef}>{title}</StyledTopicTitle>
|
||||||
|
<Stack gap={3} sx={{ width: '100%' }}>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<Item key={index} item={item} index={index} onSearch={onSearch} />
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</StyledTopicBox>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Question;
|
||||||
|
|
@ -11,7 +11,10 @@ import {
|
||||||
} from '../component/styledCommon';
|
} from '../component/styledCommon';
|
||||||
import IconWenjian from '@panda-wiki/icons/IconWenjian';
|
import IconWenjian from '@panda-wiki/icons/IconWenjian';
|
||||||
import ArrowForwardIosRoundedIcon from '@mui/icons-material/ArrowForwardIosRounded';
|
import ArrowForwardIosRoundedIcon from '@mui/icons-material/ArrowForwardIosRounded';
|
||||||
import { useFadeInText, useCardAnimation } from '../hooks/useGsapAnimation';
|
import {
|
||||||
|
useFadeInText,
|
||||||
|
useCardFadeInAnimation,
|
||||||
|
} from '../hooks/useGsapAnimation';
|
||||||
|
|
||||||
interface SimpleDocProps {
|
interface SimpleDocProps {
|
||||||
mobile?: boolean;
|
mobile?: boolean;
|
||||||
|
|
@ -62,7 +65,7 @@ const SimpleDocItem: React.FC<{
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
size: any;
|
size: any;
|
||||||
}> = React.memo(({ item, index, baseUrl, size }) => {
|
}> = React.memo(({ item, index, baseUrl, size }) => {
|
||||||
const cardRef = useCardAnimation(0.2 + index * 0.1, 0.1);
|
const cardRef = useCardFadeInAnimation(0.2 + index * 0.1, 0.1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid size={size} key={index}>
|
<Grid size={size} key={index}>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
export const decodeBase64 = (text: string) => {
|
||||||
|
try {
|
||||||
|
const buff = Buffer.from(text, 'base64');
|
||||||
|
return buff.toString('utf-8');
|
||||||
|
} catch (e) {
|
||||||
|
// 客户端如果报错,退回到 atob
|
||||||
|
if (typeof window !== 'undefined' && window.atob) {
|
||||||
|
return window.atob(text);
|
||||||
|
}
|
||||||
|
// 处理解码失败的情况
|
||||||
|
console.error('Base64 decoding failed:', e);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -4,7 +4,8 @@ import { Box, Divider, Stack, Link, alpha } from '@mui/material';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { IconDianhua, IconWeixingongzhonghao } from '@panda-wiki/icons';
|
import { IconDianhua, IconWeixingongzhonghao } from '@panda-wiki/icons';
|
||||||
import Overlay from './Overlay';
|
import Overlay from './Overlay';
|
||||||
import { DocWidth } from '../constants';
|
import { decodeBase64 } from '../utils';
|
||||||
|
import { PROJECT_NAME } from '../constants';
|
||||||
|
|
||||||
interface DomainSocialMediaAccount {
|
interface DomainSocialMediaAccount {
|
||||||
channel?: string;
|
channel?: string;
|
||||||
|
|
@ -343,7 +344,7 @@ const Footer = React.memo(
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>本网站由 PandaWiki 提供技术支持</Box>
|
<Box>{decodeBase64(PROJECT_NAME)}</Box>
|
||||||
<img src={logo} alt='PandaWiki' width={16} height={16} />
|
<img src={logo} alt='PandaWiki' width={16} height={16} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -773,7 +774,7 @@ const Footer = React.memo(
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>本网站由 PandaWiki 提供技术支持</Box>
|
<Box>{decodeBase64(PROJECT_NAME)}</Box>
|
||||||
<img src={logo} alt='PandaWiki' width={0} />
|
<img src={logo} alt='PandaWiki' width={0} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue