提交 ba8d7473 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
module.exports = {
env: {
node: true
},
extends: ['plugin:vue/essential', 'standard'],
rules: {
'vue/comment-directive': 'off',
'vue/multi-word-component-names': 'off',
'space-before-function-paren': 'off'
}
}
node_modules
.DS_Store
dist
dist-ssr
*.local
\ No newline at end of file
const fs = require('fs')
const path = require('path')
const chalk = require('chalk')
const OSS = require('ali-oss')
const log = console.log
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/qbs')
-----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="theme-color" content="#c01c40" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>紫荆题库管理系统</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"version": "0.0.0",
"scripts": {
"dev": "vite --mode dev",
"build": "cross-env BUILD_ENV=prod vite build && cross-env BUILD_ENV=prod npm run deploy",
"build:pre": "vite build",
"build:test": "vite build --mode test",
"preview": "vite preview",
"deploy": "node ./deploy.js",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src"
},
"dependencies": {
"axios": "^0.25.0",
"element-ui": "^2.15.6",
"query-string": "^7.1.1",
"vue": "^2.6.14",
"vue-router": "^3.5.3",
"vuex": "^3.6.2"
},
"devDependencies": {
"@rollup/plugin-eslint": "^8.0.1",
"ali-oss": "^6.17.1",
"chalk": "^5.0.0",
"cross-env": "^7.0.3",
"eslint": "^8.8.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-vue": "^8.4.1",
"sass": "1.49.7",
"vite": "^2.8.0",
"vite-plugin-checker": "^0.4.0",
"vite-plugin-vue2": "^1.9.3",
"vue-template-compiler": "^2.6.14"
}
}
<template>
<div id="app">
<router-view />
</div>
</template>
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) {
return httpRequest.post('https://webapp-pub.oss-cn-beijing.aliyuncs.com', data, {
withCredentials: false,
headers: { 'Content-Type': 'multipart/form-data' }
})
}
html * {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
outline: none;
-webkit-text-size-adjust: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
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: #c01c40;
}
$--color-primary: #c01c40;
$--color-info: #3c4043;
// border
$--border-radius-small: 8px !default;
// dialog
$--message-close-size: 20px !default;
/* 改变 icon 字体路径变量,必需 */
$--font-path: 'element-ui/lib/theme-chalk/fonts';
@import 'element-ui/packages/theme-chalk/src/index';
<template>
<div class="app-card">
<div class="app-card-hd">
<slot name="header">
<h2 class="app-card-hd__title" v-if="title">{{ title }}</h2>
<slot name="header-aside"></slot>
</slot>
</div>
<div class="app-card-bd">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'AppCard',
props: { title: String }
}
</script>
<style>
.app-card {
background: #ffffff;
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>
<template>
<div class="table-list">
<div class="table-list-hd">
<!-- 筛选 -->
<div class="table-list-filter" v-if="filters.length">
<el-form :inline="true" :model="params" ref="filterForm">
<template v-for="item in filters">
<el-form-item :label="item.label" :prop="item.prop" :key="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 v-if="item.type === 'input'" />
<!-- select -->
<el-select
v-model="params[item.prop]"
clearable
v-bind="item"
v-if="item.type === 'select'"
@change="search"
>
<template v-for="(option, index) in item.options">
<el-option
:label="option[item.labelKey] || option.label"
:value="option[item.valueKey] || option.value"
:key="index"
></el-option>
</template>
</el-select>
</template>
</el-form-item>
</template>
<el-form-item class="filter-buttons">
<el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
<el-button icon="el-icon-refresh-left" @click="reset">重置</el-button>
<el-button @click="showMoreFilter" v-if="hasMoreFilter">更多筛选</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"
v-on="$listeners"
style="height: 100%"
ref="table"
>
<template v-for="item in columns">
<el-table-column v-bind="item" :key="item.prop" v-if="visible(item)">
<template v-slot: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>
</template>
</el-table>
</slot>
</div>
<!-- 底部 -->
<div class="table-list-ft">
<div style="padding: 10px 0">
<slot name="footer"></slot>
</div>
<el-pagination
class="table-list-pagination"
layout="total, prev, pager, next, sizes, jumper"
:page-sizes="[10, 20, 30, 50, 100]"
:page-size="page.size"
:total="page.total"
:current-page.sync="page.currentPage"
@size-change="pageSizeChange"
@current-change="fetchList()"
:hide-on-single-page="true"
v-if="hasPagination"
>
</el-pagination>
</div>
<!-- 更多筛选 -->
<el-drawer title="更多筛选" :visible.sync="moreFilterVisible" ref="drawer">
<div class="more-filter-drawer">
<div class="more-filter">
<el-form :model="params" ref="moreFilterForm">
<template v-for="item in moreFilters">
<el-form-item :label="item.label" :prop="item.prop" :key="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 v-if="item.type === 'input'" />
<!-- select -->
<el-select
v-model="params[item.prop]"
clearable
v-bind="item"
v-if="item.type === 'select'"
style="width: 100%"
>
<template v-for="(option, index) in item.options">
<el-option
:label="option[item.labelKey] || option.label"
:value="option[item.valueKey] || option.value"
:key="index"
></el-option>
</template>
</el-select>
</template>
</el-form-item>
</template>
</el-form>
</div>
<div class="more-filter-buttons">
<el-button @click="cancelMoreFilter">取 消</el-button>
<el-button type="primary" @click="primaryMoreFilter">确定</el-button>
</div>
</div>
</el-drawer>
</div>
</template>
<script>
export default {
name: 'AppList',
props: {
// 接口请求
remote: { type: Object, default: () => ({}) },
// 筛选
filters: { type: Array, default: () => [] },
// 更多筛选
moreFilters: { type: Array, default: () => [] },
// 列表项
columns: { type: Array, default: () => [] },
// 列表数据
data: { type: Array, default: () => [] },
// 是否含有翻页
hasPagination: { type: Boolean, default: true },
// 每页多少条数据
limit: { type: Number, default: 20 }
},
data() {
return {
loading: false,
params: this.remote.params || {},
dataList: this.data,
page: { total: 0, size: this.limit, currentPage: 1 },
moreFilterVisible: false
}
},
watch: {
'remote.params': {
immediate: true,
handler(data) {
this.params = data || {}
}
},
data: {
immediate: true,
handler(data) {
this.dataList = data
}
}
},
computed: {
table() {
return this.$refs.table
},
hasMoreFilter() {
return !!this.moreFilters.length
}
},
methods: {
fetchList(isReset) {
/**
* @param function httpRequest api接口
* @param function beforeRequest 接口请求之前
* @param function callback 接口请求成功回调
*/
const { httpRequest, beforeRequest, callback } = this.remote
if (!httpRequest) {
return
}
// 参数设置
let params = this.params
// 翻页参数设置
if (this.hasPagination) {
params.page = this.page.currentPage
params.limit = this.page.size
}
// 接口请求之前
if (beforeRequest) {
params = beforeRequest(params, isReset)
}
for (const key in params) {
if (params[key] === '' || params[key] === undefined || params[key] === undefined) {
delete params[key]
}
}
this.loading = true
httpRequest(params)
.then(res => {
const { data = [], total = 0 } = res.data || {}
this.page.total = total
this.dataList = callback ? callback(data) : data
})
.catch(() => {
this.page.total = 0
this.dataList = []
})
.finally(() => {
this.loading = false
})
},
// 搜索
search() {
this.page.currentPage = 1
this.fetchList()
},
// 重置
reset() {
// 清空筛选条件
this.$refs.filterForm && this.$refs.filterForm.resetFields()
// 清空更多筛选条件
this.hasMoreFilter && this.$refs.moreFilterForm && this.$refs.moreFilterForm.resetFields()
// 初始化页码
this.page.currentPage = 1
// 刷新列表
this.fetchList(true)
},
// 刷新
refetch(isForce) {
isForce ? this.reset() : this.fetchList()
},
// 页数改变
pageSizeChange(value) {
this.page.currentPage = 1
this.page.size = value
this.fetchList()
},
visible(item) {
return Object.prototype.hasOwnProperty.call(item, 'visible') ? item.visible : true
},
// 显示更多筛选
showMoreFilter() {
this.moreFilterVisible = true
},
// 取消更多筛选
cancelMoreFilter() {
this.moreFilterVisible = false
// 清空筛选条件
this.$refs.moreFilterForm && this.$refs.moreFilterForm.resetFields()
},
// 确定更多筛选
primaryMoreFilter() {
this.moreFilterVisible = false
this.search()
}
},
beforeMount() {
this.fetchList()
}
}
</script>
<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 {
display: flex;
align-items: center;
justify-content: space-between;
}
.table-list-pagination {
padding: 10px 0;
text-align: right;
}
.el-table-column--selection .cell {
padding: 0 14px !important;
}
.more-filter-drawer {
height: 100%;
display: flex;
flex-direction: column;
padding: 0 20px 20px;
box-sizing: border-box;
}
.more-filter {
flex: 1;
overflow-y: auto;
}
.more-filter-buttons {
display: flex;
.el-button {
flex: 1;
}
}
</style>
<template>
<aside class="app-aside">
<nav class="nav">
<el-menu :default-active="defaultActive" :router="true">
<template v-for="item in menuList">
<el-submenu :index="item.path" :key="item.path" v-if="item.children">
<template #title><i :class="item.icon"></i>{{ item.name }}</template>
<el-menu-item :index="subitem.path" v-for="subitem in item.children" :key="subitem.path">
{{ subitem.name }}
</el-menu-item>
</el-submenu>
<el-menu-item :index="item.path" :key="item.path" v-else>
<i :class="item.icon"></i>{{ item.name }}
</el-menu-item>
</template>
</el-menu>
</nav>
</aside>
</template>
<script>
export default {
name: 'AppAside',
data() {
return {
menuList: [
{
name: '试题中心',
path: '/question',
icon: 'el-icon-document',
children: [
{ name: '新建试题', path: '/question/create' },
{ name: '我的题库', path: '/question/list' }
]
},
{
name: '试卷中心',
path: '/paper',
icon: 'el-icon-document',
children: [{ name: '我的试卷库', path: '/paper/list' }]
}
]
}
},
computed: {
defaultActive() {
// 扁平菜单
const flatMenuList = this.menuList.reduce((result, item) => {
result.push(item)
if (item.children) {
result = result.concat(item.children)
}
return result
}, [])
const found = flatMenuList.reverse().find(item => {
return this.$route.path.includes(item.path)
})
return found ? found.path : '/'
}
}
}
</script>
<style lang="scss">
.app-aside {
width: 200px;
background: #fff;
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: 14px;
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: 14px;
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: 18px;
font-weight: 400;
line-height: 1;
}
.el-breadcrumb__inner a {
font-weight: normal;
color: #c01c40;
}
.router-link-active {
color: #1a1b1c;
}
}
</style>
<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.svg" /></router-link>
</div>
<div class="app-name">题库系统</div>
</div>
<div class="app-header-right">
<el-dropdown>
<div class="avatar">
<img :src="user.avatar || 'https://webapp-pub.ezijing.com/website/base/images/avatar.svg'" />
</div>
<el-dropdown-menu slot="dropdown" style="width: 280px">
<div class="app-header-user">
<div class="app-header-user-avatar">
<img :src="user.avatar || 'https://webapp-pub.ezijing.com/website/base/images/avatar.svg'" />
</div>
<div class="app-header-user-main">
<h3>{{ user.realname || user.nickname }}</h3>
<p>{{ user.email || user.mobile }}</p>
</div>
<div class="app-header-user-buttons">
<el-button size="medium" round @click="logout">退出登录</el-button>
</div>
</div>
</el-dropdown-menu>
</el-dropdown>
</div>
</header>
</template>
<script>
export default {
name: 'AppHeader',
computed: {
user() {
return this.$store.state.user
}
},
methods: {
logout() {
this.$store.dispatch('logout').then(() => {
window.location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(window.location.href)}`
})
}
}
}
</script>
<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: #fff;
color: #c01c40;
.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 #c01c40;
}
}
.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>
<template>
<div class="app-layout">
<app-header></app-header>
<div class="app-layout-container">
<app-aside v-if="sidebar"></app-aside>
<app-main></app-main>
</div>
</div>
</template>
<script>
import AppHeader from './Header.vue'
import AppAside from './Aside.vue'
import AppMain from './Main.vue'
export default {
name: 'AppLayout',
props: { sidebar: { type: Boolean, default: true }, hasTitle: { type: Boolean, default: true } },
components: { AppHeader, AppAside, AppMain }
}
</script>
<style>
.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>
<template>
<editor :init="init" v-bind="$attrs" v-on="$listeners" @onChange="onChange" @onBlur="onBlur" />
</template>
<script>
import Editor from '@tinymce/tinymce-vue'
import ImageUpload from './imageUpload'
export default {
components: {
editor: Editor
},
data() {
return {
init: {
min_height: 600,
max_height: 600,
menubar: false,
statusbar: false,
plugins: 'table autoresize charmap fullscreen hr 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: ImageUpload,
automatic_uploads: true,
quickbars_insert_toolbar: false,
// style_formats: [{ title: '悬挂缩进', block: 'p', styles: { textIndent: '-2em', paddingLeft: '2em' } }],
content_style: 'img {max-width:100%;}'
}
}
},
methods: {
onChange(event, editor) {
this.dispatch('ElFormItem', 'el.form.change', editor.getContent())
},
onBlur(event, editor) {
this.dispatch('ElFormItem', 'el.form.blur', editor.getContent())
},
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root
let name = parent.$options.componentName
while (parent && (!name || name !== componentName)) {
parent = parent.$parent
if (parent) {
name = parent.$options.componentName
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params))
}
}
}
}
</script>
<style>
.tox .tox-tbtn--bespoke .tox-tbtn__select-label {
width: 4em !important;
}
</style>
import { getSignature, uploadFile } from '@/api/base'
import md5 from 'blueimp-md5'
export default function (blobInfo, succFun, failFun) {
const file = blobInfo.blob()
getSignature()
.then(response => {
const prefix = 'upload/admin/'
const fileName = file.name
const key = prefix + md5(fileName + new Date().getTime()) + fileName.substr(fileName.lastIndexOf('.'))
const { accessid, policy, signature, host } = response
const data = { key, OSSAccessKeyId: accessid, policy, signature, success_action_status: '200', file }
const fileUrl = `${host}/${key}`
uploadFile(data)
.then(() => {
succFun(fileUrl)
})
.catch(() => {
failFun('上传失败')
})
})
.catch(response => {
failFun('获取Signature失败')
})
}
<template>
<div class="app-upload">
<draggable
v-model="fileList"
group="people"
class="file-list el-upload-list--picture-card"
@end="onEnd"
v-if="showFileList"
>
<div class="file-item" v-for="(file, index) in fileList" :key="index">
<!-- <el-image style="width: 100%; height: 100%" :src="file.url" :preview-src-list="fileList.map(item => item.url)"> -->
<img :src="file.url" />
<span class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview" @click="handlePreview(file)">
<i class="el-icon-zoom-in"></i>
</span>
<span v-if="!$attrs.disabled" class="el-upload-list__item-delete" @click="handleRemove(index)">
<i class="el-icon-delete"></i>
</span>
</span>
</div>
</draggable>
<el-upload
action="https://webapp-pub.oss-cn-beijing.aliyuncs.com"
type="drag"
:before-upload="beforeUpload"
:on-success="handleSuccess"
:show-file-list="false"
:on-exceed="handleExceed"
:data="data"
v-bind="$attrs"
v-on="$listeners"
style="display: inline"
>
<slot>
<i class="el-icon-plus"></i>
</slot>
</el-upload>
<el-dialog :visible.sync="dialogVisible">
<img width="100%" :src="previewFile.url" />
</el-dialog>
</div>
</template>
<script>
import { getSignature } from '@/api/base'
import md5 from 'blueimp-md5'
import draggable from 'vuedraggable'
export default {
name: 'AppUpload',
components: { draggable },
props: {
value: { type: [String, Array] },
prefix: { type: String, default: 'upload/shop-admin/' },
showFileList: { type: Boolean, default: true }
},
data() {
return {
data: {}, // 请求参数
fileList: [], // 文件列表
dialogVisible: false,
previewFile: {},
message: null
}
},
watch: {
value: {
immediate: true,
handler(value) {
if (!value) {
return
}
let fileList = []
if (this.isMultiple) {
fileList = value.map(item => {
return { name: item.name || item, url: item.url || item }
})
} else {
fileList.push({ name: '附件', url: value })
}
this.fileList = [...fileList]
}
}
},
computed: {
isMultiple() {
return Array.isArray(this.value)
}
},
methods: {
beforeUpload(file) {
const limit = this.$attrs.limit
if (limit && this.fileList.length >= limit) {
this.message && this.message.close()
this.message = this.$message({ type: 'error', message: '文件超出个数限制' })
return false
}
const fileName = file.name
const key = this.prefix + md5(fileName + new Date().getTime()) + fileName.substr(fileName.lastIndexOf('.'))
return new Promise((resolve, reject) => {
getSignature()
.then(response => {
const { accessid, policy, signature, host } = response
this.data = { key, OSSAccessKeyId: accessid, policy, signature, success_action_status: '200' }
file.url = `${host}/${key}`
resolve(true)
})
.catch(err => {
console.log(err)
reject(err)
})
})
},
handleSuccess(response, file) {
let value = null
if (this.isMultiple) {
const limit = this.$attrs.limit
if (limit && this.fileList.length >= limit) {
return
}
this.fileList.push({ name: file.name, url: file.raw.url })
value = this.fileList
} else {
this.fileList = [file.raw.url]
value = file.raw.url
}
this.$emit('input', value)
this.dispatch('ElFormItem', 'el.form.change', value)
},
// 删除
handleRemove(index) {
this.fileList.splice(index, 1)
this.$emit('input', this.fileList)
this.dispatch('ElFormItem', 'el.form.change', this.fileList)
},
// 预览
handlePreview(file) {
this.previewFile = file
this.dialogVisible = true
},
handleExceed() {
this.$message({ type: 'error', message: '文件超出个数限制' })
},
onEnd() {
this.$emit('input', this.fileList)
},
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root
let name = parent.$options.componentName
while (parent && (!name || name !== componentName)) {
parent = parent.$parent
if (parent) {
name = parent.$options.componentName
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params))
}
}
}
}
</script>
<style lang="scss">
.el-upload-list--picture-card .el-upload-list__item-thumbnail {
object-fit: cover;
}
.el-upload-list__item {
transition: none !important;
}
.file-list {
margin: 0;
display: inline;
vertical-align: top;
}
.file-item {
position: relative;
overflow: hidden;
background-color: #fff;
border: 1px solid #c0ccda;
border-radius: 6px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
width: 148px;
height: 148px;
margin: 0 8px 8px 0;
display: inline-block;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
</style>
<template>
<app-upload class="avatar-uploader" accept="image/*" :show-file-list="false" v-bind="$attrs" v-on="$listeners">
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</app-upload>
</template>
<script>
import AppUpload from './Upload.vue'
export default {
name: 'AppUploadImage',
components: { AppUpload },
data() {
return {}
},
computed: {
imageUrl() {
return this.$attrs.value
}
}
}
</script>
<style lang="scss">
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
width: 148px;
height: 148px;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
line-height: 148px;
text-align: center;
}
.avatar {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
</style>
<template>
<app-upload class="video-uploader" accept="video/*" :show-file-list="false" v-bind="$attrs" v-on="$listeners">
<img v-if="videoUrl" :src="`${videoUrl}?x-oss-process=video/snapshot,t_10,f_jpg,w_0,h_0,m_fast`" class="avatar" />
<i v-else class="el-icon-plus video-uploader-icon"></i>
</app-upload>
</template>
<script>
import AppUpload from './Upload.vue'
export default {
name: 'AppUploadVideo',
components: { AppUpload },
data() {
return {}
},
computed: {
videoUrl() {
return this.$attrs.value
}
}
}
</script>
<style lang="scss">
.video-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
width: 148px;
height: 148px;
}
.video-uploader .el-upload:hover {
border-color: #409eff;
}
.video-uploader-icon {
font-size: 28px;
color: #8c939d;
line-height: 148px;
text-align: center;
}
.avatar {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
</style>
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import modules from './modules'
// 公共css
import './assets/css/base.css'
// Element-UI
import ElementUI from 'element-ui'
import './assets/theme/style.scss'
// 公共组件
import AppCard from './components/base/AppCard.vue'
import AppList from './components/base/AppList.vue'
// 注册element-ui组件
Vue.use(ElementUI, { size: 'small' })
// 注册公共组件
Vue.component('AppCard', AppCard)
Vue.component('AppList', AppList)
// 注册模块
modules({ router, store })
new Vue({
store,
router,
render: h => h(App)
}).$mount('#app')
const modules = Object.values(import.meta.globEager('./**/index.js'))
export default function ({ router }) {
modules.forEach(({ routes }) => {
// 注册路由
routes.forEach(route => {
router.addRoute(route)
})
})
}
import httpRequest from '@/utils/axios'
/**
* 获取应用列表
*/
export function getAppList(params) {
return httpRequest.get('/api/permissions/admin/v1/applications', { params })
}
/**
* 获取应用详情
*/
export function getApp(id, params) {
return httpRequest.get(`/api/permissions/admin/v1/${id}/application`, { params })
}
/**
* 创建应用
*/
export function createApp(data) {
return httpRequest.post('/api/permissions/admin/v1/application', data)
}
/**
* 更新应用
*/
export function updateApp(id, data) {
return httpRequest.put(`/api/permissions/admin/v1/${id}/application`, data)
}
/**
* 删除应用
*/
export function deleteApp(id, data) {
return httpRequest.delete(`/api/permissions/admin/v1/${id}/application`, data)
}
/**
* 更新应用签名秘钥
*/
export function updateAppSecretKey(id, data) {
return httpRequest.patch(`/api/permissions/admin/v1/${id}/application/secret-key`, data)
}
/**
* 获取应用管理员
*/
export function getAppAdminUsers(params) {
return httpRequest.get(`/api/permissions/admin/v1/${params.app_id}/application/assign/app-users`, { params })
}
/**
* 删除应用管理员
*/
export function deleteAppAdminUsers(appid, data) {
return httpRequest.post(`/api/permissions/admin/v1/${appid}/application/assign/remove-app-users`, data)
}
/**
* 创建成员
*/
export function createUser(appid, data) {
return httpRequest.post(`/api/permissions/admin/v1/${appid}/application/user`, data)
}
/**
* 创建应用管理员
*/
export function createAppAdminUsers(appid, data) {
return httpRequest.post(`/api/permissions/admin/v1/${appid}/application/assign/users-to-app`, data)
}
/**
* 获取事件列表
*/
export function getEventList(params) {
return httpRequest.get(`/api/permissions/admin/v1/${params.app_id}/events`, { params })
}
/**
* 创建事件
*/
export function createEvent(appid, data) {
return httpRequest.post(`/api/permissions/admin/v1/${appid}/event/create`, data)
}
/**
* 更新事件
*/
export function updateEvent(id, data) {
return httpRequest.put(`/api/permissions/admin/v1/event/${id}/update`, data)
}
/**
* 删除事件
*/
export function deleteEvents(data) {
return httpRequest.post('/api/permissions/admin/v1/event/batch-delete', data)
}
/**
* 获取事件详情
*/
export function getEvent(params) {
return httpRequest.get(`/api/permissions/admin/v1/event/${params.id}/detail`, { params })
}
/**
* 刷新事件token
*/
export function refreshEventToken(params) {
return httpRequest.patch(`/api/permissions/admin/v1/event/${params.id}/refresh-token`, { params })
}
/**
* 获取获取事件回调记录列表
*/
export function getEventRecordList(params) {
return httpRequest.get(`/api/permissions/admin/v1/${params.app_id}/event/records`, { params })
}
import AppLayout from '@/components/layout/Index.vue'
const routes = [
{
path: '/paper',
component: AppLayout,
meta: { title: '试卷中心' },
children: [
{
path: '',
redirect: '/paper/list'
},
{
path: 'list',
component: () => import('./views/List.vue'),
meta: { title: '我的试卷库' }
}
]
}
]
export { routes }
<template>
<app-card>
<app-list v-bind="tableOptions" ref="list">
<template #header-aside>
<el-button type="primary" icon="el-icon-plus" @click="visible = true">新建试题</el-button>
</template>
<template v-slot:table-x="{ row }">
<el-button type="text" @click="handleView(row)">查看</el-button>
<el-button type="text" @click="handleSettings(row)">设置</el-button>
</template>
</app-list>
</app-card>
</template>
<script>
import { getAppList } from '../api'
export default {
data() {
return {
visible: false
}
},
computed: {
tableOptions() {
return {
remote: { httpRequest: getAppList },
columns: [
{ label: '应用名称', prop: 'name' },
{ label: '应用别名', prop: 'alias_name' },
{ label: '应用描述', prop: 'desc' },
{ label: '操作', slots: 'table-x', align: 'right', width: 150 }
]
}
}
},
methods: {
// 创建成功刷新列表
handleCreateSuccess() {
this.$refs.list.refetch()
},
// 查看
handleView(row) {
this.$router.push({ name: 'appView', params: { id: row.id }, query: { appid: row.id } })
},
handleSettings(row) {
this.$router.push({ path: '/settings/users', query: { appid: row.id } })
}
}
}
</script>
import httpRequest from '@/utils/axios'
/**
* 获取应用列表
*/
export function getAppList(params) {
return httpRequest.get('/api/permissions/admin/v1/applications', { params })
}
/**
* 获取应用详情
*/
export function getApp(id, params) {
return httpRequest.get(`/api/permissions/admin/v1/${id}/application`, { params })
}
/**
* 创建应用
*/
export function createApp(data) {
return httpRequest.post('/api/permissions/admin/v1/application', data)
}
/**
* 更新应用
*/
export function updateApp(id, data) {
return httpRequest.put(`/api/permissions/admin/v1/${id}/application`, data)
}
/**
* 删除应用
*/
export function deleteApp(id, data) {
return httpRequest.delete(`/api/permissions/admin/v1/${id}/application`, data)
}
/**
* 更新应用签名秘钥
*/
export function updateAppSecretKey(id, data) {
return httpRequest.patch(`/api/permissions/admin/v1/${id}/application/secret-key`, data)
}
/**
* 获取应用管理员
*/
export function getAppAdminUsers(params) {
return httpRequest.get(`/api/permissions/admin/v1/${params.app_id}/application/assign/app-users`, { params })
}
/**
* 删除应用管理员
*/
export function deleteAppAdminUsers(appid, data) {
return httpRequest.post(`/api/permissions/admin/v1/${appid}/application/assign/remove-app-users`, data)
}
/**
* 创建成员
*/
export function createUser(appid, data) {
return httpRequest.post(`/api/permissions/admin/v1/${appid}/application/user`, data)
}
/**
* 创建应用管理员
*/
export function createAppAdminUsers(appid, data) {
return httpRequest.post(`/api/permissions/admin/v1/${appid}/application/assign/users-to-app`, data)
}
/**
* 获取事件列表
*/
export function getEventList(params) {
return httpRequest.get(`/api/permissions/admin/v1/${params.app_id}/events`, { params })
}
/**
* 创建事件
*/
export function createEvent(appid, data) {
return httpRequest.post(`/api/permissions/admin/v1/${appid}/event/create`, data)
}
/**
* 更新事件
*/
export function updateEvent(id, data) {
return httpRequest.put(`/api/permissions/admin/v1/event/${id}/update`, data)
}
/**
* 删除事件
*/
export function deleteEvents(data) {
return httpRequest.post('/api/permissions/admin/v1/event/batch-delete', data)
}
/**
* 获取事件详情
*/
export function getEvent(params) {
return httpRequest.get(`/api/permissions/admin/v1/event/${params.id}/detail`, { params })
}
/**
* 刷新事件token
*/
export function refreshEventToken(params) {
return httpRequest.patch(`/api/permissions/admin/v1/event/${params.id}/refresh-token`, { params })
}
/**
* 获取获取事件回调记录列表
*/
export function getEventRecordList(params) {
return httpRequest.get(`/api/permissions/admin/v1/${params.app_id}/event/records`, { params })
}
import AppLayout from '@/components/layout/Index.vue'
const routes = [
{
path: '/question',
component: AppLayout,
meta: { title: '试题中心' },
children: [
{
path: '',
redirect: '/question/list'
},
{
path: 'list',
component: () => import('./views/List.vue'),
meta: { title: '我的题库' }
}
]
}
]
export { routes }
<template>
<app-card>
<app-list v-bind="tableOptions" ref="list">
<template #header-aside>
<el-button type="primary" icon="el-icon-plus" @click="visible = true">新建试题</el-button>
</template>
<template v-slot:table-x="{ row }">
<el-button type="text" @click="handleView(row)">查看</el-button>
<el-button type="text" @click="handleSettings(row)">设置</el-button>
</template>
</app-list>
</app-card>
</template>
<script>
import { getAppList } from '../api'
export default {
data() {
return {
visible: false
}
},
computed: {
tableOptions() {
return {
remote: { httpRequest: getAppList },
columns: [
{ label: '应用名称', prop: 'name' },
{ label: '应用别名', prop: 'alias_name' },
{ label: '应用描述', prop: 'desc' },
{ label: '操作', slots: 'table-x', align: 'right', width: 150 }
]
}
}
},
methods: {
// 创建成功刷新列表
handleCreateSuccess() {
this.$refs.list.refetch()
},
// 查看
handleView(row) {
this.$router.push({ name: 'appView', params: { id: row.id }, query: { appid: row.id } })
},
handleSettings(row) {
this.$router.push({ path: '/settings/users', query: { appid: row.id } })
}
}
}
</script>
import Vue from 'vue'
import VueRouter from 'vue-router'
import beforeEnter from '@/utils/beforeEnter'
Vue.use(VueRouter)
const routes = [{ path: '*', redirect: '/question' }]
const router = new VueRouter({
mode: 'history',
routes
})
router.beforeEach(beforeEnter)
export default router
import Vue from 'vue'
import Vuex from 'vuex'
import { getUser, logout } from '@/api/base'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
user: {}
},
mutations: {
setUser(state, user) {
state.user = user
}
},
actions: {
// 获取用户信息
getUser({ commit }) {
getUser().then(response => {
commit('setUser', response)
})
},
// 退出登录
logout({ commit }) {
return logout().then(response => {
commit('setUser', {})
return response
})
},
async checkLogin({ commit }) {
const isLogin = await getUser()
.then(response => {
if (response.code === 0) {
commit('setUser', response.data)
return true
} else {
commit('setUser', {})
return false
}
})
.catch(() => {
commit('setUser', {})
return false
})
return isLogin
}
}
})
export default store
import axios from 'axios'
import queryString from 'query-string'
import { Message } from 'element-ui'
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) {
// 默认参数
const defaultHeaders = {
timestamp: parseInt(Date.now() / 1000),
nonce: Math.random().toString(36).slice(-8),
'secret-id': 'ezijing_20200410',
'secret-key': 'THIxz9hfbMDD5pil',
signature: 'UG7wBenexQhiuD2wpCwuxkU0jqcj006d'
}
config.headers = Object.assign(config.headers, defaultHeaders)
if (config.headers['Content-Type'] === 'application/x-www-form-urlencoded') {
config.data = queryString.stringify(config.data)
}
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 === 0) {
return data
}
// 未登录
if (data.code === 4001) {
window.location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(window.location.href)}`
}
// 没有权限
if (data.code === 4008) {
router.push('/401')
}
Message({ message: data.message, type: 'error' })
return Promise.reject(data)
},
function (error) {
if (error.response) {
const { status, message } = error.response.data
// 未登录
if (status === 403) {
window.location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(window.location.href)}`
} else {
Message.error(message || error.response.data)
}
} else {
console.log(error)
}
return Promise.reject(error.response || error)
}
)
export default httpRequest
import store from '@/store'
export default async function (to, from, next) {
// 登录白名单
const whiteList = []
if (whiteList.includes(to.path)) {
next()
return
}
const isLogin = store.state.user.id || (await store.dispatch('checkLogin'))
if (!isLogin) {
window.location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(window.location.href)}`
return
}
next()
}
import fs from 'fs'
import path from 'path'
import { defineConfig } from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2'
import checker from 'vite-plugin-checker'
export default defineConfig({
base: process.env.BUILD_ENV === 'prod' ? 'https://webapp-pub.ezijing.com/website/prod/qbs/' : '/',
plugins: [
checker({
eslint: {
lintCommand: 'eslint "./src/**/*.{vue,js,jsx,ts,tsx}"'
}
}),
createVuePlugin()
],
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': 'https://app.ezijing.com'
}
},
resolve: {
alias: [
{
find: '@',
replacement: path.resolve(__dirname, 'src')
}
]
},
css: {
// 禁用SASS警告提醒
preprocessorOptions: { scss: { quietDeps: true, charset: false } }
}
})
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论