feat: 新增个人中心页面
@ -47,8 +47,10 @@
|
|||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"pinia": "^2.1.4",
|
"pinia": "^2.1.4",
|
||||||
"qrcode": "^1.5.3",
|
|
||||||
"pinyin-pro": "^3.15.2",
|
"pinyin-pro": "^3.15.2",
|
||||||
|
"cropperjs": "^1.5.13",
|
||||||
|
"vue-tippy": "^6.2.0",
|
||||||
|
"qrcode": "^1.5.3",
|
||||||
"qs": "^6.11.2",
|
"qs": "^6.11.2",
|
||||||
"responsive-storage": "^2.2.0",
|
"responsive-storage": "^2.2.0",
|
||||||
"sortablejs": "^1.15.0",
|
"sortablejs": "^1.15.0",
|
||||||
|
39
pnpm-lock.yaml
generated
@ -28,6 +28,7 @@ specifiers:
|
|||||||
autoprefixer: ^10.4.14
|
autoprefixer: ^10.4.14
|
||||||
axios: ^1.4.0
|
axios: ^1.4.0
|
||||||
cloc: ^2.11.0
|
cloc: ^2.11.0
|
||||||
|
cropperjs: ^1.5.13
|
||||||
crypto-js: ^4.1.1
|
crypto-js: ^4.1.1
|
||||||
cssnano: ^6.0.1
|
cssnano: ^6.0.1
|
||||||
dayjs: ^1.11.8
|
dayjs: ^1.11.8
|
||||||
@ -86,6 +87,7 @@ specifiers:
|
|||||||
vue: ^3.3.4
|
vue: ^3.3.4
|
||||||
vue-eslint-parser: ^9.3.1
|
vue-eslint-parser: ^9.3.1
|
||||||
vue-router: ^4.2.2
|
vue-router: ^4.2.2
|
||||||
|
vue-tippy: ^6.2.0
|
||||||
vue-tsc: ^1.8.1
|
vue-tsc: ^1.8.1
|
||||||
vue-types: ^5.1.0
|
vue-types: ^5.1.0
|
||||||
xlsx: ^0.18.5
|
xlsx: ^0.18.5
|
||||||
@ -98,6 +100,7 @@ dependencies:
|
|||||||
"@vueuse/motion": 2.0.0_vue@3.3.4
|
"@vueuse/motion": 2.0.0_vue@3.3.4
|
||||||
animate.css: 4.1.1
|
animate.css: 4.1.1
|
||||||
axios: 1.4.0
|
axios: 1.4.0
|
||||||
|
cropperjs: 1.5.13
|
||||||
crypto-js: 4.1.1
|
crypto-js: 4.1.1
|
||||||
dayjs: 1.11.8
|
dayjs: 1.11.8
|
||||||
echarts: 5.4.2
|
echarts: 5.4.2
|
||||||
@ -117,6 +120,7 @@ dependencies:
|
|||||||
typeit: 8.7.1
|
typeit: 8.7.1
|
||||||
vue: 3.3.4
|
vue: 3.3.4
|
||||||
vue-router: 4.2.2_vue@3.3.4
|
vue-router: 4.2.2_vue@3.3.4
|
||||||
|
vue-tippy: 6.2.0_vue@3.3.4
|
||||||
vue-types: 5.1.0_vue@3.3.4
|
vue-types: 5.1.0_vue@3.3.4
|
||||||
xlsx: 0.18.5
|
xlsx: 0.18.5
|
||||||
|
|
||||||
@ -1447,6 +1451,13 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/@popperjs/core/2.11.8:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
|
||||||
|
}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@pureadmin/descriptions/1.1.1_element-plus@2.3.6:
|
/@pureadmin/descriptions/1.1.1_element-plus@2.3.6:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -3150,6 +3161,13 @@ packages:
|
|||||||
}
|
}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/cropperjs/1.5.13:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-by7jKAo73y5/Do0K6sxdTKHgndY0NMjG2bEdgeJxycbcmHuCiMXqw8sxy5C5Y5WTOTcDGmbT7Sr5CgKOXR06OA==
|
||||||
|
}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/cross-spawn/7.0.3:
|
/cross-spawn/7.0.3:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -8676,6 +8694,15 @@ packages:
|
|||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/tippy.js/6.3.7:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==
|
||||||
|
}
|
||||||
|
dependencies:
|
||||||
|
"@popperjs/core": 2.11.8
|
||||||
|
dev: false
|
||||||
|
|
||||||
/to-fast-properties/2.0.0:
|
/to-fast-properties/2.0.0:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -9178,6 +9205,18 @@ packages:
|
|||||||
he: 1.2.0
|
he: 1.2.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/vue-tippy/6.2.0_vue@3.3.4:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-UytUItp2ZDLXUwAotmioz02uLQoaAl5iVM+5yKsQWrXr29L9ivavtkL684FqbmOfbeCypBw+rVKsXhwdnCt/Cg==
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.2.0
|
||||||
|
dependencies:
|
||||||
|
tippy.js: 6.3.7
|
||||||
|
vue: 3.3.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
/vue-tsc/1.8.1_typescript@5.0.4:
|
/vue-tsc/1.8.1_typescript@5.0.4:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
|
@ -36,11 +36,41 @@ export type TokenDTO = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type CurrentLoginUserDTO = {
|
export type CurrentLoginUserDTO = {
|
||||||
userInfo: any;
|
userInfo: CurrentUserInfoDTO;
|
||||||
roleKey: string;
|
roleKey: string;
|
||||||
permissions: Set<string>;
|
permissions: Set<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前User
|
||||||
|
*/
|
||||||
|
export interface CurrentUserInfoDTO {
|
||||||
|
avatar?: string;
|
||||||
|
createTime?: Date;
|
||||||
|
creatorId?: number;
|
||||||
|
creatorName?: string;
|
||||||
|
deptId?: number;
|
||||||
|
deptName?: string;
|
||||||
|
email?: string;
|
||||||
|
loginDate?: Date;
|
||||||
|
loginIp?: string;
|
||||||
|
nickName?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
postId?: number;
|
||||||
|
postName?: string;
|
||||||
|
remark?: string;
|
||||||
|
roleId?: number;
|
||||||
|
roleName?: string;
|
||||||
|
sex?: number;
|
||||||
|
status?: number;
|
||||||
|
updaterId?: number;
|
||||||
|
updaterName?: string;
|
||||||
|
updateTime?: Date;
|
||||||
|
userId?: number;
|
||||||
|
username?: string;
|
||||||
|
userType?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type DictionaryData = {
|
export type DictionaryData = {
|
||||||
label: string;
|
label: string;
|
||||||
value: number;
|
value: number;
|
||||||
|
@ -56,6 +56,26 @@ export interface UserRequest {
|
|||||||
username?: string;
|
username?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateProfileCommand
|
||||||
|
*/
|
||||||
|
export interface UserProfileRequest {
|
||||||
|
email?: string;
|
||||||
|
nickName?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
sex?: number;
|
||||||
|
userId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ResetPasswordCommand
|
||||||
|
*/
|
||||||
|
export interface ResetPasswordRequest {
|
||||||
|
newPassword?: string;
|
||||||
|
oldPassword?: string;
|
||||||
|
userId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改密码
|
* 修改密码
|
||||||
*/
|
*/
|
||||||
@ -120,3 +140,37 @@ export const exportUserExcelApi = (params: UserQuery, fileName: string) => {
|
|||||||
params
|
params
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 用户头像上传 */
|
||||||
|
export const uploadUserAvatarApi = data => {
|
||||||
|
return http.request<ResponseData<void>>(
|
||||||
|
"post",
|
||||||
|
"/system/user/profile/avatar",
|
||||||
|
{
|
||||||
|
data
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 更改用户资料 */
|
||||||
|
export const updateUserProfileApi = (data?: UserProfileRequest) => {
|
||||||
|
return http.request<ResponseData<void>>("put", "/system/user/profile", {
|
||||||
|
data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 更改当前用户密码 */
|
||||||
|
export const updateCurrentUserPasswordApi = (data?: ResetPasswordRequest) => {
|
||||||
|
return http.request<ResponseData<void>>(
|
||||||
|
"put",
|
||||||
|
"/system/user/profile/password",
|
||||||
|
{
|
||||||
|
data
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
7
src/components/ReCropper/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import reCropper from "./src";
|
||||||
|
import { withInstall } from "@pureadmin/utils";
|
||||||
|
|
||||||
|
/** 图片裁剪组件 */
|
||||||
|
export const ReCropper = withInstall(reCropper);
|
||||||
|
|
||||||
|
export default ReCropper;
|
11
src/components/ReCropper/src/circled.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
@import "cropperjs/dist/cropper.css";
|
||||||
|
@import "tippy.js/dist/tippy.css";
|
||||||
|
@import "tippy.js/themes/light.css";
|
||||||
|
@import "tippy.js/animations/perspective.css";
|
||||||
|
|
||||||
|
.re-circled {
|
||||||
|
.cropper-view-box,
|
||||||
|
.cropper-face {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
439
src/components/ReCropper/src/index.tsx
Normal file
@ -0,0 +1,439 @@
|
|||||||
|
import "./circled.css";
|
||||||
|
import Cropper from "cropperjs";
|
||||||
|
import { ElUpload } from "element-plus";
|
||||||
|
import type { CSSProperties } from "vue";
|
||||||
|
import { useResizeObserver } from "@vueuse/core";
|
||||||
|
import { longpress } from "@/directives/longpress";
|
||||||
|
import { useTippy, directive as tippy } from "vue-tippy";
|
||||||
|
import { delay, debounce, isArray, downloadByBase64 } from "@pureadmin/utils";
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
unref,
|
||||||
|
computed,
|
||||||
|
PropType,
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
defineComponent
|
||||||
|
} from "vue";
|
||||||
|
import {
|
||||||
|
Reload,
|
||||||
|
Upload,
|
||||||
|
ArrowH,
|
||||||
|
ArrowV,
|
||||||
|
ArrowUp,
|
||||||
|
ArrowDown,
|
||||||
|
ArrowLeft,
|
||||||
|
ChangeIcon,
|
||||||
|
ArrowRight,
|
||||||
|
RotateLeft,
|
||||||
|
SearchPlus,
|
||||||
|
RotateRight,
|
||||||
|
SearchMinus,
|
||||||
|
DownloadIcon
|
||||||
|
} from "./svg";
|
||||||
|
|
||||||
|
type Options = Cropper.Options;
|
||||||
|
|
||||||
|
const defaultOptions: Options = {
|
||||||
|
aspectRatio: 1,
|
||||||
|
zoomable: true,
|
||||||
|
zoomOnTouch: true,
|
||||||
|
zoomOnWheel: true,
|
||||||
|
cropBoxMovable: true,
|
||||||
|
cropBoxResizable: true,
|
||||||
|
toggleDragModeOnDblclick: true,
|
||||||
|
autoCrop: true,
|
||||||
|
background: true,
|
||||||
|
highlight: true,
|
||||||
|
center: true,
|
||||||
|
responsive: true,
|
||||||
|
restore: true,
|
||||||
|
checkCrossOrigin: true,
|
||||||
|
checkOrientation: true,
|
||||||
|
scalable: true,
|
||||||
|
modal: true,
|
||||||
|
guides: true,
|
||||||
|
movable: true,
|
||||||
|
rotatable: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
src: { type: String, required: true },
|
||||||
|
alt: { type: String },
|
||||||
|
circled: { type: Boolean, default: false },
|
||||||
|
realTimePreview: { type: Boolean, default: true },
|
||||||
|
height: { type: [String, Number], default: "360px" },
|
||||||
|
crossorigin: {
|
||||||
|
type: String as PropType<"" | "anonymous" | "use-credentials" | undefined>,
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
|
||||||
|
options: { type: Object as PropType<Options>, default: () => ({}) }
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "ReCropper",
|
||||||
|
props,
|
||||||
|
setup(props, { attrs, emit }) {
|
||||||
|
const tippyElRef = ref<ElRef<HTMLImageElement>>();
|
||||||
|
const imgElRef = ref<ElRef<HTMLImageElement>>();
|
||||||
|
const cropper = ref<Nullable<Cropper>>();
|
||||||
|
const isReady = ref(false);
|
||||||
|
const imgBase64 = ref();
|
||||||
|
const inCircled = ref(props.circled);
|
||||||
|
const inSrc = ref(props.src);
|
||||||
|
let scaleX = 1;
|
||||||
|
let scaleY = 1;
|
||||||
|
|
||||||
|
const debounceRealTimeCroppered = debounce(realTimeCroppered, 80);
|
||||||
|
|
||||||
|
const getImageStyle = computed((): CSSProperties => {
|
||||||
|
return {
|
||||||
|
height: props.height,
|
||||||
|
maxWidth: "100%",
|
||||||
|
...props.imageStyle
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const getClass = computed(() => {
|
||||||
|
return [
|
||||||
|
attrs.class,
|
||||||
|
{
|
||||||
|
["re-circled"]: inCircled.value
|
||||||
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconClass = computed(() => {
|
||||||
|
return [
|
||||||
|
"p-[6px]",
|
||||||
|
"h-[30px]",
|
||||||
|
"w-[30px]",
|
||||||
|
"outline-none",
|
||||||
|
"rounded-[4px]",
|
||||||
|
"cursor-pointer",
|
||||||
|
"hover:bg-[rgba(0,0,0,0.06)]"
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const getWrapperStyle = computed((): CSSProperties => {
|
||||||
|
return { height: `${props.height}`.replace(/px/, "") + "px" };
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(init);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cropper.value?.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
useResizeObserver(tippyElRef, () => {
|
||||||
|
handCropper("reset");
|
||||||
|
});
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
const imgEl = unref(imgElRef);
|
||||||
|
if (!imgEl) return;
|
||||||
|
cropper.value = new Cropper(imgEl, {
|
||||||
|
...defaultOptions,
|
||||||
|
ready: () => {
|
||||||
|
isReady.value = true;
|
||||||
|
realTimeCroppered();
|
||||||
|
delay(400).then(() => emit("readied", cropper.value));
|
||||||
|
},
|
||||||
|
crop() {
|
||||||
|
debounceRealTimeCroppered();
|
||||||
|
},
|
||||||
|
zoom() {
|
||||||
|
debounceRealTimeCroppered();
|
||||||
|
},
|
||||||
|
cropmove() {
|
||||||
|
debounceRealTimeCroppered();
|
||||||
|
},
|
||||||
|
...props.options
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function realTimeCroppered() {
|
||||||
|
props.realTimePreview && croppered();
|
||||||
|
}
|
||||||
|
|
||||||
|
function croppered() {
|
||||||
|
if (!cropper.value) return;
|
||||||
|
const canvas = inCircled.value
|
||||||
|
? getRoundedCanvas()
|
||||||
|
: cropper.value.getCroppedCanvas();
|
||||||
|
// https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLCanvasElement/toBlob
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
if (!blob) return;
|
||||||
|
const fileReader: FileReader = new FileReader();
|
||||||
|
fileReader.readAsDataURL(blob);
|
||||||
|
fileReader.onloadend = e => {
|
||||||
|
if (!e.target?.result || !blob) return;
|
||||||
|
imgBase64.value = e.target.result;
|
||||||
|
emit("cropper", {
|
||||||
|
base64: e.target.result,
|
||||||
|
blob,
|
||||||
|
info: { size: blob.size, ...cropper.value.getData() }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
fileReader.onerror = () => {
|
||||||
|
emit("error");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRoundedCanvas() {
|
||||||
|
const sourceCanvas = cropper.value!.getCroppedCanvas();
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const context = canvas.getContext("2d")!;
|
||||||
|
const width = sourceCanvas.width;
|
||||||
|
const height = sourceCanvas.height;
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
context.imageSmoothingEnabled = true;
|
||||||
|
context.drawImage(sourceCanvas, 0, 0, width, height);
|
||||||
|
context.globalCompositeOperation = "destination-in";
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(
|
||||||
|
width / 2,
|
||||||
|
height / 2,
|
||||||
|
Math.min(width, height) / 2,
|
||||||
|
0,
|
||||||
|
2 * Math.PI,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
context.fill();
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handCropper(event: string, arg?: number | Array<number>) {
|
||||||
|
if (event === "scaleX") {
|
||||||
|
scaleX = arg = scaleX === -1 ? 1 : -1;
|
||||||
|
}
|
||||||
|
if (event === "scaleY") {
|
||||||
|
scaleY = arg = scaleY === -1 ? 1 : -1;
|
||||||
|
}
|
||||||
|
arg && isArray(arg)
|
||||||
|
? cropper.value?.[event]?.(...arg)
|
||||||
|
: cropper.value?.[event]?.(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeUpload(file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
inSrc.value = "";
|
||||||
|
reader.onload = e => {
|
||||||
|
inSrc.value = e.target?.result as string;
|
||||||
|
};
|
||||||
|
reader.onloadend = () => {
|
||||||
|
init();
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuContent = defineComponent({
|
||||||
|
directives: {
|
||||||
|
tippy,
|
||||||
|
longpress
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return () => (
|
||||||
|
<div class="flex flex-wrap w-[60px] justify-between">
|
||||||
|
<ElUpload
|
||||||
|
accept="image/*"
|
||||||
|
show-file-list={false}
|
||||||
|
before-upload={beforeUpload}
|
||||||
|
>
|
||||||
|
<Upload
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "上传",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ElUpload>
|
||||||
|
<DownloadIcon
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "下载",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
onClick={() => downloadByBase64(imgBase64.value, "cropping.png")}
|
||||||
|
/>
|
||||||
|
<ChangeIcon
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "圆形、矩形裁剪",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
inCircled.value = !inCircled.value;
|
||||||
|
realTimeCroppered();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Reload
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "重置",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
onClick={() => handCropper("reset")}
|
||||||
|
/>
|
||||||
|
<ArrowUp
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "上移(可长按)",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
v-longpress={[() => handCropper("move", [0, -10]), "0:100"]}
|
||||||
|
/>
|
||||||
|
<ArrowDown
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "下移(可长按)",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
v-longpress={[() => handCropper("move", [0, 10]), "0:100"]}
|
||||||
|
/>
|
||||||
|
<ArrowLeft
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "左移(可长按)",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
v-longpress={[() => handCropper("move", [-10, 0]), "0:100"]}
|
||||||
|
/>
|
||||||
|
<ArrowRight
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "右移(可长按)",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
v-longpress={[() => handCropper("move", [10, 0]), "0:100"]}
|
||||||
|
/>
|
||||||
|
<ArrowH
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "水平翻转",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
onClick={() => handCropper("scaleX", -1)}
|
||||||
|
/>
|
||||||
|
<ArrowV
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "垂直翻转",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
onClick={() => handCropper("scaleY", -1)}
|
||||||
|
/>
|
||||||
|
<RotateLeft
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "逆时针旋转",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
onClick={() => handCropper("rotate", -45)}
|
||||||
|
/>
|
||||||
|
<RotateRight
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "顺时针旋转",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
onClick={() => handCropper("rotate", 45)}
|
||||||
|
/>
|
||||||
|
<SearchPlus
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "放大(可长按)",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
v-longpress={[() => handCropper("zoom", 0.1), "0:100"]}
|
||||||
|
/>
|
||||||
|
<SearchMinus
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "缩小(可长按)",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
v-longpress={[() => handCropper("zoom", -0.1), "0:100"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function onContextmenu(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const { show, setProps } = useTippy(tippyElRef, {
|
||||||
|
content: menuContent,
|
||||||
|
arrow: false,
|
||||||
|
theme: "light",
|
||||||
|
trigger: "manual",
|
||||||
|
interactive: true,
|
||||||
|
appendTo: "parent",
|
||||||
|
// hideOnClick: false,
|
||||||
|
animation: "perspective",
|
||||||
|
placement: "bottom-start"
|
||||||
|
});
|
||||||
|
|
||||||
|
setProps({
|
||||||
|
getReferenceClientRect: () => ({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
top: event.clientY,
|
||||||
|
bottom: event.clientY,
|
||||||
|
left: event.clientX,
|
||||||
|
right: event.clientX
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
inSrc,
|
||||||
|
props,
|
||||||
|
imgElRef,
|
||||||
|
tippyElRef,
|
||||||
|
getClass,
|
||||||
|
getWrapperStyle,
|
||||||
|
getImageStyle,
|
||||||
|
isReady,
|
||||||
|
croppered,
|
||||||
|
onContextmenu
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
inSrc,
|
||||||
|
isReady,
|
||||||
|
getClass,
|
||||||
|
getImageStyle,
|
||||||
|
onContextmenu,
|
||||||
|
getWrapperStyle
|
||||||
|
} = this;
|
||||||
|
const { alt, crossorigin } = this.props;
|
||||||
|
|
||||||
|
return inSrc ? (
|
||||||
|
<div
|
||||||
|
ref="tippyElRef"
|
||||||
|
class={getClass}
|
||||||
|
style={getWrapperStyle}
|
||||||
|
onContextmenu={event => onContextmenu(event)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-show={isReady}
|
||||||
|
ref="imgElRef"
|
||||||
|
style={getImageStyle}
|
||||||
|
src={inSrc}
|
||||||
|
alt={alt}
|
||||||
|
crossorigin={crossorigin}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
});
|
1
src/components/ReCropper/src/svg/arrow-down.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M862 465.3h-81c-4.6 0-9 2-12.1 5.5L550 723.1V160c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v563.1L255.1 470.8c-3-3.5-7.4-5.5-12.1-5.5h-81c-6.8 0-10.5 8.1-6 13.2L487.9 861a31.96 31.96 0 0 0 48.3 0L868 478.5c4.5-5.2.8-13.2-6-13.2z"/></svg>
|
After Width: | Height: | Size: 347 B |
1
src/components/ReCropper/src/svg/arrow-h.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="m296.992 216.992-272 272L3.008 512l21.984 23.008 272 272 46.016-46.016L126.016 544h772L680.992 760.992l46.016 46.016 272-272L1020.992 512l-21.984-23.008-272-272-46.048 46.048L898.016 480h-772l216.96-216.992z"/></svg>
|
After Width: | Height: | Size: 325 B |
1
src/components/ReCropper/src/svg/arrow-left.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M872 474H286.9l350.2-304c5.6-4.9 2.2-14-5.2-14h-88.5c-3.9 0-7.6 1.4-10.5 3.9L155 487.8a31.96 31.96 0 0 0 0 48.3L535.1 866c1.5 1.3 3.3 2 5.2 2h91.5c7.4 0 10.8-9.2 5.2-14L286.9 550H872c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8z"/></svg>
|
After Width: | Height: | Size: 344 B |
1
src/components/ReCropper/src/svg/arrow-right.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M869 487.8 491.2 159.9c-2.9-2.5-6.6-3.9-10.5-3.9h-88.5c-7.4 0-10.8 9.2-5.2 14l350.2 304H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h585.1L386.9 854c-5.6 4.9-2.2 14 5.2 14h91.5c1.9 0 3.8-.7 5.2-2L869 536.2a32.07 32.07 0 0 0 0-48.4z"/></svg>
|
After Width: | Height: | Size: 351 B |
1
src/components/ReCropper/src/svg/arrow-up.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M868 545.5 536.1 163a31.96 31.96 0 0 0-48.3 0L156 545.5a7.97 7.97 0 0 0 6 13.2h81c4.6 0 9-2 12.1-5.5L474 300.9V864c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V300.9l218.9 252.3c3 3.5 7.4 5.5 12.1 5.5h81c6.8 0 10.5-8 6-13.2z"/></svg>
|
After Width: | Height: | Size: 339 B |
1
src/components/ReCropper/src/svg/arrow-v.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="m512 67.008-23.008 21.984-256 256 46.048 46.048L480 190.016v644L279.008 632.96l-46.048 46.08 256 256 23.008 21.984 23.008-21.984 256-256-46.016-46.016L544 834.016v-644l200.992 200.96 46.016-45.984-256-256z"/></svg>
|
After Width: | Height: | Size: 323 B |
1
src/components/ReCropper/src/svg/change.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="M956.8 988.8H585.6c-16 0-25.6-9.6-25.6-28.8V576c0-16 9.6-28.8 25.6-28.8h371.2c16 0 25.6 9.6 25.6 28.8v384c0 16-9.6 28.8-25.6 28.8zM608 937.6h326.4V598.4H608v339.2zm-121.6 44.8C262.4 982.4 144 848 144 595.2c0-19.2 9.6-28.8 25.6-28.8s25.6 12.8 25.6 28.8c0 220.8 96 326.4 288 326.4 16 0 25.6 12.8 25.6 28.8s-6.4 32-22.4 32z"/><path d="M262.4 694.4c-6.4 0-9.6-3.2-16-6.4L160 601.6c-9.6-9.6-9.6-22.4 0-28.8s22.4-9.6 28.8 0l86.4 86.4c9.6 9.6 9.6 22.4 0 28.8-3.2 3.2-6.4 6.4-12.8 6.4z"/><path d="M86.4 694.4c-6.4 0-9.6-3.2-16-6.4-9.6-9.6-9.6-22.4 0-28.8l86.4-86.4c9.6-9.6 22.4-9.6 28.8 0 9.6 9.6 9.6 22.4 0 28.8L99.2 688c-3.2 3.2-6.4 6.4-12.8 6.4zm790.4-249.6c-16 0-28.8-12.8-28.8-32 0-224-99.2-336-300.8-336-16 0-28.8-12.8-28.8-32s9.6-32 28.8-32c233.6 0 355.2 137.6 355.2 396.8 0 22.4-9.6 35.2-25.6 35.2z"/><path d="M876.8 448c-6.4 0-9.6-3.2-16-6.4l-86.4-86.4c-9.6-9.6-9.6-22.4 0-28.8s22.4-9.6 28.8 0l86.4 86.4c9.6 9.6 9.6 22.4 0 28.8 0 3.2-6.4 6.4-12.8 6.4z"/><path d="M876.8 448c-6.4 0-9.6-3.2-16-6.4-9.6-9.6-9.6-22.4 0-28.8l86.4-86.4c9.6-9.6 22.4-9.6 28.8 0s9.6 22.4 0 28.8l-86.4 86.4c-3.2 3.2-6.4 6.4-12.8 6.4zM288 524.8C156.8 524.8 48 416 48 278.4S156.8 35.2 288 35.2 528 144 528 281.6 419.2 524.8 288 524.8zm-3.2-432c-99.2 0-179.2 83.2-179.2 185.6S185.6 464 284.8 464 464 380.8 464 278.4 384 92.8 284.8 92.8z"/></svg>
|
After Width: | Height: | Size: 1.4 KiB |
1
src/components/ReCropper/src/svg/download.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M505.7 661a8 8 0 0 0 12.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9h-74.1V168c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v338.3H400c-6.7 0-10.4 7.7-6.3 12.9l112 141.8zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z"/></svg>
|
After Width: | Height: | Size: 428 B |
31
src/components/ReCropper/src/svg/index.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import Reload from "./reload.svg?component";
|
||||||
|
import Upload from "./upload.svg?component";
|
||||||
|
import ArrowH from "./arrow-h.svg?component";
|
||||||
|
import ArrowV from "./arrow-v.svg?component";
|
||||||
|
import ArrowUp from "./arrow-up.svg?component";
|
||||||
|
import ChangeIcon from "./change.svg?component";
|
||||||
|
import ArrowDown from "./arrow-down.svg?component";
|
||||||
|
import ArrowLeft from "./arrow-left.svg?component";
|
||||||
|
import DownloadIcon from "./download.svg?component";
|
||||||
|
import ArrowRight from "./arrow-right.svg?component";
|
||||||
|
import RotateLeft from "./rotate-left.svg?component";
|
||||||
|
import SearchPlus from "./search-plus.svg?component";
|
||||||
|
import RotateRight from "./rotate-right.svg?component";
|
||||||
|
import SearchMinus from "./search-minus.svg?component";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Reload,
|
||||||
|
Upload,
|
||||||
|
ArrowH,
|
||||||
|
ArrowV,
|
||||||
|
ArrowUp,
|
||||||
|
ArrowDown,
|
||||||
|
ArrowLeft,
|
||||||
|
ChangeIcon,
|
||||||
|
ArrowRight,
|
||||||
|
RotateLeft,
|
||||||
|
SearchPlus,
|
||||||
|
RotateRight,
|
||||||
|
SearchMinus,
|
||||||
|
DownloadIcon
|
||||||
|
};
|
1
src/components/ReCropper/src/svg/reload.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M168 504.2c1-43.7 10-86.1 26.9-126 17.3-41 42.1-77.7 73.7-109.4S337 212.3 378 195c42.4-17.9 87.4-27 133.9-27s91.5 9.1 133.8 27A341.5 341.5 0 0 1 755 268.8c9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47a8 8 0 0 0 3 14.1l175.7 43c5 1.2 9.9-2.6 9.9-7.7l.8-180.9c0-6.7-7.7-10.5-12.9-6.3l-56.4 44.1C765.8 155.1 646.2 92 511.8 92 282.7 92 96.3 275.6 92 503.8a8 8 0 0 0 8 8.2h60c4.4 0 7.9-3.5 8-7.8zm756 7.8h-60c-4.4 0-7.9 3.5-8 7.8-1 43.7-10 86.1-26.9 126-17.3 41-42.1 77.8-73.7 109.4A342.45 342.45 0 0 1 512.1 856a342.24 342.24 0 0 1-243.2-100.8c-9.9-9.9-19.2-20.4-27.8-31.4l60.2-47a8 8 0 0 0-3-14.1l-175.7-43c-5-1.2-9.9 2.6-9.9 7.7l-.7 181c0 6.7 7.7 10.5 12.9 6.3l56.4-44.1C258.2 868.9 377.8 932 512.2 932c229.2 0 415.5-183.7 419.8-411.8a8 8 0 0 0-8-8.2z"/></svg>
|
After Width: | Height: | Size: 865 B |
1
src/components/ReCropper/src/svg/rotate-left.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M672 418H144c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32zm-44 402H188V494h440v326z"/><path fill="currentColor" d="M819.3 328.5c-78.8-100.7-196-153.6-314.6-154.2l-.2-64c0-6.5-7.6-10.1-12.6-6.1l-128 101c-4 3.1-3.9 9.1 0 12.3L492 318.6c5.1 4 12.7.4 12.6-6.1v-63.9c12.9.1 25.9.9 38.8 2.5 42.1 5.2 82.1 18.2 119 38.7 38.1 21.2 71.2 49.7 98.4 84.3 27.1 34.7 46.7 73.7 58.1 115.8 11 40.7 14 82.7 8.9 124.8-.7 5.4-1.4 10.8-2.4 16.1h74.9c14.8-103.6-11.3-213-81-302.3z"/></svg>
|
After Width: | Height: | Size: 636 B |
1
src/components/ReCropper/src/svg/rotate-right.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M480.5 251.2c13-1.6 25.9-2.4 38.8-2.5v63.9c0 6.5 7.5 10.1 12.6 6.1L660 217.6c4-3.2 4-9.2 0-12.3l-128-101c-5.1-4-12.6-.4-12.6 6.1l-.2 64c-118.6.5-235.8 53.4-314.6 154.2-69.6 89.2-95.7 198.6-81.1 302.4h74.9c-.9-5.3-1.7-10.7-2.4-16.1-5.1-42.1-2.1-84.1 8.9-124.8 11.4-42.2 31-81.1 58.1-115.8 27.2-34.7 60.3-63.2 98.4-84.3 37-20.6 76.9-33.6 119.1-38.8z"/><path fill="currentColor" d="M880 418H352c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32zm-44 402H396V494h440v326z"/></svg>
|
After Width: | Height: | Size: 639 B |
1
src/components/ReCropper/src/svg/search-minus.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M637 443H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h312c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zm284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z"/></svg>
|
After Width: | Height: | Size: 535 B |
1
src/components/ReCropper/src/svg/search-plus.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M637 443H519V309c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v134H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h118v134c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V519h118c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zm284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z"/></svg>
|
After Width: | Height: | Size: 631 B |
1
src/components/ReCropper/src/svg/upload.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M400 317.7h73.9V656c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V317.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 163a8 8 0 0 0-12.6 0l-112 141.7c-4.1 5.3-.4 13 6.3 13zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z"/></svg>
|
After Width: | Height: | Size: 423 B |
@ -12,6 +12,7 @@ const {
|
|||||||
layout,
|
layout,
|
||||||
device,
|
device,
|
||||||
logout,
|
logout,
|
||||||
|
userProfile,
|
||||||
onPanel,
|
onPanel,
|
||||||
pureApp,
|
pureApp,
|
||||||
username,
|
username,
|
||||||
@ -51,6 +52,15 @@ const {
|
|||||||
<p v-if="username" class="dark:text-white">{{ username }}</p>
|
<p v-if="username" class="dark:text-white">{{ username }}</p>
|
||||||
</span>
|
</span>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu class="logout">
|
||||||
|
<el-dropdown-item @click="userProfile">
|
||||||
|
<IconifyIconOffline
|
||||||
|
:icon="LogoutCircleRLine"
|
||||||
|
style="margin: 5px"
|
||||||
|
/>
|
||||||
|
个人中心
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
<el-dropdown-menu class="logout">
|
<el-dropdown-menu class="logout">
|
||||||
<el-dropdown-item @click="logout">
|
<el-dropdown-item @click="logout">
|
||||||
<IconifyIconOffline
|
<IconifyIconOffline
|
||||||
|
@ -80,7 +80,7 @@ export function useNav() {
|
|||||||
|
|
||||||
/** 个人中心 */
|
/** 个人中心 */
|
||||||
function userProfile() {
|
function userProfile() {
|
||||||
router.push("/system/user/profile");
|
router.push("/global/user/profile");
|
||||||
}
|
}
|
||||||
|
|
||||||
function backTopMenu() {
|
function backTopMenu() {
|
||||||
|
20
src/router/modules/global.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export default {
|
||||||
|
path: "/global",
|
||||||
|
redirect: "/global/user/profile",
|
||||||
|
meta: {
|
||||||
|
icon: "checkboxCircleLine",
|
||||||
|
title: "首页",
|
||||||
|
rank: 0,
|
||||||
|
showLink: false
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/global/user/profile",
|
||||||
|
name: "Success",
|
||||||
|
component: () => import("@/views/system/user/profile/index.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "个人中心"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} as RouteConfigsTable;
|
@ -26,5 +26,21 @@ export default [
|
|||||||
component: () => import("@/layout/redirect.vue")
|
component: () => import("@/layout/redirect.vue")
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/system/user/profile",
|
||||||
|
component: Layout,
|
||||||
|
meta: {
|
||||||
|
title: "个人中心",
|
||||||
|
showLink: false,
|
||||||
|
rank: 102
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
name: "Redirect",
|
||||||
|
component: () => import("@/views/system/user/profile/index.vue")
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
] as Array<RouteConfigsTable>;
|
] as Array<RouteConfigsTable>;
|
||||||
|
@ -30,7 +30,9 @@ export const useUserStore = defineStore({
|
|||||||
dictionaryMap:
|
dictionaryMap:
|
||||||
storageLocal().getItem<Map<String, Map<String, DictionaryData>>>(
|
storageLocal().getItem<Map<String, Map<String, DictionaryData>>>(
|
||||||
dictionaryMapKey
|
dictionaryMapKey
|
||||||
) ?? new Map()
|
) ?? new Map(),
|
||||||
|
currentUserInfo:
|
||||||
|
storageSession().getItem<TokenDTO>(sessionKey)?.currentUser.userInfo ?? {}
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
/** 存储用户名 */
|
/** 存储用户名 */
|
||||||
|
@ -356,7 +356,7 @@ export function useHook() {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
onSearch();
|
onSearch();
|
||||||
const deptResponse = getDeptListApi();
|
const deptResponse = await getDeptListApi();
|
||||||
deptTreeList.value = await setDisabledForTreeOptions(
|
deptTreeList.value = await setDisabledForTreeOptions(
|
||||||
handleTree(deptResponse.data),
|
handleTree(deptResponse.data),
|
||||||
"status"
|
"status"
|
||||||
|
99
src/views/system/user/profile/index.vue
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import resetPwd from "./resetPwd.vue";
|
||||||
|
import userInfo from "./userInfo.vue";
|
||||||
|
import userAvatar from "./userAvatar.vue";
|
||||||
|
// import userAvatar from "./userAvatar";
|
||||||
|
// import { getUserProfile } from '@/api/system/user';
|
||||||
|
// import * as userApi from "@/api/system/userApi";
|
||||||
|
import { reactive, ref } from "vue";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { useUserStoreHook } from "@/store/modules/user";
|
||||||
|
|
||||||
|
const activeTab = ref("userinfo");
|
||||||
|
const state = reactive({
|
||||||
|
user: {},
|
||||||
|
roleName: {},
|
||||||
|
postName: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 用户名 */
|
||||||
|
const currentUserInfo = useUserStoreHook()?.currentUserInfo;
|
||||||
|
|
||||||
|
state.user = currentUserInfo;
|
||||||
|
console.log(currentUserInfo);
|
||||||
|
|
||||||
|
function getUser() {
|
||||||
|
// userApi.getUserProfile().then(response => {
|
||||||
|
// state.user = response.user;
|
||||||
|
// state.roleName = response.roleName;
|
||||||
|
// state.postName = response.postName;
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
getUser();
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="6" :xs="24">
|
||||||
|
<el-card class="box-card">
|
||||||
|
<template v-slot:header>
|
||||||
|
<div class="clearfix">
|
||||||
|
<span>个人信息</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div>
|
||||||
|
<div class="text-center">
|
||||||
|
<userAvatar :user="state.user" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-row>
|
||||||
|
<el-descriptions :column="1">
|
||||||
|
<el-descriptions-item label="用户名称">{{
|
||||||
|
currentUserInfo.username
|
||||||
|
}}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="手机号码">{{
|
||||||
|
currentUserInfo.phoneNumber
|
||||||
|
}}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用户邮箱">{{
|
||||||
|
currentUserInfo.email
|
||||||
|
}}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="部门 / 职位">
|
||||||
|
{{ currentUserInfo.deptName }} /
|
||||||
|
{{ currentUserInfo.postName }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="角色">
|
||||||
|
{{ currentUserInfo.roleName }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建日期">
|
||||||
|
{{
|
||||||
|
dayjs(currentUserInfo.createTime).format(
|
||||||
|
"YYYY-MM-DD HH:mm:ss"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="18" :xs="24">
|
||||||
|
<el-card>
|
||||||
|
<template v-slot:header>
|
||||||
|
<div class="clearfix">
|
||||||
|
<span>基本资料</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-tabs v-model="activeTab">
|
||||||
|
<el-tab-pane label="基本资料" name="userinfo">
|
||||||
|
<userInfo :user="state.user" />
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="修改密码" name="resetPwd">
|
||||||
|
<resetPwd :user="state.user" />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
89
src/views/system/user/profile/resetPwd.vue
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref, toRaw } from "vue";
|
||||||
|
import {
|
||||||
|
updateCurrentUserPasswordApi,
|
||||||
|
ResetPasswordRequest
|
||||||
|
} from "@/api/system/user";
|
||||||
|
import { FormInstance } from "element-plus";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
|
||||||
|
// const { proxy } = getCurrentInstance();
|
||||||
|
|
||||||
|
const user = reactive<ResetPasswordRequest>({
|
||||||
|
oldPassword: undefined,
|
||||||
|
newPassword: undefined,
|
||||||
|
confirmPassword: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
const pwdRef = ref<FormInstance>();
|
||||||
|
|
||||||
|
const equalToPassword = (rule, value, callback) => {
|
||||||
|
if (user.newPassword !== value) {
|
||||||
|
callback(new Error("两次输入的密码不一致"));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const rules = ref({
|
||||||
|
oldPassword: [{ required: true, message: "旧密码不能为空", trigger: "blur" }],
|
||||||
|
newPassword: [
|
||||||
|
{ required: true, message: "新密码不能为空", trigger: "blur" },
|
||||||
|
{
|
||||||
|
min: 6,
|
||||||
|
max: 20,
|
||||||
|
message: "长度在 6 到 20 个字符",
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
confirmPassword: [
|
||||||
|
{ required: true, message: "确认密码不能为空", trigger: "blur" },
|
||||||
|
{ required: true, validator: equalToPassword, trigger: "blur" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 提交按钮 */
|
||||||
|
function submit() {
|
||||||
|
console.log(user);
|
||||||
|
pwdRef.value.validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
updateCurrentUserPasswordApi(toRaw(user)).then(() => {
|
||||||
|
message("修改成功", {
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-form ref="pwdRef" :model="user" :rules="rules" label-width="80px">
|
||||||
|
<el-form-item label="旧密码" prop="oldPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="user.oldPassword"
|
||||||
|
placeholder="请输入旧密码"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新密码" prop="newPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="user.newPassword"
|
||||||
|
placeholder="请输入新密码"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认密码" prop="confirmPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="user.confirmPassword"
|
||||||
|
placeholder="请确认密码"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="submit">保存</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
136
src/views/system/user/profile/userAvatar.vue
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import ReCropper from "@/components/ReCropper";
|
||||||
|
import { formatBytes } from "@pureadmin/utils";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { uploadUserAvatarApi } from "@/api/system/user";
|
||||||
|
import { useUserStoreHook } from "@/store/modules/user";
|
||||||
|
// import * as userApi from "@/api/system/userApi";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
|
||||||
|
const currentUser = useUserStoreHook().currentUserInfo;
|
||||||
|
|
||||||
|
const infos = ref();
|
||||||
|
const imgBlob = ref();
|
||||||
|
const refCropper = ref();
|
||||||
|
const showPopover = ref(false);
|
||||||
|
const cropperImg = ref<string>("");
|
||||||
|
|
||||||
|
cropperImg.value = import.meta.env.VITE_APP_BASE_API + currentUser.avatar;
|
||||||
|
|
||||||
|
function onCropper({ base64, blob, info }) {
|
||||||
|
console.log(blob);
|
||||||
|
infos.value = info;
|
||||||
|
imgBlob.value = blob;
|
||||||
|
cropperImg.value = base64;
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = ref(false);
|
||||||
|
const visible = ref(false);
|
||||||
|
|
||||||
|
// 图片裁剪数据
|
||||||
|
// const options = reactive({
|
||||||
|
// img: avatarUrl, // 裁剪图片的地址
|
||||||
|
// autoCrop: true, // 是否默认生成截图框
|
||||||
|
// autoCropWidth: 200, // 默认生成截图框宽度
|
||||||
|
// autoCropHeight: 200, // 默认生成截图框高度
|
||||||
|
// fixedBox: true, // 固定截图框大小 不允许改变
|
||||||
|
// previews: {} // 预览数据
|
||||||
|
// });
|
||||||
|
|
||||||
|
/** 上传图片 */
|
||||||
|
function uploadImg() {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("avatarfile", imgBlob.value);
|
||||||
|
uploadUserAvatarApi(formData).then(() => {
|
||||||
|
open.value = false;
|
||||||
|
message("上传图片成功", {
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
visible.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="user-info-head" @click="open = true">
|
||||||
|
<el-avatar :size="120" :src="cropperImg" />
|
||||||
|
</div>
|
||||||
|
<el-dialog
|
||||||
|
title="修改头像"
|
||||||
|
v-model="open"
|
||||||
|
width="900px"
|
||||||
|
append-to-body
|
||||||
|
@opened="visible = true"
|
||||||
|
@close="visible = false"
|
||||||
|
>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="font-medium"> 右键下面左侧裁剪区开启功能菜单 </span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-popover
|
||||||
|
:visible="showPopover"
|
||||||
|
placement="right"
|
||||||
|
width="300px"
|
||||||
|
:teleported="false"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<ReCropper
|
||||||
|
ref="refCropper"
|
||||||
|
class="w-[500px]"
|
||||||
|
:src="cropperImg"
|
||||||
|
circled
|
||||||
|
@cropper="onCropper"
|
||||||
|
@readied="showPopover = true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div class="flex flex-wrap justify-center items-center text-center">
|
||||||
|
<el-image
|
||||||
|
v-if="cropperImg"
|
||||||
|
:src="cropperImg"
|
||||||
|
:preview-src-list="Array.of(cropperImg)"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<div v-if="infos" class="mt-1">
|
||||||
|
<p>
|
||||||
|
图像大小:{{ parseInt(infos.width) }} ×
|
||||||
|
{{ parseInt(infos.height) }}像素
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
文件大小:{{ formatBytes(infos.size) }}({{ infos.size }} 字节)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-popover>
|
||||||
|
</el-card>
|
||||||
|
<template #footer>
|
||||||
|
<div>
|
||||||
|
<el-button @click="open = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="uploadImg">保存</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.user-info-head {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info-head:hover::after {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 110px;
|
||||||
|
color: #eee;
|
||||||
|
cursor: pointer;
|
||||||
|
content: "+";
|
||||||
|
background: rgb(0 0 0 / 50%);
|
||||||
|
border-radius: 50%;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
</style>
|
89
src/views/system/user/profile/userInfo.vue
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// import { updateUserProfile } from '@/api/system/userApi';
|
||||||
|
// import * as userApi from "@/api/system/userApi";
|
||||||
|
import { ref, reactive } from "vue";
|
||||||
|
import { updateUserProfileApi, UserProfileRequest } from "@/api/system/user";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
import { FormInstance } from "element-plus";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "SystemUserProfile"
|
||||||
|
});
|
||||||
|
|
||||||
|
const userRef = ref<FormInstance>();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
user: {
|
||||||
|
type: Object
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const userModel = reactive<UserProfileRequest>({
|
||||||
|
nickname: props.user.nickname,
|
||||||
|
phoneNumber: props.user.phoneNumber,
|
||||||
|
email: props.user.email,
|
||||||
|
sex: props.user.sex
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(userModel);
|
||||||
|
console.log(props.user);
|
||||||
|
|
||||||
|
// const { proxy } = getCurrentInstance();
|
||||||
|
|
||||||
|
const rules = ref({
|
||||||
|
nickName: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
|
||||||
|
email: [
|
||||||
|
{ required: true, message: "邮箱地址不能为空", trigger: "blur" },
|
||||||
|
{
|
||||||
|
type: "email",
|
||||||
|
message: "请输入正确的邮箱地址",
|
||||||
|
trigger: ["blur", "change"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
phoneNumber: [
|
||||||
|
{ required: true, message: "手机号码不能为空", trigger: "blur" },
|
||||||
|
{
|
||||||
|
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
|
||||||
|
message: "请输入正确的手机号码",
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 提交按钮 */
|
||||||
|
function submit() {
|
||||||
|
console.log(userRef.value);
|
||||||
|
userRef.value.validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
updateUserProfileApi(userModel).then(() => {
|
||||||
|
message("修改成功", {
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-form ref="userRef" :model="userModel" :rules="rules" label-width="80px">
|
||||||
|
<el-form-item label="用户昵称">
|
||||||
|
<el-input v-model="userModel.nickname" maxlength="30" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号码">
|
||||||
|
<el-input v-model="userModel.phoneNumber" maxlength="11" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="邮箱">
|
||||||
|
<el-input v-model="userModel.email" maxlength="50" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="性别">
|
||||||
|
<el-radio-group v-model="userModel.sex">
|
||||||
|
<el-radio :label="0">男</el-radio>
|
||||||
|
<el-radio :label="1">女</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="submit">保存</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</template>
|