乐于分享
好东西不私藏

一款简易笔记软件

一款简易笔记软件

      继上一篇文章,把电脑当做服务器,仅用来查看电脑资料确实没啥需求,当时也只是玩一玩而已,我电脑基本上不离身,所以想看啥直接看电脑就行。既然有把电脑当服务器使用这种想法,那就做一个笔记软件,这个还有一丁点用,喜欢私密性好一点的人狂喜,比如写写炒股日记啥的呀!这样电脑客户端和手机网页端都可以记笔记,手机写上传图片也比较方便,就不需要发到微信再保存到电脑上了。说干就干!
       首先明确一下自己的需求:1.要有一个登录界面;2.主界面尽可能简洁一点,只需要新建笔记和展示笔记就行了;3.要能上传并展示多张图片,而且可以删除和更换图片;4.笔记嘛,万一想分享给别人看,总不能截个图给人家看吧,还是导出pdf好一点,直接分享pdf,所以要有个导出pdf的功能。5.删除笔记的功能。
        剩下的交给AI就行了,坐着喝喝茶耐心等待就可以了!一段时间后……
网页端app.py代码:
from flask import Flask, render_template, request, redirect, url_for, session, make_response, jsonifyimport sqlite3import osimport timeimport hashlibimport tracebackimport urllib.parsefrom datetime import datetime, timedeltafrom io import BytesIOfrom reportlab.pdfgen import canvasfrom reportlab.pdfbase import pdfmetricsfrom reportlab.pdfbase.cidfonts import UnicodeCIDFontfrom reportlab.lib.utils import ImageReaderfrom PIL import Imageapp = Flask(__name__)app.secret_key = 'very_secret_key_123456'app.config['UPLOAD_FOLDER'] = 'static/uploads'os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)def beijing_time():    return (datetime.utcnow() + timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S')def init_db():    """初始化数据库,如果已存在则自动迁移(安全保留数据)"""    conn = sqlite3.connect('notes.db')    c = conn.cursor()    # 确保 users 表存在    c.execute('''CREATE TABLE IF NOT EXISTS users (        id INTEGER PRIMARY KEY AUTOINCREMENT,        username TEXT UNIQUE NOT NULL,        password TEXT NOT NULL    )''')    # 检查 notes 表结构    c.execute("PRAGMA table_info(notes)")    columns = [col[1for col in c.fetchall()]    # 如果 notes 表不存在,直接创建    if not columns:        c.execute('''CREATE TABLE notes (            id INTEGER PRIMARY KEY AUTOINCREMENT,            user_id INTEGER,            title TEXT NOT NULL,            content TEXT NOT NULL,            category TEXT DEFAULT '未分类',            created_at TIMESTAMP        )''')    else:        # 存在旧表,需要迁移        has_image = 'image' in columns        has_created_at = 'created_at' in columns        if has_image:            # 创建 note_images 表            c.execute('''CREATE TABLE IF NOT EXISTS note_images (                id INTEGER PRIMARY KEY AUTOINCREMENT,                note_id INTEGER NOT NULL,                filename TEXT NOT NULL,                original_name TEXT,                uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,                FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE            )''')            # 迁移旧图片            old_notes = c.execute("SELECT id, image FROM notes WHERE image IS NOT NULL AND image != ''").fetchall()            for note_id, img_name in old_notes:                if img_name:                    exists = c.execute("SELECT 1 FROM note_images WHERE note_id=? AND filename=?", (note_id, img_name)).fetchone()                    if not exists:                        c.execute("INSERT INTO note_images (note_id, filename, original_name) VALUES (?,?,?)",                                  (note_id, img_name, img_name))            # 创建新表结构(无 image 列)            c.execute('''CREATE TABLE notes_new (                id INTEGER PRIMARY KEY AUTOINCREMENT,                user_id INTEGER,                title TEXT NOT NULL,                content TEXT NOT NULL,                category TEXT DEFAULT '未分类',                created_at TIMESTAMP            )''')            if has_created_at:                c.execute('''INSERT INTO notes_new (id, user_id, title, content, category, created_at)                             SELECT id, user_id, title, content, category, created_at FROM notes''')            else:                c.execute('''INSERT INTO notes_new (id, user_id, title, content, category, created_at)                             SELECT id, user_id, title, content, category, ? FROM notes''', (beijing_time(),))            c.execute("DROP TABLE notes")            c.execute("ALTER TABLE notes_new RENAME TO notes")        else:            # 没有 image 列,但可能缺少 created_at 或类型不对            if not has_created_at:                c.execute("ALTER TABLE notes ADD COLUMN created_at TIMESTAMP")                # 为现有记录设置默认时间(北京时间)                c.execute("UPDATE notes SET created_at = ? WHERE created_at IS NULL", (beijing_time(),))    # 确保 note_images 表存在(如果没有创建)    c.execute('''CREATE TABLE IF NOT EXISTS note_images (        id INTEGER PRIMARY KEY AUTOINCREMENT,        note_id INTEGER NOT NULL,        filename TEXT NOT NULL,        original_name TEXT,        uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,        FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE    )''')    # 修正所有 created_at 为北京时间(如果之前是 UTC)    try:        c.execute("UPDATE notes SET created_at = datetime(created_at, '+8 hours') WHERE created_at IS NOT NULL AND created_at < '2024-01-01'")    except:        pass    # 创建默认管理员(如果不存在)    c.execute("SELECT * FROM users WHERE username='admin'")    if not c.fetchone():        c.execute("INSERT INTO users (username, password) VALUES (?,?)", ("admin""123456"))    conn.commit()    conn.close()@app.route('/login', methods=['GET','POST'])def login():    if request.method=='POST':        un=request.form['username']        pw=request.form['password']        conn=sqlite3.connect('notes.db')        user=conn.execute("SELECT id FROM users WHERE username=? AND password=?",(un,pw)).fetchone()        conn.close()        if user:            session['uid']=user[0]            return redirect(url_for('index'))        return "账号密码错误"    return render_template('login.html')@app.route('/logout')def logout():    session.clear()    return redirect(url_for('login'))@app.route('/')def index():    if 'uid' not in session:        return redirect(url_for('login'))    conn=sqlite3.connect('notes.db')    notes=conn.execute("SELECT id, title, content, category, created_at FROM notes WHERE user_id=? ORDER BY created_at DESC", (session['uid'],)).fetchall()    conn.close()    return render_template('index.html', notes=notes)@app.route('/api/notes/<int:note_id>/images')def get_note_images(note_id):    if 'uid' not in session:        return jsonify([])    conn = sqlite3.connect('notes.db')    images = conn.execute("SELECT id, filename, original_name FROM note_images WHERE note_id=?", (note_id,)).fetchall()    conn.close()    return jsonify([{'id': img[0], 'filename': img[1], 'original_name': img[2]} for img in images])@app.route('/api/notes/<int:note_id>/add_image', methods=['POST'])def add_note_image(note_id):    if 'uid' not in session:        return jsonify({'error''未登录'}), 401    if 'image' not in request.files:        return jsonify({'error''没有图片'}), 400    file = request.files['image']    if file.filename == '':        return jsonify({'error''文件名为空'}), 400    ext = file.filename.split('.')[-1if '.' in file.filename else 'jpg'    safe_name = f"{int(time.time())}_{hashlib.md5(file.filename.encode()).hexdigest()[:8]}.{ext}"    filepath = os.path.join(app.config['UPLOAD_FOLDER'], safe_name)    file.save(filepath)    conn = sqlite3.connect('notes.db')    conn.execute("INSERT INTO note_images (note_id, filename, original_name) VALUES (?,?,?)",                 (note_id, safe_name, file.filename))    conn.commit()    conn.close()    return jsonify({'success'True'filename': safe_name})@app.route('/api/images/<int:image_id>', methods=['DELETE'])def delete_image(image_id):    if 'uid' not in session:        return jsonify({'error''未登录'}), 401    conn = sqlite3.connect('notes.db')    img = conn.execute("SELECT filename FROM note_images WHERE id=?", (image_id,)).fetchone()    if img:        filepath = os.path.join(app.config['UPLOAD_FOLDER'], img[0])        if os.path.exists(filepath):            os.remove(filepath)        conn.execute("DELETE FROM note_images WHERE id=?", (image_id,))        conn.commit()    conn.close()    return jsonify({'success'True})@app.route('/api/images/<int:image_id>/replace', methods=['POST'])def replace_image(image_id):    if 'uid' not in session:        return jsonify({'error''未登录'}), 401    if 'image' not in request.files:        return jsonify({'error''没有图片'}), 400    file = request.files['image']    if file.filename == '':        return jsonify({'error''文件名为空'}), 400    conn = sqlite3.connect('notes.db')    old_img = conn.execute("SELECT filename FROM note_images WHERE id=?", (image_id,)).fetchone()    if not old_img:        conn.close()        return jsonify({'error''图片不存在'}), 404    old_path = os.path.join(app.config['UPLOAD_FOLDER'], old_img[0])    if os.path.exists(old_path):        os.remove(old_path)    ext = file.filename.split('.')[-1if '.' in file.filename else 'jpg'    safe_name = f"{int(time.time())}_{hashlib.md5(file.filename.encode()).hexdigest()[:8]}.{ext}"    filepath = os.path.join(app.config['UPLOAD_FOLDER'], safe_name)    file.save(filepath)    conn.execute("UPDATE note_images SET filename=?, original_name=? WHERE id=?",                 (safe_name, file.filename, image_id))    conn.commit()    conn.close()    return jsonify({'success'True'filename': safe_name})@app.route('/add', methods=['POST'])def add():    if 'uid' not in session:        return redirect(url_for('login'))    title = request.form['title']    content = request.form['content']    cate = request.form.get('category''未分类')    conn = sqlite3.connect('notes.db')    now = beijing_time()    cursor = conn.execute(        "INSERT INTO notes (user_id, title, content, category, created_at) VALUES (?,?,?,?,?)",        (session['uid'], title, content, cate, now)    )    note_id = cursor.lastrowid    files = request.files.getlist('images')    for file in files:        if file and file.filename:            ext = file.filename.split('.')[-1if '.' in file.filename else 'jpg'            safe_name = f"{int(time.time())}_{hashlib.md5(file.filename.encode()).hexdigest()[:8]}.{ext}"            file.save(os.path.join(app.config['UPLOAD_FOLDER'], safe_name))            conn.execute("INSERT INTO note_images (note_id, filename, original_name) VALUES (?,?,?)",                         (note_id, safe_name, file.filename))    conn.commit()    conn.close()    return redirect(url_for('index'))@app.route('/edit/<int:id>', methods=['GET''POST'])def edit(id):    if 'uid' not in session:        return redirect(url_for('login'))    conn = sqlite3.connect('notes.db')    if request.method == 'POST':        title = request.form['title']        content = request.form['content']        cate = request.form.get('category''未分类')        conn.execute(            "UPDATE notes SET title=?, content=?, category=? WHERE id=? AND user_id=?",            (title, content, cate, id, session['uid'])        )        conn.commit()        conn.close()        return redirect(url_for('index'))    note = conn.execute("SELECT * FROM notes WHERE id=? AND user_id=?", (id, session['uid'])).fetchone()    conn.close()    if not note:        return redirect(url_for('index'))    return render_template('edit.html', note=note)@app.route('/delete/<int:id>')def delete(id):    if 'uid' not in session:        return redirect(url_for('login'))    conn = sqlite3.connect('notes.db')    images = conn.execute("SELECT filename FROM note_images WHERE note_id=?", (id,)).fetchall()    for img in images:        filepath = os.path.join(app.config['UPLOAD_FOLDER'], img[0])        if os.path.exists(filepath):            os.remove(filepath)    conn.execute("DELETE FROM notes WHERE id=? AND user_id=?", (id, session['uid']))    conn.commit()    conn.close()    return redirect(url_for('index'))@app.route('/pdf/<int:id>')def export_pdf(id):    if 'uid' not in session:        return redirect(url_for('login'))    try:        conn = sqlite3.connect('notes.db')        note = conn.execute("SELECT title, content, category, created_at FROM notes WHERE id=? AND user_id=?", (id, session['uid'])).fetchone()        images = conn.execute("SELECT filename FROM note_images WHERE note_id=?", (id,)).fetchall()        conn.close()        if not note:            return "笔记不存在"404        buffer = BytesIO()        p = canvas.Canvas(buffer)        try:            pdfmetrics.registerFont(UnicodeCIDFont('STSong-Light'))            font_cn = 'STSong-Light'        except:            font_cn = 'Helvetica'        page_width = 595        page_height = 842        left_margin = 50        right_margin = 50        max_text_width = page_width - left_margin - right_margin        def draw_wrapped_text(p, text, x, y, font_name, font_size, max_width):            p.setFont(font_name, font_size)            lines = []            for para in text.split('\n'):                if not para.strip():                    lines.append('')                    continue                line = ''                for ch in para:                    test_line = line + ch                    if p.stringWidth(test_line, font_name, font_size) <= max_width:                        line = test_line                    else:                        lines.append(line)                        line = ch                if line:                    lines.append(line)            for line in lines:                if y < 40:                    p.showPage()                    p.setFont(font_name, font_size)                    y = page_height - 50                p.drawString(x, y, line)                y -= font_size + 4            return y        y = page_height - 50        p.setFont(font_cn, 18)        p.drawCentredString(page_width/2, y, note[0])        y -= 30        p.setFont(font_cn, 12)        p.drawString(left_margin, y, f"分类:{note[2]}    时间:{note[3]}")        y -= 20        p.line(left_margin, y, page_width - right_margin, y)        y -= 20        y = draw_wrapped_text(p, note[1], left_margin, y, font_cn, 12, max_text_width)        y -= 20        for img in images:            img_path = os.path.join(app.config['UPLOAD_FOLDER'], img[0])            if not os.path.exists(img_path):                continue            if y < 200:                p.showPage()                y = page_height - 50            try:                with Image.open(img_path) as pil_img:                    w, h = pil_img.size                    max_w, max_h = 400200                    scale = min(max_w/w, max_h/h, 1.0)                    draw_w = w * scale                    draw_h = h * scale                    x_center = (page_width - draw_w) / 2                    y -= draw_h + 10                    p.drawImage(img_path, x_center, y, width=draw_w, height=draw_h, preserveAspectRatio=True)                    y -= 10            except:                pass        p.save()        pdf_data = buffer.getvalue()        buffer.close()        safe_filename = urllib.parse.quote(note[0] + ".pdf")        response = make_response(pdf_data)        response.headers["Content-Type"] = "application/pdf"        response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{safe_filename}"        return response    except Exception as e:        print(traceback.format_exc())        return f"PDF 生成失败: {str(e)}"500if __name__ == '__main__':    init_db()    app.run(host='0.0.0.0', port=5000, debug=True)
电脑客户端代码(有点丑,将就着用,网页版看着舒服一点):
import customtkinter as ctkimport sqlite3import osimport shutilimport hashlibimport timefrom tkinter import messagebox, filedialogfrom datetime import datetime, timedeltafrom PIL import Image, ImageTkfrom reportlab.pdfgen import canvasfrom reportlab.pdfbase import pdfmetricsfrom reportlab.pdfbase.cidfonts import UnicodeCIDFontDB_PATH = "notes.db"UPLOAD_FOLDER = "static/uploads"os.makedirs(UPLOAD_FOLDER, exist_ok=True)ctk.set_appearance_mode("light")ctk.set_default_color_theme("blue")def beijing_time():    return (datetime.utcnow() + timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S')class NoteDesktopApp(ctk.CTk):    def __init__(self):        super().__init__()        self.title("笔记系统 - 多图片版")        self.geometry("1100x800")        self.current_user = None        self.selected_note_id = None        self.selected_card = None        self.main_frame = ctk.CTkFrame(self, fg_color="transparent")        self.main_frame.pack(expand=True, fill="both", padx=24, pady=24)        self.show_login_page()    def get_db(self):        return sqlite3.connect(DB_PATH)    def clear_page(self):        for w in self.main_frame.winfo_children():            w.destroy()    def show_login_page(self):        self.clear_page()        login_card = ctk.CTkFrame(self.main_frame, width=400, height=440, corner_radius=18)        login_card.place(relx=0.5, rely=0.5, anchor="center")        ctk.CTkLabel(login_card, text="账号登录", font=("Arial"28"bold")).pack(pady=40)        self.username_input = ctk.CTkEntry(login_card, placeholder_text="账号", width=340, height=54, corner_radius=12)        self.username_input.pack(pady=8)        self.password_input = ctk.CTkEntry(login_card, placeholder_text="密码", show="*", width=340, height=54, corner_radius=12)        self.password_input.pack(pady=8)        ctk.CTkButton(login_card, text="登录", width=340, height=54, font=("Arial"16"bold"),                      command=self.login).pack(pady=24)    def login(self):        user = self.username_input.get().strip()        pwd = self.password_input.get().strip()        db = self.get_db()        res = db.execute("SELECT id FROM users WHERE username=? AND password=?", (user, pwd)).fetchone()        db.close()        if res:            self.current_user = res[0]            self.show_main_page()        else:            messagebox.showerror("错误""账号或密码错误")    def show_main_page(self):        self.clear_page()        top_bar = ctk.CTkFrame(self.main_frame, height=70, corner_radius=14)        top_bar.pack(fill="x", pady=(018))        ctk.CTkLabel(top_bar, text="我的笔记", font=("Arial"22"bold")).pack(side="left", padx=24)        ctk.CTkButton(top_bar, text="导出PDF", width=130, height=46, corner_radius=10,                      fg_color="#059669", hover_color="#047857",                      command=self.export_pdf).pack(side="right", padx=10)        ctk.CTkButton(top_bar, text="+ 新增笔记", width=150, height=46, corner_radius=10,                      fg_color="#2563eb", hover_color="#1d4ed8",                      command=self.show_add_page).pack(side="right", padx=10)        ctk.CTkButton(top_bar, text="刷新", width=100, height=46, corner_radius=10,                      command=self.load_notes).pack(side="right", padx=10)        self.note_list = ctk.CTkScrollableFrame(self.main_frame, corner_radius=16, fg_color="#f8fafc")        self.note_list.pack(fill="both", expand=True, pady=6)        self.load_notes()    def load_notes(self):        for w in self.note_list.winfo_children():            w.destroy()        self.selected_note_id = None        self.selected_card = None        db = self.get_db()        notes = db.execute("SELECT id, title, category, created_at, content FROM notes WHERE user_id=? ORDER BY created_at DESC",                           (self.current_user,)).fetchall()        db.close()        for n in notes:            note_id, title, category, create_time, content = n            card = ctk.CTkFrame(self.note_list, height=160, corner_radius=16,                                fg_color="#ffffff", border_width=1, border_color="#e2e8f0")            card.pack(fill="x", pady=10, padx=12)            ctk.CTkLabel(card, text=title, font=("Arial"20"bold")).place(x=26, y=24)            ctk.CTkLabel(card, text=f"{category} • {create_time}",                         font=("Arial"13), text_color="#64748b").place(x=26, y=62)            preview = content[:70] + "..." if len(content) > 70 else content            ctk.CTkLabel(card, text=preview, font=("Arial"14), text_color="#475569").place(x=26, y=94)            btn_y = 55            ctk.CTkButton(card, text="查看", width=78, height=36, corner_radius=10,                          command=lambda nid=note_id: self.show_detail(nid)).place(x=560, y=btn_y)            ctk.CTkButton(card, text="编辑", width=78, height=36, corner_radius=10,                          fg_color="#1d4ed8", hover_color="#1e40af",                          command=lambda nid=note_id: self.show_edit_page(nid)).place(x=650, y=btn_y)            ctk.CTkButton(card, text="删除", width=78, height=36, corner_radius=10,                          fg_color="#ef4444", hover_color="#dc2626",                          command=lambda nid=note_id: self.delete_note(nid)).place(x=740, y=btn_y)            def select_this(nid=note_id, c=card):                if self.selected_card:                    self.selected_card.configure(fg_color="#ffffff", border_color="#e2e8f0")                c.configure(fg_color="#eff6ff", border_color="#3b82f6")                self.selected_note_id = nid                self.selected_card = c            card.bind("<Button-1>"lambda e, nid=note_id, c=card: select_this(nid, c))    def show_detail(self, nid):        self.clear_page()        top = ctk.CTkFrame(self.main_frame, height=70, corner_radius=14)        top.pack(fill="x", pady=(018))        ctk.CTkButton(top, text="← 返回", width=100, height=46, corner_radius=10,                      command=self.show_main_page).pack(side="left", padx=24)        db = self.get_db()        note = db.execute("SELECT title, content FROM notes WHERE id=?", (nid,)).fetchone()        images = db.execute("SELECT filename FROM note_images WHERE note_id=?", (nid,)).fetchall()        db.close()        frame = ctk.CTkFrame(self.main_frame, corner_radius=16)        frame.pack(fill="both", expand=True)        ctk.CTkLabel(frame, text=note[0], font=("Arial"24"bold")).pack(pady=26)        txt = ctk.CTkTextbox(frame, font=("Arial"15), corner_radius=12)        txt.pack(fill="both", expand=True, padx=26, pady=(016))        txt.insert("end", note[1])        txt.configure(state="disabled")        if images:            img_frame = ctk.CTkFrame(frame, fg_color="transparent")            img_frame.pack(pady=16, padx=26, fill="x")            for img in images:                img_path = os.path.join(UPLOAD_FOLDER, img[0])                if os.path.exists(img_path):                    try:                        pil_img = Image.open(img_path)                        pil_img.thumbnail((200150))                        tk_img = ctk.CTkImage(light_image=pil_img, size=(200150))                        lbl = ctk.CTkLabel(img_frame, image=tk_img, text="")                        lbl.pack(side="left", padx=5)                    except:                        pass    def show_add_page(self):        self.clear_page()        top = ctk.CTkFrame(self.main_frame, height=70, corner_radius=14)        top.pack(fill="x", pady=(018))        ctk.CTkButton(top, text="← 返回", command=self.show_main_page).pack(side="left", padx=24)        form = ctk.CTkScrollableFrame(self.main_frame, corner_radius=16)        form.pack(fill="both", expand=True)        ctk.CTkLabel(form, text="标题", font=("Arial"16)).pack(anchor="w", padx=28, pady=(266))        self.add_title = ctk.CTkEntry(form, width=920, height=52, corner_radius=10)        self.add_title.pack(padx=28)        ctk.CTkLabel(form, text="分类", font=("Arial"16)).pack(anchor="w", padx=28, pady=(186))        self.add_cate = ctk.CTkEntry(form, width=920, height=52, corner_radius=10)        self.add_cate.pack(padx=28)        ctk.CTkLabel(form, text="内容", font=("Arial"16)).pack(anchor="w", padx=28, pady=(186))        self.add_content = ctk.CTkTextbox(form, width=920, height=200, corner_radius=10)        self.add_content.pack(padx=28, pady=(016))        self.add_images_list = []        ctk.CTkLabel(form, text="图片", font=("Arial"16)).pack(anchor="w", padx=28, pady=(106))        self.add_images_frame = ctk.CTkFrame(form, fg_color="transparent")        self.add_images_frame.pack(padx=28, pady=10, fill="x")        ctk.CTkButton(form, text="选择图片 (可多选)", width=200, height=40,                      command=self.select_add_images).pack(padx=28, pady=10)        ctk.CTkButton(form, text="💾 保存笔记", width=920, height=54, corner_radius=10,                      font=("Arial"16"bold"), command=self.save_add_note).pack(padx=28, pady=20)    def select_add_images(self):        files = filedialog.askopenfilenames(filetypes=[("图片文件""*.jpg *.jpeg *.png *.gif *.bmp")])        for f in files:            self.add_images_list.append(f)            self.refresh_add_images_preview()    def refresh_add_images_preview(self):        for w in self.add_images_frame.winfo_children():            w.destroy()        for idx, img_path in enumerate(self.add_images_list):            try:                pil_img = Image.open(img_path)                pil_img.thumbnail((10080))                tk_img = ctk.CTkImage(light_image=pil_img, size=(10080))                lbl = ctk.CTkLabel(self.add_images_frame, image=tk_img, text="")                lbl.pack(side="left", padx=5)                def remove_img(i=idx):                    self.add_images_list.pop(i)                    self.refresh_add_images_preview()                btn = ctk.CTkButton(self.add_images_frame, text="✖", width=30, height=30, command=remove_img)                btn.pack(side="left", padx=2)            except:                pass    def save_add_note(self):        title = self.add_title.get().strip()        cate = self.add_cate.get().strip() or "未分类"        content = self.add_content.get("1.0""end").strip()        if not title:            messagebox.showwarning("提示""请填写标题")            return        now = beijing_time()        db = self.get_db()        cursor = db.execute(            "INSERT INTO notes (user_id, title, category, content, created_at) VALUES (?,?,?,?,?)",            (self.current_user, title, cate, content, now)        )        note_id = cursor.lastrowid        for img_path in self.add_images_list:            if os.path.exists(img_path):                ext = img_path.split('.')[-1]                safe_name = f"{int(time.time())}_{hashlib.md5(img_path.encode()).hexdigest()[:8]}.{ext}"                dest = os.path.join(UPLOAD_FOLDER, safe_name)                shutil.copy2(img_path, dest)                db.execute("INSERT INTO note_images (note_id, filename, original_name) VALUES (?,?,?)",                           (note_id, safe_name, os.path.basename(img_path)))        db.commit()        db.close()        messagebox.showinfo("成功""笔记已保存")        self.show_main_page()    def show_edit_page(self, edit_nid):        self.current_edit_id = edit_nid        self.clear_page()        top = ctk.CTkFrame(self.main_frame, height=70, corner_radius=14)        top.pack(fill="x", pady=(018))        ctk.CTkButton(top, text="← 返回", command=self.show_main_page).pack(side="left", padx=24)        db = self.get_db()        note = db.execute("SELECT title, category, content FROM notes WHERE id=?", (edit_nid,)).fetchone()        self.edit_images = db.execute("SELECT id, filename, original_name FROM note_images WHERE note_id=?", (edit_nid,)).fetchall()        db.close()        form = ctk.CTkScrollableFrame(self.main_frame, corner_radius=16)        form.pack(fill="both", expand=True)        ctk.CTkLabel(form, text="标题", font=("Arial"16)).pack(anchor="w", padx=28, pady=(266))        self.edit_title = ctk.CTkEntry(form, width=920, height=52, corner_radius=10)        self.edit_title.insert(0, note[0])        self.edit_title.pack(padx=28)        ctk.CTkLabel(form, text="分类", font=("Arial"16)).pack(anchor="w", padx=28, pady=(186))        self.edit_cate = ctk.CTkEntry(form, width=920, height=52, corner_radius=10)        self.edit_cate.insert(0, note[1])        self.edit_cate.pack(padx=28)        ctk.CTkLabel(form, text="内容", font=("Arial"16)).pack(anchor="w", padx=28, pady=(186))        self.edit_content = ctk.CTkTextbox(form, width=920, height=200, corner_radius=10)        self.edit_content.insert("end", note[2])        self.edit_content.pack(padx=28, pady=(016))        self.edit_images_frame = ctk.CTkFrame(form, fg_color="transparent")        self.edit_images_frame.pack(padx=28, pady=10, fill="x")        self.refresh_edit_images()        ctk.CTkButton(form, text="添加图片", width=150, height=40,                      command=self.select_edit_images).pack(padx=28, pady=10)        ctk.CTkButton(form, text="💾 保存修改", width=920, height=54, corner_radius=10,                      font=("Arial"16"bold"), command=self.save_edit_note).pack(padx=28, pady=20)    def refresh_edit_images(self):        for w in self.edit_images_frame.winfo_children():            w.destroy()        for idx, img in enumerate(self.edit_images):            img_id, filename, orig_name = img            img_path = os.path.join(UPLOAD_FOLDER, filename)            if os.path.exists(img_path):                try:                    pil_img = Image.open(img_path)                    pil_img.thumbnail((12090))                    tk_img = ctk.CTkImage(light_image=pil_img, size=(12090))                    frame = ctk.CTkFrame(self.edit_images_frame)                    frame.pack(side="left", padx=8, pady=5)                    lbl = ctk.CTkLabel(frame, image=tk_img, text="")                    lbl.pack()                    ctk.CTkLabel(frame, text=orig_name[:15], font=("Arial"10)).pack()                    btn_frame = ctk.CTkFrame(frame, fg_color="transparent")                    btn_frame.pack()                    ctk.CTkButton(btn_frame, text="删除", width=50, height=28,                                  command=lambda iid=img_id: self.delete_edit_image(iid)).pack(side="left", padx=2)                    ctk.CTkButton(btn_frame, text="更换", width=50, height=28,                                  command=lambda iid=img_id: self.replace_edit_image(iid)).pack(side="left", padx=2)                except:                    pass    def delete_edit_image(self, image_id):        if messagebox.askyesno("确认""删除这张图片?"):            db = self.get_db()            img = db.execute("SELECT filename FROM note_images WHERE id=?", (image_id,)).fetchone()            if img:                filepath = os.path.join(UPLOAD_FOLDER, img[0])                if os.path.exists(filepath):                    os.remove(filepath)                db.execute("DELETE FROM note_images WHERE id=?", (image_id,))                db.commit()            db.close()            db2 = self.get_db()            self.edit_images = db2.execute("SELECT id, filename, original_name FROM note_images WHERE note_id=?", (self.current_edit_id,)).fetchall()            db2.close()            self.refresh_edit_images()    def replace_edit_image(self, image_id):        file_path = filedialog.askopenfilename(filetypes=[("图片文件""*.jpg *.jpeg *.png *.gif *.bmp")])        if not file_path:            return        db = self.get_db()        old_img = db.execute("SELECT filename FROM note_images WHERE id=?", (image_id,)).fetchone()        if old_img:            old_path = os.path.join(UPLOAD_FOLDER, old_img[0])            if os.path.exists(old_path):                os.remove(old_path)            ext = file_path.split('.')[-1]            safe_name = f"{int(time.time())}_{hashlib.md5(file_path.encode()).hexdigest()[:8]}.{ext}"            dest = os.path.join(UPLOAD_FOLDER, safe_name)            shutil.copy2(file_path, dest)            db.execute("UPDATE note_images SET filename=?, original_name=? WHERE id=?",                       (safe_name, os.path.basename(file_path), image_id))            db.commit()        db.close()        db2 = self.get_db()        self.edit_images = db2.execute("SELECT id, filename, original_name FROM note_images WHERE note_id=?", (self.current_edit_id,)).fetchall()        db2.close()        self.refresh_edit_images()    def select_edit_images(self):        files = filedialog.askopenfilenames(filetypes=[("图片文件""*.jpg *.jpeg *.png *.gif *.bmp")])        if not files:            return        db = self.get_db()        for f in files:            ext = f.split('.')[-1]            safe_name = f"{int(time.time())}_{hashlib.md5(f.encode()).hexdigest()[:8]}.{ext}"            dest = os.path.join(UPLOAD_FOLDER, safe_name)            shutil.copy2(f, dest)            db.execute("INSERT INTO note_images (note_id, filename, original_name) VALUES (?,?,?)",                       (self.current_edit_id, safe_name, os.path.basename(f)))        db.commit()        db.close()        db2 = self.get_db()        self.edit_images = db2.execute("SELECT id, filename, original_name FROM note_images WHERE note_id=?", (self.current_edit_id,)).fetchall()        db2.close()        self.refresh_edit_images()    def save_edit_note(self):        nid = self.current_edit_id        title = self.edit_title.get().strip()        cate = self.edit_cate.get().strip() or "未分类"        content = self.edit_content.get("1.0""end").strip()        db = self.get_db()        db.execute("UPDATE notes SET title=?, category=?, content=? WHERE id=?",                   (title, cate, content, nid))        db.commit()        db.close()        messagebox.showinfo("成功""修改已保存")        self.show_main_page()    def delete_note(self, nid):        if not messagebox.askyesno("确认""删除笔记及所有图片?"):            return        db = self.get_db()        images = db.execute("SELECT filename FROM note_images WHERE note_id=?", (nid,)).fetchall()        for img in images:            filepath = os.path.join(UPLOAD_FOLDER, img[0])            if os.path.exists(filepath):                os.remove(filepath)        db.execute("DELETE FROM notes WHERE id=?", (nid,))        db.commit()        db.close()        self.load_notes()    def export_pdf(self):        if not self.selected_note_id:            messagebox.showwarning("提示""请先点击笔记卡片选中(变蓝)再导出")            return        db = self.get_db()        note = db.execute("SELECT title, category, created_at, content FROM notes WHERE id=?",                          (self.selected_note_id,)).fetchone()        images = db.execute("SELECT filename FROM note_images WHERE note_id=?", (self.selected_note_id,)).fetchall()        db.close()        path = filedialog.asksaveasfilename(defaultextension=".pdf", filetypes=[("PDF文件""*.pdf")],                                            initialfile=f"{note[0]}.pdf")        if not path:            return        try:            pdfmetrics.registerFont(UnicodeCIDFont('STSong-Light'))            font_cn = 'STSong-Light'        except:            font_cn = 'Helvetica'        c = canvas.Canvas(path)        page_width = 595        page_height = 842        left_margin = 50        right_margin = 50        max_text_width = page_width - left_margin - right_margin        def draw_wrapped_text(c, text, x, y, font_name, font_size, max_width):            c.setFont(font_name, font_size)            lines = []            for para in text.split('\n'):                if not para.strip():                    lines.append('')                    continue                line = ''                for ch in para:                    test_line = line + ch                    if c.stringWidth(test_line, font_name, font_size) <= max_width:                        line = test_line                    else:                        lines.append(line)                        line = ch                if line:                    lines.append(line)            for line in lines:                if y < 40:                    c.showPage()                    c.setFont(font_name, font_size)                    y = page_height - 50                c.drawString(x, y, line)                y -= font_size + 4            return y        y = page_height - 50        c.setFont(font_cn, 18)        c.drawCentredString(page_width/2, y, note[0])        y -= 30        c.setFont(font_cn, 12)        c.drawString(left_margin, y, f"分类:{note[1]}    时间:{note[2]}")        y -= 20        c.line(left_margin, y, page_width - right_margin, y)        y -= 20        y = draw_wrapped_text(c, note[3], left_margin, y, font_cn, 12, max_text_width)        y -= 20        for img in images:            img_path = os.path.join(UPLOAD_FOLDER, img[0])            if not os.path.exists(img_path):                continue            if y < 200:                c.showPage()                y = page_height - 50            try:                pil_img = Image.open(img_path)                w, h = pil_img.size                max_w, max_h = 400200                scale = min(max_w/w, max_h/h, 1)                draw_w = w * scale                draw_h = h * scale                x_center = (page_width - draw_w) / 2                y -= draw_h + 10                c.drawImage(img_path, x_center, y, width=draw_w, height=draw_h, preserveAspectRatio=True)                y -= 10            except:                pass        c.save()        messagebox.showinfo("成功""PDF 导出完成!")if __name__ == "__main__":    app = NoteDesktopApp()    app.mainloop()
login.html
<!DOCTYPE html><html><head><metacharset="utf-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>登录</title><style>*{box-sizing:border-box}body{background:#f5f7fa;padding:20px}.box{max-width:360px;margin:50px auto;background:white;padding:28px;border-radius:14px}input{width:100%;padding:12px;margin:8px 0;border:1px solid #ddd;border-radius:8px}button{width:100%;padding:12px;background:#2563eb;color:white;border:none;border-radius:8px}</style></head><body><divclass="box">    <h2>登录</h2>    <formmethod="post">        <inputname="username"placeholder="账号">        <inputname="password"type="password"placeholder="密码">        <buttontype="submit">登录</button>    </form>    <divstyle="margin-top:10px;color:#666"></div></div></body></html>
index.html
<!DOCTYPE html><html><head><metacharset="utf-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>我的笔记</title><style>*{box-sizing:border-box;font-family:system-ui}body{background:#f5f7fa;padding:20px;margin:0}.container{max-width:1000px;margin:0 auto}.card{background:white;border-radius:16px;padding:24px;margin-bottom:20px;box-shadow:0 4px 12px rgba(0,0,0,0.05)}.btn{background:#2563eb;color:white;padding:10px 18px;border-radius:10px;text-decoration:none;display:inline-block;margin-top:10px;margin-right:8px;font-size:14px;border:none;cursor:pointer;transition:all 0.2s}.btn:hover{opacity:0.9;transform:translateY(-1px)}.del{background:#ef4444}.pdf{background:#059669}input,textarea{width:100%;padding:12px;border:1px solid #e2e8f0;border-radius:12px;margin-bottom:12px;font-size:15px}textarea{min-height:120px;resize:vertical}.head{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.head a{color:#2563eb;text-decoration:none;font-weight:500}.cate{background:#e5e7eb;color:#374151;padding:4px 10px;border-radius:20px;font-size:12px}.time{color:#6b7280;font-size:12px;margin-left:10px}.note-title{font-size:20px;font-weight:600;margin:0 0 8px 0;color:#1e293b}.note-content{margin:14px 0;white-space:pre-wrap;color:#334155;line-height:1.6}.images-grid{display:flex;gap:12px;flex-wrap:wrap;margin:16px 0}.note-img{max-width:200px;max-height:150px;border-radius:12px;object-fit:cover;box-shadow:0 2px 6px rgba(0,0,0,0.1)}.image-item{position:relative}</style></head><body><divclass="container">    <divclass="head">        <h2>📒 我的笔记</h2>        <ahref="/logout">🚪 退出</a>    </div>    <divclass="card">        <h3>✏️ 写新笔记</h3>        <formmethod="post"action="/add"enctype="multipart/form-data">            <inputname="title"placeholder="标题"required>            <inputname="category"placeholder="分类"value="未分类">            <textareaname="content"placeholder="内容"required></textarea>            <inputtype="file"name="images"multipleaccept="image/*"style="padding:8px">            <buttonclass="btn"type="submit">💾 保存笔记</button>        </form>    </div>    {% for n in notes %}    <divclass="card">        <h3class="note-title">{{ n[1] }}</h3>        <div>            <spanclass="cate">{{ n[3] }}</span>            <spanclass="time">{{ n[4] }}</span>        </div>        <divclass="note-content">{{ n[2] }}</div>        <divclass="images-grid"id="images-{{ n[0] }}"></div>        <div>            <ahref="/pdf/{{ n[0] }}"class="btn pdf">📄 导出PDF</a>            <ahref="/edit/{{ n[0] }}"class="btn">✏️ 编辑</a>            <ahref="/delete/{{ n[0] }}"class="btn del"onclick="return confirm('删除笔记及所有图片?')">🗑️ 删除</a>        </div>    </div>    {% endfor %}</div><script>// 加载所有笔记的图片async function loadImages() {    const noteCards = document.querySelectorAll('.card');    for (const card of noteCards) {        const editLink = card.querySelector('a[href*="/edit/"]');        if (!editLink) continue;        const noteId = editLink.getAttribute('href').split('/')[2];        const container = document.getElementById(`images-${noteId}`);        if (container) {            const res = await fetch(`/api/notes/${noteId}/images`);            const images = await res.json();            container.innerHTML = images.map(img =>                 `<div class="image-item"><img class="note-img" src="/static/uploads/${img.filename}" alt="${img.original_name}"></div>`            ).join('');        }    }}loadImages();</script></body></html>
edit.html
<!DOCTYPE html><htmllang="zh-CN"><head>    <metacharset="utf-8">    <metaname="viewport"content="width=device-width, initial-scale=1.0">    <title>编辑笔记</title>    <style>        *{box-sizing:border-box;font-family:system-ui}        body{background:#f5f7fa;padding:20px;margin:0}        .container{max-width:800px;margin:0 auto}        .back-btn{display:inline-flex;align-items:center;gap:6px;margin-bottom:20px;padding:8px 16px;background:white;border-radius:12px;text-decoration:none;color:#374151;font-weight:500;box-shadow:0 1px 3px rgba(0,0,0,0.08)}        .card{background:white;border-radius:20px;padding:28px;box-shadow:0 4px 16px rgba(0,0,0,0.05)}        input,textarea{width:100%;padding:14px;border:1px solid #e2e8f0;border-radius:12px;margin-bottom:16px;font-size:15px}        textarea{min-height:160px;resize:vertical}        .save-btn{width:100%;padding:14px;background:#2563eb;color:white;border:none;border-radius:12px;font-size:16px;font-weight:500;cursor:pointer}        .images-section{margin:20px 0;padding:16px;background:#f8fafc;border-radius:16px}        .images-grid{display:flex;gap:16px;flex-wrap:wrap;margin-top:12px}        .image-card{position:relative;width:160px;background:white;border-radius:12px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.1)}        .image-card img{width:100%;height:120px;object-fit:cover;border-radius:8px}        .image-actions{display:flex;gap:6px;margin-top:8px}        .img-btn{flex:1;padding:5px;border:none;border-radius:6px;cursor:pointer;font-size:12px}        .del-img{background:#ef4444;color:white}        .replace-img{background:#2563eb;color:white}        .add-img-btn{background:#10b981;color:white;border:none;padding:10px 16px;border-radius:10px;cursor:pointer;margin-top:12px}    </style></head><body><divclass="container">    <ahref="/"class="back-btn">← 返回笔记列表</a>    <divclass="card">        <formmethod="post"id="editForm">            <inputname="title"value="{{ note[2] }}"requiredplaceholder="标题">            <inputname="category"value="{{ note[4] }}"placeholder="分类">            <textareaname="content"requiredplaceholder="内容">{{ note[3] }}</textarea>            <buttonclass="save-btn"type="submit">💾 保存修改</button>        </form>        <divclass="images-section">            <h4>📷 图片管理</h4>            <divclass="images-grid"id="imagesGrid"></div>            <buttonclass="add-img-btn"id="addImageBtn">➕ 添加图片</button>            <inputtype="file"id="imageInput"accept="image/*"style="display:none">        </div>    </div></div><script>const noteId = {{ note[0] }};let currentImages = [];async function loadImages() {    const res = await fetch(`/api/notes/${noteId}/images`);    currentImages = await res.json();    renderImages();}function renderImages() {    const grid = document.getElementById('imagesGrid');    if (currentImages.length === 0) {        grid.innerHTML = '<p style="color:#94a3b8">暂无图片,点击上方按钮添加</p>';        return;    }    grid.innerHTML = currentImages.map(img => `        <div class="image-card" data-id="${img.id}">            <img src="/static/uploads/${img.filename}" alt="${img.original_name}">            <div class="image-actions">                <button class="img-btn replace-img" onclick="replaceImage(${img.id})">更换</button>                <button class="img-btn del-img" onclick="deleteImage(${img.id})">删除</button>            </div>        </div>    `).join('');}async function deleteImage(imageId) {    if (!confirm('确定删除这张图片?')) return;    const res = await fetch(`/api/images/${imageId}`, { method'DELETE' });    if (res.ok) {        await loadImages();    } else {        alert('删除失败');    }}function replaceImage(imageId) {    const input = document.createElement('input');    input.type = 'file';    input.accept = 'image/*';    input.onchange = async (e) => {        const file = e.target.files[0];        if (!file) return;        const formData = new FormData();        formData.append('image', file);        const res = await fetch(`/api/images/${imageId}/replace`, { method'POST'body: formData });        if (res.ok) {            await loadImages();        } else {            alert('更换失败');        }    };    input.click();}document.getElementById('addImageBtn').onclick = () => {    document.getElementById('imageInput').click();};document.getElementById('imageInput').onchange = async (e) => {    const file = e.target.files[0];    if (!file) return;    const formData = new FormData();    formData.append('image', file);    const res = await fetch(`/api/notes/${noteId}/add_image`, { method'POST'body: formData });    if (res.ok) {        await loadImages();    } else {        alert('添加失败');    }    e.target.value = '';};loadImages();</script></body></html>
pdf.html
<!DOCTYPE html><html><head><metacharset="utf-8"><style>body{font-family:sans-serif;line-height:1.6;margin:20px}h1{text-align:center}.meta{color:#666;margin-bottom:20px}.content{white-space:pre-wrap;margin-bottom:30px}img{max-width:100%;height:auto;margin:10px 0}</style></head><body>    <h1>{{ note[2] }}</h1>    <divclass="meta">分类:{{ note[4] }} | 时间:{{ note[6] }}</div>    <divclass="content">{{ note[3] }}</div>    {% if note[5] %}    <imgsrc="{{ base }}static/uploads/{{ note[5] }}">    {% endif %}</body></html>
然后就是运行了,缺少什么python库报什么错直接问AI就行了,安装相应的库就行了。没啥可说的。然后要在网页端登录,跟上一篇文章一样,注册个cpolar账号,访问 cpolar 官网(https://www.cpolar.com),下载cpolar,然后将网站里的这一串复制到”cpolar authtoken YOUR_AUTH_TOKEN”命令提示符里输入即可。
然后就要在软件所在文件夹里打开两个命令提示符窗口,分别运行”python app.py”和”start “cpolar http 5000″,但是这样每次电脑开机后都要在这个路径里输入就很麻烦。所有直接新建一个自启动start.bat丢进电脑自启动文件夹里就行了,这样电脑一开机,就能自动运行软件,只需要登录窗口里生成的随机网址就可以在手机浏览器上登录了。
start.bat代码(路径改为自己软件所在位置):
cd /d D:\note_web:: 先杀掉所有已运行的cpolar进程,解决并发限制taskkill /f /im cpolar.exe >nul 2>&1:: 打开第一个独立窗口:运行 Pythonstart "Python服务" cmd /k "python app.py":: 打开第二个独立窗口:运行 cpolarstart "cpolar穿透" cmd /k "cpolar http 5000"exit
至于电脑自启动文件夹位置怎么找,win+R,输入shell:startup就行了。
软件网页版主界面如下:
修改笔记界面:
导出的pdf:
如果需要源代码,私信我就行,免费给!如果回复晚了,还请见谅!