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

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

上级 774af296
......@@ -22,6 +22,7 @@ import {
DataAnalysis,
ChatDotRound,
Setting,
DocumentChecked,
} from '@element-plus/icons-vue'
export const menus: IMenuItem[] = [
{
......@@ -47,18 +48,18 @@ export const menus: IMenuItem[] = [
name: '教案',
path: '/resource/lessonplan',
},
{
tag: 'v1-resource-case',
icon: Collection,
name: '案例资源',
path: '/resource/case',
},
{
tag: 'v1-resource-other-information-list',
icon: Files,
name: '其他资料',
path: '/resource/other',
},
{
tag: 'v1-resource-case',
icon: DocumentChecked,
name: '案例管理',
path: '/resource/case',
},
{
icon: Collection,
name: '题库管理',
......
......@@ -34,6 +34,8 @@ const loading = ref(false)
const tableRef = ref()
const dataList = ref<any[]>([])
const page = reactive({ total: 0, size: props.limit, currentPage: 1 })
let requestSequence = 0
let activationCount = 0
const params = reactive({ ...props.remote?.params })
watch(
......@@ -70,15 +72,18 @@ const fetchList = (isReset = false) => {
if (beforeRequest) {
requestParams = beforeRequest(requestParams, isReset)
}
for (const key in params) {
if (params[key] === '' || params[key] === undefined || params[key] === undefined) {
delete params[key]
// 只清理请求副本,保留表单响应式参数的完整结构
for (const key in requestParams) {
if (requestParams[key] === '' || requestParams[key] === undefined || requestParams[key] === null) {
delete requestParams[key]
}
}
const currentRequest = ++requestSequence
loading.value = true
return (
httpRequest(requestParams)
.then((res: any) => {
if (currentRequest !== requestSequence) return
const { list = [], total = 0 } = callback ? callback(res.data, requestParams) : res.data || {}
page.total = total
dataList.value = list
......@@ -88,7 +93,7 @@ const fetchList = (isReset = false) => {
// dataList.value = []
// })
.finally(() => {
loading.value = false
if (currentRequest === requestSequence) loading.value = false
})
)
}
......@@ -125,7 +130,12 @@ onMounted(() => {
fetchList()
})
defineExpose({ refetch, tableRef, params, loading })
// keep-alive 页面从详情返回时,保留筛选和分页并刷新当前数据
onActivated(() => {
if (activationCount++ > 0) fetchList()
})
defineExpose({ refetch, search, reset, tableRef, params, loading, page })
</script>
<template>
......@@ -177,7 +187,7 @@ defineExpose({ refetch, tableRef, params, loading })
<slot></slot>
<!-- 主体 -->
<div class="table-list-bd">
<slot name="body" v-bind="{ data: dataList }">
<slot name="body" v-bind="{ data: dataList, loading }">
<el-table
:data="dataList"
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>
<div class="app-breadcrumb" v-if="routes.length">
<el-breadcrumb>
<el-breadcrumb-item v-for="route in routes" :key="route.path">
<router-link :to="route.path">{{ route.meta.title }}</router-link>
<nav class="app-breadcrumb" v-if="items.length" aria-label="面包屑导航">
<el-breadcrumb separator="/">
<el-breadcrumb-item v-for="(item, index) in items" :key="`${item.title}-${index}`">
<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>
</div>
</nav>
</template>
<script>
export default {
name: 'AppBreadcrumb',
computed: {
routes() {
return this.$route.matched.filter(route => route.meta.title)
}
}
}
</script>
<style lang="scss">
.app-breadcrumb {
padding: 18px 0 32px;
.el-breadcrumb {
font-size: 20px;
font-weight: 400;
line-height: 1;
}
.el-breadcrumb__inner a {
font-weight: normal;
color: #5b91fd;
}
.router-link-active {
color: #1a1b1c;
}
padding: 8px 0 20px;
.el-breadcrumb { font-size: 14px; line-height: 20px; }
.el-breadcrumb__inner { color: #909399; font-weight: 400; }
.el-breadcrumb__inner a { color: #606266; font-weight: 400; }
.el-breadcrumb__inner a:hover { color: var(--el-color-primary); }
.el-breadcrumb__item:last-child .el-breadcrumb__inner { color: #303133; font-weight: 600; }
}
</style>
......@@ -19,7 +19,12 @@ const dialogVisible = ref(false)
<app-breadcrumb v-if="hasBreadcrumb"></app-breadcrumb>
</div>
<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>
</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> = [
path: '/admin/teacher',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/List.vue') },
{ path: '/admin/teacher/detail', component: () => import('./views/Detail.vue') },
{ path: '/admin/teacher/add', name: 'update', component: () => import('./views/Add.vue') }
{ path: '', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: '/admin/teacher/detail', component: () => import('./views/Detail.vue'), meta: { title: '讲师详情' } },
{ path: '/admin/teacher/add', name: 'update', component: () => import('./views/Add.vue'), meta: { title: '新增/编辑讲师' } }
]
}
]
......@@ -6,8 +6,8 @@ export const routes: Array<RouteRecordRaw> = [
path: '/course/update-course',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/StepOne.vue') },
{ path: '/course/update-course/stepTwo', component: () => import('./views/StepTwo.vue') }
{ path: '', component: () => import('./views/StepOne.vue'), meta: { title: '课程基本信息' } },
{ path: '/course/update-course/stepTwo', component: () => import('./views/StepTwo.vue'), meta: { title: '课程内容编辑' } }
]
}
]
......@@ -6,12 +6,12 @@ export const routes: Array<RouteRecordRaw> = [
path: '/course/my',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/List.vue') },
{ path: '/course/my/view', component: () => import('./views/View.vue') },
{ path: '', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: '/course/my/view', component: () => import('./views/View.vue'), meta: { title: '课程详情' } },
// id: 章节id courseId课程id
{ path: '/course/my/view/:id/:courseId', component: () => import('./views/ViewDetails.vue') },
{ path: '/course/my/graph', component: () => import('./views/Graph.vue') },
{ path: '/course/my/view/:id/:courseId', component: () => import('./views/ViewDetails.vue'), meta: { title: '章节详情' } },
{ 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> = [
path: '/resource/case',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/List.vue') },
{ path: '/resource/case/update', component: () => import('./views/Update.vue') },
{ path: '/resource/case/view', component: () => import('./views/View.vue') },
{ path: '', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: '/resource/case/update', component: () => import('./views/Update.vue'), meta: { title: '新建/编辑案例' } },
{ path: '/resource/case/view', component: () => import('./views/View.vue'), meta: { title: '案例详情' } },
],
},
]
......@@ -6,9 +6,9 @@ export const routes: Array<RouteRecordRaw> = [
path: '/resource/courseware',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/List.vue') },
{ path: '/resource/courseware/update', component: () => import('./views/Update.vue') },
{ path: '/resource/courseware/view', component: () => import('./views/View.vue') }
{ path: '', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: '/resource/courseware/update', component: () => import('./views/Update.vue'), meta: { title: '新建/编辑课件' } },
{ path: '/resource/courseware/view', component: () => import('./views/View.vue'), meta: { title: '课件详情' } }
// { path: 'update/:id', component: () => import('./views/Update.vue'), props: true }
]
}
......
......@@ -6,6 +6,10 @@ import { useMapStore } from '@/stores/map'
import { useGetCategoryList } from '@/composables/useGetCategoryList'
import { useProjectList } from '@/composables/useGetProjectList'
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
......@@ -18,24 +22,17 @@ const departmentList: any = useProjectList('', '79806610719731712').departmentLi
// 下拉选择tree 视频分类
let { list: selectTree }: any = useGetCategoryList()
const defaultProps = {
children: 'children',
label: 'category_name',
value: 'id',
}
const appList = ref()
// 列表切换
const isCard = useStorage('isCard', true)
const changeCard = () => {
isCard.value = !isCard.value
}
type ViewMode = 'columns' | 'card' | 'list'
const viewMode = useStorage<ViewMode>('resource-courseware-view-mode', 'columns')
const selectedCategory = ref<string | number>()
// 资源出处 tab触发
const tabValue = ref('1')
const tabChange = () => {
appList.value.refetch()
appList.value.search()
}
const listOptions = computed(() => {
......@@ -83,8 +80,27 @@ const listOptions = computed(() => {
})
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>
<template>
......@@ -95,10 +111,7 @@ const typeFilter = () => {
<el-tab-pane label="部门资源" name="2"></el-tab-pane>
<el-tab-pane label="公开资源" name="3"></el-tab-pane>
</el-tabs>
<div class="video-head-icon" @click="changeCard">
<img v-if="!isCard" src="/center_resource/change_card.png" alt="" />
<img v-else src="/center_resource/change_list.png" alt="" />
</div>
<div class="view-switcher"><ResourceViewSwitch v-model="viewMode" :modes="['columns', 'card', 'list']" /></div>
</div>
<div class="video-tool-btn">
<router-link
......@@ -111,14 +124,7 @@ const typeFilter = () => {
<AppList v-bind="listOptions" ref="appList">
<template #header-aside></template>
<template #filter-type="{ params }">
<el-tree-select
node-key="id"
@change="typeFilter"
clearable
:props="defaultProps"
v-model="params.classification"
:data="selectTree"
:default-expanded-keys="selectTree.length ? [selectTree[0]?.id] : []" />
<ResourceCategorySelect v-model="params.classification" :data="selectTree" @change="categoryFilter" />
</template>
<template v-if="tabValue == '3' && isAdmin" #filter-department="{ params }">
<div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">部门:</div>
......@@ -138,7 +144,7 @@ const typeFilter = () => {
</template>
<!-- 卡片 -->
<template #body="{ data }" v-if="isCard">
<template #body="{ data }" v-if="viewMode === 'card'">
<div class="card-list" v-if="data.length">
<CardListItem
:permission="{ edit: 'v1-resource-courseware-update', view: 'v1-resource-courseware-view' }"
......@@ -148,7 +154,37 @@ const typeFilter = () => {
:key="index"
:tabIndex="tabValue"></CardListItem>
</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>
</AppList>
</AppCard>
......@@ -163,15 +199,20 @@ const typeFilter = () => {
}
.video-head {
position: relative;
.video-head-icon {
.view-switcher {
position: absolute;
top: 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 {
padding: 10px 0 30px 0;
}
......
......@@ -6,9 +6,9 @@ export const routes: Array<RouteRecordRaw> = [
path: '/resource/lessonplan',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/List.vue') },
{ path: '/resource/lessonplan/update', component: () => import('./views/Update.vue') },
{ path: '/resource/lessonplan/view', component: () => import('./views/View.vue') }
{ path: '', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: '/resource/lessonplan/update', component: () => import('./views/Update.vue'), meta: { title: '新建/编辑教案' } },
{ path: '/resource/lessonplan/view', component: () => import('./views/View.vue'), meta: { title: '教案详情' } }
// { path: 'update/:id', component: () => import('./views/Update.vue'), props: true }
]
}
......
......@@ -6,6 +6,10 @@ import { useMapStore } from '@/stores/map'
import { useGetCategoryList } from '@/composables/useGetCategoryList'
import { useProjectList } from '@/composables/useGetProjectList'
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
......@@ -15,27 +19,20 @@ const store = useMapStore()
const appList = ref()
// 列表切换
const isCard = useStorage('isCard', true)
const changeCard = () => {
isCard.value = !isCard.value
}
type ViewMode = 'columns' | 'card' | 'list'
const viewMode = useStorage<ViewMode>('resource-lessonplan-view-mode', 'columns')
const selectedCategory = ref<string | number>()
// 筛选部门列表
const departmentList: any = useProjectList('', '79806610719731712').departmentList
// 下拉选择tree 视频分类
let { list: selectTree }: any = useGetCategoryList()
const defaultProps = {
children: 'children',
label: 'category_name',
value: 'id',
}
// 资源出处 tab触发
const tabValue = ref('1')
const tabChange = () => {
appList.value.refetch()
appList.value.search()
}
const listOptions = computed(() => {
......@@ -83,8 +80,27 @@ const listOptions = computed(() => {
})
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>
<template>
......@@ -95,10 +111,7 @@ const typeFilter = () => {
<el-tab-pane label="部门资源" name="2"></el-tab-pane>
<el-tab-pane label="公开资源" name="3"></el-tab-pane>
</el-tabs>
<div class="video-head-icon" @click="changeCard">
<img v-if="!isCard" src="/center_resource/change_card.png" alt="" />
<img v-else src="/center_resource/change_list.png" alt="" />
</div>
<div class="view-switcher"><ResourceViewSwitch v-model="viewMode" :modes="['columns', 'card', 'list']" /></div>
</div>
<div class="video-tool-btn">
<router-link
......@@ -111,14 +124,7 @@ const typeFilter = () => {
<AppList v-bind="listOptions" ref="appList">
<template #header-aside></template>
<template #filter-type="{ params }">
<el-tree-select
node-key="id"
@change="typeFilter"
clearable
:props="defaultProps"
v-model="params.classification"
:data="selectTree"
:default-expanded-keys="selectTree.length ? [selectTree[0]?.id] : []" />
<ResourceCategorySelect v-model="params.classification" :data="selectTree" @change="categoryFilter" />
</template>
<template v-if="tabValue == '3' && isAdmin" #filter-department="{ params }">
<div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">部门:</div>
......@@ -138,7 +144,7 @@ const typeFilter = () => {
</template>
<!-- 卡片 -->
<template #body="{ data }" v-if="isCard">
<template #body="{ data }" v-if="viewMode === 'card'">
<div class="card-list" v-if="data.length">
<CardListItem
:tabIndex="tabValue"
......@@ -148,7 +154,37 @@ const typeFilter = () => {
:data="item"
:key="index"></CardListItem>
</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>
</AppList>
</AppCard>
......@@ -163,15 +199,20 @@ const typeFilter = () => {
}
.video-head {
position: relative;
.video-head-icon {
.view-switcher {
position: absolute;
top: 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 {
padding: 10px 0 30px 0;
}
......
......@@ -6,9 +6,9 @@ export const routes: Array<RouteRecordRaw> = [
path: '/resource/other',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/List.vue') },
{ path: '/resource/other/update', component: () => import('./views/Update.vue') },
{ path: '/resource/other/view', component: () => import('./views/View.vue') }
{ path: '', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: '/resource/other/update', component: () => import('./views/Update.vue'), meta: { title: '新建/编辑其他资料' } },
{ path: '/resource/other/view', component: () => import('./views/View.vue'), meta: { title: '其他资料详情' } }
// { path: 'update/:id', component: () => import('./views/Update.vue'), props: true }
]
}
......
......@@ -7,6 +7,10 @@ import { useProjectList } from '@/composables/useGetProjectList'
// 下拉选择tree 视频分类
import { useGetCategoryList } from '@/composables/useGetCategoryList'
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
......@@ -16,16 +20,14 @@ const store = useMapStore()
const appList = ref()
// 列表切换
const isCard = useStorage('isCard', true)
const changeCard = () => {
isCard.value = !isCard.value
}
type ViewMode = 'columns' | 'card' | 'list'
const viewMode = useStorage<ViewMode>('resource-other-view-mode', 'columns')
const selectedCategory = ref<string | number>()
// 资源出处 tab触发
const tabValue = ref('1')
const tabChange = () => {
appList.value.refetch()
appList.value.search()
}
// 筛选部门列表
......@@ -33,11 +35,6 @@ const departmentList: any = useProjectList('', '79806610719731712').departmentLi
// 下拉选择tree 视频分类
let { list: selectTree }: any = useGetCategoryList()
const defaultProps = {
children: 'children',
label: 'category_name',
value: 'id',
}
const listOptions = computed(() => {
const columns = [
......@@ -84,8 +81,27 @@ const listOptions = computed(() => {
})
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>
<template>
......@@ -96,10 +112,7 @@ const typeFilter = () => {
<el-tab-pane label="部门资源" name="2"></el-tab-pane>
<el-tab-pane label="公开资源" name="3"></el-tab-pane>
</el-tabs>
<div class="video-head-icon" @click="changeCard">
<img v-if="!isCard" src="/center_resource/change_card.png" alt="" />
<img v-else src="/center_resource/change_list.png" alt="" />
</div>
<div class="view-switcher"><ResourceViewSwitch v-model="viewMode" :modes="['columns', 'card', 'list']" /></div>
</div>
<div class="video-tool-btn">
<router-link
......@@ -112,14 +125,7 @@ const typeFilter = () => {
<AppList v-bind="listOptions" ref="appList">
<template #header-aside></template>
<template #filter-type="{ params }">
<el-tree-select
node-key="id"
@change="typeFilter"
clearable
:props="defaultProps"
v-model="params.classification"
:data="selectTree"
:default-expanded-keys="selectTree.length ? [selectTree[0]?.id] : []" />
<ResourceCategorySelect v-model="params.classification" :data="selectTree" @change="categoryFilter" />
</template>
<template v-if="tabValue == '3' && isAdmin" #filter-department="{ params }">
<div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">部门:</div>
......@@ -139,7 +145,7 @@ const typeFilter = () => {
</template>
<!-- 卡片 -->
<template #body="{ data }" v-if="isCard">
<template #body="{ data }" v-if="viewMode === 'card'">
<div class="card-list" v-if="data.length">
<CardListItem
:tabIndex="tabValue"
......@@ -149,7 +155,37 @@ const typeFilter = () => {
:data="item"
:key="index"></CardListItem>
</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>
</AppList>
</AppCard>
......@@ -164,15 +200,20 @@ const typeFilter = () => {
}
.video-head {
position: relative;
.video-head-icon {
.view-switcher {
position: absolute;
top: 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 {
padding: 10px 0 30px 0;
}
......
......@@ -6,11 +6,11 @@ export const routes: Array<RouteRecordRaw> = [
path: '/resource/video',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/List.vue') },
{ path: 'update', component: () => import('./views/Update.vue') },
{ path: 'view', component: () => import('./views/View.vue') },
{ path: 'edit-courseware', component: () => import('./views/EditCourseware.vue') },
{ path: 'upload', component: () => import('./views/BatchUpload.vue') }
{ path: '', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: 'update', component: () => import('./views/Update.vue'), meta: { title: '新建/编辑视频' } },
{ path: 'view', component: () => import('./views/View.vue'), meta: { title: '视频详情' } },
{ path: 'edit-courseware', component: () => import('./views/EditCourseware.vue'), meta: { title: '编辑视频课件' } },
{ path: 'upload', component: () => import('./views/BatchUpload.vue'), meta: { title: '批量上传视频' } }
// { path: 'update/:id', component: () => import('./views/Update.vue'), props: true }
]
}
......
......@@ -2,8 +2,11 @@
import { useStorage } from '@vueuse/core'
import { getVideoList } from '../api'
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 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 { useMapStore } from '@/stores/map'
import { useUserStore } from '@/stores/user'
......@@ -22,20 +25,18 @@ const departmentList: any = useProjectList('', '79806610719731712').departmentLi
// 筛选下拉选择tree 视频分类
let { list: selectTree }: any = useGetCategoryList()
const defaultProps = {
children: 'children',
label: 'category_name',
value: 'id',
}
// 资源出处 tab触发
const tabValue = ref('1')
const appList = ref()
// const isShowBatchImport = ref(false)
const tabChange = () => {
appList.value.refetch()
appList.value.search()
}
// 列表切换
const isCard = useStorage('isCard', true)
type ViewMode = 'columns' | 'card' | 'list'
// 展示方式:首次进入默认使用分栏模式
const viewMode = useStorage<ViewMode>('resource-video-view-mode', 'columns')
const selectedCategory = ref<string | number>()
// table 数据
const listOptions = computed(() => {
const columns = [
......@@ -83,11 +84,29 @@ const listOptions = computed(() => {
})
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 = () => {
......@@ -105,9 +124,8 @@ const handleBatchImport = () => {
<el-tab-pane label="部门资源" name="2"></el-tab-pane>
<el-tab-pane label="公开资源" name="3"></el-tab-pane>
</el-tabs>
<div class="video-head-icon" @click="changeCard">
<img v-if="!isCard" src="/center_resource/change_card.png" alt="" />
<img v-else src="/center_resource/change_list.png" alt="" />
<div class="view-switcher">
<ResourceViewSwitch v-model="viewMode" :modes="['columns', 'card', 'list']" />
</div>
</div>
<div class="video-tool-btn">
......@@ -139,14 +157,10 @@ const handleBatchImport = () => {
</div>
</template>
<template #filter-type="{ params }">
<el-tree-select
node-key="id"
@change="typeFilter"
clearable
:props="defaultProps"
<ResourceCategorySelect
v-model="params.classification"
:data="selectTree"
:default-expanded-keys="selectTree.length ? [selectTree[0]?.id] : []" />
@change="categoryFilter" />
</template>
<template v-if="tabValue == '3' && isAdmin" #filter-department="{ params }">
<div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">部门:</div>
......@@ -166,11 +180,48 @@ const handleBatchImport = () => {
</template>
<!-- 卡片 -->
<template #body="{ data }" v-if="isCard">
<template #body="{ data }" v-if="viewMode === 'card'">
<div class="card-list" v-if="data.length">
<CardListItem :tabIndex="tabValue" v-for="(item, index) in data" :data="item" :key="index"></CardListItem>
</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>
</AppList>
</AppCard>
......@@ -186,15 +237,49 @@ const handleBatchImport = () => {
}
.video-head {
position: relative;
.video-head-icon {
.view-switcher {
position: absolute;
top: 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 {
padding: 10px 0 30px 0;
}
......
......@@ -6,8 +6,8 @@ export const routes: Array<RouteRecordRaw> = [
path: '/system/dictionary',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/List.vue') },
{ path: '/system/dictionary/rowList', component: () => import('./views/rowList.vue') }
{ path: '', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: '/system/dictionary/rowList', component: () => import('./views/rowList.vue'), meta: { title: '字典项管理' } }
]
}
]
......@@ -6,9 +6,9 @@ export const routes: Array<RouteRecordRaw> = [
path: '/teach/exam',
component: AppLayout,
children: [
{ path: '/teach/exam', component: () => import('./views/List.vue') },
{ path: '/teach/stuList', component: () => import('./views/StudentList.vue') },
{ path: '/teach/view', component: () => import('./views/View.vue') }
{ path: '/teach/exam', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: '/teach/stuList', component: () => import('./views/StudentList.vue'), meta: { keepAlive: true, title: '学生列表', menuPath: '/teach/exam' } },
{ path: '/teach/view', component: () => import('./views/View.vue'), meta: { title: '试卷详情', menuPath: '/teach/exam' } }
]
}
]
......@@ -6,8 +6,8 @@ export const routes: Array<RouteRecordRaw> = [
path: '/teach/posts',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/List.vue') },
{ path: '/teach/posts/detail', component: () => import('./views/View.vue') }
{ path: '', component: () => import('./views/List.vue'), meta: { keepAlive: true } },
{ path: '/teach/posts/detail', component: () => import('./views/View.vue'), meta: { title: '帖子详情' } }
]
}
]
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论