Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
S
saas-lab
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
EzijingWeb
saas-lab
Commits
33f3aff4
提交
33f3aff4
authored
10月 13, 2026
作者:
王鹏飞
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
feat: 优化移动端布局与实验操作体验
上级
bdff3c3f
显示空白字符变更
内嵌
并排
正在显示
20 个修改的文件
包含
621 行增加
和
149 行删除
+621
-149
base.css
src/assets/styles/base.css
+66
-3
DragPanel.vue
src/components/DragPanel.vue
+15
-6
Message.vue
src/components/Message.vue
+22
-8
AppCard.vue
src/components/base/AppCard.vue
+6
-0
AppList.vue
src/components/base/AppList.vue
+120
-10
Header.vue
src/components/layout/Header.vue
+30
-18
Index.vue
src/components/layout/Index.vue
+1
-1
Index.vue
src/modules/admin/lab/book/views/Index.vue
+20
-2
Index.vue
src/modules/admin/lab/case/views/Index.vue
+20
-2
CompetitionRuleDialog.vue
...admin/lab/experiment/components/CompetitionRuleDialog.vue
+4
-2
DMLFormDialog.vue
...modules/admin/lab/experiment/components/DMLFormDialog.vue
+56
-5
Index.vue
src/modules/admin/lab/experiment/views/Index.vue
+1
-0
View.vue
src/modules/admin/lab/experiment/views/View.vue
+45
-18
Index.vue
src/modules/admin/lab/video/views/Index.vue
+20
-2
AdminHome.vue
src/modules/home/components/AdminHome.vue
+17
-3
StudentHome.vue
src/modules/home/components/StudentHome.vue
+17
-3
TeacherHome.vue
src/modules/home/components/TeacherHome.vue
+17
-3
Default.vue
src/modules/home/views/Default.vue
+16
-0
Info.vue
src/modules/student/lab/components/Info.vue
+11
-1
Index.vue
src/modules/student/lab/views/Index.vue
+117
-62
没有找到文件。
src/assets/styles/base.css
浏览文件 @
33f3aff4
...
...
@@ -89,17 +89,69 @@ video {
}
@media
(
max-width
:
767px
)
{
.el-overlay-dialog
{
padding
:
max
(
12px
,
env
(
safe-area-inset-top
))
12px
max
(
12px
,
env
(
safe-area-inset-bottom
));
box-sizing
:
border-box
;
}
.el-dialog
{
width
:
92
%
!important
;
width
:
100
%
!important
;
max-width
:
500px
;
margin-top
:
10vh
!important
;
margin
:
0
auto
!important
;
max-height
:
calc
(
100
dvh
-
max
(
12px
,
env
(
safe-area-inset-top
))
-
max
(
12px
,
env
(
safe-area-inset-bottom
)));
display
:
flex
;
flex-direction
:
column
;
}
.el-dialog__header
,
.el-dialog__footer
{
flex-shrink
:
0
;
}
.el-dialog__body
{
min-height
:
0
;
overflow-y
:
auto
;
overscroll-behavior
:
contain
;
}
.el-dialog
.el-form
:not
(
.el-form--inline
)
.el-form-item
{
display
:
block
;
}
.el-dialog
.el-form
:not
(
.el-form--inline
)
.el-form-item__label
{
display
:
block
;
width
:
auto
!important
;
height
:
auto
;
float
:
none
;
line-height
:
24px
;
padding-bottom
:
4px
;
white-space
:
normal
;
text-align
:
left
;
}
.el-dialog
.el-form
:not
(
.el-form--inline
)
.el-form-item__content
{
margin-left
:
0
!important
;
min-width
:
0
;
}
.el-dialog__footer
{
display
:
flex
;
flex-wrap
:
wrap
;
justify-content
:
flex-end
;
gap
:
8px
;
}
.el-dialog__footer
.el-button
{
min-height
:
40px
;
margin-left
:
0
;
}
.el-message-box
{
width
:
90%
!important
;
max-width
:
420px
;
}
.el-drawer
{
.el-drawer.ltr
,
.el-drawer.rtl
{
width
:
90%
!important
;
max-width
:
420px
;
}
.el-drawer.mobile-menu-drawer
{
width
:
min
(
76vw
,
280px
)
!important
;
}
.el-input__inner
,
.el-textarea__inner
{
font-size
:
16px
;
}
.el-form--inline
.el-form-item
{
margin-right
:
0
;
...
...
@@ -115,6 +167,17 @@ video {
}
}
@media
(
max-width
:
767px
)
{
.el-button.el-button
{
--el-button-size
:
40px
;
min-height
:
40px
;
font-size
:
14px
;
box-sizing
:
border-box
;
}
.el-button.el-button.is-circle
{
width
:
40px
;
min-width
:
40px
;
padding
:
8px
;
}
.hide-on-mobile
{
display
:
none
!important
;
}
...
...
src/components/DragPanel.vue
浏览文件 @
33f3aff4
<
script
setup
lang=
"ts"
>
import
{
useMediaQuery
,
useStorage
}
from
'@vueuse/core'
import
{
ArrowDown
,
ArrowUp
}
from
'@element-plus/icons-vue'
const
emit
=
defineEmits
<
{
(
e
:
'resize'
):
void
}
>
()
defineProps
<
{
isLeftShow
?:
boolean
}
>
(
)
withDefaults
(
defineProps
<
{
isLeftShow
?:
boolean
;
mobileToggleLabel
?:
string
;
hideMobileToggle
?:
boolean
}
>
(),
{
mobileToggleLabel
:
'试题'
,
hideMobileToggle
:
false
}
)
const
isMobile
=
useMediaQuery
(
'(max-width: 767px)'
)
const
leftPanelVisible
=
ref
<
boolean
>
(
true
)
...
...
@@ -67,11 +68,14 @@ onMounted(() => {
</div>
</div>
<button
v-if=
"!isLeftShow && isMobile"
v-if=
"!isLeftShow && isMobile
&& !hideMobileToggle
"
type=
"button"
class=
"panel-mobile-toggle"
:class=
"
{ 'is-collapsed': !leftPanelVisible }"
:aria-expanded="leftPanelVisible"
@click="leftPanelVisible = !leftPanelVisible">
{{
leftPanelVisible
?
'收起试题'
:
'展开试题'
}}
<span>
{{
leftPanelVisible
?
`收起${mobileToggleLabel
}
`
:
`展开${mobileToggleLabel
}
`
}}
<
/span
>
<
el
-
icon
><
ArrowUp
v
-
if
=
"leftPanelVisible"
/><
ArrowDown
v
-
else
/></
el
-
icon
>
<
/button
>
<
div
class
=
"drag-panel-right"
>
<
div
class
=
"drag-cover"
v
-
if
=
"dragFlag"
><
/div
>
...
...
@@ -187,12 +191,17 @@ onMounted(() => {
margin
-
top
:
0
;
margin
-
bottom
:
12
px
;
border
:
0
;
border-radius
:
0
0
16px
16px
;
background
:
#fff
;
gap
:
8
px
;
padding
:
0
12
px
;
border
-
radius
:
0
0
8
px
8
px
;
background
:
#
e1e4eb
;
color
:
var
(
--
main
-
color
);
font
-
size
:
14
px
;
line
-
height
:
1
;
box-shadow
:
0
6px
16px
rgba
(
0
,
0
,
0
,
0
.06
);
cursor
:
pointer
;
&
.
is
-
collapsed
{
border
-
radius
:
8
px
;
}
&
:
focus
-
visible
{
outline
:
2
px
solid
var
(
--
main
-
color
);
outline
-
offset
:
2
px
;
}
&
:
active
{
background
:
var
(
--
el
-
fill
-
color
);
}
}
.
drag
-
panel
-
left
-
content
{
height
:
100
%
;
...
...
src/components/Message.vue
浏览文件 @
33f3aff4
...
...
@@ -51,13 +51,13 @@ function handleClick(data: MessageType) {
</
script
>
<
template
>
<el-popover
trigger=
"click"
placement=
"top-start"
:width=
"380"
>
<el-popover
trigger=
"click"
placement=
"top-start"
:width=
"380"
popper-class=
"app-message-popper"
>
<template
#
reference
>
<
div
class=
"app-message
"
>
<
div
class=
"icon"
></div
>
<
p>
我的待办
</p
>
<span
v-if=
"currentList.length"
>
{{
currentList
.
length
}}
</span>
</
div
>
<
button
type=
"button"
class=
"app-message"
aria-label=
"我的待办
"
>
<
span
class=
"icon"
></span
>
<
span
class=
"app-message-text"
>
我的待办
</span
>
<span
class=
"app-message-count"
v-if=
"currentList.length"
>
{{
currentList
.
length
}}
</span>
</
button
>
</
template
>
<ul
class=
"app-message-list"
v-if=
"currentList.length"
>
<li
v-for=
"item in currentList"
:key=
"item.id"
@
click=
"handleClick(item)"
>
{{ item.payload.message }}
</li>
...
...
@@ -88,14 +88,14 @@ function handleClick(data: MessageType) {
border-radius
:
50%
;
overflow
:
hidden
;
}
p
{
.app-message-text
{
margin
:
0
5px
;
font-size
:
14px
;
font-family
:
Source
Han
Sans
CN
;
line-height
:
24px
;
color
:
#666666
;
}
span
{
.app-message-count
{
margin-top
:
-10px
;
width
:
16px
;
height
:
16px
;
...
...
@@ -117,4 +117,18 @@ function handleClick(data: MessageType) {
}
}
}
@media
(
max-width
:
767px
)
{
.app-message
{
right
:
12px
;
bottom
:
max
(
12px
,
env
(
safe-area-inset-bottom
));
min-width
:
44px
;
width
:
44px
;
height
:
44px
;
padding
:
3px
;
.app-message-text
{
display
:
none
;
}
.app-message-count
{
position
:
absolute
;
top
:
-2px
;
right
:
-2px
;
margin-top
:
0
;
}
}
:global
(
.app-message-popper
)
{
max-width
:
calc
(
100vw
-
24px
);
box-sizing
:
border-box
;
}
.app-message-list
{
max-height
:
50dvh
;
overflow-y
:
auto
;
li
{
overflow-wrap
:
anywhere
;
min-height
:
40px
;
box-sizing
:
border-box
;
}
}
}
</
style
>
src/components/base/AppCard.vue
浏览文件 @
33f3aff4
...
...
@@ -55,4 +55,10 @@ withDefaults(defineProps<Props>(), { hasBodyBackground: true })
border-radius
:
12px
;
}
}
@media
(
max-width
:
767px
)
{
.app-card.has-background
,
.app-card-bd.has-background
{
padding
:
14px
;
}
.app-card-hd
{
flex-wrap
:
wrap
;
gap
:
8px
;
}
.app-card-hd__title
{
min-width
:
0
;
font-size
:
18px
;
line-height
:
1
.4
;
overflow-wrap
:
anywhere
;
margin-bottom
:
12px
;
}
.app-card-hd-aside
{
max-width
:
100%
;
}
}
</
style
>
src/components/base/AppList.vue
浏览文件 @
33f3aff4
<
script
setup
lang=
"ts"
>
import
{
Search
,
RefreshLeft
}
from
'@element-plus/icons-vue'
import
{
Search
,
RefreshLeft
,
Filter
}
from
'@element-plus/icons-vue'
import
{
useMediaQuery
}
from
'@vueuse/core'
const
isMobile
=
useMediaQuery
(
'(max-width: 767px)'
)
const
attrs
=
useAttrs
()
// 筛选、表格和移动端卡片通过动态名称传递不同的插槽参数。
defineSlots
<
{
...
...
@@ -48,6 +49,28 @@ const dataList = ref<any[]>([])
const
page
=
reactive
({
total
:
0
,
size
:
props
.
limit
,
currentPage
:
1
})
const
params
=
reactive
({
...
props
.
remote
?.
params
})
const
mobileFilterVisible
=
ref
(
false
)
const
mobileFilterDraft
=
reactive
<
Record
<
string
,
any
>>
({})
watch
(
isMobile
,
()
=>
{
mobileFilterVisible
.
value
=
false
})
const
activeFilterCount
=
computed
(()
=>
props
.
filters
.
filter
(
item
=>
{
const
value
=
params
[
item
.
prop
]
return
value
!==
undefined
&&
value
!==
null
&&
value
!==
''
&&
(
!
Array
.
isArray
(
value
)
||
value
.
length
>
0
)
}).
length
)
function
openMobileFilters
()
{
Object
.
keys
(
mobileFilterDraft
).
forEach
(
key
=>
delete
mobileFilterDraft
[
key
])
Object
.
assign
(
mobileFilterDraft
,
params
)
mobileFilterVisible
.
value
=
true
}
function
resetMobileFilters
()
{
props
.
filters
.
forEach
(
item
=>
{
mobileFilterDraft
[
item
.
prop
]
=
props
.
remote
?.
params
?.[
item
.
prop
]
??
''
})
}
function
applyMobileFilters
()
{
Object
.
assign
(
params
,
mobileFilterDraft
)
mobileFilterVisible
.
value
=
false
search
()
}
watchEffect
(()
=>
{
Object
.
assign
(
params
,
props
.
remote
?.
params
)
})
watchEffect
(()
=>
{
dataList
.
value
=
props
.
data
||
[]
})
...
...
@@ -94,19 +117,25 @@ function toggleMobileSelectRow(row: any) {
}
function
isMobileRowSelected
(
row
:
any
)
{
return
mobileSelection
.
value
.
some
((
r
)
=>
r
===
row
)
}
function
emitSelectionChange
()
{
const
attrs
=
useAttrs
()
const
handler
=
(
attrs
as
Record
<
string
,
unknown
>
).
onSelectionChange
as
((
val
:
any
[])
=>
void
)
|
undefined
if
(
handler
)
handler
(
mobileSelection
.
value
)
}
watch
(
dataList
,
()
=>
{
if
(
mobileSelection
.
value
.
length
)
{
mobileSelection
.
value
=
[]
emitSelectionChange
()
}
})
onMounted
(()
=>
{
fetchList
()
})
defineExpose
({
refetch
,
tableRef
})
</
script
>
<
template
>
<div
class=
"table-list"
>
<div
class=
"table-list-hd"
>
<div
class=
"table-list-filter"
v-if=
"
filters
&& filters.length"
>
<div
class=
"table-list-hd"
v-if=
"!isMobile && filters.length"
>
<div
class=
"table-list-filter"
v-if=
"
!isMobile
&& filters.length"
>
<el-form
:inline=
"true"
:model=
"params"
v-bind=
"filterForm"
ref=
"filterFormRef"
@
submit
.
prevent
>
<template
v-for=
"item in filters"
:key=
"item.prop"
>
<el-form-item
:label=
"item.label"
:prop=
"item.prop"
>
...
...
@@ -129,7 +158,40 @@ defineExpose({ refetch, tableRef })
</el-form>
</div>
</div>
<div
class=
"table-list-buttons"
><slot
name=
"header-buttons"
></slot></div>
<el-drawer
v-model=
"mobileFilterVisible"
title=
"筛选"
direction=
"btt"
size=
"75%"
append-to-body
class=
"mobile-filter-drawer"
>
<div
class=
"table-list-filter"
>
<el-form
:model=
"mobileFilterDraft"
v-bind=
"filterForm"
:inline=
"false"
label-position=
"top"
@
submit
.
prevent=
"applyMobileFilters"
>
<
template
v-for=
"item in filters"
:key=
"item.prop"
>
<el-form-item
:label=
"item.label"
:prop=
"item.prop"
>
<template
v-if=
"item.slots"
>
<slot
:name=
"item.slots"
v-bind=
"
{ params: mobileFilterDraft }">
</slot>
</
template
>
<
template
v-else
>
<el-input
v-model=
"mobileFilterDraft[item.prop]"
v-bind=
"item"
clearable
style=
"width: 200px"
v-if=
"item.type === 'input'"
/>
<el-select
v-model=
"mobileFilterDraft[item.prop]"
v-bind=
"item"
filterable
clearable
v-if=
"item.type === 'select'"
value-on-clear=
""
>
<el-option
:label=
"option[item.labelKey] || option.label"
:value=
"option[item.valueKey] || option.value"
v-for=
"(option, index) in item.options"
:key=
"index"
/>
</el-select>
</
template
>
</el-form-item>
</template>
</el-form>
</div>
<
template
#
footer
>
<div
class=
"mobile-filter-footer"
>
<el-button
:icon=
"RefreshLeft"
@
click=
"resetMobileFilters"
>
重置
</el-button>
<el-button
type=
"primary"
@
click=
"applyMobileFilters"
>
确定
</el-button>
</div>
</
template
>
</el-drawer>
<div
class=
"table-list-buttons"
>
<slot
name=
"header-buttons"
></slot>
<el-button
v-if=
"isMobile && filters.length"
:icon=
"Filter"
class=
"mobile-filter-trigger"
:aria-label=
"activeFilterCount ? `筛选,已选 ${activeFilterCount} 项` : '筛选'"
:class=
"{ 'is-active': activeFilterCount > 0 }"
@
click=
"openMobileFilters"
>
<span
v-if=
"activeFilterCount"
class=
"mobile-filter-count"
>
{{ activeFilterCount }}
</span>
</el-button>
</div>
<slot></slot>
<!-- 主体 -->
<div
class=
"table-list-bd"
>
...
...
@@ -154,8 +216,8 @@ defineExpose({ refetch, tableRef })
<div
class=
"mobile-card"
v-for=
"(row, rowIndex) in dataList"
:key=
"rowIndex"
:class=
"{ 'is-selected': isMobileRowSelected(row) }"
>
<div
class=
"mobile-card-body"
>
<div
class=
"mobile-card-checkbox"
v-if=
"hasSelectionColumn"
@
click
.
stop
=
"toggleMobileSelectRow(row)"
>
<el-checkbox
:model-value=
"isMobileRowSelected(row)"
/>
<div
class=
"mobile-card-checkbox"
v-if=
"hasSelectionColumn"
@
click
.
stop
>
<el-checkbox
:model-value=
"isMobileRowSelected(row)"
@
change=
"toggleMobileSelectRow(row)"
/>
</div>
<div
class=
"mobile-card-rows"
>
<div
class=
"mobile-card-row"
v-for=
"col in cardColumns"
:key=
"col.prop || col.label"
>
...
...
@@ -226,14 +288,62 @@ defineExpose({ refetch, tableRef })
// 移动端
@media
(
max-width
:
767px
)
{
.table-list-hd
.el-form-item__label
{
display
:
none
;
}
.table-list-buttons
.el-button.mobile-filter-trigger
{
position
:
relative
;
margin-left
:
auto
;
display
:
inline-flex
;
align-items
:
center
;
justify-content
:
center
;
flex
:
0
0
40px
;
width
:
40px
;
height
:
40px
;
padding
:
0
;
border-color
:
#e6e8eb
;
border-radius
:
10px
;
>
.el-icon
,
>
span
{
margin
:
0
;
}
&
.is-active
{
color
:
var
(
--
main-color
);
border-color
:
var
(
--
main-color
);
}
}
.table-list-buttons
:has
(
.view-switch
)
.mobile-filter-trigger
{
margin-left
:
0
;
}
.mobile-filter-count
{
position
:
absolute
;
top
:
-5px
;
right
:
-5px
;
min-width
:
16px
;
height
:
16px
;
padding
:
0
3px
;
border-radius
:
8px
;
background
:
var
(
--
main-color
);
color
:
#fff
;
font-size
:
10px
;
line-height
:
16px
;
}
.mobile-filter-drawer
{
border-radius
:
16px
16px
0
0
;
.el-drawer__header
{
margin-bottom
:
0
;
padding
:
16px
;
}
.el-drawer__body
{
padding
:
16px
;
overflow-y
:
auto
;
}
.el-drawer__footer
{
padding
:
12px
16px
max
(
12px
,
env
(
safe-area-inset-bottom
));
border-top
:
1px
solid
#eee
;
}
.table-list-filter
{
min-width
:
0
;
padding
:
0
;
}
}
.mobile-filter-footer
{
display
:
flex
;
gap
:
12px
;
.el-button
{
flex
:
1
;
height
:
40px
;
margin-left
:
0
;
}
}
.table-list-hd
.el-form-item__label
{
justify-content
:
flex-start
;
height
:
auto
;
line-height
:
24px
;
padding-bottom
:
4px
;
}
.table-list-filter
{
.el-form
,
.el-form--inline
{
width
:
100%
;
}
.el-form-item
{
width
:
100%
;
margin-right
:
0
;
margin-bottom
:
8
px
;
}
.el-form-item
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
stretch
;
width
:
100%
;
margin-right
:
0
;
margin-bottom
:
12
px
;
}
.el-form-item__content
{
width
:
100%
;
}
.el-input
,
.el-select
,
.el-cascader
{
width
:
100%
!
important
;
}
.filter-buttons
{
width
:
auto
;
}
.filter-buttons
{
width
:
100%
;
.el-form-item__content
{
display
:
flex
;
gap
:
8px
;
}
.el-button
{
flex
:
1
;
min-height
:
40px
;
margin-left
:
0
;
}
}
}
.table-list-buttons
{
align-items
:
center
;
gap
:
8px
;
.el-button
{
min-height
:
40px
;
margin-left
:
0
;
}
}
.mobile-card-ft
{
justify-content
:
flex-end
;
gap
:
8px
;
padding
:
4px
12px
;
.el-button
,
.el-button.is-text
{
min-height
:
40px
;
margin-left
:
0
;
padding
:
8px
10px
;
border
:
0
;
border-radius
:
4px
;
background
:
transparent
;
color
:
var
(
--
el-text-color-regular
);
font-size
:
14px
;
a
{
margin
:
0
;
padding
:
0
;
}
&
:hover
{
background
:
var
(
--
el-fill-color-light
);
}
&
.el-button--primary
{
color
:
var
(
--
main-color
);
}
&
.el-button--danger
{
color
:
var
(
--
el-color-danger
);
}
&
.is-disabled
{
color
:
var
(
--
el-disabled-text-color
);
background
:
transparent
;
}
}
}
.mobile-card-checkbox
{
min-height
:
44px
;
display
:
flex
;
align-items
:
center
;
}
.table-list-ft
{
flex-direction
:
column
;
gap
:
8px
;
padding
:
8px
0
;
}
.table-list-pagination
.el-pagination
{
justify-content
:
center
;
...
...
src/components/layout/Header.vue
浏览文件 @
33f3aff4
...
...
@@ -4,7 +4,7 @@ export default { name: 'AppHeader' }
<
script
setup
lang=
"ts"
>
import
type
{
IMenuItem
}
from
'@/types'
import
{
CaretBottom
,
Expand
}
from
'@element-plus/icons-vue'
import
{
CaretBottom
,
Expand
,
Close
}
from
'@element-plus/icons-vue'
import
{
useUserStore
}
from
'@/stores/user'
import
{
useMenuStore
}
from
'@/stores/menu'
import
{
useAppConfig
}
from
'@/composables/useAppConfig'
...
...
@@ -17,6 +17,7 @@ const router = useRouter()
const
mobileMenuVisible
=
ref
(
false
)
const
toggleMobileMenu
=
()
=>
{
mobileMenuVisible
.
value
=
!
mobileMenuVisible
.
value
}
const
closeMobileMenu
=
()
=>
{
mobileMenuVisible
.
value
=
false
}
watch
(()
=>
route
.
fullPath
,
closeMobileMenu
)
const
userInfo
=
userStore
.
user
...
...
@@ -65,7 +66,8 @@ function handleClick(path: string) {
<
template
>
<header
class=
"app-header"
>
<div
class=
"app-header-left"
>
<button
class=
"sidebar-toggle"
@
click=
"toggleMobileMenu"
>
<button
v-if=
"menus.length"
type=
"button"
class=
"sidebar-toggle"
aria-label=
"打开导航菜单"
:aria-expanded=
"mobileMenuVisible"
aria-controls=
"mobile-navigation"
@
click=
"toggleMobileMenu"
>
<el-icon
:size=
"22"
><Expand
/></el-icon>
</button>
<template
v-if=
"appConfig.logo"
>
...
...
@@ -74,7 +76,7 @@ function handleClick(path: string) {
</div>
<div
class=
"line"
v-if=
"appConfig.title"
></div>
</
template
>
<h1
class=
"app-name"
>
<h1
class=
"app-name"
:title=
"appConfig.title"
>
<router-link
to=
"/"
>
{{ appConfig.title }}
</router-link>
</h1>
</div>
...
...
@@ -134,21 +136,24 @@ function handleClick(path: string) {
</div>
</header>
<el-drawer
v-model=
"mobileMenuVisible"
direction=
"ltr"
:size=
"260"
:with-header=
"false"
:z-index=
"2100"
>
<div
class=
"mobile-drawer"
>
<el-drawer
v-model=
"mobileMenuVisible"
class=
"mobile-menu-drawer"
direction=
"ltr"
:size=
"260"
:with-header=
"false"
:z-index=
"2100"
>
<div
id=
"mobile-navigation"
class=
"mobile-drawer"
>
<div
class=
"mobile-drawer-hd"
>
<img
v-if=
"appConfig.logo"
:src=
"appConfig.logo"
class=
"mobile-drawer-logo"
/>
<span
class=
"mobile-drawer-title"
>
{{ appConfig.title }}
</span>
<button
type=
"button"
class=
"mobile-drawer-close"
aria-label=
"关闭导航菜单"
@
click=
"closeMobileMenu"
>
<el-icon
:size=
"20"
><Close
/></el-icon>
</button>
</div>
<div
class=
"mobile-drawer-bd"
>
<
template
v-for=
"item in menus"
:key=
"item.path"
>
<div
class=
"mobile-nav-group"
v-permission=
"item.tag"
>
<div
class=
"mobile-nav-parent"
@
click=
"handleClick(item.path)"
>
{{
item
.
name
}}
</div>
<button
type=
"button"
class=
"mobile-nav-parent"
:class=
"
{ 'is-active': !item.children
&&
route.path === item.path }"
@click="handleClick(item.path); closeMobileMenu()">
{{
item
.
name
}}
</button>
<template
v-if=
"item.children"
>
<
div
class=
"mobile-nav-child"
v-for=
"subitem in item.children"
:key=
"subitem.path"
<
button
type=
"button"
class=
"mobile-nav-child"
v-for=
"subitem in item.children"
:key=
"subitem.path"
v-permission=
"subitem.tag"
:class=
"
{ 'is-active': route.path.includes(subitem.path) }"
@click="handleClick(subitem.path); closeMobileMenu()">
{{
subitem
.
name
}}
</
div
>
@click="handleClick(subitem.path); closeMobileMenu()">
{{
subitem
.
name
}}
</
button
>
</
template
>
</div>
</template>
...
...
@@ -333,34 +338,41 @@ function handleClick(path: string) {
// 移动端 (≤767px)
@media
(
max-width
:
767px
)
{
.sidebar-toggle
{
display
:
flex
;
}
.sidebar-toggle
{
display
:
flex
;
width
:
44px
;
height
:
44px
;
margin-right
:
0
;
}
.app-header
{
padding
:
0
12px
;
height
:
52px
;
}
.app-header-left
{
.logo
{
height
:
32px
;
}
.line
{
margin
:
0
10px
;
height
:
22px
;
}
.app-name
{
font-size
:
16px
;
line-height
:
22px
;
}
flex
:
1
;
min-width
:
0
;
gap
:
8px
;
.logo
{
display
:
none
;
}
.line
{
display
:
none
;
}
.app-name
{
min-width
:
0
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
font-size
:
16px
;
line-height
:
22px
;
}
}
.app-header-nav
{
display
:
none
;
}
.app-header-right
{
flex-shrink
:
0
;
margin-left
:
8px
;
}
.app-header-right
.user
{
height
:
36px
;
p
{
display
:
none
;
}
.el-icon
{
display
:
none
;
}
}
.app-header-right
.avatar
{
width
:
32px
;
height
:
32px
;
}
}
// 移动端抽屉
.mobile-drawer
{
display
:
flex
;
flex-direction
:
column
;
height
:
100%
;
}
.mobile-menu-drawer
.el-drawer__body
{
padding
:
0
;
}
.mobile-drawer
{
display
:
flex
;
flex-direction
:
column
;
height
:
100%
;
padding-top
:
env
(
safe-area-inset-top
);
padding-bottom
:
env
(
safe-area-inset-bottom
);
box-sizing
:
border-box
;
}
.mobile-drawer-hd
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
padding
:
16px
;
border-bottom
:
1px
solid
#eee
;
}
.mobile-drawer-
logo
{
height
:
36px
;
}
.mobile-drawer-
title
{
font-size
:
18px
;
font-weight
:
600
;
color
:
#333
;
}
.mobile-drawer-
title
{
flex
:
1
;
min-width
:
0
;
font-size
:
16px
;
line-height
:
1
.5
;
font-weight
:
600
;
color
:
#333
;
overflow-wrap
:
anywhere
;
}
.mobile-drawer-
close
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
flex-shrink
:
0
;
width
:
40px
;
height
:
44px
;
background
:
transparent
;
color
:
#666
;
cursor
:
pointer
;
}
.mobile-drawer-bd
{
flex
:
1
;
overflow-y
:
auto
;
padding
:
8px
0
;
}
.mobile-nav-group
{
border-bottom
:
1px
solid
#f5f5f5
;
&
:last-child
{
border-bottom
:
none
;
}
}
.mobile-nav-parent
{
padding
:
14px
20px
;
font-size
:
15px
;
font-weight
:
600
;
color
:
#333
;
cursor
:
pointer
;
.mobile-nav-parent
{
display
:
block
;
width
:
100%
;
text-align
:
left
;
background
:
transparent
;
min-height
:
44px
;
padding
:
14px
20px
;
font-size
:
15px
;
font-weight
:
600
;
color
:
#333
;
cursor
:
pointer
;
&
:active
{
background
:
#f5f5f5
;
}
&
.is-active
{
color
:
var
(
--
main-color
);
background
:
#fef5f7
;
}
}
.mobile-nav-child
{
padding
:
12px
20px
12px
36px
;
font-size
:
14px
;
color
:
#666
;
cursor
:
pointer
;
border-top
:
1px
solid
#fafafa
;
.mobile-nav-child
{
display
:
block
;
width
:
100%
;
text-align
:
left
;
background
:
transparent
;
min-height
:
44px
;
padding
:
12px
20px
12px
36px
;
font-size
:
14px
;
color
:
#666
;
cursor
:
pointer
;
border-top
:
1px
solid
#fafafa
;
&
:active
{
background
:
#f5f5f5
;
}
&
.is-active
{
color
:
var
(
--
main-color
);
background
:
#fef5f7
;
}
}
...
...
src/components/layout/Index.vue
浏览文件 @
33f3aff4
...
...
@@ -37,7 +37,7 @@ const showOverlaySidebar = computed(() => isMobile.value && sidebarVisible.value
<AppHeader
v-if=
"header"
></AppHeader>
<div
class=
"app-layout-container"
>
<div
v-if=
"showOverlaySidebar"
class=
"sidebar-overlay"
@
click=
"closeSidebar"
></div>
<AppAside
v-if=
"sidebar"
:class=
"
{ 'app-aside--overlay': isMobile, 'app-aside--visible': sidebarVisible }">
<AppAside
v-if=
"sidebar
&& !isMobile
"
:class=
"
{ 'app-aside--overlay': isMobile, 'app-aside--visible': sidebarVisible }">
</AppAside>
<AppMain
@
click=
"closeSidebar"
></AppMain>
</div>
...
...
src/modules/admin/lab/book/views/Index.vue
浏览文件 @
33f3aff4
...
...
@@ -238,11 +238,29 @@ function onUpdateSuccess() {
}
}
@media
(
max-width
:
767px
)
{
:deep
(
.table-list-filter
)
{
padding
:
12px
;
.el-form-item
:last-child
{
margin-bottom
:
0
;
}
}
:deep
(
.table-list-buttons
)
{
justify-content
:
flex-start
;
justify-content
:
space-between
;
align-items
:
center
;
.el-button
{
height
:
40px
;
min-height
:
40px
;
}
}
.view-switch
{
margin-left
:
0
;
margin-left
:
auto
;
height
:
40px
;
box-sizing
:
border-box
;
flex-shrink
:
0
;
}
.view-switch__button
{
width
:
40px
;
height
:
100%
;
}
.list-card
{
grid-template-columns
:
1fr
;
...
...
src/modules/admin/lab/case/views/Index.vue
浏览文件 @
33f3aff4
...
...
@@ -272,11 +272,29 @@ function onUpdateSuccess() {
}
}
@media
(
max-width
:
767px
)
{
:deep
(
.table-list-filter
)
{
padding
:
12px
;
.el-form-item
:last-child
{
margin-bottom
:
0
;
}
}
:deep
(
.table-list-buttons
)
{
justify-content
:
flex-start
;
justify-content
:
space-between
;
align-items
:
center
;
.el-button
{
height
:
40px
;
min-height
:
40px
;
}
}
.view-switch
{
margin-left
:
0
;
margin-left
:
auto
;
height
:
40px
;
box-sizing
:
border-box
;
flex-shrink
:
0
;
}
.view-switch__button
{
width
:
40px
;
height
:
100%
;
}
.list-card
{
grid-template-columns
:
1fr
;
...
...
src/modules/admin/lab/experiment/components/CompetitionRuleDialog.vue
浏览文件 @
33f3aff4
...
...
@@ -7,12 +7,14 @@ import AppEditor from '@/components/base/AppEditor.vue'
interface
Props
{
id
:
string
showTrigger
?:
boolean
}
const
props
=
defineProps
<
Props
>
(
)
const
props
=
withDefaults
(
defineProps
<
Props
>
(),
{
showTrigger
:
true
}
)
const
isMobile
=
useMediaQuery
(
'(max-width: 767px)'
)
const
showAdvancedFields
=
false
const
dialogVisible
=
ref
(
false
)
defineExpose
({
open
:
()
=>
{
dialogVisible
.
value
=
true
}
})
const
gradeRuleList
=
[
{
label
:
'商品品类管理'
,
value
:
1
},
...
...
@@ -182,7 +184,7 @@ function disabledMinSubmitDate(time: Date) {
</
script
>
<
template
>
<el-button
type=
"primary"
@
click=
"dialogVisible = true"
>
编辑大赛规则
</el-button>
<el-button
v-if=
"showTrigger"
type=
"primary"
@
click=
"dialogVisible = true"
>
编辑大赛规则
</el-button>
<el-dialog
title=
"编辑大赛规则"
:close-on-click-modal=
"false"
width=
"1000px"
v-model=
"dialogVisible"
append-to-body
>
<el-form
ref=
"formRef"
:model=
"form"
:rules=
"rules"
label-position=
"right"
label-width=
"100px"
>
<el-form-item
label=
"大赛名称"
prop=
"name"
>
...
...
src/modules/admin/lab/experiment/components/DMLFormDialog.vue
浏览文件 @
33f3aff4
...
...
@@ -4,12 +4,13 @@ import type { ExperimentItem } from '../types'
import
{
ElMessage
}
from
'element-plus'
import
{
getTripConfig
,
updateTripConfig
,
getLiveCommodity
}
from
'../api'
import
{
useConnection
,
useUserAttr
,
useMetaEvent
,
useTag
,
useGroup
,
useMaterial
}
from
'../composables/useAllData'
import
{
useDocumentVisibility
}
from
'@vueuse/core'
import
{
useDocumentVisibility
,
useMediaQuery
}
from
'@vueuse/core'
import
{
dmlMenus
,
type
DmlMenu
}
from
'@/utils/dmlMenus'
import
{
useAppConfig
}
from
'@/composables/useAppConfig'
import
{
urls
}
from
'@/utils/env'
const
appConfig
=
useAppConfig
()
const
isMobile
=
useMediaQuery
(
'(max-width: 767px)'
)
const
props
=
defineProps
<
{
data
:
ExperimentItem
...
...
@@ -228,16 +229,17 @@ const handleSelectAll = () => {
<
template
>
<el-dialog
title=
"配置数字营销实验"
class=
"dml-config-dialog"
:close-on-click-modal=
"false"
width=
"1000px"
@
update:modelValue=
"(value) => $emit('update:modelValue', value)"
>
<el-form
ref=
"formRef"
:model=
"form"
label-suffix=
":"
>
<el-row
justify=
"space-between"
>
<el-form
ref=
"formRef"
:model=
"form"
label-suffix=
":"
:label-position=
"isMobile ? 'top' : 'right'"
>
<el-row
justify=
"space-between"
class=
"dml-config-summary"
>
<el-form-item
label=
"实验名称"
>
{{
data
.
name
}}
</el-form-item>
<el-form-item
label=
"实验类型"
>
{{
data
.
type_name
}}
</el-form-item>
<el-form-item
label=
"实验总成绩"
>
{{
data
.
score
}}
</el-form-item>
</el-row>
<el-tabs
v-model=
"step"
tab-position=
"left
"
>
<el-tabs
v-model=
"step"
:tab-position=
"isMobile ? 'top' : 'left'
"
>
<el-tab-pane
label=
"功能"
:name=
"-1"
>
<el-checkbox
@
click=
"handleSelectAll"
v-model=
"checked"
label=
"全选"
size=
"large"
/>
<div
class=
"check-ul"
>
...
...
@@ -411,7 +413,7 @@ const handleSelectAll = () => {
</el-tabs>
</el-form>
<
template
#
footer
>
<el-row
justify=
"center"
>
<el-row
justify=
"center"
class=
"dml-config-actions"
>
<el-button
round
auto-insert-space
@
click=
"$emit('update:modelValue', false)"
v-if=
"step === -1"
>
关闭
</el-button
>
...
...
@@ -441,4 +443,53 @@ const handleSelectAll = () => {
margin
:
0
!
important
;
}
}
@media
(
max-width
:
767px
)
{
.dml-config-dialog
{
.dml-config-summary
{
display
:
flex
;
flex-direction
:
column
;
padding
:
12px
;
margin-bottom
:
16px
;
border-radius
:
8px
;
background
:
var
(
--
el-fill-color-light
);
.el-form-item
{
width
:
100%
;
margin-bottom
:
8px
;
}
.el-form-item
:last-child
{
margin-bottom
:
0
;
}
}
.el-tabs
{
min-width
:
0
;
}
.el-tabs__header
{
margin-bottom
:
16px
;
}
.el-tabs__item
{
height
:
44px
;
padding
:
0
14px
;
}
.el-tabs__content
{
min-width
:
0
;
overflow
:
visible
;
}
.el-form-item__content
{
min-width
:
0
;
gap
:
8px
;
}
.el-select
{
width
:
100%
;
margin-left
:
0
!
important
;
}
.el-radio-group
{
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
8px
16px
;
}
.el-radio
{
margin-right
:
0
;
min-height
:
40px
;
height
:
auto
;
}
.check-ul
.li
{
margin-bottom
:
12px
;
padding
:
12px
;
border
:
1px
solid
var
(
--
el-border-color-lighter
);
border-radius
:
8px
;
.check-item
{
display
:
flex
;
flex-direction
:
column
;
padding-left
:
8px
;
}
.el-divider
{
display
:
none
;
}
}
.el-checkbox-group
{
display
:
flex
;
flex-direction
:
column
;
width
:
100%
;
}
.el-checkbox
{
max-width
:
100%
;
min-height
:
44px
;
height
:
auto
;
margin-right
:
0
;
align-items
:
flex-start
;
padding
:
10px
0
;
box-sizing
:
border-box
;
.el-checkbox__input
{
margin-top
:
3px
;
}
.el-checkbox__label
{
min-width
:
0
;
white-space
:
normal
;
overflow-wrap
:
anywhere
;
line-height
:
1
.5
;
}
}
.dml-config-actions
{
display
:
flex
;
flex-wrap
:
nowrap
;
width
:
100%
;
gap
:
8px
;
.el-button
{
flex
:
1
;
min-width
:
0
;
height
:
40px
;
margin-left
:
0
;
padding
:
0
8px
;
}
}
}
}
</
style
>
src/modules/admin/lab/experiment/views/Index.vue
浏览文件 @
33f3aff4
...
...
@@ -241,3 +241,4 @@ async function handleDelete(row: ExperimentItem) {
@
update=
"onUpdateSuccess"
v-if=
"copyDialogVisible && rowData"
></CopyDialog>
</template>
src/modules/admin/lab/experiment/views/View.vue
浏览文件 @
33f3aff4
<
script
setup
lang=
"ts"
>
import
type
{
ExperimentItem
,
ClassItem
}
from
'../types'
import
{
CirclePlus
}
from
'@element-plus/icons-vue'
import
{
useMediaQuery
}
from
'@vueuse/core'
import
{
CirclePlus
,
ArrowDown
}
from
'@element-plus/icons-vue'
import
{
ElMessage
,
ElMessageBox
}
from
'element-plus'
import
AppList
from
'@/components/base/AppList.vue'
...
...
@@ -8,6 +9,8 @@ import { getExperiment, experimentAddClass } from '../api'
import
{
useAppConfig
}
from
'@/composables/useAppConfig'
import
{
urls
}
from
'@/utils/env'
const
appConfig
=
useAppConfig
()
const
isMobile
=
useMediaQuery
(
'(max-width: 767px)'
)
const
competitionRulesRef
=
ref
<
{
open
:
()
=>
void
}
|
null
>
(
null
)
const
isSchool
=
import
.
meta
.
env
.
MODE
===
'school'
const
SelectClassDialog
=
defineAsyncComponent
(()
=>
import
(
'../components/SelectClassDialog.vue'
))
...
...
@@ -127,7 +130,7 @@ function handleEditReportRules() {
<
template
>
<AppCard
title=
"实验管理"
>
<template
#
header
>
<div>
<div
class=
"experiment-header"
>
<h2
class=
"app-card-hd__title"
>
实验管理
</h2>
<div
class=
"btn-all"
style=
"margin-bottom: 15px"
>
<el-button
type=
"primary"
v-if=
"detail?.type === '4'"
>
...
...
@@ -139,22 +142,34 @@ function handleEditReportRules() {
<el-button
type=
"primary"
v-if=
"detail?.type === '6'"
>
<a
:href=
"aiURL"
target=
"_blank"
>
进入AI实验平台
</a>
</el-button>
<el-button
type=
"primary"
>
<el-button
v-if=
"!isMobile"
type=
"primary"
>
<router-link
:to=
"
{ path: '/admin/lab/score', query: { course_id: detail?.course_id, experiment_id: detail?.id } }"
target="_blank"
>实验评分
</router-link
>
</el-button>
<el-button
type=
"primary"
@
click=
"gradeRulesVisible = true"
>
成绩规则
</el-button>
<el-button
type=
"primary"
@
click=
"reportRulesVisible = true"
>
报告规则
</el-button>
<el-button
type=
"primary"
@
click=
"handleUpdateDML()"
v-if=
"detail?.type === '4'"
>
配置数字营销
</el-button>
<CompetitionRuleDialog
v-if=
"!isSchool"
:id=
"id"
></CompetitionRuleDialog>
<el-button
type=
"primary"
@
click=
"handleCopy()"
>
复制实验
</el-button>
<el-button
v-if=
"!isMobile"
type=
"primary"
@
click=
"gradeRulesVisible = true"
>
成绩规则
</el-button>
<el-button
v-if=
"!isMobile"
type=
"primary"
@
click=
"reportRulesVisible = true"
>
报告规则
</el-button>
<el-button
type=
"primary"
@
click=
"handleUpdateDML()"
v-if=
"!isMobile && detail?.type === '4'"
>
配置数字营销
</el-button>
<CompetitionRuleDialog
v-if=
"!isSchool"
:id=
"id"
:show-trigger=
"!isMobile"
ref=
"competitionRulesRef"
></CompetitionRuleDialog>
<el-button
v-if=
"!isMobile"
type=
"primary"
@
click=
"handleCopy()"
>
复制实验
</el-button>
<el-dropdown
v-if=
"isMobile"
trigger=
"click"
class=
"experiment-more"
>
<el-button>
更多操作
<el-icon
class=
"more-arrow"
><ArrowDown
/></el-icon></el-button>
<template
#
dropdown
>
<el-dropdown-menu>
<el-dropdown-item
@
click=
"gradeRulesVisible = true"
>
成绩规则
</el-dropdown-item>
<el-dropdown-item
@
click=
"reportRulesVisible = true"
>
报告规则
</el-dropdown-item>
<el-dropdown-item
v-if=
"detail?.type === '4'"
@
click=
"handleUpdateDML"
>
配置数字营销
</el-dropdown-item>
<el-dropdown-item
v-if=
"!isSchool"
@
click=
"competitionRulesRef?.open()"
>
编辑大赛规则
</el-dropdown-item>
<el-dropdown-item
divided
@
click=
"handleCopy"
>
复制实验
</el-dropdown-item>
</el-dropdown-menu>
</
template
>
</el-dropdown>
</div>
</div>
</template>
<el-descriptions
title=
"基本信息"
v-if=
"detail"
>
<el-descriptions
title=
"基本信息"
:column=
"isMobile ? 1 : 3"
v-if=
"detail"
>
<!-- <template #extra>
<el-button type="primary" v-if="detail.type === '4'">
<a :href="dmlURL" target="_blank">进入实验平台</a>
...
...
@@ -162,17 +177,17 @@ function handleEditReportRules() {
<el-button type="primary" @click="gradeRulesVisible = true">查看成绩规则</el-button>
<el-button type="primary" @click="reportRulesVisible = true">查看报告规则</el-button>
</template> -->
<el-descriptions-item
:span=
"3"
label=
"实验名称:"
>
{{ detail.name }}
</el-descriptions-item>
<el-descriptions-item
:span=
"
isMobile ? 1 :
3"
label=
"实验名称:"
>
{{ detail.name }}
</el-descriptions-item>
<el-descriptions-item
label=
"实验课程:"
>
{{ detail.course_name }}
</el-descriptions-item>
<el-descriptions-item
label=
"所属机构/学校:"
>
{{ detail.organ_id_name }}
</el-descriptions-item>
<el-descriptions-item
label=
"实验总成绩:"
>
{{ detail.score }}
</el-descriptions-item>
<el-descriptions-item
label=
"实验类型:"
>
{{ detail.type_name }}
</el-descriptions-item>
<el-descriptions-item
:span=
"2"
label=
"实验学时:"
>
{{ detail.length }}学时
</el-descriptions-item>
<el-descriptions-item
:span=
"3"
label=
"指导教师:"
>
{{ teacherText }}
</el-descriptions-item>
<el-descriptions-item
:span=
"3"
label=
"实验目的:"
>
{{ detail.purpose }}
</el-descriptions-item>
<el-descriptions-item
:span=
"3"
label=
"实验要求:"
>
{{ detail.requirements }}
</el-descriptions-item>
<el-descriptions-item
:span=
"3"
label=
"实验内容及原理:"
>
{{ detail.content }}
</el-descriptions-item>
<el-descriptions-item
:span=
"3"
label=
"实验步骤及过程:"
>
{{ detail.procedure }}
</el-descriptions-item>
<el-descriptions-item
:span=
"
isMobile ? 1 :
2"
label=
"实验学时:"
>
{{ detail.length }}学时
</el-descriptions-item>
<el-descriptions-item
:span=
"
isMobile ? 1 :
3"
label=
"指导教师:"
>
{{ teacherText }}
</el-descriptions-item>
<el-descriptions-item
:span=
"
isMobile ? 1 :
3"
label=
"实验目的:"
>
{{ detail.purpose }}
</el-descriptions-item>
<el-descriptions-item
:span=
"
isMobile ? 1 :
3"
label=
"实验要求:"
>
{{ detail.requirements }}
</el-descriptions-item>
<el-descriptions-item
:span=
"
isMobile ? 1 :
3"
label=
"实验内容及原理:"
>
{{ detail.content }}
</el-descriptions-item>
<el-descriptions-item
:span=
"
isMobile ? 1 :
3"
label=
"实验步骤及过程:"
>
{{ detail.procedure }}
</el-descriptions-item>
</el-descriptions>
<el-divider
/>
<AppList
v-bind=
"listOptions"
ref=
"appList"
>
...
...
@@ -254,8 +269,20 @@ function handleEditReportRules() {
gap
:
8px
;
}
@media
(
max-width
:
767px
)
{
.btn-all
.el-button
{
width
:
100%
;
.experiment-header
{
width
:
100%
;
}
.btn-all
{
align-items
:
center
;
.el-button
{
height
:
40px
;
margin-left
:
0
;
padding
:
0
12px
;
a
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
height
:
100%
;
}
}
>
.el-button
{
flex
:
1
;
min-width
:
0
;
}
}
.experiment-more
{
margin-left
:
auto
;
.el-button
{
width
:
100%
;
}
}
.more-arrow
{
margin-left
:
6px
;
}
:deep
(
.el-descriptions__cell
)
{
padding-bottom
:
12px
;
line-height
:
1
.7
;
overflow-wrap
:
anywhere
;
}
:deep
(
.el-descriptions__label
)
{
color
:
var
(
--
el-text-color-secondary
);
}
}
</
style
>
src/modules/admin/lab/video/views/Index.vue
浏览文件 @
33f3aff4
...
...
@@ -245,11 +245,29 @@ function onUpdateSuccess() {
}
}
@media
(
max-width
:
767px
)
{
:deep
(
.table-list-filter
)
{
padding
:
12px
;
.el-form-item
:last-child
{
margin-bottom
:
0
;
}
}
:deep
(
.table-list-buttons
)
{
justify-content
:
flex-start
;
justify-content
:
space-between
;
align-items
:
center
;
.el-button
{
height
:
40px
;
min-height
:
40px
;
}
}
.view-switch
{
margin-left
:
0
;
margin-left
:
auto
;
height
:
40px
;
box-sizing
:
border-box
;
flex-shrink
:
0
;
}
.view-switch__button
{
width
:
40px
;
height
:
100%
;
}
.list-card
{
grid-template-columns
:
1fr
;
...
...
src/modules/home/components/AdminHome.vue
浏览文件 @
33f3aff4
...
...
@@ -106,11 +106,25 @@ function handleChange(id: string, type: number) {
}
@media
(
max-width
:
767px
)
{
.bg
{
padding
:
20px
12px
;
display
:
none
;
}
.link1
,
.link2
,
.link3
,
.link4
{
max-width
:
100%
;
margin-bottom
:
16px
;
display
:
none
;
}
.select-group
{
left
:
12px
;
right
:
12px
;
bottom
:
max
(
12px
,
env
(
safe-area-inset-bottom
));
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
12px
;
.el-select
{
width
:
calc
(
50%
-
6px
);
min-width
:
0
;
}
.el-select
+
.el-select
{
margin-left
:
0
;
}
}
}
</
style
>
src/modules/home/components/StudentHome.vue
浏览文件 @
33f3aff4
...
...
@@ -172,11 +172,25 @@ const learnURL = urls.learn
}
@media
(
max-width
:
767px
)
{
.bg
{
padding
:
20px
12px
;
display
:
none
;
}
.link1
,
.link2
,
.link3
,
.link4
{
max-width
:
100%
;
margin-bottom
:
16px
;
display
:
none
;
}
.select-group
{
left
:
12px
;
right
:
12px
;
bottom
:
max
(
12px
,
env
(
safe-area-inset-bottom
));
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
12px
;
.el-select
{
width
:
calc
(
50%
-
6px
);
min-width
:
0
;
}
.el-select
+
.el-select
{
margin-left
:
0
;
}
}
}
</
style
>
src/modules/home/components/TeacherHome.vue
浏览文件 @
33f3aff4
...
...
@@ -133,11 +133,25 @@ function handleChange(id: string, type: number) {
}
@media
(
max-width
:
767px
)
{
.bg
{
padding
:
20px
12px
;
display
:
none
;
}
.link1
,
.link2
,
.link3
,
.link4
{
max-width
:
100%
;
margin-bottom
:
16px
;
display
:
none
;
}
.select-group
{
left
:
12px
;
right
:
12px
;
bottom
:
max
(
12px
,
env
(
safe-area-inset-bottom
));
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
12px
;
.el-select
{
width
:
calc
(
50%
-
6px
);
min-width
:
0
;
}
.el-select
+
.el-select
{
margin-left
:
0
;
}
}
}
</
style
>
src/modules/home/views/Default.vue
浏览文件 @
33f3aff4
...
...
@@ -22,4 +22,20 @@ const StudentHome = defineAsyncComponent(() => import('../components/StudentHome
.home
{
margin
:
0
30px
;
}
@media
(
max-width
:
767px
)
{
.home
{
margin
:
12px
;
min-height
:
calc
(
100dvh
-
76px
);
padding-bottom
:
116px
;
box-sizing
:
border-box
;
}
}
</
style
>
<
style
lang=
"scss"
>
@media
(
max-width
:
767px
)
{
.app-main
:has
(
.home
)
{
padding
:
0
;
}
}
</
style
>
src/modules/student/lab/components/Info.vue
浏览文件 @
33f3aff4
<
script
setup
>
import
{
useMediaQuery
}
from
'@vueuse/core'
const
isMobile
=
useMediaQuery
(
'(max-width: 767px)'
)
const
props
=
defineProps
({
data
:
Object
})
const
teacherText
=
computed
(()
=>
{
...
...
@@ -9,7 +11,7 @@ const teacherText = computed(() => {
<
template
>
<template
v-if=
"data"
>
<el-form
label-suffix=
":"
label-width=
"120px"
label-position=
"right
"
>
<el-form
label-suffix=
":"
label-width=
"120px"
:label-position=
"isMobile ? 'top' : 'right'
"
>
<el-form-item
label=
"实验名称"
>
{{
data
.
name
}}
</el-form-item>
<el-form-item
label=
"实验课程名称"
>
{{
data
.
course
.
name
}}
</el-form-item>
<el-form-item
label=
"所属机构/学校"
>
{{
data
.
organization
.
name
}}
</el-form-item>
...
...
@@ -28,3 +30,11 @@ const teacherText = computed(() => {
</
template
>
<el-empty
description=
"暂无数据"
v-else
/>
</template>
<
style
lang=
"scss"
scoped
>
@media
(
max-width
:
767px
)
{
:deep
(
.el-form-item__label
)
{
width
:
auto
!
important
;
line-height
:
24px
;
height
:
auto
;
padding-bottom
:
4px
;
color
:
var
(
--
el-text-color-secondary
);
}
:deep
(
.el-form-item__content
)
{
line-height
:
1
.7
;
overflow-wrap
:
anywhere
;
min-width
:
0
;
}
:deep
(
.el-divider
)
{
margin
:
16px
0
;
}
}
</
style
>
src/modules/student/lab/views/Index.vue
浏览文件 @
33f3aff4
<
script
setup
lang=
"ts"
>
import
type
{
CourseType
,
ExperimentInfo
,
ExperimentRecord
}
from
'../types'
import
{
HomeFilled
}
from
'@element-plus/icons-vue'
import
{
HomeFilled
,
ArrowDown
}
from
'@element-plus/icons-vue'
import
{
ElMessageBox
,
ElMessage
}
from
'element-plus'
import
{
useMediaQuery
}
from
'@vueuse/core'
import
DragPanel
from
'@/components/DragPanel.vue'
...
...
@@ -105,20 +105,13 @@ watchEffect(() => {
})
const
tabActive
=
ref
(
'info'
)
const
mobilePanelExpanded
=
ref
(
false
)
const
tabLabelMap
:
Record
<
string
,
string
>
=
{
info
:
'实验信息'
,
example
:
'实验案例'
,
caseResource
:
'案例资源'
,
case
:
'案例原文'
,
book
:
'实训指导'
,
video
:
'操作视频'
,
exam
:
'理论试题'
,
qa
:
'实操试题'
,
result
:
'过程与结果'
,
discuss
:
'讨论交流'
,
const
mobileActionsVisible
=
ref
(
false
)
watch
(
isMobile
,
()
=>
{
mobileActionsVisible
.
value
=
false
})
function
closeMobileActionPanel
(
event
:
MouseEvent
)
{
const
target
=
event
.
target
as
HTMLElement
const
button
=
target
.
closest
(
'button'
)
if
(
button
&&
!
button
.
disabled
)
mobileActionsVisible
.
value
=
false
}
const
currentTabLabel
=
computed
(()
=>
tabLabelMap
[
tabActive
.
value
]
||
'实验面板'
)
const
examURL
=
ref
(
''
)
// 右侧
...
...
@@ -271,11 +264,11 @@ function handleGoToFunction(path: string) {
</
script
>
<
template
>
<DragPanel
@
resize=
"handleResize"
:isLeftShow=
"experimentInfo?.exam_status === 1
"
>
<DragPanel
class=
"student-lab-layout"
@
resize=
"handleResize"
:isLeftShow=
"experimentInfo?.exam_status === 1"
mobile-toggle-label=
"实验资料
"
>
<template
#
left
>
<div
class=
"lab-left"
>
<div
class=
"lab-left-body"
>
<el-form
:model=
"form"
label-suffix=
":"
hide-required-asterisk
>
<el-form
:model=
"form"
label-suffix=
":"
hide-required-asterisk
:label-position=
"isMobile ? 'top' : 'right'"
class=
"lab-course-picker"
>
<el-form-item
label=
"请选择课程"
>
<el-select
filterable
:model-value=
"form.course_id"
@
change=
"handleCourseChange"
style=
"width: 100%"
>
<el-option
v-for=
"item in courses"
:key=
"item.id"
:label=
"item.name"
:value=
"item.id"
></el-option>
...
...
@@ -287,13 +280,7 @@ function handleGoToFunction(path: string) {
</el-select>
</el-form-item>
</el-form>
<div
class=
"mobile-panel-toggle"
v-if=
"isMobile && experimentInfo"
>
<span>
当前面板:
{{
currentTabLabel
}}
</span>
<el-button
text
type=
"primary"
@
click=
"mobilePanelExpanded = !mobilePanelExpanded"
>
{{
mobilePanelExpanded
?
'收起'
:
'展开'
}}
</el-button>
</div>
<el-tabs
type=
"border-card"
v-model=
"tabActive"
v-if=
"experimentInfo"
v-show=
"!isMobile || mobilePanelExpanded"
>
<el-tabs
type=
"border-card"
v-model=
"tabActive"
v-if=
"experimentInfo"
>
<el-tab-pane
label=
"实验信息"
name=
"info"
>
<Info
:data=
"experimentInfo"
></Info>
</el-tab-pane>
...
...
@@ -348,19 +335,20 @@ function handleGoToFunction(path: string) {
</div>
</
template
>
<
template
#
right
>
<AppCard>
<AppCard
class=
"lab-operation-card"
>
<div
class=
"exam-status"
v-if=
"experimentInfo?.exam_status === 1"
>
<el-button
type=
"primary"
@
click=
"tabActive = 'exam'"
>
理论试题
</el-button>
<el-button
type=
"primary"
@
click=
"tabActive = 'qa'"
>
实操环境
</el-button>
</div>
<el-row
justify=
"space-between"
v-else
class=
"lab-toolbar"
>
<div
class=
"lab-toolbar-group"
>
<el-button
type=
"primary"
:icon=
"HomeFilled"
:disabled=
"submitted"
@
click=
"handleBackHome"
<el-button
v-if=
"!isMobile"
type=
"primary"
:icon=
"HomeFilled"
:disabled=
"submitted"
@
click=
"handleBackHome"
>
返回首页
</el-button
>
<el-button
type=
"primary"
:disabled=
"disabled"
@
click=
"handleSubmit"
>
提交实验
</el-button>
<el-button
v-if=
"isMobile"
@
click=
"mobileActionsVisible = true"
>
更多操作
<el-icon
style=
"margin-left: 6px"
><ArrowDown
/></el-icon></el-button>
</div>
<div
class=
"lab-toolbar-group"
>
<div
class=
"lab-toolbar-group"
v-if=
"!isMobile"
>
<el-button
type=
"primary"
v-if=
"appConfig.system == 'default'"
>
<router-link
to=
"/ai"
target=
"_blank"
>
AI数据分析
</router-link>
</el-button>
...
...
@@ -429,6 +417,44 @@ function handleGoToFunction(path: string) {
</template>
</DragPanel>
<el-drawer
v-model=
"mobileActionsVisible"
title=
"实验操作"
direction=
"btt"
size=
"65%"
append-to-body
class=
"student-lab-actions"
>
<div
class=
"student-lab-action-list"
@
click=
"closeMobileActionPanel"
>
<el-button
:icon=
"HomeFilled"
:disabled=
"submitted"
@
click=
"handleBackHome"
>
返回实验首页
</el-button>
<el-button
type=
"primary"
v-if=
"appConfig.system == 'default'"
>
<router-link
to=
"/ai"
target=
"_blank"
>
AI数据分析
</router-link>
</el-button>
<el-button
type=
"primary"
:disabled=
"disabled"
:loading=
"screenshotLoading"
@
click=
"handleCapture"
>
截图
</el-button
>
<el-button
type=
"primary"
:disabled=
"disabled"
@
click=
"prepareDialogVisible = true"
>
实验准备
</el-button>
<el-button
type=
"primary"
:disabled=
"disabled"
@
click=
"resultDialogVisible = true"
>
实验结果
</el-button>
<el-button
type=
"primary"
:disabled=
"disabled"
v-if=
"experimentInfo?.report_upload_way === 2 && !experimentInfo?.is_commit_report"
>
<router-link
:to=
"`/student/lab/report/${form.experiment_id}`"
target=
"_blank"
>
在线实验报告
</router-link>
</el-button>
<el-button
type=
"primary"
:disabled=
"disabled"
@
click=
"reportDialogVisible = true"
v-if=
"experimentInfo?.report_upload_way === 1 && !submitted"
>
上传实验报告
</el-button
>
<el-button
type=
"primary"
@
click=
"handleReportView"
v-if=
"experimentInfo?.is_commit_report"
>
查看实验报告
</el-button
>
<el-button
type=
"primary"
@
click=
"handleReportExport"
v-if=
"detail?.status === 2 && experimentInfo?.is_commit_report"
>
导出实验报告
</el-button
>
</div>
</el-drawer>
<!-- 上传报告 -->
<ReportDialog
v-model=
"reportDialogVisible"
...
...
@@ -492,23 +518,6 @@ function handleGoToFunction(path: string) {
min-height
:
0
;
flex-direction
:
column
;
}
.mobile-panel-toggle
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
12px
;
margin-bottom
:
12px
;
padding
:
10px
12px
;
background
:
#fff
;
border-radius
:
8px
;
span
{
flex
:
1
;
min-width
:
0
;
color
:
#606266
;
font-size
:
14px
;
line-height
:
20px
;
}
}
.lab-toolbar
{
gap
:
12px
;
}
...
...
@@ -539,24 +548,48 @@ function handleGoToFunction(path: string) {
min-height
:
100%
;
}
@media
(
max-width
:
767px
)
{
.
lab-lef
t
{
:deep
(
.el-form-item
)
{
margin-bottom
:
12px
;
.
student-lab-layou
t
{
height
:
calc
(
100dvh
-
52px
-
24px
);
min-height
:
0
;
}
:deep
(
.el-tabs__header
)
{
overflow-x
:
auto
;
overflow-y
:
hidden
;
scrollbar-width
:
none
;
-ms-overflow-style
:
none
;
.student-lab-layout
:has
(
.drag-panel-left
:not
(
.is-hidden
))
{
height
:
auto
;
:deep
(
.drag-panel-right
)
{
flex
:
none
;
height
:
calc
(
100dvh
-
52px
-
24px
-
56px
);
min-height
:
calc
(
100dvh
-
52px
-
24px
-
56px
);
}
:deep
(
.el-tabs__header
::-webkit-scrollbar
)
{
display
:
none
;
}
:deep
(
.el-tabs__nav-wrap
)
{
:deep
(
.drag-panel-left
:not
(
.is-hidden
))
{
flex-shrink
:
0
;
height
:
auto
;
min-height
:
0
;
max-height
:
none
;
overflow
:
visible
;
border-radius
:
8px
8px
0
0
;
}
:deep
(
.el-tabs__nav
)
{
flex-wrap
:
nowrap
;
:deep
(
.panel-mobile-toggle
)
{
flex-shrink
:
0
;
}
:deep
(
.drag-panel-left-content
)
{
height
:
auto
;
overflow
:
visible
;
}
:deep
(
.drag-panel-right
)
{
flex
:
1
;
height
:
auto
;
min-height
:
0
;
}
.lab-operation-card
{
flex-shrink
:
0
;
:deep
(
.app-card-hd
)
{
display
:
none
;
}
}
.lab-course-picker
{
:deep
(
.el-form-item__label
)
{
height
:
auto
;
line-height
:
24px
;
padding-bottom
:
4px
;
}
:deep
(
.el-form-item__content
)
{
min-width
:
0
;
}
}
.lab-left
{
height
:
auto
;
overflow
:
visible
;
.el-tabs
{
flex
:
none
;
}
:deep
(
.el-tab-pane
)
{
height
:
auto
;
}
:deep
(
.el-form-item
)
{
margin-bottom
:
12px
;
}
:deep
(
.el-tabs__item
)
{
flex-shrink
:
0
;
...
...
@@ -590,15 +623,37 @@ function handleGoToFunction(path: string) {
justify-content
:
center
;
}
.lab-box
{
flex
:
1
;
margin-top
:
12px
;
display
:
block
;
height
:
74vh
;
min-height
:
74vh
;
display
:
flex
;
flex-direction
:
column
;
height
:
auto
;
min-height
:
0
;
overflow
:
hidden
;
}
.iframe
{
height
:
74vh
;
min-height
:
74vh
;
.lab-box
:deep
(
iframe
)
{
display
:
block
;
flex
:
1
;
width
:
100%
;
height
:
100%
;
min-height
:
0
;
}
}
</
style
>
<
style
lang=
"scss"
>
@media
(
max-width
:
767px
)
{
.student-lab-actions
{
border-radius
:
16px
16px
0
0
;
.el-drawer__header
{
padding
:
16px
;
margin-bottom
:
0
;
}
.el-drawer__body
{
padding
:
16px
16px
max
(
16px
,
env
(
safe-area-inset-bottom
));
}
.student-lab-action-list
{
display
:
flex
;
flex-direction
:
column
;
gap
:
12px
;
.el-button
{
width
:
100%
;
margin-left
:
0
;
}
.el-button
a
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
width
:
100%
;
}
}
}
}
</
style
>
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论