editor-viewer.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import { ReactNode, useEffect, useRef } from "react";
  2. import { useLockFn } from "ahooks";
  3. import { useRecoilValue } from "recoil";
  4. import { useTranslation } from "react-i18next";
  5. import {
  6. Button,
  7. Dialog,
  8. DialogActions,
  9. DialogContent,
  10. DialogTitle,
  11. } from "@mui/material";
  12. import { atomThemeMode } from "@/services/states";
  13. import { readProfileFile, saveProfileFile } from "@/services/cmds";
  14. import { Notice } from "@/components/base";
  15. import { nanoid } from "nanoid";
  16. import * as monaco from "monaco-editor";
  17. import { editor } from "monaco-editor/esm/vs/editor/editor.api";
  18. import { configureMonacoYaml } from "monaco-yaml";
  19. import { type JSONSchema7 } from "json-schema";
  20. import metaSchema from "meta-json-schema/schemas/meta-json-schema.json";
  21. import mergeSchema from "meta-json-schema/schemas/clash-verge-merge-json-schema.json";
  22. import pac from "types-pac/pac.d.ts?raw";
  23. interface Props {
  24. title?: string | ReactNode;
  25. mode: "profile" | "text";
  26. property: string;
  27. open: boolean;
  28. readOnly?: boolean;
  29. language: "yaml" | "javascript" | "css";
  30. schema?: "clash" | "merge";
  31. onClose: () => void;
  32. onChange?: (content?: string) => void;
  33. }
  34. // yaml worker
  35. configureMonacoYaml(monaco, {
  36. validate: true,
  37. enableSchemaRequest: true,
  38. schemas: [
  39. {
  40. uri: "http://example.com/meta-json-schema.json",
  41. fileMatch: ["**/*.clash.yaml"],
  42. //@ts-ignore
  43. schema: metaSchema as JSONSchema7,
  44. },
  45. {
  46. uri: "http://example.com/clash-verge-merge-json-schema.json",
  47. fileMatch: ["**/*.merge.yaml"],
  48. //@ts-ignore
  49. schema: mergeSchema as JSONSchema7,
  50. },
  51. ],
  52. });
  53. // PAC definition
  54. monaco.languages.typescript.javascriptDefaults.addExtraLib(pac, "pac.d.ts");
  55. export const EditorViewer = (props: Props) => {
  56. const {
  57. title,
  58. mode,
  59. property,
  60. open,
  61. readOnly,
  62. language,
  63. schema,
  64. onClose,
  65. onChange,
  66. } = props;
  67. const { t } = useTranslation();
  68. const editorRef = useRef<any>();
  69. const instanceRef = useRef<editor.IStandaloneCodeEditor | null>(null);
  70. const themeMode = useRecoilValue(atomThemeMode);
  71. useEffect(() => {
  72. if (!open) return;
  73. let fetchContent;
  74. switch (mode) {
  75. case "profile": // profile文件
  76. fetchContent = readProfileFile(property);
  77. break;
  78. case "text": // 文本内容
  79. fetchContent = Promise.resolve(property);
  80. break;
  81. }
  82. fetchContent.then((data) => {
  83. const dom = editorRef.current;
  84. if (!dom) return;
  85. if (instanceRef.current) instanceRef.current.dispose();
  86. const uri = monaco.Uri.parse(`${nanoid()}.${schema}.${language}`);
  87. const model = monaco.editor.createModel(data, language, uri);
  88. instanceRef.current = editor.create(editorRef.current, {
  89. model: model,
  90. language: language,
  91. tabSize: ["yaml", "javascript", "css"].includes(language) ? 2 : 4, // 根据语言类型设置缩进大小
  92. theme: themeMode === "light" ? "vs" : "vs-dark",
  93. minimap: { enabled: dom.clientWidth >= 1000 }, // 超过一定宽度显示minimap滚动条
  94. mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
  95. readOnly: readOnly, // 只读模式
  96. readOnlyMessage: { value: t("ReadOnlyMessage") }, // 只读模式尝试编辑时的提示信息
  97. renderValidationDecorations: "on", // 只读模式下显示校验信息
  98. quickSuggestions: {
  99. strings: true, // 字符串类型的建议
  100. comments: true, // 注释类型的建议
  101. other: true, // 其他类型的建议
  102. },
  103. padding: {
  104. top: 33, // 顶部padding防止遮挡snippets
  105. },
  106. fontFamily:
  107. "Fira Code, Roboto Mono, Source Code Pro, Menlo, Monaco, Consolas, Courier New, monospace",
  108. });
  109. });
  110. return () => {
  111. if (instanceRef.current) {
  112. instanceRef.current.dispose();
  113. instanceRef.current = null;
  114. }
  115. };
  116. }, [open]);
  117. const onSave = useLockFn(async () => {
  118. const value = instanceRef.current?.getValue();
  119. if (value == null) return;
  120. try {
  121. if (mode === "profile") {
  122. await saveProfileFile(property, value);
  123. }
  124. onChange?.(value);
  125. onClose();
  126. } catch (err: any) {
  127. Notice.error(err.message || err.toString());
  128. }
  129. });
  130. return (
  131. <Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
  132. <DialogTitle>{title ?? t("Edit File")}</DialogTitle>
  133. <DialogContent sx={{ width: "auto", height: "100vh" }}>
  134. <div style={{ width: "100%", height: "100%" }} ref={editorRef} />
  135. </DialogContent>
  136. <DialogActions>
  137. <Button onClick={onClose} variant="outlined">
  138. {t("Cancel")}
  139. </Button>
  140. <Button onClick={onSave} variant="contained">
  141. {t("Save")}
  142. </Button>
  143. </DialogActions>
  144. </Dialog>
  145. );
  146. };