你是不是也经历过这样的瞬间?
明明手机就在手边,想传个截图到电脑修图,结果打开微信,点开"文件传输助手",发送,等半天,还得在电脑上登录微信,下载......一套流程走下来,修图的心情都没了。反过来,想把电脑上写好的文案发给手机,更麻烦。
至于剪贴板?手机看到的好句子,想在电脑上搜一下,要么靠手打,要么靠发条消息再复制。这哪是科技时代,这简直是"手动挡"生活嘛!
作为每天在电脑和手机之间来回切换的一名程序媛,这个问题困扰我很久了。市面上的"隔空传送 "不是不好用,是生态限制太死。终于有一天,我忍不了了,决定自己动手,用咱程序员最熟悉的FastAPI,搭一个"私家传送站"。
核心摘要: 今天这篇文,不是让你读文档。我会手把手带你写一个轻量级的Web应用,跑在你电脑上。然后,只要是连在了同一局域网的手机或平板电脑等网络设备,打开浏览器,就能上传下载文件,还能同步剪贴板。全程代码不超过100行,安全、私有、还免费!
📦 先看看咱要搭的东西长啥样
想象一下,你电脑上开了个服务,手机浏览器里打开一个页面。页面上半部分是一个文件上传区,点一下,选手机里的照片,秒传回电脑指定文件夹。页面下半部分是一个剪贴板文本框,你在电脑上复制了代码片段,打开手机页面,它就在那等着你粘贴。反之亦然。
简单,直接,没有中间商赚差价。
🎯 为什么是FastAPI?
你可能会问:为啥不选Flask或者Django?好问题!我选FastAPI的原因很简单:快。这里的"快"是双关,一是它性能好,底层是异步的;二是它开发极快,自带交互式API文档,调试起来爽歪歪。对于咱们这种小工具,简直量身定做。
而且,它的文件上传处理,是我用过最优雅的,没有之一。
⚙️ 实战:从零开始的私家传送站
好,咱们不废话,直接撸代码。我会把完整代码拆开讲,你复制粘贴就能跑。不过,先别急着复制,听我说个坑:Python版本建议3.8以上,否则有些依赖会让你怀疑人生。当初我偷懒用3.7,结果一个依赖报错,排查了一小时,血泪教训!
📁 第一步:创建项目文件夹,安装依赖
打开终端,敲几行命令:
uv init file_clipboard_server # 创建虚拟环境
cd file_clipboard_server # 进入项目目录
uv add fastapi uvicorn python-multipart # 安装依赖
这里重点来了!python-multipart 这个库是必须的,没有它,FastAPI没法解析上传的文件。官方文档虽然说了,但很多人看文档不仔细,漏掉这步,然后回来问我为啥上传不了。记住了啊!
💻 第二步:编写核心代码 main.py
在项目文件夹里新建一个 main.py 文件,把下面这段代码丢进去。我加了详细注释,你就当我是边写边跟你唠嗑。
from fastapi import FastAPI, File, UploadFile, Request
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
import os
import uvicorn
app = FastAPI()
# 用来存放上传文件的目录,没有就自动创建
UPLOAD_DIR = "uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)
# 用来存剪贴板内容的简单变量(注意:重启服务就没了,但够用了)
clipboard_content = ""
# 主页,返回一个简单的HTML页面
@app.get("/", response_class=HTMLResponse)
async def main_page():
# 这个HTML我稍后会解释,你先复制
html_content = """
<!DOCTYPE html>
<html>
<head>
<title>私家传送站</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; max-width: 600px; margin: 40px auto; padding: 0 20px; background: #f9f9f9; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }
h2 { margin-top: 0; font-size: 1.5rem; }
input, textarea, button { width: 100%; padding: 12px; margin: 8px 0; border: 1px solid #ddd; border-radius: 8px; font-size: 16px; box-sizing: border-box; }
button { background-color: #3498db; color: white; border: none; font-weight: bold; cursor: pointer; }
button:hover { background-color: #2980b9; }
.result { margin-top: 16px; padding: 12px; background: #f0f7ff; border-radius: 8px; font-size: 14px; word-break: break-all; }
</style>
</head>
<body>
<div class="card">
<h2>📎 文件互传</h2>
<form id="uploadForm" enctype="multipart/form-data">
<input type="file" name="file" id="fileInput" required>
<button type="submit">上传到电脑</button>
</form>
<div id="uploadResult" class="result"></div>
</div>
<div class="card">
<h2>📋 剪贴板同步</h2>
<textarea id="clipText" rows="4" placeholder="在这里粘贴或查看文本..."></textarea>
<button id="syncToServer">📤 同步到电脑</button>
<button id="loadFromServer">📥 从电脑获取</button>
<div id="clipResult" class="result"></div>
</div>
<script>
// 文件上传逻辑
document.getElementById('uploadForm').onsubmit = async (e) => {
e.preventDefault();
const file = document.getElementById('fileInput').files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/upload', { method: 'POST', body: formData });
const data = await res.json();
document.getElementById('uploadResult').innerHTML = `✅ ${data.filename} 上传成功`;
};
// 剪贴板同步
document.getElementById('syncToServer').onclick = async () => {
const text = document.getElementById('clipText').value;
const res = await fetch('/clipboard', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: text }) });
const data = await res.json();
document.getElementById('clipResult').innerHTML = `📤 ${data.message}`;
};
document.getElementById('loadFromServer').onclick = async () => {
const res = await fetch('/clipboard');
const data = await res.json();
document.getElementById('clipText').value = data.content;
document.getElementById('clipResult').innerHTML = `📥 已同步: ${data.content.substring(0, 50)}`;
};
</script>
</body>
</html>
"""
return html_content
# 文件上传接口
@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
file_path = os.path.join(UPLOAD_DIR, file.filename)
# 防止文件名冲突的小处理,这里简单覆盖同名文件
with open(file_path, "wb") as buffer:
buffer.write(await file.read())
return {"filename": file.filename, "message": "上传成功"}
# 获取剪贴板内容
@app.get("/clipboard")
async def get_clipboard():
return {"content": clipboard_content}
# 更新剪贴板内容
@app.post("/clipboard")
async def update_clipboard(request: Request):
global clipboard_content
data = await request.json()
clipboard_content = data.get("content", "")
return {"message": "剪贴板已更新"}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
这段代码里,HTML部分我故意写得比较"原始",就是为了让大家看得懂。别嫌丑,功能第一!
🚀 第三步:跑起来!
在终端运行:
uv run main.py
看到 Uvicorn running on http://0.0.0.0:8000 这样的提示,就说明成功了!
现在,拿起你的手机,连上和电脑同一个Wi-Fi,打开浏览器,输入 电脑的局域网IP:8000。怎么看电脑IP?Windows用 ipconfig,Mac/Linux用 ifconfig,找到类似 192.168.x.x 的地址就行。
是不是以为这样就完了?别急,这里有个容易翻车的点:防火墙。如果手机死活打不开,八成是电脑防火墙拦住了8000端口。去防火墙设置里加一条入站规则,允许8000端口访问。我当时就卡在这步,折腾了好久。
🎉 成品长这样
当手机页面打开的那一刻,你会看到一个干净的两块区域。点"上传到电脑",你手机里的图片或文件就嗖的一下飞到电脑的 uploads 文件夹里了。
在电脑上复制粘贴一段文字到文本框中,点击"同步到电脑",在手机上点"从电脑获取",它立刻就出现了。反过来,在手机上输入文字,点"同步到电脑",这个文字就被存到了服务端的变量里,你在电脑上随时可以调用。
这个功能纯属顺手一加,结果成了真香现场。我经常在手机上刷到好文章,复制金句,点一下同步,电脑上打开IDE写文章时,直接就能贴上去。无缝衔接!
⚠️ 几点不得不提的注意事项
- 安全性:这个小工具只建议在内网(家里/公司Wi-Fi)使用。如果暴露在公网,没有做任何鉴权,别人也能访问,风险很大。别图方便把端口映射出去!
- 剪贴板持久化:我们这里用了内存变量,服务重启就没了。如果想持久化,可以改成存文件或数据库,代码改动很小,留作你的课后作业吧。
- 大文件上传:如果传视频这种大文件,可以加上进度条,或者用分片上传。但作为日常传点照片文档,这个版本绰绰有余。
🚧 进阶思考:还能怎么玩?
当你把这个小东西跑起来之后,你会发现它的潜力远不止这些。你可以把它改成一个临时的"公共相册",朋友们聚会时扫码上传照片;或者把它变成一个跨平台的"写作同步工具",在手机上写大纲,电脑上直接继续。甚至,你可以用类似的方法,实现电脑控制手机播放PPT?脑洞大开的时刻来了!
这个工具的选择,好比选螺丝刀,不是最贵的就好,而是顺手、能解决问题的就是最好的。FastAPI这个小螺丝刀,我用得挺顺手,希望你也是。
好啦,以上就是今天的全部内容。
如果你也跟着跑起来了,恭喜你,又多了一个专属的效率工具!如果卡在某个环节,欢迎留言,我会第一时间帮你看看,毕竟这些坑我也都踩过。
❤️ 如果这篇文章帮到了你,点赞、收藏、关注 支持一下~ 你的支持是我继续分享"踩坑笔记"的最大动力!
咱们下篇再见,继续聊聊那些让生活更简单的技术小玩意儿。