2025-06-18 09:26:56 +08:00
|
|
|
|
<template>
|
|
|
|
|
<!-- 搜索框 -->
|
|
|
|
|
<div class="search-box">
|
|
|
|
|
<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>
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
<a-button class="custom-button" @click="showModal">新增管理员</a-button>
|
2025-06-18 09:26:56 +08:00
|
|
|
|
<!--新增用户-->
|
|
|
|
|
<a-modal
|
|
|
|
|
v-model:open="openUser"
|
2025-07-02 09:43:17 +08:00
|
|
|
|
title="新增管理员"
|
2025-06-18 09:26:56 +08:00
|
|
|
|
@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();
|
2025-07-02 09:43:17 +08:00
|
|
|
|
form.append('file', file);
|
2025-06-18 09:26:56 +08:00
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
uploadLoading.value = true;
|
|
|
|
|
const storedToken = localStorage.getItem('token');
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
const res:any = await myAxios.post('/file/upload?biz=avatar', form, {
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: storedToken,
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
console.log(res)
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
|
|
|
|
if (res.code === 1) {
|
2025-06-18 09:26:56 +08:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
const handleEditNicknameInput = (e: any) => {
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
const trimmedValue = e.target.value.trim().slice(0, 6);
|
|
|
|
|
editForm.value.nickName = trimmedValue;
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
editFormRef.value?.validateField('nickName');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleEditPhoneInput = (e: any) => {
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
const numericValue = e.target.value.replace(/\D/g, '').slice(0, 11);
|
|
|
|
|
editForm.value.phoneNumber = numericValue;
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
editFormRef.value?.validateField('phoneNumber');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleEditPasswordInput = (e: any) => {
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
editForm.value.userPassword = e.target.value.slice(0, 10);
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
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) => {
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
const allowedKeys = [
|
|
|
|
|
'Backspace',
|
|
|
|
|
'Delete',
|
|
|
|
|
'ArrowLeft',
|
|
|
|
|
'ArrowRight',
|
|
|
|
|
'Tab',
|
|
|
|
|
];
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
if (!/[0-9]/.test(e.key) && !allowedKeys.includes(e.key)) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
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:null,
|
|
|
|
|
phoneNumber:null
|
|
|
|
|
});
|
|
|
|
|
//用户表
|
|
|
|
|
const columns = [
|
|
|
|
|
{
|
|
|
|
|
title: '用户ID',
|
|
|
|
|
dataIndex: 'id',
|
|
|
|
|
width: 50,
|
|
|
|
|
key: 'id',
|
|
|
|
|
fixed: 'left',
|
|
|
|
|
align: 'center',
|
2025-07-02 09:43:17 +08:00
|
|
|
|
sorter: true,
|
|
|
|
|
sortDirections: ['ascend', 'descend']
|
2025-06-18 09:26:56 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
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',
|
2025-07-02 09:43:17 +08:00
|
|
|
|
sorter: true,
|
|
|
|
|
sortDirections: ['ascend', 'descend']
|
2025-06-18 09:26:56 +08:00
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '邀请码',
|
|
|
|
|
dataIndex: 'invitationCode',
|
|
|
|
|
key: 'invitationCode',
|
|
|
|
|
width: 70,
|
|
|
|
|
align: 'center',
|
2025-07-02 09:43:17 +08:00
|
|
|
|
sorter: true,
|
|
|
|
|
sortDirections: ['ascend', 'descend']
|
2025-06-18 09:26:56 +08:00
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '上级ID',
|
|
|
|
|
dataIndex: 'parentUserId',
|
|
|
|
|
key: 'parentUserId',
|
|
|
|
|
width: 40,
|
|
|
|
|
align: 'center',
|
2025-07-02 09:43:17 +08:00
|
|
|
|
sorter: true,
|
|
|
|
|
sortDirections: ['ascend', 'descend']
|
2025-06-18 09:26:56 +08:00
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
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']
|
|
|
|
|
});
|
|
|
|
|
//编辑用户表单
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
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))
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
const editFormRef = ref();
|
2025-06-18 09:26:56 +08:00
|
|
|
|
|
|
|
|
|
// 修改后的分页处理函数
|
|
|
|
|
const handleTableChange = (pag: any, _: any, sorter: any) => {
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
|
|
|
|
let sortField = "id";
|
|
|
|
|
let sortOrder = "ascend";
|
2025-06-18 09:26:56 +08:00
|
|
|
|
|
|
|
|
|
if (sorter.field) {
|
|
|
|
|
sortField = sorter.field;
|
|
|
|
|
sortOrder = sorter.order;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
searchParams.value = {
|
|
|
|
|
...searchParams.value,
|
|
|
|
|
current: pag.current,
|
|
|
|
|
pageSize: pag.pageSize,
|
2025-07-02 09:43:17 +08:00
|
|
|
|
sortField: sortField,
|
|
|
|
|
sortOrder: sortOrder
|
2025-06-18 09:26:56 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
pagination.value.current = pag.current;
|
|
|
|
|
pagination.value.pageSize = pag.pageSize;
|
|
|
|
|
|
|
|
|
|
getUserList();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
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(', ') : '无'
|
|
|
|
|
};
|
|
|
|
|
});
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
pagination.value.total = res.data.total;
|
|
|
|
|
pagination.value.current = res.data.current;
|
|
|
|
|
pagination.value.pageSize = res.data.size;
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMounted(getUserList);
|
|
|
|
|
|
|
|
|
|
// ID查询方法
|
|
|
|
|
interface User {
|
|
|
|
|
id: number;
|
|
|
|
|
nickName: string;
|
|
|
|
|
userAvatar: string;
|
|
|
|
|
phoneNumber: string;
|
|
|
|
|
userAccount: string;
|
|
|
|
|
userPassword: string;
|
|
|
|
|
invitationCode: string;
|
|
|
|
|
userRole:string;
|
|
|
|
|
parentUserId:number
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
const tableData = ref<User[]>([]);
|
2025-06-18 09:26:56 +08:00
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
const searchPhone = ref("");
|
2025-06-18 09:26:56 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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('未找到登录信息');
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
const reset = () => {
|
2025-07-02 09:43:17 +08:00
|
|
|
|
searchPhone.value = "";
|
2025-06-18 09:26:56 +08:00
|
|
|
|
searchParams.value = {
|
|
|
|
|
current: 1,
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
sortField: "id",
|
|
|
|
|
sortOrder: "ascend",
|
|
|
|
|
userRole: null,
|
|
|
|
|
phoneNumber: null
|
|
|
|
|
};
|
2025-07-02 09:43:17 +08:00
|
|
|
|
getUserList();
|
2025-06-18 09:26:56 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 删除用户
|
|
|
|
|
const selectedRowKeys = ref<number[]>([]);
|
|
|
|
|
|
|
|
|
|
// 配置行选择器
|
|
|
|
|
const rowSelection = {
|
|
|
|
|
onChange: (selectedKeys: number[]) => {
|
|
|
|
|
selectedRowKeys.value = selectedKeys;
|
|
|
|
|
},
|
|
|
|
|
selectedRowKeys: selectedRowKeys,
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
getCheckboxProps: (record: User) => ({
|
2025-07-02 09:43:17 +08:00
|
|
|
|
disabled: record.userRole !== 'admin',
|
2025-06-18 09:26:56 +08:00
|
|
|
|
name: record.id.toString(),
|
|
|
|
|
}),
|
|
|
|
|
};
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
const confirmDeleteUser = (id: number) => {
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
title: '确认删除用户',
|
|
|
|
|
content: '您确定要删除这个用户吗?此操作不可恢复!',
|
|
|
|
|
okText: '确认删除',
|
|
|
|
|
okType: 'danger',
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
onOk() {
|
2025-07-02 09:43:17 +08:00
|
|
|
|
return deleteUser(id);
|
2025-06-18 09:26:56 +08:00
|
|
|
|
},
|
|
|
|
|
onCancel() {
|
|
|
|
|
console.log('用户取消删除');
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
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('用户取消批量删除');
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
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('');
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
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;
|
|
|
|
|
};
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
const handleSave = async () => {
|
|
|
|
|
try {
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
if (editForm.value.userPassword) {
|
|
|
|
|
const password = editForm.value.userPassword;
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
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('未找到登录信息');
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
const processValue = (value: any) => {
|
2025-07-02 09:43:17 +08:00
|
|
|
|
if (value === "" || value === "无") return null;
|
2025-06-18 09:26:56 +08:00
|
|
|
|
return value;
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
const processNumber = (value: any) => {
|
|
|
|
|
const num = Number(value);
|
|
|
|
|
return isNaN(num) ? null : num;
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
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)
|
|
|
|
|
: []
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
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) {
|
2025-07-02 09:43:17 +08:00
|
|
|
|
selectedUser.value = { ...updatedUser };
|
2025-06-18 09:26:56 +08:00
|
|
|
|
}
|
|
|
|
|
isEditMode.value = false;
|
|
|
|
|
} else {
|
|
|
|
|
message.error(res.message || '更新失败');
|
|
|
|
|
}
|
|
|
|
|
} catch (error:any) {
|
|
|
|
|
console.error('更新失败:', error);
|
|
|
|
|
message.error(error.response?.data?.message || '更新失败,请检查网络');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
const openUser = ref<boolean>(false);
|
|
|
|
|
const formRef = ref();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const showModal = () => {
|
|
|
|
|
openUser.value = true;
|
|
|
|
|
formData.value = {
|
|
|
|
|
nickName: '',
|
|
|
|
|
userAvatar: '',
|
|
|
|
|
phoneNumber: '',
|
|
|
|
|
userAccount: '',
|
|
|
|
|
userPassword: ''
|
|
|
|
|
};
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
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 {
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
const errorMsg = extractErrorMessage(res.message);
|
|
|
|
|
message.error(`${errorMsg}`);
|
|
|
|
|
}
|
|
|
|
|
} catch (error: any) {
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
let errorMsg = error.response?.message || error.message;
|
|
|
|
|
errorMsg = extractErrorMessage(errorMsg);
|
|
|
|
|
message.error(`${errorMsg}`);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.action-btn {
|
|
|
|
|
padding: 0 8px;
|
|
|
|
|
}
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
: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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-02 09:43:17 +08:00
|
|
|
|
|
2025-06-18 09:26:56 +08:00
|
|
|
|
.custom-search :deep(.ant-input) {
|
|
|
|
|
border-right-color: #ffa940;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.ant-checkbox-wrapper-disabled) {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
</style>
|