Skip to content

Commit 7100c53

Browse files
committed
add chart block
1 parent 89468c4 commit 7100c53

File tree

8 files changed

+309
-7
lines changed

8 files changed

+309
-7
lines changed

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44

55
A personal research project forked from the Payload CMS [website](https://github.com/payloadcms/website). Based on the official example, the following features have been added:
66

7-
- Email sending by SES
8-
- Subscription API
9-
- Notes recording
10-
- Scheduled database backups
11-
- File upload to R2/S3
7+
- Integrated Amazon SES for email delivery (Admin Panel).
8+
- API subscription (Admin Panel).
9+
- Note-taking and content recording support (Admin Panel).
10+
- Scheduled database backups (Admin Panel).
11+
- File and media uploads to Cloudflare R2 or AWS S3 (Admin Panel).
12+
- Built-in chart block for visual data analysis and presentation.
1213

1314
## Develop
1415

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react'
2+
import { Chart, ChartProps } from './charts'
3+
4+
type Props = ChartProps & {
5+
className?: string
6+
}
7+
8+
export const ChartBlock: React.FC<Props> = ({ className, ...restProps }) => {
9+
return (
10+
<div className={[className, 'not-prose'].filter(Boolean).join(' ')}>
11+
<Chart {...restProps} />
12+
</div>
13+
)
14+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
'use client'
2+
3+
import React from 'react'
4+
5+
import { ComposedChart, XAxis, YAxis, CartesianGrid, Area, Bar, Line } from 'recharts'
6+
import {
7+
ChartContainer,
8+
ChartLegend,
9+
ChartLegendContent,
10+
ChartTooltip,
11+
ChartTooltipContent,
12+
} from '@/components/ui/chart'
13+
14+
type SeriesItem = {
15+
key: string
16+
label: string
17+
type: 'line' | 'bar' | 'area'
18+
color?: string | undefined
19+
}
20+
21+
type SeriesConfigMap = Record<string, Partial<SeriesItem>>
22+
23+
export type ComposedChartProps = {
24+
title: string
25+
description?: string
26+
xAxisKey: string
27+
series: SeriesItem[]
28+
config?: SeriesConfigMap
29+
dataset: []
30+
}
31+
32+
const Chart: React.FC<ComposedChartProps> = (props) => {
33+
const { dataset = [], xAxisKey, series = [], config = {} } = props
34+
35+
const seriesConfig = series.reduce<Record<string, SeriesItem>>((prev, curr) => {
36+
const { key } = curr
37+
prev[key] = curr
38+
return prev
39+
}, {})
40+
41+
const chartConfig: Record<string, SeriesItem> = {}
42+
for (const key in seriesConfig) {
43+
if (!Object.hasOwn(seriesConfig, key)) continue
44+
const element = seriesConfig[key]
45+
chartConfig[key] = {
46+
...element,
47+
...config?.[key],
48+
}
49+
}
50+
51+
const renderSeries = (item: SeriesItem, idx: number) => {
52+
const { type, key, label } = item
53+
const color = chartConfig[key]?.color || `var(--chart-${idx + 1})`
54+
55+
switch (type) {
56+
case 'line':
57+
return <Line dataKey={key} label={label} fill={color} stroke={color} type="monotone" />
58+
case 'bar':
59+
return <Bar dataKey={key} label={label} fill={color} stroke={color} radius={4} />
60+
case 'area':
61+
return <Area dataKey={key} label={label} fill={color} stroke={color} type="monotone" />
62+
63+
default:
64+
break
65+
}
66+
67+
return null
68+
}
69+
70+
return (
71+
<ChartContainer config={chartConfig} className="min-h-[200px] w-full">
72+
<ComposedChart data={dataset}>
73+
<CartesianGrid vertical={false} stroke="#f5f5f5" />
74+
<XAxis dataKey={xAxisKey} />
75+
<YAxis />
76+
<ChartTooltip content={<ChartTooltipContent />} />
77+
<ChartLegend content={<ChartLegendContent />} />
78+
{series.map((item, idx) => renderSeries(item, idx))}
79+
</ComposedChart>
80+
</ChartContainer>
81+
)
82+
}
83+
84+
export default Chart
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use client'
2+
3+
import React from 'react'
4+
import ComposedChart, { ComposedChartProps } from './ComposedChart'
5+
6+
export type ChartProps =
7+
| ({ type: 'composed' } & ComposedChartProps)
8+
| { type: 'line' | 'bar' | 'area' | 'pie' }
9+
10+
export const Chart: React.FC<ChartProps> = (props) => {
11+
switch (props.type) {
12+
case 'composed':
13+
return <ComposedChart {...props} />
14+
}
15+
return null
16+
}

src/blocks/ChartBlock/config.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import type { Block } from 'payload'
2+
3+
export const ChartBlock = (options?: { slug?: string; label?: string }): Block => {
4+
return {
5+
slug: options?.slug ?? 'chart',
6+
interfaceName: 'ChartBlock',
7+
fields: [
8+
{
9+
type: 'row',
10+
fields: [
11+
{
12+
name: 'title',
13+
type: 'text',
14+
label: 'Chart Title',
15+
required: true,
16+
admin: { width: '50%' },
17+
},
18+
{
19+
name: 'description',
20+
type: 'text',
21+
label: 'Description',
22+
admin: { width: '50%' },
23+
},
24+
],
25+
},
26+
{
27+
name: 'type',
28+
label: 'Chart Type',
29+
type: 'select',
30+
required: true,
31+
options: [
32+
{ label: 'Line', value: 'line' },
33+
{ label: 'Bar', value: 'bar' },
34+
{ label: 'Area', value: 'area' },
35+
{ label: 'Pie', value: 'pie' },
36+
{ label: 'Composed', value: 'composed' },
37+
],
38+
defaultValue: 'line',
39+
},
40+
41+
// Dataset JSON
42+
{
43+
name: 'dataset',
44+
label: 'Dataset (JSON)',
45+
type: 'json',
46+
required: true,
47+
admin: {
48+
description: '数组对象格式,例如: [{ "time": "1:51", "Seoul": 97, "Tokyo": 86 }]',
49+
},
50+
validate: (value) => {
51+
if (!Array.isArray(value)) return 'Dataset 必须是数组'
52+
if (value.length === 0) return 'Dataset 不能为空'
53+
return true
54+
},
55+
},
56+
57+
{
58+
name: 'xAxisKey',
59+
type: 'text',
60+
label: 'X轴字段名',
61+
required: true,
62+
},
63+
{
64+
name: 'series',
65+
label: 'Series 配置',
66+
type: 'array',
67+
minRows: 1,
68+
fields: [
69+
{
70+
name: 'key',
71+
type: 'text',
72+
label: '字段名',
73+
required: true,
74+
},
75+
{
76+
name: 'label',
77+
type: 'text',
78+
label: '显示名称',
79+
},
80+
{
81+
name: 'type',
82+
type: 'select',
83+
label: 'Series 类型',
84+
options: [
85+
{ label: 'Line', value: 'line' },
86+
{ label: 'Bar', value: 'bar' },
87+
{ label: 'Area', value: 'area' },
88+
],
89+
defaultValue: 'line',
90+
},
91+
{
92+
name: 'yAxis',
93+
type: 'select',
94+
label: 'Y轴位置',
95+
options: [
96+
{ label: 'Left', value: 'left' },
97+
{ label: 'Right', value: 'right' },
98+
],
99+
defaultValue: 'left',
100+
},
101+
{
102+
name: 'color',
103+
type: 'text',
104+
label: '颜色',
105+
admin: { description: '可填十六进制或 color 名称' },
106+
},
107+
],
108+
},
109+
110+
// 高级 Chart 配置
111+
{
112+
name: 'config',
113+
label: 'Advanced Chart Options (JSON)',
114+
type: 'json',
115+
admin: {
116+
description: '透传给图表库的原生配置项',
117+
},
118+
},
119+
120+
// 数据来源
121+
{
122+
name: 'dataSource',
123+
type: 'text',
124+
label: 'Data Source',
125+
defaultValue: 'xc2f.com',
126+
},
127+
],
128+
}
129+
}

src/collections/Posts/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { authenticatedOrPublished } from '../../access/authenticatedOrPublished'
1414
import { Banner } from '../../blocks/Banner/config'
1515
import { Code } from '../../blocks/Code/config'
1616
import { MediaBlock } from '../../blocks/MediaBlock/config'
17+
import { ChartBlock } from '../../blocks/ChartBlock/config'
1718
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
1819
import { populateAuthors } from './hooks/populateAuthors'
1920
import { revalidateDelete, revalidatePost } from './hooks/revalidatePost'
@@ -91,7 +92,9 @@ export const Posts: CollectionConfig<'posts'> = {
9192
return [
9293
...rootFeatures,
9394
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
94-
BlocksFeature({ blocks: [Banner, Code, MediaBlock] }),
95+
BlocksFeature({
96+
blocks: [Banner, Code, MediaBlock, ChartBlock()],
97+
}),
9598
FixedToolbarFeature(),
9699
InlineToolbarFeature(),
97100
HorizontalRuleFeature(),

src/components/RichText/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from '@payloadcms/richtext-lexical/react'
1313

1414
import { CodeBlock, CodeBlockProps } from '@/blocks/Code/Component'
15+
import { ChartBlock, ChartBlockProps } from '@/blocks/ChartBlock/Component'
1516

1617
import type {
1718
BannerBlock as BannerBlockProps,
@@ -24,7 +25,9 @@ import { cn } from '@/utilities/ui'
2425

2526
type NodeTypes =
2627
| DefaultNodeTypes
27-
| SerializedBlockNode<CTABlockProps | MediaBlockProps | BannerBlockProps | CodeBlockProps>
28+
| SerializedBlockNode<
29+
CTABlockProps | MediaBlockProps | BannerBlockProps | CodeBlockProps | ChartBlockProps
30+
>
2831

2932
const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => {
3033
const { value, relationTo } = linkNode.fields.doc!
@@ -52,6 +55,7 @@ const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters })
5255
),
5356
code: ({ node }) => <CodeBlock className="col-start-2" {...node.fields} />,
5457
cta: ({ node }) => <CallToActionBlock {...node.fields} />,
58+
chart: ({ node }) => <ChartBlock {...node.fields} />,
5559
},
5660
})
5761

