Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
S
saas-lab
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
EzijingWeb
saas-lab
Commits
3dc107c7
提交
3dc107c7
authored
7月 09, 2026
作者:
王鹏飞
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
feat: 新增案例资源
上级
14fc05ef
隐藏空白字符变更
内嵌
并排
正在显示
17 个修改的文件
包含
806 行增加
和
164 行删除
+806
-164
ezijing.com.key
https/ezijing.com.key
+0
-27
ezijing.com.pem
https/ezijing.com.pem
+0
-114
Preview.vue
src/components/Preview.vue
+13
-1
Index.vue
src/modules/admin/lab/book/views/Index.vue
+1
-1
Index.vue
src/modules/admin/lab/case/views/Index.vue
+1
-1
api.ts
src/modules/admin/lab/experiment/api.ts
+34
-0
ExperimentCaseResource.vue
...dmin/lab/experiment/components/ExperimentCaseResource.vue
+327
-0
ExperimentCaseResourceDialog.vue
...ab/experiment/components/ExperimentCaseResourceDialog.vue
+148
-0
types.ts
src/modules/admin/lab/experiment/types.ts
+44
-0
Index.vue
src/modules/admin/lab/experiment/views/Index.vue
+1
-1
View.vue
src/modules/admin/lab/experiment/views/View.vue
+4
-0
Index.vue
src/modules/admin/lab/video/views/Index.vue
+1
-1
api.ts
src/modules/student/lab/api.ts
+5
-0
CaseResource.vue
src/modules/student/lab/components/CaseResource.vue
+195
-0
types.ts
src/modules/student/lab/types.ts
+20
-0
Index.vue
src/modules/student/lab/views/Index.vue
+5
-0
vite.config.ts
vite.config.ts
+7
-18
没有找到文件。
https/ezijing.com.key
deleted
100644 → 0
浏览文件 @
14fc05ef
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAwZGHX8Zg+EP5uwnEBWkQazq8DMnKz3QfcNRUjFsxsZjM/98r
oYYp+K6/i/b8JXAVMqjC9+X/ZajgbDPLTrgDJ4TQ70ajdusqG3swNqG0qLUogNgU
PcgVRKr2Pk+m79GD/nnxFgeaG6eBfLsb73G2cQUwsgi3bjf6mX6b9bjxjqzwX0PW
es27yyqdS9uQFzV5KLTA2FJJjGGbg54ZLxVc01aVXx2gG0eLnOFcDSmgXUN4hBmN
gYF4FYHhOVXdMl2+yGGp6IvtIIrt+cp2/TQePnxB2QYwrlDMajHrKp0x7LsqWwZT
jepPgGWWDssiSgTCYVaPx28ESbotpzR7D+LlHQIDAQABAoIBAQCCvXGrHcmXRgph
62mrodgGJQioto82aZ9mzPNCTcIVyzgDRtAoa/7jSdx0g+CwxLB8pAH0ADPtjQ/v
5VfXlnAC/DFGu/Zittmc8CqTa8si/CEcTVX2mLLMy5BAa/o8Fs5JTkGo+H99WDkc
byCiLLLmSXQne72xQzwO+rYKVN1K2JUfzch5wpv86IgfJm0gNEOOvtToeNeMbOWj
7AFfpqq7bRMIdZZZhm2n33duSCSnAO7/OuKHY2/RUkc6FvamAth/ME5S2eHrB7yb
KOMZTJH73xnZc3gmsBiIfC1BsqzV/37Ptd7ncTxuK5YZPXIyJ/BjnDTe712LAz+r
f3FEmtJdAoGBAOdPpbT1ylZKinxEIP4sUPUUrrlXqQxYn2xTOCSBz1dB7OpeqiD+
OBa6ur5Sh0+Ot/VRqV2MbXaGee/qqBiziFH23wKoFc6kxeH8RCv247y89yOHqpCP
SM4I7d10RpPh/POU0apGNOP4HOLOMY4V11u68em9hQDeN6oUBWKvDWTPAoGBANY6
mZ/EWKOFJSPfJNNtfXDO27P12ENsa9UvDv7f+FrcPqGkJq1qFBr3ySBjUE7hTv1e
jedRHtBg5Y038vj/MTofEPSaa5POFgzwfG5TPYCcrZkDoLosnuQ92yafMxZPAf16
Nt3vimpxV2Hwf1Nl3IAU/BavngdohWJkO+yOuupTAoGBAMxkTrJmdhJl75T6xoSS
OD83tWTFFvV34fdgWb24jrQ15tj6KS7aNEmxWo4Ocs5T+C1nqVamXZtcHKdpHHwn
OA/lKw435H0ikFCuDrcig/4ko+TbXlY6pkSZE0aaFjtad5/tIo9pVhcTCMNJG/TH
S/Mcb+LwunQX5+v7oP6gXB91AoGAFDbjWBgG5ScjIgdkSi8CrypVwN1p0rLgWfVe
zN4FaU1u+4GpxKNF0mWfLNcyNL893NqXOUSGS9NY1cJ5wtURqBJWXz5S+Fdx7Tak
ByUDoKcoGlAlUmvzckYD79kF0Q0Tev+VPcvkm7amMC+pVA1AiTVxtBJOLxTFwUsD
/IZbTNECgYEA5uwkBWpb/2yYk86cfuUWJjZ0k8jZPrdG4d9gTdD1Fj4abS3FniKM
x9eN8BcnH0j4raRo+MESzYf5hLbfHRG01oRtBQFZXEM0k/zkkRYdKWG1Ulg1+iZR
ZG/lblczYEUW/IOPYZKMX8c5nq7F6huLsioIvsYOdCGjLZbyRHjEPv8=
-----END RSA PRIVATE KEY-----
https/ezijing.com.pem
deleted
100644 → 0
浏览文件 @
14fc05ef
-----BEGIN CERTIFICATE-----
MIIGtDCCBZygAwIBAgIQJtN62MbxMBaLhLadTXD/LzANBgkqhkiG9w0BAQsFADBj
MQswCQYDVQQGEwJDTjE2MDQGA1UECgwtQmVpamluZyBYaW5jaGFjaGEgQ3JlZGl0
IE1hbmFnZW1lbnQgQ28uLCBMdGQuMRwwGgYDVQQDDBNYY2MgVHJ1c3QgT1YgU1NM
IENBMB4XDTIzMDkwNzA1NTM0MVoXDTI0MDkwNjA1NTM0MFowgY4xCzAJBgNVBAYT
AkNOMRIwEAYDVQQIDAnljJfkuqzluIIxEjAQBgNVBAcMCeWMl+S6rOW4gjE/MD0G
A1UECgw25riF5o6n57Sr6I2G77yI5YyX5Lqs77yJ5pWZ6IKy56eR5oqA6IKh5Lu9
5pyJ6ZmQ5YWs5Y+4MRYwFAYDVQQDDA0qLmV6aWppbmcuY29tMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwZGHX8Zg+EP5uwnEBWkQazq8DMnKz3QfcNRU
jFsxsZjM/98roYYp+K6/i/b8JXAVMqjC9+X/ZajgbDPLTrgDJ4TQ70ajdusqG3sw
NqG0qLUogNgUPcgVRKr2Pk+m79GD/nnxFgeaG6eBfLsb73G2cQUwsgi3bjf6mX6b
9bjxjqzwX0PWes27yyqdS9uQFzV5KLTA2FJJjGGbg54ZLxVc01aVXx2gG0eLnOFc
DSmgXUN4hBmNgYF4FYHhOVXdMl2+yGGp6IvtIIrt+cp2/TQePnxB2QYwrlDMajHr
Kp0x7LsqWwZTjepPgGWWDssiSgTCYVaPx28ESbotpzR7D+LlHQIDAQABo4IDNjCC
AzIwDAYDVR0TAQH/BAIwADBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8veGluY2hh
Y2hhMm92LmNybC5jZXJ0dW0ucGwveGluY2hhY2hhMm92LmNybDB5BggrBgEFBQcB
AQRtMGswLwYIKwYBBQUHMAGGI2h0dHA6Ly94aW5jaGFjaGEyb3Yub2NzcC1jZXJ0
dW0uY29tMDgGCCsGAQUFBzAChixodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwv
eGluY2hhY2hhMm92LmNlcjAfBgNVHSMEGDAWgBT6oMvCx12BtSCSByALtjtwOwkO
VTAdBgNVHQ4EFgQUXm2TAFqEePvMkmcMN+bVXSvLTH8wTAYDVR0gBEUwQzAIBgZn
gQwBAgIwNwYMKoRoAYb2dwIFARYCMCcwJQYIKwYBBQUHAgEWGWh0dHBzOi8vd3d3
LmNlcnR1bS5wbC9DUFMwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA4G
A1UdDwEB/wQEAwIFoDAlBgNVHREEHjAcgg0qLmV6aWppbmcuY29tggtlemlqaW5n
LmNvbTCCAXwGCisGAQQB1nkCBAIEggFsBIIBaAFmAHUA7s3QZNXbGs7FXLedtM0T
ojKHRny87N7DUUhZRnEftZsAAAGKbjQU6wAABAMARjBEAiAMbEi/VFFT/mgWiXPw
WgVT5THVJCfr9j1oIcfiB5MFHgIgJ2nAv7xTmfoZfcO8sWoGigLdN8QtGksVUSI2
86Wg3O4AdgDatr9rP7W2Ip+bwrtca+hwkXFsu1GEhTS9pD0wSNf7qwAAAYpuNBUS
AAAEAwBHMEUCIDmQ1/B1s3jRr+0SvDSWEFN9/b4/W8QV5yULASXqQkYqAiEA0wKZ
lklUASXMM8mluAimD5e4qSXu76+148bfZA2d404AdQBIsONr2qZHNA/lagL6nTDr
HFIBy1bdLIHZu7+rOdiEcwAAAYpuNBefAAAEAwBGMEQCICyokQlEyxPTPYNBZzwv
W8AxbKUJl+I5TbJEJsiFECRUAiA8fA86Hof/yLHhMl4amx5AttDNhpDrCHDK1N7m
NYx9XjANBgkqhkiG9w0BAQsFAAOCAQEAq4Q2vW3Xh5TOYk/DofnV2uRiK08P5a5I
NRE5Tet3J5rp2WnNgXoSAOabqWGI6VrTyenTZxrmS/2Aq2A008WOj4Z7T3P9iDS/
MgJkPmc/REqHr15LIDnyRRfmn9+1ShVMwukYHSmZgPewAzSkKdGFN85t3uCEUnXz
kna3ZmSqgJrSWSO9n/3t/kICsVivjqSUuHtnCSbBa+DTSNxIkbkr1t1RJkF4KvSk
AKkvyojoKoeCd+WWEl2MBHY6TK4CzfyLN5qhQub0XqTOh6cG9TwALcLZ4MQeaeOO
ZkGv9DcyeOz7FwwzFa+vAyzhq2XlDPLlpZ4ELALbaqnq3x7HHpzlpw==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEzzCCA7egAwIBAgIRAPJECC2rqQ2ljHLp8pqTQK4wDQYJKoZIhvcNAQELBQAw
fjELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu
QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEiMCAG
A1UEAxMZQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQTAeFw0yMjA3MDEwNzQ4NDda
Fw0yNzA2MzAwNzQ4NDdaMGMxCzAJBgNVBAYTAkNOMTYwNAYDVQQKDC1CZWlqaW5n
IFhpbmNoYWNoYSBDcmVkaXQgTWFuYWdlbWVudCBDby4sIEx0ZC4xHDAaBgNVBAMM
E1hjYyBUcnVzdCBPViBTU0wgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQCz2nxSPEsMeRBdunf+KZ8CrJIj9DUWGVu05WWyES+QkMHzVHCZt8cB2Blp
qzHHqFsXSDHVzcMxDeXBbudmQwYbg5NpSwY9vJHPTl6vlAlK4UWN4+oPTgP9//RN
N6KLmK3kjZ2Zc9F3kFKVaziSwVNjVvneacoUnz6blwLLBc5QelQ8oDT0eEv0QBAY
I0T8pHDgcvWQqTBaRrOb80plDhYju464XbVhtOHvFCiBIkKnfUBnBfptWjGM8Kis
sq4r8YX35B/pioj9g1YX34MLt+5L3vvWXEb2aMiwwB1Z3bSeeiU3N8aMuSb9E9jp
zd0uhzRameU7jAIAr8uuunb7GUJ7AgMBAAGjggFhMIIBXTASBgNVHRMBAf8ECDAG
AQH/AgEAMB0GA1UdDgQWBBT6oMvCx12BtSCSByALtjtwOwkOVTAfBgNVHSMEGDAW
gBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0lBBYw
FAYIKwYBBQUHAwEGCCsGAQUFBwMCMC8GA1UdHwQoMCYwJKAioCCGHmh0dHA6Ly9j
cmwuY2VydHVtLnBsL2N0bmNhLmNybDBrBggrBgEFBQcBAQRfMF0wKAYIKwYBBQUH
MAGGHGh0dHA6Ly9zdWJjYS5vY3NwLWNlcnR1bS5jb20wMQYIKwYBBQUHMAKGJWh0
dHA6Ly9yZXBvc2l0b3J5LmNlcnR1bS5wbC9jdG5jYS5jZXIwOgYDVR0gBDMwMTAv
BgRVHSAAMCcwJQYIKwYBBQUHAgEWGWh0dHBzOi8vd3d3LmNlcnR1bS5wbC9DUFMw
DQYJKoZIhvcNAQELBQADggEBACWzdb59JyYc/WivKUcTEZ+H8WOTuv3AiR6rPljB
QUEB/1kxIeBK+276WLJn0nFNFuT9QMUBo/iUvNvuRQqVjouuYB0Oea6zKQpeMWry
oYtbheW+5NYAbdl9tYE4MRRK9zBLMRrQz35q+XfgMInozidhNHCvuulWk38OMM51
O73z53+R1879nnoZZznoYnrnkISVDjlhe6OSMifvIJAwSYwH2z79Y6CTVgtqTDR9
793Gr7UUmk5ydoxClxnku7voTX8iJUPcuAjElxFrhSQoxm6uXXOfItEqGvLkWAcq
IC5mcyU9VpUg1YP+KkDMh+KQM7/k+2ka1em9hO+QHvJ08UQ=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEtDCCA5ygAwIBAgIRAJOShUABZXFflH8oj+/JmygwDQYJKoZIhvcNAQELBQAw
PjELMAkGA1UEBhMCUEwxGzAZBgNVBAoTElVuaXpldG8gU3AuIHogby5vLjESMBAG
A1UEAxMJQ2VydHVtIENBMB4XDTA4MTAyMjEyMDczN1oXDTI3MDYxMDEwNDYzOVow
fjELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu
QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEiMCAG
A1UEAxMZQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQTCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBAOP7faNyusLwyRSH9WsBTuFuQAe6bSddf/dbLbNax1Ff
q6QypmGHtm4PhtIwApf412lXoRg5XWpkecYBWaw8MUo4fNIE0kso6CBfOweizE1z
2/OuT8dW1Vqnlon686to1COGWSfPCSe8rG5ygxwwct/gounS4XR1Gb0qnnsVVAQb
10M5rVUoxeIau/TA5K44STPMdoWfOUXSpJ7yEoxR+HzkLX/1rF/rFp+xLdG6zJFC
d0wlyZA4b9vwzPuOHpdZPtVgTuYFKO1JeRNLukjbL/ly0znK/h/YNHL1tEDPMQHD
7N4RLRddH7hQ0V4Zp2neBzMoylCV+adUy1SGUEWp+UkCAwEAAaOCAWswggFnMA8G
A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFAh2zcsH/yT2xc3tu5C84oQ3RnX3MFIG
A1UdIwRLMEmhQqRAMD4xCzAJBgNVBAYTAlBMMRswGQYDVQQKExJVbml6ZXRvIFNw
LiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBDQYIDAQAgMA4GA1UdDwEB/wQEAwIB
BjAsBgNVHR8EJTAjMCGgH6AdhhtodHRwOi8vY3JsLmNlcnR1bS5wbC9jYS5jcmww
aAYIKwYBBQUHAQEEXDBaMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1j
ZXJ0dW0uY29tMC4GCCsGAQUFBzAChiJodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0u
cGwvY2EuY2VyMDkGA1UdIAQyMDAwLgYEVR0gADAmMCQGCCsGAQUFBwIBFhhodHRw
Oi8vd3d3LmNlcnR1bS5wbC9DUFMwDQYJKoZIhvcNAQELBQADggEBAI3m/UBmo0yc
p6uh2oTdHDAH5tvHLeyDoVbkHTwmoaUJK+h9Yr6ydZTdCPJ/KEHkgGcCToqPwzXQ
1aknKOrS9KsGhkOujOP5iH3g271CgYACEnWy6BdxqyGVMUZCDYgQOdNv7C9C6kBT
Yr/rynieq6LVLgXqM6vp1peUQl4E7Sztapx6lX0FKgV/CF1mrWHUdqx1lpdzY70a
QVkppV4ig8OLWfqaova9ML9yHRyZhpzyhTwd9yaWLy75ArG1qVDoOPqbCl60BMDO
TjksygtbYvBNWFA0meaaLNKQ1wmB1sCqXs7+0vehukvZ1oaOGR+mBkdCcuBWCgAc
eLmNzJkEN0k=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBM
MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD
QTAeFw0wMjA2MTExMDQ2MzlaFw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBM
MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD
QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6xwS7TT3zNJc4YPk/E
jG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdLkKWo
ePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GI
ULdtlkIJ89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapu
Ob7kky/ZR6By6/qmW6/KUz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUg
AKpoC6EahQGcxEZjgoi2IrHu/qpGWX7PNSzVttpd90gzFFS269lvzs2I1qsb2pY7
HVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA
uI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+GXYkHAQa
TOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTg
xSvgGrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1q
CjqTE5s7FCMTY5w/0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5x
O/fIR/RpbxXyEV6DHpx8Uq79AtoSqFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs
6GAqm4VKQPNriiTsBhYscw==
-----END CERTIFICATE-----
src/components/Preview.vue
浏览文件 @
3dc107c7
...
@@ -7,12 +7,21 @@ interface Props {
...
@@ -7,12 +7,21 @@ interface Props {
const
props
=
defineProps
<
Props
>
()
const
props
=
defineProps
<
Props
>
()
const
isMobile
=
useMediaQuery
(
'(max-width: 767px)'
)
const
isMobile
=
useMediaQuery
(
'(max-width: 767px)'
)
// 文件扩展名
// 文件扩展名
const
cleanUrl
=
$computed
(()
=>
{
return
props
.
url
.
split
(
'?'
)[
0
].
split
(
'#'
)[
0
]
})
const
fileExtensionName
=
$computed
(()
=>
{
const
fileExtensionName
=
$computed
(()
=>
{
return
props
.
url
.
split
(
'.'
).
pop
()
||
''
return
cleanUrl
.
split
(
'.'
).
pop
()?.
toLowerCase
()
||
''
})
})
const
isOfficeFile
=
$computed
(()
=>
{
const
isOfficeFile
=
$computed
(()
=>
{
return
[
'pptx'
,
'doc'
,
'docx'
,
'xls'
,
'xlsx'
].
includes
(
fileExtensionName
)
return
[
'pptx'
,
'doc'
,
'docx'
,
'xls'
,
'xlsx'
].
includes
(
fileExtensionName
)
})
})
const
isVideoFile
=
$computed
(()
=>
{
return
[
'mp4'
,
'mov'
,
'm4v'
,
'webm'
].
includes
(
fileExtensionName
)
})
// office文件
// office文件
const
officeUrl
=
$computed
(()
=>
{
const
officeUrl
=
$computed
(()
=>
{
return
isOfficeFile
return
isOfficeFile
...
@@ -32,6 +41,9 @@ const fileName = $computed(() => {
...
@@ -32,6 +41,9 @@ const fileName = $computed(() => {
<a
:href=
"officeUrl"
target=
"_blank"
rel=
"noopener noreferrer"
class=
"preview-mobile-office__link"
>
打开文档
</a>
<a
:href=
"officeUrl"
target=
"_blank"
rel=
"noopener noreferrer"
class=
"preview-mobile-office__link"
>
打开文档
</a>
</div>
</div>
<iframe
allowfullscreen
:src=
"officeUrl"
:key=
"officeUrl"
frameborder=
"0"
style=
"width: 100%; height: 100%"
v-else-if=
"officeUrl"
></iframe>
<iframe
allowfullscreen
:src=
"officeUrl"
:key=
"officeUrl"
frameborder=
"0"
style=
"width: 100%; height: 100%"
v-else-if=
"officeUrl"
></iframe>
<video
v-else-if=
"isVideoFile"
controls
playsinline
style=
"width: 100%; height: 100%; background: #000"
>
<source
:src=
"url"
/>
</video>
<object
:data=
"url"
style=
"width: 100%; height: 100%; object-fit: none"
v-else
></object>
<object
:data=
"url"
style=
"width: 100%; height: 100%; object-fit: none"
v-else
></object>
</
template
>
</
template
>
...
...
src/modules/admin/lab/book/views/Index.vue
浏览文件 @
3dc107c7
...
@@ -34,7 +34,7 @@ const listOptions = $computed(() => {
...
@@ -34,7 +34,7 @@ const listOptions = $computed(() => {
return
{
return
{
remote
:
{
remote
:
{
httpRequest
:
getBookList
,
httpRequest
:
getBookList
,
params
:
{
name
:
''
,
experiment_id
:
route
.
query
.
experiment_id
||
''
,
status
:
''
},
params
:
{
name
:
''
,
experiment_id
:
route
.
query
.
experiment_id
||
''
,
status
:
'
1
'
},
},
},
filters
:
[
filters
:
[
{
type
:
'input'
,
prop
:
'name'
,
label
:
'实验指导书名称'
,
placeholder
:
'请输入实验指导书名称'
},
{
type
:
'input'
,
prop
:
'name'
,
label
:
'实验指导书名称'
,
placeholder
:
'请输入实验指导书名称'
},
...
...
src/modules/admin/lab/case/views/Index.vue
浏览文件 @
3dc107c7
...
@@ -52,7 +52,7 @@ const listOptions = $computed(() => {
...
@@ -52,7 +52,7 @@ const listOptions = $computed(() => {
return
{
return
{
remote
:
{
remote
:
{
httpRequest
:
getCaseList
,
httpRequest
:
getCaseList
,
params
:
{
name
:
''
,
experiment_id
:
route
.
query
.
experiment_id
||
''
,
status
:
''
},
params
:
{
name
:
''
,
experiment_id
:
route
.
query
.
experiment_id
||
''
,
status
:
'
1
'
},
callback
(
data
:
{
total
:
number
;
list
:
CaseItem
[]
})
{
callback
(
data
:
{
total
:
number
;
list
:
CaseItem
[]
})
{
const
{
list
=
[],
total
=
0
}
=
data
||
{}
const
{
list
=
[],
total
=
0
}
=
data
||
{}
const
dataList
=
list
.
map
((
item
)
=>
{
const
dataList
=
list
.
map
((
item
)
=>
{
...
...
src/modules/admin/lab/experiment/api.ts
浏览文件 @
3dc107c7
...
@@ -209,6 +209,40 @@ export function getExperimentCopyResources(params: { experiment_id: string }) {
...
@@ -209,6 +209,40 @@ export function getExperimentCopyResources(params: { experiment_id: string }) {
return
httpRequest
.
get
<
ExperimentCopyResources
>
(
'/api/resource/v1/backend/experiment/copy-resources'
,
{
params
})
return
httpRequest
.
get
<
ExperimentCopyResources
>
(
'/api/resource/v1/backend/experiment/copy-resources'
,
{
params
})
}
}
// 获取实验关联案例资源列表
export
function
getExperimentCaseResourceList
(
params
:
{
experiment_id
:
string
})
{
return
httpRequest
.
get
(
'/api/lab/v1/teacher/experiment-case/list'
,
{
params
})
}
// 搜索可绑定案例资源
export
function
searchExperimentCaseResource
(
params
:
{
experiment_id
?:
string
name
?:
string
authorized
?:
string
})
{
return
httpRequest
.
get
(
'/api/lab/v1/teacher/experiment-case/search-case'
,
{
params
})
}
// 关联案例资源
export
function
bindExperimentCaseResource
(
data
:
{
experiment_id
:
string
;
case_ids
:
string
})
{
return
httpRequest
.
post
(
'/api/lab/v1/teacher/experiment-case/bind'
,
data
)
}
// 解绑案例资源
export
function
unbindExperimentCaseResource
(
data
:
{
experiment_case_id
:
string
})
{
return
httpRequest
.
post
(
'/api/lab/v1/teacher/experiment-case/unbind'
,
data
)
}
// 保存案例资源文件可见性
export
function
saveExperimentCaseResourceVisibility
(
data
:
{
experiment_case_id
:
string
files
:
Array
<
{
file_id
:
string
;
is_visible
:
string
;
sort
:
number
}
>
})
{
return
httpRequest
.
post
(
'/api/lab/v1/teacher/experiment-case/file-visibility'
,
data
,
{
headers
:
{
'Content-Type'
:
'application/json'
},
})
}
// 复制实验
// 复制实验
export
function
copyExperiment
(
data
:
{
export
function
copyExperiment
(
data
:
{
experiment_id
:
string
experiment_id
:
string
...
...
src/modules/admin/lab/experiment/components/ExperimentCaseResource.vue
0 → 100644
浏览文件 @
3dc107c7
<
script
setup
lang=
"ts"
>
import
type
{
ExperimentCaseResourceFileItem
,
ExperimentCaseResourceItem
}
from
'../types'
import
{
CirclePlus
,
Document
}
from
'@element-plus/icons-vue'
import
{
ElMessage
,
ElMessageBox
}
from
'element-plus'
import
{
getExperimentCaseResourceList
,
saveExperimentCaseResourceVisibility
,
unbindExperimentCaseResource
,
}
from
'../api'
import
Preview
from
'@/components/Preview.vue'
const
BindDialog
=
defineAsyncComponent
(()
=>
import
(
'./ExperimentCaseResourceDialog.vue'
))
interface
Props
{
experimentId
:
string
}
const
props
=
defineProps
<
Props
>
()
const
loading
=
ref
(
false
)
const
dialogVisible
=
ref
(
false
)
const
list
=
ref
<
ExperimentCaseResourceItem
[]
>
([])
const
previewVisible
=
ref
(
false
)
const
currentFile
=
ref
<
ExperimentCaseResourceFileItem
|
null
>
(
null
)
function
getFileGroups
(
files
:
ExperimentCaseResourceFileItem
[])
{
const
groups
:
Record
<
string
,
ExperimentCaseResourceFileItem
[]
>
=
{}
files
.
forEach
((
file
)
=>
{
const
key
=
file
.
type_name
||
'其他'
if
(
!
groups
[
key
])
{
groups
[
key
]
=
[]
}
groups
[
key
].
push
(
file
)
})
return
Object
.
entries
(
groups
).
map
(([
title
,
items
])
=>
({
title
,
items
}))
}
function
handleView
(
file
:
ExperimentCaseResourceFileItem
)
{
currentFile
.
value
=
file
previewVisible
.
value
=
true
}
async
function
fetchList
()
{
if
(
!
props
.
experimentId
)
return
loading
.
value
=
true
try
{
const
{
data
}
=
await
getExperimentCaseResourceList
({
experiment_id
:
props
.
experimentId
})
list
.
value
=
data
?.
list
||
[]
}
finally
{
loading
.
value
=
false
}
}
async
function
handleSave
(
item
:
ExperimentCaseResourceItem
)
{
await
saveExperimentCaseResourceVisibility
({
experiment_case_id
:
item
.
experiment_case_id
,
files
:
item
.
files
.
map
((
file
,
index
)
=>
({
file_id
:
file
.
file_id
,
is_visible
:
file
.
is_visible
,
sort
:
index
+
1
,
})),
})
ElMessage
.
success
(
'保存成功'
)
fetchList
()
}
function
handleVisibilityChange
(
item
:
ExperimentCaseResourceItem
)
{
handleSave
(
item
)
}
async
function
handleUnbind
(
item
:
ExperimentCaseResourceItem
)
{
await
ElMessageBox
.
confirm
(
`确定移除案例“
${
item
.
name
}
”吗?`
,
'提示'
,
{
type
:
'warning'
})
await
unbindExperimentCaseResource
({
experiment_case_id
:
item
.
experiment_case_id
})
ElMessage
.
success
(
'移除成功'
)
fetchList
()
}
watch
(
()
=>
props
.
experimentId
,
()
=>
{
fetchList
()
},
{
immediate
:
true
},
)
</
script
>
<
template
>
<div
class=
"case-resource-panel"
v-loading=
"loading"
>
<div
class=
"case-resource-panel__toolbar"
>
<el-button
type=
"primary"
:icon=
"CirclePlus"
@
click=
"dialogVisible = true"
>
关联案例
</el-button>
</div>
<el-empty
v-if=
"!loading && !list.length"
description=
"暂未绑定案例资源"
:image-size=
"80"
/>
<div
v-else
class=
"case-resource-list"
>
<div
class=
"case-resource-card"
v-for=
"item in list"
:key=
"item.experiment_case_id"
>
<div
class=
"case-resource-card__header"
>
<div
class=
"case-resource-card__summary"
>
<div
class=
"case-resource-card__title-row"
>
<div
class=
"case-resource-card__title"
>
{{
item
.
name
}}
</div>
<el-tag
size=
"small"
:type=
"item.is_available === '1' ? 'success' : 'danger'"
>
{{
item
.
is_available
===
'1'
?
'有效'
:
'已失效'
}}
</el-tag>
</div>
<div
class=
"case-resource-card__description"
v-if=
"item.description"
>
{{
item
.
description
}}
</div>
<div
class=
"case-resource-card__meta"
>
<span>
分类:
{{
item
.
classification_name
||
'暂无'
}}
</span>
<span>
部门:
{{
item
.
organ_id_name
||
'暂无'
}}
</span>
<span>
创建人:
{{
item
.
created_operator_name
||
'暂无'
}}
</span>
</div>
<div
class=
"case-resource-card__invalid"
v-if=
"item.invalid_reason"
>
{{
item
.
invalid_reason
}}
</div>
</div>
<div
class=
"case-resource-card__actions"
>
<el-button
type=
"danger"
plain
@
click=
"handleUnbind(item)"
>
移除
</el-button>
</div>
</div>
<div
class=
"case-resource-groups"
>
<div
class=
"case-resource-group"
v-for=
"group in getFileGroups(item.files)"
:key=
"group.title"
>
<div
class=
"case-resource-group__title"
>
{{
group
.
title
}}
</div>
<div
class=
"case-resource-files"
>
<div
class=
"case-resource-file"
v-for=
"file in group.items"
:key=
"file.file_id"
>
<div
class=
"case-resource-file__main"
>
<div
class=
"case-resource-file__icon"
>
<el-icon><Document
/></el-icon>
</div>
<div
class=
"case-resource-file__content"
>
<div
class=
"case-resource-file__name"
>
{{
file
.
name
}}
</div>
</div>
</div>
<div
class=
"case-resource-file__tools"
>
<el-switch
v-model=
"file.is_visible"
active-value=
"1"
inactive-value=
"0"
@
change=
"handleVisibilityChange(item)"
/>
<el-button
link
type=
"primary"
@
click=
"handleView(file)"
>
查看
</el-button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<el-dialog
v-model=
"previewVisible"
:title=
"currentFile?.name || '文件预览'"
width=
"80%"
destroy-on-close
append-to-body
>
<div
class=
"case-resource-preview"
v-if=
"currentFile"
>
<Preview
:url=
"currentFile.url"
></Preview>
</div>
</el-dialog>
<BindDialog
v-model=
"dialogVisible"
:experiment-id=
"experimentId"
@
update=
"fetchList"
v-if=
"dialogVisible"
></BindDialog>
</
template
>
<
style
scoped
lang=
"scss"
>
.case-resource-panel__toolbar
{
display
:
flex
;
justify-content
:
flex-start
;
margin-bottom
:
16px
;
}
.case-resource-list
{
display
:
grid
;
gap
:
12px
;
}
.case-resource-card
{
border
:
1px
solid
var
(
--
el-border-color
);
border-radius
:
12px
;
background
:
#fff
;
padding
:
14px
;
}
.case-resource-card__header
{
display
:
flex
;
justify-content
:
space-between
;
gap
:
16px
;
margin-bottom
:
14px
;
}
.case-resource-card__summary
{
flex
:
1
;
min-width
:
0
;
}
.case-resource-card__title-row
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
}
.case-resource-card__title
{
font-size
:
16px
;
font-weight
:
600
;
color
:
#303133
;
}
.case-resource-card__description
{
margin-top
:
6px
;
color
:
#606266
;
line-height
:
1
.5
;
display
:
-
webkit-box
;
-webkit-line-clamp
:
2
;
-webkit-box-orient
:
vertical
;
overflow
:
hidden
;
}
.case-resource-card__meta
{
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
12px
;
margin-top
:
8px
;
font-size
:
12px
;
color
:
#909399
;
}
.case-resource-card__invalid
{
margin-top
:
6px
;
font-size
:
12px
;
color
:
#f56c6c
;
}
.case-resource-card__actions
{
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
10px
;
flex-shrink
:
0
;
}
.case-resource-groups
{
display
:
grid
;
gap
:
10px
;
}
.case-resource-group__title
{
margin-bottom
:
8px
;
font-size
:
13px
;
font-weight
:
600
;
color
:
#303133
;
}
.case-resource-files
{
display
:
grid
;
gap
:
8px
;
}
.case-resource-file
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
12px
;
padding
:
10px
12px
;
border
:
1px
solid
var
(
--
el-border-color-lighter
);
border-radius
:
8px
;
}
.case-resource-file__main
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
min-width
:
0
;
flex
:
1
;
}
.case-resource-file__icon
{
width
:
30px
;
height
:
30px
;
border-radius
:
8px
;
background
:
#f5f7fa
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
color
:
#606266
;
flex-shrink
:
0
;
}
.case-resource-file__content
{
min-width
:
0
;
}
.case-resource-file__name
{
font-size
:
13px
;
color
:
#303133
;
word-break
:
break-all
;
}
.case-resource-file__tools
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
flex-shrink
:
0
;
}
.case-resource-preview
{
height
:
70vh
;
}
@media
(
max-width
:
767px
)
{
.case-resource-card__header
{
flex-direction
:
column
;
}
.case-resource-card__actions
{
justify-content
:
flex-start
;
}
.case-resource-file
{
flex-direction
:
column
;
align-items
:
stretch
;
}
.case-resource-file__tools
{
justify-content
:
space-between
;
}
}
</
style
>
src/modules/admin/lab/experiment/components/ExperimentCaseResourceDialog.vue
0 → 100644
浏览文件 @
3dc107c7
<
script
setup
lang=
"ts"
>
import
type
{
ExperimentCaseResourceSearchItem
}
from
'../types'
import
{
ElMessage
}
from
'element-plus'
import
{
bindExperimentCaseResource
,
searchExperimentCaseResource
}
from
'../api'
const
props
=
defineProps
<
{
experimentId
:
string
}
>
()
const
emit
=
defineEmits
<
{
(
e
:
'update'
):
void
(
e
:
'update:modelValue'
,
visible
:
boolean
):
void
}
>
()
const
loading
=
ref
(
false
)
const
list
=
ref
<
ExperimentCaseResourceSearchItem
[]
>
([])
const
selectedIds
=
ref
<
string
[]
>
([])
const
form
=
reactive
({
name
:
''
,
})
async
function
fetchList
()
{
loading
.
value
=
true
try
{
const
{
data
}
=
await
searchExperimentCaseResource
({
experiment_id
:
props
.
experimentId
,
name
:
form
.
name
||
undefined
,
})
list
.
value
=
data
?.
list
||
[]
selectedIds
.
value
=
selectedIds
.
value
.
filter
((
id
)
=>
list
.
value
.
some
((
item
)
=>
item
.
id
===
id
))
}
finally
{
loading
.
value
=
false
}
}
function
handleSelect
(
row
:
ExperimentCaseResourceSearchItem
)
{
if
(
selectedIds
.
value
.
includes
(
row
.
id
))
{
selectedIds
.
value
=
selectedIds
.
value
.
filter
((
id
)
=>
id
!==
row
.
id
)
return
}
selectedIds
.
value
=
[...
selectedIds
.
value
,
row
.
id
]
}
async
function
handleSubmit
()
{
if
(
!
selectedIds
.
value
.
length
)
{
ElMessage
.
warning
(
'请选择要关联的案例'
)
return
}
await
bindExperimentCaseResource
({
experiment_id
:
props
.
experimentId
,
case_ids
:
selectedIds
.
value
.
join
(
','
),
})
ElMessage
.
success
(
`成功关联
${
selectedIds
.
value
.
length
}
个案例`
)
emit
(
'update'
)
emit
(
'update:modelValue'
,
false
)
}
onMounted
(()
=>
{
fetchList
()
})
</
script
>
<
template
>
<el-dialog
title=
"关联案例"
width=
"900px"
:close-on-click-modal=
"false"
@
update:modelValue=
"(value) => $emit('update:modelValue', value)"
>
<div
class=
"dialog-filter"
>
<el-input
v-model=
"form.name"
clearable
placeholder=
"请输入案例名称"
@
keyup
.
enter=
"fetchList"
/>
<el-button
class=
"dialog-filter__search-btn"
type=
"primary"
@
click=
"fetchList"
>
搜索
</el-button>
</div>
<el-table
v-if=
"loading || list.length"
:data=
"list"
v-loading=
"loading"
row-key=
"id"
@
row-click=
"handleSelect"
height=
"460px"
>
<el-table-column
label=
""
width=
"54"
align=
"center"
>
<template
#
default=
"
{ row }">
<span
class=
"row-selector"
:class=
"
{ 'is-active': selectedIds.includes(row.id) }">
</span>
</
template
>
</el-table-column>
<el-table-column
label=
"案例名称"
prop=
"name"
min-width=
"260"
/>
<el-table-column
label=
"文件数"
prop=
"file_count"
width=
"90"
align=
"center"
/>
<el-table-column
label=
"状态"
prop=
"status_name"
width=
"100"
align=
"center"
/>
<el-table-column
label=
"创建人"
prop=
"created_operator_name"
width=
"120"
align=
"center"
/>
<el-table-column
label=
"所属部门"
prop=
"organ_id_name"
min-width=
"150"
/>
</el-table>
<el-empty
v-if=
"!loading && !list.length"
description=
"暂无可关联案例"
:image-size=
"72"
/>
<
template
#
footer
>
<el-row
justify=
"center"
>
<el-button
round
auto-insert-space
@
click=
"$emit('update:modelValue', false)"
>
取消
</el-button>
<el-button
type=
"primary"
round
auto-insert-space
@
click=
"handleSubmit"
>
确认关联
</el-button>
</el-row>
</
template
>
</el-dialog>
</template>
<
style
scoped
lang=
"scss"
>
.dialog-filter
{
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
12px
;
margin-bottom
:
16px
;
>
*
{
flex
:
1
1
220px
;
}
}
.dialog-filter__search-btn
{
flex
:
0
0
88px
;
}
.row-selector
{
display
:
inline-flex
;
width
:
16px
;
height
:
16px
;
border
:
2px
solid
#c0c4cc
;
border-radius
:
4px
;
position
:
relative
;
}
.row-selector.is-active
{
border-color
:
var
(
--
el-color-primary
);
}
.
row-selector
.
is-active
:
:
after
{
content
:
''
;
position
:
absolute
;
inset
:
2px
;
border-radius
:
2px
;
background
:
var
(
--
el-color-primary
);
}
@media
(
max-width
:
767px
)
{
.dialog-filter
{
>
*
{
flex-basis
:
100%
;
}
}
}
</
style
>
src/modules/admin/lab/experiment/types.ts
浏览文件 @
3dc107c7
...
@@ -147,3 +147,47 @@ export interface ExperimentCopyResources {
...
@@ -147,3 +147,47 @@ export interface ExperimentCopyResources {
books
:
ExperimentCopyResourceItem
[]
books
:
ExperimentCopyResourceItem
[]
videos
:
ExperimentCopyResourceItem
[]
videos
:
ExperimentCopyResourceItem
[]
}
}
export
interface
ExperimentCaseResourceFileItem
{
id
:
string
file_id
:
string
name
:
string
url
:
string
size
:
string
size_name
:
string
mime_type
:
string
type
:
string
type_name
:
string
is_visible
:
string
sort
:
string
}
export
interface
ExperimentCaseResourceItem
{
experiment_case_id
:
string
case_id
:
string
name
:
string
description
:
string
classification_name
:
string
organ_id_name
:
string
created_operator_name
:
string
belong_operator_name
:
string
status
:
string
status_name
:
string
is_available
:
string
invalid_reason
:
string
department_public
:
string
platform_public
:
string
files
:
ExperimentCaseResourceFileItem
[]
}
export
interface
ExperimentCaseResourceSearchItem
{
id
:
string
name
:
string
classification_name
:
string
organ_id_name
:
string
created_operator_name
:
string
status
:
string
status_name
:
string
file_count
:
string
bind_count
:
string
}
src/modules/admin/lab/experiment/views/Index.vue
浏览文件 @
3dc107c7
...
@@ -27,7 +27,7 @@ const params = reactive({
...
@@ -27,7 +27,7 @@ const params = reactive({
id
:
''
,
id
:
''
,
course_id
:
''
,
course_id
:
''
,
name
:
''
,
name
:
''
,
status
:
''
,
status
:
'
1
'
,
})
})
// watch(
// watch(
// () => params.course_id,
// () => params.course_id,
...
...
src/modules/admin/lab/experiment/views/View.vue
浏览文件 @
3dc107c7
...
@@ -14,6 +14,7 @@ const StudentListDialog = defineAsyncComponent(() => import('../components/Stude
...
@@ -14,6 +14,7 @@ const StudentListDialog = defineAsyncComponent(() => import('../components/Stude
const
ViewGradeRules
=
defineAsyncComponent
(()
=>
import
(
'../components/ViewGradeRules.vue'
))
const
ViewGradeRules
=
defineAsyncComponent
(()
=>
import
(
'../components/ViewGradeRules.vue'
))
const
ViewReportRules
=
defineAsyncComponent
(()
=>
import
(
'../components/ViewReportRules.vue'
))
const
ViewReportRules
=
defineAsyncComponent
(()
=>
import
(
'../components/ViewReportRules.vue'
))
const
ViewExam
=
defineAsyncComponent
(()
=>
import
(
'../components/ViewExam.vue'
))
const
ViewExam
=
defineAsyncComponent
(()
=>
import
(
'../components/ViewExam.vue'
))
const
ExperimentCaseResource
=
defineAsyncComponent
(()
=>
import
(
'../components/ExperimentCaseResource.vue'
))
const
CopyDialog
=
defineAsyncComponent
(()
=>
import
(
'../components/CopyDialog.vue'
))
const
CopyDialog
=
defineAsyncComponent
(()
=>
import
(
'../components/CopyDialog.vue'
))
const
DMLFormDialog
=
defineAsyncComponent
(()
=>
import
(
'../components/DMLFormDialog.vue'
))
const
DMLFormDialog
=
defineAsyncComponent
(()
=>
import
(
'../components/DMLFormDialog.vue'
))
const
GradeRulesDialog
=
defineAsyncComponent
(()
=>
import
(
'../components/GradeRulesDialog.vue'
))
const
GradeRulesDialog
=
defineAsyncComponent
(()
=>
import
(
'../components/GradeRulesDialog.vue'
))
...
@@ -207,6 +208,9 @@ function handleUpdateGradeRules() {
...
@@ -207,6 +208,9 @@ function handleUpdateGradeRules() {
<AppCard
:title=
"detail?.exam_status === '0' ? '训练考试' : '理论考试'"
>
<AppCard
:title=
"detail?.exam_status === '0' ? '训练考试' : '理论考试'"
>
<ViewExam
v-if=
"detail"
:id=
"id"
:type=
"detail?.exam_status === '0' ? '2' : '1'"
></ViewExam>
<ViewExam
v-if=
"detail"
:id=
"id"
:type=
"detail?.exam_status === '0' ? '2' : '1'"
></ViewExam>
</AppCard>
</AppCard>
<AppCard
title=
"案例资源"
v-permission=
"'experiment-case'"
>
<ExperimentCaseResource
:experiment-id=
"id"
></ExperimentCaseResource>
</AppCard>
<!-- 关联班级 -->
<!-- 关联班级 -->
<SelectClassDialog
v-model=
"selectClassVisible"
@
update=
"handleRefetch"
v-if=
"selectClassVisible"
></SelectClassDialog>
<SelectClassDialog
v-model=
"selectClassVisible"
@
update=
"handleRefetch"
v-if=
"selectClassVisible"
></SelectClassDialog>
<!-- 学生分组 -->
<!-- 学生分组 -->
...
...
src/modules/admin/lab/video/views/Index.vue
浏览文件 @
3dc107c7
...
@@ -34,7 +34,7 @@ const listOptions = $computed(() => {
...
@@ -34,7 +34,7 @@ const listOptions = $computed(() => {
return
{
return
{
remote
:
{
remote
:
{
httpRequest
:
getVideoList
,
httpRequest
:
getVideoList
,
params
:
{
name
:
''
,
experiment_id
:
route
.
query
.
experiment_id
||
''
,
status
:
''
},
params
:
{
name
:
''
,
experiment_id
:
route
.
query
.
experiment_id
||
''
,
status
:
'
1
'
},
},
},
filters
:
[
filters
:
[
{
type
:
'input'
,
prop
:
'name'
,
label
:
'实验操作视频名称'
,
placeholder
:
'请输入实验操作视频名称'
},
{
type
:
'input'
,
prop
:
'name'
,
label
:
'实验操作视频名称'
,
placeholder
:
'请输入实验操作视频名称'
},
...
...
src/modules/student/lab/api.ts
浏览文件 @
3dc107c7
...
@@ -15,6 +15,11 @@ export function getExperimentCase(params: { experiment_id: string }) {
...
@@ -15,6 +15,11 @@ export function getExperimentCase(params: { experiment_id: string }) {
return
httpRequest
.
get
(
'/api/lab/v1/student/experiment-cases/detail'
,
{
params
})
return
httpRequest
.
get
(
'/api/lab/v1/student/experiment-cases/detail'
,
{
params
})
}
}
// 获取实验案例资源
export
function
getExperimentCaseResourceList
(
params
:
{
experiment_id
:
string
})
{
return
httpRequest
.
get
(
'/api/lab/v1/student/experiment-case/detail'
,
{
params
})
}
// 获取实验指导书
// 获取实验指导书
export
function
getExperimentBookList
(
params
:
{
experiment_id
:
string
})
{
export
function
getExperimentBookList
(
params
:
{
experiment_id
:
string
})
{
return
httpRequest
.
get
(
'/api/lab/v1/student/experiment-book/list'
,
{
params
})
return
httpRequest
.
get
(
'/api/lab/v1/student/experiment-book/list'
,
{
params
})
...
...
src/modules/student/lab/components/CaseResource.vue
0 → 100644
浏览文件 @
3dc107c7
<
script
setup
lang=
"ts"
>
import
type
{
ExperimentCaseResourceFileType
,
ExperimentCaseResourceType
}
from
'../types'
import
{
Document
}
from
'@element-plus/icons-vue'
import
{
getExperimentCaseResourceList
}
from
'../api'
import
Preview
from
'@/components/Preview.vue'
interface
Props
{
experiment_id
?:
string
}
const
props
=
defineProps
<
Props
>
()
const
emits
=
defineEmits
([
'empty'
])
const
loading
=
ref
(
false
)
const
list
=
ref
<
ExperimentCaseResourceType
[]
>
([])
const
previewVisible
=
ref
(
false
)
const
currentFile
=
ref
<
ExperimentCaseResourceFileType
|
null
>
(
null
)
function
getFileGroups
(
files
:
ExperimentCaseResourceFileType
[])
{
const
groups
:
Record
<
string
,
ExperimentCaseResourceFileType
[]
>
=
{}
files
.
forEach
((
file
)
=>
{
const
key
=
file
.
type_name
||
'其他资料'
if
(
!
groups
[
key
])
{
groups
[
key
]
=
[]
}
groups
[
key
].
push
(
file
)
})
return
Object
.
entries
(
groups
).
map
(([
title
,
items
])
=>
({
title
,
items
}))
}
function
handleView
(
file
:
ExperimentCaseResourceFileType
)
{
currentFile
.
value
=
file
previewVisible
.
value
=
true
}
async
function
fetchInfo
()
{
if
(
!
props
.
experiment_id
)
return
loading
.
value
=
true
try
{
const
{
data
}
=
await
getExperimentCaseResourceList
({
experiment_id
:
props
.
experiment_id
})
list
.
value
=
data
?.
list
||
[]
if
(
!
list
.
value
.
length
)
{
emits
(
'empty'
)
}
}
finally
{
loading
.
value
=
false
}
}
watch
(
()
=>
props
.
experiment_id
,
()
=>
{
fetchInfo
()
},
{
immediate
:
true
},
)
</
script
>
<
template
>
<div
class=
"case-resource-wrap"
v-loading=
"loading"
>
<div
class=
"case-resource-list"
v-if=
"list.length"
>
<div
class=
"case-resource-card"
v-for=
"item in list"
:key=
"item.experiment_case_id"
>
<div
class=
"case-resource-card__header"
>
<div
class=
"case-resource-card__title"
>
{{
item
.
name
}}
</div>
<div
class=
"case-resource-card__description"
v-if=
"item.description"
>
{{
item
.
description
}}
</div>
</div>
<div
class=
"case-resource-group"
v-for=
"group in getFileGroups(item.files)"
:key=
"group.title"
>
<div
class=
"case-resource-group__title"
>
{{
group
.
title
}}
</div>
<div
class=
"case-resource-files"
>
<div
class=
"case-resource-file"
v-for=
"file in group.items"
:key=
"file.file_id"
>
<div
class=
"case-resource-file__main"
>
<div
class=
"case-resource-file__icon"
>
<el-icon><Document
/></el-icon>
</div>
<div
class=
"case-resource-file__content"
>
<div
class=
"case-resource-file__name"
>
{{
file
.
name
}}
</div>
</div>
</div>
<el-button
link
type=
"primary"
@
click=
"handleView(file)"
>
查看
</el-button>
</div>
</div>
</div>
</div>
</div>
<el-empty
description=
"暂无数据"
v-else-if=
"!loading"
/>
<el-dialog
v-model=
"previewVisible"
:title=
"currentFile?.name || '文件预览'"
width=
"80%"
destroy-on-close
append-to-body
>
<div
class=
"case-resource-preview"
v-if=
"currentFile"
>
<Preview
:url=
"currentFile.url"
></Preview>
</div>
</el-dialog>
</div>
</
template
>
<
style
lang=
"scss"
scoped
>
.case-resource-list
{
display
:
grid
;
gap
:
12px
;
}
.case-resource-card
{
border
:
1px
solid
#ebeef5
;
border-radius
:
12px
;
background
:
#fff
;
padding
:
14px
;
}
.case-resource-card__header
{
margin-bottom
:
12px
;
}
.case-resource-card__title
{
font-size
:
16px
;
font-weight
:
600
;
color
:
#303133
;
}
.case-resource-card__description
{
margin-top
:
6px
;
color
:
#606266
;
line-height
:
1
.5
;
display
:
-
webkit-box
;
-webkit-line-clamp
:
2
;
-webkit-box-orient
:
vertical
;
overflow
:
hidden
;
}
.case-resource-group
+
.case-resource-group
{
margin-top
:
10px
;
}
.case-resource-group__title
{
margin-bottom
:
8px
;
font-size
:
13px
;
font-weight
:
600
;
color
:
#303133
;
}
.case-resource-files
{
display
:
grid
;
gap
:
8px
;
}
.case-resource-file
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
12px
;
padding
:
10px
12px
;
border
:
1px
solid
#ebeef5
;
border-radius
:
8px
;
}
.case-resource-file__main
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
flex
:
1
;
min-width
:
0
;
}
.case-resource-file__icon
{
width
:
30px
;
height
:
30px
;
border-radius
:
8px
;
background
:
#f5f7fa
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
flex-shrink
:
0
;
color
:
#606266
;
}
.case-resource-file__content
{
min-width
:
0
;
}
.case-resource-file__name
{
font-size
:
13px
;
color
:
#303133
;
word-break
:
break-all
;
}
.case-resource-preview
{
height
:
70vh
;
}
@media
(
max-width
:
767px
)
{
.case-resource-file
{
flex-direction
:
column
;
align-items
:
stretch
;
}
}
</
style
>
src/modules/student/lab/types.ts
浏览文件 @
3dc107c7
...
@@ -30,6 +30,26 @@ export interface ExperimentVideoType {
...
@@ -30,6 +30,26 @@ export interface ExperimentVideoType {
cover
:
string
cover
:
string
}
}
export
interface
ExperimentCaseResourceFileType
{
id
:
string
file_id
:
string
name
:
string
url
:
string
size
:
string
size_name
:
string
mime_type
:
string
type
:
string
type_name
:
string
}
export
interface
ExperimentCaseResourceType
{
id
:
string
experiment_case_id
:
string
name
:
string
description
:
string
files
:
ExperimentCaseResourceFileType
[]
}
export
interface
PlayInfo
{
export
interface
PlayInfo
{
BitDepth
:
number
BitDepth
:
number
Bitrate
:
string
Bitrate
:
string
...
...
src/modules/student/lab/views/Index.vue
浏览文件 @
3dc107c7
...
@@ -20,6 +20,7 @@ const appConfig = useAppConfig()
...
@@ -20,6 +20,7 @@ const appConfig = useAppConfig()
const
Question
=
defineAsyncComponent
(()
=>
import
(
'../components/Question.vue'
))
const
Question
=
defineAsyncComponent
(()
=>
import
(
'../components/Question.vue'
))
const
Info
=
defineAsyncComponent
(()
=>
import
(
'../components/Info.vue'
))
const
Info
=
defineAsyncComponent
(()
=>
import
(
'../components/Info.vue'
))
const
Case
=
defineAsyncComponent
(()
=>
import
(
'../components/Case.vue'
))
const
Case
=
defineAsyncComponent
(()
=>
import
(
'../components/Case.vue'
))
const
CaseResource
=
defineAsyncComponent
(()
=>
import
(
'../components/CaseResource.vue'
))
const
Book
=
defineAsyncComponent
(()
=>
import
(
'../components/Book.vue'
))
const
Book
=
defineAsyncComponent
(()
=>
import
(
'../components/Book.vue'
))
const
Video
=
defineAsyncComponent
(()
=>
import
(
'../components/Video.vue'
))
const
Video
=
defineAsyncComponent
(()
=>
import
(
'../components/Video.vue'
))
const
Discuss
=
defineAsyncComponent
(()
=>
import
(
'../components/Discuss.vue'
))
const
Discuss
=
defineAsyncComponent
(()
=>
import
(
'../components/Discuss.vue'
))
...
@@ -109,6 +110,7 @@ const mobilePanelExpanded = ref(false)
...
@@ -109,6 +110,7 @@ const mobilePanelExpanded = ref(false)
const
tabLabelMap
:
Record
<
string
,
string
>
=
{
const
tabLabelMap
:
Record
<
string
,
string
>
=
{
info
:
'实验信息'
,
info
:
'实验信息'
,
example
:
'实验案例'
,
example
:
'实验案例'
,
caseResource
:
'案例资源'
,
case
:
'案例原文'
,
case
:
'案例原文'
,
book
:
'实训指导'
,
book
:
'实训指导'
,
video
:
'操作视频'
,
video
:
'操作视频'
,
...
@@ -302,6 +304,9 @@ function handleGoToFunction(path: string) {
...
@@ -302,6 +304,9 @@ function handleGoToFunction(path: string) {
@
empty=
"handleEmpty('example')"
@
empty=
"handleEmpty('example')"
@
goToFunction=
"handleGoToFunction"
></Example>
@
goToFunction=
"handleGoToFunction"
></Example>
</el-tab-pane>
</el-tab-pane>
<el-tab-pane
label=
"案例资源"
name=
"caseResource"
v-if=
"!empty.includes('caseResource')"
>
<CaseResource
:experiment_id=
"form.experiment_id"
@
empty=
"handleEmpty('caseResource')"
></CaseResource>
</el-tab-pane>
<el-tab-pane
label=
"案例原文"
name=
"case"
v-if=
"!empty.includes('case')"
>
<el-tab-pane
label=
"案例原文"
name=
"case"
v-if=
"!empty.includes('case')"
>
<Case
<Case
:course_id=
"form.course_id"
:course_id=
"form.course_id"
...
...
vite.config.ts
浏览文件 @
3dc107c7
// import fs from 'node:fs'
// import path from 'node:path'
import
{
fileURLToPath
,
URL
}
from
'node:url'
import
{
fileURLToPath
,
URL
}
from
'node:url'
import
{
defineConfig
}
from
'vite'
import
{
defineConfig
}
from
'vite'
...
@@ -25,25 +23,16 @@ export default defineConfig(({ mode }) => ({
...
@@ -25,25 +23,16 @@ export default defineConfig(({ mode }) => ({
server
:
{
server
:
{
open
:
true
,
open
:
true
,
host
:
'dev.ezijing.com'
,
host
:
'dev.ezijing.com'
,
// https: {
// key: fs.readFileSync(path.join(__dirname, './https/ezijing.com.key')),
// cert: fs.readFileSync(path.join(__dirname, './https/ezijing.com.pem'))
// },
proxy
:
{
proxy
:
{
// '/api/lab': {
'/api/resource'
:
{
// target: 'http://local-com-resource-api.frontend.ezijing.com',
target
:
'http://localhost:20082'
,
// changeOrigin: true,
changeOrigin
:
true
,
// rewrite: (path) => path.replace(/^\/api\/lab/, ''),
rewrite
:
(
path
)
=>
path
.
replace
(
/^
\/
api
\/
resource/
,
''
),
// },
},
// '/api/resource': {
'/api/lab'
:
{
// target: 'http://com-resource-admin-test.ezijing.com',
// changeOrigin: true,
// rewrite: path => path.replace(/^\/api\/resource/, '')
// },
'/api/dev'
:
{
target
:
'http://localhost:20081'
,
target
:
'http://localhost:20081'
,
changeOrigin
:
true
,
changeOrigin
:
true
,
rewrite
:
(
path
)
=>
path
.
replace
(
/^
\/
api
\/
dev
/
,
''
),
rewrite
:
(
path
)
=>
path
.
replace
(
/^
\/
api
\/
lab
/
,
''
),
},
},
'/api'
:
'https://saas-lab.ezijing.com'
,
'/api'
:
'https://saas-lab.ezijing.com'
,
},
},
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论