feat: 添加自定义滚动条样式,优化布局和组件结构
This commit is contained in:
@@ -116,13 +116,48 @@
|
|||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: color-mix(in oklch, var(--foreground) 28%, transparent)
|
||||||
|
transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:not(:disabled),
|
button:not(:disabled),
|
||||||
[role="button"]:not(:disabled) {
|
[role="button"]:not(:disabled) {
|
||||||
cursor: pointer;
|
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>
|
<CardContent>
|
||||||
<pre
|
<pre
|
||||||
class="max-h-[30rem] overflow-auto rounded-2xl bg-muted p-4 text-xs leading-5 text-muted-foreground"
|
class="max-h-120 overflow-auto rounded-2xl bg-muted p-4 text-xs leading-5 text-muted-foreground"
|
||||||
><code>{{ configText }}</code></pre>
|
><code>{{ configText }}</code>
|
||||||
|
</pre>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
<!-- app/components/layout/AppHeader.vue - 顶部导航和页面切换入口 -->
|
<!-- app/components/layout/AppHeader.vue - 顶部导航和页面切换入口 -->
|
||||||
<script lang="ts" setup>
|
<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";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
@@ -33,7 +38,9 @@ const isActive = (to: string) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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
|
<div
|
||||||
class="mx-auto flex min-h-16 w-full max-w-7xl items-center justify-between gap-4 px-4 md:px-6"
|
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 之间使用的响应类型
|
// app/interfaces/answer.ts - 前端和本地 API 之间使用的响应类型
|
||||||
|
|
||||||
/** OCS 支持的题型;空字符串表示不指定题型,交给模型根据题目判断 */
|
/** OCS 支持的题型;空字符串表示不指定题型,交给模型根据题目判断 */
|
||||||
export type QuestionType = "" | "single" | "multiple" | "judgement" | "completion";
|
export type QuestionType =
|
||||||
|
| ""
|
||||||
|
| "single"
|
||||||
|
| "multiple"
|
||||||
|
| "judgement"
|
||||||
|
| "completion";
|
||||||
|
|
||||||
/** 答题接口请求体,字段名保持旧 Python 服务和 OCS AnswererWrapper 兼容 */
|
/** 答题接口请求体,字段名保持旧 Python 服务和 OCS AnswererWrapper 兼容 */
|
||||||
export interface ISearchRequest {
|
export interface ISearchRequest {
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ import AppHeader from "@/components/layout/AppHeader.vue";
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-background text-foreground">
|
<div
|
||||||
<AppHeader />
|
class="flex h-screen flex-col overflow-hidden bg-background text-foreground"
|
||||||
<main class="mx-auto min-h-[calc(100vh-8rem)] w-full max-w-7xl px-4 py-6 md:px-6">
|
>
|
||||||
<slot />
|
<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>
|
</main>
|
||||||
<AppFooter />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import type {
|
|||||||
ISearchResponse,
|
ISearchResponse,
|
||||||
IStatsResponse
|
IStatsResponse
|
||||||
} from "@/interfaces";
|
} from "@/interfaces";
|
||||||
|
|
||||||
import BaseClientService from "@/services/base_service";
|
import BaseClientService from "@/services/base_service";
|
||||||
|
|
||||||
/** 当前 Nuxt 站点内 API 的基础路径,部署到子路径时由 Nuxt 自己处理 */
|
/** 当前 Nuxt 站点内 API 的基础路径,部署到子路径时由 Nuxt 自己处理 */
|
||||||
|
|||||||
@@ -137,10 +137,7 @@ export const useAnswerStore = defineStore("answer", () => {
|
|||||||
|
|
||||||
searchErrorMessage.value = res.data.msg || "答题失败";
|
searchErrorMessage.value = res.data.msg || "答题失败";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
searchErrorMessage.value = getClientErrorMessage(
|
searchErrorMessage.value = getClientErrorMessage(error, "服务器内部错误");
|
||||||
error,
|
|
||||||
"服务器内部错误"
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
searchLoading.value = false;
|
searchLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user