美化终端脚本 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>
This commit is contained in:
Cheng Zhou
2026-06-23 15:24:22 +08:00
parent c351ca59c4
commit efc325568d
10 changed files with 1101 additions and 310 deletions
Executable
+252
View File
@@ -0,0 +1,252 @@
#!/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 "$@"