Initial Commit
This commit is contained in:
@@ -0,0 +1,564 @@
|
||||
---
|
||||
cover: "https://image.luming.cool/i/2026/05/10/69fff354c9b08.webp"
|
||||
title: "记一次开发:田字格生成器"
|
||||
pubDate: "2025-07-13T10:20:00.000Z"
|
||||
updatedDate: "2025-07-13T10:20:00.000Z"
|
||||
categories:
|
||||
- "科技"
|
||||
tags:
|
||||
- "开发"
|
||||
- "生活"
|
||||
- "软件"
|
||||
comments: true
|
||||
---
|
||||
|
||||
## 前言
|
||||
|
||||
朋友们,上午好。今天我想和大家分享一下我近期与 AI 协作,使用 Python+Flask 进行开发的经历。
|
||||
|
||||
本次开发的软件名字叫“田字格生成器”,已经部署上线,可以点击[这里](https://tzg.rsv.ee/)使用。
|
||||
|
||||
## 立项原因
|
||||
|
||||
家母最近参与了洛阳日报社的托管班暑期托管活动,担任硬笔书法教师。这是她第一次在正式的单位进行授课,没有教材,需要她自己准备。
|
||||
|
||||
我并不知道母亲是怎么准备教材的。前几天的下午, 我在衔接班的空隙,看到母亲用尺子在一张纸上画着什么。走近一看,霍!纸上打印的是她用手机拍下来的范字,糊成了保密级画质。而她拿着尺子,是为了手动将模糊的田字格虚线描实。
|
||||
|
||||
而目光一转,她旁边还有一摞跟教辅一样厚的原始稿件等待处理,这得处理到猴年马月!于是我毫不犹豫地对她说:“交给我吧!”
|
||||
|
||||
就这样,一个名为“田字格批量生成器”的文件夹就被新建了。
|
||||
|
||||
## 需求确定
|
||||
|
||||
母亲的要求有很多,比如:
|
||||
|
||||
1. 一页A4纸,放一个田字格,撑满整张纸,贴到黑板上给学生讲;
|
||||
2. 一页A4纸,放一个学生的名字,横版,撑满整张纸,用来教学生写自己的名字;
|
||||
3. 一页A4纸,放一张字帖;
|
||||
4. 一页A4纸,放一页竖版的字帖;
|
||||
5. 一页A4纸,放30个字,用作自己练习,6行5列。
|
||||
|
||||
而网上的田字格在线生成器并不能满足母亲的需求,所以,Cursor,启动!
|
||||
|
||||
## 基础功能
|
||||
|
||||
我先将需求投喂给 Cursor,接着,它帮我写出了如下界面的程序:
|
||||
|
||||
初版程序
|
||||
|
||||
主要功能:用户往编辑框里输入需要生成的汉字→选择模式→程序根据对应的模式生成 PDF 文件并输出到 output 目录下。
|
||||
|
||||
当我把生成好的 PDF 拿给母亲看的时候,她果断地摇了摇头:
|
||||
|
||||
> 你这个字体面积占整个田字格面积的占比太小了。
|
||||
>
|
||||
> 你刚才给我看的那个字帖生成网页,那个网页的田字格我觉得符合我的需求。
|
||||
|
||||
于是我便把网络上的田字格字帖生成器打开了,母亲仔细看了看,认可地点点头,要求我按照它一样做。
|
||||
|
||||
为此,我将那个田字格生成器作为 HTML 保存了下来,并放到了项目根目录,和 Cursor 说:
|
||||
|
||||
> 模仿 xxxxxx.html 的田字格排版形式,重新排版现有的田字格。
|
||||
|
||||
等到 AI 生成完毕,我再次测试让母亲看,这次她终于接受了。
|
||||
|
||||
## 网页版改编&上线
|
||||
|
||||
既然母亲已经接受了 AI 生成的田字格样式,那么怎么排版就是我的事情了。在向 AI 投喂了一大堆提示词之后,终于生成了以下样式的田字格生成器:
|
||||
|
||||

|
||||
|
||||
期间还有一个小插曲:我在让 AI 编写出 6 行 5 列的排版时,错将“6 行 5 列”表达成了“5 行 6 列”,AI 生成了以下样式的排版:
|
||||
|
||||

|
||||
|
||||
由于这整体看起来太像是正方形了,所以我把它错看成了 6 行 6 列。于是我竭尽全力地让 AI “纠正”,却无济于事。我笑骂了 AI 几句,便想着放弃,关闭了 Cursor。
|
||||
|
||||
当我把这份废稿发给母亲的时候,母亲的一句话点醒了我——是我数错了:
|
||||
|
||||

|
||||
|
||||
得知这个消息的我兴奋极了,连忙再次打开 Cursor,输入提示词,等待改编完毕,运行,生成,成功了!
|
||||
|
||||
## 源代码
|
||||
|
||||
因为我的 Github 账号的 2FA 验证器被我卸载了,同时,恢复密钥也被我误删了,所以我的 Github 账号(Rise-forever)现在完全用不了了。
|
||||
|
||||
这个程序就先在这里开源吧,后续我可能会在 [CNB](https://cnb.cool) 上开源。
|
||||
|
||||
以下是主程序(.py)的源码,命名为 app.py:
|
||||
|
||||
```
|
||||
from flask import Flask, render_template, request, send_file, redirect, url_for, flash
|
||||
import os
|
||||
import io
|
||||
import random
|
||||
import string
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = 'tianzige_secret_key'
|
||||
|
||||
# 注册楷体字体(确保simkai.ttf在同目录下)
|
||||
pdfmetrics.registerFont(TTFont('KaiTi', 'simkai.ttf'))
|
||||
|
||||
def random_str(length=6):
|
||||
return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
|
||||
|
||||
def draw_tianzige(c, x, y, size, border_width: float = 2.0, dash_width: float = 1.0, dash_color=(0,0,0), mode=None):
|
||||
cx = x + size/2
|
||||
cy = y + size/2
|
||||
if mode == 'zitieborder':
|
||||
c.setLineWidth(dash_width)
|
||||
c.setStrokeColorRGB(*dash_color)
|
||||
segs = 5
|
||||
gap_ratio = 0.3
|
||||
for i in range(segs):
|
||||
seg_len = (size/2) / (segs + (segs-1)*gap_ratio)
|
||||
gap = seg_len * gap_ratio
|
||||
y1 = cy + i * (seg_len + gap)
|
||||
y2 = y1 + seg_len
|
||||
if y2 <= y + size:
|
||||
c.line(cx, y1, cx, y2)
|
||||
for i in range(segs):
|
||||
seg_len = (size/2) / (segs + (segs-1)*gap_ratio)
|
||||
gap = seg_len * gap_ratio
|
||||
y1 = cy - i * (seg_len + gap)
|
||||
y2 = y1 - seg_len
|
||||
if y2 >= y:
|
||||
c.line(cx, y1, cx, y2)
|
||||
for i in range(segs):
|
||||
seg_len = (size/2) / (segs + (segs-1)*gap_ratio)
|
||||
gap = seg_len * gap_ratio
|
||||
x1 = cx - i * (seg_len + gap)
|
||||
x2 = x1 - seg_len
|
||||
if x2 >= x:
|
||||
c.line(x1, cy, x2, cy)
|
||||
for i in range(segs):
|
||||
seg_len = (size/2) / (segs + (segs-1)*gap_ratio)
|
||||
gap = seg_len * gap_ratio
|
||||
x1 = cx + i * (seg_len + gap)
|
||||
x2 = x1 + seg_len
|
||||
if x2 <= x + size:
|
||||
c.line(x1, cy, x2, cy)
|
||||
c.setLineWidth(border_width)
|
||||
c.setStrokeColorRGB(0,0,0)
|
||||
c.rect(x, y, size, size)
|
||||
return
|
||||
dash = [8, 5]
|
||||
c.setLineWidth(dash_width)
|
||||
c.setStrokeColorRGB(*dash_color)
|
||||
for i in range(10):
|
||||
y1 = y + i * size / 10
|
||||
y2 = y + (i + 1) * size / 10
|
||||
c.setDash(dash)
|
||||
c.line(x + size/2, y1, x + size/2, y2)
|
||||
for i in range(10):
|
||||
x1 = x + i * size / 10
|
||||
x2 = x + (i + 1) * size / 10
|
||||
c.setDash(dash)
|
||||
c.line(x1, y + size/2, x2, y + size/2)
|
||||
c.setDash([])
|
||||
c.setLineWidth(border_width)
|
||||
c.setStrokeColorRGB(0,0,0)
|
||||
c.rect(x, y, size, size)
|
||||
|
||||
def generate_single_page(chars):
|
||||
buffer = io.BytesIO()
|
||||
c = canvas.Canvas(buffer, pagesize=A4)
|
||||
width, height = A4
|
||||
margin = 20
|
||||
size = min(width, height) - 2 * margin
|
||||
x = (width - size) / 2
|
||||
y = (height - size) / 2
|
||||
font_size = int(size * 0.725)
|
||||
for char in chars:
|
||||
draw_tianzige(c, x, y, size, border_width=4.0, dash_width=3.0, dash_color=(0.2,0.2,0.2))
|
||||
c.setFont("KaiTi", font_size)
|
||||
c.setFillColorRGB(0, 0, 0)
|
||||
c.drawCentredString(width/2, height/2 - size*0.25, char)
|
||||
c.showPage()
|
||||
c.save()
|
||||
buffer.seek(0)
|
||||
return buffer
|
||||
|
||||
def generate_zi_tie(chars):
|
||||
buffer = io.BytesIO()
|
||||
c = canvas.Canvas(buffer, pagesize=A4)
|
||||
width, height = A4
|
||||
mm = 2.8346
|
||||
grid_size = 15 * mm
|
||||
gap = 2 * mm
|
||||
cols = 12
|
||||
rows = 15
|
||||
total_grid_h = rows * grid_size + (rows - 1) * gap
|
||||
total_grid_w = cols * grid_size
|
||||
margin_x = max((width - total_grid_w) / 2, 0)
|
||||
margin_y = max((height - total_grid_h) / 2, 0)
|
||||
font_size = int(grid_size * 0.725)
|
||||
chars = list(chars)
|
||||
page_count = (len(chars) + rows - 1) // rows
|
||||
char_idx = 0
|
||||
for page in range(page_count):
|
||||
for row in range(rows):
|
||||
if char_idx >= len(chars):
|
||||
break
|
||||
char = chars[char_idx]
|
||||
for col in range(cols):
|
||||
gx = margin_x + col * grid_size
|
||||
gy = height - margin_y - (row + 1) * grid_size - row * gap
|
||||
draw_tianzige(c, gx, gy, grid_size, border_width=0.8, dash_width=0.5, dash_color=(0.69,0.69,0.69), mode='zitieborder')
|
||||
if col == 0:
|
||||
c.setFont("KaiTi", font_size)
|
||||
c.setFillColorRGB(0, 0, 0)
|
||||
c.drawCentredString(gx + grid_size/2, gy + grid_size/2 - grid_size*0.25, char)
|
||||
char_idx += 1
|
||||
c.showPage()
|
||||
c.save()
|
||||
buffer.seek(0)
|
||||
return buffer
|
||||
|
||||
def generate_name_per_page(text):
|
||||
buffer = io.BytesIO()
|
||||
c = canvas.Canvas(buffer, pagesize=(842, 595))
|
||||
width, height = 842, 595
|
||||
margin = 20
|
||||
names = [n.strip() for n in text.split() if n.strip()]
|
||||
for name in names:
|
||||
n_chars = len(name)
|
||||
if n_chars == 0:
|
||||
continue
|
||||
size = min((width - 2*margin) / n_chars, height - 2*margin)
|
||||
font_size = int(size * 0.725)
|
||||
y = (height - size) / 2
|
||||
start_x = (width - size * n_chars) / 2
|
||||
for i, char in enumerate(name):
|
||||
gx = start_x + i * size
|
||||
draw_tianzige(c, gx, y, size, border_width=4.0, dash_width=3.0, dash_color=(0.2,0.2,0.2))
|
||||
c.setFont("KaiTi", font_size)
|
||||
c.setFillColorRGB(0, 0, 0)
|
||||
c.drawCentredString(gx + size/2, height/2 - size*0.25, char)
|
||||
c.showPage()
|
||||
c.save()
|
||||
buffer.seek(0)
|
||||
return buffer
|
||||
|
||||
def generate_zi_tie_vertical(chars):
|
||||
buffer = io.BytesIO()
|
||||
c = canvas.Canvas(buffer, pagesize=A4)
|
||||
width, height = A4
|
||||
mm = 2.8346
|
||||
grid_size = 15 * mm
|
||||
gap = 2 * mm
|
||||
cols = 12
|
||||
rows = 15
|
||||
total_grid_h = rows * grid_size + (rows - 1) * gap
|
||||
total_grid_w = cols * grid_size
|
||||
margin_x = max((width - total_grid_w) / 2, 0)
|
||||
margin_y = max((height - total_grid_h) / 2, 0)
|
||||
font_size = int(grid_size * 0.725)
|
||||
chars = list(chars)
|
||||
page_count = (len(chars) + cols - 1) // cols
|
||||
char_idx = 0
|
||||
for page in range(page_count):
|
||||
# 获取当前页的字符(最多12个)
|
||||
page_chars = []
|
||||
for col in range(cols):
|
||||
if char_idx < len(chars):
|
||||
page_chars.append(chars[char_idx])
|
||||
char_idx += 1
|
||||
else:
|
||||
page_chars.append('') # 空字符占位
|
||||
# 绘制当前页
|
||||
for col in range(cols):
|
||||
char = page_chars[col]
|
||||
for row in range(rows):
|
||||
gx = margin_x + col * grid_size
|
||||
gy = height - margin_y - (row + 1) * grid_size - row * gap
|
||||
draw_tianzige(c, gx, gy, grid_size, border_width=0.8, dash_width=0.5, dash_color=(0.69,0.69,0.69), mode='zitieborder')
|
||||
# 在第1、6、11行显示范字(如果该列有字符)
|
||||
if char and (row == 0 or row == 5 or row == 10):
|
||||
c.setFont("KaiTi", font_size)
|
||||
c.setFillColorRGB(0, 0, 0)
|
||||
c.drawCentredString(gx + grid_size/2, gy + grid_size/2 - grid_size*0.25, char)
|
||||
c.showPage()
|
||||
c.save()
|
||||
buffer.seek(0)
|
||||
return buffer
|
||||
|
||||
def generate_5x6_mode(chars):
|
||||
buffer = io.BytesIO()
|
||||
c = canvas.Canvas(buffer, pagesize=A4)
|
||||
width, height = A4
|
||||
mm = 2.8346
|
||||
cols = 5 # 改为5列
|
||||
rows = 6 # 改为6行
|
||||
# 计算田字格大小,让总宽度贴边A4纸宽度
|
||||
gap = 1.5 * mm # 间距
|
||||
# 计算田字格大小和边距,确保左右边距相等
|
||||
margin_x = 10 * mm # 设置左右边距
|
||||
available_width = width - 2 * margin_x
|
||||
grid_size = (available_width - (cols - 1) * gap) / cols # 计算田字格大小
|
||||
# 重新计算边距确保居中
|
||||
total_grid_width = cols * grid_size + (cols - 1) * gap
|
||||
margin_x = (width - total_grid_width) / 2 # 确保左右边距相等
|
||||
# 计算总高度和垂直边距
|
||||
total_grid_h = rows * grid_size + (rows - 1) * gap
|
||||
margin_y = max((height - total_grid_h) / 2, 15 * mm)
|
||||
font_size = int(grid_size * 0.725)
|
||||
chars = list(chars)
|
||||
page_count = (len(chars) + 29) // 30 # 每页30个格子
|
||||
char_idx = 0
|
||||
for page in range(page_count):
|
||||
# 先绘制所有田字格(6行5列)
|
||||
for row in range(rows):
|
||||
for col in range(cols):
|
||||
gx = margin_x + col * (grid_size + gap)
|
||||
gy = height - margin_y - (row + 1) * grid_size - row * gap
|
||||
# 加重框线、虚线和字体颜色
|
||||
draw_tianzige(c, gx, gy, grid_size, border_width=3.0, dash_width=2.5, dash_color=(0.2,0.2,0.2), mode='zitieborder')
|
||||
# 再填充字符(从左往右,从上往下)
|
||||
for row in range(rows):
|
||||
for col in range(cols):
|
||||
if char_idx < len(chars):
|
||||
gx = margin_x + col * (grid_size + gap)
|
||||
gy = height - margin_y - (row + 1) * grid_size - row * gap
|
||||
c.setFont("KaiTi", font_size)
|
||||
c.setFillColorRGB(0, 0, 0) # 纯黑色字体
|
||||
c.drawCentredString(gx + grid_size/2, gy + grid_size/2 - grid_size*0.25, chars[char_idx])
|
||||
char_idx += 1
|
||||
c.showPage()
|
||||
c.save()
|
||||
buffer.seek(0)
|
||||
return buffer
|
||||
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def index():
|
||||
if request.method == 'POST':
|
||||
chars = request.form.get('chars', '').strip()
|
||||
mode = request.form.get('mode', '1')
|
||||
if not chars:
|
||||
flash('请输入要生成的汉字或名字!')
|
||||
return redirect(url_for('index'))
|
||||
if mode == '1':
|
||||
pdf_buffer = generate_single_page(chars)
|
||||
elif mode == '2':
|
||||
pdf_buffer = generate_zi_tie(chars)
|
||||
elif mode == '3':
|
||||
pdf_buffer = generate_name_per_page(chars)
|
||||
elif mode == '4':
|
||||
pdf_buffer = generate_zi_tie_vertical(chars)
|
||||
elif mode == '5':
|
||||
pdf_buffer = generate_5x6_mode(chars)
|
||||
else:
|
||||
flash('请选择生成模式!')
|
||||
return redirect(url_for('index'))
|
||||
# 文件名:前三个字+随机串
|
||||
base = chars[:3] if len(chars) >= 3 else chars
|
||||
filename = f"{base}{random_str(6)}.pdf"
|
||||
filename = secure_filename(filename)
|
||||
return send_file(pdf_buffer, as_attachment=True, download_name=filename, mimetype='application/pdf')
|
||||
return render_template('index.html')
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
```
|
||||
|
||||
以下是 index.html 的源码,index.html 需要在主程序所在目录新建一个名为“templates”的文件夹,并将 HTML 放进去。
|
||||
|
||||
```
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>田字格批量生成器</title>
|
||||
<style>
|
||||
body { font-family: '微软雅黑', Arial, sans-serif; background: #f7f7f7; }
|
||||
.container { max-width: 420px; margin: 60px auto; background: #fff; border-radius: 8px; box-shadow: 0 2px 8px #ccc; padding: 32px 28px; }
|
||||
h2 { text-align: center; margin-bottom: 24px; }
|
||||
label { display: block; margin: 16px 0 6px 0; font-size: 16px; }
|
||||
input[type="text"] { display: block; box-sizing: border-box; width: 100%; padding: 8px 10px; font-size: 18px; border: 1px solid #bbb; border-radius: 4px; margin: 0 auto; }
|
||||
.modes { margin: 18px 0 22px 0; }
|
||||
.modes label { display: block; font-size: 15px; margin-bottom: 6px; }
|
||||
button { width: 100%; background: #2d8cf0; color: #fff; border: none; border-radius: 4px; padding: 12px 0; font-size: 18px; cursor: pointer; }
|
||||
button:hover { background: #1976d2; }
|
||||
.msg { color: #d32f2f; text-align: center; margin-bottom: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>田字格批量生成器</h2>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="msg">{{ messages[0] }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="post">
|
||||
<label for="chars">请输入汉字或名字(名字用空格分隔):</label>
|
||||
<input type="text" id="chars" name="chars" required>
|
||||
<div class="modes">
|
||||
<label><input type="radio" name="mode" value="1" checked> 一张字一页模式</label>
|
||||
<label><input type="radio" name="mode" value="2"> 字帖模式</label>
|
||||
<label><input type="radio" name="mode" value="3"> 一个名字一页模式</label>
|
||||
<label><input type="radio" name="mode" value="4"> 字帖模式(竖版)</label>
|
||||
<label><input type="radio" name="mode" value="5"> 5×6模式</label>
|
||||
</div>
|
||||
<button type="submit" id="generateBtn">生成PDF</button>
|
||||
<div style="text-align:center;margin-top:18px;">
|
||||
<a href="#" id="helpLink" style="color:#1976d2;font-size:15px;text-decoration:underline;">使用说明</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
// 拦截表单提交,异步生成PDF
|
||||
const form = document.querySelector('form');
|
||||
const btn = document.getElementById('generateBtn');
|
||||
form.onsubmit = async function(e) {
|
||||
e.preventDefault();
|
||||
btn.disabled = true;
|
||||
btn.textContent = '生成中...';
|
||||
const formData = new FormData(form);
|
||||
const res = await fetch('/', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
if (res.ok) {
|
||||
// 生成成功,弹窗选择
|
||||
const blob = await res.blob();
|
||||
const filename = decodeURIComponent(res.headers.get('Content-Disposition').split('filename=')[1].replace(/['"]/g, ''));
|
||||
showChoiceDialog(blob, filename);
|
||||
} else {
|
||||
alert('生成失败,请重试!');
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.textContent = '生成PDF';
|
||||
};
|
||||
|
||||
function showChoiceDialog(blob, filename) {
|
||||
// 创建弹窗
|
||||
const mask = document.createElement('div');
|
||||
mask.style = 'position:fixed;left:0;top:0;width:100vw;height:100vh;background:rgba(0,0,0,0.25);z-index:9999;display:flex;align-items:center;justify-content:center;';
|
||||
const dialog = document.createElement('div');
|
||||
dialog.style = 'background:#fff;padding:32px 28px 28px 28px;border-radius:8px;box-shadow:0 2px 8px #888;text-align:center;min-width:260px;position:relative;';
|
||||
// 右上角关闭符号
|
||||
const closeBtn = document.createElement('span');
|
||||
closeBtn.innerHTML = '×';
|
||||
closeBtn.title = '关闭';
|
||||
closeBtn.style = 'position:absolute;top:10px;right:14px;font-size:22px;color:#888;cursor:pointer;font-weight:bold;';
|
||||
closeBtn.onmouseover = function(){closeBtn.style.color='#d32f2f';};
|
||||
closeBtn.onmouseout = function(){closeBtn.style.color='#888';};
|
||||
closeBtn.onclick = function(){ mask.remove(); };
|
||||
dialog.appendChild(closeBtn);
|
||||
// 选项按钮纵向排列
|
||||
const content = document.createElement('div');
|
||||
content.innerHTML = `<div style='font-size:18px;margin-bottom:22px;margin-top:6px;'>PDF已生成,选择操作:</div>`;
|
||||
const btnBox = document.createElement('div');
|
||||
btnBox.style = 'display:flex;flex-direction:column;gap:18px;align-items:center;';
|
||||
const dlBtn = document.createElement('button');
|
||||
dlBtn.id = 'dlBtn';
|
||||
dlBtn.textContent = '直接下载';
|
||||
dlBtn.style = 'padding:8px 28px 8px 28px;font-size:16px;';
|
||||
const shareBtn = document.createElement('button');
|
||||
shareBtn.id = 'shareBtn';
|
||||
shareBtn.textContent = '转发给好友';
|
||||
shareBtn.style = 'padding:8px 28px 8px 28px;font-size:16px;';
|
||||
btnBox.appendChild(dlBtn);
|
||||
btnBox.appendChild(shareBtn);
|
||||
content.appendChild(btnBox);
|
||||
dialog.appendChild(content);
|
||||
mask.appendChild(dialog);
|
||||
document.body.appendChild(mask);
|
||||
// 下载
|
||||
dlBtn.onclick = function() {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(()=>{
|
||||
URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
mask.remove();
|
||||
}, 100);
|
||||
};
|
||||
// 转发
|
||||
shareBtn.onclick = function() {
|
||||
// 生成临时下载链接
|
||||
const url = URL.createObjectURL(blob);
|
||||
dialog.innerHTML = `<div style='font-size:17px;margin-bottom:12px;'>将下方链接复制发给好友:</div>
|
||||
<input type='text' value='${url}' style='width:90%;padding:6px 8px;font-size:15px;' readonly onclick='this.select()'>
|
||||
<div style='margin-top:18px;'><button id='closeBtn' style='padding:7px 22px;font-size:15px;'>关闭</button></div>`;
|
||||
dialog.querySelector('#closeBtn').onclick = function() {
|
||||
setTimeout(()=>{
|
||||
URL.revokeObjectURL(url);
|
||||
mask.remove();
|
||||
}, 100);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// 使用说明弹窗
|
||||
const helpContent = `
|
||||
<h2>欢迎使用田字格生成器!</h2>
|
||||
<p>接下来,请您了解一些基本操作。</p>
|
||||
<h3>“一个字一页模式”</h3>
|
||||
<p>使用此模式,您可以生成如下图所示的田字格文件:</p>
|
||||
<img src="54a0a9d835e408fccb3e91d862aa16f2.png" alt="截图" style="zoom:30%;" />
|
||||
<p>只需选择“一个字一页模式”,在页面上方的输入框内输入您需要生成的文字,输入完毕后,点击页面下方的“生成PDF”按钮,即可生成。</p>
|
||||
<h3>“字帖模式”</h3>
|
||||
<p>使用此模式,您可以生成如下图所示的田字格文件:</p>
|
||||
<img src="d2319a550bb80518617218ac5075ce62.png" alt="截图" style="zoom:35%;" />
|
||||
<p>只需选择“字帖模式”,在页面上方的输入框内输入您需要生成的文字,输入完毕后,点击页面下方的“生成PDF”按钮,即可生成。</p>
|
||||
<h3>“字帖模式(竖版)”</h3>
|
||||
<p>使用此模式,您可以生成竖版字帖,范字出现在每列最上方的田字格内,从左往右排列,自动分页。</p>
|
||||
<p>只需选择“字帖模式(竖版)”,在页面上方的输入框内输入您需要生成的文字,输入完毕后,点击页面下方的“生成PDF”按钮,即可生成。</p>
|
||||
<h3>“5×6模式”</h3>
|
||||
<p>使用此模式,您可以生成5列6行的田字格,每页固定30个格子,文字从左往右、从上往下排列,自动分页。</p>
|
||||
<p>只需选择"5×6模式",在页面上方的输入框内输入您需要生成的文字,输入完毕后,点击页面下方的"生成PDF"按钮,即可生成。</p>
|
||||
<h3>“一个名字一页模式”</h3>
|
||||
<p>使用此模式,您可以生成如下图所示的田字格文件:</p>
|
||||
<img src="0ceb31f5ec3f99632f7533791595de9c.png" alt="截图" style="zoom:35%;" />
|
||||
<p><b>注意,这里是重头戏!</b>您需要在每输入一个人名之后,在输入框中敲击一下“空格”键,这样,程序才可以正常识别出人名,并为其分页生成。</p>
|
||||
<h3>生成之后...</h3>
|
||||
<p>不论您使用什么模式,点击“生成PDF”按钮之后,稍等片刻,程序会弹窗提醒您生成完毕。</p>
|
||||
<img src="1b71d31f5ac4f2f108146933c1d29c43.png" alt="截图" style="zoom:45%;" />
|
||||
<p>此时,您可以选择“直接下载”或者“转发给好友”。我们更建议您选择“直接下载”,这样可以在微信中直接预览。</p>
|
||||
`;
|
||||
|
||||
function showHelpDialog() {
|
||||
const mask = document.createElement('div');
|
||||
mask.style = 'position:fixed;left:0;top:0;width:100vw;height:100vh;background:rgba(0,0,0,0.25);z-index:9999;display:flex;align-items:center;justify-content:center;';
|
||||
const dialog = document.createElement('div');
|
||||
dialog.style = 'background:#fff;padding:32px 28px 28px 28px;border-radius:8px;box-shadow:0 2px 8px #888;text-align:left;min-width:320px;max-width:520px;max-height:80vh;overflow:auto;position:relative;';
|
||||
dialog.innerHTML = `<div style='margin-top:8px;'>${helpContent}</div>`;
|
||||
// 右上角关闭符号(最后append,确保在最上层)
|
||||
const closeBtn = document.createElement('span');
|
||||
closeBtn.innerHTML = '×';
|
||||
closeBtn.title = '关闭';
|
||||
closeBtn.style = 'position:absolute;top:10px;right:14px;font-size:22px;color:#888;cursor:pointer;font-weight:bold;z-index:10000;';
|
||||
closeBtn.onmouseover = function(){closeBtn.style.color='#d32f2f';};
|
||||
closeBtn.onmouseout = function(){closeBtn.style.color='#888';};
|
||||
closeBtn.onclick = function(){ mask.remove(); };
|
||||
dialog.appendChild(closeBtn);
|
||||
mask.appendChild(dialog);
|
||||
document.body.appendChild(mask);
|
||||
}
|
||||
document.getElementById('helpLink').onclick = function(e) {
|
||||
e.preventDefault();
|
||||
showHelpDialog();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## 结语
|
||||
|
||||
这次开发,真正让我体会到了科技改变生活。
|
||||
|
||||
另外,再提一嘴,蓝屏工具箱将在近一个月内发布,大家可以去关注[蓝屏工具箱官网](https://tools.cnstlapy.cn/)。
|
||||
Reference in New Issue
Block a user