Files
qingcheng-Web/src/view/userList/adminList.vue

1500 lines
37 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- 搜索框 -->
<div class="search-box" v-if="hasPermission">
<div class="search-container">
<a-form layout="inline">
<a-space>
<a-form-item label="手机号">
<a-input-search
style="width: 300px;"
placeholder="请输入手机号"
enter-button
@search="handlePhoneSearch"
v-model:value="searchPhone"
type="text"
class="custom-search"
:maxlength="11"
@input="handleSearchPhoneInput"
@keydown="handleKeyDown"
/>
</a-form-item>
<a-button class="custom-button" @click="showModal">新增管理员</a-button>
<!--新增用户-->
<a-modal
v-model:open="openUser"
title="新增管理员"
@ok="handleOk"
:footer="null"
>
<a-form
ref="formRef"
:label-col="{ span: 5 }" :wrapper-col="{ span: 12 }"
@submit="handleSubmit"
:rules="formRules"
>
<a-form-item label="昵称" name="nickName">
<a-input
v-model:value="formData.nickName"
:maxlength="6"
@input="handleNicknameInput"
autocomplete="off"
/>
<template #help>
<span v-if="formData.nickName.length > 0" class="tip">
{{ formData.nickName.length }}/6
<span v-if="formData.nickName.length > 6" class="error-tip">超过最大长度</span>
</span>
</template>
</a-form-item>
<a-form-item label="头像" name="userAvatar" required>
<a-upload
name="file"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:before-upload="beforeUpload"
:custom-request="handleUpload"
>
<img v-if="formData.userAvatar" :src="previewImage" alt="avatar" style="width: 100%"/>
<div v-else>
<loading-outlined v-if="uploadLoading"></loading-outlined>
<plus-outlined v-else></plus-outlined>
<div class="ant-upload-text">上传头像</div>
</div>
</a-upload>
</a-form-item>
<a-form-item label="手机号" name="phoneNumber">
<a-input
v-model:value="formData.phoneNumber"
:maxlength="11"
@input="handlePhoneInput"
autocomplete="off"
/>
<template #help>
<span v-if="formData.phoneNumber.length > 0" class="tip">
{{ formData.phoneNumber.length }}/11
<span v-if="formData.phoneNumber.length > 11" class="error-tip">超过最大长度</span>
</span>
</template>
</a-form-item>
<!-- 修改账户表单项 -->
<a-form-item label="账号" name="userAccount">
<a-input
v-model:value="formData.userAccount"
:maxlength="11"
@input="handleAccountInput"
autocomplete="off"
/>
<template #help>
<span v-if="formData.userAccount.length > 0" class="tip">
{{ formData.userAccount.length }}/11
<span v-if="!/^[A-Za-z0-9]+$/.test(formData.userAccount)" class="error-tip">包含非法字符</span>
</span>
</template>
</a-form-item>
<a-form-item label="密码" name="userPassword">
<a-input
v-model:value="formData.userPassword"
type="password"
:maxlength="10"
@input="handlePasswordInput"
autocomplete="off"
/>
<template #help>
<div v-if="formData.userPassword.length > 0">
<span class="tip">{{ formData.userPassword.length }}/10</span>
<div class="password-strength">
<span :class="{ 'strength-ok': hasLower }">小写字母</span>
<span :class="{ 'strength-ok': hasUpper }">大写字母</span>
<span :class="{ 'strength-ok': hasNumber }">数字</span>
</div>
</div>
<!-- 新增错误提示 -->
<span v-if="formData.userPassword.length > 0 && !passwordMeetsRequirements" class="error-tip">
请输入包含数字字母小写字母大写的组合
</span>
</template>
</a-form-item>
<a-form-item :wrapper-col="{ span: 16, offset: 5 }">
<a-button type="primary" html-type="submit">提交</a-button>
</a-form-item>
</a-form>
</a-modal>
<a-button
type="primary"
danger
:disabled="selectedRowKeys.length === 0"
@click="batchDelete"
>
批量删除({{ selectedRowKeys.length }})
</a-button>
<a-button class="custom-button" @click="reset">重置搜索</a-button>
</a-space>
</a-form>
</div>
</div>
<!-- 数据-->
<a-table
:columns="columns"
:data-source="tableData"
:scroll="{ x: 1400, y: 550 }"
:loading="loading"
:row-selection="rowSelection"
:pagination="pagination"
bordered
rowKey="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'userAvatar'">
<a-avatar :src="downLoadImage + record.userAvatar" />
</template>
<template v-if="column.key === 'operation'">
<a-space :size="16">
<a class="action-btn" @click="showDrawer(record)">详情</a>
<a-divider type="vertical" v-if="store.loginUser.userRole === 'boss' && record.userRole === 'admin'"/>
<a class="action-btn" type="link"
@click="confirmDeleteUser(record.id)"
v-if="store.loginUser.userRole === 'boss' && record.userRole === 'admin'">
删除
</a>
</a-space>
</template>
</template>
</a-table>
<!-- 抽屉-->
<a-drawer
v-model:open="open"
title="用户详细信息"
placement="right"
width="600"
>
<div v-if="selectedUser" class="user-detail">
<div class="header">
<a-avatar :size="128" :src="downLoadImage+selectedUser.userAvatar" class="avatar" />
<a-button
v-if="store.loginUser.userRole === 'boss' && selectedUser?.userRole === 'admin'"
@click="toggleEditMode"
class="custom-button"
>
{{ isEditMode ? '取消编辑' : '编辑信息' }}
</a-button>
</div>
<a-form
v-if="isEditMode"
:model="editForm"
:rules="editFormRules"
ref="editFormRef"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<!-- 可编辑表单 -->
<a-form-item label="昵称" name="nickName">
<a-input
v-model:value="editForm.nickName"
:maxlength="6"
@input="handleEditNicknameInput"
@keypress="handleMaxLength(6)"
>
<template #suffix>
<span v-if="editForm.nickName.length > 0" class="input-tip">
{{ editForm.nickName.length }}/6
<span v-if="editForm.nickName.length > 6" class="error-tip">超过最大长度</span>
</span>
</template>
</a-input>
</a-form-item>
<a-form-item label="头像" name="userAvatar" required>
<a-upload
name="file"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:before-upload="beforeUpload"
:custom-request="handleEditUpload"
>
<img v-if="editForm.userAvatar" :src="editPreviewImage" alt="avatar" style="width: 100%"/>
<div v-else>
<loading-outlined v-if="editUploadLoading"></loading-outlined>
<plus-outlined v-else></plus-outlined>
<div class="ant-upload-text">上传头像</div>
</div>
</a-upload>
</a-form-item>
<a-form-item label="手机号" name="phoneNumber">
<a-input
v-model:value="editForm.phoneNumber"
:maxlength="11"
@input="handleEditPhoneInput"
>
<template #suffix>
<span v-if="editForm.phoneNumber.length > 0" class="input-tip">
{{ editForm.phoneNumber.length }}/11
<span v-if="editForm.phoneNumber.length > 11" class="error-tip">超过最大长度</span>
</span>
</template>
</a-input>
</a-form-item>
<a-form-item label="账户">
<a-input v-model:value="editForm.userAccount" disabled />
</a-form-item>
<a-form-item label="密码" name="userPassword" required>
<a-input
v-model:value="editForm.userPassword"
type="password"
:maxlength="10"
@input="handleEditPasswordInput"
@keypress="handleMaxLength(10)"
>
<template #suffix>
<span v-if="editForm.userPassword.length > 0" class="input-tip">
{{ editForm.userPassword.length }}/10
<div class="password-strength">
<span :class="{ 'strength-ok': editHasLower }">小写字母</span>
<span :class="{ 'strength-ok': editHasUpper }">大写字母</span>
<span :class="{ 'strength-ok': editHasNumber }">数字</span>
</div>
</span>
</template>
</a-input>
</a-form-item>
<a-form-item label="身份">
<a-select v-model:value="editForm.userRole" disabled>
<a-select-option value="user">普通用户</a-select-option>
<a-select-option value="admin">管理员</a-select-option>
<a-select-option value="boss">老板</a-select-option>
</a-select>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 6 }">
<a-button class="custom-button" @click="handleSave">保存修改</a-button>
</a-form-item>
</a-form>
<div v-else class="view-mode">
<!-- 基本信息卡片 -->
<a-card title="基本信息" class="info-card">
<div class="info-item">
<user-outlined class="info-icon" />
<div class="info-content">
<span class="info-label">用户ID</span>
<span class="info-value">{{ selectedUser.id }}</span>
</div>
</div>
<div class="info-item">
<idcard-outlined class="info-icon" />
<div class="info-content">
<span class="info-label">账户信息</span>
<div class="account-detail">
<span>{{ selectedUser.userAccount }}</span>
<a-tag class="role-tag">
{{ editForm.userRole}}
</a-tag>
</div>
</div>
</div>
<div class="info-item">
<smile-outlined class="info-icon" />
<div class="info-content">
<span class="info-label">昵称</span>
<span class="info-value highlight">{{ selectedUser.nickName }}</span>
</div>
</div>
</a-card>
<!-- 联系信息卡片 -->
<a-card title="联系信息" class="info-card">
<div class="info-item">
<phone-outlined class="info-icon" />
<div class="info-content">
<span class="info-label">手机号码</span>
<span class="info-value">{{ selectedUser.phoneNumber || '未绑定' }}</span>
</div>
</div>
</a-card>
</div>
</div>
</a-drawer>
</template>
<script lang="ts" setup>
import { onMounted, ref,computed } from "vue";
import myAxios from "../../api/myAxios.ts";
import { message, Modal } from "ant-design-vue";
import { PlusOutlined, LoadingOutlined } from '@ant-design/icons-vue';
import type { UploadProps } from 'ant-design-vue';
import {userStore} from "../../store/userStore.ts";
import {downLoadImage} from '../../api/ImageUrl.ts'
const store = userStore()
import {
UserOutlined,
IdcardOutlined,
SmileOutlined,
PhoneOutlined,
} from '@ant-design/icons-vue';
const uploadLoading = ref(false);
const previewImage = ref('');
// 上传前校验
const beforeUpload: UploadProps['beforeUpload'] = file => {
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('只能上传图片文件!');
return false;
}
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isLt5M) {
message.error('图片大小不能超过5MB!');
return false;
}
return true;
};
// 自定义上传处理
const handleUpload = async ({ file }: { file: File }) => {
const form = new FormData();
form.append('file', file);
try {
uploadLoading.value = true;
const storedToken = localStorage.getItem('token');
const res:any = await myAxios.post('/file/upload?biz=avatar', form, {
headers: {
Authorization: storedToken,
}
});
console.log(res)
if (res.code === 1) {
formData.value.userAvatar = res.data;
previewImage.value = URL.createObjectURL(file);
message.success('上传成功');
} else {
message.error(res.message || '上传失败');
}
} catch (error:any) {
console.error('上传失败详情:', error);
message.error(`上传失败:${error.response?.data?.error || error.message}`);
} finally {
uploadLoading.value = false;
}
};
const handleEditNicknameInput = (e: any) => {
const trimmedValue = e.target.value.trim().slice(0, 6);
editForm.value.nickName = trimmedValue;
editFormRef.value?.validateField('nickName');
};
const handleEditPhoneInput = (e: any) => {
const numericValue = e.target.value.replace(/\D/g, '').slice(0, 11);
editForm.value.phoneNumber = numericValue;
editFormRef.value?.validateField('phoneNumber');
};
const handleEditPasswordInput = (e: any) => {
editForm.value.userPassword = e.target.value.slice(0, 10);
};
const handleMaxLength = (max: number) => (e: KeyboardEvent) => {
if (e.target instanceof HTMLInputElement && e.target.value.length >= max) {
e.preventDefault();
}
};
const formRules = {
nickName: [
{
required: true,
message: '请输入昵称',
trigger: ['input', 'blur']
},
{
min: 1,
max: 6,
message: '昵称长度需为1-6位',
trigger: ['input', 'blur']
}
],
phoneNumber: [
{
required: true,
message: '请输入手机号',
trigger: ['input', 'blur']
},
{
pattern: /^1[3-9]\d{9}$/,
message: '必须是有效的11位手机号',
trigger: ['input', 'blur']
}
],
userAccount: [
{
required: true,
message: '请输入账户',
trigger: ['input', 'blur']
},
{
min: 6,
max: 11,
message: '长度需为6-11位',
trigger: ['input', 'blur']
},
{
pattern: /^[A-Za-z0-9]+$/,
message: '不能包含中文或特殊字符',
trigger: ['input', 'blur']
}
],
userPassword: [
{
required: true,
message: '请输入密码',
trigger: ['input', 'blur']
},
{
min: 6,
max: 10,
message: '长度需为6-10位',
trigger: ['input', 'blur']
},
{
validator: (_: any, value: string) => {
if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
return Promise.reject('请输入包含数字、字母小写、字母大写的组合');
}
return Promise.resolve();
},
trigger: ['input', 'blur']
}
]
}
const handleSearchPhoneInput = (e: any) => {
let value = e.target.value;
// 只允许输入数字并截取前11位
const newValue = value.replace(/[^0-9]/g, '').slice(0, 11);
if (newValue !== value) {
searchPhone.value = newValue;
}
};
// 新增 onKeyDown 事件处理函数
const handleKeyDown = (e: KeyboardEvent) => {
const allowedKeys = [
'Backspace',
'Delete',
'ArrowLeft',
'ArrowRight',
'Tab',
];
if (!/[0-9]/.test(e.key) && !allowedKeys.includes(e.key)) {
e.preventDefault();
}
};
const formData = ref({
nickName: '',
userAvatar: '',
phoneNumber: '',
userAccount: '',
userPassword: ''
});
// 手机号实时校验(输入时显示长度提示)
const handlePhoneInput = (e: any) => {
// 实时过滤非数字字符并限制长度
formData.value.phoneNumber = e.target.value
.replace(/\D/g, '')
.slice(0, 11);
}
const handleAccountInput = (e: any) => {
// 过滤特殊字符并限制长度
formData.value.userAccount = e.target.value
.replace(/[^A-Za-z0-9]/g, '')
.slice(0, 11);
}
const handleNicknameInput = (e: any) => {
formData.value.nickName = e.target.value.slice(0, 6);
}
const passwordMeetsRequirements = computed(() => {
const password = formData.value.userPassword;
return /(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(password);
});
// 实时密码强度计算(保持原有)
const hasLower = computed(() => /[a-z]/.test(formData.value.userPassword))
const hasUpper = computed(() => /[A-Z]/.test(formData.value.userPassword))
const hasNumber = computed(() => /\d/.test(formData.value.userPassword))
// 密码输入处理
const handlePasswordInput = (e: any) => {
formData.value.userPassword = e.target.value.slice(0, 10);
}
const loading = ref(false);
const searchParams = ref({
current: 1,
pageSize: 10,
sortField: "id",
sortOrder: "ascend",
userRole:"admin",
phoneNumber:null
});
//用户表
const columns = [
{
title: '用户ID',
dataIndex: 'id',
width: 50,
key: 'id',
fixed: 'left',
align: 'center',
sorter: true,
sortDirections: ['ascend', 'descend']
},
{
title: '头像',
dataIndex: 'userAvatar',
key: 'userAvatar',
width: 50,
fixed: 'left',
align: 'center'
},
{
title: '账号',
dataIndex: 'userAccount',
width: 90,
key: 'userAccount',
fixed: 'left',
align: 'center'
},
{
title: '用户昵称',
dataIndex: 'nickName',
width: 70,
key: 'nickName',
align: 'center'
},
{
title: '身份',
dataIndex: 'userRole',
key: 'userRole',
width: 50,
align: 'center'
},
{
title: '手机号',
dataIndex: 'phoneNumber',
key: 'phoneNumber',
width: 80,
align: 'center',
sorter: true,
sortDirections: ['ascend', 'descend']
},
{
title: '操作',
key: 'operation',
fixed: 'right',
width: 90,
align: 'center'
}
];
// 分页配置
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `${total}`,
pageSizeOptions: ['10', '20', '50', '100']
});
//编辑用户表单
const editFormRules = {
nickName: [
{
required: true,
message: '请输入昵称',
trigger: ['input', 'blur']
},
{
min: 1,
max: 6,
message: '昵称长度需为1-6位',
trigger: ['input', 'blur']
}
],
phoneNumber: [
{
required: true,
message: '请输入手机号',
trigger: ['input', 'blur']
},
{
pattern: /^1[3-9]\d{9}$/,
message: '必须是有效的11位手机号',
trigger: ['input', 'blur']
}
],
userPassword: [
{
min: 6,
max: 10,
message: '长度需为6-10位',
trigger: ['input', 'blur']
},
{
validator: (_: any, value: string) => {
if (value && !/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
return Promise.reject('必须包含大小写字母和数字');
}
return Promise.resolve();
},
trigger: ['input', 'blur']
}
]
};
// 编辑表单的密码强度计算
const editHasLower = computed(() => /[a-z]/.test(editForm.value.userPassword))
const editHasUpper = computed(() => /[A-Z]/.test(editForm.value.userPassword))
const editHasNumber = computed(() => /\d/.test(editForm.value.userPassword))
const editFormRef = ref();
// 修改后的分页处理函数
const handleTableChange = (pag: any, _: any, sorter: any) => {
let sortField = "id";
let sortOrder = "ascend";
if (sorter.field) {
sortField = sorter.field;
sortOrder = sorter.order;
}
searchParams.value = {
...searchParams.value,
current: pag.current,
pageSize: pag.pageSize,
sortField: sortField,
sortOrder: sortOrder
};
pagination.value.current = pag.current;
pagination.value.pageSize = pag.pageSize;
getUserList();
};
const getUserList = async () => {
loading.value = true;
try {
const storedToken = localStorage.getItem('token');
if (!storedToken) throw new Error('未找到登录信息');
const res: any = await myAxios.post("/userInfo/page",
{...searchParams.value },
{ headers: { Authorization: storedToken } }
);
console.log(res)
if (res.code === 1 && res.data) {
tableData.value = res.data.records.map((item: any) => {
if (item.parentUserId === -1) {
item.parentUserId = '无';
}
return {
...item,
superUserList: item.superHostList? item.superHostList.join(', ') : '无'
};
});
pagination.value.total = res.data.total;
pagination.value.current = res.data.current;
pagination.value.pageSize = res.data.size;
}
} finally {
loading.value = false;
}
};
// 新增权限控制变量
const hasPermission = ref(true);
onMounted(async () => {
// 检查用户权限
await checkPermission();
// 如果有权限才加载数据
if (hasPermission.value) {
getUserList();
}
});
// 检查用户权限
const checkPermission = async () => {
try {
// 获取当前用户角色
const userRole = store.loginUser.userRole;
// 如果是管理员,显示提示并隐藏内容
if (userRole === 'admin') {
hasPermission.value = false;
// 使用Modal显示提示
Modal.warning({
title: '权限提示',
content: '管理员无权限查看管理员列表',
okText: '确定',
onOk() {
// 可以添加额外的处理逻辑
}
});
} else {
hasPermission.value = true;
}
} catch (error) {
console.error('权限检查失败:', error);
message.error('权限检查失败');
hasPermission.value = false;
}
};
// ID查询方法
interface User {
id: number;
nickName: string;
userAvatar: string;
phoneNumber: string;
userAccount: string;
userPassword: string;
invitationCode: string;
userRole:string;
parentUserId:number
}
const tableData = ref<User[]>([]);
const searchPhone = ref("");
const handlePhoneSearch = async () => {
if (!searchPhone.value) {
message.warning('请输入手机号');
return;
}
// 验证手机号格式
if (!/^1[3-9]\d{9}$/.test(searchPhone.value)) {
message.warning('请输入有效的11位手机号');
return;
}
loading.value = true;
try {
const storedToken = localStorage.getItem('token');
if (!storedToken) throw new Error('未找到登录信息');
const res: { code: number; data: any; message: any } = await myAxios.post(
"/userInfo/page",
{
current: 1,
pageSize: 10,
phoneNumber: searchPhone.value
},
{ headers: { Authorization: storedToken } }
);
if (res.code === 1 && res.data) {
tableData.value = res.data.records.map((item: any) => {
if (item.parentUserId === -1) {
item.parentUserId = '无';
}
return {
...item,
superUserList: item.superHostList? item.superHostList.join(', ') : '无'
};
});
// 更新分页信息
pagination.value.total = res.data.total;
pagination.value.current = res.data.current;
pagination.value.pageSize = res.data.size;
} else {
message.error(res.message || '查询失败');
}
} catch (error) {
console.error('查询失败:', error);
message.error('查询操作失败,请检查网络');
} finally {
loading.value = false;
}
};
const reset = () => {
searchPhone.value = "";
searchParams.value = {
current: 1,
pageSize: 10,
sortField: "id",
sortOrder: "ascend",
userRole: "admin",
phoneNumber: null
};
getUserList();
};
// 删除用户
const selectedRowKeys = ref<number[]>([]);
// 配置行选择器
const rowSelection = {
onChange: (selectedKeys: number[]) => {
selectedRowKeys.value = selectedKeys;
},
selectedRowKeys: selectedRowKeys,
getCheckboxProps: (record: User) => ({
disabled: record.userRole !== 'admin',
name: record.id.toString(),
}),
};
const confirmDeleteUser = (id: number) => {
Modal.confirm({
title: '确认删除用户',
content: '您确定要删除这个用户吗?此操作不可恢复!',
okText: '确认删除',
okType: 'danger',
cancelText: '取消',
onOk() {
return deleteUser(id);
},
onCancel() {
console.log('用户取消删除');
},
});
};
const batchDelete = async () => {
if (selectedRowKeys.value.length === 0) {
message.warning('请先选择要删除的用户');
return;
}
Modal.confirm({
title: '确认批量删除',
content: `您确定要删除选中的 ${selectedRowKeys.value.length} 个用户吗?此操作不可恢复!`,
okText: '确认删除',
okType: 'danger',
cancelText: '取消',
async onOk() {
try {
const storedToken = localStorage.getItem('token');
if (!storedToken) throw new Error('未找到登录信息');
const res:any = await myAxios.post(
'/userInfo/delBatch',
{ ids: selectedRowKeys.value },
{
headers: {
Authorization: storedToken,
'AfterScript': 'required-script'
}
}
);
if (res.code === 1) {
message.success(`成功删除${selectedRowKeys.value.length}条数据`);
selectedRowKeys.value = [];
await getUserList();
} else {
message.error(res.message || '删除失败');
}
} catch (error) {
console.error('批量删除失败:', error);
message.error('删除操作失败,请检查网络');
}
},
onCancel() {
console.log('用户取消批量删除');
},
});
};
const deleteUser = async (id: number) => {
try {
const storedToken = localStorage.getItem('token');
const res:any = await myAxios.post(
"/userInfo/delete",
{ id },
{
headers: {
Authorization: storedToken,
'AfterScript': 'required-script'
}
}
);
if (res.code === 1) {
message.success('删除成功');
await getUserList();
} else {
message.error(res.message || '删除失败');
}
} catch (error) {
console.error('删除失败:', error);
message.error('删除操作失败');
}
};
// 操作用户
const open = ref<boolean>(false);
const selectedUser = ref<any>(null);
const isEditMode = ref(false);
const editForm = ref({
id: 0,
nickName: '',
userAvatar: '',
phoneNumber: '',
userAccount: '',
userPassword: '',
invitationCode: '',
userRole: '',
parentUserId: 0,
SuperUserList: ''
});
const editUploadLoading = ref(false);
const editPreviewImage = ref('');
const handleEditUpload = async ({ file }: { file: File }) => {
const form = new FormData();
form.append('file', file);
try {
editUploadLoading.value = true;
const storedToken = localStorage.getItem('token');
const res: any = await myAxios.post('/file/upload?biz=avatar', form, {
headers: {
Authorization: storedToken,
}
});
if (res.code === 1) {
editForm.value.userAvatar = res.data;
editPreviewImage.value = URL.createObjectURL(file);
message.success('头像更新成功');
} else {
message.error(res.message || '头像上传失败');
}
} catch (error: any) {
console.error('编辑头像上传失败:', error);
message.error(`上传失败:${error.response?.data?.error || error.message}`);
} finally {
editUploadLoading.value = false;
}
};
const showDrawer = (record: any) => {
selectedUser.value = record;
editForm.value = {
id: record.id,
nickName: record.nickName,
userAvatar: record.userAvatar||'',
phoneNumber: record.phoneNumber,
userAccount: record.userAccount,
userPassword: record.userPassword,
invitationCode: record.invitationCode,
userRole: record.userRole,
parentUserId: record.parentUserId,
SuperUserList: Array.isArray(record.superHostList)
? record.superHostList.join(',')
: record.SuperUserList || ''
};
open.value = true;
isEditMode.value = false;
};
const toggleEditMode = () => {
isEditMode.value = !isEditMode.value;
};
const handleSave = async () => {
try {
if (editForm.value.userPassword) {
const password = editForm.value.userPassword;
const hasNumber = /\d/.test(password);
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
if (!(hasNumber && hasUpperCase && hasLowerCase)) {
message.error('密码必须包含数字、大写字母和小写字母');
return;
}
}
const storedToken = localStorage.getItem('token');
if (!storedToken) throw new Error('未找到登录信息');
const processValue = (value: any) => {
if (value === "" || value === "无") return null;
return value;
};
const processNumber = (value: any) => {
const num = Number(value);
return isNaN(num) ? null : num;
};
const payload = {
id: editForm.value.id,
nickName: processValue(editForm.value.nickName),
userAvatar: processValue(editForm.value.userAvatar),
phoneNumber: processValue(editForm.value.phoneNumber),
userAccount: editForm.value.userAccount, // 保持禁用状态的值
userPassword: editForm.value.userPassword || undefined, // 密码为空时不修改
invitationCode: processValue(editForm.value.invitationCode),
userRole: editForm.value.userRole || 'user',
parentUserId: processNumber(editForm.value.parentUserId),
SuperUserList: editForm.value.SuperUserList
? editForm.value.SuperUserList.split(',')
.map(item => processNumber(item.trim()))
.filter(num => num !== null)
: []
};
const cleanPayload = Object.fromEntries(
Object.entries(payload).filter(([_, v]) => v !== undefined)
);
console.log("最终请求体:", JSON.stringify(cleanPayload, null, 2));
const res:any = await myAxios.post('/userInfo/update', cleanPayload, {
headers: {
Authorization: storedToken,
'Content-Type': 'application/json'
}
});
console.log(res)
if (res.code === 1) {
message.success('更新成功');
await getUserList();
const updatedUser = tableData.value.find(item => item.id === editForm.value.id);
if (updatedUser) {
selectedUser.value = { ...updatedUser };
}
isEditMode.value = false;
} else {
message.error(res.message || '更新失败');
}
} catch (error:any) {
console.error('更新失败:', error);
message.error(error.response?.data?.message || '更新失败,请检查网络');
}
};
const openUser = ref<boolean>(false);
const formRef = ref();
const showModal = () => {
openUser.value = true;
formData.value = {
nickName: '',
userAvatar: '',
phoneNumber: '',
userAccount: '',
userPassword: ''
};
previewImage.value = '';
};
const handleOk = (e: MouseEvent) => {
console.log('点击确认:', e);
openUser.value = false;
};
const extractErrorMessage = (errorMessage: string): string => {
const pipeIndex = errorMessage.indexOf('|');
if (pipeIndex !== -1) {
return errorMessage.substring(pipeIndex + 1).trim();
}
return errorMessage;
};
const handleSubmit = async () => {
try {
const password = formData.value.userPassword;
if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(password)) {
message.error('请输入包含数字、字母小写、字母大写的组合');
return;
}
// 原有手机号校验
if (!/^1[3-9]\d{9}$/.test(formData.value.phoneNumber)) {
message.error('请输入有效的11位手机号');
return;
}
const storedToken = localStorage.getItem('token');
if (!storedToken) throw new Error('未找到登录信息');
const res: any = await myAxios.post('/userInfo/add', formData.value, {
headers: {
Authorization: storedToken,
'Content-Type': 'application/json'
}
});
if (res.code === 1) {
message.success('新增成功');
openUser.value = false;
formRef.value?.resetFields();
getUserList();
} else {
const errorMsg = extractErrorMessage(res.message);
message.error(`${errorMsg}`);
}
} catch (error: any) {
let errorMsg = error.response?.message || error.message;
errorMsg = extractErrorMessage(errorMsg);
message.error(`${errorMsg}`);
}
};
</script>
<style scoped>
.action-btn {
padding: 0 8px;
}
:deep(.ant-divider-vertical) {
border-color: rgba(0, 0, 0, 0.15);
height: 1.2em;
margin: 0 4px;
}
.user-detail {
padding: 20px;
}
.avatar {
display: block;
margin: 0 auto 20px;
}
:deep(.ant-descriptions-item-label) {
font-weight: 600;
width: 120px;
}
.header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
}
.avatar {
margin-bottom: 20px;
}
.custom-button {
width: 120px;
}
.view-mode {
display: flex;
flex-direction: column;
gap: 16px;
}
.info-card {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
margin-bottom: 16px;
}
.info-card :deep(.ant-card-head) {
background: #fafafa;
border-bottom: 1px solid #e8e8e8;
}
.info-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.info-item:last-child {
border-bottom: none;
}
.info-icon {
font-size: 18px;
color: #1890ff;
margin-right: 16px;
width: 24px;
text-align: center;
}
.info-content {
flex: 1;
}
.info-label {
display: block;
color: #666;
font-size: 12px;
margin-bottom: 4px;
}
.info-value {
font-size: 14px;
color: #333;
font-weight: 500;
}
.highlight {
color: #1890ff;
}
.role-tag {
margin-left: 8px;
font-size: 12px;
}
:root {
--role-user: #87d068;
--role-admin: #f50;
--role-boss: #722ed1;
}
.role-tag[color="user"] {
background: var(--role-user);
color: white;
}
.role-tag[color="admin"] {
background: var(--role-admin);
color: white;
}
.role-tag[color="boss"] {
background: var(--role-boss);
color: white;
}
.search-box {
margin-bottom: 10px;
}
.error-tip {
color: #ff4d4f;
font-size: 12px;
margin-left: 8px;
}
.tip {
color: rgba(0, 0, 0, 0.45);
font-size: 12px;
}
.password-strength {
margin-top: 4px;
display: flex;
gap: 8px;
}
.password-strength span {
color: #999;
font-size: 12px;
position: relative;
padding-left: 18px;
}
.password-strength span::before {
content: '';
width: 12px;
height: 12px;
border-radius: 50%;
background: #eee;
position: absolute;
left: 0;
top: 2px;
}
.strength-ok {
color: #52c41a;
}
.strength-ok::before {
background: #52c41a !important;
}
.avatar-uploader :deep(.ant-upload) {
width: 100%;
height: 100%;
overflow: hidden;
}
.ant-upload-text {
margin-top: 8px;
}
.input-tip {
font-size: 0.875rem;
color: #666;
}
.error-tip {
color: #f5222d;
margin-left: 4px;
}
.password-strength {
margin-top: 4px;
font-size: 0.875rem;
}
.strength-ok {
color: #52c41a;
}
.avatar-uploader {
width: 100%;
max-width: 200px;
margin: 0 auto;
}
.avatar-uploader .ant-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .ant-upload:hover {
border-color: #40a9ff;
}
.avatar-uploader img {
width: 100%;
height: 100%;
display: block;
}
.custom-button {
background-color: #ffa940;
border-color: #ffa940;
color: #fff;
}
.custom-button:hover,
.custom-button:focus {
background-color: #ffa940;
border-color: #ffa940;
color: #fff;
}
.custom-button[disabled] {
background-color: #ffa940;
border-color: #ffa940;
opacity: 0.6;
color: #fff;
}
.custom-search :deep(.ant-input-search-button) {
background-color: #ffa940;
border-color: #ffa940;
}
.custom-search :deep(.ant-input-search-button:hover),
.custom-search :deep(.ant-input-search-button:focus) {
background-color: #fa8c16;
border-color: #fa8c16;
}
.custom-search :deep(.ant-input) {
border-right-color: #ffa940;
}
:deep(.ant-checkbox-wrapper-disabled) {
display: none;
}
</style>