- 所有页面共用顶栏(教程 / 挑战 / 自由创作 / 作品展,右边“教学”和用户菜单); 没登录时顶栏有“登录”按钮,关掉登录框不再白屏;/submission/:id 全屏页不带顶栏 - 后台改成“教学”侧栏,分出题 / 班级 / 管理三组;“全部提交”留在侧栏里打开; admin 也能进作品展管理(后端本来就允许) - 路由按角色守卫:meta.role 为 staff / super,学生和 admin 进不了无权页面 - 配色只在 theme.ts 定义,同时生成 --wp-* CSS 变量和 Naive UI 主题,暗色值先定好 - 图标改用打包进来的 lucide(断网也能显示),去掉 @iconify/vue 和 PUBLIC_ICONIFY_URL; 代码字体换成打包的 JetBrains Mono,编辑器默认字号 24 → 16 - 教程面板去掉和顶栏重复的切换和图标按钮,图标按钮都补上文字或提示 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e47fac1732
commit
ff1eb2982e
43 files changed
+837
-429
No files matched your search
@@ -3,31 +3,33 @@
|
||||
<div class="challenge-sider">
|
||||
<n-tabs v-model:value="activeTab" type="line" class="left-tabs">
|
||||
<template #prefix>
|
||||
<n-button text @click="back" style="margin: 0 8px">
|
||||
<Icon :width="20" icon="pepicons-pencil:arrow-left" />
|
||||
</n-button>
|
||||
<n-tooltip>
|
||||
<template #trigger>
|
||||
<n-button text @click="back" style="margin: 0 8px">
|
||||
<Icon :width="18" icon="lucide:arrow-left" />
|
||||
</n-button>
|
||||
</template>
|
||||
返回挑战列表
|
||||
</n-tooltip>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<n-flex style="margin: 0 8px">
|
||||
<n-button
|
||||
v-if="roleAdmin || roleSuper"
|
||||
text
|
||||
@click="showStats = true"
|
||||
>
|
||||
<Icon :width="16" icon="lucide:bar-chart-2" />
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="authed"
|
||||
text
|
||||
@click="
|
||||
$router.push({ name: 'submissions', params: { page: 1 } })
|
||||
"
|
||||
>
|
||||
<Icon :width="16" icon="lucide:list" />
|
||||
</n-button>
|
||||
<n-button v-if="roleSuper" text @click="edit">
|
||||
<Icon :width="16" icon="lucide:edit" />
|
||||
</n-button>
|
||||
<n-tooltip v-if="roleAdmin || roleSuper">
|
||||
<template #trigger>
|
||||
<n-button text @click="showStats = true">
|
||||
<Icon :width="16" icon="lucide:bar-chart-2" />
|
||||
</n-button>
|
||||
</template>
|
||||
本题统计
|
||||
</n-tooltip>
|
||||
<n-tooltip v-if="roleSuper">
|
||||
<template #trigger>
|
||||
<n-button text @click="edit">
|
||||
<Icon :width="16" icon="lucide:edit" />
|
||||
</n-button>
|
||||
</template>
|
||||
编辑这道题
|
||||
</n-tooltip>
|
||||
</n-flex>
|
||||
</template>
|
||||
<n-tab-pane name="desc" tab="挑战描述" display-directive="show">
|
||||
@@ -141,7 +143,7 @@
|
||||
import { ref, watch, onMounted, onUnmounted, computed, useTemplateRef } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { useMessage } from "naive-ui"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Icon } from "../icons"
|
||||
import { marked, type MarkedOptions } from "marked"
|
||||
import copyFn from "copy-text-to-clipboard"
|
||||
import PromptPanel from "../components/ai/PromptPanel.vue"
|
||||
@@ -312,7 +314,7 @@ onUnmounted(() => {
|
||||
<style scoped>
|
||||
.challenge-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -396,7 +398,7 @@ onUnmounted(() => {
|
||||
.challenge-sider .codeblock-wrapper pre code {
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
font-family: Monaco, monospace;
|
||||
font-family: var(--wp-font-mono);
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
@@ -426,7 +428,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.challenge-sider .action-btn:hover {
|
||||
border-color: #18a058;
|
||||
color: #18a058;
|
||||
border-color: var(--wp-brand);
|
||||
color: var(--wp-brand);
|
||||
}
|
||||
</style>
|
||||
@@ -26,8 +26,8 @@
|
||||
width="24"
|
||||
:icon="
|
||||
item.is_public
|
||||
? 'twemoji:check-mark-button'
|
||||
: 'twemoji:locked'
|
||||
? 'lucide:globe'
|
||||
: 'lucide:lock'
|
||||
"
|
||||
></Icon>
|
||||
</n-button>
|
||||
@@ -97,7 +97,7 @@
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<MarkdownEditor
|
||||
style="height: calc(100vh - 100px)"
|
||||
style="height: calc(var(--wp-main-h) - 100px)"
|
||||
v-model="challenge.content"
|
||||
/>
|
||||
</n-flex>
|
||||
@@ -132,7 +132,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Icon } from "../icons"
|
||||
import { Challenge } from "../api"
|
||||
import { roleSuper } from "../store/user"
|
||||
import type { ChallengeSlim } from "../utils/type"
|
||||
@@ -296,7 +296,7 @@ onMounted(getContent)
|
||||
<style scoped>
|
||||
.list {
|
||||
overflow: auto;
|
||||
height: calc(100vh - 20px);
|
||||
height: calc(var(--wp-main-h) - 20px);
|
||||
}
|
||||
|
||||
.col {
|
||||
@@ -308,6 +308,6 @@ onMounted(getContent)
|
||||
}
|
||||
|
||||
.editor {
|
||||
height: calc(100vh - 200px);
|
||||
height: calc(var(--wp-main-h) - 200px);
|
||||
}
|
||||
</style>
|
||||
@@ -3,9 +3,14 @@
|
||||
<div class="creation-sider">
|
||||
<n-tabs v-model:value="activeTab" type="line" class="left-tabs">
|
||||
<template #prefix>
|
||||
<n-button text @click="back" style="margin: 0 8px">
|
||||
<Icon :width="20" icon="pepicons-pencil:arrow-left" />
|
||||
</n-button>
|
||||
<n-tooltip>
|
||||
<template #trigger>
|
||||
<n-button text @click="back" style="margin: 0 8px">
|
||||
<Icon :width="18" icon="lucide:arrow-left" />
|
||||
</n-button>
|
||||
</template>
|
||||
返回我的创作
|
||||
</n-tooltip>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<n-flex style="margin: 0 8px">
|
||||
@@ -112,7 +117,7 @@
|
||||
import { onMounted, onUnmounted, ref, watch } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { useMessage } from "naive-ui"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Icon } from "../icons"
|
||||
import PromptPanel from "../components/ai/PromptPanel.vue"
|
||||
import PromptHistoryPanel from "../components/ai/PromptHistoryPanel.vue"
|
||||
import Preview from "../components/editor/Preview.vue"
|
||||
@@ -229,7 +234,7 @@ onUnmounted(() => {
|
||||
<style scoped>
|
||||
.creation-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,7 @@
|
||||
style="margin-bottom: 32px"
|
||||
>
|
||||
<div>
|
||||
<n-flex align="center" :size="8">
|
||||
<n-button text @click="$router.push({ name: 'home' })">
|
||||
<Icon :width="20" icon="pepicons-pencil:arrow-left" />
|
||||
</n-button>
|
||||
<n-h2 style="margin: 0">自由创作</n-h2>
|
||||
</n-flex>
|
||||
<n-h2 style="margin: 0">自由创作</n-h2>
|
||||
<n-text depth="3">自己定一个主题,和 AI 一起把它做出来</n-text>
|
||||
</div>
|
||||
<n-button type="primary" @click="openCreate">
|
||||
@@ -106,7 +101,7 @@
|
||||
import { onMounted, reactive, ref } from "vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import { useDialog, useMessage } from "naive-ui"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Icon } from "../icons"
|
||||
import { Creation } from "../api"
|
||||
import type { CreationSlim } from "../utils/type"
|
||||
|
||||
|
||||
+106
-81
@@ -1,126 +1,151 @@
|
||||
<template>
|
||||
<n-flex class="container" :wrap="false">
|
||||
<div class="sidebar">
|
||||
<div class="back-btn" @click="() => goHome($router, taskTab, step)">
|
||||
← 返回
|
||||
</div>
|
||||
<n-divider style="margin: 8px 0" />
|
||||
<div
|
||||
v-for="item in menu"
|
||||
:key="item.label"
|
||||
:class="['nav-item', { active: $route.name === item.route.name }]"
|
||||
@click="$router.push(item.route)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="teach">
|
||||
<nav class="sidebar">
|
||||
<template v-for="group in groups" :key="group.label">
|
||||
<div class="group-label">{{ group.label }}</div>
|
||||
<router-link
|
||||
v-for="item in group.items"
|
||||
:key="item.label"
|
||||
:to="item.route"
|
||||
:class="['nav-item', { active: item.names.includes(String($route.name)) }]"
|
||||
>
|
||||
<Icon :icon="item.icon" :width="16" />
|
||||
<span>{{ item.label }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
</nav>
|
||||
<div class="content">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</n-flex>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import { taskTab } from "../store/task"
|
||||
import { Icon } from "../icons"
|
||||
import { step } from "../store/tutorial"
|
||||
import { roleAdmin, roleSuper } from "../store/user"
|
||||
import { goHome } from "../utils/helper"
|
||||
import { roleSuper } from "../store/user"
|
||||
|
||||
const menu = computed(() =>
|
||||
// admin 只能出挑战、看提交、管作品展;其余是超管的(和后端权限一致)
|
||||
const groups = computed(() =>
|
||||
[
|
||||
{
|
||||
label: "教程",
|
||||
route: { name: "tutorial-editor", params: { display: step.value } },
|
||||
show: roleSuper.value,
|
||||
label: "出题",
|
||||
items: [
|
||||
{
|
||||
label: "教程",
|
||||
icon: "lucide:book-open",
|
||||
route: { name: "tutorial-editor", params: { display: step.value } },
|
||||
names: ["tutorial-editor"],
|
||||
show: roleSuper.value,
|
||||
},
|
||||
{
|
||||
label: "挑战",
|
||||
icon: "lucide:swords",
|
||||
route: { name: "challenge-editor", params: { display: 0 } },
|
||||
names: ["challenge-editor"],
|
||||
show: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "挑战",
|
||||
route: { name: "challenge-editor", params: { display: 0 } },
|
||||
show: roleAdmin.value || roleSuper.value,
|
||||
label: "班级",
|
||||
items: [
|
||||
{
|
||||
label: "成绩册",
|
||||
icon: "lucide:graduation-cap",
|
||||
route: { name: "gradebook" },
|
||||
names: ["gradebook"],
|
||||
show: roleSuper.value,
|
||||
},
|
||||
{
|
||||
label: "全部提交",
|
||||
icon: "lucide:list",
|
||||
route: { name: "teach-submissions", params: { page: 1 } },
|
||||
names: ["teach-submissions"],
|
||||
show: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "用户",
|
||||
route: { name: "user-manage", params: { page: 1 } },
|
||||
show: roleSuper.value,
|
||||
label: "管理",
|
||||
items: [
|
||||
{
|
||||
label: "作品展",
|
||||
icon: "lucide:award",
|
||||
route: { name: "showcase-manage" },
|
||||
names: ["showcase-manage"],
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
label: "用户",
|
||||
icon: "lucide:users",
|
||||
route: { name: "user-manage", params: { page: 1 } },
|
||||
names: ["user-manage"],
|
||||
show: roleSuper.value,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "工坊",
|
||||
route: { name: "showcase-manage" },
|
||||
show: roleSuper.value,
|
||||
},
|
||||
{
|
||||
label: "成绩",
|
||||
route: { name: "gradebook" },
|
||||
show: roleSuper.value,
|
||||
},
|
||||
{
|
||||
label: "提交",
|
||||
route: { name: "submissions", params: { page: 1 } },
|
||||
show: roleAdmin.value || roleSuper.value,
|
||||
},
|
||||
].filter((item) => item.show),
|
||||
]
|
||||
.map((g) => ({ ...g, items: g.items.filter((i) => i.show) }))
|
||||
.filter((g) => g.items.length),
|
||||
)
|
||||
</script>
|
||||
<style scoped>
|
||||
.container {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
box-sizing: border-box;
|
||||
.teach {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 110px;
|
||||
min-width: 110px;
|
||||
padding: 12px 8px;
|
||||
border-right: 1px solid #efeff5;
|
||||
background: #fafafa;
|
||||
width: 148px;
|
||||
flex: none;
|
||||
padding: 8px;
|
||||
border-right: 1px solid var(--wp-line);
|
||||
background: var(--wp-surface);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
padding: 7px 10px;
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition:
|
||||
background-color 0.15s,
|
||||
color 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background-color: #f0f0f0;
|
||||
color: #444;
|
||||
.group-label {
|
||||
padding: 12px 10px 4px;
|
||||
font-size: 12px;
|
||||
color: var(--wp-faint);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
color: #444;
|
||||
transition:
|
||||
background-color 0.15s,
|
||||
color 0.15s;
|
||||
user-select: none;
|
||||
color: var(--wp-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background-color: #f0f0f0;
|
||||
background: var(--wp-paper);
|
||||
color: var(--wp-ink);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background-color: #e8f8f0;
|
||||
color: #18a058;
|
||||
font-weight: 500;
|
||||
background: var(--wp-brand-soft);
|
||||
color: var(--wp-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-item:focus-visible {
|
||||
outline: 2px solid var(--wp-brand);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
background: var(--wp-surface);
|
||||
}
|
||||
</style>
|
||||
@@ -87,7 +87,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref, watch } from "vue"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Icon } from "../icons"
|
||||
import { watchDebounced } from "@vueuse/core"
|
||||
import {
|
||||
NButton,
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
import { storageShim } from "../utils/previewStorage"
|
||||
import { onMounted, ref } from "vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Icon } from "../icons"
|
||||
import { Showcase } from "../api"
|
||||
import type { AwardSection, ShowcaseItem } from "../utils/type"
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ import {
|
||||
useMessage,
|
||||
type DataTableColumn,
|
||||
} from "naive-ui"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Icon } from "../icons"
|
||||
import { Showcase } from "../api"
|
||||
import type {
|
||||
AwardItemManageOut,
|
||||
@@ -640,8 +640,8 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.award-row.active {
|
||||
background: #e8f8f0;
|
||||
color: #18a058;
|
||||
background: var(--wp-brand-soft);
|
||||
color: var(--wp-brand);
|
||||
}
|
||||
|
||||
:deep(.table-number-input) {
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, onUnmounted, reactive, ref, watch } from "vue"
|
||||
import { NButton, NDataTable, NTag, useMessage, type DataTableColumn } from "naive-ui"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Icon } from "../icons"
|
||||
import { Submission } from "../api"
|
||||
import type { SubmissionOut, FlagType } from "../utils/type"
|
||||
import { parseTime } from "../utils/helper"
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
width="24"
|
||||
:icon="
|
||||
item.is_public
|
||||
? 'twemoji:check-mark-button'
|
||||
: 'twemoji:locked'
|
||||
? 'lucide:globe'
|
||||
: 'lucide:lock'
|
||||
"
|
||||
></Icon>
|
||||
</n-button>
|
||||
@@ -76,7 +76,7 @@
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<MarkdownEditor
|
||||
style="height: calc(100vh - 100px)"
|
||||
style="height: calc(var(--wp-main-h) - 100px)"
|
||||
v-model="tutorial.content"
|
||||
/>
|
||||
</n-flex>
|
||||
@@ -105,7 +105,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Icon } from "../icons"
|
||||
import { Tutorial } from "../api"
|
||||
import type { TutorialSlim } from "../utils/type"
|
||||
import { useDialog, useMessage } from "naive-ui"
|
||||
@@ -258,7 +258,7 @@ onMounted(getContent)
|
||||
<style scoped>
|
||||
.list {
|
||||
overflow: auto;
|
||||
height: calc(100vh - 20px);
|
||||
height: calc(var(--wp-main-h) - 20px);
|
||||
}
|
||||
|
||||
.col {
|
||||
@@ -270,6 +270,6 @@ onMounted(getContent)
|
||||
}
|
||||
|
||||
.editor {
|
||||
height: calc(100vh - 200px);
|
||||
height: calc(var(--wp-main-h) - 200px);
|
||||
}
|
||||
</style>
|
||||
@@ -198,7 +198,7 @@ onMounted(init)
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 10px 10px 10px 0;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user