1use crate::request::{ContentType, Method, Request};
2use crate::tables::Tables;
3use crate::{ApiResponse, Tools};
4use crate::{CONFIG, GLOBAL_DATA, PLUGIN_TOOLS};
5use br_fields::Field;
6use json::{object, JsonValue};
7use std::any::type_name;
8
9pub trait Action {
11 fn _name(&self) -> String {
13 type_name::<Self>()
14 .rsplit("::")
15 .next()
16 .unwrap_or_default()
17 .to_lowercase()
18 }
19 fn module_name(&self) -> String {
21 let t = type_name::<Self>().split("::").collect::<Vec<&str>>();
22 let plugin = t[2].to_lowercase();
23 let module = t[3].to_lowercase();
24 format!("{plugin}.{module}")
25 }
26 fn addon_name(&self) -> String {
28 let t = type_name::<Self>().split("::").collect::<Vec<&str>>();
29 t[2].to_lowercase()
30 }
31 fn api(&self) -> String {
33 let t = type_name::<Self>().split("::").collect::<Vec<&str>>();
34 let plugin = t[2].to_lowercase();
35 let module = t[3].to_lowercase();
36 let action = t[4].to_lowercase();
37 format!("{plugin}.{module}.{action}")
38 }
39 fn token(&self) -> bool {
41 true
42 }
43
44 fn sort(&self) -> usize {
45 99
46 }
47 fn group_name(&self) -> &'static str {
49 ""
50 }
51 fn title(&self) -> &'static str;
53 fn description(&self) -> &'static str {
55 ""
56 }
57 fn path(&self) -> &'static str {
59 ""
60 }
61 fn query(&self) -> JsonValue {
63 object! {}
64 }
65 fn tags(&self) -> &'static [&'static str] {
67 &[]
68 }
69 fn icon(&self) -> &'static str {
70 ""
71 }
72 fn public(&self) -> bool {
74 true
75 }
76 fn auth(&self) -> bool {
78 true
79 }
80 fn review(&self) -> bool {
82 false
83 }
84 fn review_fun(&self, _request: Request) -> Result<JsonValue, String> {
86 Err("请实现review_fun".to_string())
87 }
88 fn interface_type(&self) -> InterfaceType {
90 InterfaceType::Api
91 }
92 fn method(&mut self) -> Method {
94 Method::Post
95 }
96 fn content_type(&mut self) -> ContentType {
98 ContentType::Json
99 }
100 fn params_check(&mut self) -> bool {
102 true
103 }
104 fn params(&mut self) -> JsonValue {
106 object! {}
107 }
108 fn success(&mut self) -> ApiResponse {
110 let mut data = object! {};
111 data["code"] = br_fields::int::Int::new(true, "code", "编号", 10, 0)
112 .example(0.into())
113 .swagger();
114 data["message"] = br_fields::str::Str::new(true, "message", "成功消息", 256, "")
115 .example("成功".into())
116 .swagger();
117 data["data"] = br_fields::text::Object::new(true, "data", "返回数据", object! {}).swagger();
118 data["success"] = br_fields::int::Switch::new(true, "success", "成功状态", true)
119 .example(true.into())
120 .swagger();
121 data["timestamp"] =
122 br_fields::datetime::Timestamp::new(true, "timestamp", "时间戳", 0, 0.0).swagger();
123 ApiResponse::success(data, "请求成功")
124 }
125 fn error(&mut self) -> ApiResponse {
127 let mut data = object! {};
128 data["code"] = br_fields::int::Int::new(true, "code", "编号", 10, 1000)
129 .example(1000.into())
130 .swagger();
131 data["message"] = br_fields::str::Str::new(true, "message", "错误消息", 256, "")
132 .example("失败".into())
133 .swagger();
134 data["data"] = br_fields::text::Object::new(true, "data", "返回数据", object! {}).swagger();
135 data["success"] = br_fields::int::Switch::new(true, "success", "成功状态", false)
136 .example(false.into())
137 .swagger();
138 data["timestamp"] =
139 br_fields::datetime::Timestamp::new(true, "timestamp", "时间戳", 0, 0.0).swagger();
140 ApiResponse::error(data, "请求失败")
141 }
142 fn run(&mut self, mut request: Request) -> Result<ApiResponse, ApiResponse> {
144 if self.public()
145 && !self.method().str().is_empty()
146 && self.method().str().to_lowercase() != request.method.str().to_lowercase()
147 {
148 return Err(ApiResponse::fail(
149 -1,
150 format!(
151 "Request type error: Actual [{}] Expected [{}]",
152 request.method.str(),
153 self.method().str()
154 )
155 .as_str(),
156 ));
157 }
158 let params = self.params();
159 if self.params_check() {
160 self.check(&mut request.query, self.query())?;
161 self.check(&mut request.body, params)?;
162 }
163 if self.review() {
164 let req = Request {
165 body: object! {
166 api:self.api(),
167 org_org:self.get_global_data_key("org_org"),
168 user_user:self.get_global_data_key("user_user"),
169 params:request.body.clone()
170 },
171 ..request.clone()
172 };
173
174 match self.review_fun(req) {
175 Ok(_) => {}
176 Err(e) => return Err(ApiResponse::fail(50002, e.as_str())),
177 }
178 }
179 let res = self.index(request);
180 if res.success {
181 Ok(res)
182 } else {
183 Err(res)
184 }
185 }
186 fn check(&mut self, request: &mut JsonValue, params: JsonValue) -> Result<(), ApiResponse> {
188 let req = request.clone();
189 for (name, _) in req.entries() {
190 if !params.has_key(name) {
191 request.remove(name);
192 }
193 }
194 for (name, field) in params.entries() {
195 let require = field["require"].as_bool().unwrap_or(false);
196 let title = field["title"].as_str().unwrap_or("");
197 if request.has_key(name) {
198 match field["mode"].as_str().unwrap_or("text") {
200 "key" => {
201 if !request[name].is_string() {
202 return Err(ApiResponse::fail(
203 900_001,
204 format!(
205 "请求参数数据类型错误: 参数 [{}] 数据类型应为[{}]",
206 name, field["mode"]
207 )
208 .as_str(),
209 ));
210 }
211 if require && request[name].is_empty() {
212 return Err(ApiResponse::fail(
213 900_014,
214 format!("请求参数数据类型错误: 参数 [{name}] 不能为空").as_str(),
215 ));
216 }
217 }
218 "text" | "table" | "tree" => {
219 let is_multiple = field["multiple"].as_bool().unwrap_or(false);
220 if is_multiple {
221 if !request[name].is_array() {
222 return Err(ApiResponse::fail(
223 900_009,
224 format!(
225 "请求参数数据类型错误: 参数 [{}] 数据类型应为数组",
226 name
227 )
228 .as_str(),
229 ));
230 }
231 if require && request[name].is_empty() {
232 return Err(ApiResponse::fail(
233 900_010,
234 format!("{title} 必填").as_str(),
235 ));
236 }
237 } else {
238 if !request[name].is_string() {
239 return Err(ApiResponse::fail(
240 900_002,
241 format!(
242 "请求参数数据类型错误: 参数 [{}] 数据类型应为[{}]",
243 name, field["mode"]
244 )
245 .as_str(),
246 ));
247 }
248 if require && request[name].is_empty() {
249 return Err(ApiResponse::fail(
250 900_002,
251 format!("{title} 必填").as_str(),
252 ));
253 }
254 }
255 }
256 "file" => {
257 if !request[name].is_array() && !request[name].is_string() {
258 return Err(ApiResponse::fail(
259 900_003,
260 format!("参数 [{}] 数据类型应为[{}]", name, field["mode"]).as_str(),
261 ));
262 }
263 if require && request[name].is_empty() {
264 return Err(ApiResponse::fail(
265 900_002,
266 format!("{title} 必填").as_str(),
267 ));
268 }
269 }
270 "int" => {
271 if require && request[name].to_string().is_empty() {
272 return Err(ApiResponse::fail(
273 900_002,
274 format!("{title} 必填").as_str(),
275 ));
276 }
277 if !request[name].to_string().is_empty() {
278 match request[name].to_string().parse::<i64>() {
279 Ok(_) => {}
280 Err(_) => {
281 return Err(ApiResponse::fail(
282 900_013,
283 format!(
284 "请求参数数据类型错误: 参数 [{}] 数据类型应为[{}]",
285 name, field["mode"]
286 )
287 .as_str(),
288 ));
289 }
290 }
291 }
292 }
293 "timestamp" | "yearmonth" | "date" | "datetime" => {
294 if require && request[name].to_string().is_empty() {
295 return Err(ApiResponse::fail(
296 900_002,
297 format!("{title} 必填").as_str(),
298 ));
299 }
300 if !request[name].to_string().is_empty() {
301 if request[name].is_array() && request[name].len() == 2 {
302 for item in request[name].members() {
304 match item.to_string().parse::<f64>() {
305 Ok(_) => {}
306 Err(_) => {
307 return Err(ApiResponse::fail(
308 900_013,
309 format!(
310 "请求参数数据类型错误: 参数 [{}] 数据类型应为[{}]",
311 name, field["mode"]
312 ).as_str(),
313 ));
314 }
315 }
316 }
317 } else {
318 match request[name].to_string().parse::<f64>() {
319 Ok(_) => {}
320 Err(_) => {
321 return Err(ApiResponse::fail(
322 900_013,
323 format!(
324 "请求参数数据类型错误: 参数 [{}] 数据类型应为[{}]",
325 name, field["mode"]
326 )
327 .as_str(),
328 ));
329 }
330 }
331 }
332 }
333 }
334 "float" => {
335 if require && request[name].to_string().is_empty() {
336 return Err(ApiResponse::fail(
337 900_002,
338 format!("{title} 必填").as_str(),
339 ));
340 }
341 if !request[name].to_string().is_empty() {
342 match request[name].to_string().parse::<f64>() {
343 Ok(_) => {}
344 Err(_) => {
345 return Err(ApiResponse::fail(
346 900_023,
347 format!(
348 "请求参数数据类型错误: 参数 [{}] 数据类型应为[{}]",
349 name, field["mode"]
350 )
351 .as_str(),
352 ));
353 }
354 }
355 }
356 }
357 "string" | "url" | "time" | "code" | "pass" | "email" | "location"
358 | "color" | "barcode" | "editor" | "tel" => {
359 if !request[name].is_string() {
360 return Err(ApiResponse::fail(
361 -1,
362 format!(
363 "请求参数数据类型错误: 参数 [{}] 数据类型应为[{}]",
364 name, field["mode"]
365 )
366 .as_str(),
367 ));
368 }
369 if require && request[name].is_empty() {
370 return Err(ApiResponse::fail(
371 900_004,
372 format!("{title} 必填").as_str(),
373 ));
374 }
375 }
376 "dict" => {
377 if require && request[name].is_empty() {
378 return Err(ApiResponse::fail(
379 900_005,
380 format!("{title} 必填").as_str(),
381 ));
382 }
383 }
384 "switch" => match request[name].to_string().parse::<bool>() {
385 Ok(e) => {
386 request[name] = e.into();
387 }
388 Err(_) => {
389 return Err(ApiResponse::fail(
390 -1,
391 format!(
392 "请求参数数据类型错误: 参数 [{name}] 数据类型应为[{}]",
393 field["mode"]
394 )
395 .as_str(),
396 ));
397 }
398 },
399 "select" => {
400 if !request[name].is_array() && !request[name].is_string() {
401 return Err(ApiResponse::fail(
402 -1,
403 format!(
404 "请求参数数据类型错误: 参数 [{}] 数据类型应为[{}]",
405 name, field["mode"]
406 )
407 .as_str(),
408 ));
409 }
410 let value = if request[name].is_array() {
411 request[name]
412 .members()
413 .map(ToString::to_string)
414 .collect::<Vec<String>>()
415 } else {
416 request[name]
417 .to_string()
418 .split(',')
419 .map(ToString::to_string)
420 .collect::<Vec<String>>()
421 };
422 let option = field["option"]
423 .members()
424 .map(|m| m.as_str().unwrap_or(""))
425 .collect::<Vec<&str>>();
426
427 let require = field["require"].as_bool().unwrap_or(false);
428 for item in value.clone() {
429 if !option.contains(&&*item.clone()) && (item.is_empty() && require) {
430 let option = field["option"]
431 .members()
432 .map(|m| m.as_str().unwrap_or(""))
433 .collect::<Vec<&str>>()
434 .join(",");
435 return Err(ApiResponse::fail(-1, format!("请求参数选项错误: 参数 [{item}] 数据类型应为[{option}]之内").as_str()));
436 }
437 }
438 request[name] = value.into();
439 }
440 "radio" => {
441 if !request[name].is_string() {
442 return Err(ApiResponse::fail(
443 -1,
444 format!(
445 "请求参数数据类型错误: 参数 [{}] 数据类型应为[{}] 实际为[{}]",
446 name, field["mode"], request[name]
447 )
448 .as_str(),
449 ));
450 }
451 let option = field["option"]
452 .members()
453 .map(|m| m.as_str().unwrap_or(""))
454 .collect::<Vec<&str>>()
455 .join(",");
456 if request[name].is_string()
457 && !field["option"].contains(request[name].as_str().unwrap_or(""))
458 {
459 return Err(ApiResponse::fail(
460 -1,
461 format!(
462 "请求参数选项错误: 参数 [{}] 数据 [{}] 应为 [{}] 之一",
463 name, request[name], option
464 )
465 .as_str(),
466 ));
467 }
468 }
469 "array" | "polygon" | "keys" | "table_multiple" => {
470 if !request[name].is_array() {
471 return Err(ApiResponse::fail(
472 900_009,
473 format!(
474 "请求参数数据类型错误: 参数 [{}] 数据类型应为[{}]",
475 name, field["mode"]
476 )
477 .as_str(),
478 ));
479 }
480 if require && request[name].is_empty() {
481 return Err(ApiResponse::fail(
482 900_010,
483 format!("请求参数数据类型错误: 参数 [{name}] 不能为空").as_str(),
484 ));
485 }
486 }
487 "object" => {
488 if !request[name].is_object() {
489 return Err(ApiResponse::fail(
490 900_009,
491 format!(
492 "请求参数数据类型错误: 参数 [{}] 数据类型应为[{}]",
493 name, field["mode"]
494 )
495 .as_str(),
496 ));
497 }
498 if require && request[name].is_empty() {
499 return Err(ApiResponse::fail(
500 900_006,
501 format!("{title} 必填").as_str(),
502 ));
503 }
504 }
505 _ => {
506 log::warn!("检查未知类型: {}", field["mode"]);
507 }
508 }
509 } else {
510 if require {
511 return Err(ApiResponse::fail(900_007, format!("{title} 必填").as_str()));
512 }
513 request[name] = field["def"].clone();
514 }
515 }
516 Ok(())
517 }
518 fn index(&mut self, request: Request) -> ApiResponse;
520 #[cfg(any(
521 feature = "sqlite",
522 feature = "mssql",
523 feature = "mysql",
524 feature = "pgsql"
525 ))]
526 fn table_select(
527 &mut self,
528 request: JsonValue,
529 table_name: &str,
530 fields: Vec<&str>,
531 ) -> JsonValue {
532 self.table()
533 .main_select_fields(table_name, fields)
534 .params(request)
535 .get_table_select()
536 }
537
538 #[cfg(any(
539 feature = "sqlite",
540 feature = "mssql",
541 feature = "mysql",
542 feature = "pgsql"
543 ))]
544 fn table(&mut self) -> Tables {
545 Tables::new(self.tools().db)
546 }
547
548 fn tools(&mut self) -> Tools {
550 PLUGIN_TOOLS.get().expect("tools not initialized").clone()
551 }
552 fn config(&mut self, name: &str) -> JsonValue {
554 if let Ok(cfg) = CONFIG.lock() {
555 cfg.get(name).cloned().unwrap_or_else(|| object! {})
556 } else {
557 object! {}
558 }
559 }
560 fn btn(&mut self) -> Btn {
563 let mut btn = Btn::new(self.api().as_str());
564 btn.fields(self.params());
565 btn.tags(self.tags());
566 btn.icon(self.icon());
567 btn.desc(self.description());
568 btn.auth(self.auth());
569 btn.public(self.public());
570 btn.title(self.title());
571 btn.btn_type(BtnType::Api);
572 btn.btn_color(BtnColor::Primary);
573 btn.path(self.api().replace('.', "/").as_str());
574 btn.pass(false);
575 btn.addon();
576 btn
577 }
578 fn set_global_data(&mut self, key: &str, value: JsonValue) {
580 GLOBAL_DATA.with(|data| {
581 data.borrow_mut()[key] = value;
582 });
583 }
584 fn get_global_data(&mut self) -> JsonValue {
586 GLOBAL_DATA.with(|data| data.borrow().clone())
587 }
588 fn get_global_data_key(&mut self, key: &str) -> JsonValue {
590 GLOBAL_DATA.with(|data| data.borrow()[key].clone())
591 }
592}
593#[derive(Debug, Clone)]
594pub struct Btn {
595 api: String,
596 title: String,
597 desc: String,
598 tags: &'static [&'static str],
599 auth: bool,
600 public: bool,
601 btn_type: BtnType,
602 color: BtnColor,
603 icon: String,
604 cnd: Vec<JsonValue>,
605 url: String,
606 path: String,
607 fields: JsonValue,
608 addon: String,
609 pass: bool,
611}
612impl Btn {
613 #[must_use]
614 pub fn new(api: &str) -> Self {
615 Self {
616 api: api.to_string(),
617 title: String::new(),
618 desc: String::new(),
619 btn_type: BtnType::Api,
620 color: BtnColor::Primary,
621 icon: String::new(),
622 auth: false,
623 public: false,
624 cnd: vec![],
625 url: String::new(),
626 path: String::new(),
627 fields: object! {},
628 tags: &[],
629 pass: false,
630 addon: String::new(),
631 }
632 }
633
634 pub fn addon(&mut self) -> &mut Self {
635 self.addon = self.api.split('.').next().unwrap_or_default().to_string();
636 self
637 }
638 pub fn path(&mut self, path: &str) -> &mut Self {
639 self.path = path.to_string();
640 self
641 }
642 pub fn cnd(&mut self, cnd: Vec<JsonValue>) -> &mut Self {
643 self.cnd = cnd;
644 self
645 }
646 pub fn btn_type(&mut self, btn_type: BtnType) -> &mut Self {
647 self.btn_type = btn_type;
648 self
649 }
650 pub fn btn_color(&mut self, btn_color: BtnColor) -> &mut Self {
651 self.color = btn_color;
652 self
653 }
654 pub fn fields(&mut self, fields: JsonValue) -> &mut Self {
655 self.fields = fields;
656 self
657 }
658 pub fn pass(&mut self, pass: bool) -> &mut Self {
659 self.pass = pass;
660 self
661 }
662 pub fn url(&mut self, url: &str) -> &mut Self {
663 self.url = url.to_string();
664 self
665 }
666 pub fn title(&mut self, title: &str) -> &mut Self {
667 self.title = title.to_string();
668 self
669 }
670 pub fn desc(&mut self, desc: &str) -> &mut Self {
671 self.desc = desc.to_string();
672 self
673 }
674 pub fn tags(&mut self, tags: &'static [&'static str]) -> &mut Self {
675 self.tags = tags;
676 self
677 }
678 pub fn public(&mut self, public: bool) -> &mut Self {
679 self.public = public;
680 self
681 }
682 pub fn auth(&mut self, auth: bool) -> &mut Self {
683 self.auth = auth;
684 self
685 }
686 pub fn icon(&mut self, icon: &str) -> &mut Self {
687 self.icon = icon.to_string();
688 self
689 }
690 pub fn json(&mut self) -> JsonValue {
691 let color = self.color.clone().str();
692 object! {
693 addon:self.addon.to_string() ,
694 api:self.api.clone(),
695 title:self.title.clone(),
696 desc:self.desc.clone(),
697 auth:self.auth,
698 public:self.public,
699 btn_type:self.btn_type.clone().str(),
700 color:color,
701 icon:self.icon.clone(),
702 cnd:self.cnd.clone(),
703 url:self.url.clone(),
704 path:self.path.clone(),
705 fields:self.fields.clone(),
706 tags:self.tags,
707 pass:self.pass,
708 }
709 }
710}
711
712#[derive(Debug, Clone)]
714pub enum InterfaceType {
715 Api,
716 Btn,
717 Menu,
718 MenuApp,
719 OpenApi,
720}
721
722impl InterfaceType {
723 pub fn str(&self) -> &'static str {
724 match self {
725 InterfaceType::Api => "api",
726 InterfaceType::Btn => "btn",
727 InterfaceType::Menu => "menu",
728 InterfaceType::MenuApp => "menu_app",
729 InterfaceType::OpenApi => "openapi",
730 }
731 }
732 pub fn types() -> Vec<&'static str> {
733 vec!["api", "btn", "menu", "menu_app", "openapi"]
734 }
735}
736
737#[derive(Debug, Clone)]
739pub enum BtnType {
740 Form,
742 FormDownload,
744 FormCustom,
746 FormData,
747 Url,
749 Api,
751 Download,
753 Path,
755 DialogCustom,
757 FormApiDialogCustom,
759 Preview,
761}
762
763impl BtnType {
764 fn str(self) -> &'static str {
765 match self {
766 BtnType::Form => "form",
767 BtnType::FormDownload => "form_download",
768 BtnType::FormCustom => "form_custom",
769 BtnType::FormData => "form_data",
770 BtnType::Api => "api",
771 BtnType::Download => "download",
772 BtnType::Url => "url",
773 BtnType::Path => "path",
774 BtnType::DialogCustom => "dialog_custom",
775 BtnType::FormApiDialogCustom => "form_api_dialog_custom",
776 BtnType::Preview => "preview",
777 }
778 }
779}
780#[derive(Debug, Clone)]
782pub enum BtnColor {
783 Primary,
784 Red,
785 Yellow,
786 Green,
787 Blue,
788 Orange,
789 Purple,
790 Cyan,
791 Teal,
792 Grey,
793}
794
795impl BtnColor {
796 fn str(self) -> &'static str {
797 match self {
798 BtnColor::Primary => "primary",
799 BtnColor::Red => "negative",
800 BtnColor::Yellow => "warning",
801 BtnColor::Green => "positive",
802 BtnColor::Blue => "blue",
803 BtnColor::Orange => "orange",
804 BtnColor::Purple => "purple",
805 BtnColor::Cyan => "cyan",
806 BtnColor::Teal => "teal",
807 BtnColor::Grey => "grey",
808 }
809 }
810}
811
812pub struct Dashboard {
813 title: String,
815 data: JsonValue,
817 model: DashboardModel,
819 class: String,
821 icon: String,
823 desc: String,
825 api: String,
827 options: JsonValue,
829}
830impl Dashboard {
831 pub fn new(title: &str) -> Dashboard {
832 Dashboard {
833 title: title.to_string(),
834 data: JsonValue::Null,
835 model: DashboardModel::Number,
836 class: "col-4".to_string(),
837 icon: "".to_string(),
838 desc: "".to_string(),
839 api: "".to_string(),
840 options: JsonValue::Null,
841 }
842 }
843 pub fn options(&mut self, options: JsonValue) -> &mut Self {
844 self.options = options;
845 self
846 }
847 pub fn api(&mut self, api: &str) -> &mut Self {
848 self.api = api.to_string();
849 self
850 }
851 pub fn data(&mut self, data: JsonValue) -> &mut Self {
852 self.data = data;
853 self
854 }
855 pub fn class(&mut self, name: &str) -> &mut Self {
856 self.class = name.to_string();
857 self
858 }
859 pub fn icon(&mut self, name: &str) -> &mut Self {
860 self.icon = name.to_string();
861 self
862 }
863 pub fn model(&mut self, dashboard_model: DashboardModel) -> &mut Self {
864 self.model = dashboard_model;
865 self
866 }
867 pub fn desc(&mut self, desc: &str) -> &mut Self {
868 self.desc = desc.to_string();
869 self
870 }
871 pub fn json(&self) -> JsonValue {
872 object! {
873 title: self.title.clone(),
874 data: self.data.clone(),
875 class: self.class.clone(),
876 icon: self.icon.clone(),
877 model: self.model.str(),
878 desc: self.desc.clone(),
879 api: self.api.clone(),
880 options:self.options.clone(),
881 }
882 }
883}
884
885pub enum DashboardModel {
886 Number,
888 EchartsBar,
890 EchartsStackedBar,
892 EchartsBarRace,
894 EchartsPie,
896 EchartsDoughnut,
898 EchartsStackedLine,
900 EchartsStackedLineArea,
902 EchartsGeoGraph,
904}
905
906impl DashboardModel {
907 pub fn str(&self) -> &'static str {
908 match self {
909 Self::Number => "number",
910 Self::EchartsBar => "echarts-bar",
911 Self::EchartsStackedBar => "echarts-stacked_bar",
912 Self::EchartsBarRace => "echarts-bar_race",
913 Self::EchartsPie => "echarts-pie",
914 Self::EchartsDoughnut => "echarts-doughnut",
915 Self::EchartsStackedLine => "echarts-stacked_line",
916 Self::EchartsStackedLineArea => "echarts-stacked_line_area",
917 Self::EchartsGeoGraph => "echarts-geo_graph",
918 }
919 }
920}