feat: 添加账户设置功能页面且兼容移动端 (#1022)

This commit is contained in:
EchoByte
2024-04-12 16:20:33 +08:00
committed by GitHub
parent 296db3789b
commit 332678ba88
25 changed files with 860 additions and 22 deletions

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import { ref } from "vue";
import { message } from "@/utils/message";
import { deviceDetection } from "@pureadmin/utils";
const list = ref([
{
title: "账户密码",
illustrate: "当前密码强度:强",
button: "修改"
},
{
title: "密保手机",
illustrate: "已经绑定手机158****6789",
button: "修改"
},
{
title: "密保问题",
illustrate: "未设置密保问题,密保问题可有效保护账户安全",
button: "修改"
},
{
title: "备用邮箱",
illustrate: "已绑定邮箱pure***@163.com",
button: "修改"
}
]);
function onClick(item) {
console.log("onClick", item.title);
message("请根据具体业务自行实现", { type: "success" });
}
</script>
<template>
<div
:class="[
'min-w-[180px]',
deviceDetection() ? 'max-w-[100%]' : 'max-w-[70%]'
]"
>
<h3 class="my-8">账户管理</h3>
<div v-for="(item, index) in list" :key="index">
<div class="flex items-center">
<div class="flex-1">
<p>{{ item.title }}</p>
<el-text class="mx-1" type="info">{{ item.illustrate }}</el-text>
</div>
<el-button type="primary" text @click="onClick(item)">
{{ item.button }}
</el-button>
</div>
<el-divider />
</div>
</div>
</template>
<style lang="scss" scoped>
.el-divider--horizontal {
border-top: 0.1px var(--el-border-color) var(--el-border-style);
}
</style>

View File

@@ -0,0 +1,65 @@
<script setup lang="ts">
import { ref } from "vue";
import { message } from "@/utils/message";
import { deviceDetection } from "@pureadmin/utils";
const list = ref([
{
title: "账户密码",
illustrate: "其他用户的消息将以站内信的形式通知",
checked: true
},
{
title: "系统消息",
illustrate: "系统消息将以站内信的形式通知",
checked: true
},
{
title: "待办任务",
illustrate: "待办任务将以站内信的形式通知",
checked: true
}
]);
function onChange(val, item) {
console.log("onChange", val);
message(`${item.title}设置成功`, { type: "success" });
}
</script>
<template>
<div
:class="[
'min-w-[180px]',
deviceDetection() ? 'max-w-[100%]' : 'max-w-[70%]'
]"
>
<h3 class="my-8">偏好设置</h3>
<div v-for="(item, index) in list" :key="index">
<div class="flex items-center">
<div class="flex-1">
<p>{{ item.title }}</p>
<p class="wp-4">
<el-text class="mx-1" type="info">
{{ item.illustrate }}
</el-text>
</p>
</div>
<el-switch
v-model="item.checked"
inline-prompt
active-text=""
inactive-text=""
@change="val => onChange(val, item)"
/>
</div>
<el-divider />
</div>
</div>
</template>
<style lang="scss" scoped>
.el-divider--horizontal {
border-top: 0.1px var(--el-border-color) var(--el-border-style);
}
</style>

View File

