feat(备课-教学设计): 教案设计功能
All checks were successful
CI / lint (push) Successful in 50s
CI / test (push) Successful in 51s

This commit is contained in:
Timothy Yin 2025-05-23 20:43:39 +08:00
parent 8b16bf4b0e
commit a6acd8fd54
19 changed files with 625 additions and 83 deletions

View File

@ -7,15 +7,56 @@ export type AIGeneratedContentResponse = IResponse<{
}
}>
export const generateLessonPlan = async (params: {
export interface GenerateLessonPlanParams {
query: string
}) => {
moduleName?: string
urls?: string[]
useTemplate?: boolean
templateId?: string
}
export interface RegenerateLessonPlanSectionParams {
query: string
conversationId: string
urls?: string[]
}
export interface LessonPlanTemplate {
createBy: number
createTime: string
docxUrl: string
id: number
imgUrl: string
isActive: boolean
moduleName: string
name: string
remark: string
updateBy: number
updateTime: string
}
export const generateLessonPlan = async (params: GenerateLessonPlanParams) => {
return await http<AIGeneratedContentResponse>(`/ai/lesson-plan-design/text-block`, {
method: 'POST',
body: params,
})
}
export const regenerateLessonPlanSection = async (params: RegenerateLessonPlanSectionParams) => {
return await http<AIGeneratedContentResponse>(`/ai/lesson-plan-design/update-block`, {
method: 'POST',
body: params,
})
}
export const getLessonPlanTemplate = async () => {
return await http<IResponse<{
data: LessonPlanTemplate[]
}>>(`/ai/lesson-plan-template/list`, {
method: 'GET',
})
}
export const generateCase = async (params: {
query: string
}) => {

View File

@ -162,7 +162,9 @@ const onDeleteConversation = (conversationId: string) => {
<div
class="gradient-border"
:class="
message.role === 'assistant' && !currentConversation?.finished_at && message.content
message.role === 'assistant'
&& !currentConversation?.finished_at
&& message.content
? ''
: 'inactive'
"
@ -188,6 +190,25 @@ const onDeleteConversation = (conversationId: string) => {
思考中
</div>
</div>
<div
v-if="message.role === 'assistant' && currentConversation?.finished_at && message.content"
class="pt-4 flex items-center gap-2"
>
<!-- download button -->
<Button
size="sm"
>
<Icon name="tabler:plus" />
加入教案库
</Button>
<Button
size="sm"
variant="secondary"
>
<Icon name="tabler:download" />
下载到本地
</Button>
</div>
</div>
</div>
</div>

View File

@ -6,7 +6,8 @@ defineProps<{
}>()
defineEmits<{
(e: 'regenerate' | 'delete', itemIndex: number): void
(e: 'delete', itemIndex: number): void
(e: 'regenerate', itemIndex: number, conversationId: string): void
(e: 'regenerateAll' | 'download' | 'save'): void
}>()
</script>
@ -66,15 +67,15 @@ defineEmits<{
{{ item.title }}
</span>
<div class="flex items-center">
<!-- <Button
<Button
variant="link"
size="xs"
class="text-muted-foreground"
@click="$emit('regenerate', i)"
@click="$emit('regenerate', i, data.conversationId)"
>
<Icon name="tabler:reload" />
重新生成
</Button> -->
</Button>
<Button
variant="link"
size="xs"

View File

@ -1,9 +1,15 @@
export interface AIGeneratedContent {
conversationId: string
createdAt: number
id: string
messageId: string
raw: string
sections: AIGeneratedContentSection[]
taskId: string
title: string
sections: AIGeneratedContentItem[]
}
export interface AIGeneratedContentItem {
export interface AIGeneratedContentSection {
title: string
content: string
}

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { ChevronRight } from 'lucide-vue-next'
import type { SidebarNavItem } from './Sidebar.vue'
import type { SidebarNavItem } from '../index.vue'
defineProps<{
nav: {
@ -23,7 +23,7 @@ defineProps<{
v-for="item in group.items"
:key="item.title"
as-child
:default-open="item.isActive"
default-open
class="group/collapsible"
>
<SidebarMenuItem>
@ -74,20 +74,24 @@ defineProps<{
<!-- 无跳转链接 -->
<SidebarMenuButton
v-else
as="a"
class="py-5"
:tooltip="item.title"
>
<!-- 图标名 -->
<Icon
v-if="item.icon && typeof item.icon === 'string'"
:name="item.icon"
size="16px"
class="!size-6"
/>
<!-- 图标组件 -->
<component
:is="item.icon"
v-else
class="!size-6"
/>
<span>{{ item.title }}</span>
<!-- 有子项目 -->
<ChevronRight
v-if="item.items"
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
@ -108,6 +112,7 @@ defineProps<{
>
<SidebarMenuSubButton
as="a"
class="py-4"
as-child
:is-active="isActive"
:href

View File

@ -6,12 +6,15 @@ defineProps<{
deleteMode?: boolean
}>()
const router = useRouter()
const emit = defineEmits<{
'delete-course': [courseId: number]
}>()
const openCourse = (id: number) => {
window.open(`/course/${id}`, '_blank', 'noopener,noreferrer')
// window.open(`/course/${id}`, '_blank', 'noopener,noreferrer')
router.push(`/course/${id}`)
}
</script>

View File

@ -52,16 +52,13 @@ const onGenerateClick = () => {
<span>文本生成</span>
</div>
</TabsTrigger>
<TabsTrigger value="chapter">
<!-- <TabsTrigger value="chapter">
<div class="flex items-center gap-1">
<Icon name="tabler:text-plus" />
<span>章节生成</span>
</div>
</TabsTrigger>
<TabsTrigger
value="bot"
disabled
>
</TabsTrigger> -->
<TabsTrigger value="bot">
<div class="flex items-center gap-1">
<Icon name="tabler:robot" />
<span>课程智能体</span>
@ -71,19 +68,19 @@ const onGenerateClick = () => {
</Tabs>
<div class="flex items-start gap-4">
<div
v-if="tab === 'chapter'"
class="flex h-20 flex-col justify-center items-center gap-1 px-8 rounded-md border"
v-if="tab === 'bot'"
class="flex h-full flex-col justify-center items-center gap-1 px-8 rounded-md border"
>
<Icon
name="tabler:text-plus"
class="text-3xl"
/>
<span class="text-xs font-medium">选择章节</span>
<span class="text-xs font-medium">选择智能体</span>
</div>
<Textarea
v-model="input"
placeholder="请输入文本来生成内容"
class="h-20 flex-1"
class="h-24 flex-1"
/>
<div class="flex flex-col items-center gap-2">
<Button

View File

@ -1,42 +1,155 @@
<script lang="ts" setup>
import type { AcceptableValue } from 'reka-ui'
import { toast } from 'vue-sonner'
import { generateLessonPlan, type AIGeneratedContentResponse } from '~/api/aifn'
import {
generateLessonPlan,
getLessonPlanTemplate,
regenerateLessonPlanSection,
type LessonPlanTemplate,
type AIGeneratedContentResponse,
type GenerateLessonPlanParams,
} from '~/api/aifn'
import { uploadFile } from '~/api/file'
import type { AIGeneratedContent } from '~/components/ai'
import type { FetchError } from '~/types'
const tab = ref('text')
const loading = ref(false)
const input = ref('')
// const input = ref('')
const selectedModuleNames = ref([])
const selectedFiles = ref<File[]>([])
const onFileChange = (files: FileList) => {
selectedFiles.value = Array.from(files)
}
const form = reactive({
query: '',
urls: [] as string[],
moduleName: '',
})
const contentRequirements = ref<'custom' | 'template'>('custom')
const data = ref<AIGeneratedContent | null>(null)
const onGenerateClick = () => {
if (input.value) {
const sections = [
'学情分析',
'教学目标',
'课程重点',
'课程难点',
'教学准备',
'教学过程',
'作业与评价',
'教学反思',
'思政案例',
'思政元素',
'教学价值与分析',
]
const { data: templates } = useAsyncData(() => getLessonPlanTemplate())
const selectedTemplate = ref<LessonPlanTemplate | null>(null)
const onGenerateClick = async () => {
if (form.query) {
let payload: GenerateLessonPlanParams = {
query: form.query,
}
loading.value = true
toast.promise(
generateLessonPlan({
query: input.value,
}),
{
loading: '生成中...',
success: (res: AIGeneratedContentResponse) => {
data.value = res.data
return '生成成功'
},
error: (err: FetchError) => {
if (tab.value === 'text') {
if (selectedFiles.value.length > 0) {
for (const file of selectedFiles.value) {
form.urls.push(await uploadFile(file, 'temp'))
}
console.log('uploaded urls', form.urls)
payload = {
...payload,
urls: form.urls,
}
}
if (contentRequirements.value === 'custom') {
if (!form.moduleName) {
toast.error('请选择要生成的内容模块')
return
}
payload = {
...payload,
moduleName: form.moduleName,
}
} else if (contentRequirements.value === 'template') {
if (!selectedTemplate.value) {
toast.error('请选择模版')
return
}
payload = {
...payload,
useTemplate: true,
templateId: `${selectedTemplate.value.id}`,
}
}
}
toast.promise(generateLessonPlan(payload), {
loading: '生成中...',
success: (res: AIGeneratedContentResponse) => {
if (res.code !== 200) {
data.value = null
return err.message
},
finally: () => {
loading.value = false
},
toast.error(res.msg || '生成失败')
return
}
data.value = res.data
return '生成成功'
},
)
error: (err: FetchError) => {
data.value = null
return err.message
},
finally: () => {
loading.value = false
},
})
} else {
data.value = null
}
}
const onSectionRegenerateClick = (
itemIndex: number,
conversationId: string,
) => {
if (data.value) {
const item = data.value.sections[itemIndex]
if (item) {
loading.value = true
toast.promise(
regenerateLessonPlanSection({
query: `重新生成${item.title}`,
conversationId,
}),
{
loading: `重新生成${item.title}中...`,
success: (res: AIGeneratedContentResponse) => {
if (res.code !== 200) {
toast.error(res.msg || '生成失败')
return
}
data.value?.sections.splice(itemIndex, 1, res.data.sections[0])
return `重新生成${item.title}成功`
},
error: (err: FetchError) => {
return err.message
},
finally: () => {
loading.value = false
},
},
)
}
}
}
</script>
<template>
@ -52,16 +165,7 @@ const onGenerateClick = () => {
<span>文本生成</span>
</div>
</TabsTrigger>
<TabsTrigger value="chapter">
<div class="flex items-center gap-1">
<Icon name="tabler:text-plus" />
<span>章节生成</span>
</div>
</TabsTrigger>
<TabsTrigger
value="bot"
disabled
>
<TabsTrigger value="bot">
<div class="flex items-center gap-1">
<Icon name="tabler:robot" />
<span>课程智能体</span>
@ -69,21 +173,29 @@ const onGenerateClick = () => {
</TabsTrigger>
</TabsList>
</Tabs>
<div class="flex items-start gap-4">
<div
v-if="tab === 'chapter'"
class="flex h-20 flex-col justify-center items-center gap-1 px-8 rounded-md border"
v-if="tab === 'bot'"
class="flex h-full flex-col justify-center items-center gap-1 px-8 rounded-md border"
>
<Icon
name="tabler:text-plus"
class="text-3xl"
/>
<span class="text-xs font-medium">选择章节</span>
<span class="text-xs font-medium">选择智能体</span>
</div>
<UniFileSelector
v-if="tab === 'text'"
class="h-full"
placeholder="上传资料"
multiple
@change="onFileChange"
/>
<Textarea
v-model="input"
v-model="form.query"
placeholder="请输入文本来生成内容"
class="h-20 flex-1"
class="h-24 flex-1"
/>
<div class="flex flex-col items-center gap-2">
<Button
@ -104,7 +216,102 @@ const onGenerateClick = () => {
<p class="text-xs text-foreground/40">内容由 AI 生成仅供参考</p>
</div>
</div>
<AiGeneratedContent :data />
<div class="flex gap-4 items-start pb-2">
<!-- <p class="text-sm text-muted-foreground font-medium">内容要求</p> -->
<div class="flex flex-col gap-4 pt-1">
<RadioGroup
v-model="contentRequirements"
default-value="custom"
class="flex flex-row gap-4"
>
<div class="flex items-center space-x-2">
<RadioGroupItem
id="custom"
value="custom"
/>
<Label for="custom">自定义教案内容</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem
id="template"
value="template"
/>
<Label for="template">模版库选择</Label>
</div>
</RadioGroup>
<ToggleGroup
v-if="contentRequirements === 'custom'"
v-model="selectedModuleNames"
type="multiple"
variant="outline"
size="sm"
class="flex flex-wrap justify-start items-center gap-2 *:!px-2 *:!h-7"
@update:model-value="
(value) => {
form.moduleName = Array.from(value as AcceptableValue[]).join(',')
}
"
>
<ToggleGroupItem
v-for="section in sections"
:key="section"
class="data-[state=on]:bg-primary data-[state=on]:text-primary-foreground"
:value="section"
>
{{ section }}
</ToggleGroupItem>
</ToggleGroup>
<div
v-else
class="flex gap-4 overflow-hidden overflow-x-auto snap-x snap-mandatory"
>
<!-- {{ templates?.data }} -->
<Card
v-for="template in templates?.data || []"
:key="template.id"
class="flex-shrink-0 p-0 overflow-hidden relative snap-start snap-always snap-x"
:class="{
'border-primary': selectedTemplate?.id === template.id,
}"
@click="
() => {
selectedTemplate = template
}
"
>
<CardHeader class="px-4 pt-3 pb-1">
<h1 class="text-sm font-medium flex items-center gap-1">
<Icon
name="fluent-color:document-text-24"
class="text-lg text-muted-foreground !stroke-[5px]"
/>
{{ template.name }}
</h1>
</CardHeader>
<CardContent class="p-0">
<div
v-if="selectedTemplate?.id === template.id"
class="absolute inset-0"
>
<Icon
name="tabler:check"
class="absolute top-2 right-2 text-primary"
/>
</div>
<NuxtImg
:src="template.imgUrl"
class="h-72"
/>
</CardContent>
</Card>
</div>
</div>
</div>
<AiGeneratedContent
:data
@regenerate="onSectionRegenerateClick"
@regenerate-all="onGenerateClick"
/>
</div>
</template>

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
import type { VariantProps } from 'class-variance-authority'
import type { toggleVariants } from '@/components/ui/toggle'
import { reactiveOmit } from '@vueuse/core'
import { ToggleGroupRoot, type ToggleGroupRootEmits, type ToggleGroupRootProps, useForwardPropsEmits } from 'reka-ui'
import { type HTMLAttributes, provide } from 'vue'
import { cn } from '@/lib/utils'
type ToggleGroupVariants = VariantProps<typeof toggleVariants>
const props = defineProps<ToggleGroupRootProps & {
class?: HTMLAttributes['class']
variant?: ToggleGroupVariants['variant']
size?: ToggleGroupVariants['size']
}>()
const emits = defineEmits<ToggleGroupRootEmits>()
provide('toggleGroup', {
variant: props.variant,
size: props.size,
})
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<ToggleGroupRoot v-slot="slotProps" v-bind="forwarded" :class="cn('flex items-center justify-center gap-1', props.class)">
<slot v-bind="slotProps" />
</ToggleGroupRoot>
</template>

View File

@ -0,0 +1,34 @@
<script setup lang="ts">
import type { VariantProps } from 'class-variance-authority'
import { reactiveOmit } from '@vueuse/core'
import { ToggleGroupItem, type ToggleGroupItemProps, useForwardProps } from 'reka-ui'
import { type HTMLAttributes, inject } from 'vue'
import { cn } from '@/lib/utils'
import { toggleVariants } from '@/components/ui/toggle'
type ToggleGroupVariants = VariantProps<typeof toggleVariants>
const props = defineProps<ToggleGroupItemProps & {
class?: HTMLAttributes['class']
variant?: ToggleGroupVariants['variant']
size?: ToggleGroupVariants['size']
}>()
const context = inject<ToggleGroupVariants>('toggleGroup')
const delegatedProps = reactiveOmit(props, 'class', 'size', 'variant')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<ToggleGroupItem
v-slot="slotProps"
v-bind="forwardedProps" :class="cn(toggleVariants({
variant: context?.variant || variant,
size: context?.size || size,
}), props.class)"
>
<slot v-bind="slotProps" />
</ToggleGroupItem>
</template>

View File

@ -0,0 +1,2 @@
export { default as ToggleGroup } from './ToggleGroup.vue'
export { default as ToggleGroupItem } from './ToggleGroupItem.vue'

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { Toggle, type ToggleEmits, type ToggleProps, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
import { type ToggleVariants, toggleVariants } from '.'
const props = withDefaults(defineProps<ToggleProps & {
class?: HTMLAttributes['class']
variant?: ToggleVariants['variant']
size?: ToggleVariants['size']
}>(), {
variant: 'default',
size: 'default',
disabled: false,
})
const emits = defineEmits<ToggleEmits>()
const delegatedProps = reactiveOmit(props, 'class', 'size', 'variant')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<Toggle
v-slot="slotProps"
v-bind="forwarded"
:class="cn(toggleVariants({ variant, size }), props.class)"
>
<slot v-bind="slotProps" />
</Toggle>
</template>

View File

@ -0,0 +1,27 @@
import { cva, type VariantProps } from 'class-variance-authority'
export { default as Toggle } from './Toggle.vue'
export const toggleVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2',
{
variants: {
variant: {
default: 'bg-transparent',
outline:
'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground',
},
size: {
default: 'h-10 px-3 min-w-10',
sm: 'h-9 px-2.5 min-w-9',
lg: 'h-11 px-5 min-w-11',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
export type ToggleVariants = VariantProps<typeof toggleVariants>

View File

@ -0,0 +1,133 @@
<script lang="ts" setup>
const emit = defineEmits(['change'])
const props = defineProps({
placeholder: {
type: String,
default: '点击或拖拽文件到此处',
},
placeholderDragover: {
type: String,
default: '松开选择文件',
},
multiple: {
type: Boolean,
default: false,
},
accept: {
type: String,
default: '',
},
})
const inputRef = ref<HTMLInputElement | null>(null)
const dragover = ref(false)
const selectedFiles = ref<File[]>([])
const onIncomeFiles = (files?: FileList | null) => {
if (files && files.length > 0) {
const wantedFiles = Array.from(files).filter((file) => {
if (props.accept) {
const accept = props.accept.split(',').map((type) => type.trim())
return accept.includes(file.type)
}
return true
})
if (wantedFiles.length === 0) {
console.error('no acceptable file')
return
}
selectedFiles.value = props.multiple ? wantedFiles : [wantedFiles[0]]
emit('change', files)
}
}
</script>
<template>
<div
:class="{
'bg-neutral-300 dark:bg-neutral-900 border-primary-300 dark:border-primary-800 shadow-inner':
dragover,
}"
class="p-2 px-4 min-w-40 relative rounded-md border-2 border-dashed border-neutral-200 dark:border-neutral-800 bg-inherit cursor-pointer select-none transition duration-200 hover:border-primary-300 dark:hover:border-primary-800 overflow-hidden"
@click="inputRef?.click()"
@dragover.prevent="dragover = true"
@dragleave.prevent="dragover = false"
@drop.prevent="
($event) => {
dragover = false
if (!$event.dataTransfer?.files) return
onIncomeFiles($event.dataTransfer?.files)
}
"
>
<input
ref="inputRef"
:accept="accept"
:multiple="multiple"
class="hidden"
type="file"
@change="onIncomeFiles(inputRef?.files)"
/>
<div
:class="{
'pb-6': selectedFiles.length > 0,
}"
class="w-full h-full flex flex-col justify-center items-center gap-2 transition-all"
>
<Icon
:name="dragover ? 'i-tabler-drag-drop' : 'i-tabler-upload'"
class="text-3xl text-neutral-400 dark:text-neutral-500"
/>
<p class="text-xs font-medium text-neutral-500 dark:text-neutral-400">
{{ dragover ? '松开选择文件' : '点击或拖拽文件到此处' }}
</p>
</div>
<div
v-if="selectedFiles.length > 0"
class="absolute inset-x-0 bottom-0 pl-2 pr-0.5 py-0.5 flex justify-between items-center bg-neutral-100 dark:bg-neutral-900 border-t dark:border-neutral-800"
>
<div class="flex-1 pr-4 overflow-hidden flex items-center gap-1">
<Icon
:name="
selectedFiles.length === 1 ? 'i-tabler-file' : 'i-tabler-files'
"
class="text-neutral-500 dark:text-neutral-400"
/>
<p
:title="
selectedFiles
.slice(0, 3)
.map((file) => file.name)
.join(', ')
"
class="text-[10px] font-medium overflow-hidden text-ellipsis whitespace-nowrap"
>
{{
selectedFiles
.slice(0, 3)
.map((file) => file.name)
.join(', ')
}}
</p>
</div>
<div>
<Button
color="red"
size="xs"
variant="ghost"
@click.stop="
() => {
selectedFiles = []
inputRef!.value = ''
}
"
>
<Icon name="i-tabler-x" />
</Button>
</div>
</div>
</div>
</template>
<style scoped></style>

View File

@ -19,15 +19,26 @@ const sidebarNav: SidebarNavGroup[] = [
},
{
title: 'AI 备课',
url: `/course/prep`,
icon: 'tabler:clipboard-list',
isExternal: true,
items: [
{
title: 'AI 教学设计',
url: `/course/prep/teach`,
},
{
title: 'AI 课件设计',
url: `/course/prep/deck`,
},
{
title: 'AI 出题',
url: `/course/prep/quiz`,
},
]
},
{
title: 'AI 教科研',
url: `/course/research`,
icon: 'tabler:report-search',
isExternal: true,
},
],
},

View File

@ -23,7 +23,6 @@ const {
definePageMeta({
requiresAuth: true,
hideSidebar: true,
})
watch(

View File

@ -1,9 +1,6 @@
<script lang="ts" setup>
import { nav } from './config'
definePageMeta({
requiresAuth: true,
hideSidebar: true,
})
useHead({
@ -26,7 +23,7 @@ onMounted(() => {
</script>
<template>
<AppContainer :nav-secondary="nav">
<AppContainer :nav-secondary="[]">
<h1>AI 课件设计</h1>
</AppContainer>
</template>

View File

@ -2,11 +2,9 @@
import { z } from 'zod'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { nav } from './config'
definePageMeta({
requiresAuth: true,
hideSidebar: true,
})
useHead({
@ -63,7 +61,7 @@ onMounted(() => {
<template>
<AppContainer
:nav-secondary="nav"
:nav-secondary="[]"
content-class="flex items-start p-0 w-full h-full"
>
<div class="h-full border-r shadow-xl flex flex-col gap-4">

View File

@ -1,21 +1,16 @@
<script lang="ts" setup>
import { nav } from './config'
import {
FnTeachLessonPlan,
FnTeachCaseGen,
FnTeachStdDesign,
FnTeachKnowledgeDiagram,
FnTeachCourseChapter,
FnTeachPoliticalCase,
FnTeachResearchPlan,
FnTeachPlan,
FnTeachCourseOutline,
} from '#components'
import type { NavTertiaryItem } from '~/components/nav/Tertiary.vue'
definePageMeta({
requiresAuth: true,
hideSidebar: true,
})
useHead({
@ -27,15 +22,15 @@ const router = useRouter()
const { setBreadcrumbs } = useBreadcrumbs()
const tertiaryNavs: NavTertiaryItem[] = [
{ label: '教案设计', component: FnTeachLessonPlan },
{ label: '案例设计', component: FnTeachCaseGen },
{ label: '课程标准', component: FnTeachStdDesign },
{ label: '教案设计', component: FnTeachLessonPlan },
{ label: '知识图谱', component: FnTeachKnowledgeDiagram, disabled: true },
{ label: '课程章节', component: FnTeachCourseChapter },
{ label: '思政案例', component: FnTeachPoliticalCase },
{ label: '教研计划', component: FnTeachResearchPlan },
{ label: '教学计划', component: FnTeachPlan },
{ label: '课程大纲', component: FnTeachCourseOutline },
{ label: '教学案例', component: FnTeachCaseGen },
{ label: '思政案例', component: FnTeachPoliticalCase },
// { label: '', component: FnTeachCourseChapter },
// { label: '', component: FnTeachResearchPlan },
// { label: '', component: FnTeachPlan },
]
const currentNav = ref(0)
@ -67,7 +62,7 @@ onMounted(() => {
<template>
<AppContainer
:nav-secondary="nav"
:nav-secondary="[]"
content-class="flex items-start p-0"
>
<div class="w-[188px] h-full border-r shadow-xl">