账号管理
This commit is contained in:
parent
b390fb6b0b
commit
3ff4fa97a6
|
|
@ -12,6 +12,7 @@
|
|||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"axios": "^1.9.0",
|
||||
"cropperjs": "1.5.13",
|
||||
"crypto-js": "^4.2.0",
|
||||
"element-plus": "^2.9.10",
|
||||
"nprogress": "^0.2.0",
|
||||
|
|
|
|||
|
|
@ -16,4 +16,16 @@ export default {
|
|||
logout: async function(){
|
||||
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 {
|
||||
account: {
|
||||
list: async function(data={}){
|
||||
list: async function (data = {}) {
|
||||
return await http.get("account/list", data);
|
||||
},
|
||||
add: async function(data={}){
|
||||
add: async function (data = {}) {
|
||||
return await http.post("account/add", data);
|
||||
},
|
||||
edit: async function(data={}){
|
||||
edit: async function (data = {}) {
|
||||
return await http.put("account/edit", data);
|
||||
},
|
||||
del: async function(data={}){
|
||||
del: async function (data = {}) {
|
||||
return await http.delete("account/del", data);
|
||||
},
|
||||
},
|
||||
menu: {
|
||||
list: async function(data={}){
|
||||
list: async function (data = {}) {
|
||||
return await http.get("menu/list", data);
|
||||
},
|
||||
add: async function(data = {}){
|
||||
add: async function (data = {}) {
|
||||
return await http.post("menu/add", data);
|
||||
},
|
||||
edit: async function(data = {}){
|
||||
edit: async function (data = {}) {
|
||||
return await http.put("menu/edit", data);
|
||||
},
|
||||
del: async function(data = {}){
|
||||
del: async function (data = {}) {
|
||||
return await http.delete("menu/del", data);
|
||||
},
|
||||
option: async function(data={}){
|
||||
option: async function (data = {}) {
|
||||
return await http.get("menu/option", data);
|
||||
},
|
||||
quick: async function(data = {}){
|
||||
quick: async function (data = {}) {
|
||||
return await http.post("menu/quick", data);
|
||||
},
|
||||
},
|
||||
|
|
@ -86,4 +86,7 @@ export default {
|
|||
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>
|
||||
<el-container>
|
||||
<el-header>
|
||||
<el-header v-if="!hideAct">
|
||||
<div class="left-panel">
|
||||
<slot name="do"></slot>
|
||||
</div>
|
||||
|
|
@ -125,6 +125,7 @@ const props = defineProps({
|
|||
remoteSummary: {type: Boolean, default: false},
|
||||
hidePagination: {type: Boolean, default: false},
|
||||
hideDo: {type: Boolean, default: false},
|
||||
hideAct: {type: Boolean, default: false},
|
||||
hideRefresh: {type: Boolean, default: false},
|
||||
hideSetting: {type: Boolean, default: false},
|
||||
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) {
|
||||
if (command === "uc") {
|
||||
router.push({path: '/usercenter'});
|
||||
router.push({path: '/system/user'});
|
||||
}
|
||||
if (command === "cmd") {
|
||||
router.push({path: '/cmd'});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import errorHandler from "@/utils/errorHandler";
|
|||
import piDialog from "@/components/piDialog"
|
||||
import piTable from "@/components/piTable"
|
||||
import piPage from "@/components/piPage"
|
||||
import piUpload from "@/components/piUpload"
|
||||
|
||||
export default {
|
||||
install(app: App) {
|
||||
|
|
@ -19,6 +20,7 @@ export default {
|
|||
app.component('piDialog', piDialog)
|
||||
app.component('piTable', piTable)
|
||||
app.component('piPage', piPage)
|
||||
app.component('piUpload', piUpload)
|
||||
|
||||
//注册全局指令
|
||||
app.directive('auth', auth)
|
||||
|
|
|
|||
|
|
@ -143,6 +143,24 @@ const tools = {
|
|||
}
|
||||
}
|
||||
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-col :lg="8" :xs="24" :sm="24" :md="24">
|
||||
<el-card shadow="never" class="pi-left">
|
||||
<el-avatar :src="userInfo.avatar" :size="64">{{nicknameF}}</el-avatar>
|
||||
<p style="font-size: 20px;font-weight: bold;margin: 8px;">{{userInfo.nickname||userInfo.username}}</p>
|
||||
<p>{{userInfo.bio}}</p>
|
||||
<div class="head">
|
||||
<el-avatar :src="userInfo.avatar" :size="64">{{ nicknameF }}</el-avatar>
|
||||
<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-col>
|
||||
<el-col :lg="16" :xs="24" :sm="24" :md="24">
|
||||
<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-col>
|
||||
</el-row>
|
||||
|
|
@ -18,29 +133,164 @@
|
|||
|
||||
<script setup>
|
||||
import tools from "@/utils/tools"
|
||||
import api from "@/api/index.js";
|
||||
import {ref, onMounted} from "vue";
|
||||
import api from "@/api/index";
|
||||
import {ref, onMounted, nextTick, getCurrentInstance, computed} from "vue";
|
||||
import piPasswordStrength from '@/components/piPasswordStrength'
|
||||
|
||||
defineOptions({
|
||||
name: "systemUser"
|
||||
})
|
||||
|
||||
const {proxy} = getCurrentInstance()
|
||||
let userInfo = ref(tools.data.get("USER_INFO"));
|
||||
const passRef = ref(null)
|
||||
|
||||
let nickname = userInfo.value.nickname || userInfo.value.username;
|
||||
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(() => {
|
||||
loadUser()
|
||||
})
|
||||
|
||||
const sex = computed(() => {
|
||||
if (userInfo.value.sex == 1) {
|
||||
return '男'
|
||||
}else if (userInfo.value.sex == 2) {
|
||||
return '女'
|
||||
}else {
|
||||
return '保密'
|
||||
}
|
||||
})
|
||||
|
||||
async function loadUser() {
|
||||
const res = await api.auth.info()
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
.pi-left {text-align: center;}
|
||||
<style lang="scss" scoped>
|
||||
.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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue