Skip to content

Commit 92c39dd

Browse files
feat: Mobile deep optimization, production infrastructure, and CD pipeline
Mobile (85% → 99%): - Editor bubble menus overflow handling with viewport-aware max-width - Responsive editor padding, TOC sidebar hidden on mobile - Block menu touch support, document tree TouchSensor with delay - Comment panel full-screen overlay on mobile - Table popover viewport-aware positioning - Dashboard header/KPI grid, admin search/pagination responsive - Analytics chart padding, spaces hero, explore sort bar overflow - Slash command menu viewport cap, keyboard shortcuts hidden on mobile Infrastructure (92% → 99%): - Multi-stage API Dockerfile with non-root user (nestapi:1001) - CD workflow: build Docker images → push GHCR → SSH deploy - Automated DB backup (daily pg_dump + 30-day retention) - Docker log rotation (json-file, 10MB × 5, compressed) - Nginx reverse proxy service with SSL/certbot auto-renewal - ACME challenge support and HTTPS pre-configuration Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2ae5ba4 commit 92c39dd

22 files changed

Lines changed: 585 additions & 81 deletions

File tree

.github/workflows/cd.yml

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
name: CD
2+
3+
on:
4+
push:
5+
branches: [master]
6+
7+
env:
8+
REGISTRY: ghcr.io
9+
IMAGE_PREFIX: ${{ github.repository }}
10+
11+
jobs:
12+
build-and-push:
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: read
16+
packages: write
17+
18+
strategy:
19+
matrix:
20+
include:
21+
- app: api
22+
dockerfile: apps/api/Dockerfile
23+
- app: web
24+
dockerfile: apps/web/Dockerfile
25+
26+
steps:
27+
- uses: actions/checkout@v4
28+
29+
- name: Log in to Container Registry
30+
uses: docker/login-action@v3
31+
with:
32+
registry: ${{ env.REGISTRY }}
33+
username: ${{ github.actor }}
34+
password: ${{ secrets.GITHUB_TOKEN }}
35+
36+
- name: Extract metadata
37+
id: meta
38+
uses: docker/metadata-action@v5
39+
with:
40+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.app }}
41+
tags: |
42+
type=sha,prefix=
43+
type=raw,value=latest
44+
45+
- name: Set up Docker Buildx
46+
uses: docker/setup-buildx-action@v3
47+
48+
- name: Build and push
49+
uses: docker/build-push-action@v6
50+
with:
51+
context: .
52+
file: ${{ matrix.dockerfile }}
53+
push: true
54+
tags: ${{ steps.meta.outputs.tags }}
55+
labels: ${{ steps.meta.outputs.labels }}
56+
cache-from: type=gha
57+
cache-to: type=gha,mode=max
58+
build-args: |
59+
NEXT_PUBLIC_API_URL=${{ vars.NEXT_PUBLIC_API_URL }}
60+
NEXT_PUBLIC_WEBSOCKET_URL=${{ vars.NEXT_PUBLIC_WEBSOCKET_URL }}
61+
NEXT_PUBLIC_CDN_URL=${{ vars.NEXT_PUBLIC_CDN_URL }}
62+
NEXT_PUBLIC_SITE_URL=${{ vars.NEXT_PUBLIC_SITE_URL }}
63+
64+
deploy:
65+
needs: build-and-push
66+
runs-on: ubuntu-latest
67+
if: github.ref == 'refs/heads/master'
68+
69+
steps:
70+
- name: Deploy to server via SSH
71+
uses: appleboy/ssh-action@v1
72+
with:
73+
host: ${{ secrets.DEPLOY_HOST }}
74+
username: ${{ secrets.DEPLOY_USER }}
75+
key: ${{ secrets.DEPLOY_SSH_KEY }}
76+
script: |
77+
cd ${{ secrets.DEPLOY_PATH }}
78+
docker compose -f docker-compose.prod.yml pull api web
79+
docker compose -f docker-compose.prod.yml up -d api web
80+
docker image prune -f

apps/api/Dockerfile

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
FROM node:22-alpine
1+
# ── Stage 1: 构建 ────────────────────────────────────────────
2+
FROM node:22-alpine AS builder
23

34
RUN npm install -g pnpm@9
45

@@ -24,7 +25,33 @@ RUN pnpm --filter @docStudio/api exec prisma generate
2425
# 编译 NestJS
2526
RUN pnpm build:api
2627

