账号管理
This commit is contained in:
parent
b390fb6b0b
commit
3ff4fa97a6
|
|
@ -12,6 +12,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
|
"cropperjs": "1.5.13",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"element-plus": "^2.9.10",
|
"element-plus": "^2.9.10",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,16 @@ export default {
|
||||||
logout: async function(){
|
logout: async function(){
|
||||||
return await http.get("logout");
|
return await http.get("logout");
|
||||||
},
|
},
|
||||||
|
saveTag: async function(data = {}) {
|
||||||
|
return await http.post("save/tag", data);
|
||||||
|
},
|
||||||
|
saveInfo: async function(data = {}) {
|
||||||
|
return await http.post("save/info", data);
|
||||||
|
},
|
||||||
|
loginLog: async function(data){
|
||||||
|
return await http.get("log/login", data);
|
||||||
|
},
|
||||||
|
savePass: async function(data = {}) {
|
||||||
|
return await http.post("save/pass", data);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,36 +2,36 @@ import http from "@/utils/request"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
account: {
|
account: {
|
||||||
list: async function(data={}){
|
list: async function (data = {}) {
|
||||||
return await http.get("account/list", data);
|
return await http.get("account/list", data);
|
||||||
},
|
},
|
||||||
add: async function(data={}){
|
add: async function (data = {}) {
|
||||||
return await http.post("account/add", data);
|
return await http.post("account/add", data);
|
||||||
},
|
},
|
||||||
edit: async function(data={}){
|
edit: async function (data = {}) {
|
||||||
return await http.put("account/edit", data);
|
return await http.put("account/edit", data);
|
||||||
},
|
},
|
||||||
del: async function(data={}){
|
del: async function (data = {}) {
|
||||||
return await http.delete("account/del", data);
|
return await http.delete("account/del", data);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
menu: {
|
menu: {
|
||||||
list: async function(data={}){
|
list: async function (data = {}) {
|
||||||
return await http.get("menu/list", data);
|
return await http.get("menu/list", data);
|
||||||
},
|
},
|
||||||
add: async function(data = {}){
|
add: async function (data = {}) {
|
||||||
return await http.post("menu/add", data);
|
return await http.post("menu/add", data);
|
||||||
},
|
},
|
||||||
edit: async function(data = {}){
|
edit: async function (data = {}) {
|
||||||
return await http.put("menu/edit", data);
|
return await http.put("menu/edit", data);
|
||||||
},
|
},
|
||||||
del: async function(data = {}){
|
del: async function (data = {}) {
|
||||||
return await http.delete("menu/del", data);
|
return await http.delete("menu/del", data);
|
||||||
},
|
},
|
||||||
option: async function(data={}){
|
option: async function (data = {}) {
|
||||||
return await http.get("menu/option", data);
|
return await http.get("menu/option", data);
|
||||||
},
|
},
|
||||||
quick: async function(data = {}){
|
quick: async function (data = {}) {
|
||||||
return await http.post("menu/quick", data);
|
return await http.post("menu/quick", data);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -86,4 +86,7 @@ export default {
|
||||||
return await http.get("post/option", data);
|
return await http.get("post/option", data);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
upload: async function (data, config = {}) {
|
||||||
|
return await http.post("upload", data, config);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<svg t="1750820731881" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11613" width="256" height="256"><path d="M511.232 246.954667a32 32 0 0 1 31.850667 28.906666l0.149333 3.093334-0.021333 120.106666h74.666666v-41.493333a32 32 0 0 1 63.872-3.093333l0.149334 3.093333v41.813333a160 160 0 0 1 150.016 154.389334l0.085333 5.312v255.978666h32a32 32 0 0 1 3.072 63.872l-3.072 0.149334h-682.666667a32 32 0 0 1-3.072-63.872l3.072-0.128 32-0.021334v-256a160.064 160.064 0 0 1 127.210667-156.629333v-67.754667a32 32 0 0 1 63.872-3.072l0.149333 3.072-0.021333 64.384h74.666667v-120.106666a32 32 0 0 1 32-32zM768 662.464H277.333333v141.930667h490.666667v-141.930667z m-96-199.381333h-298.666667a96 96 0 0 0-95.893333 91.477333l-0.106667 4.522667v39.381333h490.666667v-39.381333a96 96 0 0 0-86.890667-95.573334l-4.586666-0.341333-4.522667-0.085333z m-22.101333-221.12a32 32 0 0 1 31.850666 28.928l0.149334 3.072v7.786666a32 32 0 0 1-63.850667 3.072l-0.149333-3.072v-7.786666a32 32 0 0 1 32-32z m-277.333334-24.384a32 32 0 0 1 31.850667 28.928l0.149333 3.072v10.837333a32 32 0 0 1-63.850666 3.072l-0.149334-3.072v-10.837333a32 32 0 0 1 32-32zM511.232 149.333333a32 32 0 0 1 31.850667 28.928l0.149333 3.072v15.082667a32 32 0 0 1-63.850667 3.072l-0.149333-3.072V181.333333a32 32 0 0 1 32-32z" p-id="11614"></path></svg>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<svg t="1750822072505" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5214" width="256" height="256"><path d="M519.658935 673.068396c81.129762 0 157.397796-31.572066 214.762295-88.937588 118.423133-118.421087 118.423133-311.085084 0-429.507194C677.057755 97.259114 600.788697 65.668629 519.658935 65.668629c-81.128739 0-157.39882 31.590486-214.744899 88.954985-118.402667 118.42211-118.402667 311.086108 0 429.507194C362.279558 641.496329 438.548616 673.068396 519.658935 673.068396zM346.474594 196.183148c46.267766-46.266743 107.764376-71.734788 173.184341-71.734788 65.419965 0 126.937041 25.468045 173.203784 71.734788 95.499028 95.500051 95.499028 250.889097 0 346.389148-46.266743 46.245254-107.765399 71.713298-173.203784 71.713298-65.420989 0-126.916575-25.468045-173.184341-71.713298C250.995009 447.072245 250.995009 291.682175 346.474594 196.183148z" p-id="5215"></path><path d="M793.986861 800.462854 549.061592 800.462854l0.008186-71.2743c0-16.22452-13.164834-29.389354-29.391401-29.389354s-29.389354 13.163811-29.389354 29.389354l-0.007163 71.2743L245.349429 800.462854c-16.225543 0-29.389354 13.163811-29.389354 29.390377s13.163811 29.392424 29.389354 29.392424l244.925269 0-0.00614 71.236438c0 16.22452 13.163811 29.389354 29.390377 29.389354s29.391401-13.163811 29.391401-29.389354l0.007163-71.236438L793.986861 859.245655c16.225543 0 29.391401-13.165858 29.391401-29.392424S810.213427 800.462854 793.986861 800.462854z" p-id="5216"></path></svg>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<div class="pi-cropper">
|
||||||
|
<div class="pi-cropper__img">
|
||||||
|
<img :src="src" ref="imgRef">
|
||||||
|
</div>
|
||||||
|
<div class="pi-cropper__preview">
|
||||||
|
<h4>图像预览</h4>
|
||||||
|
<div class="pi-cropper__preview__img" ref="previewRef"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import Cropper from 'cropperjs'
|
||||||
|
import 'cropperjs/dist/cropper.css'
|
||||||
|
import {ref, watch, onMounted} from "vue";
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getCropFile
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
src: { type: String, default: "" },
|
||||||
|
compress: {type: Number, default: 1},
|
||||||
|
aspectRatio: {type: Number, default: NaN},
|
||||||
|
})
|
||||||
|
const imgRef = ref(null)
|
||||||
|
const previewRef = ref(null)
|
||||||
|
|
||||||
|
let crop = ref(null)
|
||||||
|
watch(() => props.aspectRatio, (val) => {
|
||||||
|
crop.value.setAspectRatio(val)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
|
||||||
|
function init(){
|
||||||
|
crop.value = new Cropper(imgRef.value, {
|
||||||
|
viewMode: 2,
|
||||||
|
dragMode: 'move',
|
||||||
|
responsive: false,
|
||||||
|
aspectRatio: props.aspectRatio,
|
||||||
|
preview: previewRef.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function setAspectRatio(aspectRatio){
|
||||||
|
crop.value.setAspectRatio(aspectRatio)
|
||||||
|
}
|
||||||
|
function getCropData(cb, type='image/jpeg'){
|
||||||
|
cb(crop.value.getCroppedCanvas().toDataURL(type, props.compress))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCropBlob(cb, type='image/jpeg'){
|
||||||
|
crop.value.getCroppedCanvas().toBlob((blob) => {
|
||||||
|
cb(blob)
|
||||||
|
}, type, props.compress)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCropFile(cb, fileName='fileName.jpg', type='image/jpeg'){
|
||||||
|
crop.value.getCroppedCanvas().toBlob((blob) => {
|
||||||
|
let file = new File([blob], fileName, {type: type})
|
||||||
|
cb(file)
|
||||||
|
}, type, props.compress)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pi-cropper {height:300px;}
|
||||||
|
.pi-cropper__img {height:100%;width:400px;float: left;background: #EBEEF5;}
|
||||||
|
.pi-cropper__img img {display: none;}
|
||||||
|
.pi-cropper__preview {width: 120px;margin-left: 20px;float: left;}
|
||||||
|
.pi-cropper__preview h4 {font-weight: normal;font-size: 12px;color: #999;margin-bottom: 20px;}
|
||||||
|
.pi-cropper__preview__img {overflow: hidden;width: 120px;height: 120px;border: 1px solid #ebeef5;}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
<template>
|
||||||
|
<div class="pi-password-strength">
|
||||||
|
<div class="pi-password-strength-bar" :class="`pi-password-strength-level-${level}`"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {ref, watch, onMounted} from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: String, default: "" },
|
||||||
|
})
|
||||||
|
let level = ref(0)
|
||||||
|
|
||||||
|
watch(() => props.modelValue, () => {
|
||||||
|
strength(props.modelValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
strength(props.modelValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
function strength(v){
|
||||||
|
var _level = 0
|
||||||
|
//长度
|
||||||
|
var has_length = v.length >= 6
|
||||||
|
//包含数字
|
||||||
|
var has_number = /\d/.test(v)
|
||||||
|
//包含小写英文
|
||||||
|
var has_lovercase = /[a-z]/.test(v)
|
||||||
|
//包含大写英文
|
||||||
|
var has_uppercase = /[A-Z]/.test(v)
|
||||||
|
//没有连续的字符3位
|
||||||
|
var no_continuity = !/(\w)\1{2}/.test(v)
|
||||||
|
//包含特殊字符
|
||||||
|
var has_special = /[`~!@#$%^&*()_+<>?:"{},./;'[\]]/.test(v)
|
||||||
|
|
||||||
|
if(v.length <= 0){
|
||||||
|
_level = 0
|
||||||
|
level.value = _level
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if(!has_length){
|
||||||
|
_level = 1
|
||||||
|
level.value = _level
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if(has_number){
|
||||||
|
_level += 1
|
||||||
|
}
|
||||||
|
if(has_lovercase){
|
||||||
|
_level += 1
|
||||||
|
}
|
||||||
|
if(has_uppercase){
|
||||||
|
_level += 1
|
||||||
|
}
|
||||||
|
if(no_continuity){
|
||||||
|
_level += 1
|
||||||
|
}
|
||||||
|
if(has_special){
|
||||||
|
_level += 1
|
||||||
|
}
|
||||||
|
level.value = _level
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pi-password-strength {height: 5px;width: 100%;background: var(--el-color-info-light-5);border-radius: 5px;position: relative;margin:10px 0;}
|
||||||
|
.pi-password-strength:before {left: 20%;}
|
||||||
|
.pi-password-strength:after {right: 20%;}
|
||||||
|
.pi-password-strength:before, .pi-password-strength:after {position: absolute;content: "";display: block;width: 20%;height: inherit;border: 5px solid var(--el-bg-color-overlay);border-top: 0;border-bottom: 0;z-index: 1;background-color: transparent;box-sizing: border-box;}
|
||||||
|
.pi-password-strength-bar {position: absolute;height: inherit;width: 0%;border-radius: inherit;transition: width .5s ease-in-out,background .25s;background: transparent;}
|
||||||
|
.pi-password-strength-level-1 {width: 20%;background-color: var(--el-color-error);}
|
||||||
|
.pi-password-strength-level-2 {width: 40%;background-color: var(--el-color-error);}
|
||||||
|
.pi-password-strength-level-3 {width: 60%;background-color: var(--el-color-warning);}
|
||||||
|
.pi-password-strength-level-4 {width: 80%;background-color: var(--el-color-success);}
|
||||||
|
.pi-password-strength-level-5 {width: 100%;background-color: var(--el-color-success);}
|
||||||
|
</style>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-header>
|
<el-header v-if="!hideAct">
|
||||||
<div class="left-panel">
|
<div class="left-panel">
|
||||||
<slot name="do"></slot>
|
<slot name="do"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -125,6 +125,7 @@ const props = defineProps({
|
||||||
remoteSummary: {type: Boolean, default: false},
|
remoteSummary: {type: Boolean, default: false},
|
||||||
hidePagination: {type: Boolean, default: false},
|
hidePagination: {type: Boolean, default: false},
|
||||||
hideDo: {type: Boolean, default: false},
|
hideDo: {type: Boolean, default: false},
|
||||||
|
hideAct: {type: Boolean, default: false},
|
||||||
hideRefresh: {type: Boolean, default: false},
|
hideRefresh: {type: Boolean, default: false},
|
||||||
hideSetting: {type: Boolean, default: false},
|
hideSetting: {type: Boolean, default: false},
|
||||||
paginationLayout: {type: String, default: config.paginationLayout},
|
paginationLayout: {type: String, default: config.paginationLayout},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
<template>
|
||||||
|
<div class="pi-upload-file">
|
||||||
|
<el-upload
|
||||||
|
:disabled="disabled"
|
||||||
|
:auto-upload="autoUpload"
|
||||||
|
:action="action"
|
||||||
|
:name="name"
|
||||||
|
:data="data"
|
||||||
|
:http-request="request"
|
||||||
|
v-model:file-list="defaultFileList"
|
||||||
|
:show-file-list="showFileList"
|
||||||
|
:drag="drag"
|
||||||
|
:accept="accept"
|
||||||
|
:multiple="multiple"
|
||||||
|
:limit="limit"
|
||||||
|
:before-upload="before"
|
||||||
|
:on-success="success"
|
||||||
|
:on-error="error"
|
||||||
|
:on-preview="handlePreview"
|
||||||
|
:on-exceed="handleExceed">
|
||||||
|
<slot>
|
||||||
|
<el-button type="primary" :disabled="disabled">上传</el-button>
|
||||||
|
</slot>
|
||||||
|
<template #tip>
|
||||||
|
<div v-if="tip" class="el-upload__tip">{{tip}}</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
<span style="display:none!important"><el-input v-model="value"></el-input></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import config from "@/config/upload"
|
||||||
|
import {ref, watch, onMounted, getCurrentInstance} from "vue";
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: [String, Array], default: "" },
|
||||||
|
tip: { type: String, default: "" },
|
||||||
|
action: { type: String, default: "" },
|
||||||
|
apiObj: { type: Function, default: () => {} },
|
||||||
|
name: { type: String, default: config.filename },
|
||||||
|
data: { type: Object, default: () => {} },
|
||||||
|
accept: { type: String, default: "" },
|
||||||
|
maxSize: { type: Number, default: config.maxSizeFile },
|
||||||
|
limit: { type: Number, default: 0 },
|
||||||
|
autoUpload: { type: Boolean, default: true },
|
||||||
|
showFileList: { type: Boolean, default: true },
|
||||||
|
drag: { type: Boolean, default: false },
|
||||||
|
multiple: { type: Boolean, default: true },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
onSuccess: { type: Function, default: () => { return true } }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const {proxy} = getCurrentInstance()
|
||||||
|
let value = ref('')
|
||||||
|
let defaultFileList = ref([])
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
if(Array.isArray(val)){
|
||||||
|
if (JSON.stringify(val) != JSON.stringify(formatArr(defaultFileList.value))) {
|
||||||
|
defaultFileList.value = val
|
||||||
|
value.value = val
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
if (val != toStr(defaultFileList.value)) {
|
||||||
|
defaultFileList.value = toArr(val)
|
||||||
|
value.value = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(defaultFileList, (val) => {
|
||||||
|
emit('update:modelValue', Array.isArray(props.modelValue) ? formatArr(val) : toStr(val))
|
||||||
|
value.value = toStr(val)
|
||||||
|
}, {deep: true})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
defaultFileList.value = Array.isArray(props.modelValue) ? props.modelValue : toArr(props.modelValue)
|
||||||
|
value.value = props.modelValue
|
||||||
|
})
|
||||||
|
|
||||||
|
//默认值转换为数组
|
||||||
|
function toArr(str){
|
||||||
|
var _arr = []
|
||||||
|
var arr = str.split(",")
|
||||||
|
arr.forEach(item => {
|
||||||
|
if(item){
|
||||||
|
var urlArr = item.split('/');
|
||||||
|
var fileName = urlArr[urlArr.length - 1]
|
||||||
|
_arr.push({
|
||||||
|
name: fileName,
|
||||||
|
url: item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return _arr
|
||||||
|
}
|
||||||
|
|
||||||
|
//数组转换为原始值
|
||||||
|
function toStr(arr){
|
||||||
|
return arr.map(v => v.url).join(",")
|
||||||
|
}
|
||||||
|
|
||||||
|
//格式化数组值
|
||||||
|
function formatArr(arr){
|
||||||
|
var _arr = []
|
||||||
|
arr.forEach(item => {
|
||||||
|
if(item){
|
||||||
|
_arr.push({
|
||||||
|
name: item.name,
|
||||||
|
url: item.url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return _arr
|
||||||
|
}
|
||||||
|
|
||||||
|
function before(file){
|
||||||
|
const maxSize = file.size / 1024 / 1024 < props.maxSize;
|
||||||
|
if (!maxSize) {
|
||||||
|
proxy.$message.warning(`上传文件大小不能超过 ${props.maxSize}MB!`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function success(res, file){
|
||||||
|
var os = props.onSuccess(res, file)
|
||||||
|
if(os!=undefined && os==false){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
file.name = res.fileName
|
||||||
|
file.url = res.src
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(err){
|
||||||
|
proxy.$notify.error({
|
||||||
|
title: '上传文件未成功',
|
||||||
|
message: err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeRemove(uploadFile){
|
||||||
|
return proxy.$confirm(`是否移除 ${uploadFile.name} ?`, '提示', {
|
||||||
|
type: 'warning',
|
||||||
|
}).then(() => {
|
||||||
|
return true
|
||||||
|
}).catch(() => {
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExceed(){
|
||||||
|
proxy.$message.warning(`当前设置最多上传 ${props.limit} 个文件,请移除后上传!`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePreview(uploadFile){
|
||||||
|
window.open(uploadFile.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function request(param){
|
||||||
|
var apiObj = config.apiObjFile;
|
||||||
|
if(props.apiObj){
|
||||||
|
apiObj = props.apiObj;
|
||||||
|
}
|
||||||
|
const data = new FormData();
|
||||||
|
data.append(param.filename, param.file);
|
||||||
|
for (const key in param.data) {
|
||||||
|
data.append(key, param.data[key]);
|
||||||
|
}
|
||||||
|
apiObj(data, {
|
||||||
|
onUploadProgress: e => {
|
||||||
|
const complete = parseInt(((e.loaded / e.total) * 100) | 0, 10)
|
||||||
|
param.onProgress({percent: complete})
|
||||||
|
}
|
||||||
|
}).then(res => {
|
||||||
|
var response = config.parseData(res.data);
|
||||||
|
if(res.code == config.successCode){
|
||||||
|
param.onSuccess(response)
|
||||||
|
}else{
|
||||||
|
param.onError(res.msg || "未知错误")
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
param.onError(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.el-form-item.is-error .pi-upload-file:deep(.el-upload-dragger) {border-color: var(--el-color-danger);}
|
||||||
|
.pi-upload-file {width: 100%;}
|
||||||
|
.pi-upload-file:deep(.el-upload-list__item) {transition: none !important;}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,285 @@
|
||||||
|
<template>
|
||||||
|
<div class="pi-upload" :class="{'pi-upload-round':round}" :style="style">
|
||||||
|
<div v-if="file && file.status != 'success'" class="pi-upload__uploading">
|
||||||
|
<div class="pi-upload__progress">
|
||||||
|
<el-progress :percentage="file.percentage" :text-inside="true" :stroke-width="16"/>
|
||||||
|
</div>
|
||||||
|
<el-image class="image" :src="file.tempFile" fit="cover"></el-image>
|
||||||
|
</div>
|
||||||
|
<div v-if="file && file.status=='success'" class="pi-upload__img">
|
||||||
|
<el-image class="image" :src="file.url" :preview-src-list="[file.url]" fit="cover" preview-teleported :z-index="9999">
|
||||||
|
<template #placeholder>
|
||||||
|
<div class="pi-upload__img-slot">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
<div class="pi-upload__img-actions" v-if="!disabled">
|
||||||
|
<span class="del" @click="handleRemove()"><el-icon><el-icon-delete /></el-icon></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-upload v-if="!file" class="uploader" ref="uploaderRef"
|
||||||
|
:auto-upload="cropper?false:autoUpload"
|
||||||
|
:disabled="disabled"
|
||||||
|
:show-file-list="showFileList"
|
||||||
|
:action="action"
|
||||||
|
:name="name"
|
||||||
|
:data="data"
|
||||||
|
:accept="accept"
|
||||||
|
:limit="1"
|
||||||
|
:http-request="request"
|
||||||
|
:on-change="change"
|
||||||
|
:before-upload="before"
|
||||||
|
:on-success="success"
|
||||||
|
:on-error="error"
|
||||||
|
:on-exceed="handleExceed">
|
||||||
|
<slot>
|
||||||
|
<div class="el-upload--picture-card" :style="style">
|
||||||
|
<div class="file-empty">
|
||||||
|
<el-icon><component :is="icon" /></el-icon>
|
||||||
|
<h4 v-if="title">{{title}}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</el-upload>
|
||||||
|
<span style="display:none!important"><el-input v-model="value"></el-input></span>
|
||||||
|
<el-dialog title="剪裁" draggable v-model="cropperDialogVisible" :width="580" @closed="cropperClosed" destroy-on-close>
|
||||||
|
<pi-cropper :src="cropperFile.tempCropperFile" :compress="compress" :aspectRatio="aspectRatio" ref="cropperRef"></pi-cropper>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="cropperDialogVisible=false" >取 消</el-button>
|
||||||
|
<el-button type="primary" @click="cropperSave">确 定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {defineAsyncComponent, ref, watch, onMounted, getCurrentInstance, nextTick} from 'vue'
|
||||||
|
import { genFileId } from 'element-plus'
|
||||||
|
import config from "@/config/upload"
|
||||||
|
const piCropper = defineAsyncComponent(() => import('@/components/piCropper'))
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const cropperRef = ref(null)
|
||||||
|
const uploaderRef = ref(null)
|
||||||
|
const {proxy} = getCurrentInstance()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: String, default: "" },
|
||||||
|
height: {type: Number, default: 148},
|
||||||
|
width: {type: Number, default: 148},
|
||||||
|
title: { type: String, default: "" },
|
||||||
|
icon: { type: String, default: "el-icon-plus" },
|
||||||
|
action: { type: String, default: "" },
|
||||||
|
apiObj: { type: Object, default: () => {} },
|
||||||
|
name: { type: String, default: config.filename },
|
||||||
|
data: { type: Object, default: () => {} },
|
||||||
|
accept: { type: String, default: "image/gif, image/jpeg, image/png" },
|
||||||
|
maxSize: { type: Number, default: config.maxSizeFile },
|
||||||
|
limit: { type: Number, default: 1 },
|
||||||
|
autoUpload: { type: Boolean, default: true },
|
||||||
|
showFileList: { type: Boolean, default: false },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
round: { type: Boolean, default: false },
|
||||||
|
onSuccess: { type: Function, default: () => { return true } },
|
||||||
|
cropper: { type: Boolean, default: false },
|
||||||
|
compress: {type: Number, default: 1},
|
||||||
|
aspectRatio: {type: Number, default: NaN}
|
||||||
|
})
|
||||||
|
|
||||||
|
let value = ref("")
|
||||||
|
let file = ref(null)
|
||||||
|
let style = ref({
|
||||||
|
width: props.width + "px",
|
||||||
|
height: props.height + "px"
|
||||||
|
})
|
||||||
|
let cropperDialogVisible = ref(false)
|
||||||
|
let cropperFile = ref(null)
|
||||||
|
|
||||||
|
watch(() => props.modelValue, function (val) {
|
||||||
|
value.value = val
|
||||||
|
newFile(val)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(value, function (val) {
|
||||||
|
emit('update:modelValue', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
value.value = props.modelValue
|
||||||
|
newFile(props.modelValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
function newFile(url){
|
||||||
|
if(url){
|
||||||
|
file.value = {
|
||||||
|
status: "success",
|
||||||
|
url: url
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
file.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cropperSave(){
|
||||||
|
cropperRef.value.getCropFile(f => {
|
||||||
|
f.uid = cropperFile.value.uid
|
||||||
|
cropperFile.value.raw = f
|
||||||
|
file.value = cropperFile.value
|
||||||
|
file.value.tempFile = URL.createObjectURL(file.value.raw)
|
||||||
|
uploaderRef.value.submit()
|
||||||
|
|
||||||
|
}, cropperFile.value.name, cropperFile.value.type)
|
||||||
|
cropperDialogVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function cropperClosed(){
|
||||||
|
URL.revokeObjectURL(cropperFile.value.tempCropperFile)
|
||||||
|
delete cropperFile.value.tempCropperFile
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemove(){
|
||||||
|
clearFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFiles(){
|
||||||
|
URL.revokeObjectURL(file.value.tempFile)
|
||||||
|
value.value = ""
|
||||||
|
file.value = null
|
||||||
|
nextTick(()=>{
|
||||||
|
uploaderRef.value.clearFiles()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function change(f, files){
|
||||||
|
if(files.length > 1){
|
||||||
|
files.splice(0, 1)
|
||||||
|
}
|
||||||
|
if(props.cropper && f.status=='ready'){
|
||||||
|
const acceptIncludes = ["image/gif", "image/jpeg", "image/png"].includes(f.raw.type)
|
||||||
|
if(!acceptIncludes){
|
||||||
|
proxy.$notify.warning({
|
||||||
|
title: '上传文件警告',
|
||||||
|
message: '选择的文件非图像类文件'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
cropperFile.value = f
|
||||||
|
cropperFile.value.tempCropperFile = URL.createObjectURL(f.raw)
|
||||||
|
cropperDialogVisible.value = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
file.value = f
|
||||||
|
if(f.status=='ready'){
|
||||||
|
f.tempFile = URL.createObjectURL(f.raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function before(file){
|
||||||
|
const acceptIncludes = props.accept.replace(/\s/g,"").split(",").includes(file.type)
|
||||||
|
if(!acceptIncludes){
|
||||||
|
proxy.$notify.warning({
|
||||||
|
title: '上传文件警告',
|
||||||
|
message: '选择的文件非图像类文件'
|
||||||
|
})
|
||||||
|
clearFiles()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const maxSize = file.size / 1024 / 1024 < props.maxSize;
|
||||||
|
if (!maxSize) {
|
||||||
|
proxy.$message.warning(`上传文件大小不能超过 ${props.maxSize}MB!`);
|
||||||
|
clearFiles()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExceed(files){
|
||||||
|
const file = files[0]
|
||||||
|
file.uid = genFileId()
|
||||||
|
uploaderRef.value.handleStart(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
function success(res, f){
|
||||||
|
//释放内存删除blob
|
||||||
|
URL.revokeObjectURL(f.tempFile)
|
||||||
|
delete f.tempFile
|
||||||
|
var os = props.onSuccess(res, f)
|
||||||
|
if(os!=undefined && os==false){
|
||||||
|
proxy.$nextTick(() => {
|
||||||
|
file.value = null
|
||||||
|
value.value = ""
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
f.url = res.src
|
||||||
|
value.value = f.url
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(err){
|
||||||
|
nextTick(()=>{
|
||||||
|
clearFiles()
|
||||||
|
})
|
||||||
|
proxy.$notify.error({
|
||||||
|
title: '上传文件未成功',
|
||||||
|
message: err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function request(param){
|
||||||
|
var apiObj = config.apiObj;
|
||||||
|
if(props.apiObj){
|
||||||
|
apiObj = props.apiObj;
|
||||||
|
}
|
||||||
|
const data = new FormData();
|
||||||
|
data.append(param.filename, param.file);
|
||||||
|
for (const key in param.data) {
|
||||||
|
data.append(key, param.data[key]);
|
||||||
|
}
|
||||||
|
apiObj(data, {
|
||||||
|
onUploadProgress: e => {
|
||||||
|
const complete = parseInt(((e.loaded / e.total) * 100) | 0, 10)
|
||||||
|
param.onProgress({percent: complete})
|
||||||
|
}
|
||||||
|
}).then(res => {
|
||||||
|
if (res.code === config.successCode) {
|
||||||
|
var response = config.parseData(res.data);
|
||||||
|
param.onSuccess(response)
|
||||||
|
}else {
|
||||||
|
param.onError(res.msg)
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
param.onError(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.el-form-item.is-error .pi-upload .el-upload--picture-card {border-color: var(--el-color-danger);}
|
||||||
|
.pi-upload .el-upload--picture-card {border-radius: 0;}
|
||||||
|
|
||||||
|
.pi-upload .uploader,.pi-upload:deep(.el-upload) {width: 100%;height: 100%;}
|
||||||
|
|
||||||
|
.pi-upload__img {width: 100%;height: 100%;position: relative;}
|
||||||
|
.pi-upload__img .image {width: 100%;height: 100%;}
|
||||||
|
.pi-upload__img-actions {position: absolute;top:0;right: 0;display: none;}
|
||||||
|
.pi-upload__img-actions span {display: flex;justify-content: center;align-items: center;width: 25px;height:25px;cursor: pointer;color: #fff;}
|
||||||
|
.pi-upload__img-actions span i {font-size: 12px;}
|
||||||
|
.pi-upload__img-actions .del {background: #F56C6C;}
|
||||||
|
.pi-upload__img:hover .pi-upload__img-actions {display: block;}
|
||||||
|
.pi-upload__img-slot {display: flex;justify-content: center;align-items: center;width: 100%;height: 100%;font-size: 12px;background-color: var(--el-fill-color-lighter);}
|
||||||
|
|
||||||
|
.pi-upload__uploading {width: 100%;height: 100%;position: relative;}
|
||||||
|
.pi-upload__progress {position: absolute;width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;background-color: var(--el-overlay-color-lighter);z-index: 1;padding:10px;}
|
||||||
|
.pi-upload__progress .el-progress {width: 100%;}
|
||||||
|
.pi-upload__uploading .image {width: 100%;height: 100%;}
|
||||||
|
|
||||||
|
.pi-upload .file-empty {width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;flex-direction: column;}
|
||||||
|
.pi-upload .file-empty i {font-size: 28px;}
|
||||||
|
.pi-upload .file-empty h4 {font-size: 12px;font-weight: normal;color: #8c939d;margin-top: 8px;}
|
||||||
|
|
||||||
|
.pi-upload.pi-upload-round {border-radius: 50%;overflow: hidden;}
|
||||||
|
.pi-upload.pi-upload-round .el-upload--picture-card {border-radius: 50%;}
|
||||||
|
.pi-upload.pi-upload-round .pi-upload__img-actions {top: auto;left: 0;right: 0;bottom: 0;}
|
||||||
|
.pi-upload.pi-upload-round .pi-upload__img-actions span {width: 100%;}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,257 @@
|
||||||
|
<template>
|
||||||
|
<div class="pi-upload-multiple">
|
||||||
|
<el-upload ref="uploaderRef" list-type="picture-card"
|
||||||
|
:auto-upload="autoUpload"
|
||||||
|
:disabled="disabled"
|
||||||
|
:action="action"
|
||||||
|
:name="name"
|
||||||
|
:data="data"
|
||||||
|
:http-request="request"
|
||||||
|
v-model:file-list="defaultFileList"
|
||||||
|
:show-file-list="showFileList"
|
||||||
|
:accept="accept"
|
||||||
|
:multiple="multiple"
|
||||||
|
:limit="limit"
|
||||||
|
:before-upload="before"
|
||||||
|
:on-success="success"
|
||||||
|
:on-error="error"
|
||||||
|
:on-preview="handlePreview"
|
||||||
|
:on-exceed="handleExceed">
|
||||||
|
<slot>
|
||||||
|
<el-icon><el-icon-plus/></el-icon>
|
||||||
|
</slot>
|
||||||
|
<template #tip>
|
||||||
|
<div v-if="tip" class="el-upload__tip">{{tip}}</div>
|
||||||
|
</template>
|
||||||
|
<template #file="{ file }">
|
||||||
|
<div class="pi-upload-list-item">
|
||||||
|
<el-image class="el-upload-list__item-thumbnail" :src="file.url" fit="cover" :preview-src-list="preview" :initial-index="preview.findIndex(n=>n==file.url)" hide-on-click-modal append-to-body :z-index="9999">
|
||||||
|
<template #placeholder>
|
||||||
|
<div class="pi-upload-multiple-image-slot">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
<div v-if="!disabled && file.status=='success'" class="pi-upload__item-actions">
|
||||||
|
<span class="del" @click="handleRemove(file)"><el-icon><el-icon-delete /></el-icon></span>
|
||||||
|
</div>
|
||||||
|
<div v-if="file.status=='ready' || file.status=='uploading'" class="pi-upload__item-progress">
|
||||||
|
<el-progress :percentage="file.percentage" :text-inside="true" :stroke-width="16"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
<span style="display:none!important"><el-input v-model="value"></el-input></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import config from "@/config/upload"
|
||||||
|
import Sortable from 'sortablejs'
|
||||||
|
import {ref, getCurrentInstance, watch, computed, onMounted} from "vue"
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const {proxy} = getCurrentInstance()
|
||||||
|
const uploaderRef = ref(null)
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: [String, Array], default: "" },
|
||||||
|
tip: { type: String, default: "" },
|
||||||
|
action: { type: String, default: "" },
|
||||||
|
apiObj: { type: Object, default: () => {} },
|
||||||
|
name: { type: String, default: config.filename },
|
||||||
|
data: { type: Object, default: () => {} },
|
||||||
|
accept: { type: String, default: "image/gif, image/jpeg, image/png" },
|
||||||
|
maxSize: { type: Number, default: config.maxSizeFile },
|
||||||
|
limit: { type: Number, default: 0 },
|
||||||
|
autoUpload: { type: Boolean, default: true },
|
||||||
|
showFileList: { type: Boolean, default: true },
|
||||||
|
multiple: { type: Boolean, default: true },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
draggable: { type: Boolean, default: false },
|
||||||
|
onSuccess: { type: Function, default: () => { return true } }
|
||||||
|
})
|
||||||
|
|
||||||
|
let value = ref("")
|
||||||
|
let defaultFileList = ref([])
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
if(Array.isArray(val)){
|
||||||
|
if (JSON.stringify(val) != JSON.stringify(formatArr(defaultFileList.value))) {
|
||||||
|
defaultFileList.value = val
|
||||||
|
value.value = val
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
if (val != toStr(defaultFileList.value)) {
|
||||||
|
defaultFileList.value = toArr(val)
|
||||||
|
value.value = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(defaultFileList, (val) => {
|
||||||
|
emit('update:modelValue', Array.isArray(props.modelValue) ? formatArr(val) : toStr(val))
|
||||||
|
value.value = toStr(val)
|
||||||
|
}, {deep: true})
|
||||||
|
|
||||||
|
const preview = computed(() => {
|
||||||
|
return defaultFileList.value.map(v => v.url)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.modelValue) {
|
||||||
|
defaultFileList.value = Array.isArray(props.modelValue) ? props.modelValue : toArr(props.modelValue)
|
||||||
|
}
|
||||||
|
value.value = props.modelValue
|
||||||
|
if(!props.disabled && props.draggable){
|
||||||
|
rowDrop()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//默认值转换为数组
|
||||||
|
function toArr(str){
|
||||||
|
var _arr = [];
|
||||||
|
var arr = str.split(",");
|
||||||
|
arr.forEach(item => {
|
||||||
|
if(item){
|
||||||
|
var urlArr = item.split('/');
|
||||||
|
var fileName = urlArr[urlArr.length - 1]
|
||||||
|
_arr.push({
|
||||||
|
name: fileName,
|
||||||
|
url: item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return _arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
//数组转换为原始值
|
||||||
|
function toStr(arr){
|
||||||
|
return arr.map(v => v.url).join(",")
|
||||||
|
}
|
||||||
|
|
||||||
|
//格式化数组值
|
||||||
|
function formatArr(arr){
|
||||||
|
var _arr = []
|
||||||
|
arr.forEach(item => {
|
||||||
|
if(item){
|
||||||
|
_arr.push({
|
||||||
|
name: item.name,
|
||||||
|
url: item.url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return _arr
|
||||||
|
}
|
||||||
|
|
||||||
|
//拖拽
|
||||||
|
function rowDrop(){
|
||||||
|
const itemBox = uploaderRef.value.$el.querySelector('.el-upload-list')
|
||||||
|
Sortable.create(itemBox, {
|
||||||
|
handle: ".el-upload-list__item",
|
||||||
|
animation: 200,
|
||||||
|
ghostClass: "ghost",
|
||||||
|
onEnd({ newIndex, oldIndex }) {
|
||||||
|
const tableData = defaultFileList.value
|
||||||
|
const currRow = tableData.splice(oldIndex, 1)[0]
|
||||||
|
tableData.splice(newIndex, 0, currRow)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function before(file){
|
||||||
|
if(!['image/jpeg','image/png','image/gif'].includes(file.type)){
|
||||||
|
proxy.$message.warning(`选择的文件类型 ${file.type} 非图像类文件`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const maxSize = file.size / 1024 / 1024 < props.maxSize;
|
||||||
|
if (!maxSize) {
|
||||||
|
proxy.$message.warning(`上传文件大小不能超过 ${props.maxSize}MB!`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function success(res, file){
|
||||||
|
var os = props.onSuccess(res, file)
|
||||||
|
if(os!=undefined && os==false){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var response = config.parseData(res)
|
||||||
|
file.name = response.src
|
||||||
|
file.url = response.src
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(err){
|
||||||
|
proxy.$notify.error({
|
||||||
|
title: '上传文件未成功',
|
||||||
|
message: err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeRemove(uploadFile){
|
||||||
|
return proxy.$confirm(`是否移除 ${uploadFile.name} ?`, '提示', {
|
||||||
|
type: 'warning',
|
||||||
|
}).then(() => {
|
||||||
|
return true
|
||||||
|
}).catch(() => {
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemove(file){
|
||||||
|
uploaderRef.value.handleRemove(file)
|
||||||
|
//this.defaultFileList.splice(this.defaultFileList.findIndex(item => item.uid===file.uid), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExceed(){
|
||||||
|
proxy.$message.warning(`当前设置最多上传 ${props.limit} 个文件,请移除后上传!`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePreview(uploadFile){
|
||||||
|
window.open(uploadFile.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function request(param){
|
||||||
|
var apiObj = config.apiObj;
|
||||||
|
if(props.apiObj){
|
||||||
|
apiObj = props.apiObj;
|
||||||
|
}
|
||||||
|
const data = new FormData();
|
||||||
|
data.append(param.filename, param.file);
|
||||||
|
for (const key in param.data) {
|
||||||
|
data.append(key, param.data[key]);
|
||||||
|
}
|
||||||
|
apiObj(data, {
|
||||||
|
onUploadProgress: e => {
|
||||||
|
const complete = parseInt(((e.loaded / e.total) * 100) | 0, 10)
|
||||||
|
param.onProgress({percent: complete})
|
||||||
|
}
|
||||||
|
}).then(res => {
|
||||||
|
var response = config.parseData(res);
|
||||||
|
if(response.code == config.successCode){
|
||||||
|
param.onSuccess(res)
|
||||||
|
}else{
|
||||||
|
param.onError(response.msg || "未知错误")
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
param.onError(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.el-form-item.is-error .pi-upload-multiple:deep(.el-upload--picture-card) {border-color: var(--el-color-danger);}
|
||||||
|
:deep(.el-upload-list__item) {transition:none;border-radius: 0;}
|
||||||
|
.pi-upload-multiple:deep(.el-upload-list__item.el-list-leave-active) {position: static!important;}
|
||||||
|
.pi-upload-multiple:deep(.el-upload--picture-card) {border-radius: 0;}
|
||||||
|
.pi-upload-list-item {width: 100%;height: 100%;position: relative;}
|
||||||
|
.pi-upload-multiple .el-image {display: block;}
|
||||||
|
.pi-upload-multiple .el-image:deep(img) {-webkit-user-drag: none;}
|
||||||
|
.pi-upload-multiple-image-slot {display: flex;justify-content: center;align-items: center;width: 100%;height: 100%;font-size: 12px;}
|
||||||
|
.pi-upload-multiple .el-upload-list__item:hover .pi-upload__item-actions{display: block;}
|
||||||
|
.pi-upload__item-actions {position: absolute;top:0;right: 0;display: none;}
|
||||||
|
.pi-upload__item-actions span {display: flex;justify-content: center;align-items: center;;width: 25px;height:25px;cursor: pointer;color: #fff;}
|
||||||
|
.pi-upload__item-actions span i {font-size: 12px;}
|
||||||
|
.pi-upload__item-actions .del {background: #F56C6C;}
|
||||||
|
.pi-upload__item-progress {position: absolute;width: 100%;height: 100%;top: 0;left: 0;background-color: var(--el-overlay-color-lighter);}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import API from "@/api";
|
||||||
|
|
||||||
|
//上传配置
|
||||||
|
|
||||||
|
export default {
|
||||||
|
apiObj: API.system.upload, //上传请求API对象
|
||||||
|
filename: "file", //form请求时文件的key
|
||||||
|
successCode: 0, //请求完成代码
|
||||||
|
maxSize: 10, //最大文件大小 默认10MB
|
||||||
|
parseData: function (res) {
|
||||||
|
return {
|
||||||
|
fileName: res.fileName,
|
||||||
|
src: res.filePath,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
apiObjFile: API.system.upload, //附件上传请求API对象
|
||||||
|
maxSizeFile: 10 //最大文件大小 默认10MB
|
||||||
|
}
|
||||||
|
|
@ -178,7 +178,7 @@ function closeHandle() {
|
||||||
//个人信息
|
//个人信息
|
||||||
function handleUser(command) {
|
function handleUser(command) {
|
||||||
if (command === "uc") {
|
if (command === "uc") {
|
||||||
router.push({path: '/usercenter'});
|
router.push({path: '/system/user'});
|
||||||
}
|
}
|
||||||
if (command === "cmd") {
|
if (command === "cmd") {
|
||||||
router.push({path: '/cmd'});
|
router.push({path: '/cmd'});
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import errorHandler from "@/utils/errorHandler";
|
||||||
import piDialog from "@/components/piDialog"
|
import piDialog from "@/components/piDialog"
|
||||||
import piTable from "@/components/piTable"
|
import piTable from "@/components/piTable"
|
||||||
import piPage from "@/components/piPage"
|
import piPage from "@/components/piPage"
|
||||||
|
import piUpload from "@/components/piUpload"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
install(app: App) {
|
install(app: App) {
|
||||||
|
|
@ -19,6 +20,7 @@ export default {
|
||||||
app.component('piDialog', piDialog)
|
app.component('piDialog', piDialog)
|
||||||
app.component('piTable', piTable)
|
app.component('piTable', piTable)
|
||||||
app.component('piPage', piPage)
|
app.component('piPage', piPage)
|
||||||
|
app.component('piUpload', piUpload)
|
||||||
|
|
||||||
//注册全局指令
|
//注册全局指令
|
||||||
app.directive('auth', auth)
|
app.directive('auth', auth)
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,24 @@ const tools = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return arr
|
return arr
|
||||||
|
},
|
||||||
|
getBrowser(userAgent: string) {
|
||||||
|
// 检测浏览器类型和版本
|
||||||
|
if (/Opera|OPR/.test(userAgent)) {
|
||||||
|
return 'Opera';
|
||||||
|
} else if (/Edg/.test(userAgent)) {
|
||||||
|
return 'Edge';
|
||||||
|
} else if (/Chrome/.test(userAgent)) {
|
||||||
|
return 'Chrome';
|
||||||
|
} else if (/Safari/.test(userAgent)) {
|
||||||
|
return 'Safari';
|
||||||
|
} else if (/Firefox/.test(userAgent)) {
|
||||||
|
return 'Firefox';
|
||||||
|
} else if (/MSIE|Trident/.test(userAgent)) {
|
||||||
|
return 'IE';
|
||||||
|
} else {
|
||||||
|
return '未知'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,128 @@
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :lg="8" :xs="24" :sm="24" :md="24">
|
<el-col :lg="8" :xs="24" :sm="24" :md="24">
|
||||||
<el-card shadow="never" class="pi-left">
|
<el-card shadow="never" class="pi-left">
|
||||||
<el-avatar :src="userInfo.avatar" :size="64">{{nicknameF}}</el-avatar>
|
<div class="head">
|
||||||
<p style="font-size: 20px;font-weight: bold;margin: 8px;">{{userInfo.nickname||userInfo.username}}</p>
|
<el-avatar :src="userInfo.avatar" :size="64">{{ nicknameF }}</el-avatar>
|
||||||
<p>{{userInfo.bio}}</p>
|
<p style="font-size: 24px;font-weight: bold;margin: 8px;">
|
||||||
|
{{ userInfo.nickname || userInfo.username }}</p>
|
||||||
|
<p style="font-size: 14px; color: #888888;">{{ userInfo.bio }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="down">
|
||||||
|
<div class="item">
|
||||||
|
<!--岗位-->
|
||||||
|
<el-icon size="15">
|
||||||
|
<component :is="'pi-icon-post'"/>
|
||||||
|
</el-icon>
|
||||||
|
<el-text>{{ userInfo.posts.map(item => item.post_name)?.join(" && ") }}</el-text>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<!--生日-->
|
||||||
|
<el-icon size="16">
|
||||||
|
<component :is="'pi-icon-birthday'"/>
|
||||||
|
</el-icon>
|
||||||
|
<el-text>{{ userInfo.birthday || "保密" }}</el-text>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<!--性别-->
|
||||||
|
<el-icon size="16">
|
||||||
|
<component :is="'pi-icon-sex'"/>
|
||||||
|
</el-icon>
|
||||||
|
<el-text>{{ sex }}</el-text>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<!--部门-->
|
||||||
|
<el-icon size="16">
|
||||||
|
<component :is="'pi-icon-dept'"/>
|
||||||
|
</el-icon>
|
||||||
|
<el-text>{{ userInfo.dept?.join(" - ") }}</el-text>
|
||||||
|
</div>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
<!--标签-->
|
||||||
|
<div class="tags">
|
||||||
|
<el-tag v-for="tag in tags" :key="tag" closable @close="removeTag(tag)">{{ tag }}</el-tag>
|
||||||
|
<el-input
|
||||||
|
v-if="tagVisible"
|
||||||
|
v-model="tagInput"
|
||||||
|
ref="tagRef"
|
||||||
|
size="small"
|
||||||
|
@keyup.enter="saveTag"
|
||||||
|
@blur="saveTag"
|
||||||
|
style="width: 120px; margin-bottom: 10px;"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" plain v-else size="small" @click="tagShow"
|
||||||
|
icon="el-icon-plus" style="margin-bottom: 10px;"></el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :lg="16" :xs="24" :sm="24" :md="24">
|
<el-col :lg="16" :xs="24" :sm="24" :md="24">
|
||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
|
<el-tabs class="pi-right">
|
||||||
|
<el-tab-pane label="基本信息" name="0">
|
||||||
|
<el-form ref="infoRef" :model="form" style="max-width: 500px;" label-width="120px">
|
||||||
|
<el-form-item label="头像" prop="avatar">
|
||||||
|
<pi-upload v-model="form.avatar" title="头像" :cropper="true" :compress="1" :aspectRatio="1/1"></pi-upload>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="昵称" prop="nickname">
|
||||||
|
<el-input v-model="form.nickname"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="性别" prop="sex">
|
||||||
|
<el-select v-model="form.sex">
|
||||||
|
<el-option label="保密" :value="0" disabled/>
|
||||||
|
<el-option label="男" :value="1" />
|
||||||
|
<el-option label="女" :value="2" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="生日" prop="birthday">
|
||||||
|
<el-date-picker v-model="form.birthday" type="date" style="width: 100%;" value-format="YYYY-MM-DD"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="介绍" prop="bio">
|
||||||
|
<el-input v-model="form.bio" type="textarea"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :loading="isSaveing" @click="saveInfo">保 存</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="修改密码" name="1">
|
||||||
|
<el-alert title="密码更新成功后,您将被重定向到登录页面,您可以使用新密码重新登录。" type="info" show-icon style="margin-bottom: 15px;"/>
|
||||||
|
<el-form ref="passRef" :model="form2" :rules="rules" label-width="120px" style="max-width: 500px;">
|
||||||
|
<el-form-item label="当前密码" prop="old_password">
|
||||||
|
<el-input v-model="form2.old_password" type="password" show-password placeholder="请输入当前密码"></el-input>
|
||||||
|
<div class="el-form-item-msg">必须提供当前登录用户密码才能进行更改</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新密码" prop="new_password">
|
||||||
|
<el-input v-model="form2.new_password" type="password" show-password placeholder="请输入新密码"></el-input>
|
||||||
|
<pi-password-strength v-model="form2.new_password"></pi-password-strength>
|
||||||
|
<div class="el-form-item-msg">请输入包含英文、数字的6位以上密码</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认新密码" prop="confirm_password">
|
||||||
|
<el-input v-model="form2.confirm_password" type="password" show-password placeholder="请再次输入新密码"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button :loading="isSaveing" type="primary" @click="savePass">保存密码</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="登录日志" name="2">
|
||||||
|
<pi-table :apiObj="api.auth.loginLog" stripe hide-act :page-size="10">
|
||||||
|
<el-table-column label="#" type="index" width="50"></el-table-column>
|
||||||
|
<el-table-column label="操作" prop="title"></el-table-column>
|
||||||
|
<el-table-column label="状态" prop="code">
|
||||||
|
<template #default="scope">
|
||||||
|
{{scope.row.code == 0 ? '成功' : '失败'}}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="IP" prop="ip"></el-table-column>
|
||||||
|
<el-table-column label="UA" prop="ua">
|
||||||
|
<template #default="scope">
|
||||||
|
{{tools.getBrowser(scope.row.ua)}}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="时间" prop="create_time"></el-table-column>
|
||||||
|
</pi-table>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
@ -18,29 +133,164 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import tools from "@/utils/tools"
|
import tools from "@/utils/tools"
|
||||||
import api from "@/api/index.js";
|
import api from "@/api/index";
|
||||||
import {ref, onMounted} from "vue";
|
import {ref, onMounted, nextTick, getCurrentInstance, computed} from "vue";
|
||||||
|
import piPasswordStrength from '@/components/piPasswordStrength'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "systemUser"
|
name: "systemUser"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const {proxy} = getCurrentInstance()
|
||||||
let userInfo = ref(tools.data.get("USER_INFO"));
|
let userInfo = ref(tools.data.get("USER_INFO"));
|
||||||
|
const passRef = ref(null)
|
||||||
|
|
||||||
let nickname = userInfo.value.nickname || userInfo.value.username;
|
let nickname = userInfo.value.nickname || userInfo.value.username;
|
||||||
let nicknameF = nickname.substring(0, 1);
|
let nicknameF = nickname.substring(0, 1);
|
||||||
|
let tagVisible = ref(false)
|
||||||
|
let tagInput = ref("")
|
||||||
|
let tags = ref()
|
||||||
|
const tagRef = ref(null)
|
||||||
|
const form = ref({})
|
||||||
|
const isSaveing = ref(false)
|
||||||
|
const form2 = ref({
|
||||||
|
old_password: "",
|
||||||
|
new_password: "",
|
||||||
|
confirm_password: ""
|
||||||
|
})
|
||||||
|
const rules = ref({
|
||||||
|
old_password: [
|
||||||
|
{ required: true, message: '请输入当前密码'}
|
||||||
|
],
|
||||||
|
new_password: [
|
||||||
|
{ required: true, message: '请输入新密码'},
|
||||||
|
{ min: 6, max: 30, message: '密码长度在6-30位之间' },
|
||||||
|
{validator: (rule, value, callback) => {
|
||||||
|
if(value === form2.value.old_password) {
|
||||||
|
callback(new Error('新旧密码不能一致'));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
],
|
||||||
|
confirm_password: [
|
||||||
|
{ required: true, message: '请再次输入新密码'},
|
||||||
|
{ min: 6, max: 30, message: '密码长度在6-30位之间' },
|
||||||
|
{validator: (rule, value, callback) => {
|
||||||
|
if (value !== form2.value.new_password) {
|
||||||
|
callback(new Error('两次输入密码不一致'));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadUser()
|
loadUser()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const sex = computed(() => {
|
||||||
|
if (userInfo.value.sex == 1) {
|
||||||
|
return '男'
|
||||||
|
}else if (userInfo.value.sex == 2) {
|
||||||
|
return '女'
|
||||||
|
}else {
|
||||||
|
return '保密'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function loadUser() {
|
async function loadUser() {
|
||||||
const res = await api.auth.info()
|
const res = await api.auth.info()
|
||||||
userInfo.value = res.data
|
userInfo.value = res.data
|
||||||
|
tags.value = userInfo.value.tags ? userInfo.value.tags?.split(",") : []
|
||||||
|
Object.assign(form.value, res.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveTag() {
|
||||||
|
tagVisible.value = false
|
||||||
|
if (tagInput.value) {
|
||||||
|
tags.value.push(tagInput.value)
|
||||||
|
tagInput.value = ""
|
||||||
|
await saveTags()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeTag(tag) {
|
||||||
|
tags.value.splice(tags.value.indexOf(tag), 1)
|
||||||
|
await saveTags()
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagShow() {
|
||||||
|
tagVisible.value = true
|
||||||
|
nextTick(() => {
|
||||||
|
tagRef.value.input?.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveTags() {
|
||||||
|
const res = await api.auth.saveTag({tags: tags.value?.toString()})
|
||||||
|
proxy.$message.success(res.msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveInfo() {
|
||||||
|
isSaveing.value = true
|
||||||
|
const res = await api.auth.saveInfo(form.value)
|
||||||
|
proxy.$message.success(res.msg)
|
||||||
|
Object.assign(userInfo.value, form.value)
|
||||||
|
tools.data.set("USER_INFO", userInfo.value)
|
||||||
|
isSaveing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function sexOption(v) {
|
||||||
|
console.log(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePass() {
|
||||||
|
const validate = await passRef.value.validate().catch(() => {});
|
||||||
|
if(!validate){ return false }
|
||||||
|
const res = await api.auth.savePass(form2.value)
|
||||||
|
proxy.$message.success(res.msg)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="scss" scoped>
|
||||||
.pi-left {text-align: center;}
|
.pi-left {
|
||||||
|
padding: 30px 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-left .head {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-left .down {
|
||||||
|
margin: 30px 0
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-left .down .item {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-left .down .item .el-icon {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-left .down .tags .el-tag {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-left .down .tags .el-tag + .el-tag {
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-left .down .tags span:first-of-type {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-left .down .tags span:last-of-type {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue