editor-viewer.tsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. import { 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. interface Props {
  23. mode: "profile" | "text";
  24. property: string;
  25. open: boolean;
  26. language: "yaml" | "javascript" | "css";
  27. schema?: "clash" | "merge";
  28. onClose: () => void;
  29. onChange?: (content?: string) => void;
  30. }
  31. // yaml worker
  32. configureMonacoYaml(monaco, {
  33. validate: true,
  34. enableSchemaRequest: true,
  35. schemas: [
  36. {
  37. uri: "http://example.com/meta-json-schema.json",
  38. fileMatch: ["**/*.clash.yaml"],
  39. //@ts-ignore
  40. schema: metaSchema as JSONSchema7,
  41. },
  42. {
  43. uri: "http://example.com/clash-verge-merge-json-schema.json",
  44. fileMatch: ["**/*.merge.yaml"],
  45. //@ts-ignore
  46. schema: mergeSchema as JSONSchema7,
  47. },
  48. ],
  49. });
  50. export const EditorViewer = (props: Props) => {
  51. const { mode, property, open, language, schema, onClose, onChange } = props;
  52. const { t } = useTranslation();
  53. const editorRef = useRef<any>();
  54. const instanceRef = useRef<editor.IStandaloneCodeEditor | null>(null);
  55. const themeMode = useRecoilValue(atomThemeMode);
  56. useEffect(() => {
  57. if (!open) return;
  58. let fetchContent;
  59. switch (mode) {
  60. case "profile": // profile文件
  61. fetchContent = readProfileFile(property);
  62. case "text": // 文本内容
  63. fetchContent = Promise.resolve(property);
  64. }
  65. fetchContent.then((data) => {
  66. const dom = editorRef.current;
  67. if (!dom) return;
  68. if (instanceRef.current) instanceRef.current.dispose();
  69. const uri = monaco.Uri.parse(`${nanoid()}.${schema}.${language}`);
  70. const model = monaco.editor.createModel(data, language, uri);
  71. instanceRef.current = editor.create(editorRef.current, {
  72. model: model,
  73. language: language,
  74. tabSize: ["yaml", "javascript", "css"].includes(language) ? 2 : 4, // 根据语言类型设置缩进
  75. theme: themeMode === "light" ? "vs" : "vs-dark",
  76. minimap: { enabled: dom.clientWidth >= 1000 }, // 超过一定宽度显示minimap滚动条
  77. mouseWheelZoom: true, // Ctrl+滚轮调节缩放
  78. quickSuggestions: {
  79. strings: true, // 字符串类型的建议
  80. comments: true, // 注释类型的建议
  81. other: true, // 其他类型的建议
  82. },
  83. padding: {
  84. top: 33, // 顶部padding防止遮挡snippets
  85. },
  86. });
  87. });
  88. return () => {
  89. if (instanceRef.current) {
  90. instanceRef.current.dispose();
  91. instanceRef.current = null;
  92. }
  93. };
  94. }, [open]);
  95. const onSave = useLockFn(async () => {
  96. const value = instanceRef.current?.getValue();
  97. if (value == null) return;
  98. try {
  99. if (mode === "profile") {
  100. await saveProfileFile(property, value);
  101. }
  102. onChange?.(value);
  103. onClose();
  104. } catch (err: any) {
  105. Notice.error(err.message || err.toString());
  106. }
  107. });
  108. return (
  109. <Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
  110. <DialogTitle>{t("Edit File")}</DialogTitle>
  111. <DialogContent
  112. sx={{ width: "94%", height: "100vh", pb: 1, userSelect: "text" }}
  113. >
  114. <div style={{ width: "100%", height: "100%" }} ref={editorRef} />
  115. </DialogContent>
  116. <DialogActions>
  117. <Button onClick={onClose} variant="outlined">
  118. {t("Cancel")}
  119. </Button>
  120. <Button onClick={onSave} variant="contained">
  121. {t("Save")}
  122. </Button>
  123. </DialogActions>
  124. </Dialog>
  125. );
  126. };