吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 7854|回复: 274
上一主题 下一主题
收起左侧

[学习记录] 微信公众号编辑器-本地超强版带模板

    [复制链接]
跳转到指定楼层
楼主
傻瓜炒蛋 发表于 2026-7-14 11:46 回帖奖励
本帖最后由 傻瓜炒蛋 于 2026-7-15 11:09 编辑

微信公众号自带编辑器不好用,让ai帮忙做了一个。功能如下:
  • 内置几个标题、模板,也可以调用135官方模板,缓存到浏览器用。
  • 中间编辑器,带了字体,换行,可截图粘贴,查看源代码。
  • 右侧一键复制到微信编辑器,也可以把写好的文件导出html,下次方便的时候


使用方法:
源代码复制到文本编辑器,另存为网页,双击运行即可。



或者直接运行html即可。

附件下载:
editor.zip (23.53 KB, 下载次数: 269)


===========第二版 2026年7月15日 11:04:32修改================
editor-第二版.zip (25.14 KB, 下载次数: 2476)

修改说明:
修改了导入模板图片问题,第一版bg图会丢失,这个好像解决了。

修改了重复问题。

===========================================================

若觉得好用,劳烦帮忙点个赞。
截图:




具体代码如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>微信公众号文章编辑器</title>
<style>
/* ========== CSS Reset & Variables ========== */
:root {
  --primary: #07c160;
  --primary-hover: #06ad56;
  --primary-light: #e8f8ef;
  --bg: #f5f5f5;
  --bg-white: #fff;
  --border: #e0e0e0;
  --text: #333;
  --text-secondary: #666;
  --text-muted: #999;
  --shadow: 0 2px 8px rgba(0,0,0,0.08);
  --shadow-lg: 0 4px 20px rgba(0,0,0,0.12);
  --radius: 8px;
  --radius-sm: 4px;
  --transition: 0.2s ease;
  --toolbar-height: 44px;
  --editor-max-width: 680px;
  --font-stack: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: var(--font-stack);
  background: #e8ecf1;
  color: var(--text);
  min-height: 100vh;
  overflow: hidden;
}

/* ========== Header ========== */
.app-header {
  background: #fff;
  border-bottom: 1px solid var(--border);
  height: 52px;
  display: flex;
  align-items: center;
  padding: 0 20px;
  justify-content: space-between;
  z-index: 100;
  position: relative;
}
.app-header .logo {
  font-size: 18px;
  font-weight: 700;
  color: var(--primary);
  display: flex;
  align-items: center;
  gap: 8px;
}
.app-header .logo svg { width: 28px; height: 28px; }
.header-actions { display: flex; gap: 8px; align-items: center; }

/* ========== Main Layout ========== */
.app-body {
  display: flex;
  height: calc(100vh - 52px);
}

/* ========== Left Sidebar ========== */
.sidebar-left {
  width: 220px;
  background: #fff;
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
}
.sidebar-left .sidebar-tabs {
  display: flex;
  border-bottom: 1px solid var(--border);
}
.sidebar-left .sidebar-tab {
  flex: 1;
  padding: 12px 8px;
  text-align: center;
  font-size: 13px;
  cursor: pointer;
  color: var(--text-secondary);
  border-bottom: 2px solid transparent;
  transition: var(--transition);
  background: none;
  border-top: none;
  border-left: none;
  border-right: none;
}
.sidebar-left .sidebar-tab.active {
  color: var(--primary);
  border-bottom-color: var(--primary);
  font-weight: 600;
}
.sidebar-left .sidebar-tab:hover { color: var(--primary); }

.sidebar-content {
  flex: 1;
  overflow-y: auto;
  padding: 12px;
}
.sidebar-content .section-title {
  font-size: 12px;
  color: var(--text-muted);
  margin: 12px 0 8px;
  padding-left: 4px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}
.sidebar-content .section-title:first-child { margin-top: 0; }

.template-card {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px;
  margin-bottom: 8px;
  cursor: pointer;
  transition: var(--transition);
  font-size: 13px;
}
.template-card:hover {
  border-color: var(--primary);
  background: var(--primary-light);
  box-shadow: 0 1px 4px rgba(7,193,96,0.12);
}
.template-card .template-preview {
  background: #fff;
  border-radius: 2px;
  padding: 8px;
  margin-bottom: 6px;
  font-size: 11px;
  color: var(--text-muted);
  border: 1px dashed #ddd;
  min-height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  line-height: 1.4;
}
.template-card .template-name {
  font-weight: 500;
  font-size: 12px;
}

