Files
spa-view/src/views/admin/Users.vue

986 lines
40 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.
<script setup>
import { ref, onMounted, computed } from "vue"
import AOS from "aos"
import "aos/dist/aos.css"
import { message, Upload } from "ant-design-vue"
onMounted(() => {
AOS.init({
duration: 800,
offset: 120,
easing: "ease-out-cubic",
once: false,
})
})
// 角色数据
const roleOptions = [
{ id: 1, name: "管理员" },
{ id: 2, name: "编辑" },
{ id: 3, name: "普通用户" },
{ id: 4, name: "访客" },
]
// 权限数据
const allPermissions = [
{ id: 1, name: "用户管理" },
{ id: 2, name: "内容管理" },
{ id: 3, name: "订单管理" },
{ id: 4, name: "系统设置" },
{ id: 5, name: "数据统计" },
{ id: 6, name: "财务管理" },
{ id: 7, name: "客户管理" },
{ id: 8, name: "营销管理" },
]
// 用户状态选项
const statusOptions = [
{ value: "启用", label: "启用" },
{ value: "禁用", label: "禁用" },
]
// 模拟用户数据
const dataSource = ref([
{
id: 1,
username: "admin",
avatar: "https://randomuser.me/api/portraits/men/32.jpg",
email: "admin@example.com",
phone: "13800138000",
role: roleOptions[0],
status: "启用",
createdAt: "2024-01-15T08:30:00Z",
lastLogin: "2024-03-28T10:15:30Z",
permissions: [
allPermissions[0],
allPermissions[1],
allPermissions[2],
allPermissions[3],
allPermissions[4],
],
},
{
id: 2,
username: "editor",
avatar: "https://randomuser.me/api/portraits/women/44.jpg",
email: "editor@example.com",
phone: "13900139000",
role: roleOptions[1],
status: "启用",
createdAt: "2024-02-10T14:20:00Z",
lastLogin: "2024-03-27T16:45:12Z",
permissions: [allPermissions[1], allPermissions[2]],
},
{
id: 3,
username: "user1",
avatar: "https://randomuser.me/api/portraits/men/22.jpg",
email: "user1@example.com",
phone: "13700137000",
role: roleOptions[2],
status: "启用",
createdAt: "2024-02-20T09:15:00Z",
lastLogin: "2024-03-25T11:30:45Z",
permissions: [allPermissions[2]],
},
{
id: 4,
username: "user2",
avatar: "https://randomuser.me/api/portraits/women/24.jpg",
email: "user2@example.com",
phone: "13600136000",
role: roleOptions[2],
status: "禁用",
createdAt: "2024-03-05T16:40:00Z",
lastLogin: "2024-03-20T09:10:22Z",
permissions: [allPermissions[2]],
},
{
id: 5,
username: "guest",
avatar: "https://randomuser.me/api/portraits/men/67.jpg",
email: "guest@example.com",
phone: "13500135000",
role: roleOptions[3],
status: "启用",
createdAt: "2024-03-10T11:25:00Z",
lastLogin: "2024-03-15T14:20:18Z",
permissions: [],
},
])
// 分页
const currentPage = ref(1)
const pageSize = ref(10)
const totalItems = ref(dataSource.value.length)
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return dataSource.value.slice(start, end)
})
const changePage = (page) => {
currentPage.value = page
}
// 搜索和筛选
const searchQuery = ref("")
const filterRole = ref("all")
const filterStatus = ref("all")
const filteredData = computed(() => {
return dataSource.value.filter(
(user) =>
(searchQuery.value === "" ||
user.username.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
user.email.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
user.phone.includes(searchQuery.value)) &&
(filterRole.value === "all" || user.role.id === parseInt(filterRole.value)) &&
(filterStatus.value === "all" || user.status === filterStatus.value)
)
})
const filteredAndPaginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredData.value.slice(start, end)
})
// 模态框
const showModal = ref(false)
const currentItem = ref(null)
const formLoading = ref(false)
// 头像上传相关
const fileList = ref([])
const uploading = ref(false)
const previewVisible = ref(false)
const previewImage = ref("")
const handlePreview = (file) => {
previewImage.value = file.url || file.preview
previewVisible.value = true
}
const handleUploadChange = (info) => {
if (info.file.status === "uploading") {
uploading.value = true
return
}
if (info.file.status === "done") {
// 模拟上传成功后获取图片URL
currentItem.value.avatar = "https://randomuser.me/api/portraits/men/32.jpg"
uploading.value = false
message.success("头像上传成功")
}
if (info.file.status === "error") {
uploading.value = false
message.error("头像上传失败")
}
}
const beforeUpload = (file) => {
const isJpgOrPng = file.type === "image/jpeg" || file.type === "image/png"
if (!isJpgOrPng) {
message.error("只能上传JPG或PNG格式的图片!")
}
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
message.error("图片大小不能超过2MB!")
}
return isJpgOrPng && isLt2M
}
const customRequest = ({ onSuccess }) => {
// 模拟上传
setTimeout(() => {
onSuccess("ok", { status: "done" })
}, 800)
}
const openModal = (item = null) => {
currentItem.value = item
? JSON.parse(JSON.stringify(item)) // 深拷贝避免直接修改原对象
: {
username: "",
avatar: "",
email: "",
phone: "",
role: roleOptions[2], // 默认为普通用户
status: "启用",
permissions: [],
}
// 重置文件列表
fileList.value = []
if (currentItem.value.avatar) {
fileList.value = [
{
uid: "-1",
name: "avatar.jpg",
status: "done",
url: currentItem.value.avatar,
},
]
}
showModal.value = true
}
const closeModal = () => {
showModal.value = false
}
// 确认删除模态框
const showDeleteConfirm = ref(false)
const itemToDelete = ref(null)
const openDeleteConfirm = (id) => {
itemToDelete.value = id
showDeleteConfirm.value = true
}
const closeDeleteConfirm = () => {
showDeleteConfirm.value = false
itemToDelete.value = null
}
const handleDelete = () => {
if (itemToDelete.value) {
dataSource.value = dataSource.value.filter((item) => item.id !== itemToDelete.value)
totalItems.value = dataSource.value.length
closeDeleteConfirm()
message.success("用户删除成功")
}
}
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleDateString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" })
}
const handleSubmit = () => {
formLoading.value = true
// 模拟API请求
setTimeout(() => {
if (currentItem.value.id) {
// 更新现有用户
const index = dataSource.value.findIndex((item) => item.id === currentItem.value.id)
if (index !== -1) {
dataSource.value[index] = { ...currentItem.value }
}
message.success("用户更新成功")
} else {
// 添加新用户
const newId = Math.max(...dataSource.value.map((item) => item.id), 0) + 1
dataSource.value.push({
...currentItem.value,
id: newId,
createdAt: new Date().toISOString(),
lastLogin: null,
})
totalItems.value = dataSource.value.length
message.success("用户创建成功")
}
formLoading.value = false
closeModal()
}, 800)
}
// 权限管理
const permissionInputValue = ref("")
const handleAddPermission = () => {
if (permissionInputValue.value) {
// 检查是否已存在该权限
const existingPermission = allPermissions.find(
(permission) => permission.name.toLowerCase() === permissionInputValue.value.toLowerCase()
)
// 如果存在且未添加到当前用户,则添加
if (
existingPermission &&
!currentItem.value.permissions.some((permission) => permission.id === existingPermission.id)
) {
currentItem.value.permissions.push(existingPermission)
}
// 如果不存在,创建新权限并添加
else if (!existingPermission) {
const newPermissionId = Math.max(...allPermissions.map((permission) => permission.id), 0) + 1
const newPermission = { id: newPermissionId, name: permissionInputValue.value }
allPermissions.push(newPermission)
currentItem.value.permissions.push(newPermission)
}
permissionInputValue.value = ""
}
}
const removePermission = (index) => {
currentItem.value.permissions.splice(index, 1)
}
// 重置密码模态框
const showResetPasswordModal = ref(false)
const resetPasswordUserId = ref(null)
const resetPasswordUsername = ref("")
const newPassword = ref("")
const confirmPassword = ref("")
const resetPasswordLoading = ref(false)
const passwordError = ref("")
const openResetPasswordModal = (user) => {
resetPasswordUserId.value = user.id
resetPasswordUsername.value = user.username
newPassword.value = ""
confirmPassword.value = ""
passwordError.value = ""
showResetPasswordModal.value = true
}
const closeResetPasswordModal = () => {
showResetPasswordModal.value = false
}
const handleResetPassword = () => {
// 验证密码
if (!newPassword.value) {
passwordError.value = "请输入新密码"
return
}
if (newPassword.value.length < 6) {
passwordError.value = "密码长度不能少于6位"
return
}
if (newPassword.value !== confirmPassword.value) {
passwordError.value = "两次输入的密码不一致"
return
}
resetPasswordLoading.value = true
// 模拟API请求
setTimeout(() => {
message.success(`用户 ${resetPasswordUsername.value} 的密码已重置`)
resetPasswordLoading.value = false
closeResetPasswordModal()
}, 800)
}
</script>
<template>
<div class="bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-6" data-aos="fade-up">
<!-- 页面标题和新建按钮 -->
<div class="mb-6 flex justify-between items-center">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">用户管理</h2>
<button
class="px-5 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center cursor-pointer"
@click="openModal()"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
新建用户
</button>
</div>
<!-- 搜索和筛选 -->
<div class="mb-6 grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="relative">
<input
v-model="searchQuery"
type="text"
class="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="搜索用户名、邮箱或电话"
/>
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
<div>
<select
v-model="filterRole"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white cursor-pointer"
>
<option value="all">所有角色</option>
<option v-for="role in roleOptions" :key="role.id" :value="role.id">{{ role.name }}</option>
</select>
</div>
<div>
<select
v-model="filterStatus"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white cursor-pointer"
>
<option value="all">所有状态</option>
<option v-for="status in statusOptions" :key="status.value" :value="status.value">{{ status.label }}</option>
</select>
</div>
</div>
<!-- 表格 -->
<div class="rounded-2xl border border-gray-200 dark:border-gray-700">
<div class="overflow-x-auto" style="height: 60vh">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<!-- 表头 -->
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
用户信息
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
联系方式
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
角色
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
状态
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
最后登录
</th>
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
操作
</th>
</tr>
</thead>
<!-- 表格内容 -->
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
<tr v-for="user in filteredAndPaginatedData" :key="user.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center space-x-3">
<div class="flex-shrink-0 h-10 w-10 rounded-full overflow-hidden">
<img :src="user.avatar" :alt="user.username" class="h-full w-full object-cover" />
</div>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ user.username }}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">ID: {{ user.id }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900 dark:text-white">{{ user.email }}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">{{ user.phone }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
{{ user.role.name }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
:class="`px-2 py-1 text-xs font-medium rounded-full ${
user.status === '启用'
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'
: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300'
}`"
>
{{ user.status }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
{{ user.lastLogin ? formatDate(user.lastLogin) : '从未登录' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex justify-end space-x-2">
<button
class="p-1.5 text-amber-600 hover:text-amber-800 dark:text-amber-400 dark:hover:text-amber-300 hover:bg-amber-50 dark:hover:bg-amber-900/20 rounded-lg transition-colors cursor-pointer"
title="编辑"
@click="openModal(user)"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button
class="p-1.5 text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg transition-colors cursor-pointer"
title="重置密码"
@click="openResetPasswordModal(user)"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
</button>
<button
class="p-1.5 text-rose-600 hover:text-rose-800 dark:text-rose-400 dark:hover:text-rose-300 hover:bg-rose-50 dark:hover:bg-rose-900/20 rounded-lg transition-colors cursor-pointer"
title="删除"
@click="openDeleteConfirm(user.id)"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</td>
</tr>
<tr v-if="filteredAndPaginatedData.length === 0">
<td colspan="6" class="px-6 py-10 text-center text-gray-500 dark:text-gray-400">
没有找到匹配的用户
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="bg-white dark:bg-gray-800 px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between">
<div class="text-sm text-gray-700 dark:text-gray-300">
{{ filteredData.length }} 条记录
</div>
<div class="flex space-x-1">
<button
class="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer"
:disabled="currentPage === 1"
:class="{ 'opacity-50 cursor-not-allowed': currentPage === 1 }"
@click="currentPage > 1 && changePage(currentPage - 1)"
>
上一页
</button>
<button
v-for="page in Math.ceil(filteredData.length / pageSize)"
:key="page"
class="px-3 py-1 border rounded-md text-sm font-medium cursor-pointer"
:class="page === currentPage
? 'bg-rose-600 text-white border-rose-600 dark:bg-rose-600 dark:border-rose-600'
: 'border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'"
@click="changePage(page)"
>
{{ page }}
</button>
<button
class="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer"
:disabled="currentPage === Math.ceil(filteredData.length / pageSize)"
:class="{ 'opacity-50 cursor-not-allowed': currentPage === Math.ceil(filteredData.length / pageSize) }"
@click="currentPage < Math.ceil(filteredData.length / pageSize) && changePage(currentPage + 1)"
>
下一页
</button>
</div>
</div>
</div>
</div>
<!-- 用户表单模态框 -->
<div v-if="showModal" class="fixed inset-0 z-50 overflow-y-scroll" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
aria-hidden="true"
@click="closeModal"
></div>
<!-- 使模态框居中 -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<!-- 模态框内容 -->
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full relative z-50">
<div class="bg-white dark:bg-gray-800 px-6 pt-5 pb-4 sm:p-6">
<div class="flex justify-between items-center mb-5">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white" id="modal-title">
{{ currentItem?.id ? '编辑用户' : '新建用户' }}
</h3>
<button
class="text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 cursor-pointer"
@click="closeModal"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form @submit.prevent="handleSubmit" class="space-y-6">
<!-- 基本信息 -->
<div class="grid grid-cols-1 gap-6">
<!-- 用户名 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">用户名</label>
<input
v-model="currentItem.username"
type="text"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入用户名"
required
/>
</div>
<!-- 邮箱 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">邮箱</label>
<input
v-model="currentItem.email"
type="email"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入邮箱"
required
/>
</div>
<!-- 电话 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">电话</label>
<input
v-model="currentItem.phone"
type="text"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入电话"
/>
</div>
<!-- 角色 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">角色</label>
<select
v-model="currentItem.role"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white cursor-pointer"
>
<option v-for="role in roleOptions" :key="role.id" :value="role">
{{ role.name }}
</option>
</select>
</div>
<!-- 状态 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">状态</label>
<select
v-model="currentItem.status"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white cursor-pointer"
>
<option v-for="option in statusOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
<!-- 头像上传 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">头像</label>
<div class="border border-gray-300 dark:border-gray-600 rounded-xl p-4 dark:bg-gray-700">
<Upload
v-model:file-list="fileList"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="customRequest"
@preview="handlePreview"
@change="handleUploadChange"
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: true }"
:maxCount="1"
>
<div v-if="fileList.length < 1">
<div class="ant-upload-text flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span>点击上传头像</span>
</div>
</div>
</Upload>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
支持JPGPNG格式大小不超过2MB
</div>
</div>
</div>
<!-- 权限 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">权限</label>
<div class="flex flex-wrap gap-2 mb-2">
<span
v-for="(permission, index) in currentItem.permissions"
:key="index"
class="px-3 py-1.5 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-300 text-sm font-medium rounded-lg flex items-center"
>
{{ permission.name }}
<button
type="button"
class="ml-1.5 text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-200 cursor-pointer"
@click="removePermission(index)"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</span>
</div>
<div class="flex">
<select
v-model="permissionInputValue"
class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-l-xl focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value="">选择权限</option>
<option v-for="permission in allPermissions" :key="permission.id" :value="permission.name">
{{ permission.name }}
</option>
</select>
<button
type="button"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-r-xl transition-colors cursor-pointer"
@click="handleAddPermission"
>
添加
</button>
</div>
</div>
</div>
<!-- 表单按钮 -->
<div class="flex justify-end space-x-3 pt-4">
<button
type="button"
class="px-5 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
@click="closeModal"
>
取消
</button>
<button
type="submit"
class="px-5 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center cursor-pointer"
:disabled="formLoading"
>
<span v-if="formLoading" class="mr-2">
<svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
{{ currentItem?.id ? '保存修改' : '创建用户' }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- 删除确认模态框 -->
<div v-if="showDeleteConfirm" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
aria-hidden="true"
@click="closeDeleteConfirm"
></div>
<!-- 使模态框居中 -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<!-- 模态框内容 -->
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full relative z-50">
<div class="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900/30 sm:mx-0 sm:h-10 sm:w-10">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white" id="modal-title">
确认删除
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
确定要删除这个用户吗此操作无法撤销
</p>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm cursor-pointer"
@click="handleDelete"
>
确认
</button>
<button
type="button"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-800 text-base font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm cursor-pointer"
@click="closeDeleteConfirm"
>
取消
</button>
</div>
</div>
</div>
</div>
<!-- 重置密码模态框 -->
<div v-if="showResetPasswordModal" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
aria-hidden="true"
@click="closeResetPasswordModal"
></div>
<!-- 使模态框居中 -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<!-- 模态框内容 -->
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full relative z-50">
<div class="bg-white dark:bg-gray-800 px-6 pt-5 pb-4 sm:p-6">
<div class="flex justify-between items-center mb-5">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white" id="modal-title">
重置密码 - {{ resetPasswordUsername }}
</h3>
<button
class="text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 cursor-pointer"
@click="closeResetPasswordModal"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form @submit.prevent="handleResetPassword" class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">新密码</label>
<input
v-model="newPassword"
type="password"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入新密码"
required
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">确认密码</label>
<input
v-model="confirmPassword"
type="password"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请再次输入新密码"
required
/>
</div>
<div v-if="passwordError" class="text-sm text-red-600 dark:text-red-400">
{{ passwordError }}
</div>
<div class="flex justify-end space-x-3 pt-4">
<button
type="button"
class="px-5 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
@click="closeResetPasswordModal"
>
取消
</button>
<button
type="submit"
class="px-5 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center cursor-pointer"
:disabled="resetPasswordLoading"
>
<span v-if="resetPasswordLoading" class="mr-2">
<svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
确认重置
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- 图片预览模态框 -->
<div v-if="previewVisible" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
aria-hidden="true"
@click="previewVisible = false"
></div>
<!-- 使模态框居中 -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<!-- 模态框内容 -->
<div class="inline-block align-bottom bg-transparent rounded-lg text-left overflow-hidden transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl sm:w-full relative z-50">
<div class="relative">
<button
class="absolute top-2 right-2 p-2 rounded-full bg-gray-800/50 text-white hover:bg-gray-700/50 transition-colors cursor-pointer"
@click="previewVisible = false"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<img :src="previewImage" class="max-h-[80vh] max-w-full rounded-lg shadow-xl" />
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
/* 自定义样式 */
/* 毛玻璃效果增强 */
.backdrop-blur-xl {
backdrop-filter: blur(16px);
}
.backdrop-blur-md {
backdrop-filter: blur(12px);
}
.backdrop-blur-sm {
backdrop-filter: blur(4px);
}
/* 上传组件暗黑模式适配 */
:deep(.ant-upload-list-picture-card .ant-upload-list-item) {
border-color: #4b5563;
}
:deep(.ant-upload.ant-upload-select-picture-card) {
background-color: transparent;
border-color: #4b5563;
}
/* 滚动条美化 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
border-radius: 20px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(156, 163, 175, 0.7);
}
/* 暗黑模式滚动条 */
.dark ::-webkit-scrollbar-thumb {
background-color: rgba(75, 85, 99, 0.5);
}
.dark ::-webkit-scrollbar-thumb:hover {
background-color: rgba(75, 85, 99, 0.7);
}
</style>