- 所有页面共用顶栏(教程 / 挑战 / 自由创作 / 作品展,右边“教学”和用户菜单); 没登录时顶栏有“登录”按钮,关掉登录框不再白屏;/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
@@ -84,7 +84,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick } from "vue"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Icon } from "../../icons"
|
||||
import { renderMarkdown } from "../../utils/markdown"
|
||||
import {
|
||||
messages,
|
||||
@@ -168,7 +168,7 @@ watch([() => messages.value.length, streamingContent], () => {
|
||||
}
|
||||
|
||||
.guidance-msg.assistant .msg-role {
|
||||
color: #18a058;
|
||||
color: var(--wp-brand);
|
||||
}
|
||||
|
||||
.guidance-msg.assistant .msg-content {
|
||||
@@ -194,7 +194,7 @@ watch([() => messages.value.length, streamingContent], () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #18a058;
|
||||
color: var(--wp-brand);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
padding: 6px 10px;
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from "vue"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Icon } from "../../icons"
|
||||
import { useMessage } from "naive-ui"
|
||||
import { renderMarkdown } from "../../utils/markdown"
|
||||
import { Prompt } from "../../api"
|
||||
@@ -307,7 +307,7 @@ onMounted(() => {
|
||||
|
||||
.history-card.is-selected {
|
||||
--n-color: #f7fffa;
|
||||
box-shadow: 0 10px 24px rgba(24, 160, 88, 0.14);
|
||||
box-shadow: 0 10px 24px rgba(13, 122, 82, 0.14);
|
||||
}
|
||||
|
||||
.history-card.is-selected .history-main {
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
import { ref, watch, nextTick, computed } from "vue"
|
||||
import { useStorage } from "@vueuse/core"
|
||||
import { useMessage } from "naive-ui"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Icon } from "../../icons"
|
||||
import GuidancePanel from "./GuidancePanel.vue"
|
||||
import { openGuidance } from "../../store/guidance"
|
||||
import {
|
||||
@@ -289,7 +289,7 @@ watch([() => messages.value.length, streamingContent], () => {
|
||||
}
|
||||
|
||||
.message.assistant .message-role {
|
||||
color: #18a058;
|
||||
color: var(--wp-brand);
|
||||
}
|
||||
|
||||
.message-content {
|
||||
@@ -329,7 +329,7 @@ watch([() => messages.value.length, streamingContent], () => {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #18a058;
|
||||
background: var(--wp-brand);
|
||||
animation: bounce 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,20 +8,20 @@ import { computed } from "vue"
|
||||
|
||||
const styleTheme = EditorView.baseTheme({
|
||||
"& .cm-scroller": {
|
||||
"font-family": "Monaco",
|
||||
"font-family": "var(--wp-font-mono)",
|
||||
},
|
||||
"&.cm-editor.cm-focused": {
|
||||
outline: "none",
|
||||
},
|
||||
"&.cm-editor .cm-tooltip.cm-tooltip-autocomplete ul": {
|
||||
"font-family": "Monaco",
|
||||
"font-family": "var(--wp-font-mono)",
|
||||
},
|
||||
})
|
||||
|
||||
const bgColors: Record<string, string> = {
|
||||
html: "#fff6f3",
|
||||
css: "#f3f6ff",
|
||||
js: "#fffdf0",
|
||||
html: "var(--wp-html-soft)",
|
||||
css: "var(--wp-css-soft)",
|
||||
js: "var(--wp-js-soft)",
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -31,7 +31,7 @@ interface Props {
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
language: "html",
|
||||
fontSize: 20,
|
||||
fontSize: 16,
|
||||
})
|
||||
|
||||
const code = defineModel<string>("value")
|
||||
@@ -43,7 +43,7 @@ const lang = computed(() => {
|
||||
})
|
||||
|
||||
const bgTheme = computed(() => {
|
||||
const bg = bgColors[props.language] ?? "#ffffff"
|
||||
const bg = bgColors[props.language] ?? "var(--wp-surface)"
|
||||
return EditorView.theme({
|
||||
"&": { backgroundColor: bg },
|
||||
".cm-gutters": { backgroundColor: bg },
|
||||
|
||||
@@ -10,37 +10,25 @@
|
||||
>
|
||||
<n-tab-pane name="html" tab="HTML">
|
||||
<template #tab>
|
||||
<n-flex align="center">
|
||||
<Icon :width="20" icon="skill-icons:html"></Icon>
|
||||
<span>HTML</span>
|
||||
</n-flex>
|
||||
<span class="lang-tab lang-html">HTML</span>
|
||||
</template>
|
||||
<Editor v-model:value="html" :font-size="size" language="html" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="css" tab="CSS">
|
||||
<template #tab>
|
||||
<n-flex align="center">
|
||||
<Icon :width="20" icon="skill-icons:css"></Icon>
|
||||
<span>CSS</span>
|
||||
</n-flex>
|
||||
<span class="lang-tab lang-css">CSS</span>
|
||||
</template>
|
||||
<Editor v-model:value="css" :font-size="size" language="css" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="js" tab="JS">
|
||||
<template #tab>
|
||||
<n-flex align="center">
|
||||
<Icon :width="20" icon="skill-icons:javascript"></Icon>
|
||||
<span>JS</span>
|
||||
</n-flex>
|
||||
<span class="lang-tab lang-js">JS</span>
|
||||
</template>
|
||||
<Editor v-model:value="js" :font-size="size" language="js" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="actions" tab="选项">
|
||||
<template #tab>
|
||||
<n-flex align="center">
|
||||
<Icon :width="20" icon="skill-icons:actix-dark"></Icon>
|
||||
<span>选项</span>
|
||||
</n-flex>
|
||||
<span class="lang-tab">选项</span>
|
||||
</template>
|
||||
<n-flex class="wrapper" vertical>
|
||||
<n-flex align="center">
|
||||
@@ -53,7 +41,7 @@
|
||||
<span class="label">字号</span>
|
||||
<n-flex align="center">
|
||||
<span :style="{ 'font-size': size + 'px' }">{{ size }}</span>
|
||||
<n-button :disabled="size === 20" @click="changeSize(size - 2)">
|
||||
<n-button :disabled="size <= 12" @click="changeSize(size - 2)">
|
||||
调小
|
||||
</n-button>
|
||||
<n-button :disabled="size === 40" @click="changeSize(size + 2)">
|
||||
@@ -78,7 +66,6 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from "@iconify/vue"
|
||||
import Editor from "./Editor.vue"
|
||||
import Toolbar from "./Toolbar.vue"
|
||||
import { html, css, js, tab, size, reset } from "../../store/editors"
|
||||
@@ -193,14 +180,50 @@ async function formatAndSubmit() {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* HTML / CSS / JS 各有固定颜色,见 theme.ts */
|
||||
.lang-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: var(--wp-font-mono);
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
.lang-tab.lang-html::before,
|
||||
.lang-tab.lang-css::before,
|
||||
.lang-tab.lang-js::before {
|
||||
content: "";
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.lang-html {
|
||||
color: var(--wp-html);
|
||||
}
|
||||
.lang-html::before {
|
||||
background: var(--wp-html);
|
||||
}
|
||||
.lang-css {
|
||||
color: var(--wp-css);
|
||||
}
|
||||
.lang-css::before {
|
||||
background: var(--wp-css);
|
||||
}
|
||||
.lang-js {
|
||||
color: var(--wp-js);
|
||||
}
|
||||
.lang-js::before {
|
||||
background: var(--wp-js);
|
||||
}
|
||||
|
||||
:deep(.n-tabs-nav .n-tabs-tab[data-name="html"]) {
|
||||
background-color: #fff6f3 !important;
|
||||
background-color: var(--wp-html-soft) !important;
|
||||
}
|
||||
:deep(.n-tabs-nav .n-tabs-tab[data-name="css"]) {
|
||||
background-color: #f3f6ff !important;
|
||||
background-color: var(--wp-css-soft) !important;
|
||||
}
|
||||
:deep(.n-tabs-nav .n-tabs-tab[data-name="js"]) {
|
||||
background-color: #fffdf0 !important;
|
||||
background-color: var(--wp-js-soft) !important;
|
||||
}
|
||||
|
||||
.editors-root {
|
||||
@@ -208,12 +231,12 @@ async function formatAndSubmit() {
|
||||
}
|
||||
|
||||
:deep(.tab-active-html .n-tab-pane) {
|
||||
background-color: #fff6f3;
|
||||
background-color: var(--wp-html-soft);
|
||||
}
|
||||
:deep(.tab-active-css .n-tab-pane) {
|
||||
background-color: #f3f6ff;
|
||||
background-color: var(--wp-css-soft);
|
||||
}
|
||||
:deep(.tab-active-js .n-tab-pane) {
|
||||
background-color: #fffdf0;
|
||||
background-color: var(--wp-js-soft);
|
||||
}
|
||||
</style>
|
||||
@@ -57,7 +57,7 @@ import { useRouter } from "vue-router"
|
||||
import { Submission } from "../../api"
|
||||
import { submission } from "../../store/submission"
|
||||
import { useMessage } from "naive-ui"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Icon } from "../../icons"
|
||||
import copy from "copy-text-to-clipboard"
|
||||
import {
|
||||
buildPreviewDocument,
|
||||
|
||||
@@ -1,43 +1,31 @@
|
||||
<template>
|
||||
<n-flex align="center" class="corner">
|
||||
<n-button quaternary v-if="!show" @click="showTutorial"> 教程 </n-button>
|
||||
<n-button quaternary v-if="!show" @click="showTutorial">显示教程</n-button>
|
||||
<template v-if="user.loaded && authed">
|
||||
<n-button quaternary @click="emit('format')">整理</n-button>
|
||||
<n-tooltip>
|
||||
<template #trigger>
|
||||
<n-button quaternary @click="emit('format')">整理代码</n-button>
|
||||
</template>
|
||||
自动整理缩进和换行
|
||||
</n-tooltip>
|
||||
<n-button
|
||||
type="primary"
|
||||
secondary
|
||||
:disabled="submitDisabled"
|
||||
:loading="submitLoading"
|
||||
@click="emit('submit')"
|
||||
>
|
||||
提交
|
||||
</n-button>
|
||||
<n-dropdown :options="menu" @select="clickMenu">
|
||||
<n-button>{{ user.username }}</n-button>
|
||||
</n-dropdown>
|
||||
</template>
|
||||
<n-button
|
||||
v-if="user.loaded && !authed"
|
||||
@click="handleLogin"
|
||||
secondary
|
||||
type="primary"
|
||||
>
|
||||
登录
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, h } from "vue"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { authed, roleAdmin, roleNormal, user } from "../../store/user"
|
||||
import { loginModal } from "../../store/modal"
|
||||
import { computed } from "vue"
|
||||
import { authed, user } from "../../store/user"
|
||||
import { show, panelSize } from "../../store/panel"
|
||||
import { taskId } from "../../store/task"
|
||||
import { Account } from "../../api"
|
||||
import { Role } from "../../utils/type"
|
||||
import { router } from "../../router"
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
submitLoading: boolean
|
||||
}>()
|
||||
const emit = defineEmits(["format", "submit"])
|
||||
@@ -46,78 +34,13 @@ const submitDisabled = computed(() => {
|
||||
return taskId.value === 0
|
||||
})
|
||||
|
||||
const menu = computed(() => [
|
||||
{
|
||||
label: "后台管理",
|
||||
key: "dashboard",
|
||||
show: !roleNormal.value,
|
||||
icon: () =>
|
||||
h(Icon, {
|
||||
icon: "streamline-emojis:robot-face-1",
|
||||
width: 20,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "我的提交",
|
||||
key: "submissions",
|
||||
icon: () =>
|
||||
h(Icon, {
|
||||
icon: "streamline-emojis:bar-chart",
|
||||
width: 20,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "退出账号",
|
||||
key: "logout",
|
||||
icon: () =>
|
||||
h(Icon, {
|
||||
icon: "streamline-emojis:hot-beverage-2",
|
||||
width: 20,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
function showTutorial() {
|
||||
show.value = true
|
||||
panelSize.value = 2 / 5
|
||||
}
|
||||
|
||||
function clickMenu(name: string) {
|
||||
switch (name) {
|
||||
case "dashboard": {
|
||||
const route = roleAdmin.value ? "challenge-editor" : "tutorial-editor"
|
||||
router.push({ name: route, params: { display: 0 } })
|
||||
break
|
||||
}
|
||||
case "submissions":
|
||||
router.push({
|
||||
name: "submissions",
|
||||
params: { page: 1 },
|
||||
query: { username: user.username },
|
||||
})
|
||||
break
|
||||
case "logout":
|
||||
handleLogout()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogin() {
|
||||
loginModal.value = true
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
// token 已经先清掉了,退出请求失败(比如断网)界面也要显示为已退出
|
||||
try {
|
||||
await Account.logout()
|
||||
} finally {
|
||||
user.username = ""
|
||||
user.role = Role.Normal
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.corner {
|
||||
margin-right: 20px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<div class="shell">
|
||||
<header class="topbar">
|
||||
<router-link class="brand" :to="tutorialRoute">
|
||||
<img src="/noto--honeybee.ico" alt="" width="20" height="20" />
|
||||
<span>前端开发学习平台</span>
|
||||
</router-link>
|
||||
<nav class="nav">
|
||||
<router-link
|
||||
v-for="item in nav"
|
||||
:key="item.label"
|
||||
:to="item.to"
|
||||
:class="['nav-item', { active: item.active }]"
|
||||
>
|
||||
{{ item.label }}
|
||||
</router-link>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
<template v-if="user.loaded">
|
||||
<router-link
|
||||
v-if="roleStaff"
|
||||
:to="{ name: 'dashboard' }"
|
||||
:class="['nav-item', { active: inTeach }]"
|
||||
>
|
||||
教学
|
||||
</router-link>
|
||||
<n-dropdown
|
||||
v-if="authed"
|
||||
:options="userMenu"
|
||||
trigger="click"
|
||||
@select="clickUserMenu"
|
||||
>
|
||||
<n-button quaternary class="user-btn">
|
||||
{{ user.username }}
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
<n-button v-else type="primary" size="small" @click="loginModal = true">
|
||||
登录
|
||||
</n-button>
|
||||
</template>
|
||||
</header>
|
||||
<main class="main">
|
||||
<router-view v-if="$route.matched.length" />
|
||||
<div v-else-if="user.loaded && !authed" class="signed-out">
|
||||
<p>登录后开始学习</p>
|
||||
<n-button type="primary" @click="loginModal = true">登录</n-button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { authed, roleStaff, user } from "../../store/user"
|
||||
import { loginModal } from "../../store/modal"
|
||||
import { step } from "../../store/tutorial"
|
||||
import { logout } from "../../store/session"
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const routeName = computed(() => String(route.name ?? ""))
|
||||
const inTeach = computed(() => route.path.startsWith("/dashboard"))
|
||||
|
||||
const tutorialRoute = computed(() =>
|
||||
step.value
|
||||
? { name: "home-tutorial", params: { display: step.value } }
|
||||
: { name: "home-tutorial-list" },
|
||||
)
|
||||
|
||||
const nav = computed(() => [
|
||||
{
|
||||
label: "教程",
|
||||
to: tutorialRoute.value,
|
||||
active:
|
||||
routeName.value === "home" || routeName.value.startsWith("home-tutorial"),
|
||||
},
|
||||
{
|
||||
label: "挑战",
|
||||
to: { name: "home-challenge-list" },
|
||||
active: routeName.value.startsWith("home-challenge"),
|
||||
},
|
||||
{
|
||||
label: "自由创作",
|
||||
to: { name: "creation-list" },
|
||||
active: routeName.value.startsWith("creation"),
|
||||
},
|
||||
{
|
||||
label: "作品展",
|
||||
to: { name: "showcase" },
|
||||
active: routeName.value === "showcase",
|
||||
},
|
||||
])
|
||||
|
||||
const userMenu = [
|
||||
{ label: "我的提交", key: "submissions" },
|
||||
{ type: "divider", key: "d1" },
|
||||
{ label: "退出登录", key: "logout" },
|
||||
]
|
||||
|
||||
async function clickUserMenu(key: string) {
|
||||
if (key === "submissions") {
|
||||
router.push({
|
||||
name: "submissions",
|
||||
params: { page: 1 },
|
||||
query: { username: user.username },
|
||||
})
|
||||
} else if (key === "logout") {
|
||||
await logout()
|
||||
// 留在需要登录或特定角色的页面上没有意义
|
||||
if (route.meta.auth || route.meta.role) router.push(tutorialRoute.value)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.shell {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--wp-paper);
|
||||
}
|
||||
|
||||
.topbar {
|
||||
height: var(--wp-topbar-h);
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 16px;
|
||||
background: var(--wp-surface);
|
||||
border-bottom: 1px solid var(--wp-line);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-right: 20px;
|
||||
color: var(--wp-ink);
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
color: var(--wp-muted);
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: var(--wp-ink);
|
||||
background: var(--wp-paper);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--wp-brand);
|
||||
background: var(--wp-brand-soft);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-item:focus-visible,
|
||||
.brand:focus-visible {
|
||||
outline: 2px solid var(--wp-brand);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-btn {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.main {
|
||||
height: var(--wp-main-h);
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.signed-out {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
color: var(--wp-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -239,7 +239,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { NPopconfirm, NButton, useMessage } from "naive-ui"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Icon } from "../../icons"
|
||||
import { renderMarkdown } from "../../utils/markdown"
|
||||
import { storageShim } from "../../utils/previewStorage"
|
||||
import { Prompt, Submission } from "../../api"
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Icon } from "../../icons"
|
||||
import { useMessage } from "naive-ui"
|
||||
import type { UploadFileInfo } from "naive-ui"
|
||||
import { TaskAssets } from "../../api"
|
||||
|
||||
@@ -1,93 +1,53 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<n-flex align="center" justify="space-between" class="title">
|
||||
<n-flex align="center">
|
||||
<Icon
|
||||
:icon="
|
||||
taskTab === TASK_TYPE.Tutorial
|
||||
? 'twemoji:books'
|
||||
: 'twemoji:crossed-swords'
|
||||
"
|
||||
:width="20"
|
||||
></Icon>
|
||||
<n-tabs
|
||||
style="width: 150px"
|
||||
type="segment"
|
||||
animated
|
||||
:value="taskTab"
|
||||
@update:value="changeTab"
|
||||
>
|
||||
<n-tab name="tutorial" tab="教程"></n-tab>
|
||||
<n-tab name="challenge" tab="挑战"></n-tab>
|
||||
</n-tabs>
|
||||
<n-flex align="center" justify="space-between" class="title" :wrap="false">
|
||||
<n-flex align="center" :wrap="false">
|
||||
<span class="panel-name">
|
||||
{{ taskTab === TASK_TYPE.Tutorial ? "教程" : "挑战" }}
|
||||
</span>
|
||||
<template v-if="!hideNav">
|
||||
<n-button text @click="prev()" :disabled="prevDisabled()">
|
||||
<Icon :width="24" icon="pepicons-pencil:arrow-left"></Icon>
|
||||
</n-button>
|
||||
<n-tooltip>
|
||||
<template #trigger>
|
||||
<n-button text @click="prev()" :disabled="prevDisabled()">
|
||||
<Icon :width="18" icon="lucide:arrow-left" />
|
||||
</n-button>
|
||||
</template>
|
||||
上一节
|
||||
</n-tooltip>
|
||||
<span v-if="progressText" class="progress-text">
|
||||
{{ progressText }}
|
||||
</span>
|
||||
<n-button text @click="next()" :disabled="nextDisabled()">
|
||||
<Icon :width="24" icon="pepicons-pencil:arrow-right"></Icon>
|
||||
</n-button>
|
||||
<n-tooltip>
|
||||
<template #trigger>
|
||||
<n-button text @click="next()" :disabled="nextDisabled()">
|
||||
<Icon :width="18" icon="lucide:arrow-right" />
|
||||
</n-button>
|
||||
</template>
|
||||
下一节
|
||||
</n-tooltip>
|
||||
</template>
|
||||
</n-flex>
|
||||
<n-flex>
|
||||
<n-tooltip
|
||||
<n-flex align="center" :wrap="false">
|
||||
<n-button
|
||||
v-if="tutorialAssets.length && taskTab === TASK_TYPE.Tutorial"
|
||||
trigger="hover"
|
||||
size="small"
|
||||
quaternary
|
||||
@click="showAssets = true"
|
||||
>
|
||||
<template #trigger>
|
||||
<n-button text @click="showAssets = true">
|
||||
<Icon :width="16" icon="lucide:image"></Icon>
|
||||
</n-button>
|
||||
</template>
|
||||
<template #icon><Icon icon="lucide:image" /></template>
|
||||
素材
|
||||
</n-tooltip>
|
||||
<n-tooltip v-if="authed" trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button
|
||||
text
|
||||
@click="
|
||||
$router.push({ name: 'submissions', params: { page: 1 } })
|
||||
"
|
||||
>
|
||||
<Icon :width="16" icon="lucide:list"></Icon>
|
||||
</n-button>
|
||||
</template>
|
||||
提交记录
|
||||
</n-tooltip>
|
||||
<n-tooltip v-if="authed" trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button text @click="$router.push({ name: 'creation-list' })">
|
||||
<Icon :width="16" icon="lucide:sparkles"></Icon>
|
||||
</n-button>
|
||||
</template>
|
||||
自由创作
|
||||
</n-tooltip>
|
||||
<n-tooltip v-if="authed" trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button text @click="$router.push({ name: 'showcase' })">
|
||||
<Icon :width="16" icon="lucide:award"></Icon>
|
||||
</n-button>
|
||||
</template>
|
||||
创意工坊
|
||||
</n-tooltip>
|
||||
<n-tooltip v-if="roleSuper" trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button text @click="edit">
|
||||
<Icon :width="16" icon="lucide:edit"></Icon>
|
||||
</n-button>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button v-if="roleSuper" size="small" quaternary @click="edit">
|
||||
<template #icon><Icon icon="lucide:edit" /></template>
|
||||
编辑
|
||||
</n-tooltip>
|
||||
<n-tooltip trigger="hover">
|
||||
</n-button>
|
||||
<n-tooltip>
|
||||
<template #trigger>
|
||||
<n-button text @click="$emit('hide')">
|
||||
<Icon :width="24" icon="material-symbols:close-rounded"></Icon>
|
||||
<Icon :width="18" icon="lucide:x" />
|
||||
</n-button>
|
||||
</template>
|
||||
关闭
|
||||
收起教程,编辑器占满屏幕
|
||||
</n-tooltip>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
@@ -119,7 +79,7 @@
|
||||
</n-modal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Icon } from "../../icons"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import {
|
||||
step,
|
||||
@@ -129,7 +89,7 @@ import {
|
||||
prevDisabled,
|
||||
nextDisabled,
|
||||
} from "../../store/tutorial"
|
||||
import { authed, roleSuper } from "../../store/user"
|
||||
import { roleSuper } from "../../store/user"
|
||||
import { taskTab, taskId, challengeDisplay } from "../../store/task"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { TASK_TYPE } from "../../utils/const"
|
||||
@@ -167,6 +127,8 @@ function syncRoute(routeName: string) {
|
||||
taskTab.value = TASK_TYPE.Challenge
|
||||
if (route.params.display)
|
||||
challengeDisplay.value = Number(route.params.display)
|
||||
// 挑战列表里没有选中的任务,“提交”不可用
|
||||
else taskId.value = 0
|
||||
}
|
||||
}
|
||||
syncRoute(route.name as string)
|
||||
@@ -189,20 +151,6 @@ const progressText = computed(() => {
|
||||
return i === -1 ? "" : `${i + 1} / ${ids.length}`
|
||||
})
|
||||
|
||||
function changeTab(v: TASK_TYPE) {
|
||||
taskId.value = 0
|
||||
taskTab.value = v
|
||||
if (v === TASK_TYPE.Tutorial) {
|
||||
router.push(
|
||||
step.value
|
||||
? { name: "home-tutorial", params: { display: step.value } }
|
||||
: { name: "home-tutorial-list" },
|
||||
)
|
||||
} else if (v === TASK_TYPE.Challenge) {
|
||||
router.push({ name: "home-challenge-list" })
|
||||
}
|
||||
}
|
||||
|
||||
function edit() {
|
||||
const name =
|
||||
taskTab.value === TASK_TYPE.Tutorial
|
||||
@@ -222,15 +170,22 @@ function edit() {
|
||||
|
||||
.title {
|
||||
height: 43px;
|
||||
padding: 0 20px;
|
||||
padding: 0 12px 0 16px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid rgb(239, 239, 245);
|
||||
border-bottom: 1px solid var(--wp-line);
|
||||
background: var(--wp-surface);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.panel-name {
|
||||
font-weight: 600;
|
||||
color: var(--wp-ink);
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
color: var(--wp-faint);
|
||||
font-family: var(--wp-font-mono);
|
||||
min-width: 36px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
|
||||
@@ -392,7 +392,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from "vue"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Icon } from "../../icons"
|
||||
import { useRouter } from "vue-router"
|
||||
import { Submission } from "../../api"
|
||||
import type { TaskStatsOut } from "../../utils/type"
|
||||
|
||||
@@ -119,7 +119,7 @@ watch(step, (v) => {
|
||||
.markdown-body pre code {
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
font-family: Monaco;
|
||||
font-family: var(--wp-font-mono);
|
||||
}
|
||||
|
||||
.markdown-body .codeblock-wrapper {
|
||||
@@ -178,7 +178,7 @@ watch(step, (v) => {
|
||||
}
|
||||
|
||||
.codeblock-action .action-btn:hover {
|
||||
border-color: #18a058;
|
||||
color: #18a058;
|
||||
border-color: var(--wp-brand);
|
||||
color: var(--wp-brand);
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user