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

feat: 列表支持卡片和列表模式切换

上级 030b547a
...@@ -128,7 +128,7 @@ defineExpose({ refetch, tableRef }) ...@@ -128,7 +128,7 @@ defineExpose({ refetch, tableRef })
<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 v-if="!isMobile || !mobileCards" stripe :header-cell-style="{ background: '#ededed' }" <el-table v-if="!isMobile || !mobileCards" stripe :header-cell-style="{ background: '#ededed' }"
:data="dataList" v-loading="loading && hasLoading" v-bind="$attrs" ref="tableRef"> :data="dataList" v-loading="loading && hasLoading" v-bind="$attrs" ref="tableRef">
......
...@@ -10,7 +10,7 @@ export function getExperimentList(params: { course_id?: string }) { ...@@ -10,7 +10,7 @@ export function getExperimentList(params: { course_id?: string }) {
return httpRequest.get('/api/lab/v1/teacher/book/experiments', { params }) return httpRequest.get('/api/lab/v1/teacher/book/experiments', { params })
} }
// 获取指导书列表 // 获取指导书列表
export function getBookList(params?: { name?: string; experiment_id?: string; page?: number; page_size?: number }) { export function getBookList(params?: { name?: string; experiment_id?: string; status?: string; page?: number; page_size?: number }) {
return httpRequest.get('/api/lab/v1/teacher/book/list', { params }) return httpRequest.get('/api/lab/v1/teacher/book/list', { params })
} }
// 获取指导书详情 // 获取指导书详情
......
<script setup lang="ts"> <script setup lang="ts">
import type { BookItem } from '../types' import type { BookItem } from '../types'
import { CirclePlus } from '@element-plus/icons-vue' import { CirclePlus } from '@element-plus/icons-vue'
import { useLocalStorage } from '@vueuse/core'
import AppList from '@/components/base/AppList.vue' import AppList from '@/components/base/AppList.vue'
import ListItem from '../components/ListItem.vue' import ListItem from '../components/ListItem.vue'
import { getBookList } from '../api' import { getBookList } from '../api'
import { useGetExperimentList } from '../composables/useGetExperimentList' import { useGetExperimentList } from '../composables/useGetExperimentList'
import { useMapStore } from '@/stores/map'
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue')) const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
const VIEW_MODE_STORAGE_KEY = 'list-view-mode'
type ViewMode = 'card' | 'list'
const status = computed(() => useMapStore().getMapValuesByKey('system_status'))
// 实验列表 // 实验列表
const { experiments, updateExperiments } = useGetExperimentList() const { experiments, updateExperiments } = useGetExperimentList()
updateExperiments() updateExperiments()
...@@ -14,12 +19,21 @@ updateExperiments() ...@@ -14,12 +19,21 @@ updateExperiments()
const route = useRoute() const route = useRoute()
const appList = $ref<InstanceType<typeof AppList> | null>(null) const appList = $ref<InstanceType<typeof AppList> | null>(null)
const viewMode = useLocalStorage<ViewMode>(VIEW_MODE_STORAGE_KEY, 'list')
function setViewMode(mode: ViewMode) {
viewMode.value = mode
}
function isActiveStatus(statusValue: string) {
return String(statusValue) === '1'
}
// 列表配置 // 列表配置
const listOptions = $computed(() => { const listOptions = $computed(() => {
return { return {
remote: { remote: {
httpRequest: getBookList, httpRequest: getBookList,
params: { name: '', experiment_id: route.query.experiment_id || '' } params: { name: '', experiment_id: route.query.experiment_id || '', status: '' },
}, },
filters: [ filters: [
{ type: 'input', prop: 'name', label: '实验指导书名称', placeholder: '请输入实验指导书名称' }, { type: 'input', prop: 'name', label: '实验指导书名称', placeholder: '请输入实验指导书名称' },
...@@ -30,10 +44,27 @@ const listOptions = $computed(() => { ...@@ -30,10 +44,27 @@ const listOptions = $computed(() => {
options: experiments.value, options: experiments.value,
labelKey: 'name', labelKey: 'name',
valueKey: 'id', valueKey: 'id',
placeholder: '请选择关联实验' placeholder: '请选择关联实验',
} },
{
type: 'select',
prop: 'status',
label: '生效状态',
options: status.value,
placeholder: '请选择生效状态',
},
],
columns: [
{ label: '名称', prop: 'name' },
{ label: '文件大小', prop: 'size_name', width: 110 },
{ label: '观看次数', prop: 'pv', width: 110 },
{ label: '关联实验', prop: 'experiment_id_name' },
{ label: '生效状态', prop: 'status_name', width: 110, slots: 'table-status' },
{ label: '创建人', prop: 'created_operator_name', width: 120 },
{ label: '创建时间', prop: 'created_time', width: 180 },
{ label: '更新时间', prop: 'updated_time', width: 180 },
{ label: '操作', slots: 'table-x', width: 160 },
], ],
columns: [{ label: '名称', prop: 'name' }]
} }
}) })
...@@ -62,13 +93,46 @@ function onUpdateSuccess() { ...@@ -62,13 +93,46 @@ function onUpdateSuccess() {
<el-button type="primary" :icon="CirclePlus" @click="handleAdd" v-permission="'v1-teacher-book-create'" <el-button type="primary" :icon="CirclePlus" @click="handleAdd" v-permission="'v1-teacher-book-create'"
>新增实验指导书</el-button >新增实验指导书</el-button
> >
<div class="view-switch">
<button
type="button"
class="view-switch__button"
:class="{ 'is-active': viewMode === 'card' }"
@click="setViewMode('card')"
title="卡片展示">
<span class="view-switch__icon view-switch__icon--card" aria-hidden="true">
<span v-for="item in 4" :key="item" class="view-switch__cell"></span>
</span>
</button>
<button
type="button"
class="view-switch__button"
:class="{ 'is-active': viewMode === 'list' }"
@click="setViewMode('list')"
title="列表展示">
<span class="view-switch__icon view-switch__icon--list" aria-hidden="true">
<span v-for="item in 3" :key="item" class="view-switch__line"></span>
</span>
</button>
</div>
</template> </template>
<template #body="{ data }"> <template #body="{ data }" v-if="viewMode === 'card'">
<div class="list-card" v-if="data.length"> <div class="list-card" v-if="data.length">
<ListItem v-for="item in data" :data="item" :key="item.id" @clickEdit="handleUpdate(item)"></ListItem> <ListItem v-for="item in data" :data="item" :key="item.id" @clickEdit="handleUpdate(item)"></ListItem>
</div> </div>
<el-empty description="暂无数据" v-else /> <el-empty description="暂无数据" v-else />
</template> </template>
<template #table-x="{ row }: { row: BookItem }">
<el-button type="primary" round v-permission="'v1-teacher-book-view'">
<router-link :to="`/admin/lab/book/${row.id}`" target="_blank">查看</router-link>
</el-button>
<el-button type="primary" round @click="handleUpdate(row)" v-permission="'v1-teacher-book-update'"
>编辑</el-button
>
</template>
<template #table-status="{ row }: { row: BookItem }">
<span :class="isActiveStatus(row.status) ? 'status-active' : 'status-inactive'">{{ row.status_name }}</span>
</template>
</AppList> </AppList>
</AppCard> </AppCard>
<FormDialog v-model="dialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="dialogVisible"></FormDialog> <FormDialog v-model="dialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="dialogVisible"></FormDialog>
...@@ -80,11 +144,94 @@ function onUpdateSuccess() { ...@@ -80,11 +144,94 @@ function onUpdateSuccess() {
background: #fff; background: #fff;
border-radius: 12px; border-radius: 12px;
} }
:deep(.table-list-buttons) {
align-items: center;
justify-content: space-between;
}
.view-switch {
display: inline-flex;
margin-left: auto;
border: 1px solid #e6e8eb;
border-radius: 10px;
background: #fff;
overflow: hidden;
}
.view-switch__button {
width: 32px;
height: 32px;
border: 0;
padding: 0;
background: transparent;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease;
&:hover {
background: #f6f7f9;
}
&.is-active {
background: rgba(186, 20, 62, 0.12);
}
}
.view-switch__icon {
width: 14px;
height: 14px;
color: #7a7f87;
display: inline-flex;
}
.view-switch__button.is-active .view-switch__icon {
color: var(--main-color);
}
.view-switch__icon--card {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 2px;
}
.view-switch__cell {
width: 6px;
height: 6px;
border: 1.25px solid currentColor;
border-radius: 2px;
box-sizing: border-box;
}
.view-switch__icon--list {
flex-direction: column;
justify-content: space-between;
}
.view-switch__line {
width: 14px;
height: 2px;
border-radius: 999px;
background: currentColor;
}
.status-active {
color: #67c23a;
}
.status-inactive {
color: var(--main-color);
}
.list-card { .list-card {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 20px; gap: 20px;
} }
@media (max-width: 1024px) { .list-card { grid-template-columns: repeat(2, 1fr); gap: 16px; } } @media (max-width: 1024px) {
@media (max-width: 767px) { .list-card { grid-template-columns: 1fr; gap: 12px; } } .list-card {
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
}
@media (max-width: 767px) {
:deep(.table-list-buttons) {
justify-content: flex-start;
}
.view-switch {
margin-left: 0;
}
.list-card {
grid-template-columns: 1fr;
gap: 12px;
}
}
</style> </style>
...@@ -10,7 +10,7 @@ export function getExperimentList(params: { course_id?: string }) { ...@@ -10,7 +10,7 @@ export function getExperimentList(params: { course_id?: string }) {
return httpRequest.get('/api/lab/v1/teacher/experiment-cases/experiments', { params }) return httpRequest.get('/api/lab/v1/teacher/experiment-cases/experiments', { params })
} }
// 获取案例原文列表 // 获取案例原文列表
export function getCaseList(params?: { name?: string; experiment_id?: string; page?: number; page_size?: number }) { export function getCaseList(params?: { name?: string; experiment_id?: string; status?: string; page?: number; page_size?: number }) {
return httpRequest.get('/api/lab/v1/teacher/experiment-cases/list', { params }) return httpRequest.get('/api/lab/v1/teacher/experiment-cases/list', { params })
} }
// 获取案例原文详情 // 获取案例原文详情
......
...@@ -27,6 +27,9 @@ const status = useMapStore().getMapValuesByKey('system_status') ...@@ -27,6 +27,9 @@ const status = useMapStore().getMapValuesByKey('system_status')
const statusName = computed(() => { const statusName = computed(() => {
return status.find(item => item.value === props.data.status)?.label return status.find(item => item.value === props.data.status)?.label
}) })
const statusClass = computed(() => {
return String(props.data.status) === '1' ? 'status-active' : 'status-inactive'
})
const sizeName = computed(() => { const sizeName = computed(() => {
return props.data.size + 'MB' return props.data.size + 'MB'
...@@ -54,7 +57,7 @@ const sizeName = computed(() => { ...@@ -54,7 +57,7 @@ const sizeName = computed(() => {
</div> </div>
<div class="list-item-title"> <div class="list-item-title">
<h2>{{ data.name }}</h2> <h2>{{ data.name }}</h2>
<h6>{{ statusName }}</h6> <h6 :class="statusClass">{{ statusName }}</h6>
</div> </div>
<div class="list-item-main"> <div class="list-item-main">
<ol> <ol>
...@@ -175,6 +178,11 @@ const sizeName = computed(() => { ...@@ -175,6 +178,11 @@ const sizeName = computed(() => {
font-family: Source Han Sans CN; font-family: Source Han Sans CN;
font-weight: 500; font-weight: 500;
line-height: 20px; line-height: 20px;
}
.status-active {
color: #67c23a;
}
.status-inactive {
color: var(--main-color); color: var(--main-color);
} }
} }
......
<script setup lang="ts"> <script setup lang="ts">
import type { CaseItem } from '../types' import type { CaseItem } from '../types'
import { CirclePlus } from '@element-plus/icons-vue' import { CirclePlus } from '@element-plus/icons-vue'
import { useLocalStorage } from '@vueuse/core'
import AppList from '@/components/base/AppList.vue' import AppList from '@/components/base/AppList.vue'
import ListItem from '../components/ListItem.vue' import ListItem from '../components/ListItem.vue'
import { getCaseList } from '../api' import { getCaseList } from '../api'
import { useGetExperimentList } from '../composables/useGetExperimentList' import { useGetExperimentList } from '../composables/useGetExperimentList'
import { useMapStore } from '@/stores/map'
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue')) const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
const VIEW_MODE_STORAGE_KEY = 'list-view-mode'
type ViewMode = 'card' | 'list'
type CaseListRow = CaseItem & {
course_name: string
experiment_name: string
created_operator_name: string
status_name: string
size_name: string
}
const mapStore = useMapStore()
const status = computed(() => mapStore.getMapValuesByKey('system_status'))
// 实验列表 // 实验列表
const { experiments, updateExperiments } = useGetExperimentList() const { experiments, updateExperiments } = useGetExperimentList()
updateExperiments() updateExperiments()
...@@ -14,12 +28,44 @@ updateExperiments() ...@@ -14,12 +28,44 @@ updateExperiments()
const route = useRoute() const route = useRoute()
const appList = $ref<InstanceType<typeof AppList> | null>(null) const appList = $ref<InstanceType<typeof AppList> | null>(null)
const viewMode = useLocalStorage<ViewMode>(VIEW_MODE_STORAGE_KEY, 'list')
function setViewMode(mode: ViewMode) {
viewMode.value = mode
}
function getOperatorName(operator: CaseItem['created_operator']) {
return operator?.real_name || operator?.nickname || operator?.username || ''
}
function getStatusName(statusValue: string) {
return status.value.find((item) => item.value === statusValue)?.label || ''
}
function isActiveStatus(statusValue: string) {
return String(statusValue) === '1'
}
// 列表配置 // 列表配置
const listOptions = $computed(() => { const listOptions = $computed(() => {
return { return {
remote: { remote: {
httpRequest: getCaseList, httpRequest: getCaseList,
params: { name: '', experiment_id: route.query.experiment_id || '' } params: { name: '', experiment_id: route.query.experiment_id || '', status: '' },
callback(data: { total: number; list: CaseItem[] }) {
const { list = [], total = 0 } = data || {}
const dataList = list.map((item) => {
return {
...item,
course_name: item.course?.name || '',
experiment_name: item.experiment?.name || '',
created_operator_name: getOperatorName(item.created_operator),
status_name: getStatusName(item.status),
size_name: `${item.size}MB`,
}
})
return { list: dataList, total }
},
}, },
filters: [ filters: [
{ type: 'input', prop: 'name', label: '案例原文名称', placeholder: '请输入案例原文名称' }, { type: 'input', prop: 'name', label: '案例原文名称', placeholder: '请输入案例原文名称' },
...@@ -30,10 +76,27 @@ const listOptions = $computed(() => { ...@@ -30,10 +76,27 @@ const listOptions = $computed(() => {
options: experiments.value, options: experiments.value,
labelKey: 'name', labelKey: 'name',
valueKey: 'id', valueKey: 'id',
placeholder: '请选择关联实验' placeholder: '请选择关联实验',
} },
{
type: 'select',
prop: 'status',
label: '生效状态',
options: status.value,
placeholder: '请选择生效状态',
},
],
columns: [
{ label: '名称', prop: 'name' },
{ label: '文件大小', prop: 'size_name', width: 110 },
{ label: '观看次数', prop: 'pv', width: 110 },
{ label: '关联实验', prop: 'experiment_name' },
{ label: '生效状态', prop: 'status_name', width: 110, slots: 'table-status' },
{ label: '创建人', prop: 'created_operator_name', width: 120 },
{ label: '创建时间', prop: 'created_time', width: 180 },
{ label: '更新时间', prop: 'updated_time', width: 180 },
{ label: '操作', slots: 'table-x', width: 160 },
], ],
columns: [{ label: '名称', prop: 'name' }]
} }
}) })
...@@ -62,13 +125,46 @@ function onUpdateSuccess() { ...@@ -62,13 +125,46 @@ function onUpdateSuccess() {
<el-button type="primary" :icon="CirclePlus" @click="handleAdd" v-permission="'teacher-experiment-cases-create'" <el-button type="primary" :icon="CirclePlus" @click="handleAdd" v-permission="'teacher-experiment-cases-create'"
>新增案例原文</el-button >新增案例原文</el-button
> >
<div class="view-switch">
<button
type="button"
class="view-switch__button"
:class="{ 'is-active': viewMode === 'card' }"
@click="setViewMode('card')"
title="卡片展示">
<span class="view-switch__icon view-switch__icon--card" aria-hidden="true">
<span v-for="item in 4" :key="item" class="view-switch__cell"></span>
</span>
</button>
<button
type="button"
class="view-switch__button"
:class="{ 'is-active': viewMode === 'list' }"
@click="setViewMode('list')"
title="列表展示">
<span class="view-switch__icon view-switch__icon--list" aria-hidden="true">
<span v-for="item in 3" :key="item" class="view-switch__line"></span>
</span>
</button>
</div>
</template> </template>
<template #body="{ data }: { data: CaseItem[] }"> <template #body="{ data }: { data: CaseListRow[] }" v-if="viewMode === 'card'">
<div class="list-card" v-if="data.length"> <div class="list-card" v-if="viewMode === 'card' && data.length">
<ListItem v-for="item in data" :data="item" :key="item.id" @clickEdit="handleUpdate(item)"></ListItem> <ListItem v-for="item in data" :data="item" :key="item.id" @clickEdit="handleUpdate(item)"></ListItem>
</div> </div>
<el-empty description="暂无数据" v-else /> <el-empty description="暂无数据" v-else />
</template> </template>
<template #table-x="{ row }: { row: CaseListRow }">
<el-button type="primary" round v-permission="'teacher-experiment-cases-detail'">
<router-link :to="`/admin/lab/case/${row.id}`" target="_blank">查看</router-link>
</el-button>
<el-button type="primary" round @click="handleUpdate(row)" v-permission="'teacher-experiment-cases-update'"
>编辑</el-button
>
</template>
<template #table-status="{ row }: { row: CaseListRow }">
<span :class="isActiveStatus(row.status) ? 'status-active' : 'status-inactive'">{{ row.status_name }}</span>
</template>
</AppList> </AppList>
</AppCard> </AppCard>
<FormDialog v-model="dialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="dialogVisible"></FormDialog> <FormDialog v-model="dialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="dialogVisible"></FormDialog>
...@@ -80,11 +176,94 @@ function onUpdateSuccess() { ...@@ -80,11 +176,94 @@ function onUpdateSuccess() {
background: #fff; background: #fff;
border-radius: 12px; border-radius: 12px;
} }
:deep(.table-list-buttons) {
align-items: center;
justify-content: space-between;
}
.view-switch {
display: inline-flex;
margin-left: auto;
border: 1px solid #e6e8eb;
border-radius: 10px;
background: #fff;
overflow: hidden;
}
.view-switch__button {
width: 32px;
height: 32px;
border: 0;
padding: 0;
background: transparent;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease;
&:hover {
background: #f6f7f9;
}
&.is-active {
background: rgba(186, 20, 62, 0.12);
}
}
.view-switch__icon {
width: 14px;
height: 14px;
color: #7a7f87;
display: inline-flex;
}
.view-switch__button.is-active .view-switch__icon {
color: var(--main-color);
}
.view-switch__icon--card {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 2px;
}
.view-switch__cell {
width: 6px;
height: 6px;
border: 1.25px solid currentColor;
border-radius: 2px;
box-sizing: border-box;
}
.view-switch__icon--list {
flex-direction: column;
justify-content: space-between;
}
.view-switch__line {
width: 14px;
height: 2px;
border-radius: 999px;
background: currentColor;
}
.status-active {
color: #67c23a;
}
.status-inactive {
color: var(--main-color);
}
.list-card { .list-card {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 20px; gap: 20px;
} }
@media (max-width: 1024px) { .list-card { grid-template-columns: repeat(2, 1fr); gap: 16px; } } @media (max-width: 1024px) {
@media (max-width: 767px) { .list-card { grid-template-columns: 1fr; gap: 12px; } } .list-card {
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
}
@media (max-width: 767px) {
:deep(.table-list-buttons) {
justify-content: flex-start;
}
.view-switch {
margin-left: 0;
}
.list-card {
grid-template-columns: 1fr;
gap: 12px;
}
}
</style> </style>
...@@ -43,7 +43,6 @@ const rules = ref({ ...@@ -43,7 +43,6 @@ const rules = ref({
end_time: [{ required: true, message: '请选择' }], end_time: [{ required: true, message: '请选择' }],
questions: [{ required: true, message: '请选择' }], questions: [{ required: true, message: '请选择' }],
competition: [{ required: true, message: '请选择' }], competition: [{ required: true, message: '请选择' }],
'config.practical_question': [{ required: true, message: '请输入实操试题' }],
}) })
const options = ref([ const options = ref([
...@@ -196,12 +195,6 @@ function disabledMinSubmitDate(time: Date) { ...@@ -196,12 +195,6 @@ function disabledMinSubmitDate(time: Date) {
:value="item.value"></el-option> :value="item.value"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="大赛模式">
<el-radio-group v-model="form.config.mode">
<el-radio label="test">训练</el-radio>
<el-radio label="formal">正赛</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="开始时间" prop="start_time"> <el-form-item label="开始时间" prop="start_time">
<el-date-picker <el-date-picker
v-model="form.start_time" v-model="form.start_time"
...@@ -232,6 +225,12 @@ function disabledMinSubmitDate(time: Date) { ...@@ -232,6 +225,12 @@ function disabledMinSubmitDate(time: Date) {
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"></el-option> <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="大赛模式">
<el-radio-group v-model="form.config.mode">
<el-radio label="test">训练</el-radio>
<el-radio label="formal">正赛</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="实操试题" prop="config.practical_question"> <el-form-item label="实操试题" prop="config.practical_question">
<AppEditor v-model="form.config.practical_question" :height="320" placeholder="请输入实操试题内容" /> <AppEditor v-model="form.config.practical_question" :height="320" placeholder="请输入实操试题内容" />
</el-form-item> </el-form-item>
......
...@@ -10,7 +10,7 @@ export function getExperimentList(params: { course_id?: string }) { ...@@ -10,7 +10,7 @@ export function getExperimentList(params: { course_id?: string }) {
return httpRequest.get('/api/lab/v1/teacher/video/experiments', { params }) return httpRequest.get('/api/lab/v1/teacher/video/experiments', { params })
} }
// 获取操作视频列表 // 获取操作视频列表
export function getVideoList(params?: { name?: string; experiment_id?: string; page?: number; page_size?: number }) { export function getVideoList(params?: { name?: string; experiment_id?: string; status?: string; page?: number; page_size?: number }) {
return httpRequest.get('/api/lab/v1/teacher/video/list', { params }) return httpRequest.get('/api/lab/v1/teacher/video/list', { params })
} }
// 获取操作视频详情 // 获取操作视频详情
......
<script setup lang="ts"> <script setup lang="ts">
import type { VideoItem } from '../types' import type { VideoItem } from '../types'
import { CirclePlus } from '@element-plus/icons-vue' import { CirclePlus } from '@element-plus/icons-vue'
import { useLocalStorage } from '@vueuse/core'
import AppList from '@/components/base/AppList.vue' import AppList from '@/components/base/AppList.vue'
import ListItem from '../components/ListItem.vue' import ListItem from '../components/ListItem.vue'
import { getVideoList } from '../api' import { getVideoList } from '../api'
import { useGetExperimentList } from '../composables/useGetExperimentList' import { useGetExperimentList } from '../composables/useGetExperimentList'
import { useMapStore } from '@/stores/map'
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue')) const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
const VIEW_MODE_STORAGE_KEY = 'list-view-mode'
type ViewMode = 'card' | 'list'
const status = computed(() => useMapStore().getMapValuesByKey('system_status'))
// 实验列表 // 实验列表
const { experiments, updateExperiments } = useGetExperimentList() const { experiments, updateExperiments } = useGetExperimentList()
updateExperiments() updateExperiments()
...@@ -14,12 +19,21 @@ updateExperiments() ...@@ -14,12 +19,21 @@ updateExperiments()
const route = useRoute() const route = useRoute()
const appList = $ref<InstanceType<typeof AppList> | null>(null) const appList = $ref<InstanceType<typeof AppList> | null>(null)
const viewMode = useLocalStorage<ViewMode>(VIEW_MODE_STORAGE_KEY, 'list')
function setViewMode(mode: ViewMode) {
viewMode.value = mode
}
function isActiveStatus(statusValue: string) {
return String(statusValue) === '1'
}
// 列表配置 // 列表配置
const listOptions = $computed(() => { const listOptions = $computed(() => {
return { return {
remote: { remote: {
httpRequest: getVideoList, httpRequest: getVideoList,
params: { name: '', experiment_id: route.query.experiment_id || '' } params: { name: '', experiment_id: route.query.experiment_id || '', status: '' },
}, },
filters: [ filters: [
{ type: 'input', prop: 'name', label: '实验操作视频名称', placeholder: '请输入实验操作视频名称' }, { type: 'input', prop: 'name', label: '实验操作视频名称', placeholder: '请输入实验操作视频名称' },
...@@ -30,10 +44,27 @@ const listOptions = $computed(() => { ...@@ -30,10 +44,27 @@ const listOptions = $computed(() => {
options: experiments.value, options: experiments.value,
labelKey: 'name', labelKey: 'name',
valueKey: 'id', valueKey: 'id',
placeholder: '请选择关联实验' placeholder: '请选择关联实验',
} },
{
type: 'select',
prop: 'status',
label: '生效状态',
options: status.value,
placeholder: '请选择生效状态',
},
],
columns: [
{ label: '名称', prop: 'name' },
{ label: '文件大小', prop: 'size_name', width: 110 },
{ label: '观看次数', prop: 'pv', width: 110 },
{ label: '关联实验', prop: 'experiment_id_name' },
{ label: '生效状态', prop: 'status_name', width: 110, slots: 'table-status' },
{ label: '创建人', prop: 'created_operator_name', width: 120 },
{ label: '创建时间', prop: 'created_time', width: 180 },
{ label: '更新时间', prop: 'updated_time', width: 180 },
{ label: '操作', slots: 'table-x', width: 160 },
], ],
columns: [{ label: '名称', prop: 'name' }]
} }
}) })
...@@ -62,13 +93,46 @@ function onUpdateSuccess() { ...@@ -62,13 +93,46 @@ function onUpdateSuccess() {
<el-button type="primary" :icon="CirclePlus" @click="handleAdd" v-permission="'v1-teacher-video-create'" <el-button type="primary" :icon="CirclePlus" @click="handleAdd" v-permission="'v1-teacher-video-create'"
>新增操作视频</el-button >新增操作视频</el-button
> >
<div class="view-switch">
<button
type="button"
class="view-switch__button"
:class="{ 'is-active': viewMode === 'card' }"
@click="setViewMode('card')"
title="卡片展示">
<span class="view-switch__icon view-switch__icon--card" aria-hidden="true">
<span v-for="item in 4" :key="item" class="view-switch__cell"></span>
</span>
</button>
<button
type="button"
class="view-switch__button"
:class="{ 'is-active': viewMode === 'list' }"
@click="setViewMode('list')"
title="列表展示">
<span class="view-switch__icon view-switch__icon--list" aria-hidden="true">
<span v-for="item in 3" :key="item" class="view-switch__line"></span>
</span>
</button>
</div>
</template> </template>
<template #body="{ data }"> <template #body="{ data }" v-if="viewMode === 'card'">
<div class="list-card" v-if="data.length"> <div class="list-card" v-if="data.length">
<ListItem v-for="item in data" :data="item" :key="item.id" @clickEdit="handleUpdate(item)"></ListItem> <ListItem v-for="item in data" :data="item" :key="item.id" @clickEdit="handleUpdate(item)"></ListItem>
</div> </div>
<el-empty description="暂无数据" v-else /> <el-empty description="暂无数据" v-else />
</template> </template>
<template #table-x="{ row }: { row: VideoItem }">
<el-button type="primary" round v-permission="'v1-teacher-video-view'">
<router-link :to="`/admin/lab/video/${row.id}`" target="_blank">查看</router-link>
</el-button>
<el-button type="primary" round @click="handleUpdate(row)" v-permission="'v1-teacher-video-update'"
>编辑</el-button
>
</template>
<template #table-status="{ row }: { row: VideoItem }">
<span :class="isActiveStatus(row.status) ? 'status-active' : 'status-inactive'">{{ row.status_name }}</span>
</template>
</AppList> </AppList>
</AppCard> </AppCard>
<FormDialog v-model="dialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="dialogVisible"></FormDialog> <FormDialog v-model="dialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="dialogVisible"></FormDialog>
...@@ -80,12 +144,99 @@ function onUpdateSuccess() { ...@@ -80,12 +144,99 @@ function onUpdateSuccess() {
background: #fff; background: #fff;
border-radius: 12px; border-radius: 12px;
} }
:deep(.table-list-buttons) {
align-items: center;
justify-content: space-between;
}
.view-switch {
display: inline-flex;
margin-left: auto;
border: 1px solid #e6e8eb;
border-radius: 10px;
background: #fff;
overflow: hidden;
}
.view-switch__button {
width: 32px;
height: 32px;
border: 0;
padding: 0;
background: transparent;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease;
&:hover {
background: #f6f7f9;
}
&.is-active {
background: rgba(186, 20, 62, 0.12);
}
}
.view-switch__icon {
width: 14px;
height: 14px;
color: #7a7f87;
display: inline-flex;
}
.view-switch__button.is-active .view-switch__icon {
color: var(--main-color);
}
.view-switch__icon--card {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 2px;
}
.view-switch__cell {
width: 6px;
height: 6px;
border: 1.25px solid currentColor;
border-radius: 2px;
box-sizing: border-box;
}
.view-switch__icon--list {
flex-direction: column;
justify-content: space-between;
}
.view-switch__line {
width: 14px;
height: 2px;
border-radius: 999px;
background: currentColor;
}
.status-active {
color: #67c23a;
}
.status-inactive {
color: var(--main-color);
}
.list-card { .list-card {
display: grid; display: grid;
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, 1fr);
gap: 20px; gap: 20px;
} }
@media (max-width: 1400px) { .list-card { grid-template-columns: repeat(3, 1fr); } } @media (max-width: 1400px) {
@media (max-width: 1024px) { .list-card { grid-template-columns: repeat(2, 1fr); gap: 16px; } } .list-card {
@media (max-width: 767px) { .list-card { grid-template-columns: 1fr; gap: 12px; } } grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 1024px) {
.list-card {
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
}
@media (max-width: 767px) {
:deep(.table-list-buttons) {
justify-content: flex-start;
}
.view-switch {
margin-left: 0;
}
.list-card {
grid-template-columns: 1fr;
gap: 12px;
}
}
</style> </style>
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论