src/payload-types.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2039,6 +2039,57 @@ export interface BannerBlock {
20392039
blockName?: string | null;
20402040
blockType: 'banner';
20412041
}
2042+
/**
2043+
* This interface was referenced by `Config`'s JSON-Schema
2044+
* via the `definition` "ChartBlock".
2045+
*/
2046+
export interface ChartBlock {
2047+
title: string;
2048+
description?: string | null;
2049+
type: 'line' | 'bar' | 'area' | 'pie' | 'composed';
2050+
/**
2051+
* 数组对象格式,例如: [{ "time": "1:51", "Seoul": 97, "Tokyo": 86 }]
2052+
*/
2053+
dataset:
2054+
| {
2055+
[k: string]: unknown;
2056+
}
2057+
| unknown[]
2058+
| string
2059+
| number
2060+
| boolean
2061+
| null;
2062+
xAxisKey: string;
2063+
series?:
2064+
| {
2065+
key: string;
2066+
label?: string | null;
2067+
type?: ('line' | 'bar' | 'area') | null;
2068+
yAxis?: ('left' | 'right') | null;
2069+
/**
2070+
* 可填十六进制或 color 名称
2071+
*/
2072+
color?: string | null;
2073+
id?: string | null;
2074+
}[]
2075+
| null;
2076+
/**
2077+
* 透传给图表库的原生配置项
2078+
*/
2079+
config?:
2080+
| {
2081+
[k: string]: unknown;
2082+
}
2083+
| unknown[]
2084+
| string
2085+
| number
2086+
| boolean
2087+
| null;
2088+
dataSource?: string | null;
2089+
id?: string | null;
2090+
blockName?: string | null;
2091+
blockType: 'chart';
2092+
}
20422093
/**
20432094
* This interface was referenced by `Config`'s JSON-Schema
20442095
* via the `definition` "auth".

0 commit comments

Comments
 (0)