feat: 添加自定义滚动条样式,优化布局和组件结构
This commit is contained in:
@@ -116,13 +116,48 @@
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: color-mix(in oklch, var(--foreground) 28%, transparent)
|
||||
transparent;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
@apply font-sans;
|
||||
}
|
||||
|
||||
button:not(:disabled),
|
||||
[role="button"]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Chrome / Edge / Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: color-mix(in oklch, var(--foreground) 22%, transparent);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: color-mix(in oklch, var(--foreground) 38%, transparent);
|
||||
}
|
||||
|
||||
/* 去掉滚动条两头的箭头按钮 */
|
||||
*::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +68,9 @@ const copyConfig = async () => {
|
||||
|
||||
<CardContent>
|
||||
<pre
|
||||
class="max-h-[30rem] overflow-auto rounded-2xl bg-muted p-4 text-xs leading-5 text-muted-foreground"
|
||||
><code>{{ configText }}</code></pre>
|
||||
class="max-h-120 overflow-auto rounded-2xl bg-muted p-4 text-xs leading-5 text-muted-foreground"
|
||||
><code>{{ configText }}</code>
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<!-- app/components/layout/AppHeader.vue - 顶部导航和页面切换入口 -->
|
||||
<script lang="ts" setup>
|
||||
import { Activity, BookOpenText, LayoutDashboard, Search } from "lucide-vue-next";
|
||||
import {
|
||||
Activity,
|
||||
BookOpenText,
|
||||
LayoutDashboard,
|
||||
Search
|
||||
} from "lucide-vue-next";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
@@ -33,7 +38,9 @@ const isActive = (to: string) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80">
|
||||
<header
|
||||
class="border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/80"
|
||||
>
|
||||
<div
|
||||
class="mx-auto flex min-h-16 w-full max-w-7xl items-center justify-between gap-4 px-4 md:px-6"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import type { ScrollAreaRootProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
ScrollAreaCorner,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaViewport,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import ScrollBar from './ScrollBar.vue'
|
||||
|
||||
const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScrollAreaRoot
|
||||
data-slot="scroll-area"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('relative', props.class)"
|
||||
>
|
||||
<ScrollAreaViewport
|
||||
data-slot="scroll-area-viewport"
|
||||
class="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
|
||||
>
|
||||
<slot />
|
||||
</ScrollAreaViewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaCorner />
|
||||
</ScrollAreaRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import type { ScrollAreaScrollbarProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ScrollAreaScrollbar, ScrollAreaThumb } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes['class'] }>(), {
|
||||
orientation: 'vertical',
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
:data-orientation="orientation"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent flex touch-none p-px transition-colors select-none', props.class)"
|
||||
>
|
||||
<ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
class="rounded-full relative flex-1 bg-border"
|
||||
/>
|
||||
</ScrollAreaScrollbar>
|
||||
</template>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as ScrollArea } from './ScrollArea.vue'
|
||||
export { default as ScrollBar } from './ScrollBar.vue'
|
||||
@@ -1,7 +1,12 @@
|
||||
// app/interfaces/answer.ts - 前端和本地 API 之间使用的响应类型
|
||||
|
||||
/** OCS 支持的题型;空字符串表示不指定题型,交给模型根据题目判断 */
|
||||
export type QuestionType = "" | "single" | "multiple" | "judgement" | "completion";
|
||||
export type QuestionType =
|
||||
| ""
|
||||
| "single"
|
||||
| "multiple"
|
||||
| "judgement"
|
||||
| "completion";
|
||||
|
||||
/** 答题接口请求体,字段名保持旧 Python 服务和 OCS AnswererWrapper 兼容 */
|
||||
export interface ISearchRequest {
|
||||
|
||||
@@ -5,11 +5,15 @@ import AppHeader from "@/components/layout/AppHeader.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-background text-foreground">
|
||||
<AppHeader />
|
||||
<main class="mx-auto min-h-[calc(100vh-8rem)] w-full max-w-7xl px-4 py-6 md:px-6">
|
||||
<slot />
|
||||
<div
|
||||
class="flex h-screen flex-col overflow-hidden bg-background text-foreground"
|
||||
>
|
||||
<AppHeader class="shrink-0" />
|
||||
<main class="flex min-h-0 flex-1 flex-col overflow-y-auto">
|
||||
<div class="mx-auto w-full max-w-7xl flex-1 px-4 py-6 md:px-6">
|
||||
<slot />
|
||||
</div>
|
||||
<AppFooter />
|
||||
</main>
|
||||
<AppFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7,7 +7,6 @@ import type {
|
||||
ISearchResponse,
|
||||
IStatsResponse
|
||||
} from "@/interfaces";
|
||||
|
||||
import BaseClientService from "@/services/base_service";
|
||||
|
||||
/** 当前 Nuxt 站点内 API 的基础路径,部署到子路径时由 Nuxt 自己处理 */
|
||||
|
||||
@@ -137,10 +137,7 @@ export const useAnswerStore = defineStore("answer", () => {
|
||||
|
||||
searchErrorMessage.value = res.data.msg || "答题失败";
|
||||
} catch (error) {
|
||||
searchErrorMessage.value = getClientErrorMessage(
|
||||
error,
|
||||
"服务器内部错误"
|
||||
);
|
||||
searchErrorMessage.value = getClientErrorMessage(error, "服务器内部错误");
|
||||
} finally {
|
||||
searchLoading.value = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user