作者 202304001

echart图表

1 import chatStyles from "@/app/components/chat.module.scss"; 1 import chatStyles from "@/app/components/chat.module.scss";
2 import homeStyles from "@/app/components/home.module.scss"; 2 import homeStyles from "@/app/components/home.module.scss";
  3 +import styles from "./bgRemoval.module.scss";
3 4
4 import { WindowContent } from "@/app/components/home"; 5 import { WindowContent } from "@/app/components/home";
5 import { useMobileScreen } from "@/app/utils"; 6 import { useMobileScreen } from "@/app/utils";
@@ -12,14 +13,17 @@ import { getClientConfig } from "@/app/config/client"; @@ -12,14 +13,17 @@ import { getClientConfig } from "@/app/config/client";
12 import React, { useEffect, useMemo, useRef, useState } from "react"; 13 import React, { useEffect, useMemo, useRef, useState } from "react";
13 import { useAppConfig } from "@/app/store"; 14 import { useAppConfig } from "@/app/store";
14 import { BgSiderBar } from "./bg-siderBar"; 15 import { BgSiderBar } from "./bg-siderBar";
15 -import { message } from "antd"; 16 +import { Button, Flex, Upload, message } from "antd";
  17 +import { UploadOutlined } from "@ant-design/icons";
16 import type { UploadFile } from "antd"; 18 import type { UploadFile } from "antd";
17 19
18 import ReturnIcon from "@/app/icons/return.svg"; 20 import ReturnIcon from "@/app/icons/return.svg";
19 import MinIcon from "@/app/icons/min.svg"; 21 import MinIcon from "@/app/icons/min.svg";
20 import MaxIcon from "@/app/icons/max.svg"; 22 import MaxIcon from "@/app/icons/max.svg";
21 import SDIcon from "@/app/icons/sd.svg"; 23 import SDIcon from "@/app/icons/sd.svg";
22 -import { ChartComponent } from "../chart"; 24 +import LoadingIcon from "@/app/icons/three-dots.svg";
  25 +import BotIcon from "@/app/icons/bot.svg";
  26 +import CloseIcon from "@/app/icons/close.svg";
