143 lines
4.3 KiB
Bash
Executable File
143 lines
4.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# tickflow-stock-panel — 一键启动前后端
|
|
#
|
|
# 用法:
|
|
# ./dev.sh # 默认 backend:3018 frontend:3011
|
|
# BACKEND_PORT=8000 ./dev.sh # 改后端端口
|
|
# FRONTEND_PORT=5173 ./dev.sh # 改前端端口
|
|
#
|
|
# Ctrl-C 同时关闭两端。
|
|
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
BACKEND_DIR="$ROOT/backend"
|
|
FRONTEND_DIR="$ROOT/frontend"
|
|
BACKEND_PORT="${BACKEND_PORT:-3018}"
|
|
FRONTEND_PORT="${FRONTEND_PORT:-3011}"
|
|
|
|
BLUE='\033[0;34m'
|
|
GREEN='\033[0;32m'
|
|
RED='\033[0;31m'
|
|
YELLOW='\033[0;33m'
|
|
GRAY='\033[0;90m'
|
|
NC='\033[0m'
|
|
|
|
info() { echo -e "${GRAY}[dev]${NC} $*"; }
|
|
ok() { echo -e "${GREEN}[dev]${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}[dev]${NC} $*"; }
|
|
err() { echo -e "${RED}[dev]${NC} $*" >&2; }
|
|
|
|
# ===== 1. 依赖检查 =====
|
|
require_cmd() {
|
|
local cmd="$1" hint="$2"
|
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
err "$cmd 未安装"
|
|
echo " 安装方式:$hint"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
require_cmd uv "curl -LsSf https://astral.sh/uv/install.sh | sh"
|
|
require_cmd pnpm "npm i -g pnpm 或 corepack enable && corepack prepare pnpm@9 --activate"
|
|
|
|
# ===== 2. 端口占用检查 —— 占用就直接 kill =====
|
|
free_port() {
|
|
local name="$1" port="$2"
|
|
local pids
|
|
pids=$(lsof -nP -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null || true)
|
|
if [ -z "$pids" ]; then
|
|
return 0
|
|
fi
|
|
warn "端口 $port($name)被占用,kill 现有进程 PID: $(echo "$pids" | xargs)"
|
|
# 先 TERM
|
|
echo "$pids" | xargs kill 2>/dev/null || true
|
|
sleep 1
|
|
# 还活着就 KILL
|
|
pids=$(lsof -nP -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null || true)
|
|
if [ -n "$pids" ]; then
|
|
warn "TERM 没杀掉,改用 KILL -9"
|
|
echo "$pids" | xargs kill -9 2>/dev/null || true
|
|
sleep 1
|
|
fi
|
|
# 再确认一次
|
|
pids=$(lsof -nP -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null || true)
|
|
if [ -n "$pids" ]; then
|
|
err "端口 $port 仍被占用 — kill 失败。请手动处理:lsof -i :$port"
|
|
exit 1
|
|
fi
|
|
ok "端口 $port 已释放"
|
|
}
|
|
free_port backend "$BACKEND_PORT"
|
|
free_port frontend "$FRONTEND_PORT"
|
|
|
|
# ===== 3. 首次依赖安装 =====
|
|
if [ ! -d "$BACKEND_DIR/.venv" ]; then
|
|
info "后端首次启动 — 安装 Python 依赖(约 1-2 分钟)..."
|
|
( cd "$BACKEND_DIR" && uv sync )
|
|
ok "后端依赖装好了"
|
|
fi
|
|
|
|
if [ ! -d "$FRONTEND_DIR/node_modules" ]; then
|
|
info "前端首次启动 — 安装 Node 依赖..."
|
|
( cd "$FRONTEND_DIR" && pnpm install )
|
|
ok "前端依赖装好了"
|
|
fi
|
|
|
|
# ===== 4. 启动 + 日志前缀 =====
|
|
PIDS=()
|
|
|
|
cleanup() {
|
|
echo
|
|
info "关闭服务..."
|
|
for pid in "${PIDS[@]:-}"; do
|
|
if [ -n "$pid" ]; then
|
|
kill "$pid" 2>/dev/null || true
|
|
fi
|
|
done
|
|
# 等子进程退出,避免孤儿
|
|
wait 2>/dev/null || true
|
|
ok "已退出"
|
|
exit 0
|
|
}
|
|
trap cleanup INT TERM
|
|
|
|
# 用 awk 加前缀(macOS sed 没有 -u line-buffered,改用 awk + fflush 兼容)
|
|
prefix_awk() {
|
|
awk -v p="$1" '{ print p $0; fflush() }'
|
|
}
|
|
|
|
echo
|
|
echo -e "${BLUE}╭──────────────────────────────────────────────╮${NC}"
|
|
echo -e "${BLUE}│${NC} ${GREEN}tickflow-stock-panel${NC} ${BLUE}│${NC}"
|
|
echo -e "${BLUE}│${NC} ${BLUE}│${NC}"
|
|
echo -e "${BLUE}│${NC} backend ${YELLOW}http://localhost:$BACKEND_PORT${NC} ${BLUE}│${NC}"
|
|
echo -e "${BLUE}│${NC} frontend ${YELLOW}http://localhost:$FRONTEND_PORT${NC} ${BLUE}│${NC}"
|
|
echo -e "${BLUE}│${NC} ${BLUE}│${NC}"
|
|
echo -e "${BLUE}│${NC} Ctrl-C 同时关闭两端 ${BLUE}│${NC}"
|
|
echo -e "${BLUE}╰──────────────────────────────────────────────╯${NC}"
|
|
echo
|
|
|
|
(
|
|
cd "$BACKEND_DIR"
|
|
uv run uvicorn app.main:app --reload --host 0.0.0.0 --port "$BACKEND_PORT" 2>&1 \
|
|
| prefix_awk "$(printf "${BLUE}[backend ]${NC} ")"
|
|
) &
|
|
PIDS+=("$!")
|
|
|
|
(
|
|
cd "$FRONTEND_DIR"
|
|
pnpm dev --host 0.0.0.0 --port "$FRONTEND_PORT" 2>&1 \
|
|
| prefix_awk "$(printf "${GREEN}[frontend]${NC} ")"
|
|
) &
|
|
PIDS+=("$!")
|
|
|
|
# 等任一退出(bash 4.3+)或全部退出(老 bash)
|
|
if wait -n 2>/dev/null; then
|
|
warn "其中一个进程退出,正在关闭另一个..."
|
|
cleanup
|
|
else
|
|
# 老 bash 没有 wait -n,退化为 wait 全部
|
|
wait
|
|
fi
|