feat(api): standardize REST contracts and auth

This commit is contained in:
2026-07-30 20:38:51 +08:00
parent ae1a657554
commit ba947b616b
86 changed files with 54193 additions and 990 deletions
@@ -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