feat: 个人主页 + 部署配置(www.mikivl.online)
- vite.config.ts: 加 base: '/app/',App 部署在子路径 - server/index.ts: MODELS_FILE 支持环境变量覆盖(容器化写权限) - homepage/index.html: 极简开发者风格个人主页(About/Projects/Skills/Contact) - nginx/default.conf: 反向代理,SSE proxy_buffering off,SPA fallback - docker-compose.yml: Nginx + Hono 容器编排,models_data volume 持久化 - deploy.sh: 一键本地构建 + rsync 上传 + 远端重启 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9c534a920d
commit
5e01c8df4a
51
deploy.sh
Executable file
51
deploy.sh
Executable file
@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy.sh — 本地运行,构建并上传到 VPS
|
||||
set -euo pipefail
|
||||
|
||||
VPS_USER="root"
|
||||
VPS_HOST="your.vps.ip" # ← 替换为你的 VPS IP 或域名
|
||||
VPS_DIR="/opt/mikivl"
|
||||
|
||||
info() { printf "\033[36m[INFO]\033[0m %s\n" "$*"; }
|
||||
ok() { printf "\033[32m[OK]\033[0m %s\n" "$*"; }
|
||||
die() { printf "\033[31m[ERROR]\033[0m %s\n" "$*" >&2; exit 1; }
|
||||
|
||||
# ── Step 1: 本地构建 ──────────────────────────────────────────────────────────
|
||||
info "Building Vite app (base=/app/) ..."
|
||||
npm run build
|
||||
ok "Build complete → dist/"
|
||||
|
||||
# ── Step 2: 上传文件 ──────────────────────────────────────────────────────────
|
||||
info "Uploading to ${VPS_USER}@${VPS_HOST}:${VPS_DIR} ..."
|
||||
|
||||
ssh "${VPS_USER}@${VPS_HOST}" "mkdir -p ${VPS_DIR}/{homepage,app,nginx,program1/server,program1/node_modules}"
|
||||
|
||||
# 个人主页
|
||||
scp homepage/index.html "${VPS_USER}@${VPS_HOST}:${VPS_DIR}/homepage/"
|
||||
|
||||
# App 构建产物
|
||||
rsync -az --delete dist/ "${VPS_USER}@${VPS_HOST}:${VPS_DIR}/app/"
|
||||
|
||||
# 后端源码
|
||||
rsync -az server/ "${VPS_USER}@${VPS_HOST}:${VPS_DIR}/program1/server/"
|
||||
scp package.json package-lock.json "${VPS_USER}@${VPS_HOST}:${VPS_DIR}/program1/"
|
||||
|
||||
# Nginx 配置 + Docker Compose
|
||||
scp nginx/default.conf "${VPS_USER}@${VPS_HOST}:${VPS_DIR}/nginx/"
|
||||
scp docker-compose.yml "${VPS_USER}@${VPS_HOST}:${VPS_DIR}/"
|
||||
|
||||
ok "Upload complete."
|
||||
|
||||
# ── Step 3: VPS 上安装依赖并重启服务 ─────────────────────────────────────────
|
||||
info "Restarting services on VPS ..."
|
||||
|
||||
ssh "${VPS_USER}@${VPS_HOST}" bash <<'REMOTE'
|
||||
set -euo pipefail
|
||||
cd /opt/mikivl/program1
|
||||
npm ci --omit=dev
|
||||
cd /opt/mikivl
|
||||
docker compose up -d --remove-orphans
|
||||
docker compose ps
|
||||
REMOTE
|
||||
|
||||
ok "Deployment complete! → https://www.mikivl.online"
|
||||
38
docker-compose.yml
Normal file
38
docker-compose.yml
Normal file
@ -0,0 +1,38 @@
|
||||
services:
|
||||
hono-server:
|
||||
image: node:22-alpine
|
||||
working_dir: /app
|
||||
command: npx tsx server/index.ts
|
||||
environment:
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- MODELS_FILE=/app/models_data/models.json
|
||||
- NODE_ENV=production
|
||||
volumes:
|
||||
- ./program1:/app:ro
|
||||
- models_data:/app/models_data
|
||||
expose:
|
||||
- "3001"
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://localhost:3001/api/models"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
nginx:
|
||||
image: nginx:1.27-alpine
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./homepage:/usr/share/nginx/html/homepage:ro
|
||||
- ./app:/usr/share/nginx/html/app:ro
|
||||
- /etc/letsencrypt:/etc/letsencrypt:ro
|
||||
depends_on:
|
||||
hono-server:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
models_data:
|
||||
357
homepage/index.html
Normal file
357
homepage/index.html
Normal file
@ -0,0 +1,357 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="mikivl — 独立开发者,专注构建 AI 驱动的生产力工具" />
|
||||
<title>mikivl — 开发项目</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #0d0d11;
|
||||
--bg-card: #13131a;
|
||||
--bg-hover: #1a1a24;
|
||||
--border: #22222e;
|
||||
--text: #e2e2ec;
|
||||
--muted: #7070a0;
|
||||
--faint: #3a3a55;
|
||||
--accent: #818cf8;
|
||||
--accent2: #34d399;
|
||||
--tag-bg: #1e1b4b;
|
||||
--tag-text: #a5b4fc;
|
||||
}
|
||||
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: 'SF Mono', 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Nav ── */
|
||||
nav {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 clamp(1.5rem, 5vw, 4rem);
|
||||
height: 56px;
|
||||
background: rgba(13,13,17,0.88);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.nav-logo {
|
||||
font-size: 1rem; font-weight: 700; color: var(--accent);
|
||||
text-decoration: none; letter-spacing: -0.02em;
|
||||
}
|
||||
.nav-logo span { color: var(--muted); font-weight: 400; }
|
||||
.nav-links { display: flex; gap: 2rem; list-style: none; }
|
||||
.nav-links a {
|
||||
color: var(--muted); text-decoration: none; font-size: 0.8rem;
|
||||
letter-spacing: 0.06em; text-transform: uppercase;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.nav-links a:hover { color: var(--text); }
|
||||
|
||||
/* ── Layout ── */
|
||||
main { max-width: 860px; margin: 0 auto; padding: 0 clamp(1.5rem, 5vw, 2rem); }
|
||||
section { padding: 6rem 0 2rem; }
|
||||
section:first-of-type { padding-top: 9rem; }
|
||||
|
||||
/* ── Section label ── */
|
||||
.section-label {
|
||||
font-size: 0.72rem; letter-spacing: 0.15em; text-transform: uppercase;
|
||||
color: var(--accent); margin-bottom: 0.5rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: clamp(1.4rem, 3vw, 1.9rem); font-weight: 700;
|
||||
color: var(--text); margin-bottom: 1.75rem; letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
/* ── Hero ── */
|
||||
.hero-greeting {
|
||||
font-size: 0.85rem; color: var(--accent2); margin-bottom: 0.75rem;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.hero-name {
|
||||
font-size: clamp(2.2rem, 7vw, 3.6rem); font-weight: 800;
|
||||
color: var(--text); letter-spacing: -0.04em; line-height: 1.05;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.hero-name em { color: var(--accent); font-style: normal; }
|
||||
.hero-desc {
|
||||
font-size: 0.95rem; color: var(--muted); max-width: 500px;
|
||||
line-height: 1.85; margin-bottom: 2.25rem;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
.hero-actions { display: flex; gap: 0.75rem; flex-wrap: wrap; }
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; gap: 0.45rem;
|
||||
padding: 0.55rem 1.1rem;
|
||||
border-radius: 6px; font-size: 0.82rem;
|
||||
text-decoration: none; transition: all 0.15s;
|
||||
font-family: inherit;
|
||||
}
|
||||
.btn-primary {
|
||||
background: var(--accent); color: #0d0d11; border: 1px solid var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn-primary:hover { background: #a5b4fc; border-color: #a5b4fc; }
|
||||
.btn-ghost {
|
||||
background: transparent; color: var(--muted); border: 1px solid var(--border);
|
||||
}
|
||||
.btn-ghost:hover { border-color: var(--faint); color: var(--text); background: var(--bg-hover); }
|
||||
|
||||
/* ── Divider ── */
|
||||
.divider { border: none; border-top: 1px solid var(--border); margin: 0; }
|
||||
|
||||
/* ── Projects ── */
|
||||
.projects-grid { display: grid; gap: 1rem; }
|
||||
.project-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 1.5rem 1.6rem;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
transition: border-color 0.15s, background 0.15s, transform 0.18s;
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.project-card::before {
|
||||
content: '';
|
||||
position: absolute; top: 0; left: 0; right: 0; height: 2px;
|
||||
background: linear-gradient(90deg, var(--accent), var(--accent2));
|
||||
opacity: 0; transition: opacity 0.2s;
|
||||
}
|
||||
.project-card:hover {
|
||||
border-color: var(--faint);
|
||||
background: var(--bg-hover);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.project-card:hover::before { opacity: 1; }
|
||||
.project-header {
|
||||
display: flex; align-items: flex-start;
|
||||
justify-content: space-between; gap: 1rem; margin-bottom: 0.5rem;
|
||||
}
|
||||
.project-name {
|
||||
font-size: 1rem; font-weight: 700; color: var(--text);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.project-status {
|
||||
font-size: 0.7rem; padding: 0.15rem 0.5rem;
|
||||
border-radius: 20px; letter-spacing: 0.05em;
|
||||
background: #052e16; color: #6ee7b7;
|
||||
white-space: nowrap; margin-top: 2px;
|
||||
}
|
||||
.project-desc {
|
||||
font-size: 0.85rem; color: var(--muted); line-height: 1.75;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
margin-bottom: 1.1rem;
|
||||
}
|
||||
.project-tags { display: flex; flex-wrap: wrap; gap: 0.4rem; }
|
||||
.tag {
|
||||
font-size: 0.7rem; padding: 0.18rem 0.5rem;
|
||||
background: var(--tag-bg); color: var(--tag-text);
|
||||
border-radius: 4px; letter-spacing: 0.03em;
|
||||
}
|
||||
.tag.green { background: #052e16; color: #6ee7b7; }
|
||||
.tag.purple { background: #2e1065; color: #c4b5fd; }
|
||||
.tag.orange { background: #431407; color: #fdba74; }
|
||||
.tag.sky { background: #082f49; color: #7dd3fc; }
|
||||
|
||||
/* ── Skills ── */
|
||||
.skills-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.skill-group {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1.1rem 1.2rem;
|
||||
}
|
||||
.skill-group-title {
|
||||
font-size: 0.7rem; letter-spacing: 0.12em; text-transform: uppercase;
|
||||
color: var(--accent); margin-bottom: 0.8rem;
|
||||
}
|
||||
.skill-list { list-style: none; }
|
||||
.skill-list li {
|
||||
font-size: 0.83rem; color: var(--muted); padding: 0.22rem 0;
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
}
|
||||
.skill-list li::before { content: '▸'; color: var(--faint); font-size: 0.65rem; }
|
||||
|
||||
/* ── Contact ── */
|
||||
.contact-links { display: flex; flex-direction: column; gap: 0.9rem; }
|
||||
.contact-link {
|
||||
display: inline-flex; align-items: center; gap: 0.75rem;
|
||||
color: var(--muted); text-decoration: none; font-size: 0.88rem;
|
||||
transition: color 0.15s; width: fit-content;
|
||||
}
|
||||
.contact-link:hover { color: var(--accent); }
|
||||
.contact-icon { color: var(--faint); font-size: 1rem; width: 1.2rem; text-align: center; }
|
||||
|
||||
/* ── Footer ── */
|
||||
footer {
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 2rem clamp(1.5rem, 5vw, 4rem);
|
||||
text-align: center;
|
||||
color: var(--faint); font-size: 0.75rem;
|
||||
margin-top: 4rem;
|
||||
}
|
||||
|
||||
/* ── Cursor blink ── */
|
||||
.cursor {
|
||||
display: inline-block; width: 3px; height: 0.85em;
|
||||
background: var(--accent); margin-left: 1px;
|
||||
vertical-align: text-bottom; border-radius: 1px;
|
||||
animation: blink 1.1s step-end infinite;
|
||||
}
|
||||
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media (max-width: 600px) {
|
||||
.nav-links { gap: 1.2rem; }
|
||||
.skills-grid { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
@media (max-width: 400px) {
|
||||
.nav-links li:nth-child(3), .nav-links li:nth-child(4) { display: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav>
|
||||
<a class="nav-logo" href="#about">mikivl<span>.online</span></a>
|
||||
<ul class="nav-links">
|
||||
<li><a href="#about">about</a></li>
|
||||
<li><a href="#projects">projects</a></li>
|
||||
<li><a href="#skills">skills</a></li>
|
||||
<li><a href="#contact">contact</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
|
||||
<!-- About -->
|
||||
<section id="about">
|
||||
<p class="hero-greeting">// hello, world</p>
|
||||
<h1 class="hero-name">mikivl<em>.</em><span class="cursor"></span></h1>
|
||||
<p class="hero-desc">
|
||||
独立开发者,专注于构建 AI 驱动的生产力工具。<br>
|
||||
喜欢探索 AI 与实用工具的交叉地带,用代码解决真实问题。
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a class="btn btn-primary" href="#projects">查看项目 →</a>
|
||||
<a class="btn btn-ghost" href="#contact">联系我</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider" />
|
||||
|
||||
<!-- Projects -->
|
||||
<section id="projects">
|
||||
<p class="section-label">// projects</p>
|
||||
<h2>开发项目</h2>
|
||||
<div class="projects-grid">
|
||||
|
||||
<a class="project-card" href="/app/" target="_blank" rel="noopener">
|
||||
<div class="project-header">
|
||||
<span class="project-name">读书笔记 App</span>
|
||||
<span class="project-status">● 上线中</span>
|
||||
</div>
|
||||
<p class="project-desc">
|
||||
AI 驱动的个人读书笔记工具。富文本编辑器,支持 AI 续写、润色、摘要、翻译,
|
||||
多文件夹管理,标签系统,导入导出(MD / DOCX / PDF),数据本地存储,隐私优先。
|
||||
</p>
|
||||
<div class="project-tags">
|
||||
<span class="tag">React 19</span>
|
||||
<span class="tag">TypeScript</span>
|
||||
<span class="tag sky">Vite</span>
|
||||
<span class="tag green">Hono</span>
|
||||
<span class="tag purple">Claude API</span>
|
||||
<span class="tag orange">TipTap</span>
|
||||
<span class="tag">IndexedDB</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider" />
|
||||
|
||||
<!-- Skills -->
|
||||
<section id="skills">
|
||||
<p class="section-label">// skills</p>
|
||||
<h2>技术栈</h2>
|
||||
<div class="skills-grid">
|
||||
<div class="skill-group">
|
||||
<p class="skill-group-title">Frontend</p>
|
||||
<ul class="skill-list">
|
||||
<li>React / TypeScript</li>
|
||||
<li>Vite / TailwindCSS</li>
|
||||
<li>TipTap / ProseMirror</li>
|
||||
<li>Framer Motion</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="skill-group">
|
||||
<p class="skill-group-title">Backend</p>
|
||||
<ul class="skill-list">
|
||||
<li>Node.js / Hono</li>
|
||||
<li>REST / SSE</li>
|
||||
<li>Docker / Nginx</li>
|
||||
<li>Linux / VPS</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="skill-group">
|
||||
<p class="skill-group-title">AI</p>
|
||||
<ul class="skill-list">
|
||||
<li>Anthropic Claude API</li>
|
||||
<li>Prompt Engineering</li>
|
||||
<li>Streaming (SSE)</li>
|
||||
<li>RAG / Embeddings</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="skill-group">
|
||||
<p class="skill-group-title">Tooling</p>
|
||||
<ul class="skill-list">
|
||||
<li>Git / GitHub</li>
|
||||
<li>Vitest / ESLint</li>
|
||||
<li>Claude Code</li>
|
||||
<li>SSH / rsync</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider" />
|
||||
|
||||
<!-- Contact -->
|
||||
<section id="contact">
|
||||
<p class="section-label">// contact</p>
|
||||
<h2>联系方式</h2>
|
||||
<div class="contact-links">
|
||||
<a class="contact-link" href="https://github.com/MikiVL" target="_blank" rel="noopener">
|
||||
<span class="contact-icon">⌥</span>
|
||||
github.com/MikiVL
|
||||
</a>
|
||||
<a class="contact-link" href="mailto:hi@mikivl.online">
|
||||
<span class="contact-icon">@</span>
|
||||
hi@mikivl.online
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© 2026 mikivl · built with ♥ and <a href="https://claude.ai/code" style="color:var(--faint);text-decoration:none;" target="_blank">Claude Code</a></p>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
55
nginx/default.conf
Normal file
55
nginx/default.conf
Normal file
@ -0,0 +1,55 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name www.mikivl.online mikivl.online;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name www.mikivl.online mikivl.online;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/www.mikivl.online/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/www.mikivl.online/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
|
||||
add_header X-Frame-Options SAMEORIGIN;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header Referrer-Policy strict-origin-when-cross-origin;
|
||||
|
||||
# ── 后端 API(反向代理到 Hono,/api/ 优先匹配)──
|
||||
location /api/ {
|
||||
proxy_pass http://hono-server:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# SSE 流式响应必须关闭缓冲
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 300s;
|
||||
chunked_transfer_encoding on;
|
||||
}
|
||||
|
||||
# ── 读书笔记 App(/app/ 子路径,SPA)──
|
||||
location /app/ {
|
||||
alias /usr/share/nginx/html/app/;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /app/index.html;
|
||||
}
|
||||
|
||||
# ── 个人主页(根路径)──
|
||||
location / {
|
||||
root /usr/share/nginx/html/homepage;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# 静态资源长缓存
|
||||
location ~* \.(js|css|svg|ico|woff2?|png|jpg|webp)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,7 @@ const app = new Hono()
|
||||
app.use('*', cors())
|
||||
|
||||
// ── Model config persistence ──────────────────────────────────────────────────
|
||||
const MODELS_FILE = path.resolve('models.json')
|
||||
const MODELS_FILE = process.env.MODELS_FILE ?? path.resolve('models.json')
|
||||
|
||||
type ModelConfig = {
|
||||
id: string
|
||||
|
||||
@ -169,7 +169,7 @@ function nodesToDocxParagraphs(nodes: TipTapNode[]): Paragraph[] {
|
||||
})
|
||||
case 'codeBlock': {
|
||||
const code = (node.content ?? []).map((n: TipTapNode) => n.text ?? '').join('')
|
||||
return code.split('\n').map(line => new Paragraph({
|
||||
return code.split('\n').map((line: string) => new Paragraph({
|
||||
children: [new TextRun({ text: line, font: 'Courier New' })],
|
||||
}))
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
export default defineConfig({
|
||||
base: '/app/',
|
||||
plugins: [react(), tailwindcss()],
|
||||
server: {
|
||||
proxy: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user