feat: 表格展示优化
This commit is contained in:
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useMediaQuery } from "@vueuse/core";
|
||||||
|
import type { HTMLAttributes } from "vue";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
side?: "top" | "right" | "bottom" | "left";
|
||||||
|
align?: "start" | "center" | "end";
|
||||||
|
open?: boolean;
|
||||||
|
class?: HTMLAttributes["class"];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery("(max-width: 640px)");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- 移动端 Popover -->
|
||||||
|
<Popover v-if="isMobile">
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<slot name="trigger" />
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
:side="side"
|
||||||
|
:align="align"
|
||||||
|
class="w-fit px-0 py-2 relative top-2! rounded-lg! border-none!"
|
||||||
|
:class="props.class"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<!-- PC Tooltip -->
|
||||||
|
<TooltipProvider v-else :delay-duration="150">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<slot name="trigger" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
:side="side"
|
||||||
|
:align="align"
|
||||||
|
class="w-fit px-0 py-2 relative top-2! bg-popover text-popover-foreground border rounded-lg! [&>svg]:hidden [&>span>svg]:opacity-0!"
|
||||||
|
:class="props.class"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
DialogTitle
|
DialogTitle
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { useAnswerStore } from "@/stores/answer";
|
import { useAnswerStore } from "@/stores/answer";
|
||||||
|
import { getOptionLines } from "@/utils";
|
||||||
|
|
||||||
const answerStore = useAnswerStore();
|
const answerStore = useAnswerStore();
|
||||||
const { selectedRecord } = storeToRefs(answerStore);
|
const { selectedRecord } = storeToRefs(answerStore);
|
||||||
@@ -26,6 +27,15 @@ const dialogOpen = computed({
|
|||||||
if (!open) close();
|
if (!open) close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 格式化后按行拆分选项,标记每行是否为正确答案 */
|
||||||
|
const formattedOptionLines = computed(() => {
|
||||||
|
if (!selectedRecord.value?.options) return null;
|
||||||
|
return getOptionLines(
|
||||||
|
selectedRecord.value.options,
|
||||||
|
selectedRecord.value.answer ?? ""
|
||||||
|
);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -36,26 +46,46 @@ const dialogOpen = computed({
|
|||||||
<DialogDescription>{{ selectedRecord?.time }}</DialogDescription>
|
<DialogDescription>{{ selectedRecord?.time }}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div v-if="selectedRecord" class="grid max-h-[70vh] gap-4 overflow-auto pr-1">
|
<div
|
||||||
|
v-if="selectedRecord"
|
||||||
|
class="grid max-h-[70vh] gap-4 overflow-auto pr-1"
|
||||||
|
>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<span class="text-xs font-medium text-muted-foreground">题目</span>
|
<span class="text-xs font-medium text-muted-foreground">题目</span>
|
||||||
<pre
|
<pre
|
||||||
class="whitespace-pre-wrap rounded-2xl bg-muted p-3 text-sm leading-6 text-foreground"
|
class="whitespace-pre-wrap rounded-2xl bg-muted p-3 text-sm leading-6 text-foreground"
|
||||||
>{{ selectedRecord.question }}</pre>
|
>{{ selectedRecord.question }}</pre
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<span class="text-xs font-medium text-muted-foreground">选项</span>
|
<span class="text-xs font-medium text-muted-foreground">选项</span>
|
||||||
<pre
|
<div
|
||||||
class="whitespace-pre-wrap rounded-2xl bg-muted p-3 text-sm leading-6 text-foreground"
|
class="whitespace-pre-wrap rounded-2xl bg-muted p-3 text-sm leading-6"
|
||||||
>{{ selectedRecord.options || "-" }}</pre>
|
>
|
||||||
|
<template v-if="formattedOptionLines">
|
||||||
|
<div
|
||||||
|
v-for="(line, i) in formattedOptionLines"
|
||||||
|
:key="i"
|
||||||
|
:class="
|
||||||
|
line.isCorrect
|
||||||
|
? 'text-(--color-primary) font-medium'
|
||||||
|
: 'text-foreground'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ line.text }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span v-else class="text-foreground">-</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<span class="text-xs font-medium text-muted-foreground">答案</span>
|
<span class="text-xs font-medium text-muted-foreground">答案</span>
|
||||||
<pre
|
<pre
|
||||||
class="whitespace-pre-wrap rounded-2xl bg-muted p-3 text-base font-semibold leading-7 text-foreground"
|
class="whitespace-pre-wrap rounded-2xl bg-muted p-3 text-base font-semibold leading-7 text-foreground"
|
||||||
>{{ selectedRecord.answer }}</pre>
|
>{{ selectedRecord.answer }}</pre
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { storeToRefs } from "pinia";
|
|||||||
|
|
||||||
import TableCom from "@/components/Table/TableCom.vue";
|
import TableCom from "@/components/Table/TableCom.vue";
|
||||||
import type { TableColumn } from "@/components/Table/type";
|
import type { TableColumn } from "@/components/Table/type";
|
||||||
|
import TooltipCom from "@/components/TooltipCom.vue";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -21,12 +22,12 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue
|
SelectValue
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import type { IQaRecord } from "@/interfaces";
|
|
||||||
import {
|
import {
|
||||||
DASHBOARD_PAGE_SIZE_OPTIONS,
|
DASHBOARD_PAGE_SIZE_OPTIONS,
|
||||||
QUESTION_TYPE_LABELS,
|
QUESTION_TYPE_LABELS,
|
||||||
useAnswerStore
|
useAnswerStore
|
||||||
} from "@/stores/answer";
|
} from "@/stores/answer";
|
||||||
|
import { getOptionLines } from "@/utils";
|
||||||
|
|
||||||
const answerStore = useAnswerStore();
|
const answerStore = useAnswerStore();
|
||||||
const {
|
const {
|
||||||
@@ -79,11 +80,6 @@ const columns = computed<TableColumn[]>(() => [
|
|||||||
const getQuestionTypeLabel = (type: string) => {
|
const getQuestionTypeLabel = (type: string) => {
|
||||||
return QUESTION_TYPE_LABELS[type] || type || "自动判断";
|
return QUESTION_TYPE_LABELS[type] || type || "自动判断";
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 空选项统一展示占位,不在表格中撑开大段空白 */
|
|
||||||
const displayOptions = (record: IQaRecord) => {
|
|
||||||
return record.options || "-";
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -148,12 +144,49 @@ const displayOptions = (record: IQaRecord) => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #options="{ row }">
|
<template #options="{ row }">
|
||||||
<div
|
<TooltipCom side="top" align="start">
|
||||||
class="max-w-md truncate text-muted-foreground"
|
<template #trigger>
|
||||||
:title="displayOptions(row)"
|
<div class="max-w-md cursor-default truncate">
|
||||||
>
|
<template v-if="row.options">
|
||||||
{{ displayOptions(row) }}
|
<template
|
||||||
</div>
|
v-for="(line, i) in getOptionLines(
|
||||||
|
row.options,
|
||||||
|
row.answer ?? ''
|
||||||
|
)"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
|
<span v-if="i > 0" class="text-muted-foreground"
|
||||||
|
> </span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
line.isCorrect
|
||||||
|
? 'text-green-500 font-medium'
|
||||||
|
: 'text-muted-foreground'
|
||||||
|
"
|
||||||
|
>{{ line.text }}</span
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<span v-else class="text-muted-foreground">-</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-if="row.options"
|
||||||
|
class="max-w-sm whitespace-pre-wrap px-3 py-1 text-xs leading-5"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(line, i) in getOptionLines(
|
||||||
|
row.options,
|
||||||
|
row.answer ?? ''
|
||||||
|
)"
|
||||||
|
:key="i"
|
||||||
|
:class="line.isCorrect ? 'text-green-500 font-medium' : ''"
|
||||||
|
>
|
||||||
|
{{ line.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipCom>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #answer="{ value }">
|
<template #answer="{ value }">
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PopoverRootEmits, PopoverRootProps } from 'reka-ui'
|
||||||
|
import { PopoverRoot, useForwardPropsEmits } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<PopoverRootProps>()
|
||||||
|
const emits = defineEmits<PopoverRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PopoverRoot
|
||||||
|
v-slot="slotProps"
|
||||||
|
data-slot="popover"
|
||||||
|
v-bind="forwarded"
|
||||||
|
>
|
||||||
|
<slot v-bind="slotProps" />
|
||||||
|
</PopoverRoot>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PopoverAnchorProps } from 'reka-ui'
|
||||||
|
import { PopoverAnchor } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<PopoverAnchorProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PopoverAnchor
|
||||||
|
data-slot="popover-anchor"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</PopoverAnchor>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PopoverContentEmits, PopoverContentProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import {
|
||||||
|
PopoverContent,
|
||||||
|
PopoverPortal,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<PopoverContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
sideOffset: 4,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const emits = defineEmits<PopoverContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PopoverPortal>
|
||||||
|
<PopoverContent
|
||||||
|
data-slot="popover-content"
|
||||||
|
v-bind="{ ...$attrs, ...forwarded }"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 dark:ring-foreground/10 flex flex-col gap-4 rounded-3xl p-4 text-sm shadow-lg ring-1 duration-100 z-50 w-72 origin-(--reka-popover-content-transform-origin) outline-hidden',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</PopoverContent>
|
||||||
|
</PopoverPortal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<p
|
||||||
|
data-slot="popover-description"
|
||||||
|
:class="cn('text-muted-foreground', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="popover-header"
|
||||||
|
:class="cn('flex flex-col gap-1 text-sm', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="popover-title"
|
||||||
|
:class="cn('text-base font-medium cn-font-heading', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PopoverTriggerProps } from 'reka-ui'
|
||||||
|
import { PopoverTrigger } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<PopoverTriggerProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PopoverTrigger
|
||||||
|
data-slot="popover-trigger"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</PopoverTrigger>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export { default as Popover } from './Popover.vue'
|
||||||
|
export { default as PopoverAnchor } from './PopoverAnchor.vue'
|
||||||
|
export { default as PopoverContent } from './PopoverContent.vue'
|
||||||
|
export { default as PopoverDescription } from './PopoverDescription.vue'
|
||||||
|
export { default as PopoverHeader } from './PopoverHeader.vue'
|
||||||
|
export { default as PopoverTitle } from './PopoverTitle.vue'
|
||||||
|
export { default as PopoverTrigger } from './PopoverTrigger.vue'
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { TooltipRootEmits, TooltipRootProps } from 'reka-ui'
|
||||||
|
import { TooltipRoot, useForwardPropsEmits } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<TooltipRootProps>()
|
||||||
|
const emits = defineEmits<TooltipRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TooltipRoot
|
||||||
|
v-slot="slotProps"
|
||||||
|
data-slot="tooltip"
|
||||||
|
v-bind="forwarded"
|
||||||
|
>
|
||||||
|
<slot v-bind="slotProps" />
|
||||||
|
</TooltipRoot>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { TooltipContentEmits, TooltipContentProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { TooltipArrow, TooltipContent, TooltipPortal, useForwardPropsEmits } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<TooltipContentProps & { class?: HTMLAttributes['class'] }>(), {
|
||||||
|
sideOffset: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits<TooltipContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TooltipPortal>
|
||||||
|
<TooltipContent
|
||||||
|
data-slot="tooltip-content"
|
||||||
|
v-bind="{ ...forwarded, ...$attrs }"
|
||||||
|
:class="cn('data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 inline-flex items-center gap-1.5 rounded-xl px-3 py-1.5 text-xs has-data-[slot=kbd]:pr-1.5 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-lg bg-foreground text-background z-50 w-fit max-w-xs origin-(--reka-tooltip-content-transform-origin)', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<TooltipArrow class="size-2.5 rotate-45 rounded-[2px] data-[side=left]:translate-x-[-1.5px] data-[side=right]:translate-x-[1.5px] bg-foreground fill-foreground z-50 translate-y-[calc(-50%_-_2px)]" />
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPortal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { TooltipProviderProps } from 'reka-ui'
|
||||||
|
import { TooltipProvider } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<TooltipProviderProps>(), {
|
||||||
|
delayDuration: 0,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TooltipProvider v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</TooltipProvider>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { TooltipTriggerProps } from 'reka-ui'
|
||||||
|
import { TooltipTrigger } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<TooltipTriggerProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TooltipTrigger
|
||||||
|
data-slot="tooltip-trigger"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</TooltipTrigger>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export { default as Tooltip } from './Tooltip.vue'
|
||||||
|
export { default as TooltipContent } from './TooltipContent.vue'
|
||||||
|
export { default as TooltipProvider } from './TooltipProvider.vue'
|
||||||
|
export { default as TooltipTrigger } from './TooltipTrigger.vue'
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// app/utils/format.ts - 文本格式化工具
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将选项字符串从"字母在上、内容在下"的换行格式合并为"字母. 内容"的单行格式。
|
||||||
|
* 例如:
|
||||||
|
* 输入:`A.\n错\nB.\n对`
|
||||||
|
* 输出:`A. 错\nB. 对`
|
||||||
|
*/
|
||||||
|
export function formatOptions(options: string): string {
|
||||||
|
// 匹配类似 "A." 开头的选项标记,将其后紧跟的换行内容合并到同一行
|
||||||
|
return options.replace(/^([A-Za-z]\.)\s*\n\s*/gm, "$1 ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将选项字符串拆分为逐行结构,标记哪些行是正确答案。
|
||||||
|
* answer 按 # 分隔(多选),与选项正文做等值匹配(大小写不敏感)。
|
||||||
|
*/
|
||||||
|
export function getOptionLines(
|
||||||
|
options: string,
|
||||||
|
answer: string
|
||||||
|
): { text: string; isCorrect: boolean }[] {
|
||||||
|
const segments = answer
|
||||||
|
.split("#")
|
||||||
|
.map((s) => s.trim().toUpperCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return formatOptions(options)
|
||||||
|
.split("\n")
|
||||||
|
.filter((line) => line.trim())
|
||||||
|
.map((line) => {
|
||||||
|
const match = line.match(/^[A-Za-z]\.\s*(.*)/);
|
||||||
|
const content = match?.[1]?.trim().toUpperCase() ?? "";
|
||||||
|
const isCorrect = content !== "" && segments.includes(content);
|
||||||
|
return { text: line, isCorrect };
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from "./clipboard";
|
export * from "./clipboard";
|
||||||
|
export * from "./format";
|
||||||
export * from "./sortableTable";
|
export * from "./sortableTable";
|
||||||
|
|||||||
Reference in New Issue
Block a user