925 lines
33 KiB
Vue
925 lines
33 KiB
Vue
<template>
|
||
<div class="space-y-6">
|
||
<!-- 页面头部 -->
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<h1 class="text-2xl font-bold text-gray-900">用户管理</h1>
|
||
<p class="mt-1 text-sm text-gray-600">管理系统中的所有用户账户</p>
|
||
</div>
|
||
<div class="flex space-x-3">
|
||
<button
|
||
@click="exportUsers"
|
||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||
>
|
||
导出数据
|
||
</button>
|
||
<button
|
||
@click="createUser"
|
||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
|
||
>
|
||
添加用户
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 统计卡片 -->
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||
<div class="p-5">
|
||
<div class="flex items-center">
|
||
<div class="flex-shrink-0">
|
||
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
|
||
<span class="text-white text-sm font-semibold">总</span>
|
||
</div>
|
||
</div>
|
||
<div class="ml-5 w-0 flex-1">
|
||
<dl>
|
||
<dt class="text-sm font-medium text-gray-500 truncate">总用户数</dt>
|
||
<dd class="text-lg font-medium text-gray-900">{{ userStats.total }}</dd>
|
||
</dl>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||
<div class="p-5">
|
||
<div class="flex items-center">
|
||
<div class="flex-shrink-0">
|
||
<div class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
|
||
<span class="text-white text-sm font-semibold">译</span>
|
||
</div>
|
||
</div>
|
||
<div class="ml-5 w-0 flex-1">
|
||
<dl>
|
||
<dt class="text-sm font-medium text-gray-500 truncate">译员数量</dt>
|
||
<dd class="text-lg font-medium text-gray-900">{{ userStats.interpreters }}</dd>
|
||
</dl>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||
<div class="p-5">
|
||
<div class="flex items-center">
|
||
<div class="flex-shrink-0">
|
||
<div class="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center">
|
||
<span class="text-white text-sm font-semibold">管</span>
|
||
</div>
|
||
</div>
|
||
<div class="ml-5 w-0 flex-1">
|
||
<dl>
|
||
<dt class="text-sm font-medium text-gray-500 truncate">管理员数量</dt>
|
||
<dd class="text-lg font-medium text-gray-900">{{ userStats.admins }}</dd>
|
||
</dl>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 搜索和过滤 -->
|
||
<div class="bg-white shadow rounded-lg p-6">
|
||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">搜索用户</label>
|
||
<input
|
||
v-model="searchQuery"
|
||
type="text"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="搜索姓名、邮箱或手机号"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">角色筛选</label>
|
||
<select
|
||
v-model="selectedRole"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
>
|
||
<option value="">全部角色</option>
|
||
<option value="admin">管理员</option>
|
||
<option value="customer">客户</option>
|
||
<option value="interpreter">翻译员</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">状态筛选</label>
|
||
<select
|
||
v-model="selectedStatus"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
>
|
||
<option value="">全部状态</option>
|
||
<option value="active">活跃</option>
|
||
<option value="inactive">非活跃</option>
|
||
<option value="suspended">已暂停</option>
|
||
</select>
|
||
</div>
|
||
<div class="flex items-end">
|
||
<button
|
||
@click="resetFilters"
|
||
class="w-full px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 border border-gray-300 rounded-md hover:bg-gray-200"
|
||
>
|
||
重置筛选
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 用户列表 -->
|
||
<div class="bg-white shadow overflow-hidden sm:rounded-md">
|
||
<div class="overflow-x-auto">
|
||
<table class="min-w-full divide-y divide-gray-200">
|
||
<thead class="bg-gray-50">
|
||
<tr>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
用户
|
||
</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
角色
|
||
</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
状态
|
||
</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
余额
|
||
</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
注册时间
|
||
</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
操作
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="bg-white divide-y divide-gray-200">
|
||
<tr v-for="user in paginatedUsers" :key="user.id">
|
||
<td class="px-6 py-4 whitespace-nowrap">
|
||
<div class="flex items-center">
|
||
<div class="flex-shrink-0 h-10 w-10">
|
||
<div class="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center">
|
||
<span class="text-sm font-medium text-gray-700">
|
||
{{ user.full_name?.charAt(0) || '用' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="ml-4">
|
||
<div class="text-sm font-medium text-gray-900">{{ user.full_name || '未设置' }}</div>
|
||
<div class="text-sm text-gray-500">{{ user.email }}</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap">
|
||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
|
||
:class="{
|
||
'bg-purple-100 text-purple-800': user.role === 'admin',
|
||
'bg-green-100 text-green-800': user.role === 'customer',
|
||
'bg-blue-100 text-blue-800': user.role === 'interpreter'
|
||
}">
|
||
{{ getRoleText(user.role) }}
|
||
</span>
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap">
|
||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
|
||
:class="getStatusClass(user.status)">
|
||
{{ getStatusText(user.status) }}
|
||
</span>
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||
¥{{ user.credits?.toFixed(2) || '0.00' }}
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||
{{ formatDate(user.created_at) }}
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||
<button
|
||
@click="editUser(user)"
|
||
class="text-blue-600 hover:text-blue-900 mr-3"
|
||
>
|
||
编辑
|
||
</button>
|
||
<button
|
||
@click="deleteUser(user)"
|
||
class="text-red-600 hover:text-red-900"
|
||
>
|
||
删除
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- 分页组件 -->
|
||
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6" v-if="totalPages > 1">
|
||
<div class="flex-1 flex justify-between sm:hidden">
|
||
<button
|
||
@click="goToPage(currentPage - 1)"
|
||
:disabled="currentPage <= 1"
|
||
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
上一页
|
||
</button>
|
||
<button
|
||
@click="goToPage(currentPage + 1)"
|
||
:disabled="currentPage >= totalPages"
|
||
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
下一页
|
||
</button>
|
||
</div>
|
||
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||
<div>
|
||
<p class="text-sm text-gray-700">
|
||
显示第 <span class="font-medium">{{ (currentPage - 1) * 20 + 1 }}</span> 到
|
||
<span class="font-medium">{{ Math.min(currentPage * 20, filteredUsers.length) }}</span> 条,
|
||
共 <span class="font-medium">{{ filteredUsers.length }}</span> 条记录
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
|
||
<button
|
||
@click="goToPage(currentPage - 1)"
|
||
:disabled="currentPage <= 1"
|
||
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
<span class="sr-only">上一页</span>
|
||
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||
</svg>
|
||
</button>
|
||
|
||
<template v-for="page in Math.min(totalPages, 7)" :key="page">
|
||
<button
|
||
@click="goToPage(page)"
|
||
:class="[
|
||
page === currentPage
|
||
? 'z-10 bg-blue-50 border-blue-500 text-blue-600'
|
||
: 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50',
|
||
'relative inline-flex items-center px-4 py-2 border text-sm font-medium'
|
||
]"
|
||
>
|
||
{{ page }}
|
||
</button>
|
||
</template>
|
||
|
||
<button
|
||
@click="goToPage(currentPage + 1)"
|
||
:disabled="currentPage >= totalPages"
|
||
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
<span class="sr-only">下一页</span>
|
||
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||
</svg>
|
||
</button>
|
||
</nav>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 添加用户模态框 -->
|
||
<div v-if="showCreateModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white">
|
||
<div class="mt-3">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h3 class="text-lg font-medium text-gray-900">添加新用户</h3>
|
||
<button @click="showCreateModal = false" class="text-gray-400 hover:text-gray-600">
|
||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<form @submit.prevent="submitCreateUser" class="space-y-4">
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<!-- 基本信息 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">邮箱地址 *</label>
|
||
<input
|
||
v-model="newUser.email"
|
||
type="email"
|
||
required
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="请输入邮箱地址"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">密码 *</label>
|
||
<input
|
||
v-model="newUser.password"
|
||
type="password"
|
||
required
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="请输入密码"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">姓名 *</label>
|
||
<input
|
||
v-model="newUser.full_name"
|
||
type="text"
|
||
required
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="请输入姓名"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">手机号</label>
|
||
<input
|
||
v-model="newUser.phone"
|
||
type="tel"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="请输入手机号"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">角色 *</label>
|
||
<select
|
||
v-model="newUser.role"
|
||
required
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
>
|
||
<option value="">请选择角色</option>
|
||
<option value="admin">管理员</option>
|
||
<option value="customer">客户</option>
|
||
<option value="interpreter">翻译员</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">初始余额</label>
|
||
<input
|
||
v-model.number="newUser.credits"
|
||
type="number"
|
||
min="0"
|
||
step="0.01"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="0.00"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 翻译员专用字段 -->
|
||
<div v-if="newUser.role === 'interpreter'" class="space-y-4 border-t pt-4">
|
||
<h4 class="text-md font-medium text-gray-900">翻译员专业信息</h4>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">公司</label>
|
||
<input
|
||
v-model="newUser.company"
|
||
type="text"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="所属公司"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">部门</label>
|
||
<input
|
||
v-model="newUser.department"
|
||
type="text"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="所属部门"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">时薪(元/小时)</label>
|
||
<input
|
||
v-model.number="newUser.hourly_rate"
|
||
type="number"
|
||
min="0"
|
||
step="0.01"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="100.00"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">时区</label>
|
||
<select
|
||
v-model="newUser.timezone"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
>
|
||
<option value="UTC+8">北京时间 (UTC+8)</option>
|
||
<option value="UTC">协调世界时 (UTC)</option>
|
||
<option value="UTC-5">美国东部时间 (UTC-5)</option>
|
||
<option value="UTC-8">美国西部时间 (UTC-8)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">专业领域</label>
|
||
<div class="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||
<label v-for="spec in specializationOptions" :key="spec" class="flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
:value="spec"
|
||
v-model="newUser.specializations"
|
||
class="mr-2 text-blue-600"
|
||
/>
|
||
<span class="text-sm text-gray-700">{{ spec }}</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 按钮 -->
|
||
<div class="flex justify-end space-x-3 pt-4">
|
||
<button
|
||
type="button"
|
||
@click="showCreateModal = false"
|
||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
|
||
>
|
||
创建用户
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 编辑用户模态框 -->
|
||
<div v-if="showEditModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white">
|
||
<div class="mt-3">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h3 class="text-lg font-medium text-gray-900">编辑用户</h3>
|
||
<button @click="showEditModal = false" class="text-gray-400 hover:text-gray-600">
|
||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<form @submit.prevent="submitUpdateUser" class="space-y-4" v-if="editingUser">
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">邮箱地址</label>
|
||
<input
|
||
v-model="editingUser.email"
|
||
type="email"
|
||
disabled
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-50 text-gray-500"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">姓名 *</label>
|
||
<input
|
||
v-model="editingUser.full_name"
|
||
type="text"
|
||
required
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">手机号</label>
|
||
<input
|
||
v-model="editingUser.phone"
|
||
type="tel"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">角色 *</label>
|
||
<select
|
||
v-model="editingUser.role"
|
||
required
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
>
|
||
<option value="admin">管理员</option>
|
||
<option value="customer">客户</option>
|
||
<option value="interpreter">翻译员</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">余额</label>
|
||
<input
|
||
v-model.number="editingUser.credits"
|
||
type="number"
|
||
min="0"
|
||
step="0.01"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">状态</label>
|
||
<select
|
||
v-model="editingUser.status"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
>
|
||
<option value="active">活跃</option>
|
||
<option value="inactive">非活跃</option>
|
||
<option value="suspended">已暂停</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex justify-end space-x-3 pt-4">
|
||
<button
|
||
type="button"
|
||
@click="showEditModal = false"
|
||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
|
||
>
|
||
更新用户
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
// 页面元数据 - 使用管理员认证和默认布局
|
||
definePageMeta({
|
||
middleware: 'admin-auth',
|
||
layout: 'default' // 明确指定使用默认布局
|
||
})
|
||
|
||
// 页面标题
|
||
useHead({
|
||
title: '用户管理 - 翻译管理系统'
|
||
})
|
||
|
||
// 路由
|
||
const router = useRouter()
|
||
|
||
// 导入Supabase数据操作
|
||
const { getProfiles } = useSupabaseData()
|
||
|
||
// 响应式数据
|
||
const users = ref([])
|
||
const loading = ref(false)
|
||
const searchQuery = ref('')
|
||
const selectedRole = ref('')
|
||
const selectedStatus = ref('')
|
||
const currentPage = ref(1)
|
||
const totalPages = ref(1)
|
||
const showCreateModal = ref(false)
|
||
const showEditModal = ref(false)
|
||
const editingUser = ref(null)
|
||
const allUsers = ref([]) // 存储所有用户数据用于筛选
|
||
|
||
// 用户统计数据
|
||
const userStats = computed(() => {
|
||
const total = allUsers.value.length
|
||
const interpreters = allUsers.value.filter(user => user.role === 'interpreter').length
|
||
const admins = allUsers.value.filter(user => user.role === 'admin').length
|
||
const customers = allUsers.value.filter(user => user.role === 'customer').length
|
||
|
||
return {
|
||
total,
|
||
interpreters,
|
||
admins,
|
||
customers
|
||
}
|
||
})
|
||
|
||
// 计算属性:过滤后的用户列表
|
||
const filteredUsers = computed(() => {
|
||
let filtered = [...allUsers.value]
|
||
|
||
// 搜索筛选
|
||
if (searchQuery.value) {
|
||
const query = searchQuery.value.toLowerCase()
|
||
filtered = filtered.filter(user =>
|
||
user.full_name?.toLowerCase().includes(query) ||
|
||
user.email?.toLowerCase().includes(query) ||
|
||
user.phone?.includes(query)
|
||
)
|
||
}
|
||
|
||
// 角色筛选
|
||
if (selectedRole.value) {
|
||
filtered = filtered.filter(user => user.role === selectedRole.value)
|
||
}
|
||
|
||
// 状态筛选
|
||
if (selectedStatus.value) {
|
||
filtered = filtered.filter(user => user.status === selectedStatus.value)
|
||
}
|
||
|
||
return filtered
|
||
})
|
||
|
||
// 计算属性:分页后的用户列表
|
||
const paginatedUsers = computed(() => {
|
||
const pageSize = 20
|
||
totalPages.value = Math.ceil(filteredUsers.value.length / pageSize)
|
||
const startIndex = (currentPage.value - 1) * pageSize
|
||
const endIndex = startIndex + pageSize
|
||
return filteredUsers.value.slice(startIndex, endIndex)
|
||
})
|
||
|
||
// 新用户表单数据
|
||
const newUser = ref({
|
||
email: '',
|
||
password: '',
|
||
full_name: '',
|
||
role: '',
|
||
phone: '',
|
||
company: '',
|
||
department: '',
|
||
specializations: [],
|
||
hourly_rate: null,
|
||
timezone: 'UTC'
|
||
})
|
||
|
||
// 角色选项
|
||
const roleOptions = [
|
||
{ value: '', label: '所有角色' },
|
||
{ value: 'admin', label: '管理员' },
|
||
{ value: 'customer', label: '客户' },
|
||
{ value: 'interpreter', label: '翻译员' }
|
||
]
|
||
|
||
// 状态选项
|
||
const statusOptions = [
|
||
{ value: '', label: '所有状态' },
|
||
{ value: 'active', label: '活跃' },
|
||
{ value: 'inactive', label: '非活跃' },
|
||
{ value: 'suspended', label: '已暂停' }
|
||
]
|
||
|
||
// 专业领域选项
|
||
const specializationOptions = [
|
||
'医疗翻译', '法律翻译', '技术翻译', '商务翻译',
|
||
'学术翻译', '金融翻译', '手语翻译', '会议翻译'
|
||
]
|
||
|
||
// 获取用户列表
|
||
const fetchUsers = async () => {
|
||
loading.value = true
|
||
try {
|
||
console.log('开始获取用户数据...')
|
||
|
||
// 临时使用模拟数据,避免Supabase连接问题
|
||
const mockUsers = [
|
||
{
|
||
id: '1',
|
||
email: 'admin@example.com',
|
||
full_name: '系统管理员',
|
||
phone: '13800138000',
|
||
role: 'admin',
|
||
credits: 1000,
|
||
status: 'active',
|
||
created_at: new Date().toISOString(),
|
||
is_enterprise: false
|
||
},
|
||
{
|
||
id: '2',
|
||
email: 'translator1@example.com',
|
||
full_name: '李译员',
|
||
phone: '13800138001',
|
||
role: 'interpreter',
|
||
credits: 500,
|
||
status: 'active',
|
||
created_at: new Date(Date.now() - 86400000).toISOString(),
|
||
is_enterprise: false
|
||
},
|
||
{
|
||
id: '3',
|
||
email: 'customer1@example.com',
|
||
full_name: '张客户',
|
||
phone: '13800138002',
|
||
role: 'customer',
|
||
credits: 200,
|
||
status: 'active',
|
||
created_at: new Date(Date.now() - 172800000).toISOString(),
|
||
is_enterprise: false
|
||
},
|
||
{
|
||
id: '4',
|
||
email: 'translator2@example.com',
|
||
full_name: '王译员',
|
||
phone: '13800138003',
|
||
role: 'interpreter',
|
||
credits: 750,
|
||
status: 'inactive',
|
||
created_at: new Date(Date.now() - 259200000).toISOString(),
|
||
is_enterprise: false
|
||
},
|
||
{
|
||
id: '5',
|
||
email: 'customer2@example.com',
|
||
full_name: '陈客户',
|
||
phone: '13800138004',
|
||
role: 'customer',
|
||
credits: 150,
|
||
status: 'suspended',
|
||
created_at: new Date(Date.now() - 345600000).toISOString(),
|
||
is_enterprise: true
|
||
}
|
||
]
|
||
|
||
allUsers.value = mockUsers
|
||
console.log('用户数据加载成功:', allUsers.value.length, '个用户')
|
||
|
||
// 正式版本应该使用:
|
||
// const profilesData = await getProfiles()
|
||
// allUsers.value = profilesData || []
|
||
|
||
} catch (error) {
|
||
console.error('获取用户列表失败:', error)
|
||
allUsers.value = []
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 重置筛选条件
|
||
const resetFilters = () => {
|
||
searchQuery.value = ''
|
||
selectedRole.value = ''
|
||
selectedStatus.value = ''
|
||
currentPage.value = 1
|
||
}
|
||
|
||
// 筛选用户数据(保留原函数但不再需要)
|
||
const filterUsers = () => {
|
||
// 这个函数现在由计算属性 filteredUsers 和 paginatedUsers 处理
|
||
// 保留空函数以防其他地方调用
|
||
}
|
||
|
||
// 提交创建用户表单
|
||
const submitCreateUser = async () => {
|
||
try {
|
||
console.log('创建新用户:', newUser.value)
|
||
|
||
// 这里应该调用Supabase的用户创建API
|
||
// 暂时使用模拟数据添加到列表中
|
||
const newUserData = {
|
||
id: Date.now().toString(),
|
||
...newUser.value,
|
||
credits: newUser.value.credits || 0,
|
||
status: 'active',
|
||
created_at: new Date().toISOString(),
|
||
is_enterprise: false
|
||
}
|
||
|
||
allUsers.value.push(newUserData)
|
||
|
||
showCreateModal.value = false
|
||
resetNewUserForm()
|
||
|
||
alert('用户创建成功!')
|
||
} catch (error) {
|
||
console.error('创建用户失败:', error)
|
||
alert('创建用户失败,请重试')
|
||
}
|
||
}
|
||
|
||
// 创建用户按钮处理
|
||
const createUser = () => {
|
||
showCreateModal.value = true
|
||
}
|
||
|
||
// 编辑用户
|
||
const editUser = (user) => {
|
||
editingUser.value = { ...user }
|
||
showEditModal.value = true
|
||
}
|
||
|
||
// 提交更新用户表单
|
||
const submitUpdateUser = async () => {
|
||
try {
|
||
console.log('更新用户:', editingUser.value)
|
||
|
||
// 这里应该调用Supabase的用户更新API
|
||
// 暂时更新本地数据
|
||
const index = allUsers.value.findIndex(u => u.id === editingUser.value.id)
|
||
if (index !== -1) {
|
||
allUsers.value[index] = { ...editingUser.value }
|
||
}
|
||
|
||
showEditModal.value = false
|
||
editingUser.value = null
|
||
|
||
alert('用户更新成功!')
|
||
} catch (error) {
|
||
console.error('更新用户失败:', error)
|
||
alert('更新用户失败,请重试')
|
||
}
|
||
}
|
||
|
||
// 删除用户
|
||
const deleteUser = async (userId) => {
|
||
if (confirm('确定要删除此用户吗?此操作将禁用用户账户。')) {
|
||
try {
|
||
// 注意:这里需要使用Supabase的用户删除API
|
||
// 暂时保留原API调用,后续可以改为直接使用Supabase
|
||
await $fetch('/api/admin/users', {
|
||
method: 'DELETE',
|
||
body: { userId }
|
||
})
|
||
await fetchUsers()
|
||
// 显示成功提示
|
||
} catch (error) {
|
||
console.error('删除用户失败:', error)
|
||
// 显示错误提示
|
||
}
|
||
}
|
||
}
|
||
|
||
// 重置新用户表单
|
||
const resetNewUserForm = () => {
|
||
newUser.value = {
|
||
email: '',
|
||
password: '',
|
||
full_name: '',
|
||
role: '',
|
||
phone: '',
|
||
company: '',
|
||
department: '',
|
||
specializations: [],
|
||
hourly_rate: null,
|
||
timezone: 'UTC',
|
||
credits: 0
|
||
}
|
||
}
|
||
|
||
// 格式化日期
|
||
const formatDate = (dateString) => {
|
||
if (!dateString) return '从未登录'
|
||
return new Date(dateString).toLocaleString('zh-CN')
|
||
}
|
||
|
||
// 获取状态显示文本
|
||
const getStatusText = (status) => {
|
||
const statusMap = {
|
||
'suspended': '已暂停',
|
||
'inactive': '非活跃',
|
||
'active': '活跃'
|
||
}
|
||
return statusMap[status] || '未知状态'
|
||
}
|
||
|
||
// 获取状态样式类
|
||
const getStatusClass = (status) => {
|
||
const classMap = {
|
||
'suspended': 'bg-red-100 text-red-800',
|
||
'active': 'bg-green-100 text-green-800',
|
||
'inactive': 'bg-yellow-100 text-yellow-800'
|
||
}
|
||
return classMap[status] || 'bg-gray-100 text-gray-800'
|
||
}
|
||
|
||
// 获取角色显示文本
|
||
const getRoleText = (role) => {
|
||
const roleMap = {
|
||
admin: '管理员',
|
||
customer: '客户',
|
||
interpreter: '翻译员'
|
||
}
|
||
return roleMap[role] || role
|
||
}
|
||
|
||
// 监听搜索和筛选变化
|
||
watch([searchQuery, selectedRole, selectedStatus], () => {
|
||
currentPage.value = 1 // 重置到第一页
|
||
})
|
||
|
||
// 分页处理
|
||
const goToPage = (page) => {
|
||
currentPage.value = page
|
||
}
|
||
|
||
// 页面挂载时获取数据
|
||
onMounted(() => {
|
||
fetchUsers()
|
||
})
|
||
|
||
// 导出用户数据
|
||
const exportUsers = () => {
|
||
alert('导出功能开发中...')
|
||
}
|
||
</script>
|
||
|