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

Initial commit

上级
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
/* 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'],
env: {
'vue/setup-compiler-macros': true
},
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": ["johnsoncodehk.volar", "johnsoncodehk.vscode-typescript-vue-plugin"]
}
# admin-prp
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=johnsoncodehk.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.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=johnsoncodehk.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
```
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/admin-prp')
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_LOGIN_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
-----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>PRP私享星球管理系统</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
{
"name": "admin-prp",
"version": "0.0.0",
"scripts": {
"dev": "vite --mode dev",
"build": "vue-tsc --noEmit && vite build --mode prod && npm run deploy",
"build:test": "vue-tsc --noEmit && vite build --test prod",
"build:pre": "vue-tsc --noEmit && vite build --pre prod",
"preview": "vite preview --port 5050",
"typecheck": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"deploy": "node ./deploy.js"
},
"dependencies": {
"@element-plus/icons-vue": "^1.1.4",
"axios": "^0.26.1",
"blueimp-md5": "^2.19.0",
"element-plus": "^2.1.10",
"pinia": "^2.0.13",
"qs": "^6.10.3",
"sass": "^1.50.0",
"vue": "^3.2.33",
"vue-router": "^4.0.14",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.1.3",
"@types/blueimp-md5": "^2.18.0",
"@types/node": "^17.0.24",
"@vitejs/plugin-vue": "^2.3.1",
"@vue/eslint-config-typescript": "^10.0.0",
"@vue/tsconfig": "^0.1.3",
"ali-oss": "^6.17.1",
"chalk": "^5.0.1",
"eslint": "^8.13.0",
"eslint-plugin-vue": "^8.6.0",
"typescript": "~4.6.3",
"vite": "^2.9.5",
"vite-plugin-checker": "^0.4.6",
"vue-tsc": "^0.34.7"
}
}
差异被折叠。
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>
<style>
@import '@/assets/base.css';
</style>
import httpRequest from '@/utils/axios'
// 获取用户信息
export function getUser() {
return httpRequest.get('/api/passport/account/get-user-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(() => {
return data.url
})
}
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;
}
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: 8px;
padding: 32px;
}
.app-card + .app-card {
margin-top: 20px;
}
.app-card-hd {
display: flex;
}
.app-card-hd__title {
flex: 1;
font-size: 18px;
font-weight: 700;
margin-bottom: 16px;
}
</style>
差异被折叠。
<script lang="ts" setup>
import { ref, withDefaults, computed, watch } from 'vue'
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 | UploadUserFile[]; 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.map(item => ({ ...item })) : []
}
)
const showFileList = computed(() => {
return Array.isArray(props.modelValue)
})
// 上传之前
const handleBeforeUpload: UploadProps['beforeUpload'] = async file => {
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}`
}
}
// 上传成功
const handleSuccess: UploadProps['onSuccess'] = (response, file) => {
const value = showFileList.value
? [...props.modelValue, { name: file.name, url: uploadData.value.url }]
: uploadData.value.url
console.log(value)
emit('update:modelValue', value)
}
// 上传限制
const handleExceed: UploadProps['onExceed'] = () => {
ElMessage.warning('文件超出个数限制')
}
// 删除
const handleRemove: UploadProps['onRemove'] = (file, files) => {
console.log(file, files)
// const value = showFileList.value ? props.modelValue.filter(item => item.url !== file.url) : ''
}
// 预览
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"
>
<slot>
<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">点击上传</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>
</slot>
<template #tip>
<div class="el-upload__tip"><slot name="tip"></slot></div>
</template>
</el-upload>
</template>
<style lang="scss">
.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 setup lang="ts">
import { computed } from 'vue'
import type { Component } from 'vue'
import { useRoute } from 'vue-router'
import { User, Picture, Files, VideoCamera, Notebook, DishDot, QuestionFilled, Stamp } from '@element-plus/icons-vue'
const route = useRoute()
interface IMenuItem {
name: string
path: string
icon: Component
children?: IMenuItem[]
}
const menuList: IMenuItem[] = [
{ name: '学员管理', path: '/student', icon: User },
{ name: '广告管理', path: '/banner', icon: Picture },
{ name: '资料管理', path: '/doc', icon: Files },
{ name: '视频管理', path: '/video', icon: VideoCamera },
{ name: '课程管理', path: '/course', icon: Notebook },
{ name: '团队管理', path: '/team', icon: DishDot },
{ name: '问答管理', path: '/qa', icon: QuestionFilled },
{ name: '审核管理', path: '/audit', icon: Stamp }
]
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 : '/'
})
</script>
<template>
<aside class="app-aside">
<nav class="nav">
<el-menu :default-active="defaultActive" :router="true">
<template v-for="item in menuList" :key="item.path">
<el-sub-menu :index="item.path" 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">
{{ subitem.name }}
</el-menu-item>
</el-sub-menu>
<el-menu-item :index="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: #fff;
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 {
border-right: 0;
i {
margin-right: 14px;
font-size: 24px;
}
.el-icon-arrow-down {
margin-right: 0;
font-size: 16px;
}
}
.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;
&:hover {
background-color: rgba(86, 100, 210, 0.04);
}
}
}
</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 { useUserStore } from '@/stores/user'
withDefaults(defineProps<{ hasTitle?: boolean }>(), {
hasTitle: true
})
const userStore = useUserStore()
const userInfo = userStore.user
const logout = () => {
userStore.logout()
}
</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>
</div>
<div class="app-header-right">
<el-dropdown>
<div class="avatar">
<img :src="userInfo.avatar || 'https://webapp-pub.ezijing.com/website/base/images/avatar.svg'" />
</div>
<template #dropdown>
<el-dropdown-menu>
<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>{{ userStore.userName }}</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: 1000;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
height: 64px;
background-color: #3276fc;
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-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: #ededed;
}
.app-layout-container {
flex: 1;
display: flex;
}
</style>
<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>
<script>
import AppBreadcrumb from './Breadcrumb.vue'
export default {
name: 'AppMain',
props: { hasBreadcrumb: { type: Boolean, default: true } },
components: { AppBreadcrumb }
}
</script>
<style>
.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>
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
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 AppUpload from '@/components/base/AppUpload.vue'
import modules from './modules'
const app = createApp(App)
// 注册公共组件
app.component('AppCard', AppCard).component('AppList', AppList).component('AppUpload', AppUpload)
// 注册模块
modules({ router })
app.use(createPinia())
app.use(router)
app.use(ElementPlus, { locale: zhCn })
app.mount('#app')
import httpRequest from '@/utils/axios'
// 获取资料消息列表
// 类型(1:入学指南,2:学习地图,3:考试攻略,4:消息)
export function getBannerList(params?: { type?: string; page?: number; page_size?: number }) {
return httpRequest.get('/api/psp/backend/banner/index', { params })
}
// 创建资料
export function createBanner(data: { title: string; desc: string; type: string; weight?: string }) {
return httpRequest.post('/api/psp/backend/banner/create', data)
}
// 更新资料
export function updateBanner(data: { id: string; title: string; desc: string; type: string; weight?: string }) {
return httpRequest.post('/api/psp/backend/banner/update', data)
}
// 获取资料详情
export function getBanner(params: { id: string }) {
return httpRequest.get('/api/psp/backend/banner/view', { params })
}
// 删除资料
export function deleteBanner(data: { id: string }) {
return httpRequest.post('/api/psp/backend/banner/delete', data)
}
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/banner',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/List.vue') },
{ path: 'create', component: () => import('./views/Update.vue') },
{ path: 'update/:id', component: () => import('./views/Update.vue'), props: true }
]
}
]
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getBannerList, deleteBanner } from '../api'
const appList = ref()
const listOptions = {
remote: {
httpRequest: getBannerList,
params: { type: '' }
},
filters: [{ label: '类型', slots: 'filter-type' }],
columns: [
{ label: '封面图片', slots: 'table-cover', width: 224 },
{ label: 'ID', prop: 'id' },
{ label: '标题', prop: 'title' },
{ label: '类型', prop: 'type_name' },
{ label: '权重', prop: 'weight' },
{ label: '浏览量', prop: 'pv' },
{ label: '创建时间', prop: 'created_time' },
{ label: '操作', slots: 'table-operate', width: 160 }
]
}
const typeList = [
{ label: '全部', value: '' },
{ label: '新闻', value: '1' },
{ label: '跳转窗口', value: '2' }
]
const onChangeType = () => {
appList.value?.refetch()
}
const onRemove = (row: any) => {
ElMessageBox.confirm('确定要删除吗?', '提示').then(() => {
deleteBanner({ id: row.id }).then(() => {
ElMessage({ type: 'success', message: '删除成功' })
appList.value?.refetch()
})
})
}
</script>
<template>
<AppCard>
<AppList v-bind="listOptions" ref="appList">
<template #header-aside>
<router-link to="/banner/create">
<el-button type="primary">创建</el-button>
</router-link>
</template>
<template #filter-type="{ params }">
<el-radio-group v-model="params.type" @change="onChangeType">
<el-radio-button :label="item.value" v-for="item in typeList" :key="item.value">
{{ item.label }}
</el-radio-button>
</el-radio-group>
</template>
<template #table-cover="{ row }">
<el-image :src="row.cover_page" lazy fit="cover" style="width: 200px; height: 100px" />
</template>
<template #table-operate="{ row }">
<el-space>
<router-link :to="`/banner/update/${row.id}`">
<el-button plain>编辑</el-button>
</router-link>
<el-button type="danger" plain @click="onRemove(row)">删除</el-button>
</el-space>
</template>
</AppList>
</AppCard>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import type { FormInstance } from 'element-plus'
import { createBanner, updateBanner, getBanner } from '../api'
const props = defineProps<{ id?: string }>()
const router = useRouter()
const formRef = ref<FormInstance>()
const form = reactive({ title: '', type: '2', desc: '', weight: '', cover_page: '', url: '' })
const rules = {
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
type: [{ required: true, message: '请选择类型', trigger: 'change' }],
cover_page: [{ required: true, message: '请上传封面图', trigger: 'change' }],
url: [{ required: true, message: '请输入跳转链接', trigger: 'blur' }],
desc: [{ required: true, message: '请输入资料详情', trigger: 'blur' }]
}
const typeList = [
{ label: '新闻', value: '1' },
{ label: '跳转窗口', value: '2' }
]
// 提交
const onSubmit = (formRef: FormInstance) => {
if (!formRef) return
formRef.validate().then(() => {
props.id ? update() : create()
})
}
// 创建
const create = () => {
createBanner(form).then(() => {
ElMessage({ message: '创建成功', type: 'success' })
router.push('/banner')
})
}
// 修改
const update = () => {
const params = { ...form, id: props.id as string }
updateBanner(params).then(() => {
ElMessage({ message: '修改成功', type: 'success' })
router.push('/banner')
})
}
onMounted(() => {
props.id &&
getBanner({ id: props.id }).then(res => {
Object.assign(form, res.data)
})
})
</script>
<template>
<AppCard>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" />
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select v-model="form.type">
<el-option v-for="item in typeList" :label="item.label" :value="item.value" :key="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="封面" prop="cover_page">
<AppUpload v-model="form.cover_page" accept="image/*"></AppUpload>
</el-form-item>
<el-form-item label="跳转链接" prop="url" v-if="form.type === '2'">
<el-input v-model="form.url" />
</el-form-item>
<el-form-item label="详情" prop="desc" v-if="form.type === '1'">
<el-input type="textarea" v-model="form.desc" :autosize="{ minRows: 12 }" />
</el-form-item>
<el-form-item label="权重" prop="weight">
<el-input type="number" v-model="form.weight" />
</el-form-item>
<el-form-item>
<el-button type="primary" auto-insert-space @click="onSubmit(formRef)">保存</el-button>
</el-form-item>
</el-form>
</AppCard>
</template>
import httpRequest from '@/utils/axios'
// 获取学员列表
export function getCourseList(params?: { name?: string; page?: number; page_size?: number }) {
return httpRequest.get('/api/psp/backend/course/index', { params })
}
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/List.vue') }]
}
]
<script setup lang="ts">
import { getCourseList } from '../api'
const listOptions = {
remote: {
httpRequest: getCourseList,
params: { name: '', mobile: '' }
},
filters: [{ type: 'input', prop: 'name', placeholder: '课程名称' }],
columns: [
{ label: '课程图片', slots: 'table-picture', width: 224 },
{ label: 'ID', prop: 'id' },
{ label: '课程名称', prop: 'course_name' },
{ label: '课程描述', prop: 'course_represent', slots: 'table-desc' },
{ label: '权重', prop: 'weight' },
{ label: '是否显示', prop: 'status' },
{ label: '操作', slots: 'table-operate', width: 90 }
]
}
</script>
<template>
<AppCard>
<AppList v-bind="listOptions">
<template #table-picture="{ row }">
<el-image :src="row.course_picture" lazy fit="cover" style="width: 200px; height: 100px" />
</template>
<template #table-desc="{ row }">
<div v-html="row.course_represent" style="max-height: 100px"></div>
</template>
<template #table-operate>
<el-button>更新</el-button>
</template>
</AppList>
</AppCard>
</template>
import httpRequest from '@/utils/axios'
// 获取资料消息列表
// 类型(1:入学指南,2:学习地图,3:考试攻略,4:消息)
export function getDocList(params?: { type?: string; page?: number; page_size?: number }) {
return httpRequest.get('/api/psp/backend/doc/index', { params })
}
// 创建资料
export function createDoc(data: { title: string; desc: string; type: string; weight?: string }) {
return httpRequest.post('/api/psp/backend/doc/create', data)
}
// 更新资料
export function updateDoc(data: { id: string; title: string; desc: string; type: string; weight?: string }) {
return httpRequest.post('/api/psp/backend/doc/update', data)
}
// 获取资料详情
export function getDoc(params: { id: string }) {
return httpRequest.get('/api/psp/backend/doc/view', { params })
}
// 删除资料
export function deleteDoc(data: { id: string }) {
return httpRequest.post('/api/psp/backend/doc/delete', data)
}
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/doc',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/List.vue') },
{ path: 'create', component: () => import('./views/Update.vue') },
{ path: 'update/:id', component: () => import('./views/Update.vue'), props: true }
]
}
]
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getDocList, deleteDoc } from '../api'
const appList = ref()
const listOptions = {
remote: {
httpRequest: getDocList,
params: { type: '' }
},
filters: [{ label: '类型', slots: 'filter-type' }],
columns: [
{ label: 'ID', prop: 'id' },
{ label: '标题', prop: 'title' },
{ label: '类型', prop: 'type_name' },
{ label: '浏览量', prop: 'pv' },
{ label: '创建时间', prop: 'created_time' },
{ label: '操作', slots: 'table-operate', width: 160 }
]
}
const typeList = [
{ label: '全部', value: '' },
{ label: '入学指南', value: '1' },
{ label: '学习地图', value: '2' },
{ label: '考试攻略', value: '3' },
{ label: '消息', value: '4' }
]
const onChangeType = () => {
appList.value?.refetch()
}
const onRemove = (row: any) => {
ElMessageBox.confirm('确定要删除吗?', '提示').then(() => {
deleteDoc({ id: row.id }).then(() => {
ElMessage({ type: 'success', message: '删除成功' })
appList.value?.refetch()
})
})
}
</script>
<template>
<AppCard>
<AppList v-bind="listOptions" ref="appList">
<template #header-aside>
<router-link to="/doc/create">
<el-button type="primary">创建</el-button>
</router-link>
</template>
<template #filter-type="{ params }">
<el-radio-group v-model="params.type" @change="onChangeType">
<el-radio-button :label="item.value" v-for="item in typeList" :key="item.value">
{{ item.label }}
</el-radio-button>
</el-radio-group>
</template>
<template #table-operate="{ row }">
<el-space>
<router-link :to="`/doc/update/${row.id}`">
<el-button plain>编辑</el-button>
</router-link>
<el-button type="danger" plain @click="onRemove(row)">删除</el-button>
</el-space>
</template>
</AppList>
</AppCard>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import type { FormInstance } from 'element-plus'
import { createDoc, updateDoc, getDoc } from '../api'
const props = defineProps<{ id?: string }>()
const router = useRouter()
const formRef = ref<FormInstance>()
const form = reactive({ title: '', type: '', desc: '', weight: '', file: [] })
const rules = {
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
type: [{ required: true, message: '请选择类型', trigger: 'change' }],
desc: [{ required: true, message: '请输入资料详情', trigger: 'blur' }]
}
const typeList = [
{ label: '入学指南', value: '1' },
{ label: '学习地图', value: '2' },
{ label: '考试攻略', value: '3' },
{ label: '消息', value: '4' }
]
// 提交
const onSubmit = (formRef: FormInstance) => {
if (!formRef) return
formRef.validate().then(() => {
props.id ? update() : create()
})
}
// 创建
const create = () => {
const params = { ...form, file: JSON.stringify(form.file) }
createDoc(params).then(() => {
ElMessage({ message: '创建成功', type: 'success' })
router.push('/doc')
})
}
// 修改
const update = () => {
const params = { ...form, file: JSON.stringify(form.file), id: props.id as string }
updateDoc(params).then(() => {
ElMessage({ message: '修改成功', type: 'success' })
router.push('/doc')
})
}
onMounted(() => {
props.id &&
getDoc({ id: props.id }).then(res => {
let file = []
try {
file = JSON.parse(res.data.file) || []
} catch (error) {
console.log(error)
}
Object.assign(form, res.data, { file })
})
})
</script>
<template>
<AppCard>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" />
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select v-model="form.type">
<el-option v-for="item in typeList" :label="item.label" :value="item.value" :key="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="详情" prop="desc">
<el-input type="textarea" v-model="form.desc" :autosize="{ minRows: 12 }" />
</el-form-item>
<el-form-item label="附件" prop="file">
<app-upload v-model="form.file"></app-upload>
</el-form-item>
<el-form-item label="权重" prop="weight">
<el-input type="number" v-model="form.weight" />
</el-form-item>
<el-form-item>
<el-button type="primary" auto-insert-space @click="onSubmit(formRef)">保存</el-button>
</el-form-item>
</el-form>
</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 getQuestionList(params?: { name?: string; mobile?: string; page?: number; page_size?: number }) {
return httpRequest.get('/api/psp/backend/question/index', { params })
}
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/qa',
component: AppLayout,
children: [{ path: '', component: () => import('./views/List.vue') }]
}
]
<script setup lang="ts">
import { getQuestionList } from '../api'
const listOptions = {
remote: {
httpRequest: getQuestionList,
params: { name: '', mobile: '' }
},
filters: [
{ type: 'input', prop: 'name', placeholder: '姓名' },
{ type: 'input', prop: 'mobile', placeholder: '手机号' }
],
columns: [
{ label: 'ID', prop: 'id' },
{ label: '姓名', prop: 'name' },
{ label: '手机号', prop: 'mobile' },
{ label: '证书编号', prop: 'certificate_number' },
{ label: '星星数量', prop: 'star' },
{ label: '标签', prop: 'label' },
{ label: '操作', slots: 'table-operate' }
]
}
</script>
<template>
<AppCard>
<AppList v-bind="listOptions">
<template #table-operate>
<el-button>查看</el-button>
<el-button>更新</el-button>
<el-button>签到记录</el-button>
</template>
</AppList>
</AppCard>
</template>
import httpRequest from '@/utils/axios'
// 获取学员列表
export function getUserList(params?: { name?: string; mobile?: string; page?: number; page_size?: number }) {
return httpRequest.get('/api/psp/backend/user/index', { params })
}
// 获取学员详情
export function getUser(params: { id: string }) {
return httpRequest.get('/api/psp/backend/user/view', { params })
}
// 更新资料
export function updateUser(data: {
id: string
name: string
mobile: string
certificate_number: string
label?: string
}) {
return httpRequest.post('/api/psp/backend/user/update', data)
}
// 获取星星记录列表
export function getStarRecordList(params: { id: string; page?: number; page_size?: number }) {
return httpRequest.get('/api/psp/backend/user/star-record', { params })
}
// 获取签到记录列表
export function getSignInList(params: { id: string; page?: number; page_size?: number }) {
return httpRequest.get('/api/psp/backend/user/sign-in-record', { params })
}
// 学员同步
export function syncUser() {
return httpRequest.get('/api/psp/backend/user/sync')
}
<script setup lang="ts">
import { getSignInList } from '../api'
const props = defineProps<{ id: string }>()
const listOptions = {
remote: { httpRequest: getSignInList, params: { id: props.id } },
columns: [{ label: '时间', prop: 'created_time' }]
}
</script>
<template>
<AppList v-bind="listOptions"></AppList>
</template>
<script setup lang="ts">
import { getStarRecordList } from '../api'
const props = defineProps<{ id: string }>()
const listOptions = {
remote: { httpRequest: getStarRecordList, params: { id: props.id } },
columns: [
{ label: '时间', prop: 'created_time' },
{ label: '获取方式', prop: 'cause' },
{ label: '之前数量', prop: 'before' },
{ label: '之后数量', prop: 'after' },
{ label: '修改数量', prop: 'change' }
]
}
</script>
<template>
<AppList v-bind="listOptions"></AppList>
</template>
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/student',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/List.vue') },
{ path: 'update/:id', component: () => import('./views/Update.vue'), props: true },
{ path: 'view/:id', component: () => import('./views/View.vue'), props: true }
]
}
]
<script setup lang="ts">
import { getUserList, syncUser } from '../api'
const listOptions = {
remote: {
httpRequest: getUserList,
params: { name: '', mobile: '' }
},
filters: [
{ type: 'input', prop: 'name', placeholder: '姓名' },
{ type: 'input', prop: 'mobile', placeholder: '手机号' }
],
columns: [
{ label: 'ID', prop: 'id' },
{ label: '姓名', prop: 'name' },
{ label: '手机号', prop: 'mobile' },
{ label: '证书编号', prop: 'certificate_number' },
{ label: '星星数量', prop: 'star' },
{ label: '标签', prop: 'label', slots: 'table-label' },
{ label: '操作', slots: 'table-operate', width: 160 }
]
}
const onSyncUser = async () => {
await syncUser()
}
</script>
<template>
<AppCard>
<AppList v-bind="listOptions" ref="appList">
<template #header-aside>
<el-button type="primary" @click="onSyncUser">同步</el-button>
</template>
<template #table-label="{ row }">
<el-tag v-for="item in row.label" :key="item" round> {{ item }} </el-tag>
</template>
<template #table-operate="{ row }">
<el-space>
<router-link :to="`/student/update/${row.id}`"><el-button plain>编辑</el-button></router-link>
<router-link :to="`/student/view/${row.id}`" target="_blank">
<el-button type="primary" plain>查看</el-button>
</router-link>
</el-space>
</template>
</AppList>
</AppCard>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import type { FormInstance } from 'element-plus'
import { updateUser, getUser } from '../api'
const props = defineProps<{ id: string }>()
const router = useRouter()
const formRef = ref<FormInstance>()
const form = reactive({
name: '',
mobile: '',
certificate_number: '',
star: '',
notice_url: '',
certificate_url: '',
avatar: '',
label: []
})
const rules = {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
mobile: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
certificate_number: [{ required: true, message: '请输入证书编号', trigger: 'blur' }]
}
// 提交
const onSubmit = (formRef: FormInstance) => {
if (!formRef) return
formRef.validate().then(update)
}
// 修改
const update = () => {
const params = { ...form, id: props.id, label: form.label.join(',') }
updateUser(params).then(() => {
ElMessage({ message: '修改成功', type: 'success' })
router.push('/student/view/' + props.id)
})
}
const getUserInfo = () => {
getUser({ id: props.id }).then(res => {
Object.assign(form, res.data)
})
}
onMounted(() => {
getUserInfo()
})
</script>
<template>
<AppCard>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="form.mobile" />
</el-form-item>
<el-form-item label="证书编号" prop="certificate_number">
<el-input v-model="form.certificate_number" />
</el-form-item>
<el-form-item label="标签" prop="title">
<el-select v-model="form.label" multiple>
<el-option value="持证人"></el-option>
<el-option value="导师"></el-option>
</el-select>
</el-form-item>
<el-form-item label="星星数量" prop="star">
<el-input v-model="form.star" disabled />
</el-form-item>
<el-form-item label="入学通知书" prop="notice_url">
<el-image :src="form.notice_url" style="width: 100px; height: 100px"></el-image>
</el-form-item>
<el-form-item label="证书图片" prop="certificate_url">
<el-image :src="form.certificate_url" style="width: 100px; height: 100px"></el-image>
</el-form-item>
<el-form-item label="持证人头像" prop="avatar">
<el-image :src="form.avatar" style="width: 100px; height: 100px"></el-image>
</el-form-item>
<el-form-item>
<el-button type="primary" auto-insert-space @click="onSubmit(formRef)">保存</el-button>
</el-form-item>
</el-form>
</AppCard>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import StarRecord from '../components/StarRecord.vue'
import SignInRecord from '../components/SignInRecord.vue'
import { getUser } from '../api'
const props = defineProps<{ id: string }>()
let data = ref()
let loading = ref<boolean>(false)
const getUserInfo = () => {
loading.value = true
getUser({ id: props.id }).then(res => {
data.value = res.data
loading.value = false
})
}
onMounted(() => {
getUserInfo()
})
</script>
<template>
<AppCard title="基本信息" v-loading="loading">
<template #header-aside>
<router-link :to="`/student/update/${id}`">
<el-button type="primary">编辑</el-button>
</router-link>
</template>
<el-descriptions border v-if="data">
<el-descriptions-item label="姓名">{{ data.name }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ data.mobile }}</el-descriptions-item>
<el-descriptions-item label="证书编号">{{ data.certificate_number }}</el-descriptions-item>
<el-descriptions-item label="星星数量">{{ data.star }}</el-descriptions-item>
<el-descriptions-item label="标签" :span="2">
<el-tag v-for="item in data.label" :key="item" round> {{ item }} </el-tag>
</el-descriptions-item>
<el-descriptions-item label="入学通知书" :span="3">
<el-image :src="data.notice_url" style="width: 100px; height: 100px"></el-image>
</el-descriptions-item>
<el-descriptions-item label="证书图片" :span="3">
<el-image :src="data.certificate_url" style="width: 100px; height: 100px"></el-image>
</el-descriptions-item>
<el-descriptions-item label="持证人头像" :span="3">
<el-image :src="data.avatar" style="width: 100px; height: 100px"></el-image>
</el-descriptions-item>
</el-descriptions>
</AppCard>
<AppCard>
<el-tabs type="card">
<el-tab-pane label="签到记录" lazy>
<SignInRecord :id="id"></SignInRecord>
</el-tab-pane>
<el-tab-pane label="星星记录" lazy>
<StarRecord :id="id"></StarRecord>
</el-tab-pane>
</el-tabs>
</AppCard>
</template>
import httpRequest from '@/utils/axios'
// 获取学员列表
export function getTeamList(params?: { name?: string; status?: string; page?: number; page_size?: number }) {
return httpRequest.get('/api/psp/backend/team/index', { params })
}
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论