first commit
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="userInfo?.email ? '更换邮箱' : '绑定邮箱'"
|
||||
width="450px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
label-width="90px"
|
||||
>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleSave">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { bindEmail } from "@/api/user";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
userInfo: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
userId: {
|
||||
type: [Number, String],
|
||||
default: ""
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "saved"]);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit("update:modelValue", value)
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const formRef = ref(null);
|
||||
|
||||
const form = reactive({
|
||||
email: ""
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
email: [
|
||||
{ required: true, message: "请输入邮箱", trigger: "blur" },
|
||||
{ type: "email", message: "请输入正确的邮箱格式", trigger: "blur" }
|
||||
]
|
||||
};
|
||||
|
||||
const syncForm = () => {
|
||||
form.email = props.userInfo?.email || "";
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
|
||||
const res = await bindEmail(props.userId, form.email);
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(props.userInfo?.email ? "邮箱更换成功" : "邮箱绑定成功");
|
||||
visible.value = false;
|
||||
emit("saved");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存邮箱失败", error);
|
||||
if (error !== false) {
|
||||
ElMessage.error("保存邮箱失败");
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
if (value) {
|
||||
syncForm();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="userInfo?.phone ? '更换手机号' : '绑定手机号'"
|
||||
width="450px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
label-width="90px"
|
||||
>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleSave">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { bindPhone } from "@/api/user";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
userInfo: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
userId: {
|
||||
type: [Number, String],
|
||||
default: ""
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "saved"]);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit("update:modelValue", value)
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const formRef = ref(null);
|
||||
|
||||
const form = reactive({
|
||||
phone: ""
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
phone: [
|
||||
{ required: true, message: "请输入手机号", trigger: "blur" },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号", trigger: "blur" }
|
||||
]
|
||||
};
|
||||
|
||||
const syncForm = () => {
|
||||
form.phone = props.userInfo?.phone || "";
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
|
||||
const res = await bindPhone(props.userId, form.phone);
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(props.userInfo?.phone ? "手机号更换成功" : "手机号绑定成功");
|
||||
visible.value = false;
|
||||
emit("saved");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存手机号失败", error);
|
||||
if (error !== false) {
|
||||
ElMessage.error("保存手机号失败");
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
if (value) {
|
||||
syncForm();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="修改密码"
|
||||
width="450px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="passwordFormRef"
|
||||
:model="passwordForm"
|
||||
:rules="passwordFormRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="旧密码" prop="oldPassword">
|
||||
<el-input
|
||||
v-model="passwordForm.oldPassword"
|
||||
type="password"
|
||||
placeholder="请输入旧密码"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码" prop="newPassword">
|
||||
<el-input
|
||||
v-model="passwordForm.newPassword"
|
||||
type="password"
|
||||
placeholder="请输入新密码"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="passwordForm.confirmPassword"
|
||||
type="password"
|
||||
placeholder="请再次输入新密码"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleSavePassword">
|
||||
确认修改
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { changePassword } from "@/api/user";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
userId: {
|
||||
type: [Number, String],
|
||||
default: ""
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "saved"]);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit("update:modelValue", value)
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const passwordFormRef = ref(null);
|
||||
|
||||
const passwordForm = reactive({
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: ""
|
||||
});
|
||||
|
||||
const validateConfirmPassword = (rule, value, callback) => {
|
||||
if (value !== passwordForm.newPassword) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const passwordFormRules = {
|
||||
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" },
|
||||
{ validator: validateConfirmPassword, trigger: "blur" }
|
||||
]
|
||||
};
|
||||
|
||||
const handleSavePassword = async () => {
|
||||
try {
|
||||
await passwordFormRef.value.validate();
|
||||
loading.value = true;
|
||||
const res = await changePassword(props.userId, passwordForm);
|
||||
if (res.code !== 200) {
|
||||
ElMessage.error(res.msg || "修改密码失败");
|
||||
return;
|
||||
}
|
||||
ElMessage.success(res.msg || "密码修改成功");
|
||||
visible.value = false;
|
||||
emit("saved");
|
||||
} catch (error) {
|
||||
console.error("修改密码失败", error);
|
||||
if (error !== false) {
|
||||
ElMessage.error("修改密码失败");
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
passwordFormRef.value?.resetFields();
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="detail-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">详细资料</h3>
|
||||
<el-button type="primary" :icon="Edit" size="small" @click="$emit('edit')">
|
||||
编辑
|
||||
</el-button>
|
||||
</div>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="姓名">
|
||||
{{ userInfo?.name || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">
|
||||
{{ getSexText(userInfo?.sex) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="生日">
|
||||
{{ userInfo?.birth || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="userInfo?.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ userInfo?.status === 1 ? "正常" : "禁用" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="注册时间" :span="2">
|
||||
{{ formatDate(userInfo?.create_time) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最后登录IP" :span="2">
|
||||
{{ userInfo?.last_login_ip || "-" }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Edit } from "@element-plus/icons-vue";
|
||||
|
||||
defineProps({
|
||||
userInfo: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
formatDate: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
getSexText: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
defineEmits(["edit"]);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.detail-card {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
.card-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.detail-card {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="编辑资料"
|
||||
width="500px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="editFormRef"
|
||||
:model="editForm"
|
||||
:rules="editFormRules"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="editForm.name" placeholder="请输入姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="性别">
|
||||
<el-radio-group v-model="editForm.sex">
|
||||
<el-radio :value="0">保密</el-radio>
|
||||
<el-radio :value="1">男</el-radio>
|
||||
<el-radio :value="2">女</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="生日">
|
||||
<el-date-picker
|
||||
v-model="editForm.birth"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="editForm.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="editForm.phone" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleSaveProfile">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { updateUserProfile } from "@/api/user";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
userInfo: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
userId: {
|
||||
type: [Number, String],
|
||||
default: ""
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "saved"]);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit("update:modelValue", value)
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const editFormRef = ref(null);
|
||||
|
||||
const editForm = reactive({
|
||||
email: "",
|
||||
phone: "",
|
||||
name: "",
|
||||
sex: 0,
|
||||
birth: ""
|
||||
});
|
||||
|
||||
const editFormRules = {
|
||||
email: [
|
||||
{ required: true, message: "请输入邮箱", trigger: "blur" },
|
||||
{ type: "email", message: "请输入正确的邮箱格式", trigger: "blur" }
|
||||
],
|
||||
phone: [
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号", trigger: "blur" }
|
||||
]
|
||||
};
|
||||
|
||||
const syncForm = () => {
|
||||
editForm.name = props.userInfo?.name || "";
|
||||
editForm.sex = props.userInfo?.sex || 0;
|
||||
editForm.birth = props.userInfo?.birth || "";
|
||||
editForm.email = props.userInfo?.email || "";
|
||||
editForm.phone = props.userInfo?.phone || "";
|
||||
};
|
||||
|
||||
const handleSaveProfile = async () => {
|
||||
try {
|
||||
await editFormRef.value.validate();
|
||||
loading.value = true;
|
||||
const res = await updateUserProfile(props.userId, editForm);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("保存成功");
|
||||
visible.value = false;
|
||||
emit("saved");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存失败", error);
|
||||
if (error !== false) {
|
||||
ElMessage.error("保存失败");
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
editFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
if (value) {
|
||||
syncForm();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div class="profile-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">个人资料</h3>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="avatar-section">
|
||||
<el-avatar :size="100" :src="userInfo?.avatar || ''">
|
||||
{{ userInfo?.account?.charAt(0)?.toUpperCase() || "U" }}
|
||||
</el-avatar>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="$emit('change-avatar')"
|
||||
>
|
||||
更换头像
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<span class="info-label">账号</span>
|
||||
<span class="info-value">{{ userInfo?.account || "-" }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">邮箱</span>
|
||||
<span class="info-value">{{ userInfo?.email || "-" }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">手机号</span>
|
||||
<span class="info-value">{{ userInfo?.phone || "-" }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">状态</span>
|
||||
<el-tag :type="userInfo?.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ userInfo?.status === 1 ? "正常" : "禁用" }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
userInfo: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
defineEmits(["change-avatar"]);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.profile-card {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
.card-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
.avatar-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.el-avatar {
|
||||
background: linear-gradient(135deg, #3973ff 0%, #4f84ff 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.info-list {
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.profile-card {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="quick-actions">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">快捷操作</h3>
|
||||
</div>
|
||||
<div class="action-list">
|
||||
<div class="action-item" @click="$emit('logout')">
|
||||
<el-icon><SwitchButton /></el-icon>
|
||||
<span>退出登录</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { SwitchButton } from "@element-plus/icons-vue";
|
||||
|
||||
defineEmits(["logout"]);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.quick-actions {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
.card-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.action-list {
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.quick-actions {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div class="security-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">安全设置</h3>
|
||||
</div>
|
||||
<div class="security-list">
|
||||
<div class="security-item">
|
||||
<div class="security-info">
|
||||
<el-icon class="security-icon"><Lock /></el-icon>
|
||||
<div class="security-text">
|
||||
<div class="security-title">登录密码</div>
|
||||
<div class="security-desc">用于登录系统</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-button type="primary" link @click="$emit('change-password')">
|
||||
修改
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="security-item">
|
||||
<div class="security-info">
|
||||
<el-icon class="security-icon"><Iphone /></el-icon>
|
||||
<div class="security-text">
|
||||
<div class="security-title">手机绑定</div>
|
||||
<div class="security-desc">已绑定: {{ userInfo?.phone || "未绑定" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-button type="primary" link @click="$emit('bind-phone')">
|
||||
{{ userInfo?.phone ? "更换" : "绑定" }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="security-item">
|
||||
<div class="security-info">
|
||||
<el-icon class="security-icon"><Message /></el-icon>
|
||||
<div class="security-text">
|
||||
<div class="security-title">邮箱绑定</div>
|
||||
<div class="security-desc">已绑定: {{ userInfo?.email || "未绑定" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-button type="primary" link @click="$emit('bind-email')">
|
||||
{{ userInfo?.email ? "更换" : "绑定" }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Lock, Iphone, Message } from "@element-plus/icons-vue";
|
||||
|
||||
defineProps({
|
||||
userInfo: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
defineEmits(["change-password", "bind-phone", "bind-email"]);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.security-card {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
.card-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.security-list {
|
||||
.security-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary-light-7);
|
||||
background: var(--el-fill-color-lighter);
|
||||
}
|
||||
|
||||
.security-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.security-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, rgba(57, 115, 255, 0.1) 0%, rgba(79, 132, 255, 0.1) 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-color-primary);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.security-text {
|
||||
.security-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.security-desc {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.security-card {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<div class="user-profile">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
|
||||
<ProfileCard
|
||||
:user-info="userInfo"
|
||||
@change-avatar="handleAvatarChange"
|
||||
/>
|
||||
|
||||
<QuickActions @logout="handleLogout" />
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :sm="24" :md="16" :lg="16" :xl="16">
|
||||
<DetailCard
|
||||
:user-info="userInfo"
|
||||
:format-date="formatDate"
|
||||
:get-sex-text="getSexText"
|
||||
@edit="handleEditProfile"
|
||||
/>
|
||||
|
||||
<SecurityCard
|
||||
:user-info="userInfo"
|
||||
@change-password="handleChangePassword"
|
||||
@bind-phone="bindPhoneDialogVisible = true"
|
||||
@bind-email="bindEmailDialogVisible = true"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<EditProfileDialog
|
||||
v-model="editDialogVisible"
|
||||
:user-info="userInfo"
|
||||
:user-id="authStore.user?.id"
|
||||
@saved="fetchUserInfo"
|
||||
/>
|
||||
|
||||
<ChangePasswordDialog
|
||||
v-model="passwordDialogVisible"
|
||||
:user-id="authStore.user?.id"
|
||||
/>
|
||||
|
||||
<BindPhoneDialog
|
||||
v-model="bindPhoneDialogVisible"
|
||||
:user-info="userInfo"
|
||||
:user-id="authStore.user?.id"
|
||||
@saved="fetchUserInfo"
|
||||
/>
|
||||
|
||||
<BindEmailDialog
|
||||
v-model="bindEmailDialogVisible"
|
||||
:user-info="userInfo"
|
||||
:user-id="authStore.user?.id"
|
||||
@saved="fetchUserInfo"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { getUserInfo } from "@/api/user";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import ProfileCard from "./components/ProfileCard.vue";
|
||||
import QuickActions from "./components/QuickActions.vue";
|
||||
import DetailCard from "./components/DetailCard.vue";
|
||||
import SecurityCard from "./components/SecurityCard.vue";
|
||||
import EditProfileDialog from "./components/EditProfileDialog.vue";
|
||||
import ChangePasswordDialog from "./components/ChangePasswordDialog.vue";
|
||||
import BindPhoneDialog from "./components/BindPhoneDialog.vue";
|
||||
import BindEmailDialog from "./components/BindEmailDialog.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const userInfo = ref(null);
|
||||
const editDialogVisible = ref(false);
|
||||
const passwordDialogVisible = ref(false);
|
||||
const bindPhoneDialogVisible = ref(false);
|
||||
const bindEmailDialogVisible = ref(false);
|
||||
|
||||
const formatDate = (date) => {
|
||||
if (!date) return "-";
|
||||
const d = new Date(date);
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
const hours = String(d.getHours()).padStart(2, "0");
|
||||
const minutes = String(d.getMinutes()).padStart(2, "0");
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
const getSexText = (sex) => {
|
||||
const map = {
|
||||
0: "保密",
|
||||
1: "男",
|
||||
2: "女"
|
||||
};
|
||||
return map[sex] || "-";
|
||||
};
|
||||
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
const userId = authStore.user?.id;
|
||||
if (!userId) {
|
||||
ElMessage.error("用户ID不存在");
|
||||
return;
|
||||
}
|
||||
const res = await getUserInfo(userId);
|
||||
if (res.code === 200) {
|
||||
userInfo.value = res.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取用户信息失败", error);
|
||||
ElMessage.error("获取用户信息失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditProfile = () => {
|
||||
editDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleChangePassword = () => {
|
||||
passwordDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleAvatarChange = () => {
|
||||
ElMessage.info("头像上传功能开发中");
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
ElMessageBox.confirm("确定要退出登录吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(async () => {
|
||||
try {
|
||||
await authStore.logout(authStore.user?.id);
|
||||
} catch (error) {
|
||||
console.error("退出登录接口调用失败:", error);
|
||||
}
|
||||
|
||||
// 清除所有本地存储
|
||||
localStorage.removeItem("active_tab");
|
||||
localStorage.removeItem("1123567809876");
|
||||
localStorage.removeItem("tabs_list");
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("user");
|
||||
localStorage.removeItem("tenant");
|
||||
|
||||
sessionStorage.removeItem("active_tab");
|
||||
sessionStorage.removeItem("456");
|
||||
sessionStorage.removeItem("tabs_list");
|
||||
sessionStorage.removeItem("token");
|
||||
sessionStorage.removeItem("user");
|
||||
sessionStorage.removeItem("tenant");
|
||||
|
||||
// 清除所有 cookies,包括跨域和不同路径的 cookie
|
||||
const clearCookie = (name) => {
|
||||
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
|
||||
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=`;
|
||||
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${location.hostname}`;
|
||||
if (location.hostname !== "localhost") {
|
||||
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=${location.hostname}`;
|
||||
}
|
||||
};
|
||||
|
||||
// 清除所有已知 cookie
|
||||
clearCookie("PHPSESSID");
|
||||
clearCookie("token");
|
||||
clearCookie("user");
|
||||
clearCookie("tenant");
|
||||
|
||||
// 同时清除所有现有的 cookie
|
||||
const cookies = document.cookie.split(";");
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
const eqPos = cookie.indexOf("=");
|
||||
const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
|
||||
if (name) {
|
||||
clearCookie(name);
|
||||
}
|
||||
}
|
||||
|
||||
authStore.clearToken();
|
||||
router.push("/login");
|
||||
ElMessage.success("已退出登录");
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchUserInfo();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.user-profile {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user