提交 774af296 authored 作者: 王鹏飞's avatar 王鹏飞

feat: 新增案例资源

上级 019d9053
-----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-----
-----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-----
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
"typescript": "^5.9.3", "typescript": "^5.9.3",
"unplugin-auto-import": "^21.0.0", "unplugin-auto-import": "^21.0.0",
"vite": "^8.0.2", "vite": "^8.0.2",
"vite-plugin-mkcert": "^1.17.10", "vite-plugin-mkcert": "^1.17.12",
"vue-tsc": "^3.2.6" "vue-tsc": "^3.2.6"
} }
}, },
...@@ -5311,35 +5311,22 @@ ...@@ -5311,35 +5311,22 @@
} }
}, },
"node_modules/vite-plugin-mkcert": { "node_modules/vite-plugin-mkcert": {
"version": "1.17.10", "version": "1.17.12",
"resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.17.10.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.17.12.tgz",
"integrity": "sha512-703hecAoGZYgNrkY76OIbo0M9j0tfGIIM6n7c0sYvhaczQMPwD0nFi+bS44d8kwdbCtV7885FYBWnTzhsaC7QQ==", "integrity": "sha512-HhLyUZnvkKLuk9o+HBaMlGIa6CqPTfLbVlLFBjrkN4mpZ4saYFRfFx5R12a46UxmzWT9dO8fijmblVvCYvCD3A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^1.13.5",
"debug": "^4.4.3", "debug": "^4.4.3",
"picocolors": "^1.1.1" "picocolors": "^1.1.1"
}, },
"engines": { "engines": {
"node": ">=v16.7.0" "node": ">=v18.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"vite": ">=3" "vite": ">=3"
} }
}, },
"node_modules/vite-plugin-mkcert/node_modules/axios": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/vite/node_modules/picomatch": { "node_modules/vite/node_modules/picomatch": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
...@@ -9363,27 +9350,13 @@ ...@@ -9363,27 +9350,13 @@
} }
}, },
"vite-plugin-mkcert": { "vite-plugin-mkcert": {
"version": "1.17.10", "version": "1.17.12",
"resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.17.10.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.17.12.tgz",
"integrity": "sha512-703hecAoGZYgNrkY76OIbo0M9j0tfGIIM6n7c0sYvhaczQMPwD0nFi+bS44d8kwdbCtV7885FYBWnTzhsaC7QQ==", "integrity": "sha512-HhLyUZnvkKLuk9o+HBaMlGIa6CqPTfLbVlLFBjrkN4mpZ4saYFRfFx5R12a46UxmzWT9dO8fijmblVvCYvCD3A==",
"dev": true, "dev": true,
"requires": { "requires": {
"axios": "^1.13.5",
"debug": "^4.4.3", "debug": "^4.4.3",
"picocolors": "^1.1.1" "picocolors": "^1.1.1"
},
"dependencies": {
"axios": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"dev": true,
"requires": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
}
} }
}, },
"vm2": { "vm2": {
......
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
"typescript": "^5.9.3", "typescript": "^5.9.3",
"unplugin-auto-import": "^21.0.0", "unplugin-auto-import": "^21.0.0",
"vite": "^8.0.2", "vite": "^8.0.2",
"vite-plugin-mkcert": "^1.17.10", "vite-plugin-mkcert": "^1.17.12",
"vue-tsc": "^3.2.6" "vue-tsc": "^3.2.6"
} }
} }
...@@ -47,6 +47,12 @@ export const menus: IMenuItem[] = [ ...@@ -47,6 +47,12 @@ export const menus: IMenuItem[] = [
name: '教案', name: '教案',
path: '/resource/lessonplan', path: '/resource/lessonplan',
}, },
{
tag: 'v1-resource-case',
icon: Collection,
name: '案例资源',
path: '/resource/case',
},
{ {
tag: 'v1-resource-other-information-list', tag: 'v1-resource-other-information-list',
icon: Files, icon: Files,
......
...@@ -5,6 +5,8 @@ import { getOnlinePreviewUrl } from '@/utils/util' ...@@ -5,6 +5,8 @@ import { getOnlinePreviewUrl } from '@/utils/util'
interface PreviewFileItem { interface PreviewFileItem {
url?: string url?: string
name?: string name?: string
mime_type?: string
type?: string
} }
const props = defineProps<{ const props = defineProps<{
...@@ -17,21 +19,32 @@ const previewItem = computed<PreviewFileItem>(() => { ...@@ -17,21 +19,32 @@ const previewItem = computed<PreviewFileItem>(() => {
return { return {
url: props.item?.url || props.url || '', url: props.item?.url || props.url || '',
name: props.item?.name || props.name || '', name: props.item?.name || props.name || '',
mime_type: props.item?.mime_type || props.item?.type || '',
} }
}) })
const fileUrl = computed(() => previewItem.value.url || '') const fileUrl = computed(() => previewItem.value.url || '')
const fileName = computed(() => previewItem.value.name || '') const fileName = computed(() => previewItem.value.name || '')
const mimeType = computed(() => (previewItem.value.mime_type || '').toLowerCase())
const fileExtension = computed(() => { const fileExtension = computed(() => {
const cleanName = fileName.value.split('?')[0].split('#')[0].toLowerCase()
const nameMatches = cleanName.match(/\.([a-z0-9]+)$/)
if (nameMatches?.[1]) return nameMatches[1]
const cleanUrl = fileUrl.value.split('?')[0].split('#')[0].toLowerCase() const cleanUrl = fileUrl.value.split('?')[0].split('#')[0].toLowerCase()
const matches = cleanUrl.match(/\.([a-z0-9]+)$/) const urlMatches = cleanUrl.match(/\.([a-z0-9]+)$/)
return matches?.[1] || '' return urlMatches?.[1] || ''
}) })
const previewType = computed(() => { const previewType = computed(() => {
if (mimeType.value.startsWith('video/')) return 'video'
if (mimeType.value.startsWith('audio/')) return 'audio'
if (mimeType.value.startsWith('image/')) return 'image'
if (mimeType.value === 'application/pdf' || mimeType.value === 'text/plain') return 'embed'
if (['pdf', 'txt'].includes(fileExtension.value)) return 'embed' if (['pdf', 'txt'].includes(fileExtension.value)) return 'embed'
if (fileExtension.value === 'mp4') return 'video' if (['mp4', 'mov', 'm4v', 'webm'].includes(fileExtension.value)) return 'video'
if (fileExtension.value === 'mp3') return 'audio' if (fileExtension.value === 'mp3') return 'audio'
if (['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(fileExtension.value)) return 'image' if (['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(fileExtension.value)) return 'image'
if (['rar', 'zip'].includes(fileExtension.value)) return 'archive' if (['rar', 'zip'].includes(fileExtension.value)) return 'archive'
......
...@@ -25,7 +25,7 @@ const isShowClassStuDialog = ref(false) ...@@ -25,7 +25,7 @@ const isShowClassStuDialog = ref(false)
const isRelatingDialog = ref(false) const isRelatingDialog = ref(false)
const listOptions = computed(() => { const listOptions = computed(() => {
return { return {
remote: { httpRequest: getClassList, params: { name: '', organ_id: '', status: '' } }, remote: { httpRequest: getClassList, params: { name: '', organ_id: '', status: '1' } },
filters: [ filters: [
{ type: 'input', prop: 'name', label: '班级名称:', placeholder: '班级名称' }, { type: 'input', prop: 'name', label: '班级名称:', placeholder: '班级名称' },
{ type: 'select', prop: 'organ_id', slots: 'filter-department' }, { type: 'select', prop: 'organ_id', slots: 'filter-department' },
......
...@@ -16,7 +16,7 @@ const isEdit = ref('') ...@@ -16,7 +16,7 @@ const isEdit = ref('')
const isShowProDialog = ref(false) const isShowProDialog = ref(false)
const listOptions = computed(() => { const listOptions = computed(() => {
return { return {
remote: { httpRequest: getProList, params: { name: '', status: '' } }, remote: { httpRequest: getProList, params: { name: '', status: '1' } },
filters: [ filters: [
{ type: 'input', prop: 'name', label: '专业名称:', placeholder: '专业名称' }, { type: 'input', prop: 'name', label: '专业名称:', placeholder: '专业名称' },
{ type: 'select', prop: 'status', label: '状态:', options: store.getMapValuesByKey('system_status') }, { type: 'select', prop: 'status', label: '状态:', options: store.getMapValuesByKey('system_status') },
......
...@@ -20,7 +20,7 @@ const isShowAddSemDialog = ref(false) ...@@ -20,7 +20,7 @@ const isShowAddSemDialog = ref(false)
const isShowSemCourseDialog = ref(false) const isShowSemCourseDialog = ref(false)
const listOptions = computed(() => { const listOptions = computed(() => {
return { return {
remote: { httpRequest: getSemList, params: { name: '', organ_id: '', status: '' } }, remote: { httpRequest: getSemList, params: { name: '', organ_id: '', status: '1' } },
filters: [ filters: [
{ type: 'input', prop: 'name', label: '学期名称:', placeholder: '学期名称' }, { type: 'input', prop: 'name', label: '学期名称:', placeholder: '学期名称' },
{ type: 'select', prop: 'organ_id', slots: 'filter-department' }, { type: 'select', prop: 'organ_id', slots: 'filter-department' },
......
...@@ -28,7 +28,7 @@ const listOptions = computed(() => { ...@@ -28,7 +28,7 @@ const listOptions = computed(() => {
name: '', name: '',
organ_id: '', organ_id: '',
account_type: '', account_type: '',
status: '', status: '1',
mobile: '', mobile: '',
}, },
}, },
......
...@@ -41,7 +41,7 @@ const listOptions = computed(() => { ...@@ -41,7 +41,7 @@ const listOptions = computed(() => {
specialty_id: '', specialty_id: '',
class_id: '', class_id: '',
account_type: '', account_type: '',
status: '', status: '1',
}, },
beforeRequest: (requestPrams: any) => { beforeRequest: (requestPrams: any) => {
params.organ_id = requestPrams.organ_id params.organ_id = requestPrams.organ_id
......
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/case-sync',
component: AppLayout,
children: [{ path: '', component: () => import('./views/Index.vue') }],
},
]
<script setup lang="ts">
import { useCaseSync } from '../composables/useCaseSync'
const defaultProps = {
children: 'children',
label: 'category_name',
value: 'id',
}
const {
categoryList,
classification,
filteredList,
getAllChecked,
handleToggleAll,
handleToggleCase,
keyword,
loading,
progress,
progressText,
selectedCount,
selectedIds,
submitting,
submitSync,
} = useCaseSync()
</script>
<template>
<AppCard title="案例同步">
<div class="case-sync-toolbar">
<el-input v-model="keyword" clearable placeholder="请输入案例名称/编号" style="width: 280px" />
<el-tree-select
v-model="classification"
:data="categoryList"
:props="defaultProps"
node-key="id"
clearable
check-strictly
filterable
style="width: 240px"
placeholder="请选择案例分类" />
<el-button type="primary" :loading="submitting" :disabled="!selectedCount" @click="submitSync">批量同步</el-button>
</div>
<el-alert type="warning" :closable="false" class="case-sync-alert">
<div class="case-sync-alert__content">
<div class="case-sync-alert__title">重要提示:</div>
<div>1、案例将直接同步到案例资源列表,不会同步到视频、课件、教案、其他资料和实操资源。</div>
<div>2、同步时会按案例名称匹配本地案例资源;匹配到则更新,未匹配到则新增。</div>
<div>3、同步会以外部案例文件为准,已不存在的文件会从本地案例资源中移除。</div>
</div>
</el-alert>
<div v-if="selectedCount || submitting || progress" class="case-sync-progress">
<div class="case-sync-progress__header">
<span>{{ progressText || `准备处理 0/${selectedCount}` }}</span>
<span>{{ progress }}%</span>
</div>
<el-progress :percentage="progress" :show-text="false" />
</div>
<div v-loading="loading" class="case-sync-list">
<div class="case-sync-list__actions">
<el-checkbox :model-value="getAllChecked()" @update:model-value="(value) => handleToggleAll(!!value)">全选</el-checkbox>
<span class="case-sync-list__count">已选 {{ selectedCount }} 个案例</span>
</div>
<div class="case-list">
<div v-for="item in filteredList" :key="item.id" class="case-card">
<div class="case-card__top">
<div class="case-card__info">
<div class="case-card__name">{{ item.name }}</div>
<div class="case-card__id">{{ item.id }}</div>
</div>
<el-checkbox
:model-value="selectedIds.includes(item.id)"
@update:model-value="(value) => handleToggleCase(item.id, !!value)">
选择
</el-checkbox>
</div>
<div class="case-card__desc">{{ item.description || '暂无案例简介' }}</div>
<div class="case-card__meta">
<el-tag size="small">文件数 {{ item.files?.length || 0 }}</el-tag>
<el-tag size="small" type="info">创建人 {{ item.operator?.name || '-' }}</el-tag>
<el-tag size="small" type="success">更新时间 {{ item.updated_at }}</el-tag>
</div>
</div>
</div>
<el-empty v-if="!filteredList.length && !loading" description="暂无案例数据" :image-size="88" />
</div>
</AppCard>
</template>
<style scoped lang="scss">
.case-sync-toolbar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 16px;
}
.case-sync-alert {
margin-bottom: 16px;
}
.case-sync-alert__title {
font-weight: 600;
}
.case-sync-alert__content {
display: flex;
flex-direction: column;
gap: 6px;
line-height: 1.6;
}
.case-sync-progress {
margin-bottom: 16px;
padding: 12px 16px;
background: var(--el-fill-color-light);
border-radius: 8px;
}
.case-sync-progress__header {
display: flex;
justify-content: space-between;
gap: 12px;
margin-bottom: 8px;
font-size: 14px;
color: var(--el-text-color-regular);
}
.case-sync-list__actions {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.case-sync-list__count {
font-size: 14px;
color: #606266;
}
.case-list {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
.case-card {
padding: 16px;
border: 1px solid var(--el-border-color);
border-radius: 10px;
background: #fff;
}
.case-card__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.case-card__info {
min-width: 0;
}
.case-card__name {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.case-card__id {
margin-top: 4px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.case-card__desc {
display: -webkit-box;
min-height: 44px;
margin-bottom: 12px;
overflow: hidden;
font-size: 13px;
line-height: 1.7;
color: var(--el-text-color-regular);
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.case-card__meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
@media (max-width: 1200px) {
.case-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.case-list {
grid-template-columns: minmax(0, 1fr);
}
}
</style>
...@@ -57,7 +57,7 @@ const listOptions = computed(() => { ...@@ -57,7 +57,7 @@ const listOptions = computed(() => {
return { return {
remote: { remote: {
httpRequest: getCourseList, httpRequest: getCourseList,
params: { tab: tabValue, status: '', authorized: '', name: '' }, params: { tab: tabValue, status: '1', authorized: '', name: '' },
}, },
filters: [ filters: [
{ type: 'input', prop: 'name', label: '名称:' }, { type: 'input', prop: 'name', label: '名称:' },
......
import httpRequest from '@/utils/axios'
import type { ResourceCaseFilePayload, ResourceCasePayload } from './types'
export function getCaseList(params?: {
tab: string
name?: string
status?: string
authorized?: string
page?: number
['per-page']?: number
}) {
return httpRequest.get('/api/resource/v1/resource/case/list', { params })
}
export function getCaseDetails(params: { id: string }) {
return httpRequest.get('/api/resource/v1/resource/case/view', { params })
}
export function createCase(data: ResourceCasePayload) {
return httpRequest.post('/api/resource/v1/resource/case/create', data, {
headers: { 'Content-Type': 'application/json' },
})
}
export function updateCase(data: ResourceCasePayload & { id: string; deleted_file_ids?: string[] }) {
return httpRequest.post('/api/resource/v1/resource/case/update', data, {
headers: { 'Content-Type': 'application/json' },
})
}
export function setDepartment(data: { id: string }) {
return httpRequest.post('/api/resource/v1/resource/case/set-department', data)
}
export function setPlatform(data: { id: string }) {
return httpRequest.post('/api/resource/v1/resource/case/set-platform', data)
}
export function setStatus(data: { id: string }) {
return httpRequest.post('/api/resource/v1/resource/case/set-status', data)
}
export function setBelong(data: { id: string; belong_operator: string }) {
return httpRequest.post('/api/resource/v1/resource/case/set-belong', data)
}
export function deleteCase(data: { id: string }) {
return httpRequest.post('/api/resource/v1/resource/case/delete', data)
}
export const CASE_FILE_TYPES: Array<{ label: string; value: ResourceCaseFilePayload['type']; accept?: string }> = [
{
label: '案例原文',
value: 1,
accept: '.doc,.docx,.pdf',
},
{
label: '案例PPT',
value: 2,
accept: '.ppt,.pptx,.pdf',
},
{
label: '案例指导书',
value: 3,
accept: '.doc,.docx,.pdf',
},
{
label: '案例操作视频',
value: 4,
accept: '.mp4,.mov,.m4v',
},
{
label: '案例数据集',
value: 5,
accept: '.xls,.xlsx,.csv',
},
{
label: '其他资料',
value: 6,
accept: '.doc,.docx,.xls,.xlsx,.csv,.pdf,.ppt,.pptx,.mp3,.mp4,.mov,.m4v,.png,.jpg,.jpeg,.gif,.webp,.zip,.rar,.html,.txt,.md',
},
]
<script setup lang="ts">
import { deleteCase, setBelong, setDepartment, setPlatform, setStatus } from '../api'
import type { ResourceCaseDetail } from '../types'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useProjectList } from '@/composables/useGetProjectList'
const props = defineProps<{ data: ResourceCaseDetail }>()
const route = useRoute()
const router = useRouter()
const id = route.query.id as string
const form = reactive({
members: '',
})
const dialogVisible = ref(false)
const members = ref<any[]>([])
function reload() {
router.go(0)
}
function handleDepartment() {
const message =
props.data.department_public === '0'
? `该操作将会使本案例资源在您所在的部门“${props.data.organ_id_name}”内部共享,确认部门共享吗?`
: `该操作将会取消本案例资源在您所在的部门“${props.data.organ_id_name}”内部共享,确认取消部门共享吗?`
ElMessageBox.confirm(message, '提示', { type: 'warning' }).then(async () => {
await setDepartment({ id })
ElMessage.success('更改成功')
reload()
})
}
function handlePlatform() {
const message =
props.data.platform_public === '0'
? '该操作将会使本案例资源在平台公开共享供所有老师使用,确认公开该资源吗?'
: '该操作将会取消本案例资源在平台公开共享,确认取消平台共享吗?'
ElMessageBox.confirm(message, '提示', { type: 'warning' }).then(async () => {
await setPlatform({ id })
ElMessage.success('更改成功')
reload()
})
}
function handleStatus() {
const message = props.data.status === '1' ? '确认下线该案例资源吗?' : '确认再次上线该案例资源吗?'
ElMessageBox.confirm(message, '提示', { type: 'warning' }).then(async () => {
await setStatus({ id })
ElMessage.success('更改成功')
reload()
})
}
function handleDelete() {
ElMessageBox.confirm(`确定要删除案例资源“${props.data.name}”吗?`, '提示', { type: 'warning' }).then(async () => {
await deleteCase({ id })
ElMessage.success('删除成功')
router.push('/resource/case')
})
}
function handleMembers() {
dialogVisible.value = true
members.value = useProjectList(props.data.organ_id).members.value
}
async function handleSetBelong() {
await setBelong({ id, belong_operator: form.members })
ElMessage.success('更改成功')
dialogVisible.value = false
reload()
}
</script>
<template>
<div class="tool-btn-box" v-if="$route.query.id" style="margin-bottom: 20px">
<router-link v-if="props.data.auth_edit" :to="`/resource/case/update?id=${id}`">
<div class="btn-item">编辑案例信息</div>
</router-link>
<div v-if="props.data.auth_department" class="btn-item" @click="handleDepartment">
{{ props.data.department_public === '0' ? '部门共享' : '取消部门共享' }}
</div>
<div v-if="props.data.auth_platform" class="btn-item" @click="handlePlatform">
{{ props.data.platform_public === '0' ? '平台共享' : '取消平台共享' }}
</div>
<div v-if="props.data.auth_status" class="btn-item" @click="handleStatus">
{{ props.data.status === '0' ? '资源上线' : '资源下线' }}
</div>
<div v-if="props.data.auth_belong" class="btn-item" @click="handleMembers">更改负责人</div>
<div v-if="props.data.auth_delete" class="btn-item btn-item--danger" @click="handleDelete">删除资源</div>
</div>
<el-dialog v-model="dialogVisible" title="更改负责人" center>
<el-form :model="form">
<el-form-item>
<div style="width: 500px">
<el-row>
<el-col :span="12">资源创建人:{{ props.data.created_operator_name }}</el-col>
<el-col :span="12">创建时间:{{ props.data.created_time }}</el-col>
</el-row>
<el-row style="margin-top: 12px">
<el-col :span="12">资源负责人:{{ props.data.belong_operator_name }}</el-col>
<el-col :span="12">更新时间:{{ props.data.updated_time }}</el-col>
</el-row>
</div>
<el-select style="width: 500px; margin-top: 20px" v-model="form.members" placeholder="请选择新的资源负责人">
<el-option v-for="item in members" :label="item.name" :value="item.id" :key="item.id" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSetBelong">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<style scoped lang="scss">
.tool-btn-box {
display: flex;
justify-content: right;
flex-wrap: wrap;
gap: 10px;
.btn-item {
min-width: 127px;
line-height: 36px;
background: #aa1941;
border-radius: 20px;
font-size: 14px;
color: #ffffff;
text-align: center;
cursor: pointer;
}
.btn-item--danger {
background: #d44b50;
}
}
</style>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { Upload } from '@element-plus/icons-vue'
import type { UploadProps, UploadRequestOptions } from 'element-plus'
import { upload } from '@/utils/upload'
import type { ResourceCaseFileItem } from '../types'
const props = defineProps<{
title: string
accept?: string
modelValue: ResourceCaseFileItem[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: ResourceCaseFileItem[]): void
}>()
const uploadRef = ref()
const existingFiles = computed(() => props.modelValue.filter((item) => item.id))
const pendingFiles = computed(() => props.modelValue.filter((item) => !item.id))
const handleUploadRequest = async (option: UploadRequestOptions) => {
try {
const url = await upload(option.file)
;(option.file as any).url = url
;(option.file as any).raw = Object.assign((option.file as any).raw || {}, { url })
option.onSuccess?.({ url })
} catch (error: any) {
option.onError?.(error)
}
}
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
if (!props.accept) return true
const extension = `.${file.name.split('.').pop()?.toLowerCase() || ''}`
const allowList = props.accept
.split(',')
.map((item) => item.trim().toLowerCase())
.filter(Boolean)
if (!allowList.length || allowList.includes(extension)) {
return true
}
ElMessage.error(`当前类型仅支持:${allowList.join('、')}`)
return false
}
const handleSuccess: UploadProps['onSuccess'] = (_response, file: any) => {
const nextFile = {
name: file.name,
url: file.url || file.raw?.url,
size: file.size || file.raw?.size || 0,
type: file.type || file.raw?.type || '',
mime_type: file.type || file.raw?.type || '',
}
const nextFiles = [...props.modelValue.filter((item) => item.url !== nextFile.url), nextFile]
emit('update:modelValue', nextFiles)
}
const handleError: UploadProps['onError'] = () => {
ElMessage.error('文件上传失败')
}
function handleRemove(url: string) {
emit(
'update:modelValue',
props.modelValue.filter((item) => item.url !== url),
)
}
function handlePreview(url: string) {
window.open(url, '_blank')
}
function formatSize(size: string | number) {
const value = Number(size || 0)
if (!value) return ''
if (value > 1024) {
return `${(value / 1024 / 1024).toFixed(2)} MB`
}
return `${value.toFixed(2)} MB`
}
</script>
<template>
<div class="upload-card">
<div class="upload-card__header">
<div class="upload-card__title">{{ title }}</div>
<el-upload
ref="uploadRef"
action="#"
:accept="accept"
multiple
:show-file-list="false"
:before-upload="beforeUpload"
:http-request="handleUploadRequest"
:on-success="handleSuccess"
:on-error="handleError">
<el-button round class="upload-card__button">
<el-icon><Upload /></el-icon>
选择文件
</el-button>
</el-upload>
</div>
<div class="upload-card__body">
<div class="upload-card__group">
<div class="upload-card__label">已存在文件</div>
<div v-if="existingFiles.length" class="upload-card__list">
<div v-for="item in existingFiles" :key="item.id || item.url" class="upload-card__item">
<div class="upload-card__meta" @click="handlePreview(item.url)">
<div class="upload-card__name">{{ item.name }}</div>
<div v-if="formatSize(item.size)" class="upload-card__size">{{ formatSize(item.size) }}</div>
</div>
<el-button link type="danger" @click="handleRemove(item.url)">移除</el-button>
</div>
</div>
<div v-else class="upload-card__empty">当前类型还没有文件</div>
</div>
<div class="upload-card__group">
<div class="upload-card__label">待上传文件</div>
<div v-if="pendingFiles.length" class="upload-card__list">
<div v-for="item in pendingFiles" :key="item.url" class="upload-card__item">
<div class="upload-card__meta" @click="handlePreview(item.url)">
<div class="upload-card__name">{{ item.name }}</div>
<div v-if="formatSize(item.size)" class="upload-card__size">{{ formatSize(item.size) }}</div>
</div>
<el-button link type="danger" @click="handleRemove(item.url)">移除</el-button>
</div>
</div>
<div v-else class="upload-card__empty">当前没有待上传文件</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.upload-card {
display: flex;
flex-direction: column;
min-height: 288px;
padding: 20px 22px 18px;
border: 1px solid #ebeef5;
border-radius: 18px;
background: #ffffff;
box-shadow: 0 8px 24px rgba(17, 24, 39, 0.04);
}
.upload-card__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 18px;
}
.upload-card__title {
font-size: 18px;
font-weight: 600;
color: #303133;
}
.upload-card__button {
border-color: #d7dbe6;
color: #303133;
}
.upload-card__body {
display: flex;
flex: 1;
flex-direction: column;
gap: 18px;
}
.upload-card__group {
display: flex;
flex: 1;
flex-direction: column;
gap: 10px;
}
.upload-card__label {
font-size: 14px;
font-weight: 500;
color: #606266;
}
.upload-card__list {
display: flex;
flex-direction: column;
gap: 10px;
}
.upload-card__item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 14px;
border-radius: 12px;
background: #f8f9fc;
}
.upload-card__meta {
min-width: 0;
cursor: pointer;
}
.upload-card__name {
overflow: hidden;
font-size: 14px;
font-weight: 500;
color: #303133;
text-overflow: ellipsis;
white-space: nowrap;
}
.upload-card__size {
margin-top: 4px;
font-size: 12px;
color: #909399;
}
.upload-card__empty {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
min-height: 56px;
border-radius: 12px;
background: #fafafa;
color: #c0c4cc;
font-size: 13px;
}
</style>
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/resource/case',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/List.vue') },
{ path: '/resource/case/update', component: () => import('./views/Update.vue') },
{ path: '/resource/case/view', component: () => import('./views/View.vue') },
],
},
]
export interface ResourceCaseFileItem {
id?: string
name: string
url: string
size: string | number
type?: string
mime_type?: string
}
export interface ResourceCaseFilePayload {
type: 1 | 2 | 3 | 4 | 5 | 6
name: string
url: string
size: number
mime_type: string
sort: number
}
export interface ResourceCasePayload {
name: string
description: string
cover?: string
classification: string
status: string
source: string
files: ResourceCaseFilePayload[]
}
export interface ResourceCaseListItem {
id: string
name: string
description: string
organ_id_name: string
organ_id: string
status: string
status_name: string
file_count: string
bind_count: string
classification_name: string
created_operator_name: string
belong_operator_name: string
updated_time: string
created_time: string
auth_edit: boolean
auth_delete: boolean
auth_department: boolean
auth_platform: boolean
auth_status: boolean
auth_belong: boolean
department_public: string
department_public_name: string
platform_public: string
platform_public_name: string
}
export interface ResourceCaseDetail extends ResourceCaseListItem {
cover: string
source: string
source_name?: string
files: Array<{
id: string
name: string
url: string
size: string
mime_type: string
type: string
type_name: string
sort: string
}>
}
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import { deleteCase, getCaseList } from '../api'
import { useProjectList } from '@/composables/useGetProjectList'
import { useMapStore } from '@/stores/map'
import { useUserStore } from '@/stores/user'
const appList = ref()
const store = useMapStore()
const userStore = useUserStore()
const isAdmin = computed(() => userStore.roles.some((item: any) => item.name === '超级管理员'))
const departmentList: any = useProjectList('', '79806610719731712').departmentList
const listOptions = computed(() => ({
remote: {
httpRequest: getCaseList,
params: { tab: tabValue, status: '1', authorized: '', name: '' },
},
filters: [
{ type: 'input', prop: 'name', label: '案例名称:' },
{
type: 'select',
prop: 'status',
label: '状态:',
options: store.getMapValuesByKey('system_status'),
},
...(tabValue.value === '3' && isAdmin.value
? [
{
prop: 'authorized',
slots: 'filter-department',
},
]
: []),
],
columns: [
{ label: '案例名称', prop: 'name', minWidth: 220 },
{ label: '文件数', prop: 'file_count', width: 100, align: 'center' },
{ label: '绑定实验数', prop: 'bind_count', width: 120, align: 'center' },
{ label: '资源状态', prop: 'status_name', width: 100, align: 'center' },
{ label: '部门共享', prop: 'department_public_name', width: 110, align: 'center' },
{ label: '平台共享', prop: 'platform_public_name', width: 110, align: 'center' },
{ label: '创建人', prop: 'created_operator_name', width: 120, align: 'center' },
{ label: '更新日期', prop: 'updated_time', width: 180, align: 'center' },
{ label: '操作', slots: 'table-operate', width: 280, fixed: 'right', align: 'center' },
],
}))
const tabValue = ref('1')
function tabChange() {
appList.value?.refetch()
}
function refetch() {
appList.value?.refetch()
}
async function handleDelete(row: any) {
await ElMessageBox.confirm(`确定要删除案例资源“${row.name}”吗?`, '提示', { type: 'warning' })
await deleteCase({ id: row.id })
ElMessage.success('删除成功')
refetch()
}
</script>
<template>
<AppCard>
<el-tabs @tab-change="tabChange" v-model="tabValue">
<el-tab-pane label="我的资源" name="1"></el-tab-pane>
<el-tab-pane label="部门资源" name="2"></el-tab-pane>
<el-tab-pane label="公开资源" name="3"></el-tab-pane>
</el-tabs>
<div class="case-tool-btn">
<router-link to="/resource/case/update" v-if="tabValue === '1'">
<el-button type="primary" round>新建案例资源</el-button>
</router-link>
</div>
<AppList v-bind="listOptions" ref="appList">
<template #filter-department="{ params }">
<div class="filter-label">部门:</div>
<el-select clearable v-model="params.authorized" @change="refetch">
<el-option v-for="item in departmentList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</template>
<template #table-operate="{ row }">
<el-space>
<router-link :to="`/resource/case/view?id=${row.id}`">
<el-button plain>查看</el-button>
</router-link>
<router-link v-if="row.auth_edit" :to="`/resource/case/update?id=${row.id}`">
<el-button type="primary" plain>编辑</el-button>
</router-link>
<el-button v-if="row.auth_delete" type="danger" plain @click="handleDelete(row)">删除</el-button>
</el-space>
</template>
</AppList>
</AppCard>
</template>
<style scoped lang="scss">
.case-tool-btn {
padding: 10px 0 30px;
}
.filter-label {
font-size: 14px;
color: #606266;
padding-right: 12px;
}
</style>
<script setup lang="ts">
import { PictureFilled } from '@element-plus/icons-vue'
import type { FormInstance } from 'element-plus'
import type { ResourceCaseFileItem, ResourceCasePayload } from '../types'
import { ElMessage } from 'element-plus'
import { CASE_FILE_TYPES, createCase, getCaseDetails, updateCase } from '../api'
import { useGetCategoryList } from '@/composables/useGetCategoryList'
import { useMapStore } from '@/stores/map'
import UploadCard from '../components/UploadCard.vue'
import UploadImg from '@/modules/resource/video/components/UploadImg.vue'
const router = useRouter()
const route = useRoute()
const mapStore = useMapStore()
const id = route.query.id as string
const isUpdate = computed(() => !!id)
const form = reactive<Record<string, any>>({
name: '',
description: '',
cover: '',
classification: '',
status: '1',
source: '2',
type_1: [] as ResourceCaseFileItem[],
type_2: [] as ResourceCaseFileItem[],
type_3: [] as ResourceCaseFileItem[],
type_4: [] as ResourceCaseFileItem[],
type_5: [] as ResourceCaseFileItem[],
type_6: [] as ResourceCaseFileItem[],
})
const deletedFileIds = ref<string[]>([])
const { list: selectTree }: any = useGetCategoryList()
const defaultProps = {
children: 'children',
label: 'category_name',
value: 'id',
}
const statusOptions = computed(() => mapStore.getMapValuesByKey('system_status'))
CASE_FILE_TYPES.forEach((section) => {
watch(
() => form[`type_${section.value}`] as ResourceCaseFileItem[],
(value, oldValue = []) => {
oldValue.forEach((item) => {
if (item.id && !value.some((current) => current.id === item.id) && !deletedFileIds.value.includes(item.id)) {
deletedFileIds.value.push(item.id)
}
})
},
{ deep: true },
)
})
if (isUpdate.value) {
getCaseDetails({ id }).then((res: any) => {
const detail = res.data
form.name = detail.name
form.description = detail.description
form.cover = detail.cover || ''
form.classification = detail.classification
form.status = detail.status
CASE_FILE_TYPES.forEach((section) => {
form[`type_${section.value}`] = detail.files
.filter((item: any) => String(item.type) === String(section.value))
.map((item: any) => ({
id: item.id,
name: item.name,
url: item.url,
size: item.size,
mime_type: item.mime_type,
type: item.mime_type,
}))
})
})
}
const rules = {
name: [{ required: true, message: '请输入案例名称', trigger: 'blur' }],
classification: [{ required: true, message: '请选择分类', trigger: 'change' }],
}
const formRef = ref<FormInstance>()
let submitFlag = true
function collectFiles() {
return CASE_FILE_TYPES.flatMap((section) => {
return (form[`type_${section.value}`] as ResourceCaseFileItem[])
.filter((item) => !item.id)
.map((item, index) => ({
type: section.value,
name: item.name,
url: item.url,
size: normalizeFileSize(item.size),
mime_type: item.mime_type || item.type || '',
sort: index + 1,
}))
}).map((item) => ({
...item,
size: Number(item.size.toFixed ? item.size.toFixed(2) : item.size),
}))
}
function getCurrentFileCount() {
return CASE_FILE_TYPES.reduce((total, section) => {
return total + (form[`type_${section.value}`] as ResourceCaseFileItem[]).length
}, 0)
}
function normalizeFileSize(size: string | number | undefined) {
const value = Number(size || 0)
if (!value) return 0
// AppUpload returns bytes for new uploads; edited data is already stored as MB.
if (value > 1024) {
return value / 1024 / 1024
}
return value
}
async function handleSubmit(formEl?: FormInstance) {
if (!submitFlag || !formEl) return
submitFlag = false
await formEl.validate(async (valid) => {
if (!valid) {
submitFlag = true
ElMessage.error('请完善信息')
return
}
const files = collectFiles().filter((item) => item.name && item.url)
const params: ResourceCasePayload = {
name: form.name,
description: form.description,
cover: form.cover,
classification: form.classification,
status: form.status,
source: form.source,
files,
}
try {
if (isUpdate.value) {
await updateCase({ id, ...params, deleted_file_ids: deletedFileIds.value })
ElMessage.success('更新成功')
} else {
await createCase(params)
ElMessage.success('创建成功')
}
router.push('/resource/case')
} catch (error) {
submitFlag = true
}
})
}
</script>
<template>
<AppCard :title="isUpdate ? '编辑案例资源' : '新建案例资源'">
<el-form ref="formRef" :model="form" :rules="rules" style="width: 75%" label-width="110px">
<el-form-item label="案例名称:" prop="name">
<el-input v-model="form.name" maxlength="100" />
</el-form-item>
<el-form-item label="案例分类:" prop="classification">
<el-tree-select
:render-after-expand="false"
:props="defaultProps"
style="width: 100%"
v-model="form.classification"
:data="selectTree"
node-key="id"
:default-expanded-keys="selectTree.length ? [selectTree[0]?.id] : []" />
</el-form-item>
<el-form-item label="资源状态:">
<el-radio-group v-model="form.status">
<el-radio v-for="item in statusOptions" :key="item.value" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="案例描述:">
<el-input v-model="form.description" type="textarea" :rows="4" maxlength="2000" show-word-limit />
</el-form-item>
<el-form-item label="封面图片:">
<div class="case-cover">
<div class="case-cover__preview">
<el-icon v-if="!form.cover" :size="50" color="#c0c4cc">
<PictureFilled />
</el-icon>
<div v-else class="case-cover__image" :style="`background-image:url(${form.cover})`"></div>
</div>
<div class="case-cover__content">
<UploadImg accept=".jpg,.jpeg,.png,.gif,.webp" v-model="form.cover" />
<div class="case-cover__tips">
<div>该图片作为案例资源封面,用于列表和详情展示。</div>
<div>支持 jpg、jpeg、png、gif、webp 格式。</div>
</div>
</div>
</div>
</el-form-item>
</el-form>
<div class="upload-grid">
<UploadCard
v-for="section in CASE_FILE_TYPES"
:key="section.value"
v-model="form[`type_${section.value}`]"
:title="section.label"
:accept="section.accept" />
</div>
<div class="btn-box">
<div class="confirm" @click="handleSubmit(formRef)">保存</div>
<div class="cancel" @click="router.go(-1)">取消</div>
</div>
</AppCard>
</template>
<style scoped lang="scss">
.case-cover {
display: flex;
align-items: flex-start;
gap: 16px;
}
.case-cover__preview {
display: flex;
align-items: center;
justify-content: center;
width: 180px;
height: 132px;
border: 1px dashed #dcdfe6;
border-radius: 12px;
background: #fafafa;
overflow: hidden;
}
.case-cover__image {
width: 100%;
height: 100%;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
.case-cover__content {
display: flex;
flex-direction: column;
gap: 10px;
}
.case-cover__tips {
font-size: 12px;
line-height: 1.7;
color: #909399;
}
.upload-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
margin-top: 8px;
}
@media (max-width: 1200px) {
.upload-grid {
grid-template-columns: 1fr;
}
}
.btn-box {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 30px;
.confirm,
.cancel {
min-width: 120px;
line-height: 40px;
border-radius: 22px;
text-align: center;
cursor: pointer;
}
.confirm {
background: #aa1941;
color: #fff;
}
.cancel {
border: 1px solid #dcdfe6;
color: #606266;
background: #fff;
}
}
</style>
<script setup lang="ts">
import Operation from '../components/Operation.vue'
import type { ResourceCaseDetail } from '../types'
import { getCaseDetails } from '../api'
import PreviewFiles from '@/components/base/PreviewFiles.vue'
const route = useRoute()
const detail = ref<ResourceCaseDetail | null>(null)
const previewVisible = ref(false)
const previewItem = ref<any>(null)
const previewTitle = computed(() => previewItem.value?.name || '文件预览')
function handlePreview(row: any) {
previewItem.value = row
previewVisible.value = true
}
const fileGroups = computed(() => {
const groups: Record<string, any[]> = {}
;(detail.value?.files || []).forEach((item) => {
if (!groups[item.type_name]) {
groups[item.type_name] = []
}
groups[item.type_name].push(item)
})
return groups
})
getCaseDetails({ id: route.query.id as string }).then((res: any) => {
detail.value = res.data
})
</script>
<template>
<AppCard title="案例资源信息" v-if="detail">
<Operation :data="detail"></Operation>
<el-descriptions :column="2" border class="case-detail-descriptions">
<el-descriptions-item label="案例名称">{{ detail.name }}</el-descriptions-item>
<el-descriptions-item label="案例分类">{{ detail.classification_name || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="资源状态">
<span :class="detail.status === '1' ? 'status-active' : 'status-inactive'">{{ detail.status_name }}</span>
</el-descriptions-item>
<el-descriptions-item label="文件数">{{ detail.file_count }}</el-descriptions-item>
<el-descriptions-item label="绑定实验数">{{ detail.bind_count }}</el-descriptions-item>
<el-descriptions-item label="所属部门">{{ detail.organ_id_name }}</el-descriptions-item>
<el-descriptions-item label="创建人">{{ detail.created_operator_name }}</el-descriptions-item>
<el-descriptions-item label="负责人">{{ detail.belong_operator_name }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detail.created_time }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ detail.updated_time }}</el-descriptions-item>
<el-descriptions-item label="案例描述" :span="2">{{ detail.description || '暂无' }}</el-descriptions-item>
</el-descriptions>
<div class="file-groups">
<div class="file-group" v-for="(files, title) in fileGroups" :key="title">
<div class="file-group__title">{{ title }}</div>
<el-table :data="files" border>
<el-table-column label="文件名称" prop="name" min-width="260" />
<el-table-column label="文件大小(MB)" prop="size" width="140" />
<el-table-column label="文件类型" prop="mime_type" min-width="200" />
<el-table-column label="操作" width="120" align="center">
<template #default="{ row }">
<el-button type="primary" link @click="handlePreview(row)">查看</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<el-dialog
v-model="previewVisible"
modal-class="case-preview-dialog"
:title="previewTitle"
fullscreen
destroy-on-close
append-to-body>
<PreviewFiles :item="previewItem" v-if="previewItem" />
</el-dialog>
</AppCard>
</template>
<style scoped lang="scss">
:deep(.case-detail-descriptions .el-descriptions__label.is-bordered-label) {
min-width: 110px;
white-space: nowrap;
word-break: keep-all;
}
.status-active {
color: #67c23a;
}
.status-inactive {
color: #f56c6c;
}
.file-groups {
margin-top: 24px;
}
.file-group {
margin-top: 20px;
}
.file-group__title {
margin-bottom: 12px;
font-size: 16px;
font-weight: 600;
color: #303133;
}
</style>
<style lang="scss">
.case-preview-dialog .preview-container {
height: calc(100vh - 160px) !important;
}
</style>
...@@ -49,6 +49,8 @@ const listOptions = computed(() => { ...@@ -49,6 +49,8 @@ const listOptions = computed(() => {
}, },
}, },
{ label: '课件分类', prop: 'classification_name', align: 'center' }, { label: '课件分类', prop: 'classification_name', align: 'center' },
{ label: '部门共享', prop: 'department_public_name', align: 'center' },
{ label: '平台共享', prop: 'platform_public_name', align: 'center' },
{ label: '知识点', prop: 'knowledge_points', align: 'center' }, { label: '知识点', prop: 'knowledge_points', align: 'center' },
{ label: '资源状态', prop: 'status_name', align: 'center' }, { label: '资源状态', prop: 'status_name', align: 'center' },
{ label: '审核状态', prop: 'audit_status_name', align: 'center' }, { label: '审核状态', prop: 'audit_status_name', align: 'center' },
...@@ -57,16 +59,10 @@ const listOptions = computed(() => { ...@@ -57,16 +59,10 @@ const listOptions = computed(() => {
{ label: '更新日期', prop: 'updated_time', align: 'center' }, { label: '更新日期', prop: 'updated_time', align: 'center' },
{ label: '操作', slots: 'table-operate', align: 'center', width: 200, fixed: 'right' }, { label: '操作', slots: 'table-operate', align: 'center', width: 200, fixed: 'right' },
] ]
if (tabValue.value === '2') {
columns.splice(2, 0, { label: '部门共享', prop: 'department_public_name', align: 'center' })
}
if (tabValue.value === '3') {
columns.splice(2, 0, { label: '平台共享', prop: 'platform_public_name', align: 'center' })
}
return { return {
remote: { remote: {
httpRequest: getCourseList, httpRequest: getCourseList,
params: { tab: tabValue, status: '', authorized: '', name: '' }, params: { tab: tabValue, status: '1', authorized: '', name: '' },
}, },
filters: [ filters: [
{ type: 'input', prop: 'name', label: '标题:' }, { type: 'input', prop: 'name', label: '标题:' },
......
...@@ -49,6 +49,8 @@ const listOptions = computed(() => { ...@@ -49,6 +49,8 @@ const listOptions = computed(() => {
}, },
}, },
{ label: '教案分类', prop: 'classification_name', align: 'center' }, { label: '教案分类', prop: 'classification_name', align: 'center' },
{ label: '部门共享', prop: 'department_public_name', align: 'center' },
{ label: '平台共享', prop: 'platform_public_name', align: 'center' },
{ label: '知识点', prop: 'knowledge_points', align: 'center' }, { label: '知识点', prop: 'knowledge_points', align: 'center' },
{ label: '资源状态', prop: 'status_name', align: 'center' }, { label: '资源状态', prop: 'status_name', align: 'center' },
{ label: '审核状态', prop: 'audit_status_name', align: 'center' }, { label: '审核状态', prop: 'audit_status_name', align: 'center' },
...@@ -57,16 +59,10 @@ const listOptions = computed(() => { ...@@ -57,16 +59,10 @@ const listOptions = computed(() => {
{ label: '更新日期', prop: 'updated_time', align: 'center' }, { label: '更新日期', prop: 'updated_time', align: 'center' },
{ label: '操作', slots: 'table-operate', align: 'center', width: 200, fixed: 'right' }, { label: '操作', slots: 'table-operate', align: 'center', width: 200, fixed: 'right' },
] ]
if (tabValue.value === '2') {
columns.splice(2, 0, { label: '部门共享', prop: 'department_public_name', align: 'center' })
}
if (tabValue.value === '3') {
columns.splice(2, 0, { label: '平台共享', prop: 'platform_public_name', align: 'center' })
}
return { return {
remote: { remote: {
httpRequest: getLessonList, httpRequest: getLessonList,
params: { tab: tabValue, status: '', authorized: '', name: '' }, params: { tab: tabValue, status: '1', authorized: '', name: '' },
}, },
filters: [ filters: [
{ type: 'input', prop: 'name', label: '标题:' }, { type: 'input', prop: 'name', label: '标题:' },
......
...@@ -50,6 +50,8 @@ const listOptions = computed(() => { ...@@ -50,6 +50,8 @@ const listOptions = computed(() => {
}, },
}, },
{ label: '课件分类', prop: 'classification_name', align: 'center' }, { label: '课件分类', prop: 'classification_name', align: 'center' },
{ label: '部门共享', prop: 'department_public_name', align: 'center' },
{ label: '平台共享', prop: 'platform_public_name', align: 'center' },
{ label: '知识点', prop: 'knowledge_points', align: 'center' }, { label: '知识点', prop: 'knowledge_points', align: 'center' },
{ label: '资源状态', prop: 'status_name', align: 'center' }, { label: '资源状态', prop: 'status_name', align: 'center' },
{ label: '审核状态', prop: 'audit_status_name', align: 'center' }, { label: '审核状态', prop: 'audit_status_name', align: 'center' },
...@@ -58,16 +60,10 @@ const listOptions = computed(() => { ...@@ -58,16 +60,10 @@ const listOptions = computed(() => {
{ label: '更新日期', prop: 'updated_time', align: 'center' }, { label: '更新日期', prop: 'updated_time', align: 'center' },
{ label: '操作', slots: 'table-operate', align: 'center', width: 200, fixed: 'right' }, { label: '操作', slots: 'table-operate', align: 'center', width: 200, fixed: 'right' },
] ]
if (tabValue.value === '2') {
columns.splice(2, 0, { label: '部门共享', prop: 'department_public_name', align: 'center' })
}
if (tabValue.value === '3') {
columns.splice(2, 0, { label: '平台共享', prop: 'platform_public_name', align: 'center' })
}
return { return {
remote: { remote: {
httpRequest: getOtherList, httpRequest: getOtherList,
params: { tab: tabValue, status: '', authorized: '', name: '' }, params: { tab: tabValue, status: '1', authorized: '', name: '' },
}, },
filters: [ filters: [
{ type: 'input', prop: 'name', label: '标题:' }, { type: 'input', prop: 'name', label: '标题:' },
......
...@@ -48,6 +48,8 @@ const listOptions = computed(() => { ...@@ -48,6 +48,8 @@ const listOptions = computed(() => {
}, },
}, },
{ label: '视频分类', prop: 'classification_name', align: 'center' }, { label: '视频分类', prop: 'classification_name', align: 'center' },
{ label: '部门共享', prop: 'department_public_name', align: 'center' },
{ label: '平台共享', prop: 'platform_public_name', align: 'center' },
{ label: '知识点', prop: 'knowledge_points', align: 'center' }, { label: '知识点', prop: 'knowledge_points', align: 'center' },
{ label: '封面', slots: 'table-cover', width: 150, align: 'center' }, { label: '封面', slots: 'table-cover', width: 150, align: 'center' },
{ label: '资源状态', prop: 'status_name', align: 'center' }, { label: '资源状态', prop: 'status_name', align: 'center' },
...@@ -57,16 +59,10 @@ const listOptions = computed(() => { ...@@ -57,16 +59,10 @@ const listOptions = computed(() => {
{ label: '更新日期', prop: 'updated_time', align: 'center' }, { label: '更新日期', prop: 'updated_time', align: 'center' },
{ label: '操作', slots: 'table-operate', align: 'center', width: 200, fixed: 'right' }, { label: '操作', slots: 'table-operate', align: 'center', width: 200, fixed: 'right' },
] ]
if (tabValue.value === '2') {
columns.splice(2, 0, { label: '部门共享', prop: 'department_public_name', align: 'center' })
}
if (tabValue.value === '3') {
columns.splice(2, 0, { label: '平台共享', prop: 'platform_public_name', align: 'center' })
}
return { return {
remote: { remote: {
httpRequest: getVideoList, httpRequest: getVideoList,
params: { tab: tabValue, status: '', authorized: '', name: '' }, params: { tab: tabValue, status: '1', authorized: '', name: '' },
}, },
filters: [ filters: [
{ type: 'input', prop: 'name', label: '标题:' }, { type: 'input', prop: 'name', label: '标题:' },
......
...@@ -12,7 +12,7 @@ const listOptions = computed(() => { ...@@ -12,7 +12,7 @@ const listOptions = computed(() => {
return { return {
remote: { remote: {
httpRequest: getCoverList, httpRequest: getCoverList,
params: { type: '' } params: { type: '', status: '1' }
}, },
filters: [ filters: [
{ {
......
...@@ -18,7 +18,7 @@ const listOptions = computed(() => { ...@@ -18,7 +18,7 @@ const listOptions = computed(() => {
remote: { remote: {
httpRequest: getDictionaryList, httpRequest: getDictionaryList,
params: { params: {
status: '', status: '1',
type: '', type: '',
created_time_start: '', created_time_start: '',
created_time_end: '' created_time_end: ''
......
...@@ -3,7 +3,7 @@ import md5 from 'blueimp-md5' ...@@ -3,7 +3,7 @@ import md5 from 'blueimp-md5'
import { getSignature, uploadFile, getLocalFileChunk, uploadLocalFile } from '@/api/base' import { getSignature, uploadFile, getLocalFileChunk, uploadLocalFile } from '@/api/base'
// 生产环境线上部署 // 生产环境线上部署
export async function ossUpload(blob: Blob | File) { export async function ossUpload(blob: Blob | File, prefix = 'upload/resource-center/') {
let fileType = 'png' let fileType = 'png'
if (blob instanceof File && blob.name) { if (blob instanceof File && blob.name) {
const matches = blob.name.match(/\.(\w+)$/) const matches = blob.name.match(/\.(\w+)$/)
...@@ -16,8 +16,7 @@ export async function ossUpload(blob: Blob | File) { ...@@ -16,8 +16,7 @@ export async function ossUpload(blob: Blob | File) {
fileType = mimeType fileType = mimeType
} }
} }
const key = const key = prefix + md5(new Date().getTime() + Math.random().toString(36).slice(-8)) + '.' + fileType
'upload/resource-center/' + md5(new Date().getTime() + Math.random().toString(36).slice(-8)) + '.' + fileType
const response: any = await getSignature() const response: any = await getSignature()
const params = { const params = {
key, key,
......
import fs from 'fs'
import path from 'path'
import { fileURLToPath, URL } from 'url' import { fileURLToPath, URL } from 'url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
...@@ -17,11 +15,17 @@ export default defineConfig(({ mode }) => ({ ...@@ -17,11 +15,17 @@ 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/dev.ezijing.com.key')),
cert: fs.readFileSync(path.join(__dirname, './https/dev.ezijing.com.pem')),
},
proxy: { proxy: {
'/api/resource': {
target: 'http://localhost:20082',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/resource/, ''),
},
'/api/lab': {
target: 'http://localhost:20081',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/lab/, ''),
},
'/api': 'https://resource-center.ezijing.com', '/api': 'https://resource-center.ezijing.com',
}, },
}, },
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论