发布v2.0.0分支

This commit is contained in:
zhangtao
2025-07-13 19:31:23 +08:00
parent 4150a39713
commit 6a8ca3becc
342 changed files with 39441 additions and 14737 deletions
-239
View File
@@ -1,239 +0,0 @@
<template>
<div class="config-container">
<!-- 文本配置项卡片 -->
<a-card title="文本配置项" style="max-width: 1100px; flex: 1;">
<a-list :data-source="textDataSource" :grid="{ gutter: 16, column: 1 }">
<template #renderItem="{ item }">
<a-list-item>
<div class="config-item">
<span class="config-name">{{ item.config_name }}:
</span>
<a-input
v-model:value="configData[item.config_key]"
:placeholder="`请输入${item.config_name}`"
allowClear
class="long-input"
/>
<a-button
type="primary"
@click="handleSave(item)"
:disabled="!configData[item.config_key]"
:loading="isSaving[item.config_key]"
size="small"
class="save-button"
>
保存
</a-button>
</div>
</a-list-item>
</template>
</a-list>
</a-card>
<!-- 图片配置项卡片 -->
<a-card title="图片配置项" style="flex: 1; max-width: 400px; ">
<a-list :data-source="imageDataSource" :grid="{ gutter: 16, column: 1 }">
<template #renderItem="{ item }">
<a-list-item>
<div class="config-item">
<div class="config-name">{{ item.config_name }}</div>
<div class="config-value">
<a-upload
:name="item.config_key"
v-model:file-list="fileLists[item.config_key]"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="(options) => handleUpload(options, item.config_key)"
class="image-upload"
>
<div v-if="!fileLists[item.config_key] || fileLists[item.config_key].length === 0">
<plus-outlined />
<div style="margin-top: 8px">上传{{ item.config_name }}</div>
</div>
</a-upload>
</div>
<div class="config-action">
<a-button
type="primary"
@click="handleSave(item)"
:disabled="!fileLists[item.config_key]?.length"
:loading="isSaving[item.config_key]"
size="small"
class="save-button"
>
保存
</a-button>
</div>
</div>
</a-list-item>
</template>
</a-list>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { getInitConfig, updateConfig, uploadFile } from '@/api/system/config';
import type { tableDataType } from './types';
import { PlusOutlined } from '@ant-design/icons-vue';
// 定义 configData
const configData = reactive<Record<string, any>>({});
const dataSource = ref<tableDataType[]>([]);
const textDataSource = ref<tableDataType[]>([]);
const imageDataSource = ref<tableDataType[]>([]);
const configState = reactive<tableDataType>({
id: undefined,
config_name: '',
config_key: '',
config_value: '',
config_type: null,
description: ''
});
// 存储文件上传列表
const fileLists = reactive<Record<string, any[]>>({});
// 定义图片类型的配置项键名
const imageKeys = ['sys_web_favicon', 'sys_web_logo', 'sys_login_background'];
// 定义需要展示的配置项键名
const requiredKeys = [
'sys_web_title',
'sys_web_description',
'sys_web_favicon',
'sys_web_logo',
'sys_login_background',
'sys_web_copyright',
'sys_keep_record',
'sys_help_doc',
'sys_web_privacy',
'sys_web_clause',
'sys_git_code'
];
// 保存状态管理
const isSaving = reactive<Record<string, boolean>>({});
// 编辑状态管理
const isEditing = reactive<Record<string, boolean>>({});
// 加载配置数据
const loadConfigData = async () => {
try {
const response = await getInitConfig({});
// 过滤出需要展示的数据
dataSource.value = response.data.data.filter((item: any) => requiredKeys.includes(item.config_key));
// 根据键名分离文本和图片配置项
textDataSource.value = dataSource.value.filter(item => !imageKeys.includes(item.config_key));
imageDataSource.value = dataSource.value.filter(item => imageKeys.includes(item.config_key));
// 将数组转为 key-value 形式
const dataMap: Record<string, any> = {};
dataSource.value.forEach((item: any) => {
dataMap[item.config_key] = item.config_value;
});
// 更新 configData
Object.assign(configData, dataMap);
// 初始化 fileLists,确保图片类型的配置项能够正确显示
imageDataSource.value.forEach((item: any) => {
if (item.config_value) {
fileLists[item.config_key] = [{ url: item.config_value }];
}
});
} catch (error) {
console.error('加载配置数据失败:', error);
}
};
// 自定义上传逻辑
const handleUpload = async (options: any, type: string) => {
const { file, onSuccess, onError } = options;
try {
const formData = new FormData();
formData.append('file', file);
const response = await uploadFile(formData);
// 确保从响应中正确获取 file_url
const fileUrl = response.data.data.file_url;
// 更新 fileLists,确保视图能够正确响应
fileLists[type] = [{ url: fileUrl }];
// 触发视图更新
configData[type] = fileUrl;
onSuccess(response);
} catch (error) {
onError(error);
console.error('上传失败:', error);
}
};
// 保存配置
const handleSave = async (item: any) => {
isSaving[item.config_key] = true; // 设置保存状态
const tempConfigState = {
...configState,
id: item.id,
config_name: item.config_name,
config_key: item.config_key,
// 确保从 fileLists 中获取正确的图片 URL
config_value: imageKeys.includes(item.config_key)
? fileLists[item.config_key][0]?.url || configData[item.config_key]
: configData[item.config_key],
config_type: item.config_type,
description: item.description
};
try {
await updateConfig(tempConfigState);
} catch (error) {
console.error('保存失败:', error);
} finally {
isSaving[item.config_key] = false; // 重置保存状态
}
};
// 图片上传前的校验
const beforeUpload = (file: File) => {
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('只能上传图片文件!');
return false; // 确保返回 false 阻止非法文件上传
}
return true;
};
// 生命周期钩子
onMounted(() => {
loadConfigData();
});
</script>
<style lang="scss" scoped>
.config-container {
display: flex;
justify-content: space-between;
gap: 16px; /* 添加间距 */
}
.config-item {
display: flex;
align-items: center;
gap: 16px;
}
.config-name {
white-space: nowrap; /* 防止换行 */
font-weight: bold; /* 加粗显示 */
}
</style>
+309 -594
View File
@@ -1,494 +1,354 @@
<template>
<div class="profile-container">
<div class="profile-content">
<a-row :gutter="[12, 24]">
<!-- 左侧信息卡片 -->
<a-col :xs="24" :sm="24" :md="16" :lg="6">
<a-card class="info-card" >
<div class="user-info-header">
<div class="avatar-wrapper">
<a-avatar v-if="infoFormState.avatar" :src="infoFormState.avatar" :size="120" />
<a-avatar v-else :size="120">
<template #icon><UserOutlined /></template>
</a-avatar>
<div class="avatar-upload-overlay">
<a-upload
name="avatar"
v-model:file-list="fileList"
list-type="picture-card"
:show-upload-list="false"
:before-upload="beforeUpload"
:custom-request="(options) => handleUpload(options)"
>
<div>
<loading-outlined v-if="loading"></loading-outlined>
<plus-outlined v-else></plus-outlined>
<!-- <UploadOutlined /> -->
<div style="margin-top: 8px">上传头像</div>
</div>
</a-upload>
</div>
</div>
<h2 class="user-name">{{ infoFormState.name }}</h2>
<p class="user-role">{{ infoFormState.roles.join('、') }}</p>
<div class="app-container">
<el-row :gutter="12">
<!-- 左侧信息卡片 -->
<el-col :span="6" class="mb-4">
<el-card :loading="loading" shadow="hover">
<template #header>
<div class="card-header">
<span>基本信息</span>
</div>
<a-divider />
<a-descriptions :column="1" size="small" class="user-details">
<a-descriptions-item>
<template #label>
<span class="detail-label">
<IdcardOutlined />
<span>账号</span>
</span>
</template>
<div class="user-info-header">
<div class="avatar-wrapper">
<el-avatar v-if="infoFormState.avatar" :src="infoFormState.avatar" :size="120" />
<el-avatar v-else :size="120">
<template #icon>
<User />
</template>
{{ infoFormState.username }}
</a-descriptions-item>
<a-descriptions-item>
<template #label>
<span class="detail-label">
<TeamOutlined />
<span>部门</span>
</span>
</template>
{{ infoFormState.deptName }}
</a-descriptions-item>
<a-descriptions-item>
<template #label>
<span class="detail-label">
<ApartmentOutlined />
<span>岗位</span>
</span>
</template>
{{ infoFormState.positions.join('、') }}
</a-descriptions-item>
<a-descriptions-item>
<template #label>
<span class="detail-label">
<PhoneOutlined />
<span>手机</span>
</span>
</template>
{{ infoFormState.mobile }}
</a-descriptions-item>
<a-descriptions-item>
<template #label>
<span class="detail-label">
<MailOutlined />
<span>邮箱</span>
</span>
</template>
{{ infoFormState.email }}
</a-descriptions-item>
<a-descriptions-item>
<template #label>
<span class="detail-label">
<ClockCircleOutlined />
<span>加入时间</span>
</span>
</template>
{{ infoFormState.created_at }}
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
</el-avatar>
<el-button type="info" class="avatar-edit-btn" circle icon="Camera" size="small" @click="handleUpload" />
<div class="avatar-upload-overlay">
<el-upload v-model:file-list="fileList" name="avatar" list-type="picture-card" :show-upload-list="false"
:before-upload="beforeUpload" :custom-request="handleUploadCustomRequest">
<div>
<!-- <loading-outlined v-if="loading"></loading-outlined>
<plus-outlined v-else></plus-outlined> -->
<div style="margin-top: 8px">上传头像</div>
</div>
</el-upload>
</div>
</div>
<span class="user-name">
{{ infoFormState.name }}
<el-button type="primary" link :loading="infoSubmitting" icon="edit"
@click="handleOpenDialog(DialogType.ACCOUNT)" />
</span>
<!-- 右侧设置区域 -->
<a-col :xs="24" :sm="24" :md="16" :lg="18">
<a-card class="settings-card" >
<a-tabs v-model:activeKey="selectedKeys[0]" @change="handleTabChange">
<a-tab-pane key="1">
<template #tab>
<span class="tab-label">
<UserOutlined />
<span>基本设置</span>
</span>
</template>
<div class="settings-form">
<a-form
layout="vertical"
:model="infoFormState"
@finish="onInfoFormFinish"
class="info-form"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item
label="姓名"
name="name"
:rules="[{ required: true, message: '请输入姓名' }]"
>
<a-tooltip title="请输入您的真实姓名" placement="topLeft">
<a-input
v-model:value="infoFormState.name"
placeholder="请输入姓名"
allow-clear
>
<template #prefix><UserOutlined /></template>
</a-input>
</a-tooltip>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="性别"
name="gender"
>
<a-radio-group v-model:value="infoFormState.gender" class="gender-group">
<a-radio v-for="item in DictDataStore['sys_user_sex']" :value="item.dict_value">
{{ item.dict_label }}
</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="手机号码"
name="mobile"
:rules="[
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' }
]"
>
<a-tooltip title="用于接收系统通知" placement="topLeft">
<a-input
v-model:value="infoFormState.mobile"
placeholder="请输入手机号码"
allow-clear
>
<template #prefix><PhoneOutlined /></template>
</a-input>
</a-tooltip>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="邮箱"
name="email"
:rules="[{ type: 'email', message: '请输入有效的邮箱地址' }]"
>
<a-tooltip title="用于接收重要通知" placement="topLeft">
<a-input
v-model:value="infoFormState.email"
placeholder="请输入邮箱"
allow-clear
>
<template #prefix><MailOutlined /></template>
</a-input>
</a-tooltip>
</a-form-item>
</a-col>
</a-row>
<el-text>{{infoFormState.roles.map(item => item.name).join('、')}}</el-text>
</div>
<a-form-item>
<a-button
type="primary"
html-type="submit"
:loading="infoSubmitting"
class="submit-button"
>
<template #icon><SaveOutlined /></template>
保存更改
</a-button>
</a-form-item>
</a-form>
</div>
</a-tab-pane>
<el-divider />
<a-tab-pane key="2">
<template #tab>
<span class="tab-label">
<LockOutlined />
<span>安全设置</span>
</span>
</template>
<div class="settings-form">
<a-form
layout="vertical"
:model="passwordFormState"
@finish="onPasswordFormFinish"
class="password-form"
>
<a-form-item
label="当前密码"
name="oldPassword"
:rules="[{ required: true, message: '请输入当前密码' }]"
>
<a-input-password
v-model:value="passwordFormState.oldPassword"
placeholder="请输入当前密码"
allow-clear
>
<template #prefix><LockOutlined /></template>
</a-input-password>
</a-form-item>
<el-descriptions :column="1" size="small" class="user-details">
<a-form-item
label="新密码"
name="newPassword"
:rules="[
{ required: true, message: '请输入新密码' },
{ min: 6, message: '密码长度不能小于6位' }
]"
>
<a-input-password
v-model:value="passwordFormState.newPassword"
placeholder="请输入新密码"
allow-clear
>
<template #prefix><KeyOutlined /></template>
</a-input-password>
</a-form-item>
<el-descriptions-item>
<template #label>
<el-icon style="vertical-align: middle;">
<User />
</el-icon>
<span style="vertical-align: middle;">账号</span>
</template>
<span style="vertical-align: middle;">{{ infoFormState.username }}</span>
</el-descriptions-item>
<a-form-item
label="确认新密码"
name="repeatPassword"
:rules="[
{ required: true, message: '请确认新密码' },
{ validator: validateRepeatPassword }
]"
>
<a-input-password
v-model:value="passwordFormState.repeatPassword"
placeholder="请再次输入新密码"
allow-clear
>
<template #prefix><CheckOutlined /></template>
</a-input-password>
</a-form-item>
<el-descriptions-item>
<template #label>
<el-icon style="vertical-align: middle;">
<Coordinate />
</el-icon>
<span style="vertical-align: middle;">部门</span>
</template>
<span style="vertical-align: middle;">{{ infoFormState.dept_name }}</span>
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<el-icon style="vertical-align: middle;">
<OfficeBuilding />
</el-icon>
<span style="vertical-align: middle;">岗位</span>
</template>
<span style="vertical-align: middle;">{{infoFormState.positions.map(item => item.name).join('、')}}</span>
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<el-icon style="vertical-align: middle;">
<Phone />
</el-icon>
<span style="vertical-align: middle;">手机</span>
</template>
<span style="vertical-align: middle;">{{ infoFormState.mobile }}</span>
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<el-icon style="vertical-align: middle;">
<Message />
</el-icon>
<span style="vertical-align: middle;">邮箱</span>
</template>
<span style="vertical-align: middle;">{{ infoFormState.email }}</span>
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<el-icon style="vertical-align: middle;">
<Clock />
</el-icon>
<span style="vertical-align: middle;">加入时间</span>
</template>
<span style="vertical-align: middle;">{{ infoFormState.created_at }}</span>
</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
<a-form-item>
<a-button
type="primary"
html-type="submit"
:loading="passwordChanging"
class="submit-button"
>
<template #icon><SaveOutlined /></template>
更新密码
</a-button>
</a-form-item>
</a-form>
</div>
</a-tab-pane>
</a-tabs>
</a-card>
</a-col>
</a-row>
</div>
<!-- 右侧设置区域 -->
<el-col :span="18" class="mb-4">
<el-card :loading="loading" shadow="hover">
<el-tabs type="border-card">
<el-tab-pane>
<template #label>
<el-icon>
<User />
</el-icon>
<span>基本设置</span>
</template>
<div>
<el-form ref="ruleFormRef" :model="infoFormState" :rules="rules" label-width="auto" :inline="true">
<el-form-item label="姓名" name="name">
<el-input v-model:value="infoFormState.name" placeholder="请输入姓名" allow-clear>
<template #prefix>
<User />
</template>
</el-input>
</el-form-item>
<el-form-item label="手机号" name="mobile">
<el-input v-model:value="infoFormState.mobile" placeholder="请输入手机号码" allow-clear>
<template #prefix>
<Phone />
</template>
</el-input>
</el-form-item>
<el-form-item label="邮箱" name="email">
<el-input v-model:value="infoFormState.email" placeholder="请输入邮箱" allow-clear>
<template #prefix>
<Message />
</template>
</el-input>
</el-form-item>
<el-form-item label="性别" name="gender">
<el-radio-group v-model:value="infoFormState.gender">
<el-radio v-for="item in DictDataStore['sys_user_sex']" :value="item.dict_value">
{{ item.dict_label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="infoSubmitting" icon="edit">保存更改</el-button>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
<el-tab-pane>
<template #label>
<el-icon>
<Lock />
</el-icon>
<span>安全设置</span>
</template>
<div>
<el-form ref="ruleFormRef" :model="passwordFormState" :rules="resetPasswordRules" label-width="auto">
<el-form-item label="当前密码" name="oldPassword">
<el-input v-model.trim="passwordFormState.oldPassword" :placeholder="t('login.password')" type="password" show-password clearable>
<template #prefix>
<Lock />
</template>
</el-input>
</el-form-item>
<el-form-item label="新密码" name="newPassword">
<el-input v-model.trim="passwordFormState.newPassword" type="password" :placeholder="t('login.newPassword')" show-password clearable>
<template #prefix>
<Key />
</template>
</el-input>
</el-form-item>
<el-form-item label="确认新密码" name="confirmPassword">
<el-input v-model.trim="passwordFormState.confirmPassword" type="password" :placeholder="t('login.message.password.confirm')" show-password clearable>
<template #prefix>
<Check />
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="passwordChanging" icon="edit">更新密码</el-button>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted, h, watch, computed } from 'vue';
import { message } from 'ant-design-vue';
import {
UserOutlined,
LockOutlined,
PhoneOutlined,
MailOutlined,
IdcardOutlined,
TeamOutlined,
ApartmentOutlined,
KeyOutlined,
CheckOutlined,
SaveOutlined,
CloseCircleOutlined,
ClockCircleOutlined,
PlusOutlined,
LoadingOutlined
} from '@ant-design/icons-vue';
import { useUserStore } from "@/store/index";
import storage from 'store';
import { updateCurrentUserInfo, changeCurrentUserPassword, uploadCurrentUserAvatar } from '@/api/system/user';
import type { InfoFormState, PasswordFormState } from './types';
import { useDictStore } from "@/store/index";
import { logout } from '@/api/system/auth';
import { useRouter } from "vue-router";
import type { FormInstance } from 'element-plus'
import UserAPI, { type InfoFormState, type PasswordFormState } from '@/api/system/user';
import { useUserStore, useDictStore } from "@/store";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const userStore = useUserStore();
const dictStore = useDictStore();
const ruleFormRef = ref<FormInstance>();
const loading = ref<boolean>(false);
const dictStore = useDictStore();
const DictDataStore = computed(() => {
return dictStore.dictObj;
})
return dictStore.dictData;
});
// 字典数据
const getOptions = async () => {
const dictOptions = await dictStore.getDict(['sys_user_sex'])
return dictOptions
}
const dictOptions = await dictStore.getDict(['sys_user_sex']);
return dictOptions;
};
// 状态定义
const passwordChanging = ref(false);
const infoSubmitting = ref(false);
const selectedKeys = ref<string[]>(['1']);
// 表单状态
// 用户基础信息表单
const infoFormState = reactive<InfoFormState>({
name: '',
gender: null,
mobile: '',
email: '',
username: '',
deptName: '',
dept_name: '',
positions: [],
roles: [],
avatar: '',
created_at: ''
});
// 修改密码表单
const passwordFormState = reactive<PasswordFormState>({
oldPassword: '',
newPassword: '',
repeatPassword: ''
confirmPassword: ''
});
// 监听selectedKeys变化
watch(selectedKeys, (newVal) => {
if (newVal[0] === '1') {
initInfoForm();
} else {
initPasswordForm();
}
});
const userStore = useUserStore();
// 基本信息表单提交
const onInfoFormFinish = async (values: any) => {
try {
infoSubmitting.value = true;
values.avatar = infoFormState.avatar;
await updateCurrentUserInfo(values);
await userStore.getUserInfo;
} catch (error) {
console.error(error);
} finally {
infoSubmitting.value = false;
}
};
// 路由相关
const router = useRouter();
// 密码表单提交
const onPasswordFormFinish = async (values: any) => {
try {
if (values.newPassword !== values.repeatPassword) {
message.error({
content: '两次密码输入不一致',
icon: h(CloseCircleOutlined, { style: "color: #ff4d4f" })
});
return;
}
passwordChanging.value = true;
const data = {
old_password: values.oldPassword,
new_password: values.newPassword
};
await changeCurrentUserPassword(data);
await logout({ token: storage.get('Access-Token') });
// 重置 store 状态
userStore.$reset();
storage.remove('Access-Token');
storage.remove('Refresh-Token');
await userStore.clearUserInfo;
// 强制刷新页面
router.push('/login');
} catch (error) {
console.error('修改密码失败',error);
} finally {
passwordChanging.value = false;
}
};
// 图片上传前的校验
const beforeUpload = (file: File) => {
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('只能上传图片文件!');
}
return isImage;
};
// 头像上传处理优化
const fileList = ref<any[]>([]);
// 头像上传处理
const handleUpload = async (options: any) => {
const { file, onSuccess, onError } = options;
loading.value = true; // 开始上传,显示加载状态
// 上传文件处理
const handleUploadCustomRequest = async (options: any) => {
const { file, onSuccess, onError } = options;
try {
const formData = new FormData();
formData.append('file', file);
const response = await uploadCurrentUserAvatar(formData);
const response = await UserAPI.uploadCurrentUserAvatar(formData);
const fileUrl = response.data.data.file_url;
infoFormState.avatar = fileUrl;
fileList.value = [{ url: fileUrl }];
onSuccess(response, file);
} catch (error) {
onError(error);
console.error('上传失败:', error);
} finally {
loading.value = false; // 上传结束,隐藏加载状态
}
};
// 邮箱校验规则优化
const rules = {
name: [
{ required: true, message: "请输入姓名", trigger: "blur" },
],
mobile: [
{ required: true, message: "请输入手机号", trigger: "blur" },
{
pattern: /^1[3-9]\d{9}$/,
message: "手机号格式不正确,请输入有效的中国大陆手机号",
trigger: "blur",
},
],
email: [
{ required: true, message: "请输入邮箱", trigger: "blur" },
{
pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/,
message: "邮箱格式不正确,请输入有效的邮箱地址",
trigger: "blur",
},
],
};
const resetPasswordRules = {
oldPassword: [
{
required: true,
trigger: "blur",
message: t("login.password"),
},
],
newPassword: [
{
required: true,
trigger: "blur",
message: t("login.message.password.required"),
},
{
min: 6,
message: t("login.message.password.min"),
trigger: "blur",
},
],
confirmPassword: [
{
required: true,
trigger: "blur",
message: t("login.message.password.required"),
},
{
min: 6,
message: t("login.message.password.min"),
trigger: "blur",
},
{
validator: (_: any, value: string) => {
return value === model.value.new_password;
},
trigger: "blur",
message: t("login.message.password.inconformity"),
},
],
};
// 初始化表单
const initInfoForm = () => {
const basicInfo = userStore.basicInfo;
Object.assign(infoFormState, {
name: basicInfo.name,
gender: basicInfo.gender,
mobile: basicInfo.mobile,
email: basicInfo.email,
username: basicInfo.username,
deptName: basicInfo.dept_name,
positions: basicInfo.positions.map(item => item.name),
roles: basicInfo.roles.map(item => item.name),
avatar: basicInfo.avatar,
created_at: basicInfo.created_at
});
Object.assign(infoFormState, { ...basicInfo });
};
// 初始化密码表单
const initPasswordForm = () => {
Object.assign(passwordFormState, {
oldPassword: '',
newPassword: '',
repeatPassword: ''
confirmPassword: ''
});
};
// 新增密码验证函数
const validateRepeatPassword = async (_rule: any, value: string) => {
if (value !== passwordFormState.newPassword) {
return Promise.reject('两次输入的密码不一致');
}
return Promise.resolve();
};
// 替换菜单点击为标签页切换
const handleTabChange = (key: string) => {
selectedKeys.value = [key];
if (key === '1') {
initInfoForm();
} else {
initPasswordForm();
}
};
onMounted(async () => {
await getOptions();
initInfoForm();
@@ -496,185 +356,40 @@ onMounted(async () => {
</script>
<style lang="scss" scoped>
.profile-container {
/* 样式调整 */
.user-info-header {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
.info-card {
border-radius: var(--border-radius);
box-shadow: var(--card-shadow);
transition: all var(--transition-duration);
&:hover {
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12);
.avatar-wrapper {
position: relative;
margin-bottom: 16px;
.avatar-edit-btn {
position: absolute;
bottom: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
cursor: pointer;
}
.user-info-header {
text-align: center;
padding: 24px 0;
.avatar-wrapper {
position: relative;
border-radius: 50%;
overflow: hidden;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
transition: opacity var(--transition-duration);
}
.avatar-upload-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: var(--hover-bg-color);
padding: 12px 0;
opacity: 0;
transition: all var(--transition-duration);
transform: translateY(100%);
z-index: 1;
.ant-btn {
color: #fff;
font-weight: 500;
font-size: 14px;
&:hover {
color: #40a9ff;
}
.anticon {
margin-right: 4px;
font-size: 16px;
}
}
}
&:hover {
&::after {
opacity: 1;
}
.avatar-upload-overlay {
opacity: 1;
transform: translateY(0);
}
}
.ant-avatar {
width: 100%;
height: 100%;
}
}
.user-name {
margin: 8px 0 4px;
font-size: 20px;
line-height: 28px;
font-weight: 500;
}
.user-role {
font-size: 14px;
}
.avatar-upload-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: none;
z-index: 10;
}
.user-details {
:deep(.ant-descriptions-item) {
padding-bottom: 12px;
.detail-label {
display: inline-flex;
align-items: center;
white-space: nowrap;
.anticon {
margin-right: 8px;
font-size: 16px;
}
}
.ant-descriptions-item-content {
display: inline-block;
}
.ant-descriptions-item-colon {
display: inline-block;
margin: 0 8px;
}
}
&:hover .avatar-upload-overlay {
display: block;
}
}
.settings-card {
border-radius: var(--border-radius);
box-shadow: var(--card-shadow);
:deep(.ant-tabs-nav) {
margin-bottom: 24px;
}
.settings-form {
max-width: 800px;
margin: 0 auto;
padding: 24px;
@media (max-width: 768px) {
padding: 16px 0;
}
.submit-button {
min-width: 120px;
}
}
.info-form, .password-form {
.ant-form-item {
margin-bottom: 24px;
}
.ant-input-affix-wrapper {
border-radius: 4px;
&:hover, &:focus {
border-color: var(--primary-color);
}
}
.gender-group {
width: 100%;
}
}
.password-form {
max-width: 400px;
}
.tab-label {
display: flex;
align-items: center;
.anticon {
margin-right: 8px;
font-size: 16px;
}
}
}
}
// 添加过渡动画
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
</style>
-27
View File
@@ -1,27 +0,0 @@
export interface InfoFormState {
name: string;
gender: number | null;
mobile: string;
email: string;
username: string;
deptName: string;
positions: [];
roles: [];
avatar: string;
created_at: string;
}
export interface PasswordFormState {
oldPassword: string;
newPassword: string;
repeatPassword: string;
}
export interface tableDataType {
id?: number;
config_name?: string;
config_key?: string;
config_value?: string;
config_type?: boolean;
description?: string;
}