Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
S
saas-lab
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
EzijingWeb
saas-lab
Commits
719a1abe
提交
719a1abe
authored
7月 06, 2026
作者:
王鹏飞
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
feat: 列表支持卡片和列表模式切换
上级
030b547a
隐藏空白字符变更
内嵌
并排
正在显示
9 个修改的文件
包含
519 行增加
和
35 行删除
+519
-35
AppList.vue
src/components/base/AppList.vue
+1
-1
api.ts
src/modules/admin/lab/book/api.ts
+1
-1
Index.vue
src/modules/admin/lab/book/views/Index.vue
+154
-7
api.ts
src/modules/admin/lab/case/api.ts
+1
-1
ListItem.vue
src/modules/admin/lab/case/components/ListItem.vue
+9
-1
Index.vue
src/modules/admin/lab/case/views/Index.vue
+187
-8
CompetitionRuleDialog.vue
...admin/lab/experiment/components/CompetitionRuleDialog.vue
+6
-7
api.ts
src/modules/admin/lab/video/api.ts
+1
-1
Index.vue
src/modules/admin/lab/video/views/Index.vue
+159
-8
没有找到文件。
src/components/base/AppList.vue
浏览文件 @
719a1abe
...
...
@@ -128,7 +128,7 @@ defineExpose({ refetch, tableRef })
<slot></slot>
<!-- 主体 -->
<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' }"
:data=
"dataList"
v-loading=
"loading && hasLoading"
v-bind=
"$attrs"
ref=
"tableRef"
>
...
...
src/modules/admin/lab/book/api.ts
浏览文件 @
719a1abe
...
...
@@ -10,7 +10,7 @@ export function getExperimentList(params: { course_id?: string }) {
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
})
}
// 获取指导书详情
...
...
src/modules/admin/lab/book/views/Index.vue
浏览文件 @
719a1abe
<
script
setup
lang=
"ts"
>
import
type
{
BookItem
}
from
'../types'
import
{
CirclePlus
}
from
'@element-plus/icons-vue'
import
{
useLocalStorage
}
from
'@vueuse/core'
import
AppList
from
'@/components/base/AppList.vue'
import
ListItem
from
'../components/ListItem.vue'
import
{
getBookList
}
from
'../api'
import
{
useGetExperimentList
}
from
'../composables/useGetExperimentList'
import
{
useMapStore
}
from
'@/stores/map'
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
()
updateExperiments
()
...
...
@@ -14,12 +19,21 @@ updateExperiments()
const
route
=
useRoute
()
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
(()
=>
{
return
{
remote
:
{
httpRequest
:
getBookList
,
params
:
{
name
:
''
,
experiment_id
:
route
.
query
.
experiment_id
||
''
}
params
:
{
name
:
''
,
experiment_id
:
route
.
query
.
experiment_id
||
''
,
status
:
''
},
},
filters
:
[
{
type
:
'input'
,
prop
:
'name'
,
label
:
'实验指导书名称'
,
placeholder
:
'请输入实验指导书名称'
},
...
...
@@ -30,10 +44,27 @@ const listOptions = $computed(() => {
options
:
experiments
.
value
,
labelKey
:
'name'
,
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() {
<el-button
type=
"primary"
:icon=
"CirclePlus"
@
click=
"handleAdd"
v-permission=
"'v1-teacher-book-create'"
>
新增实验指导书
</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
#
body=
"{ data }"
>
<
template
#
body=
"{ data }"
v-if=
"viewMode === 'card'"
>
<div
class=
"list-card"
v-if=
"data.length"
>
<ListItem
v-for=
"item in data"
:data=
"item"
:key=
"item.id"
@
clickEdit=
"handleUpdate(item)"
></ListItem>
</div>
<el-empty
description=
"暂无数据"
v-else
/>
</
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>
</AppCard>
<FormDialog
v-model=
"dialogVisible"
:data=
"rowData"
@
update=
"onUpdateSuccess"
v-if=
"dialogVisible"
></FormDialog>
...
...
@@ -80,11 +144,94 @@ function onUpdateSuccess() {
background
:
#fff
;
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
{
display
:
grid
;
grid-template-columns
:
repeat
(
4
,
1fr
);
gap
:
20px
;
}
@media
(
max-width
:
1024px
)
{
.list-card
{
grid-template-columns
:
repeat
(
2
,
1fr
);
gap
:
16px
;
}
}
@media
(
max-width
:
767px
)
{
.list-card
{
grid-template-columns
:
1fr
;
gap
:
12px
;
}
}
@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
>
src/modules/admin/lab/case/api.ts
浏览文件 @
719a1abe
...
...
@@ -10,7 +10,7 @@ export function getExperimentList(params: { course_id?: string }) {
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
})
}
// 获取案例原文详情
...
...
src/modules/admin/lab/case/components/ListItem.vue
浏览文件 @
719a1abe
...
...
@@ -27,6 +27,9 @@ const status = useMapStore().getMapValuesByKey('system_status')
const
statusName
=
computed
(()
=>
{
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
(()
=>
{
return
props
.
data
.
size
+
'MB'
...
...
@@ -54,7 +57,7 @@ const sizeName = computed(() => {
</div>
<div
class=
"list-item-title"
>
<h2>
{{
data
.
name
}}
</h2>
<h6>
{{
statusName
}}
</h6>
<h6
:class=
"statusClass"
>
{{
statusName
}}
</h6>
</div>
<div
class=
"list-item-main"
>
<ol>
...
...
@@ -175,6 +178,11 @@ const sizeName = computed(() => {
font-family
:
Source
Han
Sans
CN
;
font-weight
:
500
;
line-height
:
20px
;
}
.status-active
{
color
:
#67c23a
;
}
.status-inactive
{
color
:
var
(
--
main-color
);
}
}
...
...
src/modules/admin/lab/case/views/Index.vue
浏览文件 @
719a1abe
<
script
setup
lang=
"ts"
>
import
type
{
CaseItem
}
from
'../types'
import
{
CirclePlus
}
from
'@element-plus/icons-vue'
import
{
useLocalStorage
}
from
'@vueuse/core'
import
AppList
from
'@/components/base/AppList.vue'
import
ListItem
from
'../components/ListItem.vue'
import
{
getCaseList
}
from
'../api'
import
{
useGetExperimentList
}
from
'../composables/useGetExperimentList'
import
{
useMapStore
}
from
'@/stores/map'
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
()
updateExperiments
()
...
...
@@ -14,12 +28,44 @@ updateExperiments()
const
route
=
useRoute
()
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
(()
=>
{
return
{
remote
:
{
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
:
[
{
type
:
'input'
,
prop
:
'name'
,
label
:
'案例原文名称'
,
placeholder
:
'请输入案例原文名称'
},
...
...
@@ -30,10 +76,27 @@ const listOptions = $computed(() => {
options
:
experiments
.
value
,
labelKey
:
'name'
,
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() {
<el-button
type=
"primary"
:icon=
"CirclePlus"
@
click=
"handleAdd"
v-permission=
"'teacher-experiment-cases-create'"
>
新增案例原文
</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
#
body=
"{ data }: { data: Case
Item[] }
"
>
<div
class=
"list-card"
v-if=
"data.length"
>
<
template
#
body=
"{ data }: { data: Case
ListRow[] }"
v-if=
"viewMode === 'card'
"
>
<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>
</div>
<el-empty
description=
"暂无数据"
v-else
/>
</
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>
</AppCard>
<FormDialog
v-model=
"dialogVisible"
:data=
"rowData"
@
update=
"onUpdateSuccess"
v-if=
"dialogVisible"
></FormDialog>
...
...
@@ -80,11 +176,94 @@ function onUpdateSuccess() {
background
:
#fff
;
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
{
display
:
grid
;
grid-template-columns
:
repeat
(
4
,
1fr
);
gap
:
20px
;
}
@media
(
max-width
:
1024px
)
{
.list-card
{
grid-template-columns
:
repeat
(
2
,
1fr
);
gap
:
16px
;
}
}
@media
(
max-width
:
767px
)
{
.list-card
{
grid-template-columns
:
1fr
;
gap
:
12px
;
}
}
@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
>
src/modules/admin/lab/experiment/components/CompetitionRuleDialog.vue
浏览文件 @
719a1abe
...
...
@@ -43,7 +43,6 @@ const rules = ref({
end_time
:
[{
required
:
true
,
message
:
'请选择'
}],
questions
:
[{
required
:
true
,
message
:
'请选择'
}],
competition
:
[{
required
:
true
,
message
:
'请选择'
}],
'config.practical_question'
:
[{
required
:
true
,
message
:
'请输入实操试题'
}],
})
const
options
=
ref
([
...
...
@@ -196,12 +195,6 @@ function disabledMinSubmitDate(time: Date) {
:value=
"item.value"
></el-option>
</el-select>
</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-date-picker
v-model=
"form.start_time"
...
...
@@ -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-select>
</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"
>
<AppEditor
v-model=
"form.config.practical_question"
:height=
"320"
placeholder=
"请输入实操试题内容"
/>
</el-form-item>
...
...
src/modules/admin/lab/video/api.ts
浏览文件 @
719a1abe
...
...
@@ -10,7 +10,7 @@ export function getExperimentList(params: { course_id?: string }) {
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
})
}
// 获取操作视频详情
...
...
src/modules/admin/lab/video/views/Index.vue
浏览文件 @
719a1abe
<
script
setup
lang=
"ts"
>
import
type
{
VideoItem
}
from
'../types'
import
{
CirclePlus
}
from
'@element-plus/icons-vue'
import
{
useLocalStorage
}
from
'@vueuse/core'
import
AppList
from
'@/components/base/AppList.vue'
import
ListItem
from
'../components/ListItem.vue'
import
{
getVideoList
}
from
'../api'
import
{
useGetExperimentList
}
from
'../composables/useGetExperimentList'
import
{
useMapStore
}
from
'@/stores/map'
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
()
updateExperiments
()
...
...
@@ -14,12 +19,21 @@ updateExperiments()
const
route
=
useRoute
()
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
(()
=>
{
return
{
remote
:
{
httpRequest
:
getVideoList
,
params
:
{
name
:
''
,
experiment_id
:
route
.
query
.
experiment_id
||
''
}
params
:
{
name
:
''
,
experiment_id
:
route
.
query
.
experiment_id
||
''
,
status
:
''
},
},
filters
:
[
{
type
:
'input'
,
prop
:
'name'
,
label
:
'实验操作视频名称'
,
placeholder
:
'请输入实验操作视频名称'
},
...
...
@@ -30,10 +44,27 @@ const listOptions = $computed(() => {
options
:
experiments
.
value
,
labelKey
:
'name'
,
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() {
<el-button
type=
"primary"
:icon=
"CirclePlus"
@
click=
"handleAdd"
v-permission=
"'v1-teacher-video-create'"
>
新增操作视频
</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
#
body=
"{ data }"
>
<
template
#
body=
"{ data }"
v-if=
"viewMode === 'card'"
>
<div
class=
"list-card"
v-if=
"data.length"
>
<ListItem
v-for=
"item in data"
:data=
"item"
:key=
"item.id"
@
clickEdit=
"handleUpdate(item)"
></ListItem>
</div>
<el-empty
description=
"暂无数据"
v-else
/>
</
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>
</AppCard>
<FormDialog
v-model=
"dialogVisible"
:data=
"rowData"
@
update=
"onUpdateSuccess"
v-if=
"dialogVisible"
></FormDialog>
...
...
@@ -80,12 +144,99 @@ function onUpdateSuccess() {
background
:
#fff
;
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
{
display
:
grid
;
grid-template-columns
:
repeat
(
5
,
1fr
);
gap
:
20px
;
}
@media
(
max-width
:
1400px
)
{
.list-card
{
grid-template-columns
:
repeat
(
3
,
1fr
);
}
}
@media
(
max-width
:
1024px
)
{
.list-card
{
grid-template-columns
:
repeat
(
2
,
1fr
);
gap
:
16px
;
}
}
@media
(
max-width
:
767px
)
{
.list-card
{
grid-template-columns
:
1fr
;
gap
:
12px
;
}
}
@media
(
max-width
:
1400px
)
{
.list-card
{
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
>
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论