28+
# 仅安装生产依赖
29+
RUN pnpm install --frozen-lockfile --prod
30+
31+
# ── Stage 2: 生产镜像(仅包含运行时所需文件)─────────────────
32+
FROM node:22-alpine AS runner
33+
34+
WORKDIR /app
35+
ENV NODE_ENV=production
36+
37+
RUN addgroup --system --gid 1001 nestjs && \
38+
adduser --system --uid 1001 nestapi
39+
40+
# 复制编译产物
41+
COPY --from=builder --chown=nestapi:nestjs /app/apps/api/dist ./apps/api/dist
42+
COPY --from=builder --chown=nestapi:nestjs /app/apps/api/package.json ./apps/api/
43+
44+
# 复制 Prisma schema 和迁移(migrate deploy 需要)
45+
COPY --from=builder --chown=nestapi:nestjs /app/apps/api/prisma ./apps/api/prisma
46+
47+
# 复制生产依赖和 Prisma Client
48+
COPY --from=builder --chown=nestapi:nestjs /app/node_modules ./node_modules
49+
COPY --from=builder --chown=nestapi:nestjs /app/packages ./packages
50+
COPY --from=builder --chown=nestapi:nestjs /app/package.json ./
51+
52+
USER nestapi
53+
2754
EXPOSE 3001 1234
2855

2956
# 启动前自动执行数据库迁移
30-
CMD ["sh", "-c", "pnpm --filter @docStudio/api exec prisma migrate deploy && node apps/api/dist/main"]
57+
CMD ["sh", "-c", "npx prisma migrate deploy --schema=apps/api/prisma/schema.prisma && node apps/api/dist/main"]

