Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
S
saas-lab
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
EzijingWeb
saas-lab
Commits
00476186
提交
00476186
authored
7月 06, 2026
作者:
王鹏飞
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
feat: 添加实验复制功能,支持选择复制资源
上级
719a1abe
显示空白字符变更
内嵌
并排
正在显示
3 个修改的文件
包含
315 行增加
和
6 行删除
+315
-6
api.ts
src/modules/admin/lab/experiment/api.ts
+15
-2
CopyDialog.vue
src/modules/admin/lab/experiment/components/CopyDialog.vue
+284
-4
types.ts
src/modules/admin/lab/experiment/types.ts
+16
-0
没有找到文件。
src/modules/admin/lab/experiment/api.ts
浏览文件 @
00476186
import
httpRequest
from
'@/utils/axios'
import
httpRequest
from
'@/utils/axios'
import
type
{
ExperimentCreateItem
}
from
'./types'
import
type
{
ExperimentC
opyResources
,
ExperimentC
reateItem
}
from
'./types'
// 获取实验列表
// 获取实验列表
export
function
getExperimentList
(
params
?:
{
name
?:
string
;
page
?:
number
;
'per-page'
?:
number
})
{
export
function
getExperimentList
(
params
?:
{
name
?:
string
;
page
?:
number
;
'per-page'
?:
number
})
{
...
@@ -205,7 +205,20 @@ export function getScoreExamList(params: { experiment_id: string }) {
...
@@ -205,7 +205,20 @@ export function getScoreExamList(params: { experiment_id: string }) {
}
}
// 复制实验
// 复制实验
export
function
copyExperiment
(
data
:
{
experiment_id
:
string
})
{
export
function
getExperimentCopyResources
(
params
:
{
experiment_id
:
string
})
{
return
httpRequest
.
get
<
ExperimentCopyResources
>
(
'/api/resource/v1/backend/experiment/copy-resources'
,
{
params
})
}
// 复制实验
export
function
copyExperiment
(
data
:
{
experiment_id
:
string
experiment_name
?:
string
sso_id
?:
string
is_copy_class
?:
number
case_ids
?:
string
[]
book_ids
?:
string
[]
video_ids
?:
string
[]
})
{
return
httpRequest
.
post
(
'/api/resource/v1/backend/experiment/copy'
,
data
)
return
httpRequest
.
post
(
'/api/resource/v1/backend/experiment/copy'
,
data
)
}
}
...
...
src/modules/admin/lab/experiment/components/CopyDialog.vue
浏览文件 @
00476186
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
type
{
FormInstance
}
from
'element-plus'
import
type
{
FormInstance
}
from
'element-plus'
import
type
{
ExperimentItem
}
from
'../types'
import
type
{
Experiment
CopyResourceItem
,
ExperimentCopyResources
,
Experiment
Item
}
from
'../types'
import
{
ElMessage
}
from
'element-plus'
import
{
ElMessage
}
from
'element-plus'
import
{
copyExperiment
}
from
'../api'
import
{
copyExperiment
,
getExperimentCopyResources
}
from
'../api'
// import { useGetProjectList } from '@/composables/useGetProjectList'
// import { useGetProjectList } from '@/composables/useGetProjectList'
import
{
useGetTeacherList
}
from
'../composables/useGetTeacherList'
import
{
useGetTeacherList
}
from
'../composables/useGetTeacherList'
import
{
pickBy
}
from
'lodash-es'
import
{
pickBy
}
from
'lodash-es'
...
@@ -16,16 +16,62 @@ const emit = defineEmits<{
...
@@ -16,16 +16,62 @@ const emit = defineEmits<{
const
formRef
=
ref
<
FormInstance
>
()
const
formRef
=
ref
<
FormInstance
>
()
type
CopyResourceType
=
keyof
ExperimentCopyResources
type
CopyResourceField
=
'case_ids'
|
'book_ids'
|
'video_ids'
const
form
=
reactive
({
const
form
=
reactive
({
experiment_name
:
''
,
experiment_name
:
''
,
organ_id
:
''
,
organ_id
:
''
,
sso_id
:
''
,
sso_id
:
''
,
is_copy_class
:
0
is_copy_class
:
0
,
case_ids
:
[]
as
string
[],
book_ids
:
[]
as
string
[],
video_ids
:
[]
as
string
[],
})
const
copyResources
=
ref
<
ExperimentCopyResources
>
({
cases
:
[],
books
:
[],
videos
:
[],
})
})
const
resourceLoading
=
ref
(
false
)
const
resourceSections
=
computed
<
Array
<
{
type
:
CopyResourceType
field
:
CopyResourceField
title
:
string
emptyText
:
string
items
:
ExperimentCopyResourceItem
[]
}
>
>
(()
=>
[
{
type
:
'cases'
,
field
:
'case_ids'
,
title
:
'案例原文'
,
emptyText
:
'暂无可复制案例原文'
,
items
:
copyResources
.
value
.
cases
,
},
{
type
:
'books'
,
field
:
'book_ids'
,
title
:
'案例指导书'
,
emptyText
:
'暂无可复制案例指导书'
,
items
:
copyResources
.
value
.
books
,
},
{
type
:
'videos'
,
field
:
'video_ids'
,
title
:
'案例操作视频'
,
emptyText
:
'暂无可复制案例操作视频'
,
items
:
copyResources
.
value
.
videos
,
},
])
onMounted
(()
=>
{
onMounted
(()
=>
{
form
.
organ_id
=
props
.
data
.
organ_id
form
.
organ_id
=
props
.
data
.
organ_id
form
.
experiment_name
=
props
.
data
.
name
+
'(copy)'
form
.
experiment_name
=
props
.
data
.
name
+
'(copy)'
fetchCopyResources
()
// const [teacher] = props.data.teacher
// const [teacher] = props.data.teacher
// form.sso_id = teacher.sso_id
// form.sso_id = teacher.sso_id
})
})
...
@@ -46,6 +92,65 @@ watch(
...
@@ -46,6 +92,65 @@ watch(
// form.sso_id = ''
// form.sso_id = ''
// }
// }
async
function
fetchCopyResources
()
{
resourceLoading
.
value
=
true
try
{
const
{
data
}
=
await
getExperimentCopyResources
({
experiment_id
:
props
.
data
.
id
})
copyResources
.
value
=
{
cases
:
data
?.
cases
||
[],
books
:
data
?.
books
||
[],
videos
:
data
?.
videos
||
[],
}
form
.
case_ids
=
copyResources
.
value
.
cases
.
map
(
item
=>
item
.
id
)
form
.
book_ids
=
copyResources
.
value
.
books
.
map
(
item
=>
item
.
id
)
form
.
video_ids
=
copyResources
.
value
.
videos
.
map
(
item
=>
item
.
id
)
}
finally
{
resourceLoading
.
value
=
false
}
}
function
getSelectedCount
(
field
:
CopyResourceField
)
{
return
form
[
field
].
length
}
function
handleSelectAll
(
field
:
CopyResourceField
,
items
:
ExperimentCopyResourceItem
[])
{
form
[
field
]
=
items
.
map
(
item
=>
item
.
id
)
}
function
handleClear
(
field
:
CopyResourceField
)
{
form
[
field
]
=
[]
}
function
getResourceMeta
(
item
:
ExperimentCopyResourceItem
,
type
:
CopyResourceType
)
{
if
(
type
===
'videos'
)
{
const
meta
=
[
item
.
length_name
,
item
.
size_name
].
filter
(
Boolean
)
return
meta
.
join
(
' | '
)
}
const
meta
=
[
formatResourceType
(
item
.
type
),
item
.
size_name
].
filter
(
Boolean
)
return
meta
.
join
(
' | '
)
}
function
formatResourceType
(
type
?:
string
)
{
if
(
!
type
)
return
''
const
typeMap
:
Record
<
string
,
string
>
=
{
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
:
'DOCX'
,
'application/msword'
:
'DOC'
,
'application/pdf'
:
'PDF'
,
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
:
'XLSX'
,
'application/vnd.ms-excel'
:
'XLS'
,
'application/vnd.openxmlformats-officedocument.presentationml.presentation'
:
'PPTX'
,
'application/vnd.ms-powerpoint'
:
'PPT'
,
}
if
(
typeMap
[
type
])
{
return
typeMap
[
type
]
}
const
parts
=
type
.
split
(
'/'
)
return
(
parts
[
1
]
||
parts
[
0
]
||
''
).
toUpperCase
()
}
async
function
handleSubmit
()
{
async
function
handleSubmit
()
{
formRef
.
value
?.
validate
().
then
(
async
()
=>
{
formRef
.
value
?.
validate
().
then
(
async
()
=>
{
const
params
=
{
...
pickBy
(
form
,
item
=>
item
!==
''
),
experiment_id
:
props
.
data
.
id
}
const
params
=
{
...
pickBy
(
form
,
item
=>
item
!==
''
),
experiment_id
:
props
.
data
.
id
}
...
@@ -58,7 +163,7 @@ async function handleSubmit() {
...
@@ -58,7 +163,7 @@ async function handleSubmit() {
</
script
>
</
script
>
<
template
>
<
template
>
<el-dialog
title=
"复制实验"
width=
"
600px
"
@
update:modelValue=
"value => $emit('update:modelValue', value)"
>
<el-dialog
title=
"复制实验"
width=
"
820px"
class=
"copy-dialog
"
@
update:modelValue=
"value => $emit('update:modelValue', value)"
>
<el-form
ref=
"formRef"
:model=
"form"
label-width=
"130px"
>
<el-form
ref=
"formRef"
:model=
"form"
label-width=
"130px"
>
<el-form-item
label=
"实验名称"
prop=
"experiment_name"
>
<el-form-item
label=
"实验名称"
prop=
"experiment_name"
>
<el-input
v-model=
"form.experiment_name"
/>
<el-input
v-model=
"form.experiment_name"
/>
...
@@ -82,6 +187,40 @@ async function handleSubmit() {
...
@@ -82,6 +187,40 @@ async function handleSubmit() {
:inactive-value=
"0"
/>
:inactive-value=
"0"
/>
</el-form-item>
</el-form-item>
</el-form>
</el-form>
<div
class=
"copy-resource-panel"
v-loading=
"resourceLoading"
>
<div
class=
"copy-resource-panel__header"
>
<div>
<div
class=
"copy-resource-panel__title"
>
资源选择
</div>
<div
class=
"copy-resource-panel__desc"
>
按资源精确选择复制内容,默认全选
</div>
</div>
</div>
<div
class=
"copy-resource-section"
v-for=
"section in resourceSections"
:key=
"section.type"
>
<div
class=
"copy-resource-section__header"
>
<div
class=
"copy-resource-section__title"
>
<span>
{{
section
.
title
}}
</span>
<span
class=
"copy-resource-section__count"
>
已选
{{
getSelectedCount
(
section
.
field
)
}}
/
{{
section
.
items
.
length
}}
</span>
</div>
<div
class=
"copy-resource-section__actions"
v-if=
"section.items.length"
>
<el-button
link
type=
"primary"
@
click=
"handleSelectAll(section.field, section.items)"
>
全选
</el-button>
<el-button
link
@
click=
"handleClear(section.field)"
>
清空
</el-button>
</div>
</div>
<el-empty
v-if=
"!section.items.length && !resourceLoading"
:description=
"section.emptyText"
:image-size=
"70"
/>
<el-checkbox-group
v-else
v-model=
"form[section.field]"
class=
"copy-resource-list"
>
<el-checkbox
v-for=
"item in section.items"
:key=
"item.id"
:label=
"item.id"
class=
"copy-resource-item"
>
<div
class=
"copy-resource-item__content"
>
<div
class=
"copy-resource-item__name"
:title=
"item.name"
>
{{
item
.
name
}}
</div>
<div
class=
"copy-resource-item__meta"
:title=
"getResourceMeta(item, section.type) || '无附加信息'"
>
{{
getResourceMeta
(
item
,
section
.
type
)
||
'无附加信息'
}}
</div>
</div>
<el-tag
size=
"small"
:type=
"item.status === '1' ? 'success' : 'info'"
>
{{
item
.
status_name
}}
</el-tag>
</el-checkbox>
</el-checkbox-group>
</div>
</div>
<template
#
footer
>
<template
#
footer
>
<el-row
justify=
"center"
>
<el-row
justify=
"center"
>
<el-button
round
auto-insert-space
@
click=
"$emit('update:modelValue', false)"
>
关闭
</el-button>
<el-button
round
auto-insert-space
@
click=
"$emit('update:modelValue', false)"
>
关闭
</el-button>
...
@@ -90,3 +229,144 @@ async function handleSubmit() {
...
@@ -90,3 +229,144 @@ async function handleSubmit() {
</
template
></el-dialog
</
template
></el-dialog
>
>
</template>
</template>
<
style
scoped
lang=
"scss"
>
:deep
(
.copy-dialog
.el-dialog__body
)
{
max-height
:
calc
(
100vh
-
180px
);
overflow
:
hidden
;
}
.copy-resource-panel
{
margin-top
:
8px
;
border-top
:
1px
solid
var
(
--
el-border-color-lighter
);
padding-top
:
20px
;
max-height
:
calc
(
100vh
-
420px
);
overflow-y
:
auto
;
padding-right
:
8px
;
}
.copy-resource-panel__title
{
font-size
:
16px
;
font-weight
:
600
;
color
:
var
(
--
el-text-color-primary
);
}
.copy-resource-panel__desc
{
margin-top
:
4px
;
font-size
:
13px
;
color
:
var
(
--
el-text-color-secondary
);
}
.copy-resource-section
{
margin-top
:
16px
;
padding
:
12px
;
border
:
1px
solid
var
(
--
el-border-color-lighter
);
border-radius
:
12px
;
background
:
#fff
;
}
.copy-resource-section__header
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
16px
;
margin-bottom
:
10px
;
}
.copy-resource-section__title
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
font-size
:
14px
;
font-weight
:
600
;
color
:
var
(
--
el-text-color-primary
);
}
.copy-resource-section__count
{
font-size
:
12px
;
font-weight
:
400
;
color
:
var
(
--
el-text-color-secondary
);
}
.copy-resource-list
{
display
:
grid
;
gap
:
8px
;
max-height
:
220px
;
overflow-y
:
auto
;
padding-right
:
6px
;
}
.copy-resource-item
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
width
:
100%
;
box-sizing
:
border-box
;
margin-right
:
0
;
min-height
:
50px
;
padding
:
6px
12px
;
border
:
1px
solid
var
(
--
el-border-color
);
border-radius
:
10px
;
transition
:
border-color
0
.2s
ease
,
background-color
0
.2s
ease
;
}
.copy-resource-item
:hover
{
border-color
:
var
(
--
el-color-primary-light-5
);
background
:
var
(
--
el-color-primary-light-9
);
}
.copy-resource-item
:deep
(
.el-checkbox__input
)
{
flex-shrink
:
0
;
align-self
:
center
;
}
.copy-resource-item
:deep
(
.el-checkbox__label
)
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
12px
;
flex
:
1
;
min-width
:
0
;
padding-left
:
12px
;
overflow
:
hidden
;
white-space
:
normal
;
line-height
:
1
.2
;
}
.copy-resource-item
:deep
(
.el-tag
)
{
flex-shrink
:
0
;
}
.copy-resource-item__content
{
min-width
:
0
;
}
.copy-resource-item__name
{
font-size
:
13px
;
font-weight
:
600
;
color
:
var
(
--
el-text-color-primary
);
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
line-height
:
1
.2
;
}
.copy-resource-item__meta
{
margin-top
:
1px
;
font-size
:
11px
;
color
:
var
(
--
el-text-color-secondary
);
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
line-height
:
1
.2
;
}
.
copy-resource-list
:
:-
webkit-scrollbar
{
width
:
6px
;
}
.
copy-resource-list
:
:-
webkit-scrollbar-thumb
{
background
:
var
(
--
el-border-color-darker
);
border-radius
:
999px
;
}
</
style
>
src/modules/admin/lab/experiment/types.ts
浏览文件 @
00476186
...
@@ -131,3 +131,19 @@ export interface GroupItem {
...
@@ -131,3 +131,19 @@ export interface GroupItem {
course_name
?:
string
course_name
?:
string
class_name
?:
string
class_name
?:
string
}
}
export
interface
ExperimentCopyResourceItem
{
id
:
string
name
:
string
status
:
string
status_name
:
string
size_name
?:
string
length_name
?:
string
type
?:
string
}
export
interface
ExperimentCopyResources
{
cases
:
ExperimentCopyResourceItem
[]
books
:
ExperimentCopyResourceItem
[]
videos
:
ExperimentCopyResourceItem
[]
}
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论