提交 80a0d6e3 authored 作者: 王鹏飞's avatar 王鹏飞

init

上级
VITE_LOGIN_URL=https://login.ezijing.com/auth/login/index
VITE_LOGIN_URL=https://login.ezijing.com/auth/login/index
VITE_LOGIN_URL=https://login2.ezijing.com/auth/login/index
{
"globals": {
"$": true,
"$$": true,
"$computed": true,
"$customRef": true,
"$ref": true,
"$shallowRef": true,
"$toRef": true,
"EffectScope": true,
"computed": true,
"createApp": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"effectScope": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onUnmounted": true,
"onUpdated": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useRoute": true,
"useRouter": true,
"useSlots": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
}
}
\ No newline at end of file
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript/recommended',
'./.eslintrc-auto-import.json'
],
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-explicit-any': 'off'
}
}
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}
# saas-learn
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```
// Generated by 'unplugin-auto-import'
export {}
declare global {
const $$: typeof import('vue/macros')['$$']
const $: typeof import('vue/macros')['$']
const $computed: typeof import('vue/macros')['$computed']
const $customRef: typeof import('vue/macros')['$customRef']
const $ref: typeof import('vue/macros')['$ref']
const $shallowRef: typeof import('vue/macros')['$shallowRef']
const $toRef: typeof import('vue/macros')['$toRef']
const EffectScope: typeof import('vue')['EffectScope']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const effectScope: typeof import('vue')['effectScope']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}
import fs from 'fs'
import path from 'path'
import chalk from 'chalk'
import OSS from 'ali-oss'
const log = console.log
const __dirname = path.resolve()
const client = new OSS({
region: 'oss-cn-beijing',
accessKeyId: 'LTAIOTuuLTaWoGJj',
accessKeySecret: 'dE5tTGm2lh35eItct2krW2DeH2lf2I',
bucket: 'webapp-pub'
})
async function uploadTarget(src, dist) {
try {
const result = await client.put(dist, path.join(__dirname, src))
log(chalk.green('上传成功', result.url))
} catch (e) {
log(chalk.red('上传失败', src))
log(e)
}
}
function generateUploadTarget(src, dist) {
fs.readdir(path.join(__dirname, src), function (err, files) {
if (err) {
log(err)
return
}
files.forEach(function (file) {
const _src = src + '/' + file
const _dist = dist + '/' + file
const stats = fs.statSync(path.join(__dirname, _src))
// 判断是否为文件
stats.isFile() && uploadTarget(_src, _dist)
// 判断是否为文件夹
stats.isDirectory() && generateUploadTarget(_src, _dist)
})
})
}
generateUploadTarget('./dist', '/website/prod/saas-learn')
/// <reference types="vite/client" />
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAn0EINdIXTDCzmR7J5FOjOV+PbXt7GNO6fanoCGe2O0CPRlNf
2Ea/wv6SlRtJPd0ohmnKqZdUbBpAsiV4ggOdOqeEB6utVYQWY/zhXRKYeRjN/iDu
WCRY5S+eRVkSzVOJP9DlBn6dnHSsWj55h1PrkIac8B862F/cVno/Wk5dqU55ZUoN
wHGw5Goz3R37w+Q0C9HRS5mrmPqI+Ogy8TJrIRxw9YAj5OlvuqBAeYAW1sNdEfsi
mMB0H2fbbXqEL4AsipE5ppP7Ij3vxVpxvmnl/SO7N6+Fit6r25VeFSvplK+PIV3c
UsK3PCKV2sOo0BDWtWFQh5hW3fK5RYjLpNDHCwIDAQABAoIBAEkiBDMzF5/VfaSD
jxNblUlzqNoOKqlsEehDblrtxbHQI/uXrhwT4VwarBXtQeU2+rU/P+JBrHM4Wx10
N7L9FecppmgfXqo2zlF8f8HOGFcEHRTm6o1vo6McCwKttQS1qAG2XHZvDtIagkuv
BQAwea0VJFzg+pUC8JyF5zIBauGkfk8eHTLFVuIEJoSJbPWBYzp7Vf1SCjXqs3YY
aZ5QkOqY7S81D2EULFAWiMIMdY/PVT5DSXxsjaJFkvxjDedA4jNCplyODBKdpnBb
kfoJTJ7qsSnqgJ2y2xRdRlvZalE49lr2MkW254s5GH35+hMYam0bffgLXdPz6RIs
7X0atYECgYEA1A9G+0+uYlyxddyR54QlWGK7L3wP+REMXultudT9rq4S6qkHoOgP
rhi2kvZOqA0sMR7XMVz5nw0ouUMUVfW0YzudgAK99tdIuk6dP6VqVo9T4kqa0rXi
3ZKD51qGXbF22SndEWV68QEPzMCbf0E+kXl5MGGNnFtjZ5nxTGS+uH8CgYEAwECs
0T36EnLOCXZoi3rTeHr2pSO20VuFSgljnHA6Ups9Chu6h/iZ8t0XVNb8J14q7lFi
NY6b4D3FR/vwO3nFt7dvFYNFaFGuFrkAaH002p8EYWSckhlGcucBuKivBVUbhXuM
HMGmqGhAnnGCvCj/v4n5/wv3wtFYfzYWnYPHC3UCgYBZgbFGNhW28sT8qIL1I3PX
4KR9oHHlgOqlzQVBYMNKzbKyVXIg2pJzu36kfU4p5JV4jjnqXgIGvjkoKUYWGkVv
dSQ/eejQnYHXEYOR77H4ozqW00KSGa+OMl92cWExfsxZUTA8PYcs3nPayplXlyRf
ptQeNa7eBjzo57NPuV4+5QKBgQCrJihzUlBYshmYNPBXE25FOHpwgz3SXT5orbke
4I4bUhXh9NN3DqrGmWqW3Zi2108ywALFGQLNe1AwiCnSWNLafZOHvEhC2Uw48FNb
sfMmmR/GMFJugc/EpMBUit7cyWppx5XxV7gs/jpgkz7GkV00P/ntwtK7fbDh9t3l
NhYxrQKBgDVE4HSDqOvZOaXGRoM0pJ3uYRTTSIDGVNMZ9t2C/t3uwoyFBe+Om2t+
G6w2Gr+Dck1v+zizU3khbAHvE67rYoUtrDvae41bmLuVcnYh4UsXfhB6BWOSaQ+l
l8aQwTfmV74szsEDcFkg038zQ6Q4c8iiurYp29nwEM7/mayBGOcv
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIHEDCCBfigAwIBAgIQC53CSHjB5MGsHDzx/2AxzjANBgkqhkiG9w0BAQsFADBb
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMRowGAYDVQQDExFTZWN1cmUgU2l0ZSBDQSBHMjAeFw0y
MDA2MTAwMDAwMDBaFw0yMjA5MTIxMjAwMDBaMFsxCzAJBgNVBAYTAkNOMRAwDgYD
VQQIEwdCZWlqaW5nMSIwIAYDVQQKExlUSEggWmlqaW5nIChCZWlqaW5nKSBJbmMu
MRYwFAYDVQQDDA0qLmV6aWppbmcuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAn0EINdIXTDCzmR7J5FOjOV+PbXt7GNO6fanoCGe2O0CPRlNf2Ea/
wv6SlRtJPd0ohmnKqZdUbBpAsiV4ggOdOqeEB6utVYQWY/zhXRKYeRjN/iDuWCRY
5S+eRVkSzVOJP9DlBn6dnHSsWj55h1PrkIac8B862F/cVno/Wk5dqU55ZUoNwHGw
5Goz3R37w+Q0C9HRS5mrmPqI+Ogy8TJrIRxw9YAj5OlvuqBAeYAW1sNdEfsimMB0
H2fbbXqEL4AsipE5ppP7Ij3vxVpxvmnl/SO7N6+Fit6r25VeFSvplK+PIV3cUsK3
PCKV2sOo0BDWtWFQh5hW3fK5RYjLpNDHCwIDAQABo4IDzjCCA8owHwYDVR0jBBgw
FoAUxBF+iECGwkG/ZfMa4bRTQKOr7H0wHQYDVR0OBBYEFHxjLRRYXe2jIjYECuN8
r3EnjOTFMCUGA1UdEQQeMByCDSouZXppamluZy5jb22CC2V6aWppbmcuY29tMA4G
A1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwbwYD
VR0fBGgwZjAxoC+gLYYraHR0cDovL2NybDMuZGlnaWNlcnQuY29tL1NlY3VyZVNp
dGVDQUcyLmNybDAxoC+gLYYraHR0cDovL2NybDQuZGlnaWNlcnQuY29tL1NlY3Vy
ZVNpdGVDQUcyLmNybDBMBgNVHSAERTBDMDcGCWCGSAGG/WwBATAqMCgGCCsGAQUF
BwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAgGBmeBDAECAjBsBggr
BgEFBQcBAQRgMF4wIQYIKwYBBQUHMAGGFWh0dHA6Ly9vY3NwLmRjb2NzcC5jbjA5
BggrBgEFBQcwAoYtaHR0cDovL2NybC5kaWdpY2VydC1jbi5jb20vU2VjdXJlU2l0
ZUNBRzIuY3J0MAwGA1UdEwEB/wQCMAAwggH1BgorBgEEAdZ5AgQCBIIB5QSCAeEB
3wB2AEalVet1+pEgMLWiiWn0830RLEF0vv1JuIWr8vxw/m1HAAABcpwT21oAAAQD
AEcwRQIgWTyqiBOL3dFTJBE2Q6cgSBzk9W5iTaC2B8T1f8gFCP0CIQDhngm9WJbO
J7v14h6w+B2Li7WEAkWLSLiTKzh7na2SuQB1ACJFRQdZVSRWlj+hL/H3bYbgIyZj
rcBLf13Gg1xu4g8CAAABcpwT2zEAAAQDAEYwRAIgckmPL6WJx9Jke4AfVLmy//ye
tsmT5si8FO8p9Fd52VECICPqDvdjlN2DtfQznTGTxaL0PQ5N8eNiX3fJn6sRCfcU
AHYAUaOw9f0BeZxWbbg3eI8MpHrMGyfL956IQpoN/tSLBeUAAAFynBPbfQAABAMA
RzBFAiEAwYooscdEijXGnRdJYnz0ClmvWcxtJ169Bq+sywhPReACIDjvE5a5d7mb
n3YTgfLOtbnuDpkDRjUfdY7cs6UfderhAHYAQcjKsd8iRkoQxqE6CUKHXk4xixsD
6+tLx2jwkGKWBvYAAAFynBPa0wAABAMARzBFAiAmJVwNfWFMKrqWTvEfHk9O/5/r
Crj/W3BqjV6p0D09hgIhAIKb4drMok8s1X0Evh4Nbzd3Nv9PuwITdICztemCrk4e
MA0GCSqGSIb3DQEBCwUAA4IBAQBWSrE/pt//MKeGpf6vMISGD0LZArebPFQ7wlgv
Y13HpCY5lqwrZItsuXWS5IYMv8ueYarCm081OJOBvSUKHOtYSe6wdFqsXehokUiy
7oVNief7Li5RvLcf6z5fyjB+i017dds73Dt94mE1imV1DR1WErp1U6QCMEh+TKFa
PL52V9X5VWiYdImzdm8AWOlNBrgicmVzEEQuglejF5uaALf9iiyAjP36apqXv77T
UtxKgjONB1tnRw4XRqzwrEK+QjeOhziKCn1v2ppFX/Z11YYA7ajICVrG6wGJ+ENc
ukf5+v8r+TU7PqxQmb62zocX22jhe8HM644UJ4FWCiBh4Lb1
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFFjCCA/6gAwIBAgIQCH4Y+4+qkn7odgoNiYL1EjANBgkqhkiG9w0BAQsFADBh
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
QTAeFw0xOTA2MjAxMjIxMzVaFw0yOTA2MjAxMjIxMzVaMFsxCzAJBgNVBAYTAlVT
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
b20xGjAYBgNVBAMTEVNlY3VyZSBTaXRlIENBIEcyMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAx7s903fR6SgpA08UdhKEUIZHa2Ig7KPNkTtwMS1+08YS
5QSEDM4DQxy48jP8dZkyyU9J/0WCm8Nlv5ga7HOAxhdJcv+CPP4oadx8EbdrmjAH
rGOv64oHvt7Ina7uzLd3krqxd0doeuxRpTHvFAyjaUhxjSfZx0wh1f6W7prPm7V5
0VcTudj4rI+xtHXUcFAuFz4bcapTcru5aaZ1v6F2usMCMVM+xJxEZcsUM4uTxdIf
W5FUTI0dbP8NyZkr/WVzL59aGwBE4ZU0JKBlgEmtkFpLPR7JCzYunafu7nMk5YY2
6WDOmezpWDjzDxJ8xakizykWYT5gdJYE3ULlUe31WQIDAQABo4IBzjCCAcowHQYD
VR0OBBYEFMQRfohAhsJBv2XzGuG0U0Cjq+x9MB8GA1UdIwQYMBaAFAPeUDVW0Uy7
ZvCj4hsbw5eyPdFVMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcD
AQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zAxBggrBgEFBQcBAQQlMCMwIQYI
KwYBBQUHMAGGFWh0dHA6Ly9vY3NwLmRjb2NzcC5jbjBEBgNVHR8EPTA7MDmgN6A1
hjNodHRwOi8vY3JsLmRpZ2ljZXJ0LWNuLmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RD
QS5jcmwwgc4GA1UdIASBxjCBwzCBwAYEVR0gADCBtzAoBggrBgEFBQcCARYcaHR0
cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzCBigYIKwYBBQUHAgIwfgx8QW55IHVz
ZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0aXR1dGVzIGFjY2VwdGFuY2Ugb2Yg
dGhlIFJlbHlpbmcgUGFydHkgQWdyZWVtZW50IGxvY2F0ZWQgYXQgaHR0cHM6Ly93
d3cuZGlnaWNlcnQuY29tL3JwYS11YTANBgkqhkiG9w0BAQsFAAOCAQEAE+8lW5Yw
IuiRsHn4gYRRVbLmIypWwYH74lIXnQiALeUsUkWfW7KA0ARF1el3YaTAg8/r6zyX
eZTdlhndxKOKvO5N+rnHWJB6a3fJURn6e0I+rDzKV1Zacv2Vx/ZHLZmza/bp4Azi
BrDOiPlW/Ktj6ALQzAgq70Oytk9htLupBWPuplJDdyhGqb9RfQvWc1Fa1HwXdBQi
oJPibfMaYkHMY3pTbOv2rzMKEoZwHDHqyC73RI9JgqqiXHw0rIL8A1uL3IrymXEr
mycTqbSozQwiiEfb+cxzY82YaNzaLpJyIst0T2QmdDDngmyd2LEmm4NKeXRrcFRh
XDDFfpIn93B7JA==
-----END CERTIFICATE-----
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="https://zws-imgs-pub.ezijing.com/pc/base/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>紫荆教育学习系统</title>
<script src="https://webapp-pub.ezijing.com/plugins/tinymce@6/tinymce.min.js"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "saas-learn",
"version": "0.0.0",
"scripts": {
"dev": "vite --mode dev",
"build": "run-p type-check build-only --mode prod && npm run deploy",
"build:test": "run-p type-check build-only --mode test",
"build:pre": "run-p type-check build-only --mode pre",
"preview": "vite preview --port 4173",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"deploy": "node ./deploy.js"
},
"dependencies": {
"@tinymce/tinymce-vue": "^5.0.0",
"@vueuse/core": "^8.9.3",
"axios": "^0.27.2",
"blueimp-md5": "^2.19.0",
"element-plus": "^2.2.9",
"pinia": "^2.0.16",
"qs": "^6.11.0",
"video.js": "^7.19.2",
"vue": "^3.2.37",
"vue-router": "^4.1.2"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.1.0",
"@types/blueimp-md5": "^2.18.0",
"@types/node": "^16.11.43",
"@types/qs": "^6.9.7",
"@types/video.js": "^7.3.42",
"@vitejs/plugin-vue": "^3.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/tsconfig": "^0.1.3",
"ali-oss": "^6.17.1",
"eslint": "^8.5.0",
"eslint-plugin-vue": "^9.2.0",
"npm-run-all": "^4.1.5",
"sass": "^1.53.0",
"typescript": "~4.7.4",
"unplugin-auto-import": "^0.9.2",
"vite": "^3.0.0",
"vue-tsc": "^0.38.5"
}
}
<template>
<RouterView />
</template>
<style>
@import '@/assets/styles/base.css';
</style>
import httpRequest from '@/utils/axios'
// 获取用户信息
export function getUser() {
return httpRequest.get('/api/resource/v1/util/info')
}
// 退出登录
export function logout() {
return httpRequest.get('/api/passport/rest/logout')
}
// 获取oss token
export function getToken() {
return httpRequest.get('/api/usercenter/aliyun/assume-role')
}
// 获取oss signature
export function getSignature() {
return httpRequest.get('/api/usercenter/aliyun/get-signature')
}
// 图片上传
export function uploadFile(data: Record<string, any>) {
return httpRequest
.post('https://webapp-pub.oss-cn-beijing.aliyuncs.com', data, {
withCredentials: false,
headers: { 'Content-Type': 'multipart/form-data' }
})
.then(() => data)
}
// 删除评论
export function deleteComment(data: { id: string }) {
return httpRequest.post('/api/resource/backend/comment/delete', data)
}
// 获取公共字典列表
export function getMapList() {
return httpRequest.get('/api/resource/v1/util/get-data-dictionary-list')
}
// 上传视频
export function getCreateAuth(data: { title: string; file_name: string }) {
return httpRequest.post('/api/resource/v1/resource/video/create-auth', data)
}
// 刷新上传视频的地址凭证
export function updateAuth(data: { source_id: string }) {
return httpRequest.post('/api/resource/v1/resource/video/create-auth', data)
}
// 获取分类列表
export function getCategoryList(params: { type: string; category_name?: string }) {
return httpRequest.get('/api/resource/v1/backend/category/list', { params })
}
// 获取项目列表
export function getProjectList(params: { organization_id?: string; project_id?: string }) {
return httpRequest.get('/api/resource/v1/util/members', { params })
}
// 获取试题分类
export function getQuestionCategory(params: { project_tag: string }) {
return httpRequest.get(`/api/qbs/admin/v2/question-category/tree/${params.project_tag}`, { params })
}
import type { IMenuItem } from '@/types'
export const menus: IMenuItem[] = [
{
name: '学习',
path: '/course'
},
{
name: '论坛',
path: '/bbs'
},
{
name: '收藏',
path: '/favorites'
},
{
name: '设置',
path: '/settings'
}
]
body,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
p,
blockquote,
dl,
dt,
dd,
ul,
ol,
li,
pre,
form,
fieldset,
legend,
button,
input,
textarea,
th,
td {
margin: 0;
padding: 0;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: 100%;
}
ul,
ol,
li {
list-style: none;
}
em,
i {
font-style: normal;
}
strong,
b {
font-weight: normal;
}
img {
border: none;
}
input,
img {
vertical-align: middle;
}
a {
color: inherit;
text-decoration: none;
}
input,
button,
select,
textarea {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-appearance: none;
border: 0;
border-radius: 0;
font: inherit;
}
textarea:focus {
outline: 0;
}
:root {
--main-color: #aa1941;
}
// styles/element/index.scss
/* 只需要重写你需要的即可 */
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
'base': #aa1941
)
)
);
// 如果只是按需导入,则可以忽略以下内容。
// 如果你想导入所有样式:
@use 'element-plus/theme-chalk/src/index.scss' as *;
import AppCard from '@/components/base/AppCard.vue'
import AppList from '@/components/base/AppList.vue'
declare module 'vue' {
export interface GlobalComponents {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
AppCard: typeof AppCard
AppList: typeof AppList
}
}
<script setup lang="ts">
defineProps<{ title?: string }>()
</script>
<template>
<div class="app-card">
<div class="app-card-hd">
<slot name="header">
<h2 class="app-card-hd__title" v-if="title">{{ title }}</h2>
<div class="app-card-hd-aside">
<slot name="header-aside"></slot>
</div>
</slot>
</div>
<div class="app-card-bd">
<slot></slot>
</div>
</div>
</template>
<style lang="scss">
.app-card {
background: #fff;
box-shadow: 0 1px 6px 0 rgb(228 232 235 / 20%);
border-radius: 6px;
padding: 20px;
}
.app-card + .app-card {
margin-top: 20px;
}
.app-card-hd {
display: flex;
}
.app-card-hd__title {
flex: 1;
padding-left: 5px;
font-size: 18px;
font-weight: 500;
line-height: 1;
margin-bottom: 24px;
border-left: 3px solid #aa1941;
}
</style>
<script setup lang="ts">
import { Search, RefreshLeft } from '@element-plus/icons-vue'
interface IRemoteProps {
params?: any
httpRequest?: any
beforeRequest?: any
callback?: any
}
const props = withDefaults(
defineProps<{
remote?: IRemoteProps
filters?: any[]
moreFilters?: any[]
columns?: any[]
data?: any[]
hasPagination?: boolean
limit?: number
isLimit?: boolean
}>(),
{
isLimit: false,
hasPagination: true,
limit: 10,
data() {
return []
}
}
)
const filterFormRef = ref()
const loading = ref(false)
const tableRef = ref()
const dataList = ref<any[]>([])
const page = reactive({ total: 0, size: props.limit, currentPage: 1 })
const params = reactive({ ...props.remote?.params })
watch(
() => props.data,
list => {
dataList.value = list || []
},
{ immediate: true }
)
// 获取数据
const fetchList = (isReset = false) => {
/**
* @param function httpRequest api接口
* @param function beforeRequest 接口请求之前
* @param function callback 接口请求成功回调
*/
const { httpRequest, beforeRequest, callback } = props.remote || {}
if (!httpRequest) {
return
}
// 参数设置
let requestParams = { ...params }
// 翻页参数设置
if (props.hasPagination) {
requestParams.page = page.currentPage
if (props.isLimit === true) {
requestParams.limit = page.size
} else {
requestParams['per-page'] = page.size
}
}
// 接口请求之前
if (beforeRequest) {
requestParams = beforeRequest(requestParams, isReset)
}
for (const key in params) {
if (params[key] === '' || params[key] === undefined || params[key] === undefined) {
delete params[key]
}
}
loading.value = true
return (
httpRequest(requestParams)
.then((res: any) => {
const { list = [], total = 0 } = callback ? callback(res.data, requestParams) : res.data || {}
page.total = total
dataList.value = list
})
// .catch(() => {
// page.total = 0
// dataList.value = []
// })
.finally(() => {
loading.value = false
})
)
}
// 搜索
const search = () => {
page.currentPage = 1
return fetchList()
}
// 重置
const reset = () => {
// 清空筛选条件
filterFormRef.value?.resetFields()
// 初始化页码
page.currentPage = 1
// 刷新列表
return fetchList(true)
}
// 刷新
const refetch = (isForce = false) => {
return isForce ? reset() : fetchList()
}
// 页数改变
const pageSizeChange = (value: number) => {
page.currentPage = 1
page.size = value
fetchList()
}
onMounted(() => {
fetchList()
})
defineExpose({ refetch, tableRef })
</script>
<template>
<div class="table-list">
<div class="table-list-hd">
<!-- 筛选 -->
<div class="table-list-filter" v-if="filters && filters.length">
<el-form :inline="true" :model="params" ref="filterFormRef" @submit.prevent>
<template v-for="item in filters" :key="item.prop">
<el-form-item :label="item.label" :prop="item.prop">
<template v-if="item.slots">
<slot :name="item.slots" v-bind="{ params }"></slot>
</template>
<template v-else>
<!-- input -->
<el-input
v-model="params[item.prop]"
v-bind="item"
clearable
@change="search"
style="width: 200px"
v-if="item.type === 'input'"
/>
<!-- select -->
<el-select
v-model="params[item.prop]"
v-bind="item"
clearable
@change="search"
v-if="item.type === 'select'"
>
<el-option
:label="option[item.labelKey] || option.label"
:value="option[item.valueKey] || option.value"
v-for="(option, index) in item.options"
:key="index"
/>
</el-select>
</template>
</el-form-item>
</template>
<el-form-item class="filter-buttons">
<el-button type="primary" :icon="Search" @click="search">搜索</el-button>
<el-button :icon="RefreshLeft" @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="table-list-hd-aside"><slot name="header-aside" /></div>
</div>
<slot></slot>
<!-- 主体 -->
<div class="table-list-bd">
<slot name="body" v-bind="{ data: dataList }">
<el-table
:data="dataList"
v-loading="loading"
v-bind="$attrs"
style="height: 100%"
ref="tableRef"
:header-cell-style="{ background: '#EFEFEF' }"
>
<el-table-column v-bind="item || {}" v-for="item in columns" :key="item.prop">
<template #default="scope" v-if="item.slots || item.computed">
<slot :name="item.slots" v-bind="scope" v-if="item.slots"></slot>
<div v-html="item.computed(scope)" v-if="item.computed"></div>
</template>
</el-table-column>
</el-table>
</slot>
</div>
<!-- 底部 -->
<div class="table-list-ft">
<div>
<slot name="footer"></slot>
</div>
<el-pagination
class="table-list-pagination"
background
layout="total, sizes, prev, pager, next, jumper"
:page-sizes="[10, 20, 30, 50, 100]"
:page-size="page.size"
:total="page.total"
v-model:currentPage="page.currentPage"
@size-change="pageSizeChange"
@current-change="fetchList()"
:hide-on-single-page="true"
v-if="hasPagination"
>
</el-pagination>
</div>
</div>
</template>
<style lang="scss">
// .table-list {
// height: 100%;
// display: flex;
// flex-direction: column;
// box-sizing: border-box;
// }
.table-list-hd {
display: flex;
margin-bottom: 10px;
}
.table-list-filter {
flex: 1;
}
// .table-list-bd {
// flex: 1;
// }
.table-list-ft {
padding: 10px 0;
display: flex;
align-items: center;
justify-content: space-between;
}
.table-list-pagination {
text-align: right;
}
.el-table-column--selection .cell {
padding: 0 14px !important;
}
</style>
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import type { UploadProps, UploadUserFile } from 'element-plus'
import md5 from 'blueimp-md5'
import { getSignature } from '@/api/base'
const props = withDefaults(defineProps<{ modelValue: string | []; prefix?: string }>(), {
prefix: 'upload/admin/'
})
const emit = defineEmits(['update:modelValue'])
const uploadData = ref()
const fileList = ref<UploadUserFile[]>([])
watch(
() => props.modelValue,
value => {
fileList.value = Array.isArray(value) ? [...value] : []
}
)
const showFileList = computed(() => {
return Array.isArray(props.modelValue)
})
// 上传之前
const handleBeforeUpload = async (file: any) => {
const fileName = file.name
const key = props.prefix + md5(fileName + new Date().getTime()) + fileName.substr(fileName.lastIndexOf('.'))
const response: Record<string, any> = await getSignature()
uploadData.value = {
key,
OSSAccessKeyId: response.accessid,
policy: response.policy,
signature: response.signature,
success_action_status: '200',
url: `${response.host}/${key}`
}
file.url = `${response.host}/${key}`
}
// 上传成功
const handleSuccess = (response: any, file: any, files: any) => {
if (!files.every((item: any) => item.status === 'success')) return
if (showFileList.value) {
emit(
'update:modelValue',
files.map((item: any) => {
console.log(item, 'items')
return {
name: item.name,
url: item.url || item.raw.url,
size: item.raw.size,
type: item.raw.type || item.raw.url
}
})
)
} else {
emit('update:modelValue', file.raw.url)
}
}
// 上传限制
const handleExceed: UploadProps['onExceed'] = () => {
ElMessage.warning('文件超出个数限制')
}
// 删除
const handleRemove: UploadProps['onRemove'] = (file, files) => {
if (showFileList.value) {
emit(
'update:modelValue',
files.map((item: any) => {
return { name: item.name, url: item.url || item.raw.url }
})
)
} else {
emit('update:modelValue', '')
}
}
// 预览
const handlePreview: UploadProps['onPreview'] = uploadFile => {
console.log(uploadFile)
}
</script>
<template>
<el-upload
action="https://webapp-pub.oss-cn-beijing.aliyuncs.com"
:data="uploadData"
:show-file-list="showFileList"
:before-upload="handleBeforeUpload"
:on-exceed="handleExceed"
:on-remove="handleRemove"
:on-preview="handlePreview"
:on-success="handleSuccess"
:file-list="fileList"
class="uploader"
>
<template v-if="showFileList">
<template v-if="$attrs['list-type'] === 'picture-card'">
<el-icon><Plus /></el-icon>
</template>
<template v-else>
<el-button type="primary" class="app-upload-btn">点击上传</el-button>
</template>
</template>
<div class="avatar-uploader" v-else>
<el-image :src="(modelValue as string)" fit="contain" v-if="modelValue" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</div>
<template #tip>
<div class="el-upload__tip"><slot name="tip"></slot></div>
</template>
</el-upload>
</template>
<style lang="scss">
.uploader {
flex: 1;
}
.avatar-uploader {
width: 178px;
height: 178px;
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
.el-image {
width: 100%;
height: 100%;
}
}
.avatar-uploader:hover {
border-color: var(--el-color-primary);
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100%;
height: 100%;
text-align: center;
}
</style>
<script lang="ts">
const DEFAULT_OPTIONS = {
controls: true,
autoplay: true,
fluid: true,
playbackRates: [0.5, 1, 1.5, 2],
restoreEl: true
}
const DEFAULT_EVENTS = [
'abort',
'canplay',
'canplaythrough',
'durationchange',
'emptied',
'ended',
'error',
'loadeddata',
'loadedmetadata',
'pause',
'play',
'playing',
'progress',
'ratechange',
'resize',
'seeked',
'seeking',
'stalled',
'suspend',
'timeupdate',
'volumechange',
'waiting'
]
</script>
<script setup lang="ts">
import videojs from 'video.js'
import type { VideoJsPlayerOptions, VideoJsPlayer } from 'video.js'
import 'video.js/dist/video-js.css'
interface Props {
src: string | { src: string; type?: string }
options?: VideoJsPlayerOptions
}
const props = defineProps<Props>()
const emit = defineEmits(['ready', ...DEFAULT_EVENTS])
let player = $ref<VideoJsPlayer | null>()
const videoRef = $ref<HTMLVideoElement>()
const videoOptions = $computed<VideoJsPlayerOptions>(() => {
return Object.assign({}, DEFAULT_OPTIONS, props.options)
})
watch(
() => props.src,
src => {
changeSrc(src)
},
{ deep: true, immediate: true }
)
// 初始化播放器
function initPlayer() {
if (!videoRef) return
if (player) {
player.dispose()
player = null
}
player = videojs(videoRef, videoOptions, function onPlayerReady() {
changeSrc(props.src)
// 注册事件
DEFAULT_EVENTS.forEach(eventName => {
this.on(eventName, (...arg) => {
// console.log(eventName, ...arg)
emit(eventName, ...arg)
})
})
emit('ready', this)
})
return player
}
function changeSrc(src: string | { src: string; type?: string }) {
if (!player) return
console.log(1)
console.log(player)
if (!player.paused()) {
console.log(2)
player.pause()
}
player.src(src)
player.load()
player.play()
}
onMounted(() => {
initPlayer()
})
onUnmounted(() => {
player && player.dispose()
})
</script>
<template>
<div>
<video class="video-js vjs-default-skin vjs-big-play-centered vjs-16-9" ref="videoRef"></video>
</div>
</template>
<script lang="ts">
export default {
name: 'AppAside'
}
</script>
<script setup lang="ts">
import { menus } from '@/assets/menus'
import type { IMenuItem } from '@/types'
const router = useRouter()
const route = useRoute()
const menuList = $computed<IMenuItem[]>(() => {
const found = menus.find(item => route.fullPath.includes(item.path))
return found?.children || []
})
const defaultActive = computed(() => {
// 扁平菜单
const flatMenuList: IMenuItem[] = menuList.reduce((result: IMenuItem[], item) => {
result.push(item)
if (item.children) {
result = result.concat(item.children)
}
return result
}, [])
const found = flatMenuList.reverse().find(item => {
return route.path.includes(item.path)
})
return found ? found.path : '/'
})
function isUrl(path: string) {
return /^https?:\/\//.test(path)
}
function handleClick(path: string) {
if (isUrl(path)) {
window.open(path)
} else {
router.push(path)
}
}
</script>
<template>
<aside class="app-aside" v-if="menuList.length">
<nav class="nav">
<el-menu :default-active="defaultActive" class="app-menu">
<template v-for="item in menuList" :key="item.path">
<el-sub-menu :index="item.path" v-permission="item.tag" v-if="item.children">
<template #title>
<el-icon><component :is="item.icon"></component></el-icon>{{ item.name }}
</template>
<el-menu-item
:index="subitem.path"
v-for="subitem in item.children"
:key="subitem.path"
v-permission="subitem.tag"
@click="handleClick(subitem.path)"
>
{{ subitem.name }}
</el-menu-item>
</el-sub-menu>
<el-menu-item :index="item.path" v-permission="item.tag" @click="handleClick(item.path)" v-else>
<el-icon><component :is="item.icon"></component></el-icon>{{ item.name }}
</el-menu-item>
</template>
</el-menu>
</nav>
</aside>
</template>
<style lang="scss">
.app-aside {
width: 200px;
background: #2e3036;
border-right: 1px solid rgba(0, 0, 0, 0.12);
overflow-x: hidden;
overflow-y: auto;
flex: 0 0 200px;
box-sizing: content-box;
}
.nav {
position: fixed;
width: 200px;
margin: 20px 0;
--el-menu-text-color: #cecece;
--el-menu-bg-color: #2e3036;
--el-menu-active-color: #fff;
--el-menu-active-bg-color: #4b4c50;
--el-menu-hover-color: #fff;
--el-menu-hover-bg-color: #4b4c50;
.el-menu {
border-right: 0;
i {
margin-right: 14px;
font-size: 24px;
}
.el-icon-arrow-down {
margin-right: 0;
font-size: 16px;
}
}
.el-menu-item {
&.is-active {
background: var(--el-menu-active-bg-color);
}
}
// .el-menu-item {
// display: flex;
// align-items: center;
// margin: 0 16px;
// font-size: 16px;
// border-radius: 8px;
// }
// .el-submenu .el-menu-item {
// min-width: auto;
// padding-left: 58px !important;
// }
// .el-submenu__title {
// display: flex;
// align-items: center;
// margin: 0 16px;
// font-size: 16px;
// border-radius: 8px;
// }
}
</style>
<template>
<div class="app-breadcrumb" v-if="routes.length">
<el-breadcrumb>
<el-breadcrumb-item v-for="route in routes" :key="route.path">
<router-link :to="route.path">{{ route.meta.title }}</router-link>
</el-breadcrumb-item>
</el-breadcrumb>
</div>
</template>
<script>
export default {
name: 'AppBreadcrumb',
computed: {
routes() {
return this.$route.matched.filter(route => route.meta.title)
}
}
}
</script>
<style lang="scss">
.app-breadcrumb {
padding: 18px 0 32px;
.el-breadcrumb {
font-size: 20px;
font-weight: 400;
line-height: 1;
}
.el-breadcrumb__inner a {
font-weight: normal;
color: #5b91fd;
}
.router-link-active {
color: #1a1b1c;
}
}
</style>
<script lang="ts">
export default { name: 'AppHeader' }
</script>
<script setup lang="ts">
import { menus } from '@/assets/menus'
import { useUserStore } from '@/stores/user'
import type { IMenuItem } from '@/types'
withDefaults(defineProps<{ hasTitle?: boolean }>(), {
hasTitle: true
})
const route = useRoute()
const userStore = useUserStore()
const userInfo = userStore.user
const logout = async () => {
await userStore.logout()
location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(location.href)}`
}
function genNavClassName(data: IMenuItem) {
return route.fullPath.includes(data.path) ? 'is-active' : ''
}
</script>
<template>
<header class="app-header">
<div class="app-header-left">
<div class="logo">
<router-link to="/"><img src="https://zws-imgs-pub.ezijing.com/pc/base/ezijing-logo-white.svg" /></router-link>
</div>
<h1 class="app-name">产业学院</h1>
</div>
<div class="app-header-nav">
<div
class="app-header-nav-item"
v-for="(item, index) in menus"
:key="index"
:class="genNavClassName(item)"
v-permission="item.tag"
>
<router-link :to="item.path">{{ item.name }}</router-link>
</div>
</div>
<div class="app-header-right">
<el-dropdown v-if="userInfo">
<div class="avatar">
<img :src="userInfo.avatar || 'https://webapp-pub.ezijing.com/website/base/images/avatar.svg'" />
</div>
<template #dropdown>
<el-dropdown-menu style="width: 280px">
<div class="app-header-user">
<div class="app-header-user-avatar">
<img :src="userInfo.avatar || 'https://webapp-pub.ezijing.com/website/base/images/avatar.svg'" />
</div>
<div class="app-header-user-main">
<h3>{{ userInfo.name }}</h3>
<p>{{ userInfo.email || userInfo.mobile }}</p>
</div>
<div class="app-header-user-buttons">
<el-button round @click="logout">退出登录</el-button>
</div>
</div>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</header>
</template>
<style lang="scss">
.app-header {
position: sticky;
top: 0;
z-index: 2001;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
height: 70px;
background-color: var(--main-color);
color: #fff;
.logo {
width: 120px;
}
}
.app-header-left {
display: flex;
align-items: center;
.app-name {
margin-left: 20px;
padding: 0 15px;
line-height: 1;
border-left: 2px solid #fff;
}
}
.app-header-nav {
flex: 1;
display: flex;
align-items: center;
// justify-content: center;
}
.app-header-nav-item {
height: 45px;
a {
display: inline-block;
font-size: 16px;
line-height: 45px;
padding: 0 18px;
}
&:hover,
&.is-active {
background: rgba(144, 6, 44, 0.39);
color: #fff;
}
}
.app-header-right {
display: flex;
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
overflow: hidden;
}
&:hover {
background-color: rgba(60, 64, 67, 0.08);
}
}
}
.app-header-user {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 16px;
}
.app-header-user-avatar {
margin-bottom: 6px;
width: 80px;
height: 80px;
border-radius: 50%;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.app-header-user-main {
h3 {
color: #202124;
font: 500 16px/22px Helvetica, Arial, sans-serif;
letter-spacing: 0.29px;
margin: 0;
text-align: center;
text-overflow: ellipsis;
overflow: hidden;
}
p {
color: #5f6368;
font: 400 14px/19px Helvetica, Arial, sans-serif;
letter-spacing: normal;
text-align: center;
text-overflow: ellipsis;
overflow: hidden;
}
}
.app-header-user-buttons {
padding-top: 16px;
}
</style>
<script lang="ts">
export default { name: 'AppLayout' }
</script>
<script setup lang="ts">
import AppHeader from './Header.vue'
import AppAside from './Aside.vue'
import AppMain from './Main.vue'
withDefaults(defineProps<{ sidebar?: boolean; hasTitle?: boolean }>(), {
sidebar: true,
hasTitle: true
})
</script>
<template>
<div class="app-layout">
<app-header :hasTitle="hasTitle"></app-header>
<div class="app-layout-container">
<app-aside v-if="sidebar"></app-aside>
<app-main></app-main>
</div>
</div>
</template>
<style lang="scss">
.app-layout {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f8f8f8;
}
.app-layout-container {
flex: 1;
display: flex;
}
</style>
<script lang="ts">
export default { name: 'AppMain' }
</script>
<script setup lang="ts">
import AppBreadcrumb from './Breadcrumb.vue'
withDefaults(defineProps<{ hasBreadcrumb?: boolean }>(), {
hasBreadcrumb: true
})
</script>
<template>
<section class="app-main">
<div class="app-main-inner">
<div class="app-main-header">
<app-breadcrumb v-if="hasBreadcrumb"></app-breadcrumb>
</div>
<div class="app-main-container">
<router-view></router-view>
</div>
</div>
</section>
</template>
<style>
.manual-btn {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: 50%;
right: 0;
width: 60px;
height: 60px;
background: rgba(255, 255, 255, 1);
box-shadow: 1px 3px 12px rgba(0, 0, 0, 0.11);
border-radius: 50%;
z-index: 999999;
}
.app-main {
position: relative;
flex: 1;
padding: 20px;
overflow: hidden;
}
.app-main-inner {
margin: 0 auto;
}
.app-main-container::after {
content: '';
display: table;
clear: both;
}
.el-form--label-top .el-form-item__label {
padding-bottom: 0;
}
</style>
<script setup lang="ts">
import Editor from '@tinymce/tinymce-vue'
import md5 from 'blueimp-md5'
import { getSignature, uploadFile } from '@/api/base'
const props = defineProps({
height: {
type: Number,
default: 600
}
})
const ImageUploadHandler = (blobInfo: any) =>
new Promise((resolve, reject) => {
const file = blobInfo.blob()
getSignature()
.then((response: any) => {
const prefix = 'upload/admin/'
const key = prefix + md5(file.name + new Date().getTime()) + file.name.substr(file.name.lastIndexOf('.'))
const { accessid, policy, signature, host } = response
const params = {
key,
OSSAccessKeyId: accessid,
policy,
signature,
success_action_status: '200',
file,
url: `${host}/${key}`
}
uploadFile(params)
.then((res: any) => {
resolve(res.url)
})
.catch(() => {
reject('上传失败')
})
})
.catch(() => {
reject('获取Signature失败')
})
})
const init = {
language: 'zh-Hans',
height: props.height,
menubar: false,
statusbar: false,
plugins: 'table charmap fullscreen lists link code preview quickbars',
toolbar:
'undo redo | fontsizeselect lineheight bold italic underline strikethrough forecolor backcolor | link quickimage image media table | align hangingindent indent outdent numlist bullist | charmap blockquote hr fullscreen | code preview',
// font_formats:
// '微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Times New Roman',
fontsize_formats: '8px 10px 12px 14px 15px 16px 17px 18px 20px 24px',
lineheight_formats: '0.5 1 1.2 1.5 2',
images_upload_handler: ImageUploadHandler,
automatic_uploads: true,
quickbars_insert_toolbar: false,
// style_formats: [{ title: '悬挂缩进', block: 'p', styles: { textIndent: '-2em', paddingLeft: '2em' } }],
content_style: 'img {max-width:100%;}'
}
</script>
<template>
<editor :init="init" v-bind="$attrs" style="width: 100%" />
</template>
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import '@/assets/styles/element/index.scss'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import AppCard from '@/components/base/AppCard.vue'
import AppList from '@/components/base/AppList.vue'
import modules from './modules'
import { permissionDirective } from '@/utils/permission'
const app = createApp(App)
// 注册公共组件
app.component('AppCard', AppCard).component('AppList', AppList)
app.directive('permission', permissionDirective)
// 注册模块
modules({ router })
app.use(createPinia())
app.use(router)
app.use(ElementPlus, { locale: zhCn })
app.mount('#app')
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/bbs',
component: AppLayout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
<script setup lang="ts"></script>
<template></template>
<script setup lang="ts"></script>
<template>
<div class="course-item"></div>
</template>
<!-- 学习 -->
<script setup lang="ts"></script>
<template>学习</template>
<script setup lang="ts">
import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue'
import type { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js'
import { useStorage } from '@vueuse/core'
const options = $ref<VideoJsPlayerOptions>()
const playList = [
{
Status: 'Normal',
StreamType: 'video',
HDRType: 'other',
Size: 2989764,
Definition: 'FD',
Fps: '25',
Specification: 'H264.LD',
ModificationTime: '2022-07-07T09:17:14Z',
Duration: '52.2251',
Bitrate: '457.981',
BitDepth: 8,
Encrypt: 0,
PreprocessStatus: 'UnPreprocess',
Format: 'm3u8',
NarrowBandType: '0',
PlayURL: 'https://media.w3.org/2010/05/sintel/trailer.mp4',
CreationTime: '2022-07-07T09:17:09Z',
Height: 360,
Width: 640,
JobId: '5d679a7ddacd479da554ad825ca6199a'
},
{
Status: 'Normal',
StreamType: 'video',
HDRType: 'other',
Size: 6089320,
Definition: 'LD',
Fps: '25',
Specification: 'H264.SD',
ModificationTime: '2022-07-07T09:17:14Z',
Duration: '52.2251',
Bitrate: '932.78',
BitDepth: 8,
Encrypt: 0,
PreprocessStatus: 'UnPreprocess',
Format: 'm3u8',
NarrowBandType: '0',
PlayURL:
'https://vod.ezijing.com/cd737c13318749668e2d43ba85834637/c4037699664e457a861ef50f978ad639-a072aad43705bb8db65e0eb0befd0353-ld.m3u8?auth_key=1657933158-c408366eb5e14c239e06854a312258ad-0-1afb40ec08bef7139e9cf1df05289118',
CreationTime: '2022-07-07T09:17:09Z',
Height: 540,
Width: 960,
JobId: '7e504265b68748d48c5ee719668cff6d'
},
{
Status: 'Normal',
StreamType: 'video',
HDRType: 'other',
Size: 9788972,
Definition: 'SD',
Fps: '25',
Specification: 'H264.SD',
ModificationTime: '2022-07-07T09:17:16Z',
Duration: '52.2251',
Bitrate: '1499.504',
BitDepth: 8,
Encrypt: 0,
PreprocessStatus: 'UnPreprocess',
Format: 'm3u8',
NarrowBandType: '0',
PlayURL:
'https://vod.ezijing.com/cd737c13318749668e2d43ba85834637/c4037699664e457a861ef50f978ad639-786ec47c6129e95b464fa1a789c60fba-sd.m3u8?auth_key=1657933158-0dd779f01e9f457caf99397d5de6bc6c-0-dc818d66343fdc6510424b1f86bd9e41',
CreationTime: '2022-07-07T09:17:09Z',
Height: 720,
Width: 1280,
JobId: '0ece5ba795584a9eb1ad77b90859b45a'
},
{
Status: 'Normal',
StreamType: 'audio',
Size: 831803,
Definition: 'SQ',
Fps: '0',
Specification: 'Audio',
ModificationTime: '2022-07-07T09:17:14Z',
Duration: '51.9576',
Bitrate: '128.074',
Encrypt: 0,
PreprocessStatus: 'UnPreprocess',
Format: 'mp3',
NarrowBandType: '0',
PlayURL:
'https://vod.ezijing.com/cd737c13318749668e2d43ba85834637/c4037699664e457a861ef50f978ad639-58975598a5cef52ba59f3a18af9c98a8-sq.mp3?auth_key=1657933158-1385695e64154355b775f1cebb924f26-0-fec34e38bbdb9973a4e577b89e123232',
CreationTime: '2022-07-07T09:17:09Z',
Height: 0,
Width: 0,
JobId: 'ba0bb88b427048cab7d4abe8a112833f'
}
]
let src = $ref({ src: '//bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8', type: 'application/x-mpegURL' })
// 跳过片头
const isSkip = useStorage('isSkip', false)
// 连续播放
const isAutoPlayNext = useStorage('isAutoPlayNext', false)
// 播放器ready
let isReady = $ref<boolean>(false)
let videoJsPlayer = $ref<VideoJsPlayer | null>()
const onReady = (player: VideoJsPlayer) => {
videoJsPlayer = player
isReady = true
console.log(videoJsPlayer)
// videoJsPlayer.src('/trailer.mp4')
}
function changeSrc(data: any) {
console.log(data)
src = { src: data.PlayURL, type: 'application/x-mpegURL' }
}
</script>
<template>
<AppVideoPlayer :options="options" :src="src" @ready="onReady"></AppVideoPlayer>
<!-- 设置 -->
<teleport to=".vjs-control-bar" v-if="isReady">
<el-popover trigger="hover" effect="dark" placement="top" :teleported="false">
<template #reference>
<button class="vjs-hd-control vjs-control vjs-button" type="button">
<span class="vjs-icon-hd"></span>
</button>
</template>
<ul>
<li v-for="(item, index) in playList" :key="index" @click="changeSrc(item)">{{ item.Definition }}</li>
</ul>
</el-popover>
<el-popover trigger="hover" effect="dark" placement="top" :teleported="false">
<template #reference>
<button class="vjs-cog-control vjs-control vjs-button" type="button">
<span class="vjs-icon-cog"></span>
</button>
</template>
<ul>
<li>始终跳过片头 <el-switch v-model="isSkip"></el-switch></li>
<li>连续播放 <el-switch v-model="isAutoPlayNext"></el-switch></li>
</ul>
</el-popover>
</teleport>
</template>
<style lang="scss" scoped>
.vjs-icon-hd,
.vjs-icon-cog {
font-size: 1.8em;
}
</style>
<!-- 课程考核 -->
<script setup lang="ts"></script>
<template>课程考核</template>
<!-- 论坛 -->
<script setup lang="ts"></script>
<template>论坛</template>
<!-- 学习 -->
<script setup lang="ts"></script>
<template>学习</template>
<!-- 考试 -->
<script setup lang="ts"></script>
<template>考试</template>
<!-- 直播 -->
<script setup lang="ts"></script>
<template>直播</template>
<!-- 大作业 -->
<script setup lang="ts"></script>
<template>大作业</template>
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/course',
component: AppLayout,
children: [
{
path: '',
component: () => import('./views/CourseIndex.vue'),
children: [{ path: ':id', component: () => import('./views/CourseView.vue'), props: true }]
},
{ path: 'player', component: () => import('./views/CoursePlayer.vue'), props: true },
{ path: 'exam', component: () => import('./views/CourseExam.vue'), props: true }
]
}
]
<script setup lang="ts">
const CoursePlayerVideo = defineAsyncComponent(() => import('../components/CoursePlayerVideo.vue'))
const CoursePlayerChapter = defineAsyncComponent(() => import('../components/CoursePlayerChapter.vue'))
</script>
<template>
<div class="course-player">
<div class="course-player-main">
<CoursePlayerVideo />
<el-tabs>
<el-tab-pane label="课件"> </el-tab-pane>
<el-tab-pane label="教案" lazy> </el-tab-pane>
<el-tab-pane label="作业" lazy> </el-tab-pane>
<el-tab-pane label="资料" lazy> </el-tab-pane>
<el-tab-pane label="考试/测验" lazy></el-tab-pane>
<el-tab-pane label="直播" lazy></el-tab-pane>
</el-tabs>
</div>
<div class="course-player-aside">
<CoursePlayerChapter />
</div>
</div>
</template>
<style lang="scss" scoped>
.course-player {
display: flex;
}
.course-player-main {
flex: 1;
}
</style>
<script setup lang="ts"></script>
<template>
<AppCard>
<section class="course">
<div class="course-left">
<router-link to="/course/123">123</router-link>
</div>
<div class="course-right"><router-view :key="$route.fullPath"></router-view></div>
</section>
</AppCard>
</template>
<style lang="scss">
.course {
display: flex;
}
.course-left {
width: 400px;
overflow-y: auto;
}
.course-right {
flex: 1;
}
</style>
<script setup lang="ts">
const CoursePlayerVideo = defineAsyncComponent(() => import('../components/CoursePlayerVideo.vue'))
const CoursePlayerChapter = defineAsyncComponent(() => import('../components/CoursePlayerChapter.vue'))
</script>
<template>
<div class="course-player">
<div class="course-player-main">
<CoursePlayerVideo />
<el-tabs>
<el-tab-pane label="课件"> </el-tab-pane>
<el-tab-pane label="教案" lazy> </el-tab-pane>
<el-tab-pane label="作业" lazy> </el-tab-pane>
<el-tab-pane label="资料" lazy> </el-tab-pane>
<el-tab-pane label="考试/测验" lazy></el-tab-pane>
<el-tab-pane label="直播" lazy></el-tab-pane>
</el-tabs>
</div>
<div class="course-player-aside">
<CoursePlayerChapter />
</div>
</div>
</template>
<style lang="scss" scoped>
.course-player {
display: flex;
}
.course-player-main {
flex: 1;
}
</style>
<script setup lang="ts">
const CourseViewChapter = defineAsyncComponent(() => import('../components/CourseViewChapter.vue'))
const CourseViewBBS = defineAsyncComponent(() => import('../components/CourseViewBBS.vue'))
const CourseViewWork = defineAsyncComponent(() => import('../components/CourseViewWork.vue'))
const CourseViewExam = defineAsyncComponent(() => import('../components/CourseViewExam.vue'))
const CourseViewAssess = defineAsyncComponent(() => import('../components/CourseViewAssess.vue'))
const CourseViewLive = defineAsyncComponent(() => import('../components/CourseViewLive.vue'))
defineProps<{ id: string }>()
</script>
<template>
<section class="course-view-header">
<h1>Design Thinking for Managers 设计思维与管理创新</h1>
<ul>
<li>必修课</li>
<li>3学分</li>
<li>第二学期</li>
</ul>
</section>
<el-tabs>
<el-tab-pane label="学习">
<CourseViewChapter />
</el-tab-pane>
<el-tab-pane label="论坛" lazy>
<CourseViewBBS />
</el-tab-pane>
<el-tab-pane label="大作业" lazy>
<CourseViewWork />
</el-tab-pane>
<el-tab-pane label="考试" lazy>
<CourseViewExam />
</el-tab-pane>
<el-tab-pane label="课程考核" lazy>
<CourseViewAssess />
</el-tab-pane>
<el-tab-pane label="直播" lazy>
<CourseViewLive />
</el-tab-pane>
</el-tabs>
</template>
const routes = [
{
path: '/401',
component: () => import('./views/401.vue')
}
]
export { routes }
<script setup lang="ts">
const loginUrl = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(location.origin)}`
</script>
<template>
<div class="container">
<svg width="251" height="294">
<g fill="none" fill-rule="evenodd">
<path
d="M0 129.023v-2.084C0 58.364 55.591 2.774 124.165 2.774h2.085c68.574 0 124.165 55.59 124.165 124.165v2.084c0 68.575-55.59 124.166-124.165 124.166h-2.085C55.591 253.189 0 197.598 0 129.023"
fill="#E4EBF7"
></path>
<path d="M41.417 132.92a8.231 8.231 0 1 1-16.38-1.65 8.231 8.231 0 0 1 16.38 1.65" fill="#FFF"></path>
<path d="M38.652 136.36l10.425 5.91M49.989 148.505l-12.58 10.73" stroke="#FFF" stroke-width="2"></path>
<path
d="M41.536 161.28a5.636 5.636 0 1 1-11.216-1.13 5.636 5.636 0 0 1 11.216 1.13M59.154 145.261a5.677 5.677 0 1 1-11.297-1.138 5.677 5.677 0 0 1 11.297 1.138M100.36 29.516l29.66-.013a4.562 4.562 0 1 0-.004-9.126l-29.66.013a4.563 4.563 0 0 0 .005 9.126M111.705 47.754l29.659-.013a4.563 4.563 0 1 0-.004-9.126l-29.66.013a4.563 4.563 0 1 0 .005 9.126"
fill="#FFF"
></path>
<path
d="M114.066 29.503V29.5l15.698-.007a4.563 4.563 0 1 0 .004 9.126l-15.698.007v-.002a4.562 4.562 0 0 0-.004-9.122M185.405 137.723c-.55 5.455-5.418 9.432-10.873 8.882-5.456-.55-9.432-5.418-8.882-10.873.55-5.455 5.418-9.432 10.873-8.882 5.455.55 9.432 5.418 8.882 10.873"
fill="#FFF"
></path>
<path d="M180.17 143.772l12.572 7.129M193.841 158.42L178.67 171.36" stroke="#FFF" stroke-width="2"></path>
<path
d="M185.55 171.926a6.798 6.798 0 1 1-13.528-1.363 6.798 6.798 0 0 1 13.527 1.363M204.12 155.285a6.848 6.848 0 1 1-13.627-1.375 6.848 6.848 0 0 1 13.626 1.375"
fill="#FFF"
></path>
<path
d="M152.988 194.074a2.21 2.21 0 1 1-4.42 0 2.21 2.21 0 0 1 4.42 0zM225.931 118.217a2.21 2.21 0 1 1-4.421 0 2.21 2.21 0 0 1 4.421 0zM217.09 153.051a2.21 2.21 0 1 1-4.421 0 2.21 2.21 0 0 1 4.42 0zM177.84 109.842a2.21 2.21 0 1 1-4.422 0 2.21 2.21 0 0 1 4.421 0zM196.114 94.454a2.21 2.21 0 1 1-4.421 0 2.21 2.21 0 0 1 4.421 0zM202.844 182.523a2.21 2.21 0 1 1-4.42 0 2.21 2.21 0 0 1 4.42 0z"
stroke="#FFF"
stroke-width="2"
></path>
<path
stroke="#FFF"
stroke-width="2"
d="M215.125 155.262l-1.902 20.075-10.87 5.958M174.601 176.636l-6.322 9.761H156.98l-4.484 6.449M175.874 127.28V111.56M221.51 119.404l-12.77 7.859-15.228-7.86V96.668"
></path>
<path
d="M180.68 29.32C180.68 13.128 193.806 0 210 0c16.193 0 29.32 13.127 29.32 29.32 0 16.194-13.127 29.322-29.32 29.322-16.193 0-29.32-13.128-29.32-29.321"
fill="#A26EF4"
></path>
<path
d="M221.45 41.706l-21.563-.125a1.744 1.744 0 0 1-1.734-1.754l.071-12.23a1.744 1.744 0 0 1 1.754-1.734l21.562.125c.964.006 1.74.791 1.735 1.755l-.071 12.229a1.744 1.744 0 0 1-1.754 1.734"
fill="#FFF"
></path>
<path
d="M215.106 29.192c-.015 2.577-2.049 4.654-4.543 4.64-2.494-.014-4.504-2.115-4.489-4.693l.04-6.925c.016-2.577 2.05-4.654 4.543-4.64 2.494.015 4.504 2.116 4.49 4.693l-.04 6.925zm-4.53-14.074a6.877 6.877 0 0 0-6.916 6.837l-.043 7.368a6.877 6.877 0 0 0 13.754.08l.042-7.368a6.878 6.878 0 0 0-6.837-6.917zM167.566 68.367h-3.93a4.73 4.73 0 0 1-4.717-4.717 4.73 4.73 0 0 1 4.717-4.717h3.93a4.73 4.73 0 0 1 4.717 4.717 4.73 4.73 0 0 1-4.717 4.717"
fill="#FFF"
></path>
<path
d="M168.214 248.838a6.611 6.611 0 0 1-6.61-6.611v-66.108a6.611 6.611 0 0 1 13.221 0v66.108a6.611 6.611 0 0 1-6.61 6.61"
fill="#5BA02E"
></path>
<path
d="M176.147 248.176a6.611 6.611 0 0 1-6.61-6.61v-33.054a6.611 6.611 0 1 1 13.221 0v33.053a6.611 6.611 0 0 1-6.61 6.611"
fill="#92C110"
></path>
<path
d="M185.994 293.89h-27.376a3.17 3.17 0 0 1-3.17-3.17v-45.887a3.17 3.17 0 0 1 3.17-3.17h27.376a3.17 3.17 0 0 1 3.17 3.17v45.886a3.17 3.17 0 0 1-3.17 3.17"
fill="#F2D7AD"
></path>
<path
d="M81.972 147.673s6.377-.927 17.566-1.28c11.729-.371 17.57 1.086 17.57 1.086s3.697-3.855.968-8.424c1.278-12.077 5.982-32.827.335-48.273-1.116-1.339-3.743-1.512-7.536-.62-1.337.315-7.147-.149-7.983-.1l-15.311-.347s-3.487-.17-8.035-.508c-1.512-.113-4.227-1.683-5.458-.338-.406.443-2.425 5.669-1.97 16.077l8.635 35.642s-3.141 3.61 1.219 7.085"
fill="#FFF"
></path>
<path
d="M75.768 73.325l-.9-6.397 11.982-6.52s7.302-.118 8.038 1.205c.737 1.324-5.616.993-5.616.993s-1.836 1.388-2.615 2.5c-1.654 2.363-.986 6.471-8.318 5.986-1.708.284-2.57 2.233-2.57 2.233"
fill="#FFC6A0"
></path>
<path
d="M52.44 77.672s14.217 9.406 24.973 14.444c1.061.497-2.094 16.183-11.892 11.811-7.436-3.318-20.162-8.44-21.482-14.496-.71-3.258 2.543-7.643 8.401-11.76M141.862 80.113s-6.693 2.999-13.844 6.876c-3.894 2.11-10.137 4.704-12.33 7.988-6.224 9.314 3.536 11.22 12.947 7.503 6.71-2.651 28.999-12.127 13.227-22.367"
fill="#FFB594"
></path>
<path
d="M76.166 66.36l3.06 3.881s-2.783 2.67-6.31 5.747c-7.103 6.195-12.803 14.296-15.995 16.44-3.966 2.662-9.754 3.314-12.177-.118-3.553-5.032.464-14.628 31.422-25.95"
fill="#FFC6A0"
></path>
<path
d="M64.674 85.116s-2.34 8.413-8.912 14.447c.652.548 18.586 10.51 22.144 10.056 5.238-.669 6.417-18.968 1.145-20.531-.702-.208-5.901-1.286-8.853-2.167-.87-.26-1.611-1.71-3.545-.936l-1.98-.869zM128.362 85.826s5.318 1.956 7.325 13.734c-.546.274-17.55 12.35-21.829 7.805-6.534-6.94-.766-17.393 4.275-18.61 4.646-1.121 5.03-1.37 10.23-2.929"
fill="#FFF"
></path>
<path
d="M78.18 94.656s.911 7.41-4.914 13.078"
stroke="#E4EBF7"
stroke-width="1.051"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M87.397 94.68s3.124 2.572 10.263 2.572c7.14 0 9.074-3.437 9.074-3.437"
stroke="#E4EBF7"
stroke-width=".932"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M117.184 68.639l-6.781-6.177s-5.355-4.314-9.223-.893c-3.867 3.422 4.463 2.083 5.653 4.165 1.19 2.082.848 1.143-2.083.446-5.603-1.331-2.082.893 2.975 5.355 2.091 1.845 6.992.955 6.992.955l2.467-3.851z"
fill="#FFC6A0"
></path>
<path
d="M105.282 91.315l-.297-10.937-15.918-.027-.53 10.45c-.026.403.17.788.515.999 2.049 1.251 9.387 5.093 15.799.424.287-.21.443-.554.431-.91"
fill="#FFB594"
></path>
<path
d="M107.573 74.24c.817-1.147.982-9.118 1.015-11.928a1.046 1.046 0 0 0-.965-1.055l-4.62-.365c-7.71-1.044-17.071.624-18.253 6.346-5.482 5.813-.421 13.244-.421 13.244s1.963 3.566 4.305 6.791c.756 1.041.398-3.731 3.04-5.929 5.524-4.594 15.899-7.103 15.899-7.103"
fill="#5C2552"
></path>
<path
d="M88.426 83.206s2.685 6.202 11.602 6.522c7.82.28 8.973-7.008 7.434-17.505l-.909-5.483c-6.118-2.897-15.478.54-15.478.54s-.576 2.044-.19 5.504c-2.276 2.066-1.824 5.618-1.824 5.618s-.905-1.922-1.98-2.321c-.86-.32-1.897.089-2.322 1.98-1.04 4.632 3.667 5.145 3.667 5.145"
fill="#FFC6A0"
></path>
<path
stroke="#DB836E"
stroke-width="1.145"
stroke-linecap="round"
stroke-linejoin="round"
d="M100.843 77.099l1.701-.928-1.015-4.324.674-1.406"
></path>
<path
d="M105.546 74.092c-.022.713-.452 1.279-.96 1.263-.51-.016-.904-.607-.882-1.32.021-.713.452-1.278.96-1.263.51.016.904.607.882 1.32M97.592 74.349c-.022.713-.452 1.278-.961 1.263-.509-.016-.904-.607-.882-1.32.022-.713.452-1.279.961-1.263.51.016.904.606.882 1.32"
fill="#552950"
></path>
<path
d="M91.132 86.786s5.269 4.957 12.679 2.327"
stroke="#DB836E"
stroke-width="1.145"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M99.776 81.903s-3.592.232-1.44-2.79c1.59-1.496 4.897-.46 4.897-.46s1.156 3.906-3.457 3.25"
fill="#DB836E"
></path>
<path
d="M102.88 70.6s2.483.84 3.402.715M93.883 71.975s2.492-1.144 4.778-1.073"
stroke="#5C2552"
stroke-width="1.526"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M86.32 77.374s.961.879 1.458 2.106c-.377.48-1.033 1.152-.236 1.809M99.337 83.719s1.911.151 2.509-.254"
stroke="#DB836E"
stroke-width="1.145"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M87.782 115.821l15.73-3.012M100.165 115.821l10.04-2.008"
stroke="#E4EBF7"
stroke-width="1.051"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M66.508 86.763s-1.598 8.83-6.697 14.078"
stroke="#E4EBF7"
stroke-width="1.114"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M128.31 87.934s3.013 4.121 4.06 11.785"
stroke="#E4EBF7"
stroke-width="1.051"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M64.09 84.816s-6.03 9.912-13.607 9.903"
stroke="#DB836E"
stroke-width=".795"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M112.366 65.909l-.142 5.32s5.993 4.472 11.945 9.202c4.482 3.562 8.888 7.455 10.985 8.662 4.804 2.766 8.9 3.355 11.076 1.808 4.071-2.894 4.373-9.878-8.136-15.263-4.271-1.838-16.144-6.36-25.728-9.73"
fill="#FFC6A0"
></path>
<path
d="M130.532 85.488s4.588 5.757 11.619 6.214"
stroke="#DB836E"
stroke-width=".75"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M121.708 105.73s-.393 8.564-1.34 13.612"
stroke="#E4EBF7"
stroke-width="1.051"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M115.784 161.512s-3.57-1.488-2.678-7.14"
stroke="#648BD8"
stroke-width="1.051"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M101.52 290.246s4.326 2.057 7.408 1.03c2.842-.948 4.564.673 7.132 1.186 2.57.514 6.925 1.108 11.772-1.269-.104-5.551-6.939-4.01-12.048-6.763-2.582-1.39-3.812-4.757-3.625-8.863h-9.471s-1.402 10.596-1.169 14.68"
fill="#CBD1D1"
></path>
<path
d="M101.496 290.073s2.447 1.281 6.809.658c3.081-.44 3.74.485 7.479 1.039 3.739.554 10.802-.07 11.91-.9.415 1.108-.347 2.077-.347 2.077s-1.523.608-4.847.831c-2.045.137-5.843.293-7.663-.507-1.8-1.385-5.286-1.917-5.77-.243-3.947.958-7.41-.288-7.41-.288l-.16-2.667z"
fill="#2B0849"
></path>
<path d="M108.824 276.19h3.116s-.103 6.751 4.57 8.62c-4.673.624-8.62-2.32-7.686-8.62" fill="#A4AABA"></path>
<path
d="M57.65 272.52s-2.122 7.47-4.518 12.396c-1.811 3.724-4.255 7.548 5.505 7.548 6.698 0 9.02-.483 7.479-6.648-1.541-6.164.268-13.296.268-13.296H57.65z"
fill="#CBD1D1"
></path>
<path
d="M51.54 290.04s2.111 1.178 6.682 1.178c6.128 0 8.31-1.662 8.31-1.662s.605 1.122-.624 2.18c-1 .862-3.624 1.603-7.444 1.559-4.177-.049-5.876-.57-6.786-1.177-.831-.554-.692-1.593-.138-2.078"
fill="#2B0849"
></path>
<path
d="M58.533 274.438s.034 1.529-.315 2.95c-.352 1.431-1.087 3.127-1.139 4.17-.058 1.16 4.57 1.592 5.194.035.623-1.559 1.303-6.475 1.927-7.306.622-.831-4.94-2.135-5.667.15"
fill="#A4AABA"
></path>
<path
d="M100.885 277.015l13.306.092s1.291-54.228 1.843-64.056c.552-9.828 3.756-43.13.997-62.788l-12.48-.64-22.725.776s-.433 3.944-1.19 9.921c-.062.493-.677.838-.744 1.358-.075.582.42 1.347.318 1.956-2.35 14.003-6.343 32.926-8.697 46.425-.116.663-1.227 1.004-1.45 2.677-.04.3.21 1.516.112 1.785-6.836 18.643-10.89 47.584-14.2 61.551l14.528-.014s2.185-8.524 4.008-16.878c2.796-12.817 22.987-84.553 22.987-84.553l3-.517 1.037 46.1s-.223 1.228.334 2.008c.558.782-.556 1.117-.39 2.233l.39 1.784s-.446 7.14-.892 11.826c-.446 4.685-.092 38.954-.092 38.954"
fill="#7BB2F9"
></path>
<path
d="M77.438 220.434c1.146.094 4.016-2.008 6.916-4.91M107.55 223.931s2.758-1.103 6.069-3.862"
stroke="#648BD8"
stroke-width="1.051"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M108.459 220.905s2.759-1.104 6.07-3.863"
stroke="#648BD8"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M76.099 223.557s2.608-.587 6.47-3.346M87.33 150.82c-.27 3.088.297 8.478-4.315 9.073M104.829 149.075s.11 13.936-1.286 14.983c-2.207 1.655-2.975 1.934-2.975 1.934M101.014 149.63s.035 12.81-1.19 24.245M94.93 174.965s7.174-1.655 9.38-1.655M75.671 204.754c-.316 1.55-.64 3.067-.973 4.535 0 0-1.45 1.822-1.003 3.756.446 1.934-.943 2.034-4.96 15.273-1.686 5.559-4.464 18.49-6.313 27.447-.078.38-4.018 18.06-4.093 18.423M77.043 196.743a313.269 313.269 0 0 1-.877 4.729M83.908 151.414l-1.19 10.413s-1.091.148-.496 2.23c.111 1.34-2.66 15.692-5.153 30.267M57.58 272.94h13.238"
stroke="#648BD8"
stroke-width="1.051"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M117.377 147.423s-16.955-3.087-35.7.199c.157 2.501-.002 4.128-.002 4.128s14.607-2.802 35.476-.31c.251-2.342.226-4.017.226-4.017"
fill="#192064"
></path>
<path
d="M107.511 150.353l.004-4.885a.807.807 0 0 0-.774-.81c-2.428-.092-5.04-.108-7.795-.014a.814.814 0 0 0-.784.81l-.003 4.88c0 .456.371.82.827.808a140.76 140.76 0 0 1 7.688.017.81.81 0 0 0 .837-.806"
fill="#FFF"
></path>
<path
d="M106.402 149.426l.002-3.06a.64.64 0 0 0-.616-.643 94.135 94.135 0 0 0-5.834-.009.647.647 0 0 0-.626.643l-.001 3.056c0 .36.291.648.651.64 1.78-.04 3.708-.041 5.762.012.36.009.662-.279.662-.64"
fill="#192064"
></path>
<path
d="M101.485 273.933h12.272M102.652 269.075c.006 3.368.04 5.759.11 6.47M102.667 263.125c-.009 1.53-.015 2.98-.016 4.313M102.204 174.024l.893 44.402s.669 1.561-.224 2.677c-.892 1.116 2.455.67.893 2.231-1.562 1.562.893 1.116 0 3.347-.592 1.48-.988 20.987-1.09 34.956"
stroke="#648BD8"
stroke-width="1.051"
stroke-linecap="round"
stroke-linejoin="round"
></path>
</g>
</svg>
<p class="tips">很抱歉,你暂时无权限访问...</p>
<el-space>
<router-link to="/"><el-button round type="default">返回首页</el-button></router-link>
<a :href="loginUrl"><el-button round type="primary">重新登录</el-button></a>
</el-space>
</div>
</template>
<style lang="scss" scoped>
.container {
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.tips {
margin: 30px;
font-size: 24px;
color: #313131;
}
</style>
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/favorites',
component: AppLayout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
<script setup lang="ts">
const tabValue = $ref('')
const tabs = $ref([
{ label: '全部', value: '' },
{ label: '视频', value: '1' },
{ label: '课件', value: '2' },
{ label: '教案', value: '3' },
{ label: '作业', value: '4' },
{ label: '帖子', value: '5' },
{ label: '其它资料', value: '6' }
])
</script>
<template>
<AppCard>
<el-tabs v-model="tabValue">
<el-tab-pane v-for="item in tabs" :label="item.label" :name="item.value" :key="item.value"></el-tab-pane>
</el-tabs>
</AppCard>
</template>
import type { Router, RouteRecordRaw } from 'vue-router'
export default function ({ router }: { router: Router }) {
const modules: Array<{ routes: Array<RouteRecordRaw> }> = Object.values(import.meta.globEager('./**/index.ts'))
modules.forEach(({ routes = [] }) => {
// 注册路由
routes.forEach(route => {
router.addRoute(route)
})
})
}
import httpRequest from '@/utils/axios'
// 修改用户资料
export function updateUser(data: { name: string; avatar: string; sex: string }) {
return httpRequest.post('/api/usercenter/v2/frontend/user/update-user', data)
}
// 通过cookie修改密码
export function updatePassword(data: { old_password: string; password: string; password_r: string }) {
return httpRequest.post('/api/usercenter/v2/frontend/user/change-pwd-by-cookie', data)
}
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { updatePassword } from '../api'
const formRef = $ref<FormInstance>()
const form = reactive({
old_password: '',
password: '',
password_r: ''
})
const validatePass2 = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error('请再次输入新密码'))
} else if (value !== form.password) {
callback(new Error('两次密码输入不一致'))
} else {
callback()
}
}
const rules = ref<FormRules>({
old_password: [{ required: true, message: '请输入旧密码', trigger: 'blur' }],
password: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, message: '密码长度至少6位字符', trigger: 'blur' }
],
password_r: [{ validator: validatePass2, trigger: 'blur' }]
})
// 提交
function handleSubmit() {
formRef?.validate().then(update)
}
// 修改
const update = () => {
updatePassword(form).then(() => {
// 重置表单
formRef?.resetFields()
ElMessage({ message: '修改成功', type: 'success' })
})
}
</script>
<template>
<el-form ref="formRef" :model="form" :rules="rules" hide-required-asterisk>
<el-form-item label="旧密码" prop="old_password">
<el-input type="password" v-model="form.old_password" />
</el-form-item>
<el-form-item label="新密码" prop="password">
<el-input type="password" v-model="form.password" />
</el-form-item>
<el-form-item label="重复新密码" prop="password_r">
<el-input type="password" v-model="form.password_r" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">保存</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts"></script>
<template>
<el-form> </el-form>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import AppUpload from '@/components/base/AppUpload.vue'
import type { FormInstance, FormRules } from 'element-plus'
import { updateUser } from '../api'
const formRef = $ref<FormInstance>()
const form = reactive({
name: '',
avatar: '',
sex: ''
})
const rules = ref<FormRules>({
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
mobile: [{ required: true, message: '请输入手机号', trigger: 'blur' }]
})
// 提交
function handleSubmit() {
formRef?.validate().then(update)
}
// 修改
const update = () => {
updateUser(form).then(() => {
ElMessage({ message: '修改成功', type: 'success' })
})
}
</script>
<template>
<el-form ref="formRef" :model="form" :rules="rules" hide-required-asterisk>
<el-form-item label="昵称" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="头像" prop="avatar">
<AppUpload v-model="form.avatar"></AppUpload>
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-radio-group v-model="form.sex" class="ml-4">
<el-radio label="1" size="large"></el-radio>
<el-radio label="2" size="large"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">保存</el-button>
</el-form-item>
</el-form>
</template>
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/settings',
component: AppLayout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
<script setup lang="ts">
const User = defineAsyncComponent(() => import('../components/User.vue'))
const Password = defineAsyncComponent(() => import('../components/Password.vue'))
const Suggestions = defineAsyncComponent(() => import('../components/Suggestions.vue'))
</script>
<template>
<AppCard>
<el-tabs>
<el-tab-pane label="个人资料">
<User />
</el-tab-pane>
<el-tab-pane label="账号密码" lazy>
<Password />
</el-tab-pane>
<el-tab-pane label="投诉建议" lazy>
<Suggestions />
</el-tab-pane>
</el-tabs>
</AppCard>
</template>
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'
const router = createRouter({
history: createWebHistory(),
routes: [{ path: '/:pathMatch(.*)*', redirect: '/course' }]
})
router.beforeEach(async (to, from, next) => {
const whiteList = ['/401']
const user = useUserStore()
if (!user.isLogin && !whiteList.includes(to.path)) {
try {
await user.getUser()
} catch (e) {
console.error(e)
}
user.isLogin ? next() : next('/401')
return
}
next()
})
export default router
import { defineStore } from 'pinia'
import { getMapList } from '@/api/base'
interface State {
mapList: IMapState[]
}
interface IMapState {
id: string
key: string
name: string
remark: string
values: IValuesList[]
}
interface IValuesList {
data_dictionary_id: string
id: string
label: string
remark: string
sort: string
value: string
}
export const useMapStore = defineStore({
id: 'map',
state: (): State => {
return {
mapList: []
}
},
getters: {
getMapValuesByKey: state => {
return (key: string) => state.mapList.find(map => map.key === key)?.values || []
}
},
actions: {
async getMapList() {
const res = await getMapList()
this.mapList = res.data || []
}
}
})
useMapStore().getMapList()
import { defineStore } from 'pinia'
import { getUser, logout } from '@/api/base'
import type { UserType, ProjectType, OrganizationType, RoleType, PermissionType } from '@/types'
interface State {
user: UserType | null
project: ProjectType | null
organization: OrganizationType | null
roles: RoleType[]
permissions: PermissionType[]
}
export const useUserStore = defineStore({
id: 'user',
state: (): State => ({
user: null,
organization: null,
project: null,
roles: [],
permissions: []
}),
getters: {
isLogin: state => !!state.user
},
actions: {
async getUser() {
const res = await getUser()
const { info } = res.data
const { organization, project, roles, permissions } = res.data.permissions
this.user = info
this.organization = organization
this.project = project
this.roles = roles
this.permissions = permissions
},
async logout() {
await logout()
this.user = null
}
}
})
import type { Component } from 'vue'
export interface IMenuItem {
tag?: string
name: string
path: string
icon?: Component
children?: IMenuItem[]
}
// 用户信息
export interface UserType {
id: string
mobile: string
name: string
email: string
username: string
avatar: string
}
// 项目信息
export interface ProjectType {
id: string
tab: string
name: string
}
// 机构信息
export interface OrganizationType {
id: string
name: string
contact_name: string
contact_information: string
validity_date: string
is_valid: 1 | 2
}
// 角色信息
export interface RoleType {
id: string
name: string
desc: string
}
// 权限信息
export interface PermissionType {
desc: string
effect_uris: string
id: string
name: string
parent_id: string
system_tag: number
type: number
tag: string
}
// 课程信息
export interface CourseType {
auth_view: boolean
belong_operator: string
belong_operator_name: string
big: string
classification: string
classification_name: string
cover: string
created_operator: string
created_operator_name: string
created_time: string
credit: string
department_public: string
elective_type: string
elective_type_name: string
id: string
name: string
online_type: string
online_type_name: string
organ_id: string
organ_id_name: string
platform_public: string
project_id: string
project_id_name: string
small: string
status: string
status_name: string
updated_operator: string
updated_operator_name: string
updated_time: string
}
import axios from 'axios'
import qs from 'qs'
import { ElMessage } from 'element-plus'
import router from '@/router'
const httpRequest = axios.create({
timeout: 60000,
withCredentials: true,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
// 请求拦截
httpRequest.interceptors.request.use(
function (config) {
if (config.headers?.['Content-Type'] === 'application/x-www-form-urlencoded') {
config.data = qs.stringify(config.data, { skipNulls: true })
}
if (config.headers?.['Content-Type'] === 'multipart/form-data') {
const formData = new window.FormData()
for (const key in config.data) {
formData.append(key, config.data[key])
}
config.data = formData
}
return config
},
function (error) {
return Promise.reject(error)
}
)
// 响应拦截
httpRequest.interceptors.response.use(
function (response) {
const { data } = response
// 未登录
if (data.code === 4001) {
location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(location.href)}`
return Promise.reject(data)
}
if (Object.hasOwn(data, 'code') && data.code !== 0) {
ElMessage.error(data.message || data.msg)
return Promise.reject(data)
}
return data
},
function (error) {
if (error.response) {
const { status, message } = error.response.data
// 未登录
if (status === 403) {
location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(location.href)}`
} else if (status === 402) {
// 未授权
router.push('/401')
} else {
ElMessage.error(message)
console.error(`${status}: ${message}`)
}
} else {
console.log(error)
}
return Promise.reject(error.response || error)
}
)
export default httpRequest
// json to array
export const json2Array = function (data, isValueToNumber = true) {
return Object.keys(data).map(value => ({ label: data[value], value: isValueToNumber ? parseInt(value) : value }))
}
// 组卷模式
export const paperType = {
1: '选题组卷',
2: '自动组卷',
3: '自由组卷'
}
// 组卷模式列表
export const paperTypeList = json2Array(paperType)
// 试题类型
export const questionType = {
1: '单选题',
2: '多选题',
3: '问答题',
5: '案例题',
6: '判断题',
7: '实操题',
8: '情景题'
}
// 试题类型列表
export const questionTypeList = json2Array(questionType, false)
// 试题难度
export const questionDifficulty = {
1: '易',
2: '中',
3: '难',
0: '无'
}
// 试题难度列表
export const questionDifficultyList = json2Array(questionDifficulty, false)
import { useUserStore } from '@/stores/user'
import type { DirectiveBinding } from 'vue'
// 判断是否有权限
export function checkPermission(value: string | string[]): boolean {
const userStore = useUserStore()
const permissions = userStore.permissions
if (Array.isArray(value)) {
return permissions.some(item => value.includes(item.tag))
} else {
return !!permissions.find(item => item.tag === value)
}
}
// 权限指令
export function permissionDirective(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding
if (!value) return
if (!checkPermission(value)) {
el.parentNode && el.parentNode.removeChild(el)
}
}
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["auto-imports.d.ts", "env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"allowJs": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"types": ["element-plus/global"]
},
"references": [
{
"path": "./tsconfig.config.json"
}
]
}
import fs from 'fs'
import path from 'path'
import { fileURLToPath, URL } from 'url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
base: mode === 'prod' ? 'https://webapp-pub.ezijing.com/website/prod/center-resource/' : '/',
plugins: [
vue({ reactivityTransform: true }),
AutoImport({
imports: ['vue', 'vue/macros', 'vue-router'],
dts: true,
eslintrc: { enabled: true }
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
open: true,
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: {
'/api/qbs': {
target: 'https://question-api.ezijing.com',
changeOrigin: true,
rewrite: path => path.replace(/^\/api\/qbs/, '')
},
'/api': 'https://resource-center.ezijing.com'
}
}
}))
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论