xhs graphic production - 1.0.html 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Markdown 编辑与预览 (带同步滚动和滑块)</title>
  7. <script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"></script>
  8. <script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
  9. <style>
  10. body {
  11. display: flex;
  12. flex-direction: column;
  13. align-items: center;
  14. min-height: 100vh;
  15. background: #f0f0f0;
  16. margin: 0;
  17. font-family: 'Arial', sans-serif;
  18. padding-top: 20px;
  19. overflow: hidden; /* Prevent body scrollbars if editor is full height */
  20. }
  21. .editor-container {
  22. display: flex;
  23. width: 90%; /* Use percentage for better responsiveness */
  24. max-width: 1200px; /* Max width for large screens */
  25. height: calc(100vh - 100px); /* Adjust height to fill more of the viewport */
  26. background: #333333;
  27. border-radius: 8px;
  28. overflow: hidden;
  29. box-shadow: 0 4px 8px rgba(0,0,0,0.2);
  30. position: relative; /* For resizer positioning if needed */
  31. }
  32. .markdown-editor, .preview-pane {
  33. padding: 20px;
  34. box-sizing: border-box;
  35. overflow-y: auto;
  36. height: 100%; /* Fill the container height */
  37. }
  38. .markdown-editor {
  39. /* flex: 1; Will be set by JS */
  40. background: #222222;
  41. color: #e0e0e0;
  42. border: none;
  43. resize: none;
  44. font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
  45. font-size: 14px;
  46. line-height: 1.6;
  47. outline: none;
  48. }
  49. .resizer {
  50. flex: 0 0 10px; /* Don't grow, don't shrink, base width 10px */
  51. background: #555555;
  52. cursor: col-resize;
  53. height: 100%;
  54. display: flex;
  55. align-items: center;
  56. justify-content: center;
  57. user-select: none; /* Prevent text selection during drag */
  58. }
  59. .resizer::before { /* Optional: visual indicator for dragging */
  60. content: '⋮';
  61. color: #aaa;
  62. font-size: 18px;
  63. line-height: 0;
  64. }
  65. .preview-pane {
  66. /* flex: 1; Will be set by JS */
  67. background: #000000;
  68. color: #ffffff;
  69. /* border-left: 2px solid #666666; /* Replaced by resizer */
  70. word-wrap: break-word;
  71. }
  72. /* Styling for rendered Markdown elements in preview-pane */
  73. .preview-pane h1, .preview-pane h2, .preview-pane h3, .preview-pane h4, .preview-pane h5, .preview-pane h6 {
  74. color: #ffffff;
  75. border-bottom: 1px solid #444444;
  76. padding-bottom: 5px;
  77. margin-top: 1em;
  78. margin-bottom: 0.5em;
  79. }
  80. .preview-pane h1 { font-size: 24px; font-weight: bold; border-bottom: 2px solid #666666; padding-bottom: 10px; margin-bottom: 15px;}
  81. .preview-pane p {
  82. font-size: 16px;
  83. line-height: 1.5;
  84. margin-bottom: 1em;
  85. }
  86. .preview-pane ul, .preview-pane ol {
  87. margin-left: 20px;
  88. margin-bottom: 1em;
  89. }
  90. .preview-pane li {
  91. margin-bottom: 0.5em;
  92. }
  93. .preview-pane blockquote {
  94. border-left: 4px solid #555555;
  95. padding-left: 10px;
  96. color: #aaaaaa;
  97. margin-left: 0;
  98. margin-right: 0;
  99. margin-bottom: 1em;
  100. }
  101. .preview-pane pre {
  102. background-color: #1e1e1e;
  103. padding: 10px;
  104. border-radius: 4px;
  105. overflow-x: auto;
  106. font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
  107. }
  108. .preview-pane code {
  109. font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
  110. background-color: #282c34;
  111. padding: 2px 4px;
  112. border-radius: 3px;
  113. font-size: 0.9em;
  114. }
  115. .preview-pane pre code {
  116. background-color: transparent;
  117. padding: 0;
  118. border-radius: 0;
  119. }
  120. .preview-pane table {
  121. border-collapse: collapse;
  122. width: 100%;
  123. margin-bottom: 1em;
  124. }
  125. .preview-pane th, .preview-pane td {
  126. border: 1px solid #666666;
  127. padding: 8px;
  128. text-align: left;
  129. }
  130. .preview-pane th {
  131. background-color: #1a1a1a;
  132. }
  133. .preview-pane img {
  134. max-width: 100%;
  135. height: auto;
  136. border-radius: 4px;
  137. }
  138. .button-container {
  139. margin-top: 20px;
  140. text-align: center;
  141. }
  142. .export-btn {
  143. padding: 10px 20px;
  144. background: #000000;
  145. color: #ffffff;
  146. border: 2px solid #666666;
  147. cursor: pointer;
  148. font-size: 16px;
  149. }
  150. .export-btn:hover {
  151. background: #333333;
  152. }
  153. </style>
  154. </head>
  155. <body>
  156. <div class="editor-container">
  157. <textarea id="markdown-input" class="markdown-editor" placeholder="在此输入 Markdown... 例如:\n# 标题\n\n- 列表项1\n- 列表项2\n\n**加粗文本**"></textarea>
  158. <div id="resizer" class="resizer"></div>
  159. <div id="preview-output" class="preview-pane"></div>
  160. </div>
  161. <div class="button-container">
  162. <button class="export-btn" onclick="exportToPng()">导出为PNG</button>
  163. </div>
  164. <script>
  165. const markdownInput = document.getElementById('markdown-input');
  166. const previewOutput = document.getElementById('preview-output');
  167. const resizer = document.getElementById('resizer');
  168. const editorContainer = document.querySelector('.editor-container');
  169. // Initial flex distribution
  170. markdownInput.style.flex = '1';
  171. previewOutput.style.flex = '1';
  172. const converter = new showdown.Converter({
  173. tables: true,
  174. strikethrough: true,
  175. tasklists: true,
  176. ghCompatibleHeaderId: true,
  177. simpleLineBreaks: true
  178. });
  179. converter.setFlavor('github');
  180. function renderMarkdown() {
  181. const markdownText = markdownInput.value;
  182. const html = converter.makeHtml(markdownText);
  183. previewOutput.innerHTML = html;
  184. }
  185. markdownInput.addEventListener('input', renderMarkdown);
  186. renderMarkdown(); // Initial render
  187. // --- Resizer Logic ---
  188. let isResizing = false;
  189. let startX, startEditorWidth, startPreviewWidth;
  190. resizer.addEventListener('mousedown', (e) => {
  191. e.preventDefault(); // Prevent text selection, etc.
  192. isResizing = true;
  193. startX = e.clientX;
  194. startEditorWidth = markdownInput.offsetWidth;
  195. startPreviewWidth = previewOutput.offsetWidth;
  196. document.addEventListener('mousemove', handleMouseMove);
  197. document.addEventListener('mouseup', stopResize);
  198. });
  199. function handleMouseMove(e) {
  200. if (!isResizing) return;
  201. const dx = e.clientX - startX;
  202. let newEditorWidth = startEditorWidth + dx;
  203. let newPreviewWidth = startPreviewWidth - dx;
  204. const minWidth = 50; // Minimum width for panes
  205. if (newEditorWidth < minWidth) {
  206. newEditorWidth = minWidth;
  207. newPreviewWidth = startEditorWidth + startPreviewWidth - minWidth;
  208. } else if (newPreviewWidth < minWidth) {
  209. newPreviewWidth = minWidth;
  210. newEditorWidth = startEditorWidth + startPreviewWidth - minWidth;
  211. }
  212. const totalWidth = editorContainer.offsetWidth - resizer.offsetWidth;
  213. markdownInput.style.flex = `0 0 ${newEditorWidth}px`;
  214. previewOutput.style.flex = `0 0 ${newPreviewWidth}px`;
  215. }
  216. function stopResize() {
  217. isResizing = false;
  218. document.removeEventListener('mousemove', handleMouseMove);
  219. document.removeEventListener('mouseup', stopResize);
  220. }
  221. // --- Synchronized Scrolling ---
  222. let scrollTimeout;
  223. let isSyncingEditor = false;
  224. let isSyncingPreview = false;
  225. markdownInput.addEventListener('scroll', () => {
  226. if (isSyncingEditor) {
  227. isSyncingEditor = false; // Reset for next manual scroll
  228. return;
  229. }
  230. if (isResizing) return; // Don't sync scroll while resizing
  231. isSyncingPreview = true; // Prevent preview scroll from triggering editor scroll
  232. const scrollPercentage = markdownInput.scrollTop / (markdownInput.scrollHeight - markdownInput.clientHeight);
  233. if (isNaN(scrollPercentage)) return; // Avoid NaN if scrollHeight equals clientHeight
  234. previewOutput.scrollTop = scrollPercentage * (previewOutput.scrollHeight - previewOutput.clientHeight);
  235. // Clear any existing timeout to avoid race conditions
  236. clearTimeout(scrollTimeout);
  237. scrollTimeout = setTimeout(() => {
  238. isSyncingPreview = false;
  239. }, 100); // Adjust delay as needed
  240. });
  241. previewOutput.addEventListener('scroll', () => {
  242. if (isSyncingPreview) {
  243. isSyncingPreview = false; // Reset for next manual scroll
  244. return;
  245. }
  246. if (isResizing) return; // Don't sync scroll while resizing
  247. isSyncingEditor = true; // Prevent editor scroll from triggering preview scroll
  248. const scrollPercentage = previewOutput.scrollTop / (previewOutput.scrollHeight - previewOutput.clientHeight);
  249. if (isNaN(scrollPercentage)) return;
  250. markdownInput.scrollTop = scrollPercentage * (markdownInput.scrollHeight - markdownInput.clientHeight);
  251. clearTimeout(scrollTimeout);
  252. scrollTimeout = setTimeout(() => {
  253. isSyncingEditor = false;
  254. }, 100); // Adjust delay as needed
  255. });
  256. function exportToPng() {
  257. const previewPane = document.getElementById('preview-output');
  258. const originalHeight = previewPane.style.height;
  259. const originalOverflowY = previewPane.style.overflowY;
  260. previewPane.style.height = previewPane.scrollHeight + 'px';
  261. previewPane.style.overflowY = 'visible';
  262. previewPane.style.backgroundColor = '#000000';
  263. html2canvas(previewPane, {
  264. backgroundColor: '#000000',
  265. scale: 2,
  266. useCORS: true,
  267. logging: false,
  268. onclone: (documentClone) => {
  269. const clonedPreviewPane = documentClone.getElementById('preview-output');
  270. if (clonedPreviewPane) {
  271. clonedPreviewPane.style.backgroundColor = '#000000';
  272. clonedPreviewPane.style.color = '#ffffff';
  273. clonedPreviewPane.style.height = clonedPreviewPane.scrollHeight + 'px';
  274. clonedPreviewPane.style.overflowY = 'visible';
  275. }
  276. }
  277. }).then(canvas => {
  278. const link = document.createElement('a');
  279. link.download = 'markdown_card.png';
  280. link.href = canvas.toDataURL('image/png');
  281. link.click();
  282. }).finally(() => {
  283. previewPane.style.height = originalHeight;
  284. previewPane.style.overflowY = originalOverflowY;
  285. });
  286. }
  287. </script>
  288. </body>
  289. </html>