Delete docs directory
This commit is contained in:
BIN
docs/favicon.ico
BIN
docs/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
584
docs/index.html
584
docs/index.html
@@ -1,584 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>IP地址命令生成器</title>
|
|
||||||
<link rel="icon" href="favicon.ico">
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--primary: #4f46e5;
|
|
||||||
--primary-hover: #4338ca;
|
|
||||||
--secondary: #10b981;
|
|
||||||
--secondary-hover: #059669;
|
|
||||||
--danger: #ef4444;
|
|
||||||
--danger-hover: #dc2626;
|
|
||||||
--text: #1f2937;
|
|
||||||
--text-light: #6b7280;
|
|
||||||
--bg: #f9fafb;
|
|
||||||
--card-bg: #ffffff;
|
|
||||||
--border: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
background-color: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
line-height: 1.6;
|
|
||||||
padding: 20px;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: flex-start;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05), 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
width: 100%;
|
|
||||||
max-width: 800px;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header p {
|
|
||||||
color: var(--text-light);
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-col {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
input, textarea, select {
|
|
||||||
padding: 10px 12px;
|
|
||||||
font-size: 1rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
transition: border-color 0.2s, box-shadow 0.2s;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus, textarea:focus, select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--primary);
|
|
||||||
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#interface {
|
|
||||||
max-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ipInput {
|
|
||||||
min-height: 120px;
|
|
||||||
resize: vertical;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 4px 12px;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 100px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background-color: var(--primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background-color: var(--primary-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background-color: var(--secondary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background-color: var(--secondary-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background-color: var(--danger);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger:hover {
|
|
||||||
background-color: var(--danger-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-container {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-buttons {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-button {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-light);
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-button.active {
|
|
||||||
color: var(--primary);
|
|
||||||
border-bottom: 2px solid var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content {
|
|
||||||
display: none;
|
|
||||||
padding-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-container {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-header h3 {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-box {
|
|
||||||
background-color: #f8fafc;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
min-height: 150px;
|
|
||||||
max-height: 300px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
font-family: 'Roboto Mono', monospace;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
overflow: auto;
|
|
||||||
scrollbar-width: none;
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-box::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.doc-section {
|
|
||||||
margin-top: 20px;
|
|
||||||
padding-top: 16px;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.doc-section h4 {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.doc-section ul {
|
|
||||||
padding-left: 20px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.doc-section ul li {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.doc-section code {
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: 'Roboto Mono', monospace;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.container {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
width: 100%;
|
|
||||||
min-width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#interface {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-row {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="header">
|
|
||||||
<h1>公网IP地址添加命令生成器</h1>
|
|
||||||
<p>为Linux服务器生成添加公网IP的命令</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-container">
|
|
||||||
<div class="tab-buttons">
|
|
||||||
<button class="tab-button active" onclick="changeTab(event, 'ipv4Tab')">IPv4</button>
|
|
||||||
<button class="tab-button" onclick="changeTab(event, 'ipv6Tab')">IPv6</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ipv4Tab" class="tab-content active">
|
|
||||||
<div class="input-section">
|
|
||||||
<div class="input-row">
|
|
||||||
<label for="interface">网卡名称</label>
|
|
||||||
<input type="text" id="interface" value="eth0" placeholder="例如: eth0" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="input-row">
|
|
||||||
<label for="ipInput">公网IPv4地址列表 (每行一个)</label>
|
|
||||||
<textarea id="ipInput" placeholder="例如:
|
|
||||||
203.0.113.10
|
|
||||||
192.0.2.100/24"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="button-group">
|
|
||||||
<button class="btn-primary" onclick="generateCommand('ipv4')">生成命令</button>
|
|
||||||
<button class="btn-danger" onclick="generateCSectionCommand()">生成C段命令</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ipv6Tab" class="tab-content">
|
|
||||||
<div class="input-section">
|
|
||||||
<div class="input-row">
|
|
||||||
<label for="interfaceIpv6">网卡名称</label>
|
|
||||||
<input type="text" id="interfaceIpv6" value="eth0" placeholder="例如: eth0" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="input-row">
|
|
||||||
<label for="ipv6Prefix">IPv6地址或网段前缀</label>
|
|
||||||
<input type="text" id="ipv6Prefix" placeholder="例如: 2001:475:35:3f4::6/64" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="input-row">
|
|
||||||
<div class="flex-row">
|
|
||||||
<div class="flex-col">
|
|
||||||
<label for="ipv6Count">生成IPv6地址数量</label>
|
|
||||||
<input type="number" id="ipv6Count" value="10" min="1" max="100" />
|
|
||||||
</div>
|
|
||||||
<div class="flex-col">
|
|
||||||
<label for="ipv6Mask">子网掩码长度</label>
|
|
||||||
<input type="number" id="ipv6Mask" value="64" min="1" max="128" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="button-group">
|
|
||||||
<button class="btn-primary" onclick="generateIPv6Command()">生成IPv6命令</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="result-container">
|
|
||||||
<div class="result-header">
|
|
||||||
<h3>生成的命令:</h3>
|
|
||||||
<button class="btn-secondary" onclick="copyResult()">复制命令</button>
|
|
||||||
</div>
|
|
||||||
<div class="result-box" id="resultBox">生成的命令将显示在这里...</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="doc-section">
|
|
||||||
<h4>Linux服务器添加公网IP说明</h4>
|
|
||||||
<ul>
|
|
||||||
<li><strong>临时添加IP</strong>: 上述命令会临时添加IP,系统重启后失效</li>
|
|
||||||
<li><strong>永久添加</strong>: 需要修改网络配置文件 <code>/etc/network/interfaces</code> 或 <code>/etc/sysconfig/network-scripts/</code> 下的配置</li>
|
|
||||||
<li><strong>验证添加</strong>: 使用 <code>ip addr show</code> 命令验证IP是否添加成功</li>
|
|
||||||
<li><strong>注意事项</strong>: 添加公网IP前需确认IP已分配给您的服务器,否则可能导致IP冲突</li>
|
|
||||||
<li><strong>IPv6注意</strong>: 添加IPv6地址前确保服务器已开启IPv6支持</li>
|
|
||||||
</ul>
|
|
||||||
<p><small>使用<code>sysctl -w net.ipv6.conf.all.forwarding=1</code>开启IPv6转发,<code>sysctl -p</code>使配置生效</small></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function changeTab(evt, tabId) {
|
|
||||||
const tabContents = document.getElementsByClassName("tab-content");
|
|
||||||
for (let i = 0; i < tabContents.length; i++) {
|
|
||||||
tabContents[i].classList.remove("active");
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabButtons = document.getElementsByClassName("tab-button");
|
|
||||||
for (let i = 0; i < tabButtons.length; i++) {
|
|
||||||
tabButtons[i].classList.remove("active");
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById(tabId).classList.add("active");
|
|
||||||
evt.currentTarget.classList.add("active");
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateCommand(type) {
|
|
||||||
const interfaceName = document.getElementById(type === 'ipv6' ? 'interfaceIpv6' : 'interface').value.trim();
|
|
||||||
const ipInput = document.getElementById('ipInput').value.trim().split('\n');
|
|
||||||
let commands = '';
|
|
||||||
|
|
||||||
if (interfaceName && ipInput.length > 0) {
|
|
||||||
ipInput.forEach(ip => {
|
|
||||||
const trimmedIp = ip.trim();
|
|
||||||
if (trimmedIp) {
|
|
||||||
const ipWithMask = trimmedIp.includes('/') ? trimmedIp : `${trimmedIp}/32`;
|
|
||||||
commands += `sudo ip addr add ${ipWithMask} dev ${interfaceName}\n`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('resultBox').textContent = commands || "没有有效的IP地址输入";
|
|
||||||
} else {
|
|
||||||
document.getElementById('resultBox').textContent = "请填写网卡名称和IP地址";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateCSectionCommand() {
|
|
||||||
const interfaceName = document.getElementById('interface').value.trim();
|
|
||||||
if (!interfaceName) {
|
|
||||||
document.getElementById('resultBox').textContent = "请先填写网卡名称";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取第一个IP作为C段基准
|
|
||||||
let firstIp = document.getElementById('ipInput').value.trim().split('\n')[0] || '';
|
|
||||||
firstIp = firstIp.split('/')[0];
|
|
||||||
|
|
||||||
if (!firstIp) {
|
|
||||||
firstIp = '198.51.100.1'; // 使用RFC 5737定义的测试网段作为默认值
|
|
||||||
}
|
|
||||||
|
|
||||||
const ipParts = firstIp.split('.');
|
|
||||||
if (ipParts.length !== 4 || ipParts.some(part => isNaN(parseInt(part)) || parseInt(part) > 255)) {
|
|
||||||
document.getElementById('resultBox').textContent = "请输入有效的IP地址作为C段基准";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let commands = '';
|
|
||||||
const baseIp = `${ipParts[0]}.${ipParts[1]}.${ipParts[2]}`;
|
|
||||||
|
|
||||||
for (let i = 1; i <= 254; i++) {
|
|
||||||
commands += `sudo ip addr add ${baseIp}.${i}/24 dev ${interfaceName}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('resultBox').textContent = commands;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateIPv6Command() {
|
|
||||||
const interfaceName = document.getElementById('interfaceIpv6').value.trim();
|
|
||||||
const ipv6Input = document.getElementById('ipv6Prefix').value.trim();
|
|
||||||
const ipv6Count = parseInt(document.getElementById('ipv6Count').value);
|
|
||||||
let ipv6Mask = parseInt(document.getElementById('ipv6Mask').value);
|
|
||||||
|
|
||||||
if (!interfaceName || !ipv6Input) {
|
|
||||||
document.getElementById('resultBox').textContent = "请填写网卡名称和IPv6前缀";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNaN(ipv6Count) || ipv6Count < 1 || ipv6Count > 1000) {
|
|
||||||
document.getElementById('resultBox').textContent = "IPv6一次最多生成1000个";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNaN(ipv6Mask) || ipv6Mask < 1 || ipv6Mask > 128) {
|
|
||||||
document.getElementById('resultBox').textContent = "IPv6子网掩码长度必须在1-128之间";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let prefix = ipv6Input;
|
|
||||||
let inputMask = null;
|
|
||||||
if (ipv6Input.includes('/')) {
|
|
||||||
const parts = ipv6Input.split('/');
|
|
||||||
prefix = parts[0];
|
|
||||||
inputMask = parseInt(parts[1]);
|
|
||||||
if (!isNaN(inputMask) && inputMask >= 1 && inputMask <= 128) {
|
|
||||||
ipv6Mask = inputMask;
|
|
||||||
document.getElementById('ipv6Mask').value = ipv6Mask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const prefixSegments = Math.ceil(ipv6Mask / 16);
|
|
||||||
|
|
||||||
let networkPrefix = '';
|
|
||||||
|
|
||||||
if (prefix.includes('::')) {
|
|
||||||
const expandedAddress = expandIPv6Address(prefix);
|
|
||||||
const segments = expandedAddress.split(':');
|
|
||||||
networkPrefix = segments.slice(0, prefixSegments).join(':');
|
|
||||||
if (prefixSegments < 8) {
|
|
||||||
networkPrefix += ':';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const segments = prefix.split(':');
|
|
||||||
networkPrefix = segments.slice(0, prefixSegments).join(':');
|
|
||||||
if (prefixSegments < 8) {
|
|
||||||
networkPrefix += ':';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let commands = '';
|
|
||||||
for (let i = 0; i < ipv6Count; i++) {
|
|
||||||
const randomAddress = generateRandomIPv6InterfaceID(networkPrefix, ipv6Mask);
|
|
||||||
commands += `sudo ip addr add ${randomAddress}/${ipv6Mask} dev ${interfaceName}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('resultBox').textContent = commands;
|
|
||||||
}
|
|
||||||
function expandIPv6Address(address) {
|
|
||||||
if (address.includes('/')) {
|
|
||||||
address = address.split('/')[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!address.includes('::')) {
|
|
||||||
return address;
|
|
||||||
}
|
|
||||||
const parts = address.split('::');
|
|
||||||
const beforeDoubleColon = parts[0] ? parts[0].split(':') : [];
|
|
||||||
const afterDoubleColon = parts[1] ? parts[1].split(':') : [];
|
|
||||||
const missingGroups = 8 - (beforeDoubleColon.length + afterDoubleColon.length);
|
|
||||||
let expandedAddress = '';
|
|
||||||
if (beforeDoubleColon.length > 0) {
|
|
||||||
expandedAddress += beforeDoubleColon.join(':') + ':';
|
|
||||||
}
|
|
||||||
for (let i = 0; i < missingGroups; i++) {
|
|
||||||
expandedAddress += '0:';
|
|
||||||
}
|
|
||||||
if (afterDoubleColon.length > 0) {
|
|
||||||
expandedAddress += afterDoubleColon.join(':');
|
|
||||||
} else {
|
|
||||||
expandedAddress = expandedAddress.slice(0, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return expandedAddress;
|
|
||||||
}
|
|
||||||
function generateRandomIPv6InterfaceID(networkPrefix, prefixLength) {
|
|
||||||
const segmentsToKeep = Math.ceil(prefixLength / 16);
|
|
||||||
const segmentsToGenerate = 8 - segmentsToKeep;
|
|
||||||
if (segmentsToGenerate <= 0) {
|
|
||||||
return networkPrefix;
|
|
||||||
}
|
|
||||||
const cleanPrefix = networkPrefix.endsWith(':') ?
|
|
||||||
networkPrefix.slice(0, -1) : networkPrefix;
|
|
||||||
const existingSegments = cleanPrefix.split(':');
|
|
||||||
let randomSegments = [];
|
|
||||||
for (let i = 0; i < segmentsToGenerate; i++) {
|
|
||||||
randomSegments.push(generateRandomHex(4));
|
|
||||||
}
|
|
||||||
return [...existingSegments, ...randomSegments].join(':');
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateRandomHex(length) {
|
|
||||||
const hexChars = '0123456789abcdef';
|
|
||||||
let result = '';
|
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
result += hexChars.charAt(Math.floor(Math.random() * hexChars.length));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyResult() {
|
|
||||||
const resultBox = document.getElementById('resultBox');
|
|
||||||
const textToCopy = resultBox.textContent;
|
|
||||||
|
|
||||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
|
||||||
const copyBtn = document.querySelector('.btn-secondary');
|
|
||||||
const originalText = copyBtn.textContent;
|
|
||||||
copyBtn.textContent = '已复制!';
|
|
||||||
setTimeout(() => {
|
|
||||||
copyBtn.textContent = originalText;
|
|
||||||
}, 2000);
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('复制失败: ', err);
|
|
||||||
alert('复制失败,请手动选择文本复制');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,691 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Markdown 实时编辑器</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
/* 默认主题(白天模式) */
|
|
||||||
--bg-color: #f5f5f5;
|
|
||||||
--text-color: #333;
|
|
||||||
--header-bg: #fff;
|
|
||||||
--header-text: #333;
|
|
||||||
--button-bg: #c4c4c4;
|
|
||||||
--button-hover: #939393;
|
|
||||||
--editor-bg: #fff;
|
|
||||||
--border-color: #ddd;
|
|
||||||
--code-bg: #f6f8fa;
|
|
||||||
--blockquote-color: #6a737d;
|
|
||||||
--blockquote-border: #dfe2e5;
|
|
||||||
--dropdown-bg: #fff;
|
|
||||||
--dropdown-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
|
||||||
--dropdown-hover-bg: #f1f1f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 黑夜模式 */
|
|
||||||
body.theme-dark {
|
|
||||||
--bg-color: #1a1a1a;
|
|
||||||
--text-color: #e0e0e0;
|
|
||||||
--header-bg: #121212;
|
|
||||||
--header-text: #f0f0f0;
|
|
||||||
--button-bg: #2c3e50;
|
|
||||||
--button-hover: #34495e;
|
|
||||||
--editor-bg: #2d2d2d;
|
|
||||||
--border-color: #444;
|
|
||||||
--code-bg: #383838;
|
|
||||||
--blockquote-color: #aaa;
|
|
||||||
--blockquote-border: #666;
|
|
||||||
--dropdown-bg: #3e3e3e;
|
|
||||||
--dropdown-shadow: 0 8px 16px rgba(0,0,0,0.3);
|
|
||||||
--dropdown-hover-bg: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 蓝色模式 */
|
|
||||||
body.theme-blue {
|
|
||||||
--bg-color: #e8f4f8;
|
|
||||||
--text-color: #2c3e50;
|
|
||||||
--header-bg: #1e88e5;
|
|
||||||
--header-text: white;
|
|
||||||
--button-bg: #0d47a1;
|
|
||||||
--button-hover: #1565c0;
|
|
||||||
--editor-bg: #f1f8fe;
|
|
||||||
--border-color: #bbdefb;
|
|
||||||
--code-bg: #e3f2fd;
|
|
||||||
--blockquote-color: #546e7a;
|
|
||||||
--blockquote-border: #64b5f6;
|
|
||||||
--dropdown-bg: #ffffff;
|
|
||||||
--dropdown-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
|
||||||
--dropdown-hover-bg: #e3f2fd;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 赛博朋克模式 */
|
|
||||||
body.theme-cyberpunk {
|
|
||||||
--bg-color: #0a0a16;
|
|
||||||
--text-color: #f0f2f5;
|
|
||||||
--header-bg: #120458;
|
|
||||||
--header-text: #00ff9f;
|
|
||||||
--button-bg: #9900ff;
|
|
||||||
--button-hover: #b14aff;
|
|
||||||
--editor-bg: #1a1a2e;
|
|
||||||
--border-color: #ff00ff;
|
|
||||||
--code-bg: #2d1b54;
|
|
||||||
--blockquote-color: #00fff9;
|
|
||||||
--blockquote-border: #ff00ff;
|
|
||||||
--dropdown-bg: #1a1a2e;
|
|
||||||
--dropdown-shadow: 0 5px 15px rgba(255, 0, 255, 0.3);
|
|
||||||
--dropdown-hover-bg: #2d1b54;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Microsoft YaHei', sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: var(--text-color);
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
transition: background-color 0.3s, color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
max-width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
background-color: var(--header-bg);
|
|
||||||
color: var(--header-text);
|
|
||||||
padding: 1rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
header h1 {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: var(--button-bg);
|
|
||||||
color: var(--header-text);
|
|
||||||
border: none;
|
|
||||||
padding: 8px 15px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: var(--button-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
background-color: #95a5a6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Dropdown Menu Styles --- */
|
|
||||||
.dropdown {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
background-color: var(--dropdown-bg);
|
|
||||||
min-width: 160px;
|
|
||||||
box-shadow: var(--dropdown-shadow);
|
|
||||||
z-index: 1;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 5px 0;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu button {
|
|
||||||
color: var(--text-color);
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 10px 15px;
|
|
||||||
text-decoration: none;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu button:hover {
|
|
||||||
background-color: var(--dropdown-hover-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu.show {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
/* --- End Dropdown Styles --- */
|
|
||||||
|
|
||||||
.theme-selector {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#themeSelector {
|
|
||||||
padding: 6px 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
background-color: var(--editor-bg);
|
|
||||||
color: var(--text-color);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: all 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-container,
|
|
||||||
.preview-container {
|
|
||||||
flex: 1;
|
|
||||||
padding: 1rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
height: 100%;
|
|
||||||
transition: flex 0.3s ease-in-out, opacity 0.3s ease-in-out;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-container {
|
|
||||||
background-color: var(--editor-bg);
|
|
||||||
border-right: 1px solid var(--border-color);
|
|
||||||
/* 保持与.preview-container一致,无多余样式 */
|
|
||||||
border-radius: 0;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-container {
|
|
||||||
background-color: var(--editor-bg);
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#editor {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: none;
|
|
||||||
resize: none;
|
|
||||||
font-family: 'Consolas', monospace;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.6;
|
|
||||||
padding: 4px 10px;
|
|
||||||
outline: none;
|
|
||||||
background-color: transparent;
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#editor::-webkit-scrollbar {
|
|
||||||
height: 6px;
|
|
||||||
width: 0 !important;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
#editor {
|
|
||||||
scrollbar-width: thin; /* Firefox */
|
|
||||||
-ms-overflow-style: none; /* IE 10+ */
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body {
|
|
||||||
padding: 10px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { margin-top: 1.5rem; margin-bottom: 1rem; font-weight: 600; line-height: 1.25; }
|
|
||||||
.markdown-body h1 { font-size: 2em; border-bottom: 1px solid var(--border-color); padding-bottom: 0.3em; }
|
|
||||||
.markdown-body h2 { font-size: 1.5em; border-bottom: 1px solid var(--border-color); padding-bottom: 0.3em; }
|
|
||||||
.markdown-body p { margin-top: 0; margin-bottom: 1rem; }
|
|
||||||
.markdown-body blockquote { padding: 0 1em; color: var(--blockquote-color); border-left: 0.25em solid var(--blockquote-border); margin: 0 0 1rem 0; }
|
|
||||||
.markdown-body pre { background-color: var(--code-bg); border-radius: 3px; padding: 16px; overflow: auto; margin-bottom: 1rem; }
|
|
||||||
.markdown-body pre code { padding: 0; background-color: transparent; }
|
|
||||||
.markdown-body code { font-family: 'Consolas', monospace; background-color: var(--code-bg); padding: 0.2em 0.4em; border-radius: 3px; }
|
|
||||||
.markdown-body img { max-width: 100%; }
|
|
||||||
.markdown-body ul, .markdown-body ol { padding-left: 2em; margin-bottom: 1rem; }
|
|
||||||
.markdown-body table { border-collapse: collapse; margin-bottom: 1rem; display: block; overflow: auto; width: 100%; }
|
|
||||||
.markdown-body th, .markdown-body td { padding: 6px 13px; border: 1px solid var(--border-color); }
|
|
||||||
|
|
||||||
.editor-container::-webkit-scrollbar,
|
|
||||||
.preview-container::-webkit-scrollbar {
|
|
||||||
width: 0 !important;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.editor-container,
|
|
||||||
.preview-container {
|
|
||||||
scrollbar-width: none; /* Firefox */
|
|
||||||
-ms-overflow-style: none; /* IE 10+ */
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
header { flex-direction: column; gap: 1rem; }
|
|
||||||
main { flex-direction: column; }
|
|
||||||
.editor-container, .preview-container { flex: none; height: 50%; width: 100%; }
|
|
||||||
.editor-container { border-right: none; border-bottom: 1px solid var(--border-color); }
|
|
||||||
main.preview-hidden .editor-container,
|
|
||||||
main.editor-hidden .preview-container { height: 100%; border-bottom: none; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.sync-scroll-toggle {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-left: 18px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--header-text);
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.sync-scroll-toggle input[type="checkbox"] {
|
|
||||||
accent-color: var(--button-bg);
|
|
||||||
margin-right: 4px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<header>
|
|
||||||
<h1>Markdown 实时编辑器</h1>
|
|
||||||
<div class="toolbar">
|
|
||||||
<!-- 文件操作 -->
|
|
||||||
<button id="saveBtn">保存</button>
|
|
||||||
<button id="importBtn">导入 MD</button>
|
|
||||||
<div class="dropdown">
|
|
||||||
<button class="dropdown-toggle">导出</button>
|
|
||||||
<div id="export-menu" class="dropdown-menu">
|
|
||||||
<button id="exportBtn">导出 MD</button>
|
|
||||||
<button id="exportHtmlBtn">导出 HTML</button>
|
|
||||||
<button id="exportPdfBtn">导出 PDF</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 视图选项 -->
|
|
||||||
<div class="dropdown">
|
|
||||||
<button class="dropdown-toggle">视图</button>
|
|
||||||
<div id="view-menu" class="dropdown-menu">
|
|
||||||
<button id="toggleEditorBtn">切换编辑区</button>
|
|
||||||
<button id="togglePreviewBtn">切换预览区</button>
|
|
||||||
<button id="mdGuideBtn">Markdown 指南</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 主题选择 -->
|
|
||||||
<div class="theme-selector">
|
|
||||||
<select id="themeSelector">
|
|
||||||
<option value="default">白天模式</option>
|
|
||||||
<option value="dark">黑夜模式</option>
|
|
||||||
<option value="blue">蓝色模式</option>
|
|
||||||
<option value="cyberpunk">赛博朋克</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<!-- 同步滚动开关 -->
|
|
||||||
<label class="sync-scroll-toggle" title="编辑区滚动时预览区也跟随滚动">
|
|
||||||
<input type="checkbox" id="syncScrollToggle" checked>
|
|
||||||
同步滚动
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<div class="editor-container">
|
|
||||||
<textarea id="editor" placeholder="在此输入 Markdown 内容..."></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="preview-container">
|
|
||||||
<div id="preview" class="markdown-body"></div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 依赖库 -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
|
||||||
|
|
||||||
<!-- 主逻辑 -->
|
|
||||||
<script>
|
|
||||||
window.addEventListener('load', function () {
|
|
||||||
// --- DOM 元素获取 ---
|
|
||||||
const editor = document.getElementById('editor');
|
|
||||||
const preview = document.getElementById('preview');
|
|
||||||
const mainContainer = document.querySelector('main');
|
|
||||||
const syncScrollToggle = document.getElementById('syncScrollToggle');
|
|
||||||
|
|
||||||
// 工具栏按钮
|
|
||||||
const saveBtn = document.getElementById('saveBtn');
|
|
||||||
const importBtn = document.getElementById('importBtn');
|
|
||||||
const exportBtn = document.getElementById('exportBtn');
|
|
||||||
const exportHtmlBtn = document.getElementById('exportHtmlBtn');
|
|
||||||
const exportPdfBtn = document.getElementById('exportPdfBtn');
|
|
||||||
const toggleEditorBtn = document.getElementById('toggleEditorBtn');
|
|
||||||
const togglePreviewBtn = document.getElementById('togglePreviewBtn');
|
|
||||||
const mdGuideBtn = document.getElementById('mdGuideBtn');
|
|
||||||
const themeSelector = document.getElementById('themeSelector');
|
|
||||||
const dropdownToggles = document.querySelectorAll('.dropdown-toggle');
|
|
||||||
|
|
||||||
// --- 状态变量 ---
|
|
||||||
let isShowingGuide = false;
|
|
||||||
let userContentBeforeGuide = '';
|
|
||||||
|
|
||||||
const fileInput = document.createElement('input');
|
|
||||||
fileInput.type = 'file';
|
|
||||||
fileInput.accept = '.md,.markdown,text/markdown';
|
|
||||||
fileInput.style.display = 'none';
|
|
||||||
document.body.appendChild(fileInput);
|
|
||||||
|
|
||||||
marked.setOptions({ breaks: true, gfm: true, headerIds: true, sanitize: false });
|
|
||||||
|
|
||||||
// --- 核心功能 ---
|
|
||||||
function renderMarkdown(shouldSave = true) {
|
|
||||||
const markdownText = editor.value;
|
|
||||||
const htmlContent = marked.parse(markdownText);
|
|
||||||
preview.innerHTML = htmlContent;
|
|
||||||
if (shouldSave && !isShowingGuide) {
|
|
||||||
localStorage.setItem('markdown-content', markdownText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFilename() {
|
|
||||||
const firstLine = editor.value.trim().split('\n')[0];
|
|
||||||
const sanitized = firstLine.replace(/[^a-zA-Z0-9\u4e00-\u9fa5\s]/g, '').trim();
|
|
||||||
return sanitized && sanitized.length > 0 ? sanitized : 'markdown-export';
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 初始化 ---
|
|
||||||
const savedContent = localStorage.getItem('markdown-content');
|
|
||||||
if (savedContent) editor.value = savedContent;
|
|
||||||
renderMarkdown(false);
|
|
||||||
|
|
||||||
const initialTheme = localStorage.getItem('markdown-theme') || 'default';
|
|
||||||
applyTheme(initialTheme);
|
|
||||||
themeSelector.value = initialTheme;
|
|
||||||
updateViewButtonsText();
|
|
||||||
|
|
||||||
// --- 事件监听器 ---
|
|
||||||
editor.addEventListener('input', () => {
|
|
||||||
if (isShowingGuide) {
|
|
||||||
isShowingGuide = false;
|
|
||||||
mdGuideBtn.textContent = 'Markdown 指南';
|
|
||||||
}
|
|
||||||
renderMarkdown(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
saveBtn.addEventListener('click', () => {
|
|
||||||
localStorage.setItem('markdown-content', editor.value);
|
|
||||||
alert('内容已手动保存到本地存储!');
|
|
||||||
});
|
|
||||||
|
|
||||||
importBtn.addEventListener('click', () => fileInput.click());
|
|
||||||
|
|
||||||
fileInput.addEventListener('change', (e) => {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
editor.value = e.target.result;
|
|
||||||
if (isShowingGuide) {
|
|
||||||
isShowingGuide = false;
|
|
||||||
mdGuideBtn.textContent = '返回编辑';
|
|
||||||
}
|
|
||||||
renderMarkdown(true);
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
exportBtn.addEventListener('click', () => {
|
|
||||||
const blob = new Blob([editor.value], { type: 'text/markdown' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = `${getFilename()}.md`;
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
});
|
|
||||||
|
|
||||||
exportHtmlBtn.addEventListener('click', () => {
|
|
||||||
const filename = getFilename() + '.html';
|
|
||||||
// 构造完整HTML文档
|
|
||||||
const htmlContent = `<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"UTF-8\">\n<title>${filename}</title>\n<style>${document.querySelector('style').innerHTML}</style>\n</head>\n<body>\n<div class=\"markdown-body\">${preview.innerHTML}</div>\n</body>\n</html>`;
|
|
||||||
const blob = new Blob([htmlContent], { type: 'text/html' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = filename;
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
});
|
|
||||||
|
|
||||||
exportPdfBtn.addEventListener('click', async () => {
|
|
||||||
const btn = exportPdfBtn;
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = '正在生成...';
|
|
||||||
try {
|
|
||||||
const element = document.getElementById('preview');
|
|
||||||
const opt = {
|
|
||||||
margin: 15,
|
|
||||||
filename: `${getFilename()}.pdf`,
|
|
||||||
image: { type: 'jpeg', quality: 0.98 },
|
|
||||||
html2canvas: { scale: 2, useCORS: true },
|
|
||||||
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
|
|
||||||
};
|
|
||||||
await html2pdf().from(element).set(opt).save();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("PDF导出失败:", error);
|
|
||||||
alert("导出 PDF 时出错,请查看控制台获取更多信息。");
|
|
||||||
} finally {
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = '导出 PDF';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- 视图切换功能 ---
|
|
||||||
function updateViewButtonsText() {
|
|
||||||
toggleEditorBtn.textContent = mainContainer.classList.contains('editor-hidden') ? '显示编辑区' : '隐藏编辑区';
|
|
||||||
togglePreviewBtn.textContent = mainContainer.classList.contains('preview-hidden') ? '显示预览区' : '隐藏预览区';
|
|
||||||
mdGuideBtn.textContent = isShowingGuide ? '返回编辑' : 'Markdown 指南';
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleEditorBtn.addEventListener('click', function () {
|
|
||||||
mainContainer.classList.remove('preview-hidden');
|
|
||||||
mainContainer.classList.toggle('editor-hidden');
|
|
||||||
updateViewButtonsText();
|
|
||||||
});
|
|
||||||
|
|
||||||
togglePreviewBtn.addEventListener('click', function () {
|
|
||||||
mainContainer.classList.remove('editor-hidden');
|
|
||||||
mainContainer.classList.toggle('preview-hidden');
|
|
||||||
updateViewButtonsText();
|
|
||||||
});
|
|
||||||
|
|
||||||
const markdownGuideContent = `...`; // 指南内容太长,为保持代码清爽此处省略,实际代码会包含完整内容。
|
|
||||||
mdGuideBtn.addEventListener('click', () => {
|
|
||||||
isShowingGuide = !isShowingGuide;
|
|
||||||
if (isShowingGuide) {
|
|
||||||
userContentBeforeGuide = editor.value;
|
|
||||||
editor.value = markdownGuideContent.replace('...', `
|
|
||||||
# Markdown 语法指南
|
|
||||||
|
|
||||||
这是一个 Markdown 格式的快速参考指南,您可以随时查看这个页面来学习 Markdown 的使用方法。
|
|
||||||
|
|
||||||
## 基本语法
|
|
||||||
|
|
||||||
### 标题
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
# 一级标题
|
|
||||||
## 二级标题
|
|
||||||
### 三级标题
|
|
||||||
#### 四级标题
|
|
||||||
##### 五级标题
|
|
||||||
###### 六级标题
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 强调
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
*斜体文本* 或 _斜体文本_
|
|
||||||
**粗体文本** 或 __粗体文本__
|
|
||||||
***粗斜体文本*** 或 ___粗斜体文本___
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 列表
|
|
||||||
|
|
||||||
无序列表:
|
|
||||||
\`\`\`
|
|
||||||
- 项目1
|
|
||||||
- 项目2
|
|
||||||
- 子项目A
|
|
||||||
- 子项目B
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
有序列表:
|
|
||||||
\`\`\`
|
|
||||||
1. 第一项
|
|
||||||
2. 第二项
|
|
||||||
3. 第三项
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 链接
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
[链接文本](https://www.example.com)
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 图片
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||

|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
## 高级语法
|
|
||||||
|
|
||||||
### 表格
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
| 表头1 | 表头2 | 表头3 |
|
|
||||||
| :--- | :---: | ---: |
|
|
||||||
| 左对齐 | 居中对齐 | 右对齐 |
|
|
||||||
| 单元格4 | 单元格5 | 单元格6 |
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 代码块
|
|
||||||
|
|
||||||
行内代码使用反引号 \`code\` 包裹。
|
|
||||||
|
|
||||||
代码块使用三个反引号包裹:
|
|
||||||
\`\`\`javascript
|
|
||||||
function greet(name) {
|
|
||||||
console.log("Hello, " + name + "!");
|
|
||||||
}
|
|
||||||
greet('World');
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 引用
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
> 这是一段引用的文字。
|
|
||||||
>
|
|
||||||
> > 引用可以嵌套。
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 分隔线
|
|
||||||
|
|
||||||
使用三个或更多的星号、破折号或下划线来创建分隔线。
|
|
||||||
\`\`\`
|
|
||||||
***
|
|
||||||
---
|
|
||||||
___
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 删除线
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
~~这段文字将被划掉。~~
|
|
||||||
\`\`\`
|
|
||||||
`);
|
|
||||||
renderMarkdown(false);
|
|
||||||
} else {
|
|
||||||
editor.value = userContentBeforeGuide;
|
|
||||||
renderMarkdown(true);
|
|
||||||
}
|
|
||||||
updateViewButtonsText();
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- 主题切换 ---
|
|
||||||
themeSelector.addEventListener('change', (e) => {
|
|
||||||
applyTheme(e.target.value);
|
|
||||||
localStorage.setItem('markdown-theme', e.target.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
function applyTheme(theme) {
|
|
||||||
document.body.className = '';
|
|
||||||
document.body.classList.add(`theme-${theme}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 下拉菜单交互逻辑 ---
|
|
||||||
function closeAllDropdowns() {
|
|
||||||
document.querySelectorAll('.dropdown-menu').forEach(menu => {
|
|
||||||
menu.classList.remove('show');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dropdownToggles.forEach(toggle => {
|
|
||||||
toggle.addEventListener('click', function(event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
const currentMenu = this.nextElementSibling;
|
|
||||||
const isShown = currentMenu.classList.contains('show');
|
|
||||||
closeAllDropdowns(); // Close others first
|
|
||||||
if (!isShown) {
|
|
||||||
currentMenu.classList.add('show');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click outside to close dropdowns
|
|
||||||
window.addEventListener('click', function(event) {
|
|
||||||
if (!event.target.matches('.dropdown-toggle')) {
|
|
||||||
closeAllDropdowns();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- 同步滚动功能 ---
|
|
||||||
let isSyncScroll = true;
|
|
||||||
let isPreviewScrolling = false;
|
|
||||||
syncScrollToggle.addEventListener('change', function() {
|
|
||||||
isSyncScroll = this.checked;
|
|
||||||
});
|
|
||||||
editor.addEventListener('scroll', function() {
|
|
||||||
if (!isSyncScroll) return;
|
|
||||||
if (isPreviewScrolling) return;
|
|
||||||
const editorScroll = editor.scrollTop;
|
|
||||||
const editorHeight = editor.scrollHeight - editor.clientHeight;
|
|
||||||
const percent = editorHeight > 0 ? editorScroll / editorHeight : 0;
|
|
||||||
const previewContainer = preview.parentElement;
|
|
||||||
const previewHeight = previewContainer.scrollHeight - previewContainer.clientHeight;
|
|
||||||
isPreviewScrolling = true;
|
|
||||||
previewContainer.scrollTop = percent * previewHeight;
|
|
||||||
setTimeout(() => { isPreviewScrolling = false; }, 10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user