23 27
24 export function BgRemoval() { 28 export function BgRemoval() {
25 const isMobileScreen = useMobileScreen(); 29 const isMobileScreen = useMobileScreen();
@@ -117,16 +121,31 @@ export function BgRemoval() { @@ -117,16 +121,31 @@ export function BgRemoval() {
117 </div> 121 </div>
118 </div> 122 </div>
119 <div className={chatStyles["chat-body"]} ref={scrollRef}> 123 <div className={chatStyles["chat-body"]} ref={scrollRef}>
120 - {/* <Flex vertical justify='center' align="center" gap="middle" className={styles['panelFlex']}> 124 + <Flex
  125 + vertical
  126 + justify="center"
  127 + align="center"
  128 + gap="middle"
  129 + className={styles["panelFlex"]}
  130 + >
121 {isLoading ? ( 131 {isLoading ? (
122 <div className={clsx("no-dark", styles["loading-content"])}> 132 <div className={clsx("no-dark", styles["loading-content"])}>
123 <BotIcon /> 133 <BotIcon />
124 - <LoadingIcon/> 134 + <LoadingIcon />
125 </div> 135 </div>
126 ) : previewUrl ? ( 136 ) : previewUrl ? (
127 - <div className={styles['preview']}>  
128 - <img src={previewUrl} alt="Preview" className={styles['previewImage']} />  
129 - <IconButton icon={<CloseIcon />} bordered onClick={closePic} className={styles['icon']} /> 137 + <div className={styles["preview"]}>
  138 + <img
  139 + src={previewUrl}
  140 + alt="Preview"
  141 + className={styles["previewImage"]}
  142 + />
  143 + <IconButton
  144 + icon={<CloseIcon />}
  145 + bordered
  146 + onClick={closePic}
  147 + className={styles["icon"]}
  148 + />
130 </div> 149 </div>
131 ) : ( 150 ) : (
132 <Upload 151 <Upload
@@ -138,13 +157,12 @@ export function BgRemoval() { @@ -138,13 +157,12 @@ export function BgRemoval() {
138 showUploadList={false} 157 showUploadList={false}
139 accept="image/*" 158 accept="image/*"
140 > 159 >
141 - <Button icon={<UploadOutlined />} size='large'> 160 + <Button icon={<UploadOutlined />} size="large">
142 上传图片 161 上传图片
143 </Button> 162 </Button>
144 </Upload> 163 </Upload>
145 )} 164 )}
146 - </Flex> */}  
147 - <ChartComponent /> 165 + </Flex>
148 </div> 166 </div>
149 </div> 167 </div>
150 </WindowContent> 168 </WindowContent>
1 import { Card } from "antd"; 1 import { Card } from "antd";
2 import * as echarts from "echarts/core"; 2 import * as echarts from "echarts/core";
3 -import { GridComponent, GridComponentOption } from "echarts/components";  
4 -import { BarChart, BarSeriesOption } from "echarts/charts"; 3 +import {
  4 + GridComponent,
  5 + TooltipComponent,
  6 + LegendComponent,
  7 + TitleComponent,
  8 +} from "echarts/components";
  9 +import {
  10 + BarChart,
  11 + PieChart,
  12 + LineChart,
  13 + BarSeriesOption,
  14 + PieSeriesOption,
  15 + LineSeriesOption,
  16 +} from "echarts/charts";
5 import { CanvasRenderer } from "echarts/renderers"; 17 import { CanvasRenderer } from "echarts/renderers";
6 -import { useRef, useState } from "react";  
7 -const tabList = [  
8 - {  
9 - key: "bar",  
10 - label: "柱状图",  
11 - },  
12 - {  
13 - key: "pie",  
14 - label: "饼图",  
15 - },  
16 - {  
17 - key: "line",  
18 - label: "折线图",  
19 - },  
20 -]; 18 +import { useRef, useEffect, useState } from "react";
  19 +import { ChartComponentProps, ChartData } from "./getChartData";
  20 +
  21 +// 注册必要的组件
  22 +echarts.use([
  23 + GridComponent,
  24 + TooltipComponent,
  25 + LegendComponent,
  26 + TitleComponent,
  27 + BarChart,
  28 + PieChart,
  29 + LineChart,
  30 + CanvasRenderer,
  31 +]);
21 32
22 -echarts.use([GridComponent, BarChart, CanvasRenderer]);  
23 type EChartsOption = echarts.ComposeOption< 33 type EChartsOption = echarts.ComposeOption<
24 - GridComponentOption | BarSeriesOption 34 + BarSeriesOption | PieSeriesOption | LineSeriesOption
25 >; 35 >;
26 36
27 -var option: EChartsOption; 37 +const tabList = [
  38 + { key: "bar", label: "柱状图" },
  39 + { key: "pie", label: "饼图" },
  40 + { key: "line", label: "折线图" },
  41 +];
28 42
29 -option = {  
30 - xAxis: {  
31 - type: "category",  
32 - data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],  
33 - },  
34 - yAxis: {  
35 - type: "value", 43 +// 图表配置生成器
  44 +const getOption = (type: string, data: ChartData): EChartsOption => {
  45 + const commonOption = {
  46 + title: { text: `${tabList.find((t) => t.key === type)?.label}` },
  47 + tooltip: { trigger: "item" },
  48 + };
  49 +
  50 + // 提取数值数组
  51 + const values = data.values.map((item) => item.value);
  52 +
  53 + switch (type) {
  54 + case "bar":
  55 + return {
  56 + ...commonOption,
  57 + xAxis: { type: "category", data: data.categories },
  58 + yAxis: { type: "value" },
  59 + series: [{ data: values, type: "bar" }],
  60 + };
  61 + case "pie":
  62 + return {
  63 + ...commonOption,
  64 + series: [
  65 + {
  66 + type: "pie",
  67 + data: data.categories.map((name, i) => ({
  68 + name,
  69 + value: values[i],
  70 + })),
  71 + radius: "50%",
36 }, 72 },
  73 + ],
  74 + };
  75 + case "line":
  76 + return {
  77 + ...commonOption,
  78 + xAxis: { type: "category", data: data.categories },
  79 + yAxis: { type: "value" },
37 series: [ 80 series: [
38 { 81 {
39 - data: [120, 200, 150, 80, 70, 110, 130],  
40 - type: "bar", 82 + data: values,
  83 + type: "line",
  84 + smooth: true,
  85 + areaStyle: {},
41 }, 86 },
42 ], 87 ],
  88 + };
  89 + default:
  90 + return {};
  91 + }
43 }; 92 };
44 -export function ChartComponent() {  
45 - const [activeTabKey, setActiveTabKey] = useState<string>("bar");  
46 - const line = useRef<HTMLCanvasElement>(null);  
47 - const bar = useRef<HTMLCanvasElement>(null);  
48 - const pie = useRef<HTMLCanvasElement>(null);  
49 - const contentListNoTitle: Record<string, React.ReactNode> = {  
50 - bar: <canvas ref={bar}></canvas>,  
51 - pie: <canvas ref={pie}></canvas>,  
52 - line: <canvas ref={line}></canvas>, 93 +
  94 +export function ChartComponent({
  95 + data = { categories: [], values: [] },
  96 +}: ChartComponentProps) {
  97 + const [activeTabKey, setActiveTabKey] = useState("bar");
  98 + const chartRef = useRef<HTMLDivElement>(null);
  99 + const chartInstance = useRef<echarts.ECharts | null>(null);
  100 +
  101 + useEffect(() => {
  102 + if (!chartRef.current) return;
  103 +
  104 + // 数据校验
  105 + if (
  106 + !data?.categories?.length ||
  107 + !data?.values?.length ||
  108 + data.values.some((item) => typeof item.value !== "number")
  109 + ) {
  110 + console.warn("Invalid chart data");
  111 + return;
  112 + }
  113 +
  114 + // 销毁旧实例
  115 + if (chartInstance.current) {
  116 + chartInstance.current.dispose();
  117 + }
  118 +
  119 + // 初始化新图表
  120 + chartInstance.current = echarts.init(chartRef.current);
  121 + chartInstance.current.setOption(getOption(activeTabKey, data));
  122 +
  123 + // 窗口resize监听
  124 + const resizeHandler = () => chartInstance.current?.resize();
  125 + window.addEventListener("resize", resizeHandler);
  126 +
  127 + // 清理函数
  128 + return () => {
  129 + window.removeEventListener("resize", resizeHandler);
  130 + chartInstance.current?.dispose();
53 }; 131 };
54 - // var myChart = echarts.init(bar); 132 + }, [activeTabKey, data]);
  133 +
55 return ( 134 return (
56 - <>  
57 <Card 135 <Card
58 - style={{ width: "100%" }} 136 + style={{ width: "100%", minHeight: 400 }}
59 tabList={tabList} 137 tabList={tabList}
60 activeTabKey={activeTabKey} 138 activeTabKey={activeTabKey}
61 onTabChange={setActiveTabKey} 139 onTabChange={setActiveTabKey}
62 tabProps={{ size: "middle" }} 140 tabProps={{ size: "middle" }}
63 > 141 >
64 - {contentListNoTitle[activeTabKey]} 142 + <div
  143 + ref={chartRef}
  144 + style={{
  145 + width: "100%",
  146 + height: 400,
  147 + minHeight: 400,
  148 + }}
  149 + />
65 </Card> 150 </Card>
66 - </>  
67 ); 151 );
68 } 152 }
  1 +export interface ValueItem {
  2 + value: number;
  3 + itemStyle?: {
  4 + color: string;
  5 + };
  6 +}
  7 +
  8 +export interface ChartData {
  9 + categories: string[];
  10 + values: ValueItem[];
  11 +}
  12 +export interface ChartComponentProps {
  13 + data: {
  14 + categories: string[];
  15 + values: ValueItem[];
  16 + };
  17 +}
  18 +
  19 +export function extractDataFromText(text: string): ChartComponentProps | null {
  20 + const startMarkers = ["```javascript", "```json"];
  21 + let dataStartIndex = -1;
  22 + let markerLength = 0;
  23 +
  24 + // 查找有效的数据块起始标记
  25 + for (const marker of startMarkers) {
  26 + const index = text.indexOf(marker);
  27 + if (index !== -1) {
  28 + dataStartIndex = index;
  29 + markerLength = marker.length;
  30 + break;
  31 + }
  32 + }
  33 +
  34 + let parsedData: any = null;
  35 +
  36 + // 如果找到有效数据块
  37 + if (dataStartIndex !== -1) {
  38 + // 查找数据块结束标记
  39 + const dataEndIndex = text.indexOf("```", dataStartIndex + markerLength);
  40 + if (dataEndIndex !== -1) {
  41 + // 提取并清理数据块内容
  42 + const dataBlock = text
  43 + .slice(dataStartIndex + markerLength, dataEndIndex)
  44 + .trim()
  45 + .replace(/'/g, '"') // 转换单引号为双引号
  46 + .replace(/(\w+)(?=\s*:)/g, '"$1"'); // 为键添加双引号
  47 +
  48 + try {
  49 + // 尝试安全解析数据块内容
  50 + parsedData = JSON.parse(dataBlock);
  51 + } catch (error) {
  52 + return null;
  53 + }
  54 + }
  55 + }
  56 +
  57 + // 如果没有找到数据块,尝试将整个文本解析为 ChartData
  58 + if (!parsedData) {
  59 + try {
  60 + const potentialData: any = JSON.parse(text);
  61 + // 验证解析后的数据是否符合 ChartData 结构
  62 + if (
  63 + potentialData &&
  64 + Array.isArray(potentialData.categories) &&
  65 + potentialData.categories.every((c: any) => typeof c === "string") &&
  66 + Array.isArray(potentialData.values) &&
  67 + potentialData.values.every(
  68 + (v: any) =>
  69 + typeof v?.value === "number" &&
  70 + v?.itemStyle &&
  71 + typeof v.itemStyle?.color === "string",
  72 + )
  73 + ) {
  74 + parsedData = potentialData;
  75 + }
  76 + } catch (error) {
  77 + return null; // 如果解析失败,返回 null
  78 + }
  79 + }
  80 +
  81 + // 如果成功解析了数据并且符合 ChartData 类型,返回
  82 + if (
  83 + parsedData &&
  84 + Array.isArray(parsedData.categories) &&
  85 + Array.isArray(parsedData.values) &&
  86 + parsedData.values.every(
  87 + (v: any) =>
  88 + typeof v?.value === "number" &&
  89 + v?.itemStyle &&
  90 + typeof v.itemStyle?.color === "string",
  91 + )
  92 + ) {
  93 + return { data: parsedData as ChartData };
  94 + }
  95 +
  96 + return null; // 如果未满足条件,则返回 null
  97 +}
@@ -25,6 +25,9 @@ import { IconButton } from "./button"; @@ -25,6 +25,9 @@ import { IconButton } from "./button";
25 import { useAppConfig } from "../store/config"; 25 import { useAppConfig } from "../store/config";
26 import clsx from "clsx"; 26 import clsx from "clsx";
27 27
  28 +import { ChartComponentProps, extractDataFromText } from "./chart/getChartData";
  29 +import { ChartComponent } from "./chart";
  30 +
28 export function Mermaid(props: { code: string }) { 31 export function Mermaid(props: { code: string }) {
29 const ref = useRef<HTMLDivElement>(null); 32 const ref = useRef<HTMLDivElement>(null);
30 const [hasError, setHasError] = useState(false); 33 const [hasError, setHasError] = useState(false);
@@ -79,6 +82,7 @@ export function PreCode(props: { children: any }) { @@ -79,6 +82,7 @@ export function PreCode(props: { children: any }) {
79 const { height } = useWindowSize(); 82 const { height } = useWindowSize();
80 const chatStore = useChatStore(); 83 const chatStore = useChatStore();
81 const session = chatStore.currentSession(); 84 const session = chatStore.currentSession();
  85 + const [chartData, setChartData] = useState<ChartComponentProps | null>(null);
82 86
83 const renderArtifacts = useDebouncedCallback(() => { 87 const renderArtifacts = useDebouncedCallback(() => {
84 if (!ref.current) return; 88 if (!ref.current) return;
@@ -102,10 +106,20 @@ export function PreCode(props: { children: any }) { @@ -102,10 +106,20 @@ export function PreCode(props: { children: any }) {
102 const config = useAppConfig(); 106 const config = useAppConfig();
103 const enableArtifacts = 107 const enableArtifacts =
104 session.mask?.enableArtifacts !== false && config.enableArtifacts; 108 session.mask?.enableArtifacts !== false && config.enableArtifacts;
105 -  
106 //Wrap the paragraph for plain-text 109 //Wrap the paragraph for plain-text
107 useEffect(() => { 110 useEffect(() => {
108 if (ref.current) { 111 if (ref.current) {
  112 + const textContent = ref.current.innerText;
  113 + if (textContent) {
  114 + console.log("有textContent");
  115 + console.log(textContent);
  116 + const data = extractDataFromText(textContent);
  117 + console.log(data);
  118 + if (data) {
  119 + console.log("data");
  120 + setChartData(data);
  121 + }
  122 + }
109 const codeElements = ref.current.querySelectorAll( 123 const codeElements = ref.current.querySelectorAll(
110 "code", 124 "code",
111 ) as NodeListOf<HTMLElement>; 125 ) as NodeListOf<HTMLElement>;
@@ -156,6 +170,7 @@ export function PreCode(props: { children: any }) { @@ -156,6 +170,7 @@ export function PreCode(props: { children: any }) {
156 /> 170 />
157 </FullScreen> 171 </FullScreen>
158 )} 172 )}
  173 + {chartData && <ChartComponent data={chartData.data} />}
159 <pre ref={ref}> 174 <pre ref={ref}>
160 <span 175 <span
161 className="copy-code-button" 176 className="copy-code-button"
@@ -271,7 +286,6 @@ function _MarkDownContent(props: { content: string }) { @@ -271,7 +286,6 @@ function _MarkDownContent(props: { content: string }) {
271 const escapedContent = useMemo(() => { 286 const escapedContent = useMemo(() => {
272 return tryWrapHtmlCode(escapeBrackets(props.content)); 287 return tryWrapHtmlCode(escapeBrackets(props.content));
273 }, [props.content]); 288 }, [props.content]);
274 -  
275 return ( 289 return (
276 <ReactMarkdown 290 <ReactMarkdown
277 remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]} 291 remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}