From 9fa25189a2194b5488d5ef98f2e4c4448012d435 Mon Sep 17 00:00:00 2001 From: kronecker <2026692687@qq.com> Date: Mon, 5 Oct 2026 01:48:53 +0800 Subject: [PATCH] feat(frontend): establish shared components and blue page layouts Add reusable headings, panels, tables, charts, pagination and explicit preview fixtures. Apply the shared blue theme, responsive top navigation and Chinese UI locale. --- frontend/src/App.vue | 11 +- frontend/src/components/DifficultyTag.vue | 15 + frontend/src/components/EmptyState.vue | 10 + frontend/src/components/PageHeading.vue | 13 + frontend/src/components/PanelCard.vue | 15 + frontend/src/components/ProblemTable.vue | 39 + frontend/src/components/SubmissionChart.vue | 150 ++ frontend/src/components/SubmissionTable.vue | 40 + frontend/src/components/UiIcon.vue | 54 + frontend/src/components/VerdictTag.vue | 9 + frontend/src/composables/usePagination.ts | 13 + frontend/src/data/preview.ts | 465 ++++++ frontend/src/env.d.ts | 2 + frontend/src/layouts/AppLayout.vue | 94 +- frontend/src/main.ts | 7 +- frontend/src/plugins/ui.ts | 65 + frontend/src/styles/base.css | 1437 ++++++++++++++++++- frontend/src/styles/blueprint.css | 492 +++++++ 18 files changed, 2867 insertions(+), 64 deletions(-) create mode 100644 frontend/src/components/DifficultyTag.vue create mode 100644 frontend/src/components/EmptyState.vue create mode 100644 frontend/src/components/PageHeading.vue create mode 100644 frontend/src/components/PanelCard.vue create mode 100644 frontend/src/components/ProblemTable.vue create mode 100644 frontend/src/components/SubmissionChart.vue create mode 100644 frontend/src/components/SubmissionTable.vue create mode 100644 frontend/src/components/UiIcon.vue create mode 100644 frontend/src/components/VerdictTag.vue create mode 100644 frontend/src/composables/usePagination.ts create mode 100644 frontend/src/data/preview.ts create mode 100644 frontend/src/env.d.ts create mode 100644 frontend/src/plugins/ui.ts create mode 100644 frontend/src/styles/blueprint.css diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 1e40838..792e08c 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,9 +1,14 @@ - - - + + + + + + + diff --git a/frontend/src/components/DifficultyTag.vue b/frontend/src/components/DifficultyTag.vue new file mode 100644 index 0000000..ccac152 --- /dev/null +++ b/frontend/src/components/DifficultyTag.vue @@ -0,0 +1,15 @@ + + + {{ value }} + diff --git a/frontend/src/components/EmptyState.vue b/frontend/src/components/EmptyState.vue new file mode 100644 index 0000000..922cbe2 --- /dev/null +++ b/frontend/src/components/EmptyState.vue @@ -0,0 +1,10 @@ + + + + + {{ description }} + + + diff --git a/frontend/src/components/PageHeading.vue b/frontend/src/components/PageHeading.vue new file mode 100644 index 0000000..b11b840 --- /dev/null +++ b/frontend/src/components/PageHeading.vue @@ -0,0 +1,13 @@ + + + + + {{ eyebrow }} + {{ title }} + {{ description }} + + + + diff --git a/frontend/src/components/PanelCard.vue b/frontend/src/components/PanelCard.vue new file mode 100644 index 0000000..1f42cfa --- /dev/null +++ b/frontend/src/components/PanelCard.vue @@ -0,0 +1,15 @@ + + + + + + {{ title }}{{ subtitle }} + + + + + + diff --git a/frontend/src/components/ProblemTable.vue b/frontend/src/components/ProblemTable.vue new file mode 100644 index 0000000..26db178 --- /dev/null +++ b/frontend/src/components/ProblemTable.vue @@ -0,0 +1,39 @@ + + + + {{ row.id }} + {{ + row.title + }} + + {{ tag }} + + + {{ acceptance({ accepted: row.accepted, submissions: row.submissions }) }}% + + + diff --git a/frontend/src/components/SubmissionChart.vue b/frontend/src/components/SubmissionChart.vue new file mode 100644 index 0000000..79bc754 --- /dev/null +++ b/frontend/src/components/SubmissionChart.vue @@ -0,0 +1,150 @@ + + + + + + 通过 + 总提交单位:次 · 示例统计 + + + + + {{ tick }} + + + {{ point.day }} + + + + + + + + {{ point.day }}:总提交 {{ point.total }},通过 {{ point.accepted }} + + + + + {{ + selected + ? selected.day + ' · 总提交 ' + selected.total + ' 次 · 通过 ' + selected.accepted + ' 次' + : '近 7 天的提交与通过情况' + }} + + + + diff --git a/frontend/src/components/SubmissionTable.vue b/frontend/src/components/SubmissionTable.vue new file mode 100644 index 0000000..495629f --- /dev/null +++ b/frontend/src/components/SubmissionTable.vue @@ -0,0 +1,40 @@ + + + #{{ row.id }}{{ + findProblem(row.problemId)?.title + }}{{ row.username }}{{ + row.time === null ? '—' : row.time + ' ms' + }}{{ + row.memory === null ? '—' : row.memory + ' KiB' + }} + diff --git a/frontend/src/components/UiIcon.vue b/frontend/src/components/UiIcon.vue new file mode 100644 index 0000000..f11e6cb --- /dev/null +++ b/frontend/src/components/UiIcon.vue @@ -0,0 +1,54 @@ + + + + diff --git a/frontend/src/components/VerdictTag.vue b/frontend/src/components/VerdictTag.vue new file mode 100644 index 0000000..007b149 --- /dev/null +++ b/frontend/src/components/VerdictTag.vue @@ -0,0 +1,9 @@ + + + {{ + full ? verdictLabels[value] : value === 'Pending' ? '等待评测' : value + }} + diff --git a/frontend/src/composables/usePagination.ts b/frontend/src/composables/usePagination.ts new file mode 100644 index 0000000..6d15e13 --- /dev/null +++ b/frontend/src/composables/usePagination.ts @@ -0,0 +1,13 @@ +import { computed, ref, watch, type Ref } from 'vue' +export function usePagination(items: Readonly>, initialSize = 10) { + const page = ref(1) + const pageSize = ref(initialSize) + const total = computed(() => items.value.length) + const paged = computed(() => + items.value.slice((page.value - 1) * pageSize.value, page.value * pageSize.value), + ) + watch([items, pageSize], () => { + page.value = 1 + }) + return { page, pageSize, total, paged } +} diff --git a/frontend/src/data/preview.ts b/frontend/src/data/preview.ts new file mode 100644 index 0000000..d0b0496 --- /dev/null +++ b/frontend/src/data/preview.ts @@ -0,0 +1,465 @@ +// Explicit UI fixtures; business endpoints will be connected through API composables. +export type Difficulty = '入门' | '简单' | '中等' | '困难' +export interface Problem { + id: string + title: string + difficulty: Difficulty + tags: string[] + accepted: number + submissions: number + updated: string + description: string + input: string + output: string + sampleInput: string + sampleOutput: string + constraint: string +} +const definitions: [string, string, Difficulty, string[], number, number][] = [ + ['1001', 'A + B Problem', '入门', ['模拟', '输入输出'], 428, 536], + ['1002', '两数之和', '简单', ['数组', '哈希表'], 315, 482], + ['1003', '最长无重复子串', '中等', ['字符串', '滑动窗口'], 206, 527], + ['1004', '旅行者的背包', '中等', ['动态规划', '背包'], 186, 461], + ['1005', '城市间的最短路径', '中等', ['图论', '最短路'], 152, 387], + ['1006', '寻找第一个位置', '简单', ['二分', '数组'], 241, 402], + ['1007', '括号匹配', '简单', ['栈', '字符串'], 287, 389], + ['1008', '岛屿的数量', '中等', ['搜索', '图论'], 143, 328], + ['1009', '区间求和', '中等', ['数据结构', '前缀和'], 124, 306], + ['1010', '最小生成树', '困难', ['图论', '并查集'], 96, 288], + ['1011', '活动安排', '简单', ['贪心', '排序'], 203, 312], + ['1012', '最长公共子序列', '中等', ['动态规划', '字符串'], 112, 345], +] +export const problems: Problem[] = definitions.map( + ([id, title, difficulty, tags, accepted, submissions], index) => ({ + id, + title, + difficulty, + tags, + accepted, + submissions, + updated: '2026-10-' + String(4 - Math.floor(index / 4)).padStart(2, '0'), + description: + id === '1001' + ? '给定两个整数 A 和 B,请计算它们的和。认真阅读输入和输出格式,完成你的第一次提交。' + : '这道题的完整题面将在题库接入后展示。当前提供列表与编辑器的界面预览。', + input: + id === '1001' + ? '输入一行,包含两个整数 A 和 B,以一个空格分隔。' + : '完整输入格式待题库接入。', + output: id === '1001' ? '输出一个整数,表示 A 与 B 的和。' : '完整输出格式待题库接入。', + sampleInput: id === '1001' ? '1 2' : '', + sampleOutput: id === '1001' ? '3' : '', + constraint: + id === '1001' + ? '−10⁹ ≤ A, B ≤ 10⁹。建议使用 64 位整数保存计算结果。' + : '数据范围待题库接入。', + }), +) +export const problemTags = [...new Set(problems.flatMap((problem) => problem.tags))] +export const rankings = [ + { + username: 'Ada', + motto: '每天进步一点点', + solved: 167, + submissions: 238, + rating: 1924, + color: '#6196ec', + }, + { + username: 'Kronecker', + motto: '写下思路,再写代码。', + solved: 142, + submissions: 216, + rating: 1846, + color: '#6b70d6', + }, + { + username: 'Sora', + motto: '今天也要 AC', + solved: 129, + submissions: 204, + rating: 1732, + color: '#3fb7a1', + }, + { + username: 'Natsume', + motto: '慢慢来,比较快', + solved: 118, + submissions: 182, + rating: 1685, + color: '#e9a35d', + }, + { + username: 'Lin', + motto: '保持好奇', + solved: 106, + submissions: 175, + rating: 1621, + color: '#dc82a4', + }, + { + username: 'Mika', + motto: '从一道题开始', + solved: 98, + submissions: 153, + rating: 1594, + color: '#63a9c9', + }, + { + username: 'Chen', + motto: 'Hello, algorithm', + solved: 92, + submissions: 151, + rating: 1540, + color: '#897cb7', + }, + { + username: 'Yuki', + motto: '专注这一题', + solved: 87, + submissions: 144, + rating: 1506, + color: '#6aaa8a', + }, + { + username: 'Joago', + motto: 'Thinking in code', + solved: 81, + submissions: 132, + rating: 1482, + color: '#bb9b67', + }, + { + username: 'Hyperbola', + motto: '努力成为更好的自己', + solved: 74, + submissions: 128, + rating: 1428, + color: '#8095b9', + }, +] +export const notices = [ + { + id: '1', + title: '欢迎来到 KF OnlineJudge', + date: '2026-10-04', + category: '平台公告', + text: '从基础练习开始,记录每一次提交,让思路在代码里落地。当前页面使用示例内容。', + }, + { + id: '2', + title: '新学期基础训练题单已更新', + date: '2026-10-03', + category: '训练', + text: '输入输出、数组、字符串和简单搜索,按顺序完成你的第一组算法练习。', + }, + { + id: '3', + title: '周末练习赛 · 热身题单', + date: '2026-10-01', + category: '比赛', + text: '练习赛页面已准备好,实际比赛安排以平台发布的信息为准。', + }, +] +export const trainings = [ + { + id: 'basics', + title: '从零开始 · 编程基础', + category: '基础入门', + level: '入门', + description: '从输入输出到数组与字符串,熟悉在线评测的基本流程。', + ids: ['1001', '1002', '1006', '1007'], + color: 'blue', + icon: '01', + }, + { + id: 'search', + title: '搜索与图论入门', + category: '算法专题', + level: '中等', + description: '理解遍历、连通性与路径,建立图论的基础解题思路。', + ids: ['1008', '1005', '1010'], + color: 'green', + icon: '02', + }, + { + id: 'dp', + title: '动态规划 · 一步一步推出来', + category: '算法专题', + level: '中等', + description: '从状态定义开始,练习转移、边界和空间优化。', + ids: ['1004', '1012'], + color: 'violet', + icon: '03', + }, + { + id: 'structures', + title: '常用数据结构练习', + category: '进阶练习', + level: '中等', + description: '通过具体问题理解哈希表、栈、前缀和与并查集。', + ids: ['1002', '1007', '1009', '1010'], + color: 'amber', + icon: '04', + }, +] +export const contests = [ + { + id: 'weekly-01', + title: 'KF 周末练习赛 #01', + type: 'ACM', + status: '即将开始', + start: '2026-10-10 14:00', + duration: '2 小时', + description: '五道由浅入深的算法题,给自己一个专注思考的下午。', + ids: ['1001', '1002', '1003', '1004', '1005'], + }, + { + id: 'starter', + title: '新同学编程热身赛', + type: 'OI', + status: '即将开始', + start: '2026-10-12 19:00', + duration: '2 小时', + description: '面向刚开始学习程序设计的同学,练习输入输出与基本逻辑。', + ids: ['1001', '1006', '1007'], + }, + { + id: 'graph', + title: '图论专题 · 路径与连通性', + type: 'ACM', + status: '进行中', + start: '2026-10-04 14:00', + duration: '4 小时', + description: '题单式比赛界面示例,状态与时间为展示用数据。', + ids: ['1005', '1008', '1010'], + }, + { + id: 'weekly-00', + title: 'KF 周末练习赛 #00', + type: 'ACM', + status: '已结束', + start: '2026-09-27 14:00', + duration: '2 小时', + description: '往期比赛界面示例,便于浏览题目和比赛说明。', + ids: ['1001', '1003', '1011'], + }, + { + id: 'dp-practice', + title: '动态规划专项练习', + type: 'OI', + status: '已结束', + start: '2026-09-20 09:00', + duration: '3 小时', + description: '集中练习常见动态规划模型。', + ids: ['1004', '1012'], + }, +] +export type Verdict = 'AC' | 'WA' | 'TLE' | 'RE' | 'CE' | 'Pending' +export const verdictLabels: Record = { + AC: 'Accepted', + WA: 'Wrong Answer', + TLE: 'Time Limit Exceeded', + RE: 'Runtime Error', + CE: 'Compile Error', + Pending: '等待评测', +} +export const submissions: { + id: string + problemId: string + username: string + verdict: Verdict + language: string + time: number | null + memory: number | null + created: string +}[] = [ + { + id: '10248', + problemId: '1001', + username: 'Kronecker', + verdict: 'AC', + language: 'Rust', + time: 12, + memory: 2048, + created: '2026-10-04 14:32:18', + }, + { + id: '10247', + problemId: '1003', + username: 'Ada', + verdict: 'AC', + language: 'C++', + time: 34, + memory: 4096, + created: '2026-10-04 14:30:05', + }, + { + id: '10246', + problemId: '1004', + username: 'Sora', + verdict: 'WA', + language: 'Python', + time: 62, + memory: 8192, + created: '2026-10-04 14:28:41', + }, + { + id: '10245', + problemId: '1005', + username: 'Lin', + verdict: 'TLE', + language: 'C++', + time: 1000, + memory: 6144, + created: '2026-10-04 14:26:22', + }, + { + id: '10244', + problemId: '1002', + username: 'Mika', + verdict: 'AC', + language: 'Python', + time: 48, + memory: 7168, + created: '2026-10-04 14:24:09', + }, + { + id: '10243', + problemId: '1007', + username: 'Chen', + verdict: 'CE', + language: 'C++', + time: null, + memory: null, + created: '2026-10-04 14:21:33', + }, + { + id: '10242', + problemId: '1008', + username: 'Yuki', + verdict: 'RE', + language: 'Rust', + time: 18, + memory: 3072, + created: '2026-10-04 14:19:06', + }, + { + id: '10241', + problemId: '1006', + username: 'Kronecker', + verdict: 'AC', + language: 'Rust', + time: 11, + memory: 2048, + created: '2026-10-04 14:17:26', + }, + { + id: '10240', + problemId: '1011', + username: 'Natsume', + verdict: 'Pending', + language: 'C++', + time: null, + memory: null, + created: '2026-10-04 14:15:12', + }, + { + id: '10239', + problemId: '1009', + username: 'Ada', + verdict: 'AC', + language: 'C++', + time: 22, + memory: 4096, + created: '2026-10-04 14:12:45', + }, + { + id: '10238', + problemId: '1002', + username: 'Sora', + verdict: 'WA', + language: 'Python', + time: 56, + memory: 7168, + created: '2026-10-04 14:10:18', + }, + { + id: '10237', + problemId: '1012', + username: 'Hyperbola', + verdict: 'AC', + language: 'C++', + time: 71, + memory: 10240, + created: '2026-10-04 14:08:51', + }, +] +export const discussions = [ + { + id: '1', + title: '第一次使用在线评测,可以从哪里开始?', + category: '交流', + author: 'Kronecker', + replies: 12, + views: 328, + date: '2026-10-04', + pinned: true, + content: + '欢迎来到 KF OnlineJudge!可以先打开「从零开始 · 编程基础」题单,了解标准输入输出,再尝试 A + B Problem。\n\n遇到问题时,记录题号、语言和具体报错,把自己的思路一起写出来,讨论会更容易展开。', + }, + { + id: '2', + title: 'A + B Problem:输入输出与整数范围', + category: '题解', + author: 'Ada', + replies: 8, + views: 216, + date: '2026-10-04', + pinned: false, + content: + '读取两个整数,计算它们的和,输出结果。\n\n记得检查整数范围。Rust 可以选择 i64;Python 的整数可以直接参与计算。输出只需要答案和换行。', + problemId: '1001', + }, + { + id: '3', + title: '动态规划中的状态应该怎么定义?', + category: '问答', + author: 'Sora', + replies: 6, + views: 184, + date: '2026-10-03', + pinned: false, + content: + '练习背包问题时,我经常卡在状态定义上。现在尝试先描述「已经处理哪些物品」和「剩余多少容量」,再考虑每一步有哪些选择。\n\n大家平时会怎样从题意里提取状态?', + }, + { + id: '4', + title: '分享我的 Rust 刷题输入模板', + category: '交流', + author: 'Lin', + replies: 4, + views: 152, + date: '2026-10-02', + pinned: false, + content: + '先用 read_to_string 获取输入,再按空白分隔。\n\n不同题目的输入结构可能不同,使用模板之前还是需要认真阅读题面。', + }, + { + id: '5', + title: '关于题库筛选的一点建议', + category: '反馈', + author: 'Mika', + replies: 2, + views: 96, + date: '2026-10-01', + pinned: false, + content: '希望题库可以同时按照标签和难度筛选,这样练习某个知识点会更方便。', + }, +] +export function findProblem(id: string) { + return problems.find((problem) => problem.id === id) +} +export function acceptance(problem: Pick) { + return Math.round((problem.accepted / problem.submissions) * 100) +} diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts new file mode 100644 index 0000000..f614c5b --- /dev/null +++ b/frontend/src/env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/frontend/src/layouts/AppLayout.vue b/frontend/src/layouts/AppLayout.vue index efa2fbd..851d43b 100644 --- a/frontend/src/layouts/AppLayout.vue +++ b/frontend/src/layouts/AppLayout.vue @@ -1,12 +1,94 @@ + - KFOnlineJudge - 编程 · 练习 · 成长 + + KF.OnlineJudgeCODE · PRACTICE · GROW + + 导航 + + + {{ link.label }} + + + 界面预览登录注册 + + - - - - + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 00dddce..4c9b9af 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,8 +1,11 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' -import 'element-plus/dist/index.css' +import { registerUi } from './plugins/ui' import App from './App.vue' import { router } from './router' import './styles/base.css' +import './styles/blueprint.css' -createApp(App).use(createPinia()).use(router).mount('#app') +const app = createApp(App) +registerUi(app) +app.use(createPinia()).use(router).mount('#app') diff --git a/frontend/src/plugins/ui.ts b/frontend/src/plugins/ui.ts new file mode 100644 index 0000000..72fb75a --- /dev/null +++ b/frontend/src/plugins/ui.ts @@ -0,0 +1,65 @@ +import type { App } from 'vue' +import { + ElButton, + ElConfigProvider, + ElDescriptions, + ElDescriptionsItem, + ElDialog, + ElEmpty, + ElForm, + ElFormItem, + ElIcon, + ElInput, + ElOption, + ElPagination, + ElPopover, + ElProgress, + ElRadio, + ElRadioButton, + ElRadioGroup, + ElResult, + ElSelect, + ElSlider, + ElSwitch, + ElTabPane, + ElTable, + ElTableColumn, + ElTabs, + ElTag, + ElTooltip, +} from 'element-plus' +import 'element-plus/dist/index.css' + +export function registerUi(app: App) { + for (const component of [ + ElButton, + ElConfigProvider, + ElDescriptions, + ElDescriptionsItem, + ElDialog, + ElEmpty, + ElForm, + ElFormItem, + ElIcon, + ElInput, + ElOption, + ElPagination, + ElPopover, + ElProgress, + ElRadio, + ElRadioButton, + ElRadioGroup, + ElResult, + ElSelect, + ElSlider, + ElSwitch, + ElTabPane, + ElTable, + ElTableColumn, + ElTabs, + ElTag, + ElTooltip, + ]) { + app.use(component) + } +} diff --git a/frontend/src/styles/base.css b/frontend/src/styles/base.css index ce5705c..1b02049 100644 --- a/frontend/src/styles/base.css +++ b/frontend/src/styles/base.css @@ -1,11 +1,19 @@ :root { font-family: Inter, 'Noto Sans SC', 'Microsoft YaHei', sans-serif; - color: #18243c; - background: #f5f7fc; + color: #354257; + background: #f0f3f7; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; - --el-color-primary: #2563eb; + --el-color-primary: #409eff; + --el-border-radius-base: 5px; + --el-border-color: #e0e6ee; + --el-text-color-primary: #354257; + --el-text-color-regular: #566379; + --el-fill-color-lighter: #f8fafc; + --blue: #409eff; + --muted: #8490a3; + --line: #edf0f5; } * { box-sizing: border-box; @@ -17,9 +25,31 @@ a { color: inherit; text-decoration: none; } +a:hover { + color: var(--blue); +} button, -a { - -webkit-tap-highlight-color: transparent; +input, +textarea { + font: inherit; +} +button { + cursor: pointer; +} +button:disabled { + cursor: not-allowed; +} +h1, +h2, +h3, +p { + margin: 0; +} +p { + line-height: 1.75; +} +::selection { + background: #daecff; } .app-shell { min-height: 100vh; @@ -27,79 +57,1380 @@ a { flex-direction: column; } .app-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 24px 6vw; - border-bottom: 1px solid #e4e9f2; background: #fff; + border-bottom: 1px solid #e5eaf1; + box-shadow: 0 2px 5px #263c5605; +} +.header-inner { + width: min(100% - 48px, 1480px); + margin: auto; + display: flex; + align-items: center; + min-height: 68px; + gap: 28px; } .brand { - font-weight: 800; - font-size: 24px; - color: #2563eb; + display: flex; + align-items: center; + gap: 10px; + white-space: nowrap; } -.brand span { - font-size: 17px; - margin-left: 10px; - color: #18243c; +.brand-mark { + font-size: 29px; + letter-spacing: -2px; + font-weight: 850; + color: #409eff; + line-height: 1; } -.header-caption, -.app-footer { - color: #65738b; +.brand-dot { + color: #91bff3; +} +.brand-name { + font-size: 15px; + font-weight: 750; + color: #2d4261; + letter-spacing: -0.3px; +} +.brand-name small { + display: block; + font-size: 8px; + letter-spacing: 1px; + color: #91a3bb; + margin-top: 4px; +} +.main-nav { + display: flex; + align-self: stretch; + gap: 3px; +} +.main-nav a { + display: flex; + align-items: center; + gap: 7px; + padding: 0 13px; + font-size: 13px; + border-bottom: 2px solid transparent; + white-space: nowrap; + color: #67758a; +} +.main-nav a.active { + color: var(--blue); + background: #f4f9ff; + border-color: var(--blue); +} +.main-nav .el-icon { font-size: 14px; } +.header-account { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; + white-space: nowrap; +} +.preview-label { + font-size: 11px; + color: #91a0b2; + margin-right: 6px; +} +.mobile-toggle { + display: none; +} .app-main { - width: min(100% - 40px, 1040px); - margin: 0 auto; + width: min(100% - 48px, 1480px); + margin: 24px auto 40px; flex: 1; } -.welcome { - padding: 80px 0; - max-width: 720px; +.app-footer { + background: #fff; + border-top: 1px solid #e6ecf3; + padding: 36px 0 20px; } -h1 { - font-size: clamp(32px, 5vw, 52px); - line-height: 1.3; - letter-spacing: -1px; - margin: 24px 0; +.footer-inner, +.footer-bottom { + width: min(100% - 64px, 1400px); + margin: auto; + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr; + gap: 32px; } -.welcome-description { - color: #65738b; - font-size: 17px; - line-height: 1.8; - max-width: 620px; +.footer-brand strong { + display: block; + margin: 15px 0 7px; + font-size: 14px; + font-weight: 500; } -.connection-card { - margin-top: 40px; - border-radius: 14px; +.footer-brand p { + font-size: 12px; + color: var(--muted); } -.connection-row { +.footer-inner h3 { + font-size: 13px; + font-weight: 600; + margin: 2px 0 12px; +} +.footer-inner a { + display: block; + font-size: 12px; + color: #8490a3; + margin: 10px 0; +} +.footer-inner a:hover { + color: var(--blue); +} +.footer-bottom { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + border-top: 1px solid var(--line); + padding-top: 20px; + margin-top: 30px; + font-size: 11px; + color: #9aa5b5; +} +.panel { + background: #fff; + border: 1px solid #e8edf4; + border-radius: 6px; + box-shadow: 0 2px 7px #263c5605; + min-width: 0; + overflow: hidden; +} +.panel-heading { + min-height: 54px; + padding: 15px 20px; display: flex; - gap: 24px; align-items: center; justify-content: space-between; + gap: 12px; + border-bottom: 1px solid var(--line); } -.connection-row h2 { - font-size: 16px; - margin: 0 0 8px; +.panel-heading h2 { + font-size: 15px; + color: #4995e5; + font-weight: 600; + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; } -.connection-row p { +.panel-heading small { + font-size: 11px; + font-weight: 400; + color: #98a4b4; + margin-left: 4px; +} +.panel-actions { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: var(--muted); + flex-shrink: 0; +} +.panel-actions a { + color: var(--blue); +} +.panel-body { + padding: 20px; + min-width: 0; +} +.page-heading { + display: flex; + justify-content: space-between; + align-items: center; + gap: 20px; + margin: 8px 0 24px; +} +.page-heading h1 { + font-size: 26px; + font-weight: 650; + letter-spacing: -0.6px; + margin-bottom: 8px; + color: #2e405b; +} +.page-heading p { + font-size: 13px; +} +.page-heading-actions { + display: flex; + align-items: center; + gap: 10px; +} +.eyebrow { + text-transform: uppercase; + font-size: 11px !important; + letter-spacing: 2px; + color: #86a0be; + margin-bottom: 8px; +} +.muted { + color: var(--muted); +} +.small { + font-size: 12px; +} +.mono { + font-family: ui-monospace, SFMono-Regular, Consolas, monospace; +} +.tabular { + font-variant-numeric: tabular-nums; +} +.text-link { + color: var(--blue); + font-size: 13px; +} +.table-link { + color: #455e80; + font-weight: 500; +} +.table-link:hover { + color: var(--blue); +} +.tag-list { + display: flex; + gap: 5px; + flex-wrap: wrap; +} +.tag { + display: inline-flex; + align-items: center; + padding: 3px 7px; + color: #8492a6; + background: #f3f6fa; + border-radius: 3px; + font-size: 11px; + white-space: nowrap; +} +.difficulty { + padding: 3px 7px; + border-radius: 4px; + font-size: 11px; + white-space: nowrap; + background: #f0f3f7; + color: #8a99ac; +} +.difficulty-easy { + color: #49a389; + background: #eff9f5; +} +.difficulty-medium { + color: #c28e37; + background: #fff7e8; +} +.difficulty-hard { + color: #c86c85; + background: #fff0f4; +} +.verdict { + font: + 600 11px ui-monospace, + monospace; + padding: 4px 8px; + border-radius: 4px; + white-space: nowrap; +} +.verdict-ac { + color: #22a278; + background: #edf9f4; +} +.verdict-wa, +.verdict-re { + color: #dd7184; + background: #fff0f3; +} +.verdict-tle { + color: #c39737; + background: #fff8e7; +} +.verdict-ce { + color: #9879cc; + background: #f6f0fc; +} +.verdict-pending { + color: #8897ac; + background: #f0f4f9; +} +.filters { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + margin-bottom: 22px; +} +.filters .el-input { + width: 260px; +} +.filters .el-select { + width: 155px; +} +.filter-label { + font-size: 12px; + color: #8a99ad; +} +.pagination-row { + padding: 20px 0 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + font-size: 12px; + color: var(--muted); +} +.el-table { + --el-table-header-bg-color: #f8fafc; + --el-table-header-text-color: #7e8a9c; + --el-table-border-color: #edf0f5; + font-size: 12px; +} +.el-table th.el-table__cell { + font-weight: 500; +} +.el-table .el-table__cell { + padding: 12px 0; +} +.stack { + display: flex; + flex-direction: column; + gap: 20px; + min-width: 0; +} +.content-with-aside { + display: grid; + grid-template-columns: minmax(0, 1fr) 320px; + gap: 20px; +} +.summary-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; + margin-bottom: 20px; +} +.summary-card { + background: #fff; + border: 1px solid #e8edf4; + border-radius: 6px; + padding: 20px; +} +.summary-card p { + font-size: 12px; + color: var(--muted); +} +.summary-card strong { + display: block; + font-size: 27px; + font-weight: 600; + margin: 8px 0 2px; + letter-spacing: -0.5px; +} +.summary-card small { + font-size: 11px; + color: #9cabbc; +} +.avatar { + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border-radius: 50%; + color: #fff; + font-size: 12px; + flex-shrink: 0; +} +.user-inline { + display: flex; + align-items: center; + gap: 10px; +} +.number-medal { + display: inline-grid; + place-items: center; + width: 21px; + height: 25px; + color: #9aa5b5; + font-weight: 600; + font-size: 12px; +} +.number-medal.top-1 { + color: #e3983e; + background: #fff3df; +} +.number-medal.top-2 { + color: #8498b7; + background: #edf2f9; +} +.number-medal.top-3 { + color: #c58d74; + background: #f9efe8; +} +.empty-state { + text-align: center; + padding: 12px 0 24px; +} +.empty-state .el-empty { + padding: 20px; +} +.empty-state > p { + font-size: 13px; + margin-bottom: 14px; +} +.breadcrumbs { + margin: 0 0 20px; + font-size: 12px; + color: #95a1b1; + display: flex; + gap: 10px; + flex-wrap: wrap; +} +.breadcrumbs a { + color: #7299c2; +} +.intro-banner { + background: linear-gradient(115deg, #eaf4ff, #f5f8fd); + border: 1px solid #e2edfa; + border-radius: 6px; + padding: 30px; + margin-bottom: 24px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 24px; +} +.intro-banner h2 { + font-size: 25px; + margin: 8px 0 12px; + font-weight: 600; +} +.intro-banner p { + font-size: 13px; + color: #7a8eaa; + max-width: 540px; +} +.intro-number { + font-size: 65px; + color: #afceef; + font-family: Georgia, serif; +} +.card-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 20px; +} +.training-card { + padding: 26px; + background: #fff; + border: 1px solid #e8edf4; + border-radius: 6px; + display: flex; + gap: 22px; +} +.training-illustration { + width: 70px; + height: 78px; + border-radius: 12px; + display: grid; + place-items: center; + flex-shrink: 0; + font: + 600 27px ui-monospace, + monospace; + background: #ecf4ff; + color: #81b2ea; +} +.training-illustration.green { + background: #edf8f3; + color: #73b39f; +} +.training-illustration.violet { + background: #f3effb; + color: #a291cc; +} +.training-illustration.amber { + background: #fff7ea; + color: #d6b072; +} +.training-card h2 { + font-size: 17px; + margin: 10px 0; + color: #425874; +} +.training-card p { + font-size: 12px; + color: #92a0b2; + margin: 9px 0 18px; +} +.card-meta { + display: flex; + align-items: center; + gap: 15px; + font-size: 12px; + color: #92a0b2; +} +.card-meta .text-link { + margin-left: auto; +} +.detail-prose { font-size: 14px; - color: #65738b; + line-height: 1.9; + color: #566379; +} +.detail-prose h2 { + font-size: 17px; + color: #40536d; + margin: 24px 0 10px; + font-weight: 600; +} +.detail-prose h2:first-child { + margin-top: 0; +} +.detail-prose p { + white-space: pre-line; +} +.sample-block { + background: #f7f9fc; + border: 1px solid #e9eef5; + border-radius: 5px; + padding: 16px; + font: + 13px/1.8 ui-monospace, + monospace; + white-space: pre-wrap; + margin: 12px 0; +} +.detail-meta { + display: flex; + align-items: center; + gap: 18px; + flex-wrap: wrap; + font-size: 12px; + color: #92a0b2; + margin: 14px 0 20px; +} +.details-list { + display: flex; + flex-direction: column; + gap: 15px; + font-size: 13px; +} +.details-list div { + display: flex; + justify-content: space-between; + gap: 20px; +} +.details-list dt { + color: #91a0b2; +} +.details-list dd { margin: 0; + text-align: right; } -.app-footer { - padding: 24px 6vw; +.notice-strip { + padding: 12px 16px; + background: #f5f9ff; + border: 1px solid #e6f0fc; + border-radius: 5px; + font-size: 12px; + color: #7b94b2; + margin-bottom: 20px; + line-height: 1.7; } -@media (max-width: 600px) { - .header-caption { +.form-panel { + max-width: 860px; + margin: auto; +} +.form-panel .panel-body { + padding: 32px; +} +.form-help { + font-size: 12px; + color: #98a5b6; + margin: 5px 0 0; +} +.form-actions { + display: flex; + gap: 12px; + align-items: center; + margin-top: 28px; +} +.form-actions .muted { + font-size: 12px; +} +.tab-summary { + padding: 16px 0; + font-size: 13px; + color: var(--muted); +} +.about-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 18px; +} +.about-feature { + padding: 24px; + border: 1px solid #edf0f5; + border-radius: 6px; +} +.about-feature h3 { + font-size: 16px; + margin: 12px 0; +} +.about-feature p { + font-size: 13px; + color: #8c9db2; +} +.about-feature .el-icon { + font-size: 23px; + color: #7cacde; +} +.service-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; +} +.service-row h3 { + font-size: 15px; + margin-bottom: 8px; +} +.service-row p { + font-size: 12px; + color: #94a2b6; +} +.profile-banner { + background: #fff; + border: 1px solid #e8edf4; + border-radius: 6px; + padding: 36px; + display: flex; + align-items: center; + gap: 25px; + margin-bottom: 22px; +} +.profile-banner > .avatar { + width: 84px; + height: 84px; + font-size: 32px; +} +.profile-banner h1 { + font-size: 27px; + margin-bottom: 8px; +} +.profile-banner p { + color: #92a0b2; + font-size: 13px; +} +.profile-actions { + margin-left: auto; + display: flex; + gap: 10px; +} +.profile-tags { + margin-top: 12px; + display: flex; + gap: 7px; +} +.heatmap { + display: grid; + grid-template-columns: repeat(20, minmax(0, 1fr)); + gap: 5px; + padding: 15px 0; +} +.heat-cell { + aspect-ratio: 1; + border-radius: 3px; + background: #edf3fa; +} +.heat-cell[data-level='1'] { + background: #d6eaff; +} +.heat-cell[data-level='2'] { + background: #a3d1fc; +} +.heat-cell[data-level='3'] { + background: #6caff0; +} +.heat-cell[data-level='4'] { + background: #4094e4; +} +.skill-row { + margin: 17px 0; +} +.skill-row > div { + display: flex; + justify-content: space-between; + font-size: 12px; + margin-bottom: 8px; +} +.admin-shell { + display: grid; + grid-template-columns: 210px minmax(0, 1fr); + gap: 22px; +} +.admin-sidebar { + padding: 20px 12px; + align-self: start; +} +.admin-sidebar h2 { + font-size: 14px; + padding: 0 12px; + margin-bottom: 18px; +} +.admin-sidebar a { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + border-radius: 5px; + font-size: 13px; + color: #7b8da5; +} +.admin-sidebar a.active { + background: #eef6ff; + color: #409eff; +} +.admin-notice { + font-size: 12px; + color: #8fa0b6; + padding: 18px 12px 0; + border-top: 1px solid #edf0f5; + margin-top: 20px; + line-height: 1.7; +} +.status-dot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: #80b69a; + margin-right: 6px; +} +.status-dot.offline { + background: #c5cedb; +} +.home-grid { + display: grid; + grid-template-columns: minmax(0, 1.75fr) minmax(320px, 1fr); + gap: 20px; +} +.home-banner .panel-body { + padding: 16px; +} +.hero-image { + position: relative; + min-height: 300px; + overflow: hidden; + border-radius: 4px; + background: #e6f2ff; + isolation: isolate; +} +.hero-image > img { + position: absolute; + width: 100%; + height: 100%; + object-fit: cover; + inset: 0; + z-index: -1; +} +.hero-copy { + position: relative; + padding: 42px 36px; + max-width: 75%; +} +.hero-copy .eyebrow { + font-size: 10px !important; + color: #6b96c6; +} +.hero-copy h1 { + font-size: clamp(25px, 2.35vw, 38px); + font-weight: 600; + line-height: 1.4; + color: #335983; + letter-spacing: -1px; + margin: 12px 0; +} +.hero-copy p { + font-size: 13px; + color: #7093b7; + max-width: 320px; + margin-bottom: 23px; +} +.hero-controls { + display: flex; + justify-content: center; + gap: 7px; + padding: 12px 0 1px; +} +.hero-controls button { + width: 20px; + height: 3px; + padding: 0; + border: 0; + background: #d9e3ee; + border-radius: 2px; +} +.hero-controls button.active { + background: #409eff; +} +.home-rank .panel-body { + padding: 10px 20px; +} +.home-rank .el-table .el-table__cell { + padding: 8px 0; +} +.home-rank .avatar { + width: 23px; + height: 23px; + font-size: 10px; +} +.home-rank .number-medal { + height: 21px; + font-size: 11px; +} +.home-notice { + padding: 15px 4px; + display: flex; + gap: 15px; + border-bottom: 1px solid #edf0f5; +} +.home-notice:last-child { + border: 0; + padding-bottom: 2px; +} +.home-notice > div { + min-width: 0; +} +.home-notice-title { + display: block; + font-size: 13px; + margin: 0 0 5px; + font-weight: 500; + color: #4c6685; +} +.home-notice p { + font-size: 12px; + color: #98a5b7; +} +.home-notice time { + font-size: 11px; + color: #a0adbc; + white-space: nowrap; + margin-left: auto; +} +.notice-symbol { + background: #edf6ff; + width: 28px; + height: 28px; + border-radius: 5px; + display: grid; + place-items: center; + color: #66a8eb; + flex-shrink: 0; +} +.contest-mini { + display: flex; + gap: 11px; + padding: 13px 0; + border-bottom: 1px solid #edf0f5; + align-items: center; +} +.contest-mini:last-child { + border-bottom: 0; +} +.contest-mini strong { + display: block; + font-size: 12px; + color: #59708d; + font-weight: 500; +} +.contest-mini p { + font-size: 11px; + color: #9cacbf; + margin-top: 4px; +} +.contest-format { + font-size: 10px; + background: #eaf5ff; + color: #68a3de; + border-radius: 3px; + min-width: 31px; + text-align: center; + padding: 4px; +} +.contest-format.oi { + background: #eaf8f1; + color: #64b296; +} +.contest-status { + margin-left: auto; + font-size: 11px; + color: #cfad76; + white-space: nowrap; +} +.home-quick { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} +.home-quick a { + padding: 14px; + background: #f8fafc; + border: 1px solid #edf2f7; + border-radius: 5px; + font-size: 12px; + display: flex; + align-items: center; + gap: 9px; + color: #7b94b4; +} +.home-quick a:hover { + background: #eff6ff; + color: #409eff; +} +.auth-shell { + max-width: 1000px; + margin: 35px auto; + display: grid; + grid-template-columns: 1fr 1fr; + min-height: 540px; + background: #fff; + border: 1px solid #e5ecf4; + border-radius: 8px; + overflow: hidden; +} +.auth-art { + padding: 48px 40px; + background: #e9f3ff url('/images/home-banner.svg') center bottom/cover; +} +.auth-art h1 { + font-size: 33px; + color: #4f78a4; + margin: 35px 0 15px; +} +.auth-art p { + font-size: 14px; + color: #86a4c5; +} +.auth-form { + padding: 40px; +} +.auth-form h2 { + font-size: 25px; + margin-bottom: 8px; +} +.auth-form > p { + font-size: 12px; + color: #9caabc; + margin-bottom: 25px; +} +.auth-form .el-form-item { + margin-bottom: 21px; +} +.auth-form .el-button { + width: 100%; +} +.auth-links { + font-size: 12px; + display: flex; + justify-content: space-between; + margin-top: 20px; + color: #91a2b8; +} +.discussion-item { + display: flex; + align-items: center; + gap: 16px; + padding: 23px 0; + border-bottom: 1px solid #edf0f5; +} +.discussion-item:last-child { + border-bottom: 0; +} +.discussion-item .avatar { + width: 40px; + height: 40px; + font-size: 16px; +} +.discussion-item h2 { + font-size: 15px; + font-weight: 500; + margin-bottom: 7px; + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; +} +.discussion-item p { + display: flex; + gap: 12px; + font-size: 11px; + color: #9cabbc; +} +.discussion-numbers { + margin-left: auto; + font-size: 12px; + color: #9dabbc; + display: flex; + gap: 20px; + white-space: nowrap; +} +.discussion-numbers strong { + font-weight: 500; + color: #6a819e; +} +.comment-placeholder { + padding: 25px; + background: #f7f9fc; + border-radius: 5px; + margin: 20px 0 0; + font-size: 13px; + color: #94a4b9; + text-align: center; +} +.team-card { + padding: 24px; + display: flex; + gap: 20px; + align-items: start; +} +.team-logo { + width: 55px; + height: 55px; + border-radius: 10px; + background: #edf4ff; + color: #7faae0; + font: + 600 20px ui-monospace, + monospace; + display: grid; + place-items: center; +} +.team-card h2 { + font-size: 17px; + margin: 3px 0 8px; +} +.team-card p { + font-size: 12px; + color: #96a5b7; +} +.team-card .tag-list { + margin-top: 15px; +} +@media (max-width: 1280px) { + .header-inner { + gap: 16px; + } + .main-nav a { + padding: 0 9px; + gap: 5px; + } + .main-nav .el-icon { display: none; } - .welcome { - padding-top: 48px; + .preview-label { + display: none; } - .connection-row { - flex-wrap: wrap; + .home-grid { + grid-template-columns: minmax(0, 1.6fr) minmax(300px, 1fr); + } + .hero-copy { + padding: 36px 28px; + } + .hero-copy p { + max-width: 270px; + } + .card-grid { + gap: 16px; + } + .training-card { + padding: 20px; + gap: 16px; + } + .training-illustration { + width: 55px; + height: 65px; + } + .content-with-aside { + grid-template-columns: minmax(0, 1fr) 280px; + } +} +@media (max-width: 960px) { + .brand-name { + font-size: 13px; + } + .brand-mark { + font-size: 25px; + } + .header-inner { + gap: 12px; + } + .main-nav a { + font-size: 12px; + padding: 0 7px; + } + .header-account { + gap: 5px; + } + .hero-copy { + max-width: 100%; + padding: 30px 24px; + } + .home-grid { + grid-template-columns: minmax(0, 1.45fr) minmax(270px, 1fr); + gap: 16px; + } + .home-grid .panel-heading { + padding: 14px 16px; + } + .home-grid .panel-body { + padding: 16px; + } + .home-rank .panel-body { + padding: 8px 10px; + } + .contest-status { + display: none; + } + .content-with-aside, + .card-grid, + .about-grid { + grid-template-columns: 1fr; + } + .admin-shell { + grid-template-columns: 175px minmax(0, 1fr); + } + .summary-card { + padding: 16px; + } + .summary-card strong { + font-size: 24px; + } +} +@media (max-width: 760px) { + .app-main { + width: calc(100% - 28px); + margin-top: 18px; + } + .header-inner { + width: calc(100% - 28px); + flex-wrap: wrap; + gap: 12px; + min-height: 64px; + padding: 14px 0; + } + .brand-name small { + font-size: 7px; + } + .mobile-toggle { + display: flex; + align-items: center; + gap: 5px; + padding: 5px 7px; + background: none; + border: 1px solid #e6edf5; + border-radius: 4px; + font-size: 12px; + color: #7e93ad; + margin-left: auto; + } + .header-account { + margin-left: 0; + } + .main-nav { + display: none; + order: 4; + width: 100%; + flex-wrap: wrap; + gap: 4px; + padding-top: 5px; + } + .main-nav.mobile-open { + display: flex; + } + .main-nav a { + padding: 9px 13px; + flex: 1 0 auto; + justify-content: center; + border-radius: 4px; + border-bottom: 0; + } + .main-nav .el-icon { + display: inline-flex; + } + .home-grid { + grid-template-columns: minmax(0, 1fr); + } + .hero-copy h1 { + font-size: 32px; + } + .hero-copy { + padding: 32px; + } + .hero-copy p { + max-width: 270px; + } + .footer-inner { + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 20px; + } + .footer-brand { + grid-column: 1/-1; + } + .footer-bottom { + gap: 12px; + } + .page-heading { + align-items: flex-start; + flex-wrap: wrap; + gap: 12px; + } + .page-heading h1 { + font-size: 23px; + } + .filters .el-input { + width: 100%; + } + .filters .el-select { + flex: 1; + min-width: 130px; + width: auto; + } + .pagination-row { + flex-wrap: wrap; + justify-content: center; + } + .pagination-row .el-pagination { + flex-wrap: wrap; + justify-content: center; + gap: 8px; + } + .summary-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + } + .intro-banner { + padding: 24px; + } + .intro-number { + display: none; + } + .profile-banner { + padding: 24px; + gap: 16px; + flex-wrap: wrap; + } + .profile-banner > .avatar { + width: 60px; + height: 60px; + font-size: 24px; + } + .profile-actions { + margin-left: 0; + width: 100%; + } + .heatmap { + grid-template-columns: repeat(14, minmax(0, 1fr)); + } + .auth-shell { + grid-template-columns: 1fr; + margin: 15px auto; + } + .auth-art { + display: none; + } + .auth-form { + padding: 28px; + } + .admin-shell { + grid-template-columns: 1fr; + } + .admin-sidebar { + display: flex; + flex-wrap: wrap; + gap: 5px; + padding: 12px; + } + .admin-sidebar h2, + .admin-notice { + display: none; + } + .admin-sidebar a { + padding: 8px; + font-size: 12px; + } + .discussion-numbers { + display: none; + } + .form-panel .panel-body { + padding: 22px; + } + .service-row { + flex-wrap: wrap; + } + .panel-heading { + padding: 14px 16px; + } + .panel-body { + padding: 16px; + } + .details-list { + font-size: 12px; + } + .footer-inner, + .footer-bottom { + width: calc(100% - 40px); + } +} +@media (max-width: 420px) { + .brand-name { + font-size: 11px; + } + .brand-name small { + display: none; + } + .brand { + gap: 7px; + } + .header-inner { + gap: 7px; + } + .header-account .el-button { + padding: 7px 10px; + } + .mobile-toggle { + padding: 5px; + font-size: 11px; + } + .hero-copy { + padding: 30px 24px; + } + .hero-copy h1 { + font-size: 27px; + } + .hero-copy p { + max-width: 230px; + font-size: 12px; + } + .panel-heading h2 { + font-size: 14px; + } + .panel-heading small { + display: none; + } + .training-illustration { + display: none; + } + .discussion-item { + gap: 10px; + } + .discussion-item h2 { + font-size: 14px; + } + .discussion-item p { + gap: 7px; + flex-wrap: wrap; + } + .summary-card strong { + font-size: 22px; } } diff --git a/frontend/src/styles/blueprint.css b/frontend/src/styles/blueprint.css new file mode 100644 index 0000000..4101af6 --- /dev/null +++ b/frontend/src/styles/blueprint.css @@ -0,0 +1,492 @@ +:root { + --el-border-radius-base: 8px; +} +.app-header { + position: sticky; + top: 0; + z-index: 100; + box-shadow: none; +} +.header-inner { + min-height: 76px; + max-width: 1360px; +} +.brand-mark { + font-size: 25px; + padding: 10px 12px; + border-radius: 11px; + background: #eaf4ff; + letter-spacing: -1.5px; +} +.brand-dot { + color: #409eff; +} +.brand-name { + font-size: 13px; +} +.brand-name small { + letter-spacing: 1.3px; +} +.main-nav { + gap: 5px; + align-items: center; +} +.main-nav a { + align-self: center; + padding: 10px 13px; + border: 0; + border-radius: 8px; + font-size: 13px; +} +.main-nav a.active { + border: 0; + background: #edf6ff; + font-weight: 650; +} +.main-nav .el-icon { + display: none; +} +.header-account .el-button { + border-radius: 8px; + padding: 8px 14px; +} +.app-main { + width: min(100% - 64px, 1360px); + margin-top: 30px; + margin-bottom: 50px; +} +.app-footer { + background: transparent; + box-shadow: none; + padding: 38px 0 22px; +} +.footer-inner, +.footer-bottom { + max-width: 1360px; +} +.footer-brand .brand-mark { + display: inline-block; + background: #e3eefb; +} +.footer-inner h3 { + font-size: 12px; + color: #657e9e; +} +.panel { + border-radius: 16px; + box-shadow: none; + border: 1px solid #e1e8f1; +} +.panel-heading { + border: 0; + padding: 23px 24px 10px; + min-height: 68px; + align-items: flex-start; +} +.panel-heading h2 { + color: #455e80; + font-size: 17px; + font-weight: 650; + gap: 6px; + display: grid; +} +.panel-heading h2 > .el-icon { + display: none; +} +.panel-heading small { + margin: 0; + font-size: 10px; + letter-spacing: 0.3px; +} +.panel-body { + padding: 18px 24px 24px; +} +.panel-actions { + padding-top: 4px; + font-size: 11px; +} +.panel-actions a { + display: inline-flex; + align-items: center; + gap: 8px; +} +.page-heading { + margin: 8px 0 30px; + align-items: flex-end; +} +.page-heading h1 { + font-size: 34px; + letter-spacing: -1.2px; + color: #2e405b; + font-weight: 700; + margin: 10px 0 10px; +} +.page-heading .eyebrow { + color: #91a3bb; + font: + 10px ui-monospace, + monospace; + letter-spacing: 2px; +} +.page-heading p { + font-size: 13px; +} +.el-button { + border-radius: 8px; +} +.el-table { + --el-table-header-bg-color: #f3f7fc; + --el-table-row-hover-bg-color: #f4f9ff; + --el-fill-color-lighter: #fff; + font-size: 13px; + border-radius: 8px; +} +.el-table .el-table__cell { + padding: 16px 0; +} +.el-table th.el-table__cell { + padding: 13px 0; + font-size: 11px; + color: #8a99ac; + letter-spacing: 0.3px; +} +.el-table .cell { + padding: 0 14px; +} +.table-link { + font-weight: 600; +} +.filters { + padding: 16px; + background: #f6f9fd; + border-radius: 10px; + gap: 10px; + margin-bottom: 22px; +} +.filters .el-input { + width: 260px; +} +.pagination-row { + margin-top: 22px; + padding-top: 18px; + border-top: 1px solid var(--line); +} +.tag, +.difficulty, +.el-tag { + border-radius: 5px; +} +.summary-card { + border-radius: 14px; + position: relative; + padding: 24px; + border-bottom: 3px solid #dbeaff; +} +.summary-card strong { + font: + 650 32px ui-monospace, + monospace; + margin-top: 12px; +} +.intro-banner { + border-radius: 16px; + border-left: 4px solid #9cc8f5; + padding: 32px; +} +.training-card { + border-radius: 16px; + flex-direction: column; + gap: 10px; + position: relative; + padding: 26px; +} +.training-illustration { + width: 48px; + height: 48px; + border-radius: 12px; + font-size: 21px; +} +.training-card h2 { + font-size: 20px; + margin: 15px 0 10px; +} +.training-card p { + font-size: 13px; + line-height: 1.9; +} +.card-meta { + padding-top: 15px; + border-top: 1px solid var(--line); + flex-wrap: wrap; +} +.team-card { + border-radius: 16px; + flex-direction: column; +} +.team-logo { + width: 48px; + height: 48px; +} +.profile-banner { + border-radius: 18px; + border-left: 5px solid #a9cef5; +} +.profile-banner > .avatar { + border-radius: 22px; +} +.discussion-item { + padding: 24px 8px; +} +.discussion-item .avatar { + border-radius: 12px; +} +.discussion-item h2 { + font-size: 16px; + font-weight: 600; +} +.detail-prose h2 { + color: #455e80; + font-size: 18px; + padding-left: 12px; + border-left: 3px solid #b5d5f7; +} +.sample-block { + border-radius: 10px; + background: #f6f9fd; + border: 1px solid #e4edf7; +} +.auth-shell { + border-radius: 22px; + border: 1px solid #e1e8f1; + box-shadow: none; +} +.auth-art { + padding: 50px; + background-image: url('/images/practice-space.svg'); + background-size: 90%; + background-position: center bottom; + background-repeat: no-repeat; + min-height: 520px; +} +.auth-art h1 { + font-size: 33px; + max-width: 300px; + line-height: 1.5; +} +.about-feature { + border-radius: 14px; + padding: 26px; +} +#tags { + scroll-margin-top: 98px; +} +.admin-application { + min-height: 100dvh; +} +.admin-app-header { + min-height: 80px; + display: flex; + align-items: center; + gap: 26px; + padding: 0 30px; + background: #fff; + border-bottom: 1px solid #e1e8f1; + position: sticky; + top: 0; + z-index: 100; +} +.admin-app-label { + font-size: 11px; + color: #91a3bb; + padding-left: 26px; + border-left: 1px solid #e1e8f1; +} +.admin-return { + font-size: 12px; + color: #657e9e; + display: flex; + align-items: center; + gap: 10px; + margin-left: auto; +} +.admin-app-nav { + position: fixed; + left: 0; + top: 80px; + bottom: 0; + width: 210px; + background: #fff; + border-right: 1px solid #e1e8f1; + padding: 30px 18px; + display: flex; + flex-direction: column; + gap: 7px; +} +.admin-app-nav a { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 18px; + font-size: 13px; + color: #7b8da5; + border-radius: 9px; +} +.admin-app-nav a.active { + background: #edf6ff; + color: #409eff; + font-weight: 600; +} +.admin-app-main { + margin: 32px 32px 48px 242px; + min-width: 0; +} +.admin-app-main .admin-shell { + display: block; +} +.admin-app-footer { + margin-left: 242px; + padding: 22px 32px 28px 0; + border-top: 1px solid #e1e8f1; + display: flex; + gap: 12px; + justify-content: space-between; + flex-wrap: wrap; + color: #9aa5b5; + font-size: 10px; +} +@media (min-width: 1200px) { + .card-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} +@media (max-width: 1100px) { + .header-inner { + gap: 18px; + } + .main-nav a { + padding: 10px; + } + .brand-name { + display: none; + } + .preview-label { + display: none; + } +} +@media (max-width: 960px) { + .panel-body { + padding: 16px 20px 22px; + } + .panel-heading { + padding: 22px 20px 8px; + } + .page-heading h1 { + font-size: 30px; + } + .admin-app-label { + display: none; + } +} +@media (max-width: 760px) { + .header-inner { + min-height: 66px; + padding: 10px 0; + } + .brand-name { + display: block; + } + .brand-mark { + font-size: 20px; + padding: 9px 10px; + } + .main-nav a { + padding: 11px 15px; + } + .main-nav .el-icon { + display: none; + } + .header-account .el-button { + padding: 8px 11px; + } + .mobile-toggle { + border-radius: 7px; + padding: 7px; + } + .app-main { + width: calc(100% - 28px); + margin-top: 22px; + } + .page-heading { + align-items: flex-start; + } + .page-heading h1 { + font-size: 28px; + } + .panel-heading { + padding: 20px 18px 8px; + min-height: 65px; + } + .panel-heading h2 { + font-size: 16px; + } + .panel-heading small { + display: block; + } + .panel-body { + padding: 16px 18px 20px; + } + .filters { + padding: 12px; + } + .filters .el-input { + width: 100%; + } + .training-card, + .team-card { + padding: 22px; + } + .training-illustration { + display: grid; + } + .summary-card { + padding: 20px; + } + .summary-card strong { + font-size: 26px; + } + .admin-app-header { + min-height: 70px; + padding: 0 18px; + } + .admin-app-header .brand-name { + font-size: 12px; + } + .admin-app-nav { + width: 62px; + top: 70px; + padding: 20px 7px; + } + .admin-app-nav a { + padding: 14px 15px; + } + .admin-app-nav a span { + display: none; + } + .admin-app-main { + margin: 24px 14px 35px 76px; + } + .admin-app-footer { + margin-left: 76px; + padding-right: 14px; + } + .admin-return { + font-size: 11px; + } + .auth-art { + min-height: 0; + } + .footer-inner, + .footer-bottom { + width: calc(100% - 36px); + } +}
{{ description }}
{{ eyebrow }}
+ {{ + selected + ? selected.day + ' · 总提交 ' + selected.total + ' 次 · 通过 ' + selected.accepted + ' 次' + : '近 7 天的提交与通过情况' + }} +