.color-presets {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.color-dot {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  cursor: pointer;
  border: 2px solid transparent;
  transition: var(--transition);
}
.color-dot:hover { border-color: #999; transform: scale(1.1); }

/* ========== Center Editor ========== */
.editor-main {
  flex: 1;
  display: flex;
  flex-direction: column;
  background: #e8ecf1;
  min-width: 0;
}

/* Toolbar */
.editor-toolbar {
  background: #fff;
  border-bottom: 1px solid var(--border);
  padding: 6px 12px;
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  align-items: center;
  flex-shrink: 0;
}
.toolbar-group {
  display: flex;
  gap: 1px;
  align-items: center;
  padding: 0 4px;
}
.toolbar-divider {
  width: 1px;
  height: 24px;
  background: #e8e8e8;
  margin: 0 4px;
}
.tool-btn {
  width: 32px;
  height: 32px;
  border: none;
  background: transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  color: #555;
  font-size: 15px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: var(--transition);
  position: relative;
  flex-shrink: 0;
}
.tool-btn:hover { background: #f0f0f0; color: #333; }
.tool-btn:active, .tool-btn.active { background: #e6e6e6; color: var(--primary); }
.tool-btn svg { width: 16px; height: 16px; }
.tool-btn[title]:hover::after {
  content: attr(title);
  position: absolute;
  bottom: -28px;
  left: 50%;
  transform: translateX(-50%);
  background: #333;
  color: #fff;
  font-size: 11px;
  padding: 3px 8px;
  border-radius: 3px;
  white-space: nowrap;
  z-index: 10;
  pointer-events: none;
}

.tool-select {
  height: 32px;
  border: 1px solid #e0e0e0;
  border-radius: var(--radius-sm);
  padding: 0 6px;
  font-size: 13px;
  color: #555;
  background: #fff;
  cursor: pointer;
  outline: none;
  font-family: var(--font-stack);
}
.tool-select:focus { border-color: var(--primary); }

.color-picker-wrap {
  position: relative;
  display: flex;
  align-items: center;
}
.color-indicator {
  width: 18px;
  height: 3px;
  position: absolute;
  bottom: 3px;
  left: 50%;
  transform: translateX(-50%);
  border-radius: 2px;
}

/* Editor scroll area */
.editor-scroll {
  flex: 1;
  overflow-y: auto;
  padding: 24px 40px;
  display: flex;
  justify-content: center;
  align-items: flex-start;
}

/* Article card */
.editor-card {
  background: #fff;
  width: 100%;
  max-width: var(--editor-max-width);
  min-height: calc(100% - 48px);
  box-shadow: var(--shadow-lg);
  border-radius: 4px;
  padding: 32px 28px 80px;
  position: relative;
}

/* Article meta fields */
.article-meta {
  margin-bottom: 20px;
}
.article-title-input {
  width: 100%;
  border: none;
  border-bottom: 1px dashed transparent;
  font-size: 22px;
  font-weight: 700;
  color: #1a1a1a;
  padding: 8px 0;
  outline: none;
  font-family: var(--font-stack);
  resize: none;
  line-height: 1.4;
  transition: var(--transition);
  overflow: hidden;
  white-space: pre-wrap;
  word-wrap: break-word;
}
.article-title-input:focus { border-bottom-color: var(--primary); }
.article-title-input::placeholder { color: #bbb; font-weight: 400; font-size: 18px; }

.article-author-input {
  width: 100%;
  border: none;
  font-size: 14px;
  color: #666;
  padding: 4px 0;
  outline: none;
  font-family: var(--font-stack);
  margin-bottom: 16px;
}
.article-author-input::placeholder { color: #ccc; }

/* Rich text editing area */
.editor-content {
  outline: none;
  min-height: 400px;
  font-size: 15px;
  line-height: 1.8;
  color: #3e3e3e;
  word-break: break-word;
  cursor: text;
}
.editor-content:empty::before {
  content: '在这里开始写作...\A按 Ctrl+V 可粘贴内容,建议先清除格式后再排版';
  color: #ccc;
  font-size: 14px;
  white-space: pre;
  line-height: 1.6;
}
.editor-content h1 { font-size: 22px; font-weight: 700; margin: 16px 0 10px; line-height: 1.4; }
.editor-content h2 { font-size: 20px; font-weight: 700; margin: 14px 0 8px; line-height: 1.4; }
.editor-content h3 { font-size: 17px; font-weight: 600; margin: 12px 0 8px; line-height: 1.4; }
.editor-content p { margin: 0 0 8px; min-height: 1em; }
.editor-content blockquote {
  border-left: 3px solid #07c160;
  padding: 8px 16px;
  margin: 12px 0;
  background: #f9fdfb;
  color: #666;
  font-size: 14px;
}
.editor-content ul, .editor-content ol { padding-left: 24px; margin: 8px 0; }
.editor-content li { margin: 4px 0; }
.editor-content img {
  max-width: 100%;
  height: auto;
  display: block;
  margin: 16px auto;
  border-radius: 4px;
  cursor: pointer;
  transition: outline 0.1s;
  outline: 2px solid transparent;
}
.editor-content img.selected {
  outline: 2px solid #07c160;
  outline-offset: 3px;
}
.editor-content img.resizable {
  resize: both;
  overflow: auto;
}

/* Image floating toolbar */
.image-toolbar {
  position: fixed;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0,0,0,0.18);
  padding: 4px 6px;
  z-index: 999;
  display: flex;
  gap: 2px;
  align-items: center;
  animation: imgToolbarIn 0.15s ease;
  pointer-events: auto;
}
@keyframes imgToolbarIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
.image-toolbar .img-tool-btn {
  width: 30px;
  height: 30px;
  border: none;
  background: transparent;
  border-radius: 4px;
  cursor: pointer;
  color: #555;
  font-size: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: 0.15s;
}
.image-toolbar .img-tool-btn:hover { background: #f0f0f0; color: #333; }
.image-toolbar .img-tool-btn.danger:hover { background: #fff0f0; color: #e64340; }
.image-toolbar .img-tool-btn.active { background: #e8f8ef; color: #07c160; }
.image-toolbar .img-tool-divider {
  width: 1px; height: 20px; background: #eee; margin: 0 2px;
}
.image-toolbar .img-size-input {
  width: 50px;
  height: 26px;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  text-align: center;
  font-size: 11px;
  color: #555;
  outline: none;
  font-family: var(--font-stack);
}
.image-toolbar .img-size-input:focus { border-color: var(--primary); }
.image-toolbar .img-size-label {
  font-size: 10px; color: #999; margin: 0 2px;
}
.editor-content table {
  border-collapse: collapse;
  width: 100%;
  margin: 12px 0;
  font-size: 14px;
}
.editor-content table td, .editor-content table th {
  border: 1px solid #e0e0e0;
  padding: 8px 12px;
  min-width: 40px;
}
.editor-content hr {
  border: none;
  border-top: 1px solid #e0e0e0;
  margin: 20px 0;
}
.editor-content a { color: #576b95; text-decoration: none; }
.editor-content pre {
  background: #f5f5f5;
  padding: 12px 16px;
  border-radius: 4px;
  font-size: 13px;
  overflow-x: auto;
  margin: 12px 0;
}
.editor-content code {
  background: #f0f0f0;
  padding: 2px 6px;
  border-radius: 2px;
  font-size: 90%;
}

/* ========== Right Panel ========== */
.sidebar-right {
  width: 300px;
  background: #fff;
  border-left: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  overflow-y: auto;
}
.panel-section {
  padding: 16px 20px;
  border-bottom: 1px solid #f0f0f0;
}
.panel-section:last-child { border-bottom: none; }
.panel-label {
  font-size: 13px;
  color: var(--text-secondary);
  margin-bottom: 8px;
  font-weight: 500;
  display: flex;
  align-items: center;
  gap: 6px;
}

/* Cover image */
.cover-upload {
  width: 100%;
  aspect-ratio: 2.35/1;
  background: #f7f8fa;
  border: 2px dashed #e0e0e0;
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: var(--transition);
  position: relative;
  overflow: hidden;
  font-size: 13px;
  color: #999;
  flex-direction: column;
  gap: 6px;
}
.cover-upload:hover { border-color: var(--primary); background: #fafbfb; }
.cover-upload img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.cover-upload.has-image { border-style: solid; }
.cover-upload .remove-cover {
  position: absolute;
  top: 6px;
  right: 6px;
  background: rgba(0,0,0,0.5);
  color: #fff;
  border: none;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  cursor: pointer;
  font-size: 14px;
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 2;
}
.cover-upload.has-image:hover .remove-cover { display: flex; }

/* Summary */
.summary-textarea {
  width: 100%;
  border: 1px solid #e0e0e0;
  border-radius: var(--radius-sm);
  padding: 10px;
  font-size: 13px;
  resize: vertical;
  font-family: var(--font-stack);
  outline: none;
  min-height: 80px;
  color: #555;
  line-height: 1.6;
}
.summary-textarea:focus { border-color: var(--primary); }
.char-count { font-size: 11px; color: #bbb; text-align: right; margin-top: 4px; }

/* Action buttons */
.action-btn {
  width: 100%;
  padding: 10px 16px;
  border: none;
  border-radius: var(--radius-sm);
  font-size: 14px;
  cursor: pointer;
  font-weight: 500;
  transition: var(--transition);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-family: var(--font-stack);
  margin-bottom: 8px;
}
.action-btn.primary {
  background: var(--primary);
  color: #fff;
}
.action-btn.primary:hover { background: var(--primary-hover); }
.action-btn.secondary {
  background: #f0f0f0;
  color: #555;
}
.action-btn.secondary:hover { background: #e4e4e4; }
.action-btn.outline {
  background: #fff;
  color: var(--primary);
  border: 1px solid var(--primary);
}
.action-btn.outline:hover { background: var(--primary-light); }

/* Article info */
.article-info {
  font-size: 12px;
  color: #999;
  line-height: 1.8;
}
.article-info span { color: #666; font-weight: 500; }

/* ========== Modal ========== */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.5);
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  animation: fadeIn 0.15s ease;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }

.modal {
  background: #fff;
  border-radius: 12px;
  box-shadow: 0 20px 60px rgba(0,0,0,0.2);
  width: 90%;
  max-width: 500px;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  animation: slideUp 0.2s ease;
}
@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }

.modal-header {
  padding: 16px 20px;
  border-bottom: 1px solid #f0f0f0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-weight: 600;
  font-size: 15px;
}
.modal-close {
  background: none;
  border: none;
  font-size: 20px;
  cursor: pointer;
  color: #999;
  padding: 0 4px;
  line-height: 1;
}
.modal-close:hover { color: #333; }
.modal-body { padding: 20px; overflow-y: auto; flex: 1; }
.modal-footer {
  padding: 12px 20px;
  border-top: 1px solid #f0f0f0;
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

.modal-input {
  width: 100%;
  border: 1px solid #e0e0e0;
  border-radius: var(--radius-sm);
  padding: 8px 12px;
  font-size: 14px;
  outline: none;
  font-family: var(--font-stack);
  margin-bottom: 12px;
}
.modal-input:focus { border-color: var(--primary); }
.modal-label { font-size: 13px; color: #666; margin-bottom: 4px; font-weight: 500; }

/* ========== Preview Modal ========== */
.preview-phone {
  width: 375px;
  max-height: 75vh;
  background: #fff;
  border-radius: 24px;
  box-shadow: 0 20px 60px rgba(0,0,0,0.3);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.preview-phone-header {
  background: #f8f8f8;
  padding: 12px 16px;
  text-align: center;
  font-size: 13px;
  color: #666;
  border-bottom: 1px solid #eee;
  font-weight: 500;
}
.preview-phone-body {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  font-size: 15px;
  line-height: 1.8;
  color: #3e3e3e;
}
.preview-phone-body h1 { font-size: 20px; margin: 12px 0 8px; }
.preview-phone-body h2 { font-size: 18px; margin: 10px 0 6px; }
.preview-phone-body h3 { font-size: 16px; margin: 8px 0 6px; }
.preview-phone-body p { margin: 0 0 8px; }
.preview-phone-body blockquote { border-left: 3px solid #07c160; padding: 6px 14px; margin: 10px 0; background: #f9fdfb; color: #666; font-size: 14px; }
.preview-phone-body ul, .preview-phone-body ol { padding-left: 22px; margin: 6px 0; }
.preview-phone-body img { max-width: 100%; margin: 10px auto; display: block; border-radius: 4px; }
.preview-phone-body table { border-collapse: collapse; width: 100%; font-size: 13px; }
.preview-phone-body table td, .preview-phone-body table th { border: 1px solid #e0e0e0; padding: 6px 10px; }
.preview-phone-body hr { border: none; border-top: 1px solid #e0e0e0; margin: 16px 0; }

.preview-title { font-size: 20px; font-weight: 700; margin-bottom: 6px; color: #1a1a1a; }
.preview-author { font-size: 13px; color: #999; margin-bottom: 16px; }

/* Source code textarea */
.source-code-textarea {
  width: 100%;
  min-height: 350px;
  border: 1px solid #e0e0e0;
  border-radius: var(--radius-sm);
  padding: 12px;
  font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace;
  font-size: 12px;
  outline: none;
  resize: vertical;
  line-height: 1.6;
  color: #333;
}
.source-code-textarea:focus { border-color: var(--primary); }

/* Toast */
.toast {
  position: fixed;
  top: 20px;
  left: 50%;
  transform: translateX(-50%);
  background: #333;
  color: #fff;
  padding: 10px 24px;
  border-radius: 20px;
  font-size: 14px;
  z-index: 2000;
  animation: toastIn 0.3s ease;
  pointer-events: none;
}
@keyframes toastIn { from { opacity: 0; transform: translateX(-50%) translateY(-10px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }

/* Link dialog inline */
.link-popup {
  position: fixed;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 8px 30px rgba(0,0,0,0.15);
  padding: 12px;
  z-index: 500;
  display: flex;
  gap: 6px;
  align-items: center;
}
.link-popup input {
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  padding: 6px 10px;
  font-size: 13px;
  outline: none;
  width: 240px;
}
.link-popup input:focus { border-color: var(--primary); }

/* ========== Scrollbar ========== */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #aaa; }

/* ========== Responsive ========== */
@media (max-width: 1200px) {
  .sidebar-left { width: 180px; }
  .sidebar-right { width: 260px; }
  .editor-scroll { padding: 20px 24px; }
}
@media (max-width: 900px) {
  .sidebar-left { display: none; }
  .sidebar-right { width: 240px; }
  .editor-scroll { padding: 16px; }
}
</style>
</head>
<body>

<!-- ==================== HEADER ==================== -->
<header class="app-header">
  <div class="logo">
    <svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="4" fill="#07c160"/><path d="M7 12h10M7 8h10M7 16h6" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>
    微信编辑器
  </div>
  <div class="header-actions">
    <button class="tool-btn" title="保存草稿">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
    </button>
    <span style="font-size:12px;color:#999;" id="saveStatus"></span>
  </div>
</header>

<!-- ==================== MAIN BODY ==================== -->
<div class="app-body">

  <!-- ===== LEFT SIDEBAR: Templates & Styles ===== -->
  <aside class="sidebar-left">
    <div class="sidebar-tabs">
      <button class="sidebar-tab active">样式</button>
      <button class="sidebar-tab">模板</button>
      <button class="sidebar-tab">配色</button>
      <button class="sidebar-tab">我的</button>
    </div>
    <div class="sidebar-content" id="sidebarContent">
      <!-- Styles panel -->
      <div id="panel-styles">
        <div class="section-title">标题样式</div>
        <div class="template-card">
          <div class="template-preview"><b style="font-size:20px">主标题</b></div>
          <div class="template-name">大标题 · 22px 加粗</div>
        </div>
        <div class="template-card">
          <div class="template-preview"><b style="font-size:18px">副标题</b></div>
          <div class="template-name">副标题 · 18px 加粗</div>
        </div>
        <div class="template-card">
          <div class="template-preview"><b style="font-size:16px">小标题</b></div>
          <div class="template-name">小标题 · 16px 加粗</div>
        </div>
        <div class="section-title">正文样式</div>
        <div class="template-card">
          <div class="template-preview">正文段落样式</div>
          <div class="template-name">正文 · 15px #3e3e3e</div>
        </div>
        <div class="template-card">
          <div class="template-preview" style="border-left:3px solid #07c160;padding-left:8px;color:#666">引用文字</div>
          <div class="template-name">引用块 · 左边框</div>
        </div>
        <div class="section-title">分隔元素</div>
        <div class="template-card">
          <div class="template-preview">──────</div>
          <div class="template-name">分隔线</div>
        </div>
      </div>
      <!-- Templates panel -->
      <div id="panel-templates" style="display:none;">
        <div class="section-title">文章模板</div>
        <div class="template-card">
          <div class="template-preview" style="flex-direction:column;gap:4px">
            <b style="font-size:14px">📰 图文消息</b>
            <span style="font-size:10px">标题 + 正文 + 引导关注</span>
          </div>
          <div class="template-name">标准图文模板</div>
        </div>
        <div class="template-card">
          <div class="template-preview" style="flex-direction:column;gap:4px">
            <b style="font-size:14px">🎉 节日祝福</b>
            <span style="font-size:10px">节日标题 + 祝福语 + 卡片</span>
          </div>
          <div class="template-name">节日祝福模板</div>
        </div>
        <div class="template-card">
          <div class="template-preview" style="flex-direction:column;gap:4px">
            <b style="font-size:14px">🛒 产品介绍</b>
            <span style="font-size:10px">产品亮点 + 详情 + 购买引导</span>
          </div>
          <div class="template-name">产品推文模板</div>
        </div>
        <div class="template-card">
          <div class="template-preview" style="flex-direction:column;gap:4px">
            <b style="font-size:14px">📚 知识干货</b>
            <span style="font-size:10px">要点列表 + 分段解析</span>
          </div>
          <div class="template-name">知识分享模板</div>
        </div>
      </div>
      <!-- Colors panel -->
      <div id="panel-colors" style="display:none;">
        <div class="section-title">推荐配色</div>
        <div class="color-presets">
          <span class="color-dot" style="background:#07c160" title="微信绿"></span>
          <span class="color-dot" style="background:#576b95" title="链接蓝"></span>
          <span class="color-dot" style="background:#e64340" title="强调红"></span>
          <span class="color-dot" style="background:#f0883a" title="活力橙"></span>
          <span class="color-dot" style="background:#8b5cf6" title="优雅紫"></span>
          <span class="color-dot" style="background:#3e3e3e" title="正文黑"></span>
          <span class="color-dot" style="background:#888" title="次要灰"></span>
          <span class="color-dot" style="background:#b8b8b8" title="浅灰"></span>
          <span class="color-dot" style="background:#f5a623" title="金色"></span>
          <span class="color-dot" style="background:#4a90d9" title="天空蓝"></span>
          <span class="color-dot" style="background:#50c878" title="翡翠绿"></span>
          <span class="color-dot" style="background:#ff6b81" title="粉色"></span>
        </div>
        <div class="section-title">背景配色</div>
        <div class="color-presets">
          <span class="color-dot" style="background:#fffacd" title="淡黄"></span>
          <span class="color-dot" style="background:#e8f8ef" title="浅绿"></span>
          <span class="color-dot" style="background:#e6f0ff" title="浅蓝"></span>
          <span class="color-dot" style="background:#fff0f5" title="浅粉"></span>
          <span class="color-dot" style="background:#f5f0ff" title="浅紫"></span>
          <span class="color-dot" style="background:#fff5e6" title="浅橙"></span>
          <span class="color-dot" style="background:#d9d9d9" title="灰色"></span>
          <span class="color-dot" style="background:#fdf6ec" title="米色"></span>
          <span class="color-dot" style="background:#fef0f0" title="浅红"></span>
          <span class="color-dot" style="background:#f0fdf4" title="薄荷"></span>
          <span class="color-dot" style="background:#fffff0" title="象牙"></span>
          <span class="color-dot" style="background:transparent;border:2px dashed #ccc" title="清除背景"></span>
        </div>
      </div>
      <!-- My Templates panel -->
      <div id="panel-mytemplates" style="display:none;">
        <div class="section-title">📥 导入模板</div>
        <div style="margin-bottom:12px">
          <input type="text" id="templateUrlInput" class="modal-input" placeholder="输入135编辑器模板ID或URL" style="margin-bottom:6px;font-size:12px">
          <div style="display:flex;gap:6px;margin-bottom:6px">
            <button class="action-btn primary" style="flex:1;margin:0;padding:6px 10px;font-size:12px">🔍 获取模板</button>
            <button class="action-btn secondary" style="flex:1;margin:0;padding:6px 10px;font-size:12px">📋 粘贴源码</button>
          </div>
          <div style="font-size:10px;color:#bbb;line-height:1.4">
            支持格式:<br>
            · 模板ID:142601<br>
            · URL:135editor.com/editor_styles/142601.html<br>
            · 直接粘贴HTML源码
          </div>
        </div>
        <div class="section-title" style="display:flex;justify-content:space-between;align-items:center">
          📚 我的模板
          <span style="font-size:10px;font-weight:400;color:#bbb" id="myTemplateCount">0个</span>
        </div>
        <div id="myTemplateList">
          <div style="text-align:center;color:#ccc;font-size:12px;padding:20px 0">暂无模板<br>导入后这里显示</div>
        </div>
        <div class="section-title" style="margin-top:16px">🗑️ 管理</div>
        <button class="action-btn secondary" style="font-size:12px">清空全部模板</button>
        <div style="margin-top:12px;text-align:center">
          <a href="https://www.135editor.com/moban.html" target="_blank" rel="noopener" style="color:#07c160;text-decoration:none;font-size:12px;display:inline-flex;align-items:center;gap:4px">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
            135编辑器模板中心
          </a>
        </div>
      </div>
    </div>
    <!-- Always visible sidebar footer -->
    <div style="border-top:1px solid #f0f0f0;padding:10px 12px;text-align:center;flex-shrink:0">
      <a href="https://www.135editor.com/moban.html" target="_blank" rel="noopener"
         style="color:#07c160;text-decoration:none;font-size:12px;display:inline-flex;align-items:center;gap:5px;font-weight:500;padding:6px 12px;border-radius:6px;background:#f0fdf4;width:100%;justify-content:center;transition:0.15s"
         onmouseover="this.style.background='#e2f8e9'">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
        135编辑器 · 模板中心
      </a>
    </div>
  </aside>

  <!-- ===== CENTER: Editor ===== -->
  <main class="editor-main">
    <!-- Toolbar Row 1 -->
    <div class="editor-toolbar" id="toolbar">
      <div class="toolbar-group">
        <button class="tool-btn" title="撤销">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 102.13-9.36L1 10"/></svg>
        </button>
        <button class="tool-btn" title="重做">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
        </button>
      </div>
      <span class="toolbar-divider"></span>
      <div class="toolbar-group">
        <select class="tool-select" id="fontFamily" style="width:100px">
          <option value="">默认字体</option>
          <option value="PingFang SC, Microsoft YaHei, sans-serif">苹方/雅黑</option>
          <option value="SimSun, STSong, serif">宋体</option>
          <option value="KaiTi, STKaiti, serif">楷体</option>
          <option value="FangSong, STFangsong, serif">仿宋</option>
          <option value="Georgia, serif">Georgia</option>
          <option value="Arial, Helvetica, sans-serif">Arial</option>
        </select>
        <select class="tool-select" id="fontSize" style="width:70px">
          <option value="1">12px</option>
          <option value="2">14px</option>
          <option value="3" selected>15px</option>
          <option value="4">18px</option>
          <option value="5">22px</option>
          <option value="6">28px</option>
          <option value="7">36px</option>
        </select>
      </div>
      <span class="toolbar-divider"></span>
      <div class="toolbar-group">
        <button class="tool-btn" title="加粗"><b>B</b></button>
        <button class="tool-btn" title="斜体"><i>I</i></button>
        <button class="tool-btn" title="下划线"><u>U</u></button>
        <button class="tool-btn" title="删除线"><s>S</s></button>
      </div>
      <span class="toolbar-divider"></span>
      <div class="toolbar-group">
        <div class="color-picker-wrap">
          <button class="tool-btn" title="字体颜色">
            <span style="font-weight:700;font-size:15px">A</span>
            <span class="color-indicator" id="fontColorIndicator" style="background:#3e3e3e"></span>
          </button>
          <input type="color" id="fontColorPicker" value="#3e3e3e" style="position:absolute;opacity:0;width:0;height:0">
        </div>
        <div class="color-picker-wrap">
          <button class="tool-btn" title="背景颜色">
            <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><rect x="2" y="2" width="20" height="20" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/><rect x="5" y="5" width="14" height="5" fill="currentColor" opacity="0.3"/></svg>
            <span class="color-indicator" id="bgColorIndicator" style="background:transparent"></span>
          </button>
          <input type="color" id="bgColorPicker" value="#ffff00" style="position:absolute;opacity:0;width:0;height:0">
        </div>
      </div>
      <span class="toolbar-divider"></span>
      <div class="toolbar-group">
        <button class="tool-btn" title="清除格式">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="7" x2="20" y2="7"/><line x1="10" y1="11" x2="10" y2="21"/><line x1="14" y1="11" x2="14" y2="21"/><path d="M5.41 4L9 20h6l3.59-16"/><line x1="4" y1="4" x2="20" y2="4"/></svg>
        </button>
      </div>
    </div>

    <!-- Toolbar Row 2 -->
    <div class="editor-toolbar" style="border-top:none">
      <div class="toolbar-group">
        <button class="tool-btn" title="左对齐">
          <svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 3h18v2H3zm0 8h12v2H3zm0-4h18v2H3zm0 8h12v2H3z"/></svg>
        </button>
        <button class="tool-btn" title="居中">
          <svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 3h18v2H3zm4 8h10v2H7zM3 7h18v2H3zm4 8h10v2H7z"/></svg>
        </button>
        <button class="tool-btn" title="右对齐">
          <svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 3h18v2H3zm6 8h12v2H9zM3 7h18v2H3zm6 8h12v2H9z"/></svg>
        </button>
        <button class="tool-btn" title="两端对齐">
          <svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 3h18v2H3zm0 8h18v2H3zm0-4h18v2H3zm0 8h18v2H3z"/></svg>
        </button>
      </div>
      <span class="toolbar-divider"></span>
      <div class="toolbar-group">
        <button class="tool-btn" title="首行缩进2字符" style="font-size:12px;width:auto;padding:0 8px;gap:4px">
          <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M3 3h18v2H3zm0 4h10v2H3zm0 12h18v2H3zm0-4h10v2H3z"/><polygon points="13,11 17,13 13,15"/></svg>
          缩进
        </button>
      </div>
      <span class="toolbar-divider"></span>
      <div class="toolbar-group">
        <button class="tool-btn" title="增加缩进">
          <svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 21V3h18v18H3zm2-2h14V5H5v14zm2-8h4v2H7v-2zm0-3h10v2H7V8zm0 6h10v2H7v-2z"/><polygon points="9,10 12,12 9,14"/></svg>
        </button>
        <button class="tool-btn" title="减少缩进">
          <svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 21V3h18v18H3zm2-2h14V5H5v14zm2-8h4v2H7v-2zm0-3h10v2H7V8zm0 6h10v2H7v-2z"/><polygon points="12,10 9,12 12,14"/></svg>
        </button>
      </div>
      <span class="toolbar-divider"></span>
      <div class="toolbar-group">
        <button class="tool-btn" title="段后空行" style="font-size:12px;width:auto;padding:0 8px;gap:3px">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="15" x2="20" y2="15"/><polyline points="8 4 4 9 8 14"/></svg>
          ↲空行
        </button>
      </div>
      <span class="toolbar-divider"></span>
      <div class="toolbar-group">
        <select class="tool-select" style="width:80px" id="letterSpacingSelect">
          <option value="0">字间距</option>
          <option value="0.5">0.5px</option>
          <option value="1">1px</option>
          <option value="1.5">1.5px</option>
          <option value="2">2px</option>
          <option value="3">3px</option>
        </select>
        <select class="tool-select" style="width:80px" id="lineHeightSelect">
          <option value="1.5">行高1.5</option>
          <option value="1.6">行高1.6</option>
          <option value="1.8" selected>行高1.8</option>
          <option value="2">行高2.0</option>
          <option value="2.5">行高2.5</option>
        </select>
      </div>
      <span class="toolbar-divider"></span>
      <div class="toolbar-group">
        <button class="tool-btn" title="插入图片">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
        </button>
        <input type="file" id="imageInput" accept="image/*" style="display:none">
        <button class="tool-btn" title="插入分隔线">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="12" x2="20" y2="12"/></svg>
        </button>
        <button class="tool-btn" title="插入表格">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/></svg>
        </button>
        <button class="tool-btn" title="插入链接">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>
        </button>
      </div>
      <span class="toolbar-divider"></span>
      <div class="toolbar-group">
        <button class="tool-btn" title="无序列表">
          <svg viewBox="0 0 24 24" fill="currentColor"><circle cx="4" cy="6" r="1.5"/><rect x="8" y="5" width="13" height="2"/><circle cx="4" cy="12" r="1.5"/><rect x="8" y="11" width="13" height="2"/><circle cx="4" cy="18" r="1.5"/><rect x="8" y="17" width="13" height="2"/></svg>
        </button>
        <button class="tool-btn" title="有序列表">
          <svg viewBox="0 0 24 24" fill="currentColor"><rect x="8" y="5" width="13" height="2"/><text x="1.5" y="7.5" font-size="8">1.</text><rect x="8" y="11" width="13" height="2"/><text x="1.5" y="13.5" font-size="8">2.</text><rect x="8" y="17" width="13" height="2"/><text x="1.5" y="19.5" font-size="8">3.</text></svg>
        </button>
        <button class="tool-btn" title="引用">
          <svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 17h3l2-4V7H5v6h3l-2 4zm8 0h3l2-4V7h-6v6h3l-2 4z"/></svg>
        </button>
      </div>
      <div style="flex:1"></div>
      <div class="toolbar-group">
        <button class="tool-btn" title="源代码模式" id="sourceBtn">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
        </button>
        <button class="tool-btn" title="手机预览" style="color:#07c160">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
        </button>
        <button class="tool-btn" title="一键复制到微信" style="color:#07c160;font-weight:600;font-size:12px;width:auto;padding:0 10px;gap:4px">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
          微信复制
        </button>
      </div>
    </div>

    <!-- Source code textarea (hidden by default) -->
    <div id="sourceModePanel" style="display:none;padding:12px;background:#fff;border-bottom:1px solid #e0e0e0;">
      <textarea id="sourceCodeTextarea" class="source-code-textarea"></textarea>
      <div style="margin-top:8px;font-size:12px;color:#999;">💡 直接编辑 HTML 源代码,修改完成后点击上方「源代码」按钮切换回可视化模式</div>
    </div>

    <!-- Editor scroll area -->
    <div class="editor-scroll" id="editorScroll">
      <div class="editor-card" id="editorCard">
        <!-- Article meta -->
        <div class="article-meta">
          <textarea class="article-title-input" id="articleTitle" placeholder="请输入文章标题" rows="1"></textarea>
          <input class="article-author-input" id="articleAuthor" placeholder="作者名称(选填)">
        </div>
        <!-- Rich text content -->
        <div class="editor-content" id="editorContent" contenteditable="true"></div>
      </div>
    </div>
  </main>

  <!-- ===== RIGHT PANEL: Article Settings ===== -->
  <aside class="sidebar-right">
    <div class="panel-section">
      <div class="panel-label">📷 封面图片</div>
      <div class="cover-upload" id="coverUpload">
        <svg viewBox="0 0 24 24" fill="none" stroke="#ccc" stroke-width="1.5" width="32" height="32"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
        <span>点击上传封面图</span>
        <span style="font-size:11px">建议尺寸 900×383</span>
        <button class="remove-cover">×</button>
      </div>
      <input type="file" id="coverInput" accept="image/*" style="display:none">
    </div>

    <div class="panel-section">
      <div class="panel-label">📝 文章摘要 <span style="font-weight:400;font-size:11px">(选填)</span></div>
      <textarea class="summary-textarea" id="articleSummary" placeholder="不填写则默认抓取正文前54个字" maxlength="120"></textarea>
      <div class="char-count"><span id="charCount">0</span>/120</div>
    </div>

    <div class="panel-section">
      <div class="panel-label">📊 文章信息</div>
      <div class="article-info" id="articleInfo">
        字数:<span id="wordCount">0</span> 字<br>
        段落:<span id="paraCount">0</span> 段<br>
        图片:<span id="imgCount">0</span> 张
      </div>
    </div>

    <div class="panel-section">
      <div class="panel-label">⚡ 快捷操作</div>
      <button class="action-btn primary">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
        复制到微信公众号
      </button>
      <button class="action-btn outline">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><rect x="5" y="2" width="14" height="20" rx="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
        手机预览
      </button>
      <button class="action-btn secondary">🗑️ 清空内容</button>
      <button class="action-btn secondary">💾 导出 HTML 文件</button>
    </div>

    <div class="panel-section" style="font-size:11px;color:#bbb;text-align:center;padding:12px;">
      数据自动保存在本地浏览器<br>关闭页面不会丢失
    </div>
  </aside>
</div>

<!-- ==================== HIDDEN: Cover input (in right panel) ==================== -->

<!-- ==================== MODALS (created dynamically by JS) ==================== -->
<div id="modalContainer"></div>

<!-- ==================== TOAST ==================== -->
<div id="toastContainer"></div>

<!-- ==================== SCRIPTS ==================== -->
<script>
// ============ Initialization ============
const editor = document.getElementById('editorContent');
const titleInput = document.getElementById('articleTitle');
const authorInput = document.getElementById('articleAuthor');
const summaryInput = document.getElementById('articleSummary');
let sourceMode = false;
let coverDataUrl = null;

// Load saved draft
window.addEventListener('DOMContentLoaded', () => {
  loadDraft();
  updateArticleInfo();
  // Set default fontSize
  setTimeout(() => {
    if (!editor.innerHTML || editor.innerHTML === '<br>' || editor.innerHTML === '<br>') {
      // Don't force content, just ensure cursor works
    }
    editor.focus();
  }, 100);
});

// Auto-save on changes
let autoSaveTimer;
editor.addEventListener('input', debounceAutoSave);
titleInput.addEventListener('input', debounceAutoSave);
authorInput.addEventListener('input', debounceAutoSave);
summaryInput.addEventListener('input', debounceAutoSave);

// Track selection changes to update toolbar state
document.addEventListener('selectionchange', updateToolbarState);

function debounceAutoSave() {
  clearTimeout(autoSaveTimer);
  autoSaveTimer = setTimeout(() => { saveDraft(); updateArticleInfo(); }, 500);
}

// ============ Auto-resize title ============
function autoResizeTitle() {
  titleInput.style.height = 'auto';
  titleInput.style.height = titleInput.scrollHeight + 'px';
}

// ============ Toolbar Commands ============
function exec(command, value) {
  editor.focus();
  document.execCommand(command, false, value || null);
  updateToolbarState();
}

function clearFormat() {
  editor.focus();
  const sel = window.getSelection();
  if (!sel.rangeCount || sel.isCollapsed) {
    showToast('⚠️ 请先选中要清除格式的文字');
    return;
  }

  const range = sel.getRangeAt(0);
  // Only process if there's actual selected content
  if (range.collapsed) return;

  // Extract the selected HTML fragment
  const fragment = range.extractContents();
  const wrapper = document.createElement('div');
  wrapper.appendChild(fragment);

  // Recursively strip formatting from all elements
  function stripFormatting(node) {
    const children = Array.from(node.childNodes);
    children.forEach(child => {
      if (child.nodeType === 1) { // Element node
        const el = child;
        // Keep structural elements, strip their inline formatting
        const structuralTags = ['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
          'BLOCKQUOTE', 'UL', 'OL', 'LI', 'TABLE', 'TBODY', 'TR', 'TD', 'TH',
          'HR', 'BR', 'IMG', 'A', 'STRONG', 'B', 'EM', 'I', 'U', 'S', 'DEL',
          'SUB', 'SUP', 'CODE', 'PRE', 'SPAN'];

        const tagName = el.tagName;
        if (!structuralTags.includes(tagName)) {
          // Unwrap non-structural elements (font, etc.)
          while (el.firstChild) {
            el.parentNode.insertBefore(el.firstChild, el);
          }
          el.parentNode.removeChild(el);
          return;
        }

        // Strip inline style
        el.removeAttribute('style');
        // Strip class (template-specific classes)
        el.removeAttribute('class');
        // Strip color/size/face attributes (legacy)
        el.removeAttribute('color');
        el.removeAttribute('size');
        el.removeAttribute('face');
        el.removeAttribute('bgcolor');
        el.removeAttribute('align');
        el.removeAttribute('valign');
        // Strip inline event handlers
        Array.from(el.attributes).forEach(attr => {
          if (attr.name.startsWith('on')) el.removeAttribute(attr.name);
        });

        // Recurse into child nodes
        stripFormatting(el);
      }
      // Text nodes are left as-is
    });
  }

  stripFormatting(wrapper);

  // Restore B/STRONG → bold, EM/I → italic, etc. by converting them to execCommand-friendly form
  // (they're already kept as tags above, so this is fine)

  // Re-insert the cleaned content
  range.insertNode(wrapper);

  // Normalize: merge adjacent text nodes
  wrapper.normalize();

  // Unwrap the temporary wrapper div, moving children up
  while (wrapper.firstChild) {
    wrapper.parentNode.insertBefore(wrapper.firstChild, wrapper);
  }
  wrapper.parentNode.removeChild(wrapper);

  // Restore selection
  sel.removeAllRanges();
  updateToolbarState();
  saveDraft();
}

function setFontColor(color) {
  editor.focus();
  document.execCommand('foreColor', false, color);
  document.getElementById('fontColorIndicator').style.background = color;
  document.getElementById('fontColorPicker').value = color;
}

function setBgColor(color) {
  editor.focus();
  if (color === 'transparent') {
    // Only remove background color, preserve all other formatting
    document.execCommand('styleWithCSS', false, true);
    document.execCommand('hiliteColor', false, '#ffffff');
    // Select the span that was just created and remove only its background-color
    setTimeout(() => {
      const sel = window.getSelection();
      if (sel.rangeCount > 0 && !sel.isCollapsed) {
        // Find the span with white background that hiliteColor created
        const spans = editor.querySelectorAll('span[style*="background"]');
        spans.forEach(span => {
          span.style.backgroundColor = '';
          span.style.background = '';
          // If span has no other styles left, unwrap it
          if (!span.getAttribute('style') || span.getAttribute('style').trim() === '') {
            while (span.firstChild) {
              span.parentNode.insertBefore(span.firstChild, span);
            }
            span.parentNode.removeChild(span);
          }
        });
      }
    }, 10);
  } else {
    document.execCommand('hiliteColor', false, color);
  }
  document.getElementById('bgColorIndicator').style.background = color === 'transparent' ? 'transparent' : color;
  document.getElementById('bgColorPicker').value = color === 'transparent' ? '#ffff00' : color;
}

function setLetterSpacing(value) {
  editor.focus();
  document.execCommand('styleWithCSS', false, true);
  const sel = window.getSelection();
  if (sel.rangeCount > 0 && !sel.isCollapsed) {
    // Apply letter-spacing via span
    const range = sel.getRangeAt(0);
    const span = document.createElement('span');
    span.style.letterSpacing = value + 'px';
    try {
      range.surroundContents(span);
    } catch(e) {
      // If selection crosses elements, use alternative approach
      document.execCommand('insertHTML', false, `<span style="letter-spacing:${value}px">${sel.toString()}</span>`);
    }
  }
}

function setLineHeight(value) {
  editor.focus();
  document.execCommand('styleWithCSS', false, true);
  const sel = window.getSelection();
  if (sel.rangeCount > 0 && !sel.isCollapsed) {
    const range = sel.getRangeAt(0);
    const span = document.createElement('span');
    span.style.lineHeight = value;
    try {
      range.surroundContents(span);
    } catch(e) {
      document.execCommand('insertHTML', false, `<span style="line-height:${value}">${sel.toString()}</span>`);
    }
  }
}

// ============ Update toolbar button states ============
function updateToolbarState() {
  const btns = document.querySelectorAll('.tool-btn');
  btns.forEach(b => {
    const cmd = b.getAttribute('onclick')?.match(/exec\('(\w+)'/)?.[1];
    if (cmd && document.queryCommandState) {
      try {
        if (document.queryCommandState(cmd)) {
          b.classList.add('active');
        } else {
          b.classList.remove('active');
        }
      } catch(e) {}
    }
  });
}

// ============ Insert Image ============
function insertImage(event) {
  const file = event.target.files[0];
  if (!file) return;
  const reader = new FileReader();
  reader.onload = function(e) {
    editor.focus();
    // Insert image at cursor or at end
    const img = document.createElement('img');
    img.src = e.target.result;
    img.style.maxWidth = '100%';
    img.style.display = 'block';
    img.style.margin = '16px auto';
    img.style.borderRadius = '4px';

    const sel = window.getSelection();
    if (sel.rangeCount > 0) {
      const range = sel.getRangeAt(0);
      range.deleteContents();
      range.insertNode(img);
      // Move cursor after image
      range.setStartAfter(img);
      range.collapse(true);
      sel.removeAllRanges();
      sel.addRange(range);
    } else {
      editor.appendChild(img);
    }
    updateArticleInfo();
    saveDraft();
  };
  reader.readAsDataURL(file);
  event.target.value = '';
}

// ============ Insert Divider ============
function insertDivider() {
  insertHtmlAtEnd('<hr>');
}

function insertBlankLine() {
  editor.focus();
  const sel = window.getSelection();
  if (sel.rangeCount > 0) {
    const range = sel.getRangeAt(0);
    // Collapse to end of current selection, then insert after
    range.collapse(false);
    // Create two empty paragraph elements for a clean gap
    const blankP1 = document.createElement('p');
    blankP1.innerHTML = '<br>';
    const blankP2 = document.createElement('p');
    blankP2.innerHTML = '<br>';
    // Insert both paragraphs at cursor position (in reverse order so they appear in correct order)
    range.insertNode(blankP2);
    range.insertNode(blankP1);
    // Move cursor to the second blank line (between the two)
    range.setStartAfter(blankP2);
    range.collapse(true);
    sel.removeAllRanges();
    sel.addRange(range);
  } else {
    // No cursor? Insert at end
    const p1 = document.createElement('p');
    p1.innerHTML = '<br>';
    const p2 = document.createElement('p');
    p2.innerHTML = '<br>';
    editor.appendChild(p1);
    editor.appendChild(p2);
  }
  editor.scrollTop = editor.scrollHeight;
  saveDraft();
}

// ============ First-line Indent (首行缩进2字符) ============
function indentFirstLine() {
  editor.focus();
  const sel = window.getSelection();
  if (!sel.rangeCount) return;

  const range = sel.getRangeAt(0);
  // Find the block-level ancestor of the current selection
  let node = range.commonAncestorContainer;
  const blockTags = ['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'LI', 'TD', 'TH'];
  while (node && node !== editor) {
    if (node.nodeType === 1 && blockTags.includes(node.tagName)) {
      const curIndent = node.style.textIndent || '';
      // Toggle: if already has 2em indent, remove it; otherwise set it
      if (curIndent === '2em') {
        node.style.textIndent = '';
      } else {
        node.style.textIndent = '2em';
      }
      saveDraft();
      return;
    }
    node = node.parentNode;
  }
  // If no block element found, wrap selection in a styled p
  showToast('⚠️ 请将光标放在段落中再使用首行缩进');
}

// ============ Image Toolbar ============
let selectedImage = null;
let imageToolbar = null;

function createImageToolbar() {
  if (imageToolbar) return imageToolbar;
  const tb = document.createElement('div');
  tb.className = 'image-toolbar';
  tb.style.display = 'none';
  tb.innerHTML = `
    <button class="img-tool-btn" title="左对齐" data-action="align-left">
      <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M3 3h18v2H3zm0 8h10v2H3zm0-4h18v2H3zm0 8h10v2H3z"/></svg>
    </button>
    <button class="img-tool-btn" title="居中" data-action="align-center">
      <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M3 3h18v2H3zm4 8h10v2H7zM3 7h18v2H3zm4 8h10v2H7z"/></svg>
    </button>
    <button class="img-tool-btn" title="右对齐" data-action="align-right">
      <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M3 3h18v2H3zm8 8h10v2H11zM3 7h18v2H3zm8 8h10v2H11z"/></svg>
    </button>
    <span class="img-tool-divider"></span>
    <span class="img-size-label">宽</span>
    <input class="img-size-input" type="number" id="imgWidthInput" value="" placeholder="auto" min="20" max="680" step="10">
    <span class="img-size-label">px</span>
    <button class="img-tool-btn" title="重置宽度" data-action="reset-width">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 102.13-9.36L1 10"/></svg>
    </button>
    <span class="img-tool-divider"></span>
    <button class="img-tool-btn danger" title="删除图片" data-action="delete">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
    </button>
  `;
  document.body.appendChild(tb);

  // Event delegation for toolbar buttons
  tb.addEventListener('click', (e) => {
    const btn = e.target.closest('[data-action]');
    if (!btn || !selectedImage) return;
    const action = btn.dataset.action;
    switch (action) {
      case 'align-left':
        selectedImage.style.margin = '16px auto 16px 0';
        selectedImage.style.display = 'block';
        highlightActiveAlign(tb, 'left');
        break;
      case 'align-center':
        selectedImage.style.margin = '16px auto';
        selectedImage.style.display = 'block';
        highlightActiveAlign(tb, 'center');
        break;
      case 'align-right':
        selectedImage.style.margin = '16px 0 16px auto';
        selectedImage.style.display = 'block';
        highlightActiveAlign(tb, 'right');
        break;
      case 'reset-width':
        selectedImage.style.width = '';
        selectedImage.style.maxWidth = '100%';
        document.getElementById('imgWidthInput').value = '';
        break;
      case 'delete':
        deleteSelectedImage();
        return;
    }
    saveDraft();
  });

  // Width input
  const widthInput = tb.querySelector('#imgWidthInput');
  widthInput.addEventListener('input', () => {
    if (!selectedImage) return;
    const val = parseInt(widthInput.value);
    if (val && val >= 20 && val <= 680) {
      selectedImage.style.width = val + 'px';
      saveDraft();
    } else if (!widthInput.value) {
      selectedImage.style.width = '';
    }
  });

  // Hide toolbar when clicking outside
  document.addEventListener('mousedown', (e) => {
    if (selectedImage && !tb.contains(e.target) && e.target !== selectedImage) {
      // Small delay to allow image click to register first
      setTimeout(() => {
        if (selectedImage && e.target !== selectedImage) {
          deselectImage();
        }
      }, 100);
    }
  });

  imageToolbar = tb;
  return tb;
}

function highlightActiveAlign(tb, align) {
  tb.querySelectorAll('[data-action^="align-"]').forEach(b => b.classList.remove('active'));
  const activeBtn = tb.querySelector('[data-action="align-' + align + '"]');
  if (activeBtn) activeBtn.classList.add('active');
}

function positionToolbar(img) {
  const tb = createImageToolbar();
  const rect = img.getBoundingClientRect();
  const top = rect.top + window.scrollY - 44;
  const left = rect.left + window.scrollX + (rect.width / 2);
  tb.style.display = 'flex';
  tb.style.top = Math.max(8, top) + 'px';
  tb.style.left = Math.max(8, Math.min(left - 120, window.innerWidth - 280)) + 'px';
}

function selectImage(img) {
  // Deselect previous
  if (selectedImage && selectedImage !== img) {
    selectedImage.classList.remove('selected');
  }
  selectedImage = img;
  img.classList.add('selected');
  img.style.outline = '2px solid #07c160';
  img.style.outlineOffset = '3px';
  positionToolbar(img);

  // Update width input
  const widthInput = document.querySelector('#imgWidthInput');
  if (widthInput) {
    const curWidth = img.style.width ? parseInt(img.style.width) : '';
    widthInput.value = curWidth || '';
  }

  // Detect current alignment
  const margin = img.style.margin || getComputedStyle(img).margin;
  const tb = createImageToolbar();
  if (margin.includes('0') && margin.includes('auto') && !margin.includes('0 0 0 auto') && !margin.includes('0 auto 0 0')) {
    // Could be centered: check if left and right are auto
  }
  if (img.style.margin === '16px auto' || img.style.margin === '16px auto 16px auto') {
    highlightActiveAlign(tb, 'center');
  } else if (img.style.margin.includes('16px auto 16px 0') || img.style.margin.includes('0 auto 0 0')) {
    highlightActiveAlign(tb, 'left');
  } else if (img.style.margin.includes('16px 0 16px auto') || img.style.margin.includes('0 0 0 auto')) {
    highlightActiveAlign(tb, 'right');
  } else {
    tb.querySelectorAll('[data-action^="align-"]').forEach(b => b.classList.remove('active'));
  }
}

function deselectImage() {
  if (selectedImage) {
    selectedImage.classList.remove('selected');
    selectedImage.style.outline = '2px solid transparent';
    selectedImage = null;
  }
  if (imageToolbar) {
    imageToolbar.style.display = 'none';
  }
}

function deleteSelectedImage() {
  if (!selectedImage) return;
  const img = selectedImage;
  deselectImage();
  img.parentNode.removeChild(img);
  updateArticleInfo();
  saveDraft();
}

// Click handler on editor to detect image clicks
editor.addEventListener('click', (e) => {
  const target = e.target;
  if (target.tagName === 'IMG') {
    e.stopPropagation();
    selectImage(target);
  } else {
    deselectImage();
  }
});

// Keyboard: Delete/Backspace to remove selected image
editor.addEventListener('keydown', (e) => {
  if (selectedImage && (e.key === 'Delete' || e.key === 'Backspace')) {
    e.preventDefault();
    deleteSelectedImage();
  }
  // Escape to deselect
  if (e.key === 'Escape' && selectedImage) {
    deselectImage();
  }
});

// ============ Link Dialog ============
function showLinkDialog() {
  editor.focus();
  const sel = window.getSelection();
  const selectedText = sel.toString() || '';

  // Remove existing popup
  document.querySelectorAll('.link-popup').forEach(p => p.remove());

  const popup = document.createElement('div');
  popup.className = 'link-popup';
  popup.innerHTML = `
    <input type="text" id="linkText" placeholder="链接文字" value="${escapeHtml(selectedText)}" style="width:120px">
    <input type="url" id="linkUrl" placeholder="https://..." value="">
    <button class="action-btn primary" style="margin:0;padding:6px 14px;font-size:12px;white-space:nowrap">确定</button>
    <button style="background:none;border:none;cursor:pointer;color:#999;font-size:16px;padding:0 4px">×</button>
  `;

  document.body.appendChild(popup);

  // Position near selection
  if (sel.rangeCount > 0) {
    const rect = sel.getRangeAt(0).getBoundingClientRect();
    popup.style.top = (rect.bottom + window.scrollY + 8) + 'px';
    popup.style.left = Math.min(rect.left + window.scrollX, window.innerWidth - 400) + 'px';
  } else {
    popup.style.top = '200px';
    popup.style.left = '50%';
    popup.style.transform = 'translateX(-50%)';
  }

  document.getElementById('linkUrl').focus();
}

function insertLink() {
  const text = document.getElementById('linkText').value || '链接';
  const url = document.getElementById('linkUrl').value || '#';
  editor.focus();
  document.execCommand('createLink', false, url);
  // Update link text if selection was replaced
  const sel = window.getSelection();
  if (sel.rangeCount > 0) {
    const range = sel.getRangeAt(0);
    const linkEl = range.commonAncestorContainer;
    if (linkEl.nodeType === 3) {
      // Text node parent might be the link
    }
  }
  document.querySelectorAll('.link-popup').forEach(p => p.remove());
  saveDraft();
}

// ============ Table Dialog ============
function showTableDialog() {
  showModal('插入表格', `
    <div class="modal-label">行数</div>
    <input class="modal-input" type="number" id="tableRows" value="3" min="1" max="20">
    <div class="modal-label">列数</div>
    <input class="modal-input" type="number" id="tableCols" value="3" min="1" max="10">
  `, () => {
    const rows = parseInt(document.getElementById('tableRows').value) || 3;
    const cols = parseInt(document.getElementById('tableCols').value) || 3;
    insertTable(rows, cols);
  });
}

function insertTable(rows, cols) {
  editor.focus();
  let html = '<table style="border-collapse:collapse;width:100%;margin:12px 0;font-size:14px"><tbody>';
  for (let r = 0; r < rows; r++) {
    html += '<tr>';
    for (let c = 0; c < cols; c++) {
      html += `<td style="border:1px solid #e0e0e0;padding:8px 12px;min-width:40px"> </td>`;
    }
    html += '</tr>';
  }
  html += '</tbody></table>';
  document.execCommand('insertHTML', false, html);
  saveDraft();
}

// ============ Source Mode ============
function toggleSourceMode() {
  sourceMode = !sourceMode;
  const panel = document.getElementById('sourceModePanel');
  const btn = document.getElementById('sourceBtn');
  const editorScroll = document.getElementById('editorScroll');

  if (sourceMode) {
    // Switch to source view
    panel.style.display = 'block';
    document.getElementById('sourceCodeTextarea').value = editor.innerHTML;
    btn.classList.add('active');
  } else {
    // Switch back to visual mode
    editor.innerHTML = document.getElementById('sourceCodeTextarea').value;
    panel.style.display = 'none';
    btn.classList.remove('active');
    saveDraft();
  }
}

function syncSourceToEditor() {
  if (sourceMode) {
    editor.innerHTML = document.getElementById('sourceCodeTextarea').value;
  }
}

// ============ Cursor helpers ============
function moveCursorToEnd() {
  editor.focus();
  const sel = window.getSelection();
  if (sel.rangeCount > 0) {
    const range = sel.getRangeAt(0);
    range.selectNodeContents(editor);
    range.collapse(false); // collapse to end
    sel.removeAllRanges();
    sel.addRange(range);
  }
}

function insertHtmlAtEnd(html) {
  editor.focus();
  const hasContent = editor.textContent && editor.textContent.trim().length > 0;
  const separator = hasContent ? '<p><br></p>' : '';
  const fullHtml = separator + html;

  // Ensure we have a valid range at the end of the editor content
  const sel = window.getSelection();
  let range;

  if (sel.rangeCount > 0) {
    range = sel.getRangeAt(0);
    // If the range is not inside the editor, create a new one at the end
    if (!editor.contains(range.commonAncestorContainer)) {
      range = document.createRange();
      range.selectNodeContents(editor);
      range.collapse(false);
    } else {
      // Move to the very end of the editor content
      range.selectNodeContents(editor);
      range.collapse(false);
    }
  } else {
    range = document.createRange();
    range.selectNodeContents(editor);
    range.collapse(false);
  }

  // Create document fragment from HTML (single reliable insertion path)
  const temp = document.createElement('div');
  temp.innerHTML = fullHtml;
  const fragment = document.createDocumentFragment();
  let child;
  while ((child = temp.firstChild)) {
    fragment.appendChild(child);
  }

  // Remember the last inserted node for cursor positioning
  const lastChild = fragment.lastChild;

  // Insert the fragment
  range.insertNode(fragment);

  // Move cursor after the inserted content
  if (lastChild) {
    range.setStartAfter(lastChild);
    range.collapse(true);
  } else {
    range.collapse(false);
  }
  sel.removeAllRanges();
  sel.addRange(range);

  // Scroll to bottom
  const editorScroll = document.getElementById('editorScroll');
  if (editorScroll) {
    editorScroll.scrollTop = editorScroll.scrollHeight;
  }
  saveDraft();
  updateArticleInfo();
}

// ============ Templates ============
function insertTemplate(type, text) {
  let html = '';
  switch(type) {
    case 'h1':
      html = `<h1>${text}</h1><p> </p>`;
      break;
    case 'h2':
      html = `<h2>${text}</h2><p> </p>`;
      break;
    case 'h3':
      html = `<h3>${text}</h3><p> </p>`;
      break;
    case 'para':
      html = `<p style="font-size:15px;color:#3e3e3e;line-height:1.8">${text}</p>`;
      break;
    case 'quote':
      html = `<blockquote>${text}</blockquote><p> </p>`;
      break;
  }
  insertHtmlAtEnd(html);
}

function insertFullTemplate(type) {
  let html = '';
  switch(type) {
    case 'news':
      html = `<h2 style="text-align:center">📰 标题在这里</h2>
<p style="text-align:center;color:#999;font-size:13px">作者名 · 2026-07-09</p>
<hr>
<p>正文内容从这里开始,编辑你的文章内容。选择文字后可以使用上方的工具栏调整样式。</p>
<p>文章的第二段内容,继续编辑你的文字。</p>
<hr>
<p style="text-align:center;color:#999;font-size:13px">👇 关注我们,获取更多精彩内容</p>`;
      break;
    case 'holiday':
      html = `<h1 style="text-align:center;color:#e64340">🎉 节日快乐!</h1>
<p style="text-align:center;font-size:18px;color:#f0883a">愿你每一天都充满阳光与温暖</p>
<hr>
<blockquote>在这个特别的日子里,送上我最真挚的祝福。愿你事业顺利,家庭美满,身体健康,万事如意!</blockquote>
<p style="text-align:right;color:#888">—— 来自你的朋友</p>`;
      break;
    case 'product':
      html = `<h2 style="text-align:center">🛒 产品名称</h2>
<p style="text-align:center;color:#666">一句话卖点描述</p>
<hr>
<p><b>✨ 核心亮点:</b></p>
<ul><li>亮点一:功能描述</li><li>亮点二:优势说明</li><li>亮点三:用户价值</li></ul>
<hr>
<p style="text-align:center"><b>限时优惠 ¥99</b></p>
<p style="text-align:center;color:#07c160">👇 点击下方立即购买</p>`;
      break;
    case 'knowledge':
      html = `<h2>📚 文章主题</h2>
<p>开篇引入,简述这篇文章要解决的问题和读者的收获。</p>
<h3>一、核心概念</h3>
<p>详细解释核心概念,用通俗易懂的语言让读者理解。</p>
<h3>二、实操步骤</h3>
<ol><li>第一步:准备工作</li><li>第二步:关键操作</li><li>第三步:验证结果</li></ol>
<h3>三、总结</h3>
<blockquote>核心要点回顾,一句话总结最重要的收获。</blockquote>`;
      break;
  }
  insertHtmlAtEnd(html);
  showToast('模板已插入 ✅');
}

// ============ Sidebar Tab Switching ============
function switchSidebarTab(tab, btn) {
  document.querySelectorAll('.sidebar-tab').forEach(t => t.classList.remove('active'));
  btn.classList.add('active');
  ['styles', 'templates', 'colors', 'mytemplates'].forEach(t => {
    document.getElementById('panel-' + t).style.display = t === tab ? 'block' : 'none';
  });
  if (tab === 'mytemplates') renderMyTemplates();
}

// ============ My Templates Management ============
const MY_TEMPLATES_KEY = 'wechat_editor_my_templates';

function getMyTemplates() {
  try {
    const raw = localStorage.getItem(MY_TEMPLATES_KEY);
    return raw ? JSON.parse(raw) : [];
  } catch(e) { return []; }
}

function saveMyTemplates(templates) {
  try {
    localStorage.setItem(MY_TEMPLATES_KEY, JSON.stringify(templates));
  } catch(e) {
    showToast('⚠️ 存储空间不足,请清理旧模板');
  }
}

function addMyTemplate(name, htmlContent) {
  const templates = getMyTemplates();
  // Determine the base URL for resolving relative image paths
  const inputVal = document.getElementById('templateUrlInput').value.trim();
  let baseUrl = '';
  if (/^\d+$/.test(inputVal)) {
    baseUrl = `https://www.135editor.com/editor_styles/${inputVal}.html`;
  } else if (inputVal.startsWith('http')) {
    baseUrl = inputVal;
  } else if (inputVal.includes('editor_styles')) {
    baseUrl = 'https://www.135editor.com/' + inputVal.replace(/^\//, '');
  }

  // Extract styles from the HTML
  const styles = extractStyles(htmlContent);
  // Clean the body content (pass baseUrl for resolving relative image URLs)
  const bodyContent = extractBodyContent(htmlContent, baseUrl);

  const template = {
    id: Date.now().toString(36) + Math.random().toString(36).substr(2, 5),
    name: name || '模板 ' + (templates.length + 1),
    html: bodyContent,
    styles: styles,
    sourceUrl: baseUrl || inputVal,
    createdAt: new Date().toISOString()
  };

  templates.push(template);
  saveMyTemplates(templates);
  renderMyTemplates();
  document.getElementById('templateUrlInput').value = '';
  showToast('✅ 模板已缓存到本地!');
}

function extractStyles(html) {
  // Extract all <style> blocks
  const styleMatches = html.match(/<style[^>]*>([\s\S]*?)<\/style>/gi) || [];
  let allStyles = '';
  styleMatches.forEach(s => {
    const content = s.replace(/<style[^>]*>/gi, '').replace(/<\/style>/gi, '');
    allStyles += content + '\n';
  });
  // Also collect <link rel="stylesheet"> URLs for reference
  const linkMatches = html.match(/<link[^>]*rel=["']stylesheet["'][^>]*>/gi) || [];
  if (linkMatches.length > 0) {
    allStyles += '\n/* External stylesheets: */\n';
    linkMatches.forEach(l => {
      const hrefMatch = l.match(/href=["']([^"']+)["']/i);
      if (hrefMatch) allStyles += '/* ' + hrefMatch[1] + ' */\n';
    });
  }
  return allStyles.trim();
}

function resolveRelativeUrls(html, baseUrl) {
  // Resolve relative URLs in src, href, and CSS url() to absolute URLs
  if (!baseUrl) return html;

  let baseOrigin;
  try {
    const u = new URL(baseUrl);
    baseOrigin = u.origin;
  } catch(e) { return html; }

  // Create a temporary document to leverage the browser's URL resolution
  const temp = document.createElement('div');
  temp.innerHTML = html;

  // Resolve <img src>, <source src>, <video poster> etc.
  const srcEls = temp.querySelectorAll('[src]');
  srcEls.forEach(el => {
    try {
      const raw = el.getAttribute('src');
      if (raw && !raw.startsWith('data:') && !raw.startsWith('blob:')) {
        el.setAttribute('src', new URL(raw, baseUrl).href);
      }
    } catch(e) {}
  });

  // Resolve <a href> (keep for completeness)
  const hrefEls = temp.querySelectorAll('[href]');
  hrefEls.forEach(el => {
    try {
      const raw = el.getAttribute('href');
      if (raw && !raw.startsWith('data:') && !raw.startsWith('blob:') && !raw.startsWith('javascript:') && !raw.startsWith('#')) {
        el.setAttribute('href', new URL(raw, baseUrl).href);
      }
    } catch(e) {}
  });

  // Resolve url() in inline styles
  const allEls = temp.querySelectorAll('*');
  allEls.forEach(el => {
    if (el.style && el.style.backgroundImage) {
      el.style.backgroundImage = el.style.backgroundImage.replace(
        /url\(\s*["']?(?!data:|https?:|blob:)([^"')]+)["']?\s*\)/gi,
        (match, path) => {
          try {
            return 'url("' + new URL(path.trim(), baseUrl).href + '")';
          } catch(e) { return match; }
        }
      );
    }
    // Also check style attribute string for any url()
    const styleAttr = el.getAttribute('style');
    if (styleAttr && /url\(/i.test(styleAttr)) {
      const resolved = styleAttr.replace(
        /url\(\s*["']?(?!data:|https?:|blob:)([^"')]+)["']?\s*\)/gi,
        (match, path) => {
          try {
            return 'url("' + new URL(path.trim(), baseUrl).href + '")';
          } catch(e) { return match; }
        }
      );
      el.setAttribute('style', resolved);
    }
  });

  return temp.innerHTML;
}

function unwrapSvgContent(html) {
  // 135 编辑器的「SVG 布局模式」模板会把所有真实内容(封面大图、文字等)
  // 包进 <svg><foreignObject>...</foreignObject></svg>。旧逻辑直接把整个 <svg>
  // 删掉,导致顶部封面大图(完全由 SVG 包裹)被整体抹掉。
  // 这里把 <foreignObject> 内部的真实 HTML 解包出来保留,再移除空壳 <svg>,
  // 既恢复顶部大图,又顺手把 SVG 模板转成可编辑的 HTML。
  if (!html) return html;
  // 1) 用 foreignObject 内部 HTML 替换 foreignObject 本身
  html = html.replace(/<foreignobject\b[^>]*>([\s\S]*?)<\/foreignobject>/gi, '$1');
  // 2) 移除残留的 <svg> 外壳(空占位或纯矢量装饰)
  html = html.replace(/<svg\b[^>]*>([\s\S]*?)<\/svg>/gi, '$1');
  // 3) 兜底:清掉任何未配对的 <svg> 开/闭标签
  html = html.replace(/<svg\b[^>]*>/gi, '');
  html = html.replace(/<\/svg>/gi, '');
  return html;
}

function extractBodyContent(html, sourceUrl) {
  // Step 1: Get body content
  let bodyHtml = html;
  const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
  if (bodyMatch) {
    bodyHtml = bodyMatch[1];
  }

  // Step 2: Remove non-content elements (nav, header, footer, aside, script, style, svg等)
  bodyHtml = bodyHtml.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
  bodyHtml = bodyHtml.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
  bodyHtml = bodyHtml.replace(/<noscript[^>]*>[\s\S]*?<\/noscript>/gi, '');
  bodyHtml = bodyHtml.replace(/<header[^>]*>[\s\S]*?<\/header>/gi, '');
  bodyHtml = bodyHtml.replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, '');
  bodyHtml = bodyHtml.replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, '');
  bodyHtml = bodyHtml.replace(/<aside[^>]*>[\s\S]*?<\/aside>/gi, '');
  bodyHtml = bodyHtml.replace(/<iframe[^>]*>[\s\S]*?<\/iframe>/gi, '');
  // Keep SVG-mode template content: unwrap <foreignObject> and drop empty <svg> shells.
  // (Previously every <svg> was deleted here, which wiped the top banner / cover image.)
  bodyHtml = unwrapSvgContent(bodyHtml);

  // Step 3: Use DOMParser to find sections properly (handles nested tags)
  let foundSection = false;
  try {
    const parser = new DOMParser();
    const doc = parser.parseFromString(bodyHtml, 'text/html');
    const sections = doc.querySelectorAll('section');

    if (sections.length > 0) {
      // Only keep TOP-LEVEL sections (no <section> ancestor). Nested sections are
      // already contained inside their top-level parent's outerHTML, so including
      // them again would duplicate the whole template (the "循环/重复" bug).
      const topLevel = Array.from(sections).filter(s => {
        let p = s.parentElement;
        while (p && p !== doc.documentElement && p.tagName.toLowerCase() !== 'section') {
          p = p.parentElement;
        }
        return !(p && p.tagName.toLowerCase() === 'section');
      });

      // Filter top-level sections with meaningful content (> 200 chars innerHTML).
      // Keeping every top-level section preserves the top banner, main content,
      // footer, etc. even when the template splits them into sibling sections.
      const contentSections = topLevel.filter(s => s.innerHTML.length > 200);

      if (contentSections.length > 0) {
        bodyHtml = contentSections.map(s => s.outerHTML).join('\n');
        foundSection = true;
      }
    }
  } catch(e) {
    // DOMParser failed — fall back to regex approach
    const sectionMatches = bodyHtml.match(/<section[\s>][\s\S]*?<\/section>/gi);
    if (sectionMatches && sectionMatches.length > 0) {
      // Keep only outermost sections: drop any match fully contained inside
      // another match (those are nested sections, would duplicate content).
      const outer = sectionMatches.filter(m =>
        !sectionMatches.some(other => other !== m && other.length > m.length && other.indexOf(m) !== -1)
      );
      const contentSections = outer.filter(s => s.length > 200);
      if (contentSections.length > 0) {
        bodyHtml = contentSections.join('\n');
        foundSection = true;
      }
    }
  }

  // Step 4: Try to find main content containers (only if no sections found)
  if (!foundSection) {
    const contentSelectors = [
      /<div[^>]*(?:class|id)=["'][^"']*(?:content|main|article|post|entry|editor|preview|style-content|template|wrap)[^"']*["'][^>]*>[\s\S]*?<\/div>/gi,
      /<article[^>]*>[\s\S]*?<\/article>/gi,
      /<main[^>]*>[\s\S]*?<\/main>/gi,
    ];
    for (const selector of contentSelectors) {
      const matches = bodyHtml.match(selector);
      if (matches && matches.length > 0) {
        const meaningful = matches.filter(m => m.length > 300);
        if (meaningful.length > 0) {
          bodyHtml = meaningful.join('\n');
          break;
        }
      }
    }
  }

  // Step 5: Clean up HTML comments
  bodyHtml = bodyHtml.replace(/<!--[\s\S]*?-->/g, '');

  // Step 6: Only strip obvious tracking pixels
  bodyHtml = bodyHtml.replace(/<img[^>]*\b(?:tracking|analytics|pixel|beacon|stats|count)[^>]*>/gi, '');

  // Step 7: Resolve relative image URLs to absolute (using the source page as base)
  if (sourceUrl) {
    bodyHtml = resolveRelativeUrls(bodyHtml, sourceUrl);
  }

  // Step 8: If result is too short, fall back to full body content (without scripts/styles)
  if (!bodyHtml || bodyHtml.trim().length < 50) {
    // Remove all tags and check if there's text
    const textOnly = bodyHtml.replace(/<[^>]*>/g, '').trim();
    if (textOnly.length < 20) {
      // Last-ditch: return cleaned full body
      const cleanedBody = bodyHtml.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
      if (cleanedBody.length > 20) {
        return bodyHtml; // There IS content, just couldn't isolate it
      }
      return '(未能提取到有效内容,请确认源码正确)';
    }
  }

  return bodyHtml.trim();
}

function deleteMyTemplate(id) {
  let templates = getMyTemplates();
  templates = templates.filter(t => t.id !== id);
  saveMyTemplates(templates);
  renderMyTemplates();
  showToast('🗑️ 模板已删除');
}

function clearAllMyTemplates() {
  if (getMyTemplates().length === 0) {
    showToast('没有可清空的模板');
    return;
  }
  showModal('清空全部模板', `
    <p style="text-align:center;color:#666;padding:20px 0">确定要删除所有自定义模板吗?<br>此操作不可撤销。</p>
  `, () => {
    saveMyTemplates([]);
    renderMyTemplates();
    showToast('🗑️ 全部模板已清空');
  });
}

function useMyTemplate(id) {
  const templates = getMyTemplates();
  const tpl = templates.find(t => t.id === id);
  if (!tpl) { showToast('❌ 模板未找到'); return; }

  // Check that the template has actual content
  let contentHtml = (tpl.html || '').trim();
  if (!contentHtml || contentHtml.length < 20 ||
      contentHtml === '(未能提取到有效内容,请确认源码正确)') {
    showToast('⚠️ 模板内容为空,请重新导入');
    return;
  }

  // Resolve relative URLs at insertion time (second pass, also handles old templates
  // that were saved before the extractBodyContent fix)
  if (tpl.sourceUrl) {
    contentHtml = resolveRelativeUrls(contentHtml, tpl.sourceUrl);
  }

  // Generate a unique class name to scope styles
  const scopeClass = 'tpl-scope-' + Date.now().toString(36);

  // Inject scoped styles into document head (not contenteditable,
  // because execCommand('insertHTML') strips <style> tags in many browsers)
  if (tpl.styles && tpl.styles.trim()) {
    // Remove any previous style block with the same scope class
    const oldStyle = document.getElementById(scopeClass);
    if (oldStyle) oldStyle.remove();

    const styleEl = document.createElement('style');
    styleEl.id = scopeClass;

    // Resolve relative URLs in CSS (background images etc.) before scoping
    let cssText = tpl.styles;
    if (tpl.sourceUrl) {
      cssText = cssText.replace(
        /url\(\s*["']?(?!data:|https?:|blob:)([^"')]+)["']?\s*\)/gi,
        (match, path) => {
          try {
            return 'url("' + new URL(path.trim(), tpl.sourceUrl).href + '")';
          } catch(e) { return match; }
        }
      );
    }

    // Scope the CSS by prefixing each selector
    const rules = cssText.split('}');
    const scopedRules = rules.map(rule => {
      rule = rule.trim();
      if (!rule) return '';
      if (rule.startsWith('/*') || rule.startsWith('*') || rule.startsWith('@')) return rule + '}';
      const parts = rule.split('{');
      if (parts.length < 2) return rule;
      const selectors = parts[0].trim();
      const body = parts.slice(1).join('{').trim();
      const scopedSelectors = selectors.split(',').map(s => {
        s = s.trim();
        if (!s) return '';
        // Don't scope html/body selectors
        if (s === 'html' || s === 'body') return s;
        return '.' + scopeClass + ' ' + s;
      }).filter(Boolean).join(',\n');
      return scopedSelectors + ' {\n' + body + '\n}';
    }).filter(Boolean).join('\n');
    styleEl.textContent = scopedRules;
    document.head.appendChild(styleEl);
  }

  // Insert only the wrapper div + content into the editor
  const wrappedHtml = '<div class="' + scopeClass + '" style="max-width:100%;width:100%;box-sizing:border-box;overflow:hidden;word-wrap:break-word">' + contentHtml + '</div>';

  insertHtmlAtEnd(wrappedHtml);
  showToast('✅ 模板已插入');
}

function renderMyTemplates() {
  const templates = getMyTemplates();
  const container = document.getElementById('myTemplateList');
  const countEl = document.getElementById('myTemplateCount');
  if (countEl) countEl.textContent = templates.length + '个';

  if (!container) return;

  if (templates.length === 0) {
    container.innerHTML = '<div style="text-align:center;color:#ccc;font-size:12px;padding:20px 0">暂无模板<br>导入后这里显示</div>';
    return;
  }

  container.innerHTML = templates.map((t, i) => `
    <div class="template-card" style="position:relative">
      <div class="template-preview" style="max-height:50px;overflow:hidden;font-size:10px;text-align:left">
        ${t.html.replace(/<[^>]*>/g, ' ').substring(0, 80) || '(空模板)'}
      </div>
      <div class="template-name" style="display:flex;justify-content:space-between;align-items:center">
        <span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escapeHtml(t.name)}">📌 ${escapeHtml(t.name)}</span>
        <span style="display:flex;gap:2px;flex-shrink:0">
          <button class="tool-btn" style="width:22px;height:22px;font-size:10px" title="插入">+</button>
          <button class="tool-btn" style="width:22px;height:22px;font-size:10px;color:#e64340" title="删除">×</button>
        </span>
      </div>
      ${t.sourceUrl ? `<div style="font-size:9px;color:#bbb;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-top:2px">来源: ${escapeHtml(t.sourceUrl)}</div>` : ''}
    </div>
  `).join('');
}

async function fetchTemplate() {
  const input = document.getElementById('templateUrlInput').value.trim();
  if (!input) { showToast('⚠️ 请输入模板ID或URL'); return; }

  // Build base URL from various input formats
  let baseUrl = input;
  if (/^\d+$/.test(input)) {
    baseUrl = `https://www.135editor.com/editor_styles/${input}.html`;
  } else if (input.includes('editor_styles') && !input.startsWith('http')) {
    baseUrl = 'https://www.135editor.com/' + input.replace(/^\//, '');
  }
  if (!baseUrl.startsWith('http')) {
    baseUrl = 'https://' + baseUrl;
  }

  showToast('🔍 正在获取模板...');

  // Try both regular URL and preview mode (?preview=1 strips chrome)
  const urlsToTry = [baseUrl];
  if (!baseUrl.includes('preview=1')) {
    urlsToTry.push(baseUrl + (baseUrl.includes('?') ? '&' : '?') + 'preview=1');
  }

  let html = null;
  let lastError = null;

  for (const url of urlsToTry) {
    try {
      const resp = await fetch(url, { mode: 'cors' });
      if (!resp.ok) throw new Error('HTTP ' + resp.status);
      html = await resp.text();
      if (html && html.length > 200) break;
    } catch(e) {
      lastError = e;
    }
  }

  if (html && html.length > 200) {
    const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
    let name = titleMatch ? titleMatch[1].replace(/[\s\|-]+135编辑器.*$/, '').trim() : '';
    if (!name || name.length > 50) {
      const idMatch = baseUrl.match(/(\d+)/);
      name = idMatch ? '模板 ' + idMatch[1] : '导入模板';
    }
    addMyTemplate(name, html);
  } else {
    const corsMsg = (lastError && (lastError.message.includes('Failed to fetch') || lastError.name === 'TypeError'))
      ? '⚠️ 跨域限制,请用「粘贴源码」方式导入'
      : '❌ 获取失败:' + (lastError ? lastError.message : '内容为空');
    showToast(corsMsg);
    if (corsMsg.includes('跨域')) {
      setTimeout(() => showPasteDialog(), 1500);
    }
  }
}

function showPasteDialog() {
  const inputUrl = document.getElementById('templateUrlInput').value.trim();
  let defaultName = '';
  if (/^\d+$/.test(inputUrl)) {
    defaultName = '模板 ' + inputUrl;
  } else if (inputUrl.includes('editor_styles')) {
    const m = inputUrl.match(/(\d+)/);
    defaultName = m ? '模板 ' + m[1] : '';
  }

  showModal('粘贴模板源码', `
    <div class="modal-label">模板名称</div>
    <input class="modal-input" type="text" id="pasteTemplateName" placeholder="给模板起个名字" value="${escapeHtml(defaultName)}">
    <div class="modal-label">HTML 源码</div>
    <textarea class="source-code-textarea" id="pasteTemplateHtml" placeholder="将网页的 HTML 源代码粘贴到这里..." style="min-height:200px"></textarea>
    <div style="font-size:11px;color:#999;margin-top:4px;line-height:1.6">
      💡 <b>方法一(推荐)</b>:打开模板页面 → F12 开发者工具 → Elements 面板 → 找到 <b><section></b> 标签 → 右键 Copy → Copy outerHTML → 粘贴到这里<br>
      💡 <b>方法二</b>:右键「查看网页源代码」(Ctrl+U) → 全选复制 → 粘贴到这里(会自动提取 section 内容)<br>
      💡 <b>方法三</b>:模板页面 URL 加 <b>?preview=1</b> 参数后,复制页面源码(内容更干净)
    </div>
  `, () => {
    const name = document.getElementById('pasteTemplateName').value.trim() || '导入模板';
    const html = document.getElementById('pasteTemplateHtml').value.trim();
    if (!html) { showToast('⚠️ 请粘贴HTML源码'); return; }
    addMyTemplate(name, html);
  });
}

// ============ Cover Image ============
function setCoverImage(event) {
  const file = event.target.files[0];
  if (!file) return;
  const reader = new FileReader();
  reader.onload = function(e) {
    coverDataUrl = e.target.result;
    const upload = document.getElementById('coverUpload');
    upload.innerHTML = `
      <img src="${coverDataUrl}" alt="封面图">
      <button class="remove-cover">×</button>
    `;
    upload.classList.add('has-image');
    saveDraft();
  };
  reader.readAsDataURL(file);
  event.target.value = '';
}

function removeCover() {
  coverDataUrl = null;
  const upload = document.getElementById('coverUpload');
  upload.classList.remove('has-image');
  upload.innerHTML = `
    <svg viewBox="0 0 24 24" fill="none" stroke="#ccc" stroke-width="1.5" width="32" height="32"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
    <span>点击上传封面图</span>
    <span style="font-size:11px">建议尺寸 900×383</span>
    <button class="remove-cover">×</button>
  `;
  saveDraft();
}

// ============ Article Info ============
function updateArticleInfo() {
  const text = editor.innerText || '';
  const html = editor.innerHTML || '';

  // Word count
  const chineseChars = (text.match(/[一-鿿]/g) || []).length;
  const words = text.trim() ? text.trim().split(/\s+/).length : 0;
  document.getElementById('wordCount').textContent = chineseChars + words;

  // Paragraph count
  const paras = editor.querySelectorAll('p, div, h1, h2, h3, h4, h5, h6, blockquote, li').length || 1;
  document.getElementById('paraCount').textContent = paras;

  // Image count
  const imgs = editor.querySelectorAll('img').length;
  document.getElementById('imgCount').textContent = imgs;

  // Summary char count
  document.getElementById('charCount').textContent = summaryInput.value.length;
}

function updateCharCount() {
  document.getElementById('charCount').textContent = summaryInput.value.length;
}

// ============ Preview ============
function showPreview() {
  const title = titleInput.value || '文章标题';
  const author = authorInput.value || '';
  const contentHtml = editor.innerHTML;
  const summary = summaryInput.value || (editor.innerText || '').substring(0, 54);

  const previewHtml = `
    <div class="modal-overlay">
      <div class="preview-phone">
        <div class="preview-phone-header">📱 手机预览</div>
        <div class="preview-phone-body">
          <div class="preview-title">${escapeHtml(title)}</div>
          ${author ? `<div class="preview-author">${escapeHtml(author)}</div>` : ''}
          ${contentHtml}
          ${summary ? `<hr style="border:none;border-top:1px solid #eee;margin:12px 0"><div style="color:#999;font-size:12px">📝 ${escapeHtml(summary)}</div>` : ''}
        </div>
      </div>
    </div>
  `;
  document.getElementById('modalContainer').innerHTML = previewHtml;
}

// ============ Copy to WeChat ============
function copyToWechat() {
  const title = titleInput.value.trim();
  const author = authorInput.value.trim();
  const contentHtml = editor.innerHTML;

  if (!contentHtml || contentHtml === '<br>') {
    showToast('⚠️ 请先输入文章内容');
    return;
  }

  // Build clean HTML suitable for WeChat Official Account editor
  let wechatHtml = contentHtml;

  // Convert data URLs to placeholder (WeChat doesn't support blob URLs)
  // Keep base64 images as they work in WeChat editor

  // Try the modern clipboard API
  const blob = new Blob([wechatHtml], { type: 'text/html' });
  const clipboardItem = new ClipboardItem({
    'text/html': blob,
    'text/plain': new Blob([editor.innerText || ''], { type: 'text/plain' })
  });

  navigator.clipboard.write([clipboardItem]).then(() => {
    showToast('✅ 已复制!可粘贴到微信公众号后台编辑器中');
  }).catch(() => {
    // Fallback: use execCommand
    fallbackCopy(wechatHtml);
  });
}

function fallbackCopy(html) {
  // Create a temporary container
  const container = document.createElement('div');
  container.innerHTML = html;
  container.style.position = 'fixed';
  container.style.left = '-9999px';
  container.style.top = '0';
  document.body.appendChild(container);

  const range = document.createRange();
  range.selectNodeContents(container);
  const sel = window.getSelection();
  sel.removeAllRanges();
  sel.addRange(range);

  try {
    document.execCommand('copy');
    showToast('✅ 已复制!可粘贴到微信公众号后台编辑器中');
  } catch(e) {
    showToast('❌ 复制失败,请尝试导出HTML或手动复制');
  }

  sel.removeAllRanges();
  document.body.removeChild(container);
}

// ============ Export HTML ============
function exportHTML() {
  const title = titleInput.value || '公众号文章';
  const author = authorInput.value || '';
  const contentHtml = editor.innerHTML;

  const fullHtml = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${escapeHtml(title)}</title>
<style>
  body { max-width: 680px; margin: 0 auto; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif; font-size: 15px; line-height: 1.8; color: #3e3e3e; }
  h1 { font-size: 22px; margin: 16px 0 10px; }
  h2 { font-size: 20px; margin: 14px 0 8px; }
  h3 { font-size: 17px; margin: 12px 0 8px; }
  blockquote { border-left: 3px solid #07c160; padding: 8px 16px; margin: 12px 0; background: #f9fdfb; color: #666; }
  img { max-width: 100%; height: auto; display: block; margin: 16px auto; border-radius: 4px; }
  table { border-collapse: collapse; width: 100%; margin: 12px 0; }
  table td, table th { border: 1px solid #e0e0e0; padding: 8px 12px; }
  hr { border: none; border-top: 1px solid #e0e0e0; margin: 20px 0; }
  .article-title { font-size: 22px; font-weight: 700; color: #1a1a1a; text-align: center; margin-bottom: 4px; }
  .article-author { font-size: 13px; color: #999; text-align: center; margin-bottom: 20px; }
</style>
</head>
<body>
  <h1 class="article-title">${escapeHtml(title)}</h1>
  ${author ? `<p class="article-author">${escapeHtml(author)}</p>` : ''}
  ${contentHtml}
</body>
</html>`;

  const blob = new Blob([fullHtml], { type: 'text/html;charset=UTF-8' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `${title}.html`;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
  showToast('💾 HTML 文件已下载');
}

// ============ Clear All ============
function clearAll() {
  showModal('清空内容', `
    <p style="text-align:center;color:#666;padding:20px 0">确定要清空所有编辑内容吗?<br>此操作不可撤销。</p>
  `, () => {
    editor.innerHTML = '';
    titleInput.value = '';
    authorInput.value = '';
    summaryInput.value = '';
    removeCover();
    document.getElementById('charCount').textContent = '0';
    updateArticleInfo();
    localStorage.removeItem('wechat_editor_draft');
    showToast('🗑️ 内容已清空');
  });
}

// ============ Save / Load Draft ============
function saveDraft() {
  const draft = {
    title: titleInput.value,
    author: authorInput.value,
    content: editor.innerHTML,
    summary: summaryInput.value,
    coverImage: coverDataUrl,
    savedAt: new Date().toISOString()
  };
  try {
    localStorage.setItem('wechat_editor_draft', JSON.stringify(draft));
    document.getElementById('saveStatus').textContent = '已保存 ' + new Date().toLocaleTimeString();
    setTimeout(() => { document.getElementById('saveStatus').textContent = ''; }, 2000);
  } catch(e) {
    // Storage full - likely due to large images
    showToast('⚠️ 自动保存失败,存储空间不足');
  }
}

function loadDraft() {
  try {
    const raw = localStorage.getItem('wechat_editor_draft');
    if (!raw) return;
    const draft = JSON.parse(raw);
    if (draft.title) titleInput.value = draft.title;
    if (draft.author) authorInput.value = draft.author;
    if (draft.content) editor.innerHTML = draft.content;
    if (draft.summary) summaryInput.value = draft.summary;
    if (draft.coverImage) {
      coverDataUrl = draft.coverImage;
      const upload = document.getElementById('coverUpload');
      upload.innerHTML = `
        <img src="${draft.coverImage}" alt="封面图">
        <button class="remove-cover">×</button>
      `;
      upload.classList.add('has-image');
    }
    if (draft.savedAt) {
      const savedTime = new Date(draft.savedAt);
      if (!isNaN(savedTime)) {
        document.getElementById('saveStatus').textContent = '上次保存 ' + savedTime.toLocaleString();
        setTimeout(() => { document.getElementById('saveStatus').textContent = ''; }, 3000);
      }
    }
    autoResizeTitle();
    updateCharCount();
    updateArticleInfo();
  } catch(e) {
    console.error('Failed to load draft:', e);
  }
}

// ============ Modal ============
function showModal(title, bodyHtml, onConfirm) {
  const overlay = document.createElement('div');
  overlay.className = 'modal-overlay';
  overlay.innerHTML = `
    <div class="modal">
      <div class="modal-header">
        <span>${title}</span>
        <button class="modal-close">×</button>
      </div>
      <div class="modal-body">${bodyHtml}</div>
      <div class="modal-footer">
        <button class="action-btn secondary" style="width:auto">取消</button>
        <button class="action-btn primary" style="width:auto" id="modalConfirmBtn">确定</button>
      </div>
    </div>
  `;
  overlay.addEventListener('click', function(e) {
    if (e.target === overlay) overlay.remove();
  });
  document.body.appendChild(overlay);

  document.getElementById('modalConfirmBtn').addEventListener('click', () => {
    if (onConfirm) onConfirm();
    overlay.remove();
  });

  // ESC to close
  const escHandler = (e) => {
    if (e.key === 'Escape') {
      overlay.remove();
      document.removeEventListener('keydown', escHandler);
    }
  };
  document.addEventListener('keydown', escHandler);
}

// ============ Toast ============
function showToast(msg) {
  const toast = document.createElement('div');
  toast.className = 'toast';
  toast.textContent = msg;
  document.body.appendChild(toast);
  setTimeout(() => {
    toast.style.opacity = '0';
    toast.style.transition = 'opacity 0.3s';
    setTimeout(() => toast.remove(), 300);
  }, 2000);
}

// ============ Helpers ============
function escapeHtml(str) {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

// ============ Keyboard shortcuts ============
document.addEventListener('keydown', function(e) {
  if (e.ctrlKey || e.metaKey) {
    switch(e.key.toLowerCase()) {
      case 'b': e.preventDefault(); exec('bold'); break;
      case 'i': e.preventDefault(); exec('italic'); break;
      case 'u': e.preventDefault(); exec('underline'); break;
      case 's': e.preventDefault(); saveDraft(); showToast('💾 已保存'); break;
      case 'z':
        if (e.shiftKey) { e.preventDefault(); exec('redo'); }
        break;
    }
  }
});

// ============ Periodic auto-save ============
setInterval(() => {
  if (editor.innerHTML) saveDraft();
  updateArticleInfo();
}, 30000);

// Initial article info update
updateArticleInfo();
</script>
</body>
</html>


免费评分

参与人数 97吾爱币 +91 热心值 +84 收起 理由
兔杀鸡 + 1 + 1 谢谢@Thanks!
Navyzhou + 1 我很赞同!
d8lost + 1 + 1 我很赞同!
BeginForEnd + 1 + 1 用心讨论,共获提升!
normal52 + 1 + 1 牛的牛的
天夜ss + 1 + 1 我很赞同!
akang158 + 1 + 1 谢谢@Thanks!
ylx0605 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
清炒藕片丶 + 1 + 1 我很赞同!
mhaitao + 1 + 1 我很赞同!
chensir01 + 1 + 1 谢谢@Thanks!
wenpenpen + 1 + 1 热心回复!
小坏坏 + 1 + 1 谢谢@Thanks!
zjun777 + 1 + 1 谢谢@Thanks!
cctvboss + 1 + 1 热心回复!
crik2010 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
春秋战国 + 1 + 1 鼓励转贴优秀软件安全工具和文档!
本本010 + 1 + 1 热心回复!
zj_tj + 1 + 1 我很赞同!
petnnar185 + 1 谢谢@Thanks!
苏紫方璇 + 5 + 1 用心讨论,共获提升!
冰水混合物 + 1 我很赞同!
chinalaodeng + 1 + 1 谢谢@Thanks!
zc444 + 1 + 1 谢谢@Thanks!
不着调的君子 + 1 谢谢@Thanks!
mucc + 2 + 1 谢谢@Thanks!
心怀感恩 + 1 + 1 谢谢@Thanks!
gqdsc + 1 + 1 这个必须要支持下
seven2024 + 1 热心回复!
AiMuTiLooy + 1 谢谢@Thanks!
holes + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
ckn011 + 1 + 1 谢谢@Thanks!
Leidus + 1 + 1 谢谢@Thanks!
leger1210 + 1 热心回复!
猪啃菠萝 + 1 + 1 谢谢@Thanks!
wyk0531 + 1 + 1 我很赞同!
shehui0927 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
changgesujiao + 1 + 1 谢谢@Thanks!
aaa661179 + 1 + 1 热心回复!
hinsley + 1 + 1 我很赞同!
5Ekko + 1 + 1 谢谢@Thanks!
zfd914 + 1 + 1 热心回复!
cccfind911 + 1 谢谢@Thanks!
Erdon7c + 1 + 1 热心回复!
deTrident + 1 + 1 谢谢@Thanks!
kw1993 + 1 + 1 谢谢@Thanks!
她说我不如黄瓜 + 1 + 1 谢谢@Thanks!
CDP + 1 + 1 谢谢@Thanks!
子子木木木子子 + 1 我很赞同!
蕤宾廿 + 1 + 1 我很赞同!
coolfenny + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
zcx200601 + 1 下载下来吃灰
784337045 + 1 + 1 我很赞同!
mm0476 + 1 + 1 鼓励转贴优秀软件安全工具和文档!
wanfeng1239 + 1 热心回复!
pntvmin + 1 + 1 谢谢@Thanks!
yuridexiaoyu + 2 + 1 这个就叫做专业
wwm9985 + 1 + 1 我很赞同!
arcool + 1 + 1 非常棒!但是有的135模板导入进来显示异常
MF1998 + 1 + 1 谢谢@Thanks!
浅夏丶莫离 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
yuanpoxue + 1 谢谢@Thanks!
y158520 + 1 + 1 我很赞同!
花心乞丐 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
xiaokanliunian + 1 + 1 热心回复!
yxr1988 + 1 + 1 已经处理,感谢您对吾爱破解论坛的支持!
hello95271 + 1 + 1 我很赞同!
cccc13775 + 1 + 1 谢谢@Thanks!
☆木木可可★ + 1 + 1 谢谢@Thanks!
lin686 + 1 我很赞同!
aa154560 + 1 + 1 期望持久更新!!
ClipperX + 1 + 1 我很赞同!
bugof52pj + 1 谢谢@Thanks!
22yasha + 1 + 1 谢谢@Thanks!
Issacclark1 + 1 谢谢@Thanks!
江南晚來客 + 1 + 1 谢谢@Thanks!
Chwx + 1 + 1 谢谢@Thanks!
wooailein + 1 + 1 热心回复!
禾木2080 + 1 + 1 谢谢@Thanks!
小志在90 + 1 我很赞同!
wanfon + 1 + 1 热心回复!
山田凉粉 + 1 + 1 谢谢@Thanks!
bryanh66 + 1 我很赞同!
cksincerely + 1 + 1 好多年前公众号刚出那会管理过一阵子,现在先收藏。
zsrundev + 1 + 1 很有用,正缺一个公众号编辑器
Sublime + 1 谢谢@Thanks!
快乐王子 + 1 + 1 我很赞同!
laohucai + 1 + 1 谢谢@Thanks!
xueren114 + 1 + 1 我很赞同!
laozhang4201 + 1 + 1 热心回复!
yanglinman + 1 谢谢@Thanks!
GS9452 + 1 谢谢@Thanks!
freeness2006 + 1 谢谢@Thanks!
忘情的城市 + 1 + 1 用心讨论,共获提升!
jfy168 + 1 我很赞同!
darkwarrior + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
我的小苹果 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
RoyPenn 发表于 2026-7-14 11:53
我现在都直接在线md格式做,直接复制粘贴就可以了,AI自动生成
3#
 楼主| 傻瓜炒蛋 发表于 2026-7-14 11:56 |楼主
RoyPenn 发表于 2026-7-14 11:53
我现在都直接在线md格式做,直接复制粘贴就可以了,AI自动生成

md图片是个问题
这个编辑器,完美解决图片问题
4#
w360 发表于 2026-7-14 11:57
RoyPenn 发表于 2026-7-14 11:53
我现在都直接在线md格式做,直接复制粘贴就可以了,AI自动生成

在线在哪个网页推荐下多谢
5#
w360 发表于 2026-7-14 12:00
小白不会用,有成品才好
6#
 楼主| 傻瓜炒蛋 发表于 2026-7-14 12:14 |楼主
w360 发表于 2026-7-14 12:00
小白不会用,有成品才好

源代码复制下来,保存成index.html,双击就能用了
7#
柒渊网络 发表于 2026-7-14 12:24
能本地编辑,这样也能更安全一些
8#
sangzixu 发表于 2026-7-14 12:42
还有简洁方便的吗,天天用秀米,全是收费的
9#
dotcrack 发表于 2026-7-14 12:46
楼主良心贡献,赚狗粮的辅助利器,谢谢楼主
10#
bjtxwz 发表于 2026-7-14 13:01
保存了,可以运行,但没法编辑呢?

如何调整字体,没有反应,其他功能也用不了,请大佬指导一下。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-8-13 10:37

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表