提交 a7e77ccc authored 作者: 王鹏飞's avatar 王鹏飞

feat: 优化资源列表分栏展示及状态保留

上级 774af296
...@@ -22,6 +22,7 @@ import { ...@@ -22,6 +22,7 @@ import {
DataAnalysis, DataAnalysis,
ChatDotRound, ChatDotRound,
Setting, Setting,
DocumentChecked,
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
export const menus: IMenuItem[] = [ export const menus: IMenuItem[] = [
{ {
...@@ -47,18 +48,18 @@ export const menus: IMenuItem[] = [ ...@@ -47,18 +48,18 @@ export const menus: IMenuItem[] = [
name: '教案', name: '教案',
path: '/resource/lessonplan', path: '/resource/lessonplan',
}, },
{
tag: 'v1-resource-case',
icon: Collection,
name: '案例资源',
path: '/resource/case',
},
{ {
tag: 'v1-resource-other-information-list', tag: 'v1-resource-other-information-list',
icon: Files, icon: Files,
name: '其他资料', name: '其他资料',
path: '/resource/other', path: '/resource/other',
}, },
{
tag: 'v1-resource-case',
icon: DocumentChecked,
name: '案例管理',
path: '/resource/case',
},
{ {
icon: Collection, icon: Collection,
name: '题库管理', name: '题库管理',
......
...@@ -34,6 +34,8 @@ const loading = ref(false) ...@@ -34,6 +34,8 @@ const loading = ref(false)
const tableRef = ref() const tableRef = ref()
const dataList = ref<any[]>([]) const dataList = ref<any[]>([])
const page = reactive({ total: 0, size: props.limit, currentPage: 1 }) const page = reactive({ total: 0, size: props.limit, currentPage: 1 })
let requestSequence = 0
let activationCount = 0
const params = reactive({ ...props.remote?.params }) const params = reactive({ ...props.remote?.params })
watch( watch(
...@@ -70,15 +72,18 @@ const fetchList = (isReset = false) => { ...@@ -70,15 +72,18 @@ const fetchList = (isReset = false) => {
if (beforeRequest) { if (beforeRequest) {
requestParams = beforeRequest(requestParams, isReset) requestParams = beforeRequest(requestParams, isReset)
} }
for (const key in params) { // 只清理请求副本,保留表单响应式参数的完整结构
if (params[key] === '' || params[key] === undefined || params[key] === undefined) { for (const key in requestParams) {
delete params[key] if (requestParams[key] === '' || requestParams[key] === undefined || requestParams[key] === null) {
delete requestParams[key]
} }
} }
const currentRequest = ++requestSequence
loading.value = true loading.value = true
return ( return (
httpRequest(requestParams) httpRequest(requestParams)
.then((res: any) => { .then((res: any) => {
if (currentRequest !== requestSequence) return
const { list = [], total = 0 } = callback ? callback(res.data, requestParams) : res.data || {} const { list = [], total = 0 } = callback ? callback(res.data, requestParams) : res.data || {}
page.total = total page.total = total
dataList.value = list dataList.value = list
...@@ -88,7 +93,7 @@ const fetchList = (isReset = false) => { ...@@ -88,7 +93,7 @@ const fetchList = (isReset = false) => {
// dataList.value = [] // dataList.value = []
// }) // })
.finally(() => { .finally(() => {
loading.value = false if (currentRequest === requestSequence) loading.value = false
}) })
) )
} }
...@@ -125,7 +130,12 @@ onMounted(() => { ...@@ -125,7 +130,12 @@ onMounted(() => {
fetchList() fetchList()
}) })
defineExpose({ refetch, tableRef, params, loading }) // keep-alive 页面从详情返回时,保留筛选和分页并刷新当前数据
onActivated(() => {
if (activationCount++ > 0) fetchList()
})
defineExpose({ refetch, search, reset, tableRef, params, loading, page })
</script> </script>
<template> <template>
...@@ -177,7 +187,7 @@ defineExpose({ refetch, tableRef, params, loading }) ...@@ -177,7 +187,7 @@ defineExpose({ refetch, tableRef, params, loading })
<slot></slot> <slot></slot>
<!-- 主体 --> <!-- 主体 -->
<div class="table-list-bd"> <div class="table-list-bd">
<slot name="body" v-bind="{ data: dataList }"> <slot name="body" v-bind="{ data: dataList, loading }">
<el-table <el-table
:data="dataList" :data="dataList"
v-loading="loading" v-loading="loading"
......
<script setup lang="ts">
import { menus } from '@/assets/menus'
interface BreadcrumbItem {
title: string
path?: string
}
const route = useRoute()
const isExternal = (path: string) => /^https?:\/\//.test(path)
const isMatch = (currentPath: string, targetPath: string) =>
!!targetPath && !isExternal(targetPath) && (currentPath === targetPath || currentPath.startsWith(`${targetPath}/`))
const resolvePageTitle = (title: string) => {
if (!title.startsWith('新建/编辑')) return title
const resourceName = title.slice('新建/编辑'.length)
return `${route.query.id ? '编辑' : '新建'}${resourceName}`
}
const items = computed<BreadcrumbItem[]>(() => {
const currentPath = route.path
const menuPath = String(route.meta.menuPath || currentPath)
const group = menus.find((item) => item.children?.some((child) => isMatch(menuPath, child.path)))
if (!group) {
return route.matched
.filter((item) => item.meta.title)
.map((item) => ({ title: String(item.meta.title), path: item.path }))
}
const menuItem = [...(group.children || [])]
.filter((item) => isMatch(menuPath, item.path))
.sort((a, b) => b.path.length - a.path.length)[0]
const result: BreadcrumbItem[] = [{ title: group.name }]
if (menuItem) result.push({ title: menuItem.name, path: menuItem.path })
const pageTitle = [...route.matched].reverse().find((item) => item.meta.title)?.meta.title
if (pageTitle && pageTitle !== menuItem?.name) result.push({ title: resolvePageTitle(String(pageTitle)) })
return result
})
</script>
<template> <template>
<div class="app-breadcrumb" v-if="routes.length"> <nav class="app-breadcrumb" v-if="items.length" aria-label="面包屑导航">
<el-breadcrumb> <el-breadcrumb separator="/">
<el-breadcrumb-item v-for="route in routes" :key="route.path"> <el-breadcrumb-item v-for="(item, index) in items" :key="`${item.title}-${index}`">
<router-link :to="route.path">{{ route.meta.title }}</router-link> <router-link v-if="item.path && index < items.length - 1" :to="item.path">{{ item.title }}</router-link>
<span v-else>{{ item.title }}</span>
</el-breadcrumb-item> </el-breadcrumb-item>
</el-breadcrumb> </el-breadcrumb>
</div> </nav>
</template> </template>
<script>
export default {
name: 'AppBreadcrumb',
computed: {
routes() {
return this.$route.matched.filter(route => route.meta.title)
}
}
}
</script>
<style lang="scss"> <style lang="scss">
.app-breadcrumb { .app-breadcrumb {
padding: 18px 0 32px; padding: 8px 0 20px;
.el-breadcrumb { font-size: 14px; line-height: 20px; }
.el-breadcrumb { .el-breadcrumb__inner { color: #909399; font-weight: 400; }
font-size: 20px; .el-breadcrumb__inner a { color: #606266; font-weight: 400; }
font-weight: 400; .el-breadcrumb__inner a:hover { color: var(--el-color-primary); }
line-height: 1; .el-breadcrumb__item:last-child .el-breadcrumb__inner { color: #303133; font-weight: 600; }
}
.el-breadcrumb__inner a {
font-weight: normal;
color: #5b91fd;
}
.router-link-active {
color: #1a1b1c;
}
} }
</style> </style>
...@@ -19,7 +19,12 @@ const dialogVisible = ref(false) ...@@ -19,7 +19,12 @@ const dialogVisible = ref(false)
<app-breadcrumb v-if="hasBreadcrumb"></app-breadcrumb> <app-breadcrumb v-if="hasBreadcrumb"></app-breadcrumb>
</div> </div>
<div class="app-main-container"> <div class="app-main-container">
<router-view></router-view> <router-view v-slot="{ Component, route }">
<keep-alive :max="10">
<component :is="Component" v-if="route.meta.keepAlive" :key="route.path" />
</keep-alive>
<component :is="Component" v-if="!route.meta.keepAlive" :key="route.path" />
</router-view>
</div> </div>
</div> </div>
</section> </section>
......
<script setup lang="ts">
defineProps<{
modelValue?: string | number
data: any[]
}>()
const emit = defineEmits<{
(event: 'update:modelValue', value?: string | number): void
(event: 'change', value?: string | number): void
}>()
const treeProps = {
children: 'children',
label: 'category_name',
value: 'id',
}
const handleChange = (value?: string | number) => {
emit('update:modelValue', value)
emit('change', value)
}
</script>
<template>
<el-tree-select
node-key="id"
value-key="id"
clearable
check-strictly
:render-after-expand="false"
:model-value="modelValue"
:props="treeProps"
:data="data"
:default-expanded-keys="data.length ? [data[0]?.id] : []"
@update:model-value="handleChange" />
</template>
<script setup lang="ts">
import { useStorage } from '@vueuse/core'
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
import ResourceViewSwitch from './ResourceViewSwitch.vue'
const props = withDefaults(
defineProps<{
data: any[]
resources: any[]
modelValue?: string | number
storageKey?: string
emptyText?: string
loading?: boolean
}>(),
{
storageKey: 'resource-column-content-mode',
emptyText: '当前类别暂无资源',
}
)
const emit = defineEmits<{
(event: 'update:modelValue', value?: string | number): void
(event: 'change', value?: string | number): void
}>()
type ContentMode = 'card' | 'list'
const contentMode = useStorage<ContentMode>(props.storageKey, 'list')
const selectedPath = ref<any[]>([])
const findPath = (nodes: any[], id: string | number, parents: any[] = []): any[] => {
for (const node of nodes || []) {
const path = [...parents, node]
if (String(node.id) === String(id)) return path
const childPath = findPath(node.children || [], id, path)
if (childPath.length) return childPath
}
return []
}
watch(
[() => props.modelValue, () => props.data],
([id]) => {
selectedPath.value = id === undefined || id === '' ? [] : findPath(props.data, id as string | number)
},
{ immediate: true, deep: true }
)
const visibleCategories = computed(() => {
if (!selectedPath.value.length) return props.data || []
const current = selectedPath.value.at(-1)
if (current.children?.length) return current.children
return selectedPath.value.at(-2)?.children || props.data || []
})
const updateCategory = (path: any[]) => {
selectedPath.value = path
const value = path.at(-1)?.id
emit('update:modelValue', value)
emit('change', value)
}
const selectCategory = (category: any) => {
const current = selectedPath.value.at(-1)
const parent = selectedPath.value.at(-2)
if (current?.children?.some((item: any) => String(item.id) === String(category.id))) {
updateCategory([...selectedPath.value, category])
} else if (parent?.children?.some((item: any) => String(item.id) === String(category.id))) {
updateCategory([...selectedPath.value.slice(0, -1), category])
} else {
updateCategory([category])
}
}
const navigateTo = (level: number) => updateCategory(selectedPath.value.slice(0, level + 1))
const clearCategory = () => updateCategory([])
const goBack = () => {
if (selectedPath.value.length <= 1) clearCategory()
else updateCategory(selectedPath.value.slice(0, -1))
}
</script>
<template>
<div class="resource-column-browser">
<div class="category-breadcrumb">
<div class="breadcrumb-path">
<button type="button" class="breadcrumb-item" @click="clearCategory">全部</button>
<template v-for="(category, level) in selectedPath" :key="category.id">
<ArrowRight />
<button type="button" class="breadcrumb-item" @click="navigateTo(level)">{{ category.category_name }}</button>
</template>
</div>
<ResourceViewSwitch v-model="contentMode" />
</div>
<div class="browser-content">
<section class="category-column">
<div class="column-toolbar">
<el-button :disabled="!selectedPath.length" link type="primary" @click="goBack">
<ArrowLeft />
返回
</el-button>
</div>
<button
v-for="category in visibleCategories"
:key="category.id"
type="button"
class="category-row"
:class="{ selected: String(selectedPath.at(-1)?.id) === String(category.id) }"
@click="selectCategory(category)">
<span>{{ category.category_name }}</span>
<ArrowRight v-if="category.children?.length" />
</button>
<el-empty v-if="!visibleCategories.length" description="暂无子类别" :image-size="56" />
</section>
<section class="resource-content" v-loading="loading">
<slot v-if="resources.length" :name="contentMode" :data="resources" />
<el-empty v-else :description="emptyText" />
</section>
</div>
</div>
</template>
<style lang="scss" scoped>
.resource-column-browser { min-height: 520px; border: 1px solid #e4e7ed; border-radius: 8px; background: #fff; }
.category-breadcrumb { display: flex; align-items: center; height: 46px; gap: 12px; padding: 0 14px; border-bottom: 1px solid #e4e7ed; }
.breadcrumb-path { display: flex; min-width: 0; flex: 1; align-items: center; gap: 5px; overflow-x: auto; white-space: nowrap; }
.breadcrumb-path > svg { width: 12px; flex: 0 0 auto; color: #c0c4cc; }
.breadcrumb-item { padding: 4px 5px; color: #606266; border: 0; background: transparent; cursor: pointer; }
.breadcrumb-item:hover { color: var(--el-color-primary); }
.breadcrumb-item:last-child { color: #303133; font-weight: 600; }
.browser-content { display: flex; min-height: 474px; }
.category-column { width: 240px; min-width: 240px; padding: 8px; border-right: 1px solid #e4e7ed; }
.column-toolbar { display: flex; align-items: center; min-height: 40px; padding: 0 8px; }
.column-toolbar .el-button svg { width: 14px; margin-right: 3px; }
.category-row { display: flex; align-items: center; width: 100%; height: 38px; padding: 0 9px; color: #303133; border: 0; border-radius: 6px; background: transparent; cursor: pointer; text-align: left; }
.category-row span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.category-row svg { width: 14px; color: #909399; }
.category-row:hover { background: #f5f7fa; }
.category-row.selected { color: #fff; background: var(--el-color-primary); }
.category-row.selected svg { color: #fff; }
.resource-content { min-width: 0; flex: 1; padding: 8px; background: #fafafa; }
@media (max-width: 1100px) {
.category-column { width: 200px; min-width: 200px; }
}
@media (max-width: 767px) {
.category-breadcrumb { padding: 0 10px; }
.browser-content { display: block; }
.category-column { width: auto; min-width: 0; border-right: 0; border-bottom: 1px solid #e4e7ed; }
.resource-content { min-height: 320px; }
}
</style>
<script setup lang="ts">
type ViewMode = 'columns' | 'card' | 'list'
const props = withDefaults(
defineProps<{
modelValue?: ViewMode
modes?: ViewMode[]
}>(),
{ modes: () => ['card', 'list'] }
)
const emit = defineEmits<{
(event: 'update:modelValue', value: ViewMode): void
}>()
const labels: Record<ViewMode, string> = {
columns: '分栏展示',
card: '卡片展示',
list: '列表展示',
}
const defaultMode = computed<ViewMode>(() => (props.modes.includes('columns') ? 'columns' : 'list'))
onMounted(() => {
if (!props.modelValue || !props.modes.includes(props.modelValue)) emit('update:modelValue', defaultMode.value)
})
</script>
<template>
<div class="view-switch" role="group" aria-label="展示方式">
<button
v-for="mode in modes"
:key="mode"
type="button"
class="view-switch__button"
:class="{ 'is-active': modelValue === mode }"
:title="labels[mode]"
:aria-label="labels[mode]"
:aria-pressed="modelValue === mode"
@click="emit('update:modelValue', mode)">
<span v-if="mode === 'card'" class="view-switch__icon view-switch__icon--card" aria-hidden="true">
<span v-for="item in 4" :key="item"></span>
</span>
<span v-else-if="mode === 'list'" class="view-switch__icon view-switch__icon--list" aria-hidden="true">
<span v-for="item in 3" :key="item"></span>
</span>
<span v-else class="view-switch__icon view-switch__icon--columns" aria-hidden="true">
<span v-for="item in 3" :key="item"></span>
</span>
</button>
</div>
</template>
<style lang="scss" scoped>
.view-switch { display: inline-flex; flex: 0 0 auto; overflow: hidden; border: 1px solid #e6e8eb; border-radius: 10px; background: #fff; }
.view-switch__button { display: inline-flex; width: 34px; height: 32px; padding: 0; align-items: center; justify-content: center; color: #7a7f87; border: 0; background: transparent; cursor: pointer; transition: color 0.2s ease, background-color 0.2s ease; }
.view-switch__button:hover { background: #f6f7f9; }
.view-switch__button:focus-visible { position: relative; outline: 2px solid var(--el-color-primary-light-5); outline-offset: -2px; }
.view-switch__button.is-active { color: var(--el-color-primary); background: var(--el-color-primary-light-9); }
.view-switch__icon { display: inline-flex; width: 14px; height: 14px; color: currentColor; }
.view-switch__icon--card { display: grid; grid-template-columns: repeat(2, 1fr); gap: 2px; }
.view-switch__icon--card span { width: 6px; height: 6px; box-sizing: border-box; border: 1.25px solid currentColor; border-radius: 2px; }
.view-switch__icon--list { flex-direction: column; justify-content: space-between; padding: 1px 0; }
.view-switch__icon--list span { width: 14px; height: 2px; border-radius: 999px; background: currentColor; }
.view-switch__icon--columns { gap: 2px; }
.view-switch__icon--columns span { width: 4px; height: 14px; box-sizing: border-box; border: 1.25px solid currentColor; border-radius: 1.5px; }
</style>
...@@ -6,9 +6,9 @@ export const routes: Array<RouteRecordRaw> = [ ...@@ -6,9 +6,9 @@ export const routes: Array<RouteRecordRaw> = [
path: '/admin/teacher', path: '/admin/teacher',
component: AppLayout, component: AppLayout,
children: [ children: [
{ path: '', component: () => import('./views/List.vue') }, { path: '', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: '/admin/teacher/detail', component: () => import('./views/Detail.vue') }, { path: '/admin/teacher/detail', component: () => import('./views/Detail.vue'), meta: { title: '讲师详情' } },
{ path: '/admin/teacher/add', name: 'update', component: () => import('./views/Add.vue') } { path: '/admin/teacher/add', name: 'update', component: () => import('./views/Add.vue'), meta: { title: '新增/编辑讲师' } }
] ]
} }
] ]
...@@ -6,8 +6,8 @@ export const routes: Array<RouteRecordRaw> = [ ...@@ -6,8 +6,8 @@ export const routes: Array<RouteRecordRaw> = [
path: '/course/update-course', path: '/course/update-course',
component: AppLayout, component: AppLayout,
children: [ children: [
{ path: '', component: () => import('./views/StepOne.vue') }, { path: '', component: () => import('./views/StepOne.vue'), meta: { title: '课程基本信息' } },
{ path: '/course/update-course/stepTwo', component: () => import('./views/StepTwo.vue') } { path: '/course/update-course/stepTwo', component: () => import('./views/StepTwo.vue'), meta: { title: '课程内容编辑' } }
] ]
} }
] ]
...@@ -6,12 +6,12 @@ export const routes: Array<RouteRecordRaw> = [ ...@@ -6,12 +6,12 @@ export const routes: Array<RouteRecordRaw> = [
path: '/course/my', path: '/course/my',
component: AppLayout, component: AppLayout,
children: [ children: [
{ path: '', component: () => import('./views/List.vue') }, { path: '', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: '/course/my/view', component: () => import('./views/View.vue') }, { path: '/course/my/view', component: () => import('./views/View.vue'), meta: { title: '课程详情' } },
// id: 章节id courseId课程id // id: 章节id courseId课程id
{ path: '/course/my/view/:id/:courseId', component: () => import('./views/ViewDetails.vue') }, { path: '/course/my/view/:id/:courseId', component: () => import('./views/ViewDetails.vue'), meta: { title: '章节详情' } },
{ path: '/course/my/graph', component: () => import('./views/Graph.vue') }, { path: '/course/my/graph', component: () => import('./views/Graph.vue'), meta: { title: '课程图谱' } },
], ],
}, },
{ path: '/course/my/preview', component: () => import('./views/Preview.vue') }, { path: '/course/my/preview', component: () => import('./views/Preview.vue'), meta: { title: '课程预览', menuPath: '/course/my' } },
] ]
...@@ -6,9 +6,9 @@ export const routes: Array<RouteRecordRaw> = [ ...@@ -6,9 +6,9 @@ export const routes: Array<RouteRecordRaw> = [
path: '/resource/case', path: '/resource/case',
component: AppLayout, component: AppLayout,
children: [ children: [
{ path: '', component: () => import('./views/List.vue') }, { path: '', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: '/resource/case/update', component: () => import('./views/Update.vue') }, { path: '/resource/case/update', component: () => import('./views/Update.vue'), meta: { title: '新建/编辑案例' } },
{ path: '/resource/case/view', component: () => import('./views/View.vue') }, { path: '/resource/case/view', component: () => import('./views/View.vue'), meta: { title: '案例详情' } },
], ],
}, },
] ]
...@@ -6,9 +6,9 @@ export const routes: Array<RouteRecordRaw> = [ ...@@ -6,9 +6,9 @@ export const routes: Array<RouteRecordRaw> = [
path: '/resource/courseware', path: '/resource/courseware',
component: AppLayout, component: AppLayout,
children: [ children: [
{ path: '', component: () => import('./views/List.vue') }, { path: '', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: '/resource/courseware/update', component: () => import('./views/Update.vue') }, { path: '/resource/courseware/update', component: () => import('./views/Update.vue'), meta: { title: '新建/编辑课件' } },
{ path: '/resource/courseware/view', component: () => import('./views/View.vue') } { path: '/resource/courseware/view', component: () => import('./views/View.vue'), meta: { title: '课件详情' } }
// { path: 'update/:id', component: () => import('./views/Update.vue'), props: true } // { path: 'update/:id', component: () => import('./views/Update.vue'), props: true }
] ]
} }
......
...@@ -6,6 +6,10 @@ import { useMapStore } from '@/stores/map' ...@@ -6,6 +6,10 @@ import { useMapStore } from '@/stores/map'
import { useGetCategoryList } from '@/composables/useGetCategoryList' import { useGetCategoryList } from '@/composables/useGetCategoryList'
import { useProjectList } from '@/composables/useGetProjectList' import { useProjectList } from '@/composables/useGetProjectList'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { ArrowRight } from '@element-plus/icons-vue'
import ResourceCategorySelect from '@/components/resource/ResourceCategorySelect.vue'
import ResourceColumnBrowser from '@/components/resource/ResourceColumnBrowser.vue'
import ResourceViewSwitch from '@/components/resource/ResourceViewSwitch.vue'
// 判断当前用户是不是超级管理员 // 判断当前用户是不是超级管理员
const user = useUserStore().roles const user = useUserStore().roles
...@@ -18,24 +22,17 @@ const departmentList: any = useProjectList('', '79806610719731712').departmentLi ...@@ -18,24 +22,17 @@ const departmentList: any = useProjectList('', '79806610719731712').departmentLi
// 下拉选择tree 视频分类 // 下拉选择tree 视频分类
let { list: selectTree }: any = useGetCategoryList() let { list: selectTree }: any = useGetCategoryList()
const defaultProps = {
children: 'children',
label: 'category_name',
value: 'id',
}
const appList = ref() const appList = ref()
// 列表切换 type ViewMode = 'columns' | 'card' | 'list'
const isCard = useStorage('isCard', true) const viewMode = useStorage<ViewMode>('resource-courseware-view-mode', 'columns')
const changeCard = () => { const selectedCategory = ref<string | number>()
isCard.value = !isCard.value
}
// 资源出处 tab触发 // 资源出处 tab触发
const tabValue = ref('1') const tabValue = ref('1')
const tabChange = () => { const tabChange = () => {
appList.value.refetch() appList.value.search()
} }
const listOptions = computed(() => { const listOptions = computed(() => {
...@@ -83,8 +80,27 @@ const listOptions = computed(() => { ...@@ -83,8 +80,27 @@ const listOptions = computed(() => {
}) })
const typeFilter = () => { const typeFilter = () => {
appList.value.refetch() appList.value.search()
}
const categoryFilter = (category?: string | number) => {
selectedCategory.value = category
appList.value.search()
}
const changeColumnCategory = (category?: string | number) => {
selectedCategory.value = category
if (appList.value?.params) {
if (category === undefined || category === '') delete appList.value.params.classification
else appList.value.params.classification = category
appList.value.search()
}
} }
watch(
() => appList.value?.params?.classification,
(category) => { selectedCategory.value = category }
)
</script> </script>
<template> <template>
...@@ -95,10 +111,7 @@ const typeFilter = () => { ...@@ -95,10 +111,7 @@ const typeFilter = () => {
<el-tab-pane label="部门资源" name="2"></el-tab-pane> <el-tab-pane label="部门资源" name="2"></el-tab-pane>
<el-tab-pane label="公开资源" name="3"></el-tab-pane> <el-tab-pane label="公开资源" name="3"></el-tab-pane>
</el-tabs> </el-tabs>
<div class="video-head-icon" @click="changeCard"> <div class="view-switcher"><ResourceViewSwitch v-model="viewMode" :modes="['columns', 'card', 'list']" /></div>
<img v-if="!isCard" src="/center_resource/change_card.png" alt="" />
<img v-else src="/center_resource/change_list.png" alt="" />
</div>
</div> </div>
<div class="video-tool-btn"> <div class="video-tool-btn">
<router-link <router-link
...@@ -111,14 +124,7 @@ const typeFilter = () => { ...@@ -111,14 +124,7 @@ const typeFilter = () => {
<AppList v-bind="listOptions" ref="appList"> <AppList v-bind="listOptions" ref="appList">
<template #header-aside></template> <template #header-aside></template>
<template #filter-type="{ params }"> <template #filter-type="{ params }">
<el-tree-select <ResourceCategorySelect v-model="params.classification" :data="selectTree" @change="categoryFilter" />
node-key="id"
@change="typeFilter"
clearable
:props="defaultProps"
v-model="params.classification"
:data="selectTree"
:default-expanded-keys="selectTree.length ? [selectTree[0]?.id] : []" />
</template> </template>
<template v-if="tabValue == '3' && isAdmin" #filter-department="{ params }"> <template v-if="tabValue == '3' && isAdmin" #filter-department="{ params }">
<div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">部门:</div> <div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">部门:</div>
...@@ -138,7 +144,7 @@ const typeFilter = () => { ...@@ -138,7 +144,7 @@ const typeFilter = () => {
</template> </template>
<!-- 卡片 --> <!-- 卡片 -->
<template #body="{ data }" v-if="isCard"> <template #body="{ data }" v-if="viewMode === 'card'">
<div class="card-list" v-if="data.length"> <div class="card-list" v-if="data.length">
<CardListItem <CardListItem
:permission="{ edit: 'v1-resource-courseware-update', view: 'v1-resource-courseware-view' }" :permission="{ edit: 'v1-resource-courseware-update', view: 'v1-resource-courseware-view' }"
...@@ -148,7 +154,37 @@ const typeFilter = () => { ...@@ -148,7 +154,37 @@ const typeFilter = () => {
:key="index" :key="index"
:tabIndex="tabValue"></CardListItem> :tabIndex="tabValue"></CardListItem>
</div> </div>
<!-- <el-empty v-else description="暂无数据" /> --> <el-empty v-else description="暂无课件" />
</template>
<template #body="{ data, loading }" v-else-if="viewMode === 'columns'">
<ResourceColumnBrowser
v-model="selectedCategory"
:data="selectTree"
:resources="data"
:loading="loading"
storage-key="resource-courseware-column-content-mode"
empty-text="当前类别暂无课件"
@change="changeColumnCategory">
<template #card="{ data: resources }">
<div class="column-card-list">
<CardListItem
v-for="item in resources"
:key="item.id"
:data="item"
:tabIndex="tabValue"
path="/resource/courseware"
:permission="{ edit: 'v1-resource-courseware-update', view: 'v1-resource-courseware-view' }" />
</div>
</template>
<template #list="{ data: resources }">
<div class="column-resource-list">
<router-link v-for="item in resources" v-permission="'v1-resource-courseware-view'" :key="item.id" class="column-resource-row" :to="`/resource/courseware/view/?id=${item.id}`">
<div class="resource-summary"><strong>{{ item.name }}</strong><span>{{ item.classification_name || '未分类' }} · {{ item.updated_time || '-' }}</span></div>
<ArrowRight />
</router-link>
</div>
</template>
</ResourceColumnBrowser>
</template> </template>
</AppList> </AppList>
</AppCard> </AppCard>
...@@ -163,15 +199,20 @@ const typeFilter = () => { ...@@ -163,15 +199,20 @@ const typeFilter = () => {
} }
.video-head { .video-head {
position: relative; position: relative;
.video-head-icon { .view-switcher {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
font-size: 30px;
color: #666;
cursor: pointer;
} }
} }
.column-card-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 16px; padding: 8px; }
.column-resource-list { padding: 8px; }
.column-resource-row { display: flex; align-items: center; gap: 14px; padding: 14px 10px; color: #303133; border-bottom: 1px solid #ebeef5; text-decoration: none; }
.column-resource-row:hover { background: #fff; }
.column-resource-row > svg { width: 16px; color: #c0c4cc; }
.resource-summary { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 7px; }
.resource-summary strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.resource-summary span { color: #909399; font-size: 13px; }
.video-tool-btn { .video-tool-btn {
padding: 10px 0 30px 0; padding: 10px 0 30px 0;
} }
......
...@@ -6,9 +6,9 @@ export const routes: Array<RouteRecordRaw> = [ ...@@ -6,9 +6,9 @@ export const routes: Array<RouteRecordRaw> = [
path: '/resource/lessonplan', path: '/resource/lessonplan',
component: AppLayout, component: AppLayout,
children: [ children: [
{ path: '', component: () => import('./views/List.vue') }, { path: '', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: '/resource/lessonplan/update', component: () => import('./views/Update.vue') }, { path: '/resource/lessonplan/update', component: () => import('./views/Update.vue'), meta: { title: '新建/编辑教案' } },
{ path: '/resource/lessonplan/view', component: () => import('./views/View.vue') } { path: '/resource/lessonplan/view', component: () => import('./views/View.vue'), meta: { title: '教案详情' } }
// { path: 'update/:id', component: () => import('./views/Update.vue'), props: true } // { path: 'update/:id', component: () => import('./views/Update.vue'), props: true }
] ]
} }
......
...@@ -6,6 +6,10 @@ import { useMapStore } from '@/stores/map' ...@@ -6,6 +6,10 @@ import { useMapStore } from '@/stores/map'
import { useGetCategoryList } from '@/composables/useGetCategoryList' import { useGetCategoryList } from '@/composables/useGetCategoryList'
import { useProjectList } from '@/composables/useGetProjectList' import { useProjectList } from '@/composables/useGetProjectList'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { ArrowRight } from '@element-plus/icons-vue'
import ResourceCategorySelect from '@/components/resource/ResourceCategorySelect.vue'
import ResourceColumnBrowser from '@/components/resource/ResourceColumnBrowser.vue'
import ResourceViewSwitch from '@/components/resource/ResourceViewSwitch.vue'
// 判断当前用户是不是超级管理员 // 判断当前用户是不是超级管理员
const user = useUserStore().roles const user = useUserStore().roles
...@@ -15,27 +19,20 @@ const store = useMapStore() ...@@ -15,27 +19,20 @@ const store = useMapStore()
const appList = ref() const appList = ref()
// 列表切换 type ViewMode = 'columns' | 'card' | 'list'
const isCard = useStorage('isCard', true) const viewMode = useStorage<ViewMode>('resource-lessonplan-view-mode', 'columns')
const changeCard = () => { const selectedCategory = ref<string | number>()
isCard.value = !isCard.value
}
// 筛选部门列表 // 筛选部门列表
const departmentList: any = useProjectList('', '79806610719731712').departmentList const departmentList: any = useProjectList('', '79806610719731712').departmentList
// 下拉选择tree 视频分类 // 下拉选择tree 视频分类
let { list: selectTree }: any = useGetCategoryList() let { list: selectTree }: any = useGetCategoryList()
const defaultProps = {
children: 'children',
label: 'category_name',
value: 'id',
}
// 资源出处 tab触发 // 资源出处 tab触发
const tabValue = ref('1') const tabValue = ref('1')
const tabChange = () => { const tabChange = () => {
appList.value.refetch() appList.value.search()
} }
const listOptions = computed(() => { const listOptions = computed(() => {
...@@ -83,8 +80,27 @@ const listOptions = computed(() => { ...@@ -83,8 +80,27 @@ const listOptions = computed(() => {
}) })
const typeFilter = () => { const typeFilter = () => {
appList.value.refetch() appList.value.search()
}
const categoryFilter = (category?: string | number) => {
selectedCategory.value = category
appList.value.search()
} }
const changeColumnCategory = (category?: string | number) => {
selectedCategory.value = category
if (appList.value?.params) {
if (category === undefined || category === '') delete appList.value.params.classification
else appList.value.params.classification = category
appList.value.search()
}
}
watch(
() => appList.value?.params?.classification,
(category) => { selectedCategory.value = category }
)
</script> </script>
<template> <template>
...@@ -95,10 +111,7 @@ const typeFilter = () => { ...@@ -95,10 +111,7 @@ const typeFilter = () => {
<el-tab-pane label="部门资源" name="2"></el-tab-pane> <el-tab-pane label="部门资源" name="2"></el-tab-pane>
<el-tab-pane label="公开资源" name="3"></el-tab-pane> <el-tab-pane label="公开资源" name="3"></el-tab-pane>
</el-tabs> </el-tabs>
<div class="video-head-icon" @click="changeCard"> <div class="view-switcher"><ResourceViewSwitch v-model="viewMode" :modes="['columns', 'card', 'list']" /></div>
<img v-if="!isCard" src="/center_resource/change_card.png" alt="" />
<img v-else src="/center_resource/change_list.png" alt="" />
</div>
</div> </div>
<div class="video-tool-btn"> <div class="video-tool-btn">
<router-link <router-link
...@@ -111,14 +124,7 @@ const typeFilter = () => { ...@@ -111,14 +124,7 @@ const typeFilter = () => {
<AppList v-bind="listOptions" ref="appList"> <AppList v-bind="listOptions" ref="appList">
<template #header-aside></template> <template #header-aside></template>
<template #filter-type="{ params }"> <template #filter-type="{ params }">
<el-tree-select <ResourceCategorySelect v-model="params.classification" :data="selectTree" @change="categoryFilter" />
node-key="id"
@change="typeFilter"
clearable
:props="defaultProps"
v-model="params.classification"
:data="selectTree"
:default-expanded-keys="selectTree.length ? [selectTree[0]?.id] : []" />
</template> </template>
<template v-if="tabValue == '3' && isAdmin" #filter-department="{ params }"> <template v-if="tabValue == '3' && isAdmin" #filter-department="{ params }">
<div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">部门:</div> <div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">部门:</div>
...@@ -138,7 +144,7 @@ const typeFilter = () => { ...@@ -138,7 +144,7 @@ const typeFilter = () => {
</template> </template>
<!-- 卡片 --> <!-- 卡片 -->
<template #body="{ data }" v-if="isCard"> <template #body="{ data }" v-if="viewMode === 'card'">
<div class="card-list" v-if="data.length"> <div class="card-list" v-if="data.length">
<CardListItem <CardListItem
:tabIndex="tabValue" :tabIndex="tabValue"
...@@ -148,7 +154,37 @@ const typeFilter = () => { ...@@ -148,7 +154,37 @@ const typeFilter = () => {
:data="item" :data="item"
:key="index"></CardListItem> :key="index"></CardListItem>
</div> </div>
<!-- <el-empty v-else description="暂无数据" /> --> <el-empty v-else description="暂无教案" />
</template>
<template #body="{ data, loading }" v-else-if="viewMode === 'columns'">
<ResourceColumnBrowser
v-model="selectedCategory"
:data="selectTree"
:resources="data"
:loading="loading"
storage-key="resource-lessonplan-column-content-mode"
empty-text="当前类别暂无教案"
@change="changeColumnCategory">
<template #card="{ data: resources }">
<div class="column-card-list">
<CardListItem
v-for="item in resources"
:key="item.id"
:data="item"
:tabIndex="tabValue"
path="/resource/lessonplan"
:permission="{ edit: 'v1-resource-lesson-plan-update', view: 'v1-resource-lesson-plan-view' }" />
</div>
</template>
<template #list="{ data: resources }">
<div class="column-resource-list">
<router-link v-for="item in resources" v-permission="'v1-resource-lesson-plan-view'" :key="item.id" class="column-resource-row" :to="`/resource/lessonplan/view?id=${item.id}`">
<div class="resource-summary"><strong>{{ item.name }}</strong><span>{{ item.classification_name || '未分类' }} · {{ item.updated_time || '-' }}</span></div>
<ArrowRight />
</router-link>
</div>
</template>
</ResourceColumnBrowser>
</template> </template>
</AppList> </AppList>
</AppCard> </AppCard>
...@@ -163,15 +199,20 @@ const typeFilter = () => { ...@@ -163,15 +199,20 @@ const typeFilter = () => {
} }
.video-head { .video-head {
position: relative; position: relative;
.video-head-icon { .view-switcher {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
font-size: 30px;
color: #666;
cursor: pointer;
} }
} }
.column-card-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 16px; padding: 8px; }
.column-resource-list { padding: 8px; }
.column-resource-row { display: flex; align-items: center; gap: 14px; padding: 14px 10px; color: #303133; border-bottom: 1px solid #ebeef5; text-decoration: none; }
.column-resource-row:hover { background: #fff; }
.column-resource-row > svg { width: 16px; color: #c0c4cc; }
.resource-summary { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 7px; }
.resource-summary strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.resource-summary span { color: #909399; font-size: 13px; }
.video-tool-btn { .video-tool-btn {
padding: 10px 0 30px 0; padding: 10px 0 30px 0;
} }
......
...@@ -6,9 +6,9 @@ export const routes: Array<RouteRecordRaw> = [ ...@@ -6,9 +6,9 @@ export const routes: Array<RouteRecordRaw> = [
path: '/resource/other', path: '/resource/other',
component: AppLayout, component: AppLayout,
children: [ children: [
{ path: '', component: () => import('./views/List.vue') }, { path: '', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: '/resource/other/update', component: () => import('./views/Update.vue') }, { path: '/resource/other/update', component: () => import('./views/Update.vue'), meta: { title: '新建/编辑其他资料' } },
{ path: '/resource/other/view', component: () => import('./views/View.vue') } { path: '/resource/other/view', component: () => import('./views/View.vue'), meta: { title: '其他资料详情' } }
// { path: 'update/:id', component: () => import('./views/Update.vue'), props: true } // { path: 'update/:id', component: () => import('./views/Update.vue'), props: true }
] ]
} }
......
...@@ -7,6 +7,10 @@ import { useProjectList } from '@/composables/useGetProjectList' ...@@ -7,6 +7,10 @@ import { useProjectList } from '@/composables/useGetProjectList'
// 下拉选择tree 视频分类 // 下拉选择tree 视频分类
import { useGetCategoryList } from '@/composables/useGetCategoryList' import { useGetCategoryList } from '@/composables/useGetCategoryList'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { ArrowRight } from '@element-plus/icons-vue'
import ResourceCategorySelect from '@/components/resource/ResourceCategorySelect.vue'
import ResourceColumnBrowser from '@/components/resource/ResourceColumnBrowser.vue'
import ResourceViewSwitch from '@/components/resource/ResourceViewSwitch.vue'
// 判断当前用户是不是超级管理员 // 判断当前用户是不是超级管理员
const user = useUserStore().roles const user = useUserStore().roles
...@@ -16,16 +20,14 @@ const store = useMapStore() ...@@ -16,16 +20,14 @@ const store = useMapStore()
const appList = ref() const appList = ref()
// 列表切换 type ViewMode = 'columns' | 'card' | 'list'
const isCard = useStorage('isCard', true) const viewMode = useStorage<ViewMode>('resource-other-view-mode', 'columns')
const changeCard = () => { const selectedCategory = ref<string | number>()
isCard.value = !isCard.value
}
// 资源出处 tab触发 // 资源出处 tab触发
const tabValue = ref('1') const tabValue = ref('1')
const tabChange = () => { const tabChange = () => {
appList.value.refetch() appList.value.search()
} }
// 筛选部门列表 // 筛选部门列表
...@@ -33,11 +35,6 @@ const departmentList: any = useProjectList('', '79806610719731712').departmentLi ...@@ -33,11 +35,6 @@ const departmentList: any = useProjectList('', '79806610719731712').departmentLi
// 下拉选择tree 视频分类 // 下拉选择tree 视频分类
let { list: selectTree }: any = useGetCategoryList() let { list: selectTree }: any = useGetCategoryList()
const defaultProps = {
children: 'children',
label: 'category_name',
value: 'id',
}
const listOptions = computed(() => { const listOptions = computed(() => {
const columns = [ const columns = [
...@@ -84,8 +81,27 @@ const listOptions = computed(() => { ...@@ -84,8 +81,27 @@ const listOptions = computed(() => {
}) })
const typeFilter = () => { const typeFilter = () => {
appList.value.refetch() appList.value.search()
}
const categoryFilter = (category?: string | number) => {
selectedCategory.value = category
appList.value.search()
} }
const changeColumnCategory = (category?: string | number) => {
selectedCategory.value = category
if (appList.value?.params) {
if (category === undefined || category === '') delete appList.value.params.classification
else appList.value.params.classification = category
appList.value.search()
}
}
watch(
() => appList.value?.params?.classification,
(category) => { selectedCategory.value = category }
)
</script> </script>
<template> <template>
...@@ -96,10 +112,7 @@ const typeFilter = () => { ...@@ -96,10 +112,7 @@ const typeFilter = () => {
<el-tab-pane label="部门资源" name="2"></el-tab-pane> <el-tab-pane label="部门资源" name="2"></el-tab-pane>
<el-tab-pane label="公开资源" name="3"></el-tab-pane> <el-tab-pane label="公开资源" name="3"></el-tab-pane>
</el-tabs> </el-tabs>
<div class="video-head-icon" @click="changeCard"> <div class="view-switcher"><ResourceViewSwitch v-model="viewMode" :modes="['columns', 'card', 'list']" /></div>
<img v-if="!isCard" src="/center_resource/change_card.png" alt="" />
<img v-else src="/center_resource/change_list.png" alt="" />
</div>
</div> </div>
<div class="video-tool-btn"> <div class="video-tool-btn">
<router-link <router-link
...@@ -112,14 +125,7 @@ const typeFilter = () => { ...@@ -112,14 +125,7 @@ const typeFilter = () => {
<AppList v-bind="listOptions" ref="appList"> <AppList v-bind="listOptions" ref="appList">
<template #header-aside></template> <template #header-aside></template>
<template #filter-type="{ params }"> <template #filter-type="{ params }">
<el-tree-select <ResourceCategorySelect v-model="params.classification" :data="selectTree" @change="categoryFilter" />
node-key="id"
@change="typeFilter"
clearable
:props="defaultProps"
v-model="params.classification"
:data="selectTree"
:default-expanded-keys="selectTree.length ? [selectTree[0]?.id] : []" />
</template> </template>
<template v-if="tabValue == '3' && isAdmin" #filter-department="{ params }"> <template v-if="tabValue == '3' && isAdmin" #filter-department="{ params }">
<div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">部门:</div> <div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">部门:</div>
...@@ -139,7 +145,7 @@ const typeFilter = () => { ...@@ -139,7 +145,7 @@ const typeFilter = () => {
</template> </template>
<!-- 卡片 --> <!-- 卡片 -->
<template #body="{ data }" v-if="isCard"> <template #body="{ data }" v-if="viewMode === 'card'">
<div class="card-list" v-if="data.length"> <div class="card-list" v-if="data.length">
<CardListItem <CardListItem
:tabIndex="tabValue" :tabIndex="tabValue"
...@@ -149,7 +155,37 @@ const typeFilter = () => { ...@@ -149,7 +155,37 @@ const typeFilter = () => {
:data="item" :data="item"
:key="index"></CardListItem> :key="index"></CardListItem>
</div> </div>
<!-- <el-empty v-else description="暂无数据" /> --> <el-empty v-else description="暂无其他资料" />
</template>
<template #body="{ data, loading }" v-else-if="viewMode === 'columns'">
<ResourceColumnBrowser
v-model="selectedCategory"
:data="selectTree"
:resources="data"
:loading="loading"
storage-key="resource-other-column-content-mode"
empty-text="当前类别暂无其他资料"
@change="changeColumnCategory">
<template #card="{ data: resources }">
<div class="column-card-list">
<CardListItem
v-for="item in resources"
:key="item.id"
:data="item"
:tabIndex="tabValue"
path="/resource/other"
:permission="{ edit: 'v1-resource-other-information-update', view: 'v1-resource-other-information-view' }" />
</div>
</template>
<template #list="{ data: resources }">
<div class="column-resource-list">
<router-link v-for="item in resources" v-permission="'v1-resource-other-information-view'" :key="item.id" class="column-resource-row" :to="`/resource/other/view/?id=${item.id}`">
<div class="resource-summary"><strong>{{ item.name }}</strong><span>{{ item.classification_name || '未分类' }} · {{ item.updated_time || '-' }}</span></div>
<ArrowRight />
</router-link>
</div>
</template>
</ResourceColumnBrowser>
</template> </template>
</AppList> </AppList>
</AppCard> </AppCard>
...@@ -164,15 +200,20 @@ const typeFilter = () => { ...@@ -164,15 +200,20 @@ const typeFilter = () => {
} }
.video-head { .video-head {
position: relative; position: relative;
.video-head-icon { .view-switcher {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
font-size: 30px;
color: #666;
cursor: pointer;
} }
} }
.column-card-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 16px; padding: 8px; }
.column-resource-list { padding: 8px; }
.column-resource-row { display: flex; align-items: center; gap: 14px; padding: 14px 10px; color: #303133; border-bottom: 1px solid #ebeef5; text-decoration: none; }
.column-resource-row:hover { background: #fff; }
.column-resource-row > svg { width: 16px; color: #c0c4cc; }
.resource-summary { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 7px; }
.resource-summary strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.resource-summary span { color: #909399; font-size: 13px; }
.video-tool-btn { .video-tool-btn {
padding: 10px 0 30px 0; padding: 10px 0 30px 0;
} }
......
...@@ -6,11 +6,11 @@ export const routes: Array<RouteRecordRaw> = [ ...@@ -6,11 +6,11 @@ export const routes: Array<RouteRecordRaw> = [
path: '/resource/video', path: '/resource/video',
component: AppLayout, component: AppLayout,
children: [ children: [
{ path: '', component: () => import('./views/List.vue') }, { path: '', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: 'update', component: () => import('./views/Update.vue') }, { path: 'update', component: () => import('./views/Update.vue'), meta: { title: '新建/编辑视频' } },
{ path: 'view', component: () => import('./views/View.vue') }, { path: 'view', component: () => import('./views/View.vue'), meta: { title: '视频详情' } },
{ path: 'edit-courseware', component: () => import('./views/EditCourseware.vue') }, { path: 'edit-courseware', component: () => import('./views/EditCourseware.vue'), meta: { title: '编辑视频课件' } },
{ path: 'upload', component: () => import('./views/BatchUpload.vue') } { path: 'upload', component: () => import('./views/BatchUpload.vue'), meta: { title: '批量上传视频' } }
// { path: 'update/:id', component: () => import('./views/Update.vue'), props: true } // { path: 'update/:id', component: () => import('./views/Update.vue'), props: true }
] ]
} }
......
...@@ -2,8 +2,11 @@ ...@@ -2,8 +2,11 @@
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { getVideoList } from '../api' import { getVideoList } from '../api'
import CardListItem from '../components/CardListItem.vue' import CardListItem from '../components/CardListItem.vue'
import { PictureFilled } from '@element-plus/icons-vue' import { ArrowRight, PictureFilled, VideoPlay } from '@element-plus/icons-vue'
import { useGetCategoryList } from '@/composables/useGetCategoryList' import { useGetCategoryList } from '@/composables/useGetCategoryList'
import ResourceCategorySelect from '@/components/resource/ResourceCategorySelect.vue'
import ResourceColumnBrowser from '@/components/resource/ResourceColumnBrowser.vue'
import ResourceViewSwitch from '@/components/resource/ResourceViewSwitch.vue'
import { useProjectList } from '@/composables/useGetProjectList' import { useProjectList } from '@/composables/useGetProjectList'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
...@@ -22,20 +25,18 @@ const departmentList: any = useProjectList('', '79806610719731712').departmentLi ...@@ -22,20 +25,18 @@ const departmentList: any = useProjectList('', '79806610719731712').departmentLi
// 筛选下拉选择tree 视频分类 // 筛选下拉选择tree 视频分类
let { list: selectTree }: any = useGetCategoryList() let { list: selectTree }: any = useGetCategoryList()
const defaultProps = {
children: 'children',
label: 'category_name',
value: 'id',
}
// 资源出处 tab触发 // 资源出处 tab触发
const tabValue = ref('1') const tabValue = ref('1')
const appList = ref() const appList = ref()
// const isShowBatchImport = ref(false) // const isShowBatchImport = ref(false)
const tabChange = () => { const tabChange = () => {
appList.value.refetch() appList.value.search()
} }
// 列表切换 type ViewMode = 'columns' | 'card' | 'list'
const isCard = useStorage('isCard', true)
// 展示方式:首次进入默认使用分栏模式
const viewMode = useStorage<ViewMode>('resource-video-view-mode', 'columns')
const selectedCategory = ref<string | number>()
// table 数据 // table 数据
const listOptions = computed(() => { const listOptions = computed(() => {
const columns = [ const columns = [
...@@ -83,11 +84,29 @@ const listOptions = computed(() => { ...@@ -83,11 +84,29 @@ const listOptions = computed(() => {
}) })
const typeFilter = () => { const typeFilter = () => {
appList.value.refetch() appList.value.search()
}
const categoryFilter = (category?: string | number) => {
selectedCategory.value = category
appList.value.search()
} }
const changeCard = () => { // 顶部筛选、重置和分栏选择保持双向同步
isCard.value = !isCard.value watch(
() => appList.value?.params?.classification,
(category) => {
selectedCategory.value = category
}
)
const changeColumnCategory = (category?: string | number) => {
selectedCategory.value = category
if (appList.value?.params) {
if (category === undefined || category === '') delete appList.value.params.classification
else appList.value.params.classification = category
appList.value.search()
}
} }
// 批量上传 // 批量上传
const handleBatchImport = () => { const handleBatchImport = () => {
...@@ -105,9 +124,8 @@ const handleBatchImport = () => { ...@@ -105,9 +124,8 @@ const handleBatchImport = () => {
<el-tab-pane label="部门资源" name="2"></el-tab-pane> <el-tab-pane label="部门资源" name="2"></el-tab-pane>
<el-tab-pane label="公开资源" name="3"></el-tab-pane> <el-tab-pane label="公开资源" name="3"></el-tab-pane>
</el-tabs> </el-tabs>
<div class="video-head-icon" @click="changeCard"> <div class="view-switcher">
<img v-if="!isCard" src="/center_resource/change_card.png" alt="" /> <ResourceViewSwitch v-model="viewMode" :modes="['columns', 'card', 'list']" />
<img v-else src="/center_resource/change_list.png" alt="" />
</div> </div>
</div> </div>
<div class="video-tool-btn"> <div class="video-tool-btn">
...@@ -139,14 +157,10 @@ const handleBatchImport = () => { ...@@ -139,14 +157,10 @@ const handleBatchImport = () => {
</div> </div>
</template> </template>
<template #filter-type="{ params }"> <template #filter-type="{ params }">
<el-tree-select <ResourceCategorySelect
node-key="id"
@change="typeFilter"
clearable
:props="defaultProps"
v-model="params.classification" v-model="params.classification"
:data="selectTree" :data="selectTree"
:default-expanded-keys="selectTree.length ? [selectTree[0]?.id] : []" /> @change="categoryFilter" />
</template> </template>
<template v-if="tabValue == '3' && isAdmin" #filter-department="{ params }"> <template v-if="tabValue == '3' && isAdmin" #filter-department="{ params }">
<div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">部门:</div> <div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">部门:</div>
...@@ -166,11 +180,48 @@ const handleBatchImport = () => { ...@@ -166,11 +180,48 @@ const handleBatchImport = () => {
</template> </template>
<!-- 卡片 --> <!-- 卡片 -->
<template #body="{ data }" v-if="isCard"> <template #body="{ data }" v-if="viewMode === 'card'">
<div class="card-list" v-if="data.length"> <div class="card-list" v-if="data.length">
<CardListItem :tabIndex="tabValue" v-for="(item, index) in data" :data="item" :key="index"></CardListItem> <CardListItem :tabIndex="tabValue" v-for="(item, index) in data" :data="item" :key="index"></CardListItem>
</div> </div>
<!-- <el-empty v-else description="暂无数据" /> --> <el-empty v-else description="暂无视频" />
</template>
<!-- Finder 风格分栏 -->
<template #body="{ data, loading }" v-else-if="viewMode === 'columns'">
<ResourceColumnBrowser
v-model="selectedCategory"
:data="selectTree"
:resources="data"
:loading="loading"
storage-key="resource-video-column-content-mode"
empty-text="当前类别暂无视频"
@change="changeColumnCategory">
<template #card="{ data: resources }">
<div class="column-card-list">
<CardListItem v-for="item in resources" :key="item.id" :tabIndex="tabValue" :data="item" />
</div>
</template>
<template #list="{ data: resources }">
<div class="column-video-list">
<router-link
v-for="item in resources"
v-show="item.auth_view"
:key="item.id"
class="column-video-row"
:to="`/resource/video/view?id=${item.id}`">
<div class="video-thumb" :style="item.cover ? { backgroundImage: `url(${item.cover})` } : {}">
<VideoPlay v-if="!item.cover" />
</div>
<div class="video-summary">
<strong>{{ item.name }}</strong>
<span>{{ item.classification_name || '未分类' }} · {{ item.updated_time || '-' }}</span>
</div>
<ArrowRight />
</router-link>
</div>
</template>
</ResourceColumnBrowser>
</template> </template>
</AppList> </AppList>
</AppCard> </AppCard>
...@@ -186,15 +237,49 @@ const handleBatchImport = () => { ...@@ -186,15 +237,49 @@ const handleBatchImport = () => {
} }
.video-head { .video-head {
position: relative; position: relative;
.video-head-icon { .view-switcher {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
font-size: 30px;
color: #666;
cursor: pointer;
} }
} }
.column-card-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
gap: 16px;
padding: 8px;
}
.column-video-list { padding: 8px; }
.column-video-row {
display: flex;
align-items: center;
gap: 14px;
padding: 10px;
color: #303133;
border-bottom: 1px solid #ebeef5;
text-decoration: none;
&:hover { background: #fff; }
> svg { width: 16px; color: #c0c4cc; }
}
.video-thumb {
display: grid;
width: 96px;
height: 54px;
flex: 0 0 auto;
place-items: center;
border-radius: 5px;
background: #dcdfe6 center / cover no-repeat;
svg { width: 25px; color: #fff; }
}
.video-summary {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
gap: 7px;
strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
span { color: #909399; font-size: 13px; }
}
.video-tool-btn { .video-tool-btn {
padding: 10px 0 30px 0; padding: 10px 0 30px 0;
} }
......
...@@ -6,8 +6,8 @@ export const routes: Array<RouteRecordRaw> = [ ...@@ -6,8 +6,8 @@ export const routes: Array<RouteRecordRaw> = [
path: '/system/dictionary', path: '/system/dictionary',
component: AppLayout, component: AppLayout,
children: [ children: [
{ path: '', component: () => import('./views/List.vue') }, { path: '', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: '/system/dictionary/rowList', component: () => import('./views/rowList.vue') } { path: '/system/dictionary/rowList', component: () => import('./views/rowList.vue'), meta: { title: '字典项管理' } }
] ]
} }
] ]
...@@ -6,9 +6,9 @@ export const routes: Array<RouteRecordRaw> = [ ...@@ -6,9 +6,9 @@ export const routes: Array<RouteRecordRaw> = [
path: '/teach/exam', path: '/teach/exam',
component: AppLayout, component: AppLayout,
children: [ children: [
{ path: '/teach/exam', component: () => import('./views/List.vue') }, { path: '/teach/exam', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: '/teach/stuList', component: () => import('./views/StudentList.vue') }, { path: '/teach/stuList', component: () => import('./views/StudentList.vue'), meta: { keepAlive: true, title: '学生列表', menuPath: '/teach/exam' } },
{ path: '/teach/view', component: () => import('./views/View.vue') } { path: '/teach/view', component: () => import('./views/View.vue'), meta: { title: '试卷详情', menuPath: '/teach/exam' } }
] ]
} }
] ]
...@@ -6,8 +6,8 @@ export const routes: Array<RouteRecordRaw> = [ ...@@ -6,8 +6,8 @@ export const routes: Array<RouteRecordRaw> = [
path: '/teach/posts', path: '/teach/posts',
component: AppLayout, component: AppLayout,
children: [ children: [
{ path: '', component: () => import('./views/List.vue') }, { path: '', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: '/teach/posts/detail', component: () => import('./views/View.vue') } { path: '/teach/posts/detail', component: () => import('./views/View.vue'), meta: { title: '帖子详情' } }
] ]
} }
] ]
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论