好友
阅读权限20
听众
最后登录1970-1-1
|
本帖最后由 傻瓜炒蛋 于 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>
|
免费评分
-
查看全部评分
|