Files
ctms/install.sh
T
Cheng Zhou efc325568d 美化终端脚本 UI 并修复对齐与闪屏
- 配色体系:256 色语义色板(primary/accent/success/warn/danger)与统一图标常量
- 修复右边框对齐:ctms_text_width 用 perl 精确计算显示宽度,剥离 sgr0 的 \e(B 序列
- 修复菜单闪屏:改用光标归位原地覆盖重绘,去掉每帧清屏
- 美化各面板:install.sh banner/确认/成功框、根菜单、status.sh 资源表格,标签列对齐
- 新增 ctms_pad_right 按显示宽度填充含中文标签

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 15:24:22 +08:00

253 lines
8.0 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/common.sh
source "$ROOT_DIR/scripts/common.sh"
SELECTED_INDEX=0
SELECTED_ENV=""
SELECTED_BASE_URL=""
ENV_STATUS_LINES=""
UI_BOX_WIDTH=76
usage() {
cat <<EOF
${C_BOLD}用法:${C_RESET}
./install.sh
${C_BOLD}说明:${C_RESET}
install.sh 不接受命令行参数。
执行后通过键盘 ↑/↓ 选择安装、更新、卸载、状态等操作,按 Q 退出。
EOF
}
require_interactive_terminal() {
[[ -t 0 && -t 1 ]] || ctms_fail "install.sh 需要在交互式终端中运行"
}
banner() {
printf '\n'
ctms_box_top "${C_BLUE}${C_BOLD}" "CTMS DEPLOYMENT CONSOLE" "$UI_BOX_WIDTH"
ctms_box_line "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH" "${ICON_BULLET} 安装 · 更新 · 卸载 · 资源状态" "${CC_INFO}${C_BOLD}${ICON_BULLET}${C_RESET} 安装 · 更新 · 卸载 · 资源状态"
ctms_box_line "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH" "↑/↓ 选择 Enter 确认 Q 退出" "${CC_MUTED}↑/↓ 选择 Enter 确认 Q 退出${C_RESET}"
ctms_box_bottom "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH"
printf '\n'
render_status_panel
}
render_status_panel() {
ctms_box_top "${C_MAGENTA}${C_BOLD}" "环境状态" "$UI_BOX_WIDTH"
printf '%b\n' "$ENV_STATUS_LINES" | while IFS= read -r line; do
ctms_box_line "${C_MAGENTA}${C_BOLD}" "$UI_BOX_WIDTH" "$line" "$line"
done
ctms_box_bottom "${C_MAGENTA}${C_BOLD}" "$UI_BOX_WIDTH"
printf '\n'
}
build_env_status() {
local output=""
local detected=0
local docker_error=""
if ! command -v docker >/dev/null 2>&1; then
ENV_STATUS_LINES=" ${C_YELLOW}Docker 未安装或 docker 命令不可用${C_RESET}"
return
fi
if ! docker ps >/dev/null 2>&1; then
docker_error="$(docker ps 2>&1 >/dev/null || true)"
ENV_STATUS_LINES=" ${C_YELLOW}Docker 未运行或当前用户无权限访问 Docker${C_RESET}"
[[ -n "$docker_error" ]] && ENV_STATUS_LINES="${ENV_STATUS_LINES}\n ${C_DIM}${docker_error}${C_RESET}"
return
fi
local env project rows total running stopped status_label
for env in dev main release; do
project="$(ctms_project_name "$env")"
rows="$(docker ps -a \
--filter "label=com.docker.compose.project=$project" \
--format '{{.Names}}|{{.State}}|{{.Status}}' 2>/dev/null || true)"
[[ -n "$rows" ]] || continue
detected=1
total="$(printf '%s\n' "$rows" | awk 'NF { count++ } END { print count + 0 }')"
running="$(printf '%s\n' "$rows" | awk -F'|' '$2 == "running" { count++ } END { print count + 0 }')"
stopped=$(( total - running ))
if [[ "$running" -eq "$total" && "$total" -gt 0 ]]; then
status_label="${CC_SUCCESS}${ICON_RUNNING} 运行中${C_RESET}"
elif [[ "$running" -gt 0 ]]; then
status_label="${CC_WARN}${ICON_RUNNING} 部分运行${C_RESET}"
else
status_label="${CC_DANGER}${ICON_STOPPED} 已停止${C_RESET}"
fi
output="${output} ${CC_INFO}${C_BOLD}${env}${C_RESET} ${CC_MUTED}(${project})${C_RESET} ${status_label} ${CC_MUTED}${running}/${total} 运行"
[[ "$stopped" -gt 0 ]] && output="${output}${stopped} 已停止"
output="${output}${C_RESET}\n"
output="${output}$(printf '%s\n' "$rows" | awk -F'|' '{ printf " %-18s %-8s %s\n", $1, $2, $3 }')\n"
done
if [[ "$detected" -eq 0 ]]; then
rows="$(docker ps -a \
--filter "name=ctms_" \
--format '{{.Names}}|{{.State}}|{{.Status}}' 2>/dev/null || true)"
if [[ -n "$rows" ]]; then
total="$(printf '%s\n' "$rows" | awk 'NF { count++ } END { print count + 0 }')"
running="$(printf '%s\n' "$rows" | awk -F'|' '$2 == "running" { count++ } END { print count + 0 }')"
ENV_STATUS_LINES=" ${C_YELLOW}检测到 CTMS 命名容器,但无法从 Compose 标签确认环境${C_RESET}\n"
ENV_STATUS_LINES="${ENV_STATUS_LINES} ${running}/${total} 运行\n"
ENV_STATUS_LINES="${ENV_STATUS_LINES}$(printf '%s\n' "$rows" | awk -F'|' '{ printf " - %-18s %-8s %s\n", $1, $2, $3 }')"
else
ENV_STATUS_LINES=" ${C_DIM}未检测到 CTMS Compose 容器(ctms_dev / ctms_main / ctms_release${C_RESET}"
fi
else
ENV_STATUS_LINES="${output%\\n}"
fi
}
select_menu() {
local title="$1"
shift
local options=("$@")
local selected=0
local key=""
[[ "${#options[@]}" -gt 0 ]] || ctms_fail "菜单没有可选项"
trap ctms_restore_cursor EXIT INT TERM
printf '\033[?25l\033[2J'
while true; do
printf '\033[H'
banner
ctms_box_top "${C_BLUE}${C_BOLD}" "$title" "$UI_BOX_WIDTH"
local i
for i in "${!options[@]}"; do
if [[ "$i" -eq "$selected" ]]; then
ctms_box_line "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH" "${ICON_BULLET} ${options[$i]}" "${CC_INFO}${C_BOLD}${ICON_BULLET} ${options[$i]}${C_RESET}"
else
ctms_box_line "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH" "${ICON_BULLET_OFF} ${options[$i]}" "${CC_MUTED}${ICON_BULLET_OFF} ${options[$i]}${C_RESET}"
fi
done
ctms_box_bottom "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH"
printf '\033[J'
key="$(ctms_read_key)"
case "$key" in
"[A") selected=$(( (selected - 1 + ${#options[@]}) % ${#options[@]} )) ;;
"[B") selected=$(( (selected + 1) % ${#options[@]} )) ;;
"") ctms_restore_cursor; SELECTED_INDEX="$selected"; return ;;
q|Q) ctms_restore_cursor; exit 0 ;;
esac
done
}
pick_env() {
select_menu "选择环境" \
"dev 本地开发环境 Vite 热加载 / 默认密钥" \
"main 内网测试环境 预发布验证 / 生产模式" \
"release 生产环境 HTTPS / RSA 密钥"
case "$SELECTED_INDEX" in
0) SELECTED_ENV="dev" ;;
1) SELECTED_ENV="main" ;;
2) SELECTED_ENV="release" ;;
esac
}
collect_install_options() {
local env="$1"
SELECTED_BASE_URL=""
if [[ "$env" == "release" ]]; then
SELECTED_BASE_URL="$(ctms_ask_line "请输入健康检查地址,例如 https://ctms.example.com")"
[[ -n "$SELECTED_BASE_URL" ]] || ctms_fail "release 环境必须提供健康检查地址"
elif [[ "$env" == "main" ]]; then
SELECTED_BASE_URL="$(ctms_ask_line "请输入健康检查地址" "http://127.0.0.1:8888")"
fi
}
run_script() {
local script="$1"
shift
[[ -x "$ROOT_DIR/scripts/$script" ]] || ctms_fail "脚本不可执行: $ROOT_DIR/scripts/$script"
"$ROOT_DIR/scripts/$script" "$@"
}
run_install_like_script() {
local script="$1"
local env="$2"
if [[ -n "$SELECTED_BASE_URL" ]]; then
run_script "$script" "$env" --base-url "$SELECTED_BASE_URL"
else
run_script "$script" "$env"
fi
}
# 操作完成后暂停,等用户确认再返回菜单
pause_return() {
printf '\n %s%s%s 按 Enter 返回主菜单…' "${CC_INFO}${C_BOLD}" "$ICON_PROMPT" "${C_RESET}"
read -r _ || true
}
interactive_main() {
select_menu "选择操作" \
"安装部署 配置环境、构建容器、迁移数据库、健康检查" \
"滚动更新 重建服务、执行迁移、复用安装健康检查" \
"安全卸载 停止并移除容器,保留 .env 与数据文件" \
"资源状态 查看 CPU、内存、网络与磁盘 I/O 快照" \
"使用帮助 查看入口和底层脚本说明" \
"退出控制台"
case "$SELECTED_INDEX" in
0)
pick_env
collect_install_options "$SELECTED_ENV"
run_install_like_script "install.sh" "$SELECTED_ENV"
pause_return
;;
1)
pick_env
collect_install_options "$SELECTED_ENV"
run_install_like_script "update.sh" "$SELECTED_ENV"
pause_return
;;
2)
pick_env
run_script "uninstall.sh" "$SELECTED_ENV"
pause_return
;;
3)
pick_env
run_script "status.sh" "$SELECTED_ENV"
pause_return
;;
4)
usage
pause_return
;;
5)
return 1
;;
esac
return 0
}
main() {
if [[ $# -gt 0 ]]; then
usage
ctms_fail "install.sh 不接受参数,请直接执行 ./install.sh"
fi
require_interactive_terminal
while true; do
build_env_status
interactive_main || break
done
exit 0
}
main "$@"