feat: 同步服务托管网页应用,daywk 单域名部署就绪
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
release/
|
||||
*.local
|
||||
android/.gradle/
|
||||
|
||||
+24
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<rect x="1.6" y="1.6" width="20.8" height="20.8" rx="4.6" fill="#3370FF"/>
|
||||
<rect x="5.8" y="7.2" width="12.4" height="11.5" rx="1.1" fill="#FFFFFF"/>
|
||||
<rect x="5.8" y="7.2" width="12.4" height="2.2" fill="#3370FF"/>
|
||||
<rect x="7.9" y="4.9" width="1.1" height="3.2" rx="0.5" fill="#FFFFFF"/>
|
||||
<rect x="15.0" y="4.9" width="1.1" height="3.2" rx="0.5" fill="#FFFFFF"/>
|
||||
<circle cx="8.6" cy="12.4" r="0.62" fill="#3370FF"/>
|
||||
<circle cx="12" cy="12.4" r="0.62" fill="#3370FF"/>
|
||||
<circle cx="15.4" cy="12.4" r="0.62" fill="#3370FF"/>
|
||||
<circle cx="8.6" cy="15.4" r="0.62" fill="#3370FF"/>
|
||||
<circle cx="12" cy="15.4" r="0.62" fill="#3370FF"/>
|
||||
<circle cx="15.4" cy="15.4" r="0.62" fill="#3370FF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 770 B |
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
ACCENT = (51, 112, 255, 255)
|
||||
WHITE = (255, 255, 255, 255)
|
||||
|
||||
def make(size, path):
|
||||
img = Image.new('RGBA', (size, size), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(img)
|
||||
m = size * 0.07
|
||||
d.rounded_rectangle([m, m, size - m, size - m], radius=size * 0.19, fill=ACCENT)
|
||||
x0, y0, x1, y1 = size * 0.24, size * 0.30, size * 0.76, size * 0.78
|
||||
d.rounded_rectangle([x0, y0, x1, y1], radius=size * 0.045, fill=WHITE)
|
||||
d.rectangle([x0, y0 + size * 0.045, x1, y0 + size * 0.09], fill=ACCENT)
|
||||
for cx in (0.36, 0.64):
|
||||
d.rounded_rectangle([size * cx - size * 0.02, size * 0.20, size * cx + size * 0.02, size * 0.33], radius=size * 0.02, fill=WHITE)
|
||||
for dx in (0.34, 0.5, 0.66):
|
||||
for dy in (0.5, 0.63):
|
||||
d.ellipse([size * dx - size * 0.025, size * dy - size * 0.025, size * dx + size * 0.025, size * dy + size * 0.025], fill=ACCENT)
|
||||
img.save(path)
|
||||
|
||||
make(192, 'icon-192.png')
|
||||
make(512, 'icon-512.png')
|
||||
print('icons generated')
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#FFFFFF" />
|
||||
<link rel="manifest" href="./manifest.webmanifest" />
|
||||
<link rel="icon" href="./icons/icon.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="./icons/icon-192.png" />
|
||||
<title>实习工作日志</title>
|
||||
<script type="module" crossorigin src="./assets/index-DMG3WiLM.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-hTAHZZ1V.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "实习工作日志",
|
||||
"short_name": "工作日志",
|
||||
"description": "实习打卡、工作记录、备忘录与加密密码库",
|
||||
"start_url": "./",
|
||||
"scope": "./",
|
||||
"display": "standalone",
|
||||
"background_color": "#FFFFFF",
|
||||
"theme_color": "#FFFFFF",
|
||||
"icons": [
|
||||
{ "src": "./icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "./icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
|
||||
{ "src": "./icons/icon.svg", "sizes": "any", "type": "image/svg+xml" }
|
||||
]
|
||||
}
|
||||
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
const CACHE = 'intern-log-v1'
|
||||
const CORE = ['./', './index.html']
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(caches.open(CACHE).then(c => c.addAll(CORE)).then(() => self.skipWaiting()))
|
||||
})
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k))))
|
||||
.then(() => self.clients.claim())
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
const req = event.request
|
||||
if (req.method !== 'GET') return
|
||||
const url = new URL(req.url)
|
||||
if (url.origin !== location.origin) return
|
||||
|
||||
// 页面导航:网络优先,离线回退缓存的应用壳
|
||||
if (req.mode === 'navigate') {
|
||||
event.respondWith(
|
||||
fetch(req)
|
||||
.then(resp => {
|
||||
const clone = resp.clone()
|
||||
caches.open(CACHE).then(c => c.put('./index.html', clone))
|
||||
return resp
|
||||
})
|
||||
.catch(() => caches.match('./index.html'))
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// 静态资源:缓存优先,未命中时拉取并写入缓存
|
||||
event.respondWith(
|
||||
caches.match(req).then(hit => hit || fetch(req).then(resp => {
|
||||
if (resp.ok) {
|
||||
const clone = resp.clone()
|
||||
caches.open(CACHE).then(c => c.put(req, clone))
|
||||
}
|
||||
return resp
|
||||
}))
|
||||
)
|
||||
})
|
||||
@@ -35,6 +35,11 @@ Caddy 会自动为 daywk.linyux.cn 申请并续期 HTTPS 证书(需 DNS 已生
|
||||
|
||||
应返回 {"ok":true}。
|
||||
|
||||
## 重要:该域名同时是应用入口
|
||||
|
||||
同步服务容器同时托管了网页应用本体(daywork 仓库中的构建产物)。手机与电脑浏览器直接打开
|
||||
https://daywk.linyux.cn 就是应用(可添加到主屏幕作为 PWA),无需另外部署前端;/api 路径为数据接口。
|
||||
|
||||
## 五、客户端
|
||||
|
||||
应用 → 设置 → 多端数据同步:地址填 https://daywk.linyux.cn,注册云端账号后同步。
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
COPY package.json index.js ./
|
||||
COPY server/package.json server/index.js ./
|
||||
COPY intern-log/dist ./dist
|
||||
EXPOSE 3780
|
||||
CMD ["node", "index.js"]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"secret":"ab5dd60e3a42a471a54c8db1176d09ddd65b2d60f42629b8ba94274341ae9dc4"}
|
||||
@@ -1,6 +1,8 @@
|
||||
services:
|
||||
internlog-sync:
|
||||
build: .
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: server/Dockerfile
|
||||
container_name: internlog-sync
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
|
||||
@@ -122,6 +122,22 @@ function readBody(req, limit = 20 * 1024 * 1024) {
|
||||
})
|
||||
}
|
||||
|
||||
// ---------- 静态托管网页应用 ----------
|
||||
// 容器内为 /app/dist;本机直跑时回退到仓库内的 intern-log/dist
|
||||
const DIST_CANDIDATES = [path.join(__dirname, 'dist'), path.join(__dirname, '..', 'intern-log', 'dist')]
|
||||
const DIST_DIR = process.env.DIST_DIR || DIST_CANDIDATES.find(p => fs.existsSync(path.join(p, 'index.html'))) || path.join(__dirname, 'dist')
|
||||
const MIME = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.js': 'text/javascript',
|
||||
'.css': 'text/css',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.png': 'image/png',
|
||||
'.json': 'application/json',
|
||||
'.webmanifest': 'application/manifest+json',
|
||||
'.ico': 'image/x-icon',
|
||||
'.woff2': 'font/woff2'
|
||||
}
|
||||
|
||||
// ---------- 路由 ----------
|
||||
async function handle(req, res) {
|
||||
const url = new URL(req.url, 'http://localhost')
|
||||
@@ -129,6 +145,27 @@ async function handle(req, res) {
|
||||
|
||||
if (req.method === 'OPTIONS') { json(res, 204, {}); return }
|
||||
|
||||
// 非 API 的 GET 请求:返回网页应用(构建产物)
|
||||
if (req.method === 'GET' && !route.startsWith('/api')) {
|
||||
let rel = decodeURIComponent(url.pathname)
|
||||
if (rel === '/' || rel === '') rel = '/index.html'
|
||||
const file = path.normalize(path.join(DIST_DIR, rel))
|
||||
if (!file.startsWith(DIST_DIR)) { json(res, 403, { error: 'forbidden' }); return }
|
||||
fs.readFile(file, (err, data) => {
|
||||
if (err) {
|
||||
fs.readFile(path.join(DIST_DIR, 'index.html'), (e2, d2) => {
|
||||
if (e2) { json(res, 404, { error: 'not found' }); return }
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
||||
res.end(d2)
|
||||
})
|
||||
return
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': MIME[path.extname(file).toLowerCase()] || 'application/octet-stream' })
|
||||
res.end(data)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && route === '/api/health') { json(res, 200, { ok: true }); return }
|
||||
|
||||
// SSE 事件流:token 经查询参数传递(EventSource 无法自定义请求头)
|
||||
|
||||
@@ -534,3 +534,4 @@ src/
|
||||
- v1.13.1:登录会话持久化(localStorage,关闭浏览器后免登录,无需每次使用都登录);部署方案调整为 Docker + Caddy 自动 HTTPS(独立子域名与现有域名路径两种反代方式);移除 30 分钟无操作自动退出(密码库仍保留 5 分钟自动锁定与主密码门槛)
|
||||
- v1.13.2:同步服务独立子域名部署 daywk.linyux.cn(Caddy 新站点块,客户端同步地址 https://daywk.linyux.cn)
|
||||
- v1.13.3:准实时同步——本地数据变更 3 秒内自动推送(Dexie 变更事件触发);服务端新增 SSE 事件流,其他端变更秒级拉取;60 秒轮询兜底;修复引擎与服务端认证方式不匹配问题(登录换取 HMAC token 并缓存,失效自动重登);同步并发加锁防重入
|
||||
- v1.13.4:同步服务同时托管网页应用构建产物——daywk.linyux.cn 单域名即应用入口与数据接口,手机 PWA 零额外部署;dist 随仓库分发,服务器克隆后一次构建全部完成
|
||||
|
||||
Reference in New Issue
Block a user