apps/web/src/app/(main)/dashboard/page.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export default function DashboardPage() {
8989
<div className="space-y-6">
9090

9191
{/* ── Header ── */}
92-
<div className="flex items-end justify-between">
92+
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-3">
9393
<div>
9494
<p className="text-sm text-gray-500 dark:text-gray-400">{greeting}</p>
9595
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white mt-1 tracking-tight">
@@ -99,13 +99,13 @@ export default function DashboardPage() {
9999
<div className="flex items-center gap-2">
100100
<button
101101
onClick={() => router.push('/spaces')}
102-
className="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
102+
className="inline-flex items-center gap-1.5 px-3 sm:px-4 py-2 text-sm font-medium text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
103103
>
104104
管理空间
105105
</button>
106106
<button
107107
onClick={() => router.push('/spaces')}
108-
className="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors cursor-pointer"
108+
className="inline-flex items-center gap-1.5 px-3 sm:px-4 py-2 text-sm font-medium bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors cursor-pointer"
109109
>
110110
<Plus className="w-4 h-4" />
111111
新建空间
@@ -115,9 +115,9 @@ export default function DashboardPage() {
115115

116116
{/* ── KPI Stats Strip (VisActor style: border-separated, trend badges) ── */}
117117
<div className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
118-
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 divide-x divide-gray-200 dark:divide-gray-700">
118+
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6">
119119
{statItems.map((s) => (
120-
<div key={s.label} className="px-5 py-5 first:rounded-l-xl last:rounded-r-xl">
120+
<div key={s.label} className="px-5 py-5 border-b border-r border-gray-200 dark:border-gray-700 last:border-r-0 [&:nth-child(2n)]:border-r-0 sm:[&:nth-child(2n)]:border-r sm:[&:nth-child(3n)]:border-r-0 lg:[&:nth-child(3n)]:border-r lg:[&:nth-child(6n)]:border-r-0 lg:border-b-0">
121121
<p className="text-xs text-gray-500 dark:text-gray-400 font-medium mb-2">{s.label}</p>
122122
<div className="flex items-center gap-2">
123123
<span className="text-2xl font-semibold text-gray-900 dark:text-white tabular-nums">

apps/web/src/app/(main)/spaces/[id]/analytics/page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export default function SpaceAnalyticsPage() {
165165
<FadeIn delay={0.2} y={20}>
166166
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
167167
{/* Doc Growth Trend */}
168-
<div className="lg:col-span-2 rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-7 py-6">
168+
<div className="lg:col-span-2 rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-4 py-4 sm:px-7 sm:py-6">
169169
<p className="text-xs text-gray-400 dark:text-gray-500 font-medium tracking-wide uppercase mb-4">
170170
文档增长趋势(近 30 天)
171171
</p>
@@ -186,7 +186,7 @@ export default function SpaceAnalyticsPage() {
186186
</div>
187187

188188
{/* Action Distribution */}
189-
<div className="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-7 py-6">
189+
<div className="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-4 py-4 sm:px-7 sm:py-6">
190190
<p className="text-xs text-gray-400 dark:text-gray-500 font-medium tracking-wide uppercase mb-4">
191191
操作分布
192192
</p>
@@ -219,7 +219,7 @@ export default function SpaceAnalyticsPage() {
219219
<FadeIn delay={0.3} y={20}>
220220
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
221221
{/* Top Documents */}
222-
<div className="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-7 py-6">
222+
<div className="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-4 py-4 sm:px-7 sm:py-6">
223223
<p className="text-xs text-gray-400 dark:text-gray-500 font-medium tracking-wide uppercase mb-4">
224224
热门文档 Top 10
225225
</p>
@@ -255,7 +255,7 @@ export default function SpaceAnalyticsPage() {
255255
</div>
256256

257257
{/* Top Members */}
258-
<div className="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-7 py-6">
258+
<div className="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-4 py-4 sm:px-7 sm:py-6">
259259
<p className="text-xs text-gray-400 dark:text-gray-500 font-medium tracking-wide uppercase mb-4">
260260
活跃成员 Top 10(近 30 天)
261261
</p>

apps/web/src/app/(main)/spaces/[id]/documents/[documentId]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ export default function DocumentPage() {
477477
导出 PDF
478478
</DropdownMenuItem>
479479
<DropdownMenuSeparator />
480-
<DropdownMenuItem onClick={() => setShowShortcuts(true)}>
480+
<DropdownMenuItem onClick={() => setShowShortcuts(true)} className="hidden md:flex">
481481
<Keyboard className="h-4 w-4" />
482482
快捷键
483483
</DropdownMenuItem>

apps/web/src/app/(main)/spaces/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export default function SpacesPage() {
6969
if (loading && !spaces.length) {
7070
return (
7171
<div className="space-y-8 animate-pulse">
72-
<div className="rounded-3xl bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-800 dark:to-gray-800 px-12 py-14">
72+
<div className="rounded-3xl bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-800 dark:to-gray-800 px-6 sm:px-12 py-14">
7373
<div className="h-9 w-64 bg-white/60 dark:bg-gray-700 rounded-lg mb-3" />
7474
<div className="h-5 w-96 bg-white/40 dark:bg-gray-700 rounded" />
7575
</div>
@@ -86,7 +86,7 @@ export default function SpacesPage() {
8686
<div className="space-y-8">
8787
{/* ═══════ Hero ═══════ */}
8888
<FadeIn delay={0} y={30}>
89-
<div className="relative overflow-hidden rounded-3xl bg-gradient-to-br from-blue-50 via-indigo-50 to-violet-50 dark:from-gray-800 dark:via-gray-800 dark:to-gray-800 border border-blue-100/80 dark:border-gray-700 px-12 py-12">
89+
<div className="relative overflow-hidden rounded-3xl bg-gradient-to-br from-blue-50 via-indigo-50 to-violet-50 dark:from-gray-800 dark:via-gray-800 dark:to-gray-800 border border-blue-100/80 dark:border-gray-700 px-6 sm:px-12 py-12">
9090
<div className="absolute -top-20 -right-20 w-72 h-72 bg-blue-200/30 dark:bg-blue-500/5 rounded-full blur-3xl" />
9191
<div className="absolute -bottom-16 left-1/4 w-80 h-36 bg-violet-200/20 dark:bg-violet-500/5 rounded-full blur-3xl" />
9292

apps/web/src/app/admin/users/page.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -383,8 +383,8 @@ export default function AdminUsersPage() {
383383

384384
{/* 搜索 + 筛选 */}
385385
<FadeIn delay={0.1} y={16} duration={0.4}>
386-
<div className="flex items-center gap-3 mb-4 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-3 py-2.5">
387-
<div className="relative flex-1 max-w-sm">
386+
<div className="flex flex-col sm:flex-row sm:items-center gap-3 mb-4 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-3 py-2.5">
387+
<div className="relative flex-1 sm:max-w-sm">
388388
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
389389
<input
390390
type="text"
@@ -394,7 +394,7 @@ export default function AdminUsersPage() {
394394
className="w-full pl-9 pr-4 py-1.5 border border-gray-300 dark:border-gray-600 rounded-md text-sm bg-gray-50 dark:bg-gray-700 focus:bg-white dark:focus:bg-gray-800 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-colors"
395395
/>
396396
</div>
397-
<div className="h-6 w-px bg-gray-200 dark:bg-gray-700" />
397+
<div className="hidden sm:block h-6 w-px bg-gray-200 dark:bg-gray-700" />
398398
<div className="relative inline-flex items-center">
399399
<select
400400
value={selectedSpaceId}
@@ -417,7 +417,7 @@ export default function AdminUsersPage() {
417417
<FadeIn delay={0.2} y={16} duration={0.4}>
418418
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden flex-1 min-h-0">
419419
<div className="overflow-x-auto">
420-
<table className="w-full text-sm">
420+
<table className="w-full text-sm min-w-[640px]">
421421
<thead>
422422
<tr className="border-b border-gray-100 dark:border-gray-700">
423423
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400">用户</th>
@@ -488,7 +488,7 @@ export default function AdminUsersPage() {
488488

489489
{/* 分页 */}
490490
<FadeIn delay={0.3} y={12} duration={0.4}>
491-
<div className="flex items-center justify-between mt-4 pt-2 text-sm text-gray-500 dark:text-gray-400 shrink-0">
491+
<div className="flex flex-wrap items-center justify-between gap-3 mt-4 pt-2 text-sm text-gray-500 dark:text-gray-400 shrink-0">
492492
<div className="flex items-center gap-3">
493493
<label className="whitespace-nowrap">每页</label>
494494
<select

apps/web/src/app/contact/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export default function ContactPage() {
186186

187187
<PublicHeader />
188188

189-
<div className="relative pt-20 pb-20 px-4 flex flex-col items-center">
189+
<div className="relative min-h-screen px-4 pt-10 flex flex-col items-center justify-center">
190190
<motion.div
191191
variants={container}
192192
initial="hidden"

apps/web/src/app/explore/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export default function ExplorePage() {
161161

162162
<div className="flex items-center gap-3">
163163
{/* Sort Selector */}
164-
<div className="flex items-center gap-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl px-1 py-1 shadow-sm">
164+
<div className="flex items-center gap-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl px-1 py-1 shadow-sm overflow-x-auto max-w-[calc(100vw-6rem)]">
165165
<ArrowUpDown className="w-3.5 h-3.5 text-gray-400 ml-2" />
166166
{SORT_OPTIONS.map((option) => (
167167
<button

apps/web/src/app/page.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import Link from 'next/link';
44
import Image from 'next/image';
5+
import { useEffect } from 'react';
56
import { ArrowRight, Globe, Lock, Zap, Sparkles, Layout, Users, Database, BrainCircuit, Link2, History, FileInput, BarChart3, WifiOff, MessageSquareText, Search, Github } from 'lucide-react';
67
import { motion } from 'framer-motion';
78
import { PublicHeader } from '@/components/layout/public-header';
@@ -127,6 +128,13 @@ export default function Home() {
127128
const { theme } = useTheme();
128129
const isDark = theme === 'dark';
129130

131+
useEffect(() => {
132+
document.documentElement.style.overscrollBehavior = 'none';
133+
return () => {
134+
document.documentElement.style.overscrollBehavior = '';
135+
};
136+
}, []);
137+
130138
return (
131139
<div className="min-h-screen bg-white dark:bg-gray-900 selection:bg-blue-100 selection:text-blue-900">
132140
{/* 导航栏 */}
@@ -650,7 +658,7 @@ export default function Home() {
650658
</section>
651659

652660
{/* Community Reviews Section */}
653-
<section className="py-24 bg-white dark:bg-zinc-900 relative overflow-hidden border-t border-slate-100 dark:border-zinc-800/50">
661+
<section className="py-24 bg-white dark:bg-gray-950 relative overflow-hidden border-t border-slate-100 dark:border-zinc-800/50">
654662
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative mb-16">
655663
<motion.div
656664
initial={{ opacity: 0, y: 20 }}
@@ -677,8 +685,8 @@ export default function Home() {
677685
</Marquee>
678686

679687
{/* Gradient Overlays */}
680-
<div className="pointer-events-none absolute inset-y-0 left-0 w-1/4 bg-gradient-to-r from-white dark:from-zinc-900"></div>
681-
<div className="pointer-events-none absolute inset-y-0 right-0 w-1/4 bg-gradient-to-l from-white dark:from-zinc-900"></div>
688+
<div className="pointer-events-none absolute inset-y-0 left-0 w-1/4 bg-gradient-to-r from-white dark:from-gray-950"></div>
689+
<div className="pointer-events-none absolute inset-y-0 right-0 w-1/4 bg-gradient-to-l from-white dark:from-gray-950"></div>
682690
</div>
683691
</section>
684692
{/* ==================== Bottom CTA ==================== */}

0 commit comments

Comments
 (0)