@@ -0,0 +1,187 @@
<script setup lang="ts">
import { reactive, ref } from "vue";
import { formUpload } from "@/api/mock";
import { message } from "@/utils/message";
import { type UserInfo, getMine } from "@/api/user";
import type { FormInstance, FormRules } from "element-plus";
import ReCropperPreview from "@/components/ReCropperPreview";
import { createFormData, deviceDetection } from "@pureadmin/utils";
import uploadLine from "@iconify-icons/ri/upload-line";
const imgSrc = ref("");
const cropperInfo = ref();
const cropRef = ref();
const uploadRef = ref();
const isShow = ref(false);
const userInfoFormRef = ref<FormInstance>();
const userInfos = reactive({
avatar: "",
nickname: "",
email: "",
phone: "",
description: ""
});
const rules = reactive<FormRules<UserInfo>>({
nickname: [{ required: true, message: "昵称必填", trigger: "blur" }]
});
function queryEmail(queryString, callback) {
const emailList = [
{ value: "@qq.com" },
{ value: "@126.com" },
{ value: "@163.com" }
];
let results = [];
let queryList = [];
emailList.map(item =>
queryList.push({ value: queryString.split("@")[0] + item.value })
);
results = queryString
? queryList.filter(
item =>
item.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
)
: queryList;
callback(results);
}
const onChange = uploadFile => {
const reader = new FileReader();
reader.onload = e => {
imgSrc.value = e.target.result as string;
isShow.value = true;
};
reader.readAsDataURL(uploadFile.raw);
};
const handleClose = () => {
cropRef.value.hidePopover();
uploadRef.value.clearFiles();
isShow.value = false;
};
const onCropper = info => (cropperInfo.value = info);
const handleSubmitImage = () => {
const formData = createFormData({
files: new File([cropperInfo.value], "avatar")
});
formUpload(formData)
.then(({ success, data }) => {
if (success) {
message("更新头像成功", { type: "success" });
handleClose();
} else {
message("更新头像失败");
}
})
.catch(error => {
message(`提交异常 ${error}`, { type: "error" });
});
};
// 更新信息
const onSubmit = async (formEl: FormInstance) => {
await formEl.validate((valid, fields) => {
if (valid) {
console.log(userInfos);
message("更新信息成功", { type: "success" });
} else {
console.log("error submit!", fields);
}
});
};
getMine().then(res => {
Object.assign(userInfos, res.data);
});
</script>
<template>
<div
:class="[
'min-w-[180px]',
deviceDetection() ? 'max-w-[100%]' : 'max-w-[70%]'
]"
>
<h3 class="my-8">个人信息</h3>
<el-form
ref="userInfoFormRef"
label-position="top"
:rules="rules"
:model="userInfos"
>
<el-form-item label="头像">
<el-avatar :size="80" :src="userInfos.avatar" />
<el-upload
ref="uploadRef"
accept="image/*"
action="#"
:limit="1"
:auto-upload="false"
:show-file-list="false"
:on-change="onChange"
>
<el-button plain class="ml-4">
<IconifyIconOffline :icon="uploadLine" />
<span class="ml-2">更新头像</span>
</el-button>
</el-upload>
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input v-model="userInfos.nickname" placeholder="请输入昵称" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-autocomplete
v-model="userInfos.email"
:fetch-suggestions="queryEmail"
:trigger-on-focus="false"
placeholder="请输入邮箱"
clearable
class="w-full"
/>
</el-form-item>
<el-form-item label="联系电话">
<el-input
v-model="userInfos.phone"
placeholder="请输入联系电话"
clearable
/>
</el-form-item>
<el-form-item label="简介">
<el-input
v-model="userInfos.description"
placeholder="请输入简介"
type="textarea"
:autosize="{ minRows: 6, maxRows: 8 }"
maxlength="56"
show-word-limit
/>
</el-form-item>
<el-button type="primary" @click="onSubmit(userInfoFormRef)">
更新信息
</el-button>
</el-form>
<el-dialog
v-model="isShow"
width="40%"
title="编辑头像"
destroy-on-close
:closeOnClickModal="false"
:before-close="handleClose"
:fullscreen="deviceDetection()"
>
<ReCropperPreview ref="cropRef" :imgSrc="imgSrc" @cropper="onCropper" />
<template #footer>
<div class="dialog-footer">
<el-button bg text @click="handleClose">取消</el-button>
<el-button bg text type="primary" @click="handleSubmitImage">
确定
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>

View File

@@ -0,0 +1,87 @@
<script setup lang="ts">
import dayjs from "dayjs";
import { getMineLogs } from "@/api/user";
import { reactive, ref, onMounted } from "vue";
import { deviceDetection } from "@pureadmin/utils";
import type { PaginationProps } from "@pureadmin/table";
const loading = ref(true);
const dataList = ref([]);
const pagination = reactive<PaginationProps>({
total: 0,
pageSize: 10,
currentPage: 1,
background: true,
layout: "prev, pager, next"
});
const columns: TableColumnList = [
{
label: "详情",
prop: "summary",
minWidth: 140
},
{
label: "IP 地址",
prop: "ip",
minWidth: 100
},
{
label: "地点",
prop: "address",
minWidth: 140
},
{
label: "操作系统",
prop: "system",
minWidth: 100
},
{
label: "浏览器类型",
prop: "browser",
minWidth: 100
},
{
label: "时间",
prop: "operatingTime",
minWidth: 180,
formatter: ({ operatingTime }) =>
dayjs(operatingTime).format("YYYY-MM-DD HH:mm:ss")
}
];
async function onSearch() {
loading.value = true;
const { data } = await getMineLogs();
dataList.value = data.list;
pagination.total = data.total;
pagination.pageSize = data.pageSize;
pagination.currentPage = data.currentPage;
setTimeout(() => {
loading.value = false;
}, 200);
}
onMounted(() => {
onSearch();
});
</script>
<template>
<div
:class="[
'min-w-[180px]',
deviceDetection() ? 'max-w-[100%]' : 'max-w-[70%]'
]"
>
<h3 class="my-8">安全日志</h3>
<pure-table
row-key="id"
table-layout="auto"
:loading="loading"
:data="dataList"
:columns="columns"
:pagination="pagination"
/>
</div>
</template>