Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bcff55c4f | ||
|
|
c365454395 | ||
|
|
ce07f6b3b7 | ||
|
|
54684fac7c | ||
|
|
160d58b2d2 | ||
|
|
8aad781581 |
@@ -150,9 +150,11 @@ jobs:
|
||||
- name: 从 Runner 验证 Nginx 健康状态
|
||||
env:
|
||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||
DEPLOY_PUBLIC_HOST: 117.72.28.96
|
||||
run: |
|
||||
curl --fail --silent --show-error \
|
||||
--retry 5 \
|
||||
--retry-delay 2 \
|
||||
--header "Host: $DEPLOY_PUBLIC_HOST" \
|
||||
"http://$DEPLOY_HOST:4321/health/" \
|
||||
| grep -q '"status": "ok"'
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
STATIC_ROOT = Path(settings.BASE_DIR) / "static"
|
||||
|
||||
|
||||
def test_app_js_幂等键兼容非安全上下文():
|
||||
source = (STATIC_ROOT / "js" / "app.js").read_text(encoding="utf-8")
|
||||
|
||||
assert "function createIdempotencyKey()" in source
|
||||
assert "crypto.randomUUID()" not in source
|
||||
assert source.count('"Idempotency-Key": createIdempotencyKey()') == 2
|
||||
|
||||
|
||||
def test_app_css_答题提交按钮可见且长弹窗可滚动():
|
||||
source = (STATIC_ROOT / "css" / "app.css").read_text(encoding="utf-8")
|
||||
|
||||
assert ".choice-list .primary-button" in source
|
||||
assert "background: var(--green); color: white; text-align: center" in source
|
||||
assert "max-height: calc(100dvh - 30px)" in source
|
||||
assert "overflow-y: auto" in source
|
||||
|
||||
|
||||
def test_admin_css_深色系统下仍保持完整浅色主题():
|
||||
source = (STATIC_ROOT / "admin" / "css" / "hulumath_admin.css").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert "color-scheme: light" in source
|
||||
assert "--body-fg: #17211b" in source
|
||||
assert "--body-bg: #f3f1e9" in source
|
||||
assert "--darkened-bg: #ebeae3" in source
|
||||
assert "background-color: #ffffff !important" in source
|
||||
assert ".theme-toggle" in source
|
||||
assert "display: none" in source
|
||||
@@ -1,4 +1,5 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--hulu-ink: #17211b;
|
||||
--hulu-green: #196548;
|
||||
--hulu-green-dark: #124d37;
|
||||
@@ -8,18 +9,44 @@
|
||||
--hulu-line: #dfe2da;
|
||||
--hulu-muted: #6b756e;
|
||||
--primary: #196548;
|
||||
--primary-fg: #ffffff;
|
||||
--secondary: #17211b;
|
||||
--accent: #cce85b;
|
||||
--body-fg: #17211b;
|
||||
--body-bg: #f3f1e9;
|
||||
--body-quiet-color: #6b756e;
|
||||
--body-loud-color: #0d1510;
|
||||
--header-bg: #17211b;
|
||||
--header-color: #ffffff;
|
||||
--header-branding-color: #ffffff;
|
||||
--header-link-color: #ffffff;
|
||||
--breadcrumbs-fg: #f3f1e9;
|
||||
--breadcrumbs-bg: #235f47;
|
||||
--breadcrumbs-link-fg: #f3f1e9;
|
||||
--link-fg: #196548;
|
||||
--link-hover-color: #124d37;
|
||||
--link-selected-fg: #124d37;
|
||||
--hairline-color: #e4e6df;
|
||||
--border-color: #ccd2ca;
|
||||
--error-fg: #b84136;
|
||||
--message-success-bg: #dcefe5;
|
||||
--message-warning-bg: #fff1c7;
|
||||
--message-error-bg: #f8deda;
|
||||
--darkened-bg: #ebeae3;
|
||||
--selected-bg: #e2e9e2;
|
||||
--selected-row: #dce9df;
|
||||
--button-fg: #ffffff;
|
||||
--button-bg: #196548;
|
||||
--button-hover-bg: #124d37;
|
||||
--default-button-bg: #17211b;
|
||||
--default-button-hover-bg: #2c3931;
|
||||
--close-button-bg: #667169;
|
||||
--close-button-hover-bg: #3e4942;
|
||||
--delete-button-bg: #b84136;
|
||||
--delete-button-hover-bg: #913128;
|
||||
--object-tools-fg: #ffffff;
|
||||
--object-tools-bg: #196548;
|
||||
--object-tools-hover-bg: #124d37;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -27,6 +54,10 @@ body {
|
||||
color: var(--hulu-ink);
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#header {
|
||||
min-height: 70px;
|
||||
padding: 0 28px;
|
||||
@@ -125,6 +156,19 @@ div.breadcrumbs {
|
||||
background: #ebeae3;
|
||||
}
|
||||
|
||||
#nav-sidebar .module,
|
||||
#nav-sidebar .module table,
|
||||
#nav-sidebar .module th,
|
||||
#nav-sidebar .module td {
|
||||
background: var(--hulu-panel);
|
||||
color: var(--hulu-ink);
|
||||
}
|
||||
|
||||
#nav-sidebar .module td,
|
||||
#nav-sidebar .module th {
|
||||
border-color: var(--hulu-line);
|
||||
}
|
||||
|
||||
#nav-sidebar .current-app .section,
|
||||
#nav-sidebar .current-model {
|
||||
background: rgba(25, 101, 72, .1);
|
||||
@@ -164,6 +208,14 @@ select,
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
input:not([type=submit]):not([type=button]):not([type=checkbox]):not([type=radio]),
|
||||
textarea,
|
||||
select,
|
||||
.vTextField {
|
||||
background-color: #ffffff !important;
|
||||
color: var(--hulu-ink) !important;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
@@ -172,6 +224,29 @@ select:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-row,
|
||||
.submit-row {
|
||||
border-color: var(--hulu-line);
|
||||
background: var(--hulu-panel);
|
||||
}
|
||||
|
||||
.form-row label,
|
||||
.form-row p,
|
||||
.form-row .help,
|
||||
.form-row div.help,
|
||||
.form-row div.help li {
|
||||
color: var(--hulu-muted);
|
||||
}
|
||||
|
||||
.form-row label {
|
||||
color: #334039;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.submit-row {
|
||||
border: 1px solid var(--hulu-line);
|
||||
}
|
||||
|
||||
.button,
|
||||
input[type=submit],
|
||||
input[type=button],
|
||||
|
||||
@@ -198,7 +198,7 @@ button { color: inherit; }
|
||||
.video-complete { position: absolute; right: 12px; top: 12px; background: var(--green); color: white; border-radius: 99px; padding: 5px 8px; font-size: 9px; }
|
||||
.video-body { padding: 19px; }.ability-badge { display: inline-block; border: 1px solid var(--badge-color); color: var(--badge-color); border-radius: 8px; padding: 5px 8px; font-size: 10px; }.video-body h3 { min-height: 48px; margin: 14px 0 16px; font: 20px/1.3 Georgia, serif; }.video-metrics, .video-author { display: flex; justify-content: space-between; color: var(--muted); font-size: 11px; }.video-author { justify-content: flex-start; gap: 8px; align-items: center; margin-top: 14px; }.video-author i { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 50%; background: var(--badge-color); color: white; font-style: normal; }
|
||||
.video-dialog { width: min(760px, calc(100% - 30px)); }.video-player { min-height: 300px; border-radius: 17px; background: var(--ink); display: grid; place-items: center; color: white; text-align: center; padding: 30px; }.video-player span { display: block; color: var(--lime); font-size: 55px; }.video-player p { color: #aeb9b1; }.video-detail-meta { display: flex; gap: 12px; flex-wrap: wrap; color: var(--muted); font-size: 11px; margin: 18px 0; }.video-dialog h2 { font: 31px/1.2 Georgia, serif; }.video-dialog .video-description { color: var(--muted); line-height: 1.8; }.video-dialog-actions { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-top: 22px; }
|
||||
dialog { width: min(460px, calc(100% - 30px)); border: 1px solid var(--line); border-radius: 20px; padding: 30px; color: var(--ink); background: #faf9f3; box-shadow: 0 30px 100px rgba(0,0,0,.22); }
|
||||
dialog { width: min(460px, calc(100% - 30px)); max-height: calc(100vh - 30px); max-height: calc(100dvh - 30px); overflow-y: auto; border: 1px solid var(--line); border-radius: 20px; padding: 30px; color: var(--ink); background: #faf9f3; box-shadow: 0 30px 100px rgba(0,0,0,.22); }
|
||||
dialog::backdrop { background: rgba(17,25,20,.55); backdrop-filter: blur(5px); }
|
||||
.dialog-close { position: absolute; right: 16px; top: 13px; border: 0; background: transparent; font-size: 25px; cursor: pointer; }
|
||||
.dialog-tabs { display: flex; border-bottom: 1px solid var(--line); margin-bottom: 25px; }
|
||||
@@ -213,6 +213,9 @@ dialog::backdrop { background: rgba(17,25,20,.55); backdrop-filter: blur(5px); }
|
||||
.choice-list { display: grid; gap: 9px; margin-top: 25px; }
|
||||
.choice-list button { text-align: left; border: 1px solid var(--line); border-radius: 12px; padding: 14px; background: white; cursor: pointer; }
|
||||
.choice-list button:hover { border-color: var(--green); }
|
||||
.choice-list .primary-button { width: 100%; border-color: var(--green); background: var(--green); color: white; text-align: center; font-weight: 650; }
|
||||
.choice-list .primary-button:hover { border-color: var(--green); background: var(--green); }
|
||||
.choice-list button:disabled { cursor: wait; opacity: .65; }
|
||||
.quiz-progress { height: 4px; background: #e4e3da; margin: 15px 0 30px; }.quiz-progress i { display: block; height: 100%; background: var(--green); transition: width .2s; }
|
||||
.toast { position: fixed; right: 25px; bottom: 25px; background: var(--ink); color: white; border-radius: 12px; padding: 13px 17px; opacity: 0; transform: translateY(10px); pointer-events: none; transition: .2s; z-index: 20; }
|
||||
.toast.show { opacity: 1; transform: none; }
|
||||
|
||||
@@ -17,6 +17,32 @@ function csrfToken() {
|
||||
return match ? decodeURIComponent(match[1]) : "";
|
||||
}
|
||||
|
||||
function createIdempotencyKey() {
|
||||
const cryptoApi = globalThis.crypto;
|
||||
if (typeof cryptoApi?.randomUUID === "function") {
|
||||
return cryptoApi.randomUUID();
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(16);
|
||||
if (typeof cryptoApi?.getRandomValues === "function") {
|
||||
cryptoApi.getRandomValues(bytes);
|
||||
} else {
|
||||
for (let index = 0; index < bytes.length; index += 1) {
|
||||
bytes[index] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
}
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
const hex = [...bytes].map((value) => value.toString(16).padStart(2, "0"));
|
||||
return [
|
||||
hex.slice(0, 4).join(""),
|
||||
hex.slice(4, 6).join(""),
|
||||
hex.slice(6, 8).join(""),
|
||||
hex.slice(8, 10).join(""),
|
||||
hex.slice(10).join(""),
|
||||
].join("-");
|
||||
}
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const headers = { Accept: "application/json", ...(options.headers || {}) };
|
||||
if (options.body && typeof options.body !== "string") {
|
||||
@@ -165,7 +191,7 @@ function renderStoryNode(run) {
|
||||
try {
|
||||
const next = await api(`math-life/runs/${run.run_id}/choice/`, {
|
||||
method: "POST",
|
||||
headers: { "Idempotency-Key": crypto.randomUUID() },
|
||||
headers: { "Idempotency-Key": createIdempotencyKey() },
|
||||
body: { choice_index: choice.index },
|
||||
});
|
||||
renderStoryNode(next);
|
||||
@@ -341,7 +367,7 @@ function renderAttempt(attempt) {
|
||||
try {
|
||||
const result = await api(`contests/attempts/${attempt.attempt_id}/submit/`, {
|
||||
method: "POST",
|
||||
headers: { "Idempotency-Key": crypto.randomUUID() },
|
||||
headers: { "Idempotency-Key": createIdempotencyKey() },
|
||||
body: { answers },
|
||||
});
|
||||
renderAttempt(result);
|
||||
|
||||
Reference in New Issue
Block a user