feat(api): standardize REST contracts and auth
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
# Keycloak 登录主题
|
||||
|
||||
`themes/tjwater` 是 TJWater 智慧水务平台的 Keycloak 登录主题。主题继承
|
||||
`keycloak.v2`,只覆盖样式、消息和本地 SVG 资源,不修改认证模板或认证流程。
|
||||
|
||||
## 在管理控制台切换登录主题
|
||||
|
||||
确认 `themes/tjwater` 已挂载到容器的
|
||||
`/opt/keycloak/themes/tjwater`,然后按以下步骤切换:
|
||||
|
||||
1. 打开 Keycloak 管理控制台:
|
||||
`http://<Keycloak 地址>:<端口>/admin/`。
|
||||
2. 使用管理员账号登录。
|
||||
3. 在左上角选择需要应用主题的 realm,例如 `tjwater`。不要停留在
|
||||
`master`,除非确实要修改 `master` realm。
|
||||
4. 在左侧菜单进入 `Realm settings`,打开 `Themes` 标签页。
|
||||
5. 在 `Login theme` 下拉框中选择 `tjwater`。
|
||||
6. 点击 `Save` 保存。
|
||||
7. 继续检查业务客户端是否单独指定了登录主题,再从业务前端重新进入登录页。
|
||||
|
||||
切回 Keycloak 默认登录页时,将 `Login theme` 改为 `keycloak` 并保存。
|
||||
|
||||
### 检查业务客户端的主题配置
|
||||
|
||||
Keycloak 的 realm 和 client 都可以设置登录主题。client 的配置优先于 realm。
|
||||
因此,即使 `Realm settings > Themes > Login theme` 已选择 `tjwater`,业务
|
||||
客户端如果仍指定 `keycloak`,从业务系统跳转后看到的还是默认登录页。
|
||||
|
||||
以授权地址中包含 `client_id=tjwater` 的业务系统为例:
|
||||
|
||||
1. 确认左上角当前 realm 是 `tjwater`。
|
||||
2. 在左侧菜单进入 `Clients`。
|
||||
3. 打开 `Client ID` 为 `tjwater` 的客户端。
|
||||
4. 在 `Settings` 页面找到 `Login settings > Login theme`。
|
||||
5. 将该字段设置为以下任一选项:
|
||||
- `Choose...`:不在 client 层指定主题,继承 realm 的 `tjwater` 主题,
|
||||
推荐使用此方式。
|
||||
- `tjwater`:在 client 层明确指定 `tjwater` 主题。
|
||||
6. 不要保留 `keycloak`,否则它会覆盖 realm 的主题。
|
||||
7. 点击 `Save`,关闭旧登录页,再从业务前端重新发起一次登录。
|
||||
|
||||
`Choose...` 不是未配置完成,而是表示当前 client 继承 realm 配置。管理控制台
|
||||
登录、账户中心和业务系统可能使用不同的 client。某一个入口已经显示
|
||||
`tjwater` 主题,并不能证明业务 client 也已正确配置。
|
||||
|
||||
验证时以业务系统实际生成的 OpenID Connect 授权地址为准,并检查其中的
|
||||
`client_id`。浏览器加载的主题资源路径应包含
|
||||
`/resources/<版本>/login/tjwater/`;如果路径仍包含
|
||||
`/resources/<版本>/login/keycloak/`,说明该 client 仍在使用默认主题。
|
||||
|
||||
如果 `Login theme` 下拉框中没有 `tjwater`,先检查容器内的主题文件:
|
||||
|
||||
```bash
|
||||
docker compose \
|
||||
--env-file .env \
|
||||
-f infra/docker/docker-compose.yml \
|
||||
exec -T keycloak \
|
||||
test -f /opt/keycloak/themes/tjwater/login/theme.properties
|
||||
```
|
||||
|
||||
命令成功但控制台仍未显示主题时,重新创建 Keycloak 容器后再检查:
|
||||
|
||||
```bash
|
||||
docker compose \
|
||||
--env-file .env \
|
||||
-f infra/docker/docker-compose.yml \
|
||||
up -d --force-recreate keycloak
|
||||
```
|
||||
|
||||
主题名称已经正确,但页面仍显示旧样式时,也执行上述命令,并在容器启动后使用
|
||||
`Ctrl+F5` 强制刷新登录页,避免继续使用浏览器缓存的 CSS。
|
||||
|
||||
## 启用
|
||||
|
||||
先启动 `infra/docker/docker-compose.yml` 中的 Keycloak,再从仓库根目录执行:
|
||||
|
||||
```bash
|
||||
bash infra/docker/keycloak/configure-theme.sh apply
|
||||
```
|
||||
|
||||
脚本默认配置 `tjwater` realm、简体中文默认语言、中英文切换和
|
||||
`TJWater 智慧水务平台` 品牌名,并清除 `tjwater` client 对登录主题的覆盖,
|
||||
使其继承 realm 主题。其他环境可临时覆盖:
|
||||
|
||||
```bash
|
||||
TJWATER_KEYCLOAK_REALM=example \
|
||||
TJWATER_KEYCLOAK_CLIENT_ID=example-web \
|
||||
TJWATER_KEYCLOAK_DISPLAY_NAME="示例智慧水务平台" \
|
||||
bash infra/docker/keycloak/configure-theme.sh apply
|
||||
```
|
||||
|
||||
管理员凭据继续使用 Compose 已注入的 `KC_BOOTSTRAP_ADMIN_USERNAME` /
|
||||
`KC_BOOTSTRAP_ADMIN_PASSWORD`,并兼容现有的 `KEYCLOAK_ADMIN` /
|
||||
`KEYCLOAK_ADMIN_PASSWORD`。
|
||||
|
||||
## 验证与回滚
|
||||
|
||||
```bash
|
||||
bash infra/docker/keycloak/configure-theme.sh verify
|
||||
bash infra/docker/keycloak/configure-theme.sh rollback
|
||||
```
|
||||
|
||||
使用 `latest` 镜像时,每次重新拉取 Keycloak 后都应重新执行 `verify`,并在
|
||||
1280px、375px 和 320px 视口检查登录、错误提示、忘记密码和 OTP 页面。
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
action="${1:-apply}"
|
||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd -- "${script_dir}/../../.." && pwd)"
|
||||
compose_file="${repo_root}/infra/docker/docker-compose.yml"
|
||||
realm="${TJWATER_KEYCLOAK_REALM:-tjwater}"
|
||||
client_id="${TJWATER_KEYCLOAK_CLIENT_ID:-tjwater}"
|
||||
display_name="${TJWATER_KEYCLOAK_DISPLAY_NAME:-TJWater 智慧水务平台}"
|
||||
|
||||
case "${action}" in
|
||||
apply|verify|rollback) ;;
|
||||
*)
|
||||
echo "用法: bash infra/docker/keycloak/configure-theme.sh [apply|verify|rollback]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
compose_args=(docker compose)
|
||||
if [[ -f "${repo_root}/.env" ]]; then
|
||||
compose_args+=(--env-file "${repo_root}/.env")
|
||||
fi
|
||||
compose_args+=(-f "${compose_file}")
|
||||
|
||||
"${compose_args[@]}" exec -T \
|
||||
-e TJWATER_KEYCLOAK_ACTION="${action}" \
|
||||
-e TJWATER_KEYCLOAK_REALM="${realm}" \
|
||||
-e TJWATER_KEYCLOAK_CLIENT_ID="${client_id}" \
|
||||
-e TJWATER_KEYCLOAK_DISPLAY_NAME="${display_name}" \
|
||||
keycloak sh -s <<'KEYCLOAK_SCRIPT'
|
||||
set -eu
|
||||
|
||||
action="${TJWATER_KEYCLOAK_ACTION}"
|
||||
realm="${TJWATER_KEYCLOAK_REALM}"
|
||||
client_id="${TJWATER_KEYCLOAK_CLIENT_ID}"
|
||||
display_name="${TJWATER_KEYCLOAK_DISPLAY_NAME}"
|
||||
server_url="${TJWATER_KEYCLOAK_SERVER_URL:-http://127.0.0.1:8080}"
|
||||
admin_user="${KC_BOOTSTRAP_ADMIN_USERNAME:-${KEYCLOAK_ADMIN:-}}"
|
||||
admin_password="${KC_BOOTSTRAP_ADMIN_PASSWORD:-${KEYCLOAK_ADMIN_PASSWORD:-}}"
|
||||
config_file="/tmp/tjwater-kcadm-$$.config"
|
||||
kcadm="/opt/keycloak/bin/kcadm.sh"
|
||||
|
||||
cleanup() {
|
||||
rm -f "${config_file}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [ -z "${admin_user}" ] || [ -z "${admin_password}" ]; then
|
||||
echo "缺少 Keycloak 管理员用户名或密码环境变量。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${action}" = "apply" ] && [ ! -f /opt/keycloak/themes/tjwater/login/theme.properties ]; then
|
||||
echo "未找到 tjwater 登录主题,请检查主题目录挂载。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"${kcadm}" config credentials \
|
||||
--config "${config_file}" \
|
||||
--server "${server_url}" \
|
||||
--realm master \
|
||||
--user "${admin_user}" \
|
||||
--password "${admin_password}" >/dev/null
|
||||
|
||||
client_uuid="$(
|
||||
"${kcadm}" get clients \
|
||||
--config "${config_file}" \
|
||||
--target-realm "${realm}" \
|
||||
--query "clientId=${client_id}" \
|
||||
--fields id \
|
||||
--format csv \
|
||||
--noquotes |
|
||||
sed -n '1p'
|
||||
)"
|
||||
|
||||
if [ -z "${client_uuid}" ]; then
|
||||
echo "realm ${realm} 中未找到 client ${client_id}。" >&2
|
||||
echo "可通过 TJWATER_KEYCLOAK_CLIENT_ID 指定实际的 client ID。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "${action}" in
|
||||
apply)
|
||||
"${kcadm}" update "realms/${realm}" \
|
||||
--config "${config_file}" \
|
||||
-s "displayName=${display_name}" \
|
||||
-s "displayNameHtml=${display_name}" \
|
||||
-s "loginTheme=tjwater" \
|
||||
-s "internationalizationEnabled=true" \
|
||||
-s 'supportedLocales=["zh-CN","en"]' \
|
||||
-s "defaultLocale=zh-CN" >/dev/null
|
||||
"${kcadm}" update "clients/${client_uuid}" \
|
||||
--config "${config_file}" \
|
||||
--target-realm "${realm}" \
|
||||
--set attributes.login_theme= >/dev/null
|
||||
echo "已为 realm ${realm} 启用 tjwater 登录主题。"
|
||||
echo "client ${client_id} 已改为继承 realm 登录主题。"
|
||||
;;
|
||||
rollback)
|
||||
"${kcadm}" update "realms/${realm}" \
|
||||
--config "${config_file}" \
|
||||
-s "loginTheme=keycloak" >/dev/null
|
||||
echo "已将 realm ${realm} 恢复为 Keycloak 默认登录主题。"
|
||||
;;
|
||||
esac
|
||||
|
||||
"${kcadm}" get "realms/${realm}" \
|
||||
--config "${config_file}" \
|
||||
--fields realm,displayName,loginTheme,internationalizationEnabled,supportedLocales,defaultLocale
|
||||
"${kcadm}" get "clients/${client_uuid}" \
|
||||
--config "${config_file}" \
|
||||
--target-realm "${realm}" \
|
||||
--fields 'clientId,attributes(login_theme)'
|
||||
KEYCLOAK_SCRIPT
|
||||
@@ -0,0 +1,3 @@
|
||||
loginAccountTitle=Account sign in
|
||||
doLogIn=Sign in
|
||||
doForgotPassword=Forgot password
|
||||
@@ -0,0 +1,9 @@
|
||||
loginAccountTitle=账号登录
|
||||
usernameOrEmail=用户名或邮箱
|
||||
doLogIn=登录
|
||||
doForgotPassword=忘记密码
|
||||
rememberMe=记住我
|
||||
invalidUserMessage=用户名或密码错误
|
||||
invalidUsernameOrPasswordMessage=用户名或密码错误
|
||||
expiredCodeMessage=登录已超时,请重新登录
|
||||
loginTimeout=登录已超时,请重新开始登录
|
||||
@@ -0,0 +1,535 @@
|
||||
:root {
|
||||
--tjwater-canvas: oklch(0.965 0.014 205);
|
||||
--tjwater-surface: oklch(0.995 0.004 205);
|
||||
--tjwater-surface-soft: oklch(0.982 0.008 205);
|
||||
--tjwater-ink: oklch(0.3 0.055 215);
|
||||
--tjwater-muted: oklch(0.52 0.035 215);
|
||||
--tjwater-line: oklch(0.86 0.025 210);
|
||||
--tjwater-blue: oklch(0.57 0.16 242);
|
||||
--tjwater-blue-dark: oklch(0.49 0.15 242);
|
||||
--tjwater-teal: oklch(0.58 0.12 180);
|
||||
--tjwater-danger: oklch(0.55 0.19 27);
|
||||
--tjwater-radius-sm: 6px;
|
||||
--tjwater-radius-md: 12px;
|
||||
--tjwater-radius-lg: 18px;
|
||||
}
|
||||
|
||||
html.login-pf {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
overflow-x: hidden;
|
||||
background: var(--tjwater-canvas);
|
||||
}
|
||||
|
||||
body#keycloak-bg,
|
||||
.login-pf body {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--tjwater-ink);
|
||||
background: var(--tjwater-canvas);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI",
|
||||
"PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.pf-v5-c-login,
|
||||
.pf-v5-c-login * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pf-v5-c-login {
|
||||
min-height: 100svh;
|
||||
padding: 0;
|
||||
background-color: var(--tjwater-canvas);
|
||||
background-image:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
transparent 50%,
|
||||
oklch(0.975 0.01 205 / 62%) 68%,
|
||||
oklch(0.975 0.01 205 / 82%) 100%
|
||||
),
|
||||
url("../img/network-blueprint.svg");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.pf-v5-c-login__container {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: 1720px;
|
||||
min-height: 100svh;
|
||||
margin: 0 auto;
|
||||
padding: clamp(32px, 4.5vw, 76px) clamp(40px, 5vw, 88px);
|
||||
grid-template-columns: minmax(360px, 1fr) minmax(400px, 460px);
|
||||
grid-template-areas: "header main";
|
||||
align-items: center;
|
||||
gap: clamp(64px, 8vw, 152px);
|
||||
}
|
||||
|
||||
#kc-header {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
grid-area: header;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
align-self: center;
|
||||
justify-self: start;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
isolation: isolate;
|
||||
animation: tjwater-enter 480ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
#kc-header::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
inset: -54px -72px;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
oklch(0.925 0.018 205 / 98%) 0%,
|
||||
oklch(0.925 0.018 205 / 94%) 48%,
|
||||
oklch(0.925 0.018 205 / 62%) 65%,
|
||||
transparent 82%
|
||||
);
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
}
|
||||
|
||||
#kc-header-wrapper {
|
||||
display: flex;
|
||||
max-width: 680px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
color: var(--tjwater-ink) !important;
|
||||
font-size: clamp(34px, 3vw, 46px);
|
||||
font-weight: 720;
|
||||
line-height: 1.28;
|
||||
letter-spacing: 0;
|
||||
text-align: left;
|
||||
text-transform: none;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
#kc-header-wrapper::before {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin-bottom: 24px;
|
||||
background: url("../img/logo-mark.svg") center / contain no-repeat;
|
||||
content: "";
|
||||
}
|
||||
|
||||
#kc-header-wrapper::after {
|
||||
width: 64px;
|
||||
height: 3px;
|
||||
margin-top: 26px;
|
||||
border-radius: 999px;
|
||||
background: var(--tjwater-teal);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.pf-v5-c-login__main {
|
||||
grid-area: main;
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
margin: 0;
|
||||
align-self: center;
|
||||
justify-self: stretch;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-radius: var(--tjwater-radius-lg);
|
||||
background: oklch(0.995 0.004 205 / 97%);
|
||||
box-shadow:
|
||||
0 32px 80px rgb(22 65 75 / 16%),
|
||||
0 5px 18px rgb(22 65 75 / 9%);
|
||||
animation: tjwater-enter 520ms 70ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
.pf-v5-c-login__main-header {
|
||||
display: grid;
|
||||
margin: 0;
|
||||
padding: 36px 36px 18px;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
#kc-page-title {
|
||||
margin: 0;
|
||||
color: var(--tjwater-ink);
|
||||
font-size: 26px;
|
||||
font-weight: 720;
|
||||
line-height: 1.4;
|
||||
letter-spacing: 0;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.pf-v5-c-login__main-header-utilities {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pf-v5-c-login__main-body {
|
||||
margin: 0;
|
||||
padding: 0 36px 38px;
|
||||
}
|
||||
|
||||
.pf-v5-c-form {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.pf-v5-c-form__group {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pf-v5-c-form__group-label {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.pf-v5-c-form__label-text {
|
||||
color: var(--tjwater-ink);
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.pf-v5-c-form-control {
|
||||
min-height: 48px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--tjwater-line);
|
||||
border-radius: var(--tjwater-radius-sm);
|
||||
background: var(--tjwater-surface-soft);
|
||||
box-shadow: none;
|
||||
transition-property: border-color, box-shadow, background-color;
|
||||
transition-duration: 160ms;
|
||||
transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.pf-v5-c-form-control::before,
|
||||
.pf-v5-c-form-control::after {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.pf-v5-c-form-control:focus-within {
|
||||
border-color: var(--tjwater-blue);
|
||||
background: var(--tjwater-surface);
|
||||
box-shadow: 0 0 0 3px oklch(0.78 0.1 235 / 28%);
|
||||
}
|
||||
|
||||
.pf-v5-c-form-control > input,
|
||||
.pf-v5-c-form-control > select {
|
||||
min-height: 46px;
|
||||
padding-inline: 14px;
|
||||
color: var(--tjwater-ink);
|
||||
font-size: 16px;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.pf-v5-c-login__main-header-utilities .pf-v5-c-form-control {
|
||||
width: 116px;
|
||||
min-height: 40px;
|
||||
background: var(--tjwater-surface);
|
||||
}
|
||||
|
||||
#login-select-toggle {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 38px;
|
||||
padding-inline: 12px 32px;
|
||||
color: var(--tjwater-muted);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pf-v5-c-form-control.pf-m-error {
|
||||
border-color: var(--tjwater-danger);
|
||||
}
|
||||
|
||||
.pf-v5-c-input-group {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pf-v5-c-input-group__item.pf-m-fill {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-control {
|
||||
min-width: 48px;
|
||||
min-height: 48px;
|
||||
border: 1px solid var(--tjwater-line);
|
||||
border-radius: var(--tjwater-radius-sm);
|
||||
color: var(--tjwater-muted);
|
||||
background: var(--tjwater-surface-soft);
|
||||
touch-action: manipulation;
|
||||
transition-property: color, border-color, background-color, transform;
|
||||
transition-duration: 160ms;
|
||||
transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-control:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-control:focus-visible,
|
||||
.pf-v5-c-button.pf-m-primary:focus-visible,
|
||||
.pf-v5-c-button.pf-m-secondary:focus-visible,
|
||||
a:focus-visible {
|
||||
outline: 3px solid oklch(0.74 0.12 235 / 60%);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.pf-v5-c-form__helper-text {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.pf-v5-c-helper-text {
|
||||
min-height: 22px;
|
||||
}
|
||||
|
||||
.pf-v5-c-helper-text__item-text {
|
||||
color: var(--tjwater-muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.pf-v5-c-helper-text__item-text a,
|
||||
#kc-registration a,
|
||||
.pf-v5-c-login__main-footer a {
|
||||
color: var(--tjwater-blue-dark);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.kc-feedback-text.pf-m-error,
|
||||
.pf-v5-c-helper-text__item.pf-m-error .kc-feedback-text {
|
||||
color: var(--tjwater-danger);
|
||||
}
|
||||
|
||||
.pf-v5-c-check__input {
|
||||
accent-color: var(--tjwater-blue);
|
||||
}
|
||||
|
||||
.pf-v5-c-check__label {
|
||||
color: var(--tjwater-muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.pf-v5-c-form__actions {
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-primary {
|
||||
min-height: 48px;
|
||||
border: 0;
|
||||
border-radius: var(--tjwater-radius-md);
|
||||
color: oklch(0.99 0.004 230);
|
||||
background: var(--tjwater-blue);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8px 18px oklch(0.48 0.15 242 / 20%);
|
||||
touch-action: manipulation;
|
||||
transition-property: transform, background-color, box-shadow;
|
||||
transition-duration: 160ms;
|
||||
transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-primary:active {
|
||||
transform: scale(0.96);
|
||||
background: var(--tjwater-blue-dark);
|
||||
box-shadow: 0 4px 10px oklch(0.48 0.15 242 / 18%);
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-secondary {
|
||||
min-height: 44px;
|
||||
border-radius: var(--tjwater-radius-md);
|
||||
color: var(--tjwater-blue-dark);
|
||||
border-color: var(--tjwater-line);
|
||||
}
|
||||
|
||||
.pf-v5-c-alert {
|
||||
border-radius: var(--tjwater-radius-md);
|
||||
}
|
||||
|
||||
.pf-v5-c-login__main-footer {
|
||||
color: var(--tjwater-muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.pf-v5-c-login__main-footer-band {
|
||||
margin-top: 26px;
|
||||
padding: 18px 0 0;
|
||||
border-top: 1px solid var(--tjwater-line);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@keyframes tjwater-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.pf-v5-c-button.pf-m-primary:hover {
|
||||
background: var(--tjwater-blue-dark);
|
||||
box-shadow: 0 10px 22px oklch(0.48 0.15 242 / 25%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-control:hover {
|
||||
color: var(--tjwater-blue-dark);
|
||||
border-color: oklch(0.69 0.08 230);
|
||||
background: var(--tjwater-surface);
|
||||
}
|
||||
|
||||
.pf-v5-c-helper-text__item-text a:hover,
|
||||
#kc-registration a:hover,
|
||||
.pf-v5-c-login__main-footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.pf-v5-c-login {
|
||||
background-image:
|
||||
linear-gradient(oklch(0.965 0.014 205 / 34%), oklch(0.965 0.014 205 / 34%)),
|
||||
url("../img/network-blueprint.svg");
|
||||
background-position: 34% center;
|
||||
}
|
||||
|
||||
.pf-v5-c-login__container {
|
||||
max-width: 560px;
|
||||
padding:
|
||||
max(28px, env(safe-area-inset-top))
|
||||
max(24px, env(safe-area-inset-right))
|
||||
max(32px, env(safe-area-inset-bottom))
|
||||
max(24px, env(safe-area-inset-left));
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-areas:
|
||||
"header"
|
||||
"main";
|
||||
align-content: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
#kc-header-wrapper {
|
||||
max-width: none;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
font-size: clamp(22px, 5vw, 28px);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
#kc-header::before {
|
||||
inset: -24px -20px;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
oklch(0.965 0.014 205 / 98%) 0%,
|
||||
oklch(0.965 0.014 205 / 88%) 58%,
|
||||
transparent 84%
|
||||
);
|
||||
}
|
||||
|
||||
#kc-header-wrapper::before {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
margin: 0;
|
||||
flex: 0 0 46px;
|
||||
}
|
||||
|
||||
#kc-header-wrapper::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pf-v5-c-login__main {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.pf-v5-c-login__container {
|
||||
gap: 18px;
|
||||
padding-inline:
|
||||
max(14px, env(safe-area-inset-left))
|
||||
max(14px, env(safe-area-inset-right));
|
||||
}
|
||||
|
||||
#kc-header-wrapper {
|
||||
gap: 12px;
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
#kc-header-wrapper::before {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
flex-basis: 42px;
|
||||
}
|
||||
|
||||
.pf-v5-c-login__main {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.pf-v5-c-login__main-header {
|
||||
gap: 12px;
|
||||
padding: 26px 22px 15px;
|
||||
}
|
||||
|
||||
#kc-page-title {
|
||||
font-size: 23px;
|
||||
}
|
||||
|
||||
.pf-v5-c-login__main-header-utilities .pf-v5-c-form-control {
|
||||
width: 108px;
|
||||
}
|
||||
|
||||
.pf-v5-c-login__main-body {
|
||||
padding: 0 22px 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 680px) and (min-width: 901px) {
|
||||
.pf-v5-c-login__container {
|
||||
padding-block: 24px;
|
||||
}
|
||||
|
||||
#kc-header-wrapper::before {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
#kc-header-wrapper::after {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pf-v5-c-login__main-header {
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
.pf-v5-c-login__main-body {
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
#kc-header,
|
||||
.pf-v5-c-login__main {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.pf-v5-c-button,
|
||||
.pf-v5-c-form-control {
|
||||
transition-duration: 0.01ms;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72" role="img" aria-labelledby="title">
|
||||
<title id="title">TJWater</title>
|
||||
<rect width="72" height="72" rx="18" fill="#1478d4"/>
|
||||
<path d="M36 13c-7.8 11.1-16.1 19.4-16.1 29.3A16.1 16.1 0 0 0 36 58.4a16.1 16.1 0 0 0 16.1-16.1C52.1 32.4 43.8 24.1 36 13Z" fill="#f5fbfc"/>
|
||||
<path d="M26.5 43.5h19M31 36.5l5 7 5-7" fill="none" stroke="#0b8f82" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/>
|
||||
<circle cx="26.5" cy="43.5" r="2.7" fill="#0b8f82"/>
|
||||
<circle cx="45.5" cy="43.5" r="2.7" fill="#0b8f82"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 585 B |
@@ -0,0 +1,39 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 900" preserveAspectRatio="xMidYMid slice">
|
||||
<rect width="1440" height="900" fill="#edf5f5"/>
|
||||
<path d="M0 0h790L650 900H0Z" fill="#dceced"/>
|
||||
<path d="M0 182c187-78 303-73 451-16 153 59 288 47 409-25M0 552c193-45 327-19 461 74 128 90 266 100 410 47" fill="none" stroke="#c8dddd" stroke-width="2"/>
|
||||
<g fill="none" stroke="#a9ccce" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M-32 725 178 608l142 51 155-190 184 67 176-205" stroke-width="5"/>
|
||||
<path d="m178 608 25-222 151-92 121 175" stroke-width="3"/>
|
||||
<path d="m203 386-92-95 55-161M354 294l92-141 152 58 98-106" stroke-width="3"/>
|
||||
<path d="m320 659-4 132 171 81M659 536l88 116 124-42" stroke-width="3"/>
|
||||
</g>
|
||||
<g fill="#edf5f5" stroke="#1478d4" stroke-width="4">
|
||||
<circle cx="178" cy="608" r="10"/>
|
||||
<circle cx="203" cy="386" r="9"/>
|
||||
<circle cx="354" cy="294" r="9"/>
|
||||
<circle cx="475" cy="469" r="11"/>
|
||||
<circle cx="659" cy="536" r="10"/>
|
||||
<circle cx="747" cy="652" r="9"/>
|
||||
<circle cx="320" cy="659" r="8"/>
|
||||
</g>
|
||||
<g fill="#0b8f82">
|
||||
<circle cx="111" cy="291" r="6"/>
|
||||
<circle cx="166" cy="130" r="6"/>
|
||||
<circle cx="446" cy="153" r="7"/>
|
||||
<circle cx="598" cy="211" r="6"/>
|
||||
<circle cx="696" cy="105" r="6"/>
|
||||
<circle cx="316" cy="791" r="6"/>
|
||||
<circle cx="487" cy="872" r="6"/>
|
||||
<circle cx="835" cy="331" r="7"/>
|
||||
</g>
|
||||
<g fill="none" stroke="#86b8bb" stroke-width="2" opacity=".72">
|
||||
<circle cx="615" cy="448" r="222"/>
|
||||
<circle cx="615" cy="448" r="276"/>
|
||||
<circle cx="615" cy="448" r="334"/>
|
||||
</g>
|
||||
<g fill="#1478d4" opacity=".08">
|
||||
<rect x="40" y="40" width="118" height="10" rx="5"/>
|
||||
<rect x="40" y="62" width="72" height="6" rx="3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,21 @@
|
||||
const localizeLocaleOptions = () => {
|
||||
const localeSelect = document.querySelector("#login-select-toggle");
|
||||
|
||||
if (!(localeSelect instanceof HTMLSelectElement)) return;
|
||||
|
||||
for (const option of localeSelect.options) {
|
||||
const optionUrl = new URL(option.value, window.location.origin);
|
||||
const locale = optionUrl.searchParams.get("kc_locale");
|
||||
|
||||
if (locale === "zh-CN") option.textContent = "简体中文";
|
||||
if (locale === "en") option.textContent = "English";
|
||||
}
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", localizeLocaleOptions, {
|
||||
once: true,
|
||||
});
|
||||
} else {
|
||||
localizeLocaleOptions();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
parent=keycloak.v2
|
||||
import=common/keycloak
|
||||
|
||||
styles=css/styles.css css/tjwater-login.css
|
||||
scripts=js/locale-labels.js
|
||||
locales=zh-CN,en
|
||||
darkMode=false
|
||||
Reference in New Issue
Block a user