| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939 |
- <template>
- <div class="user-list-container">
- <div class="header">
- <div class="header-left">
- <h2>用户管理</h2>
- </div>
- <div class="header-actions">
- <el-input
- v-model="searchQuery"
- placeholder="搜索手机/姓名/英文名"
- style="width: 240px"
- clearable
- @clear="fetchUsers"
- @keyup.enter="handleSearch"
- >
- <template #append>
- <el-button :icon="Search" @click="handleSearch" />
- </template>
- </el-input>
- <el-select v-model="statusFilter" placeholder="状态筛选" clearable @change="handleSearch" style="width: 120px">
- <el-option label="待审核" value="PENDING" />
- <el-option label="正常" value="ACTIVE" />
- <el-option label="已禁用" value="DISABLED" />
- </el-select>
- <el-select v-model="roleFilter" placeholder="角色筛选" clearable @change="handleSearch" style="width: 120px">
- <el-option label="开发者" value="DEVELOPER" />
- <el-option label="管理员" value="SUPER_ADMIN" />
- </el-select>
-
- <el-divider direction="vertical" class="action-divider" />
-
- <el-button type="primary" @click="fetchUsers" circle>
- <el-icon><Refresh /></el-icon>
- </el-button>
- <el-button type="success" @click="handleCreateUser">
- <el-icon style="margin-right: 4px"><Plus /></el-icon> 新增用户
- </el-button>
- <el-button type="primary" plain @click="showImportDialog = true">
- <el-icon style="margin-right: 4px"><Upload /></el-icon> Excel导入
- </el-button>
- <el-button type="warning" plain @click="handleBatchResetClick" :disabled="selectedUsers.length === 0">
- <el-icon style="margin-right: 4px"><EditPen /></el-icon> 批量重置英文名
- </el-button>
- <el-button @click="openLogDrawer">
- <el-icon style="margin-right: 4px"><List /></el-icon> 操作日志
- </el-button>
- </div>
- </div>
- <el-table
- :data="users"
- v-loading="loading"
- stripe
- border
- style="width: 100%"
- @selection-change="handleSelectionChange"
- >
- <el-table-column type="selection" width="55" />
- <el-table-column prop="id" label="ID" width="80" />
- <el-table-column prop="mobile" label="手机号" min-width="120" />
- <el-table-column prop="name" label="姓名" min-width="100" />
- <el-table-column prop="english_name" label="英文名" min-width="120" />
- <el-table-column prop="role" label="角色" width="120" align="center">
- <template #default="scope">
- <el-tag :type="scope.row.role === 'SUPER_ADMIN' ? 'danger' : (scope.row.role === 'DEVELOPER' ? 'warning' : 'info')">
- {{ getRoleLabel(scope.row.role) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="status" label="状态" width="100" align="center">
- <template #default="scope">
- <el-tag :type="getStatusType(scope.row.status)">
- {{ getStatusLabel(scope.row.status) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="created_at" label="注册时间" min-width="180">
- <template #default="scope">
- {{ formatDate(scope.row.created_at) }}
- </template>
- </el-table-column>
-
- <el-table-column label="操作" fixed="right" width="220">
- <template #default="scope">
- <div class="action-buttons">
- <!-- Allow Edit for everyone, including Super Admin -->
- <el-button type="primary" link @click="handleEditUser(scope.row)">编辑</el-button>
- <!-- Other actions restricted for Super Admin -->
- <template v-if="scope.row.role !== 'SUPER_ADMIN'">
- <el-divider direction="vertical" />
-
- <!-- PENDING Actions -->
- <template v-if="scope.row.status === 'PENDING'">
- <el-button type="primary" link @click="handleStatus(scope.row, 'ACTIVE')">通过</el-button>
- <el-divider direction="vertical" />
- <el-button type="primary" link @click="handleStatus(scope.row, 'DISABLED')">拒绝</el-button>
- </template>
-
- <!-- ACTIVE Actions -->
- <template v-if="scope.row.status === 'ACTIVE'">
- <el-button type="primary" link @click="handleStatus(scope.row, 'DISABLED')">禁用</el-button>
- </template>
-
- <!-- DISABLED Actions -->
- <template v-if="scope.row.status === 'DISABLED'">
- <el-button type="primary" link @click="handleStatus(scope.row, 'ACTIVE')">启用</el-button>
- </template>
- <el-divider direction="vertical" />
- <el-dropdown trigger="click">
- <span class="el-dropdown-link">
- 更多<el-icon class="el-icon--right"><ArrowDown /></el-icon>
- </span>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item
- @click="handleChangeRole(scope.row)"
- >
- 变更角色
- </el-dropdown-item>
- <el-dropdown-item
- @click="handleResetPassword(scope.row)"
- >
- 重置密码
- </el-dropdown-item>
- </el-dropdown-menu>
- </template>
- </el-dropdown>
- </template>
- </div>
- </template>
- </el-table-column>
- </el-table>
- <div class="pagination">
- <el-pagination
- v-model:current-page="currentPage"
- v-model:page-size="pageSize"
- :page-sizes="[10, 20, 50, 100]"
- layout="total, sizes, prev, pager, next, jumper"
- :total="total"
- @size-change="handleSizeChange"
- @current-change="handleCurrentChange"
- />
- </div>
- <!-- Change Role Dialog -->
- <el-dialog v-model="changeRoleDialogVisible" title="变更角色" width="400px">
- <p>变更用户 <strong>{{ roleTarget?.mobile }}</strong> 的角色。</p>
-
- <el-form :model="roleForm" label-position="top">
- <el-form-item label="选择角色">
- <el-select v-model="roleForm.role" style="width: 100%">
- <el-option label="普通用户" value="ORDINARY_USER" />
- <el-option label="开发者" value="DEVELOPER" />
- <el-option label="超级管理员" value="SUPER_ADMIN" />
- </el-select>
- </el-form-item>
- <el-form-item label="管理员密码验证" required>
- <el-input
- v-model="roleForm.admin_password"
- type="password"
- show-password
- placeholder="请输入管理员密码"
- :name="dynamicPwdField"
- autocomplete="new-password"
- />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="changeRoleDialogVisible = false">取消</el-button>
- <el-button type="primary" @click="confirmChangeRole" :loading="changingRole">确认变更</el-button>
- </template>
- </el-dialog>
- <!-- Admin Verify Dialog for Status/Reset -->
- <el-dialog v-model="verifyDialogVisible" title="安全验证" width="400px">
- <p>此操作需要验证管理员权限。</p>
- <p v-if="verifyTargetUser">操作对象: <strong>{{ verifyTargetUser.mobile }}</strong></p>
-
- <div style="margin-top: 20px;">
- <p style="margin-bottom: 10px;">管理员密码</p>
- <el-input
- v-model="adminPasswordVerify"
- type="password"
- show-password
- placeholder="请输入管理员密码"
- :name="dynamicPwdField"
- autocomplete="new-password"
- @keyup.enter="confirmVerify"
- />
- </div>
- <template #footer>
- <el-button @click="verifyDialogVisible = false">取消</el-button>
- <el-button type="primary" @click="confirmVerify" :loading="verifying">确认</el-button>
- </template>
- </el-dialog>
- <!-- Reset Password Dialog -->
- <el-dialog v-model="resetPasswordDialogVisible" title="重置密码成功" width="400px">
- <div style="text-align: center;">
- <p>用户 <b>{{ resetPasswordUserMobile }}</b> 的新密码为:</p>
- <div style="margin: 20px 0; font-size: 24px; font-weight: bold; color: #409EFF; background: #f4f4f5; padding: 10px; border-radius: 4px;">
- {{ newPassword }}
- </div>
- <p style="color: #f56c6c; font-size: 12px;">请立即复制并保存,此密码只显示一次!</p>
- </div>
- <template #footer>
- <span class="dialog-footer">
- <el-button @click="copyPassword">复制密码</el-button>
- <el-button type="primary" @click="resetPasswordDialogVisible = false">关闭</el-button>
- </span>
- </template>
- </el-dialog>
- <!-- Batch Reset Dialog -->
- <el-dialog v-model="batchResetDialogVisible" title="批量重置英文名" width="400px">
- <div class="warning-text" style="margin-bottom: 20px; color: #e6a23c; display: flex; align-items: flex-start; gap: 8px;">
- <el-icon style="margin-top: 2px"><Warning /></el-icon>
- <span>
- 将根据用户的姓名自动生成拼音英文名。如有重复将自动添加数字后缀。
- <br>
- 已选择 <strong>{{ selectedUsers.length }}</strong> 位用户。
- </span>
- </div>
-
- <el-form label-position="top">
- <el-form-item label="管理员密码验证" required>
- <el-input
- v-model="batchAdminPassword"
- type="password"
- show-password
- placeholder="请输入管理员密码"
- :name="dynamicPwdField"
- autocomplete="new-password"
- />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="batchResetDialogVisible = false">取消</el-button>
- <el-button type="primary" @click="confirmBatchReset" :loading="batchResetting">确认重置</el-button>
- </template>
- </el-dialog>
- <!-- Create User Dialog -->
- <el-dialog v-model="createDialogVisible" title="新增用户" width="500px">
- <el-form :model="createForm" :rules="createRules" ref="createFormRef" label-width="100px">
- <el-form-item label="手机号" prop="mobile">
- <el-input v-model="createForm.mobile" placeholder="请输入手机号" />
- </el-form-item>
- <el-form-item label="姓名" prop="name">
- <el-input v-model="createForm.name" placeholder="请输入姓名" />
- </el-form-item>
- <el-form-item label="英文名" prop="english_name">
- <el-input v-model="createForm.english_name" placeholder="请输入英文名(选填,自动生成拼音)" />
- </el-form-item>
- <el-form-item label="密码" prop="password">
- <el-input v-model="createForm.password" type="password" show-password placeholder="请输入密码" />
- </el-form-item>
- <el-form-item label="角色" prop="role">
- <el-select v-model="createForm.role" placeholder="请选择角色" style="width: 100%">
- <el-option label="普通用户" value="ORDINARY_USER" />
- <el-option label="开发者" value="DEVELOPER" />
- <el-option label="超级管理员" value="SUPER_ADMIN" />
- </el-select>
- </el-form-item>
- <el-form-item label="管理员验证" prop="admin_password">
- <el-input
- v-model="createForm.admin_password"
- type="password"
- show-password
- placeholder="请输入管理员密码"
- :name="dynamicPwdField"
- autocomplete="new-password"
- />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="createDialogVisible = false">取消</el-button>
- <el-button type="primary" @click="submitCreateUser" :loading="creating">确定</el-button>
- </template>
- </el-dialog>
- <!-- Edit User Dialog -->
- <el-dialog v-model="editDialogVisible" title="编辑用户信息" width="500px">
- <el-form :model="editForm" :rules="editRules" ref="editFormRef" label-width="100px">
- <el-form-item label="手机号" prop="mobile">
- <el-input v-model="editForm.mobile" placeholder="请输入手机号" />
- </el-form-item>
- <el-form-item label="姓名" prop="name">
- <el-input v-model="editForm.name" placeholder="请输入姓名" />
- </el-form-item>
- <el-form-item label="英文名" prop="english_name">
- <el-input v-model="editForm.english_name" placeholder="请输入英文名(选填,自动生成拼音)" />
- </el-form-item>
- <el-form-item label="管理员验证" prop="admin_password">
- <el-input
- v-model="editForm.admin_password"
- type="password"
- show-password
- placeholder="请输入管理员密码"
- :name="dynamicPwdField"
- autocomplete="new-password"
- />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="editDialogVisible = false">取消</el-button>
- <el-button type="primary" @click="submitEditUser" :loading="editing">保存</el-button>
- </template>
- </el-dialog>
- <!-- Logs Drawer -->
- <el-drawer
- v-model="logDrawerVisible"
- title="操作日志"
- direction="rtl"
- size="60%"
- >
- <div class="log-filter" style="margin-bottom: 20px; display: flex; gap: 10px; flex-wrap: wrap;">
- <el-select v-model="logFilter.action_type" placeholder="操作类型" clearable style="width: 150px" @change="fetchLogsData">
- <el-option label="新增用户" value="MANUAL_ADD" />
- <el-option label="删除用户" value="DELETE" />
- <el-option label="更新信息" value="UPDATE" />
- <el-option label="禁用" value="DISABLE" />
- <el-option label="启用" value="ENABLE" />
- <el-option label="变更角色" value="CHANGE_ROLE" />
- <el-option label="重置密码" value="RESET_PASSWORD" />
- </el-select>
- <el-input
- v-model="logFilter.keyword"
- placeholder="搜索目标手机号"
- style="width: 200px"
- clearable
- @clear="fetchLogsData"
- @keyup.enter="fetchLogsData"
- >
- <template #append>
- <el-button :icon="Search" @click="fetchLogsData" />
- </template>
- </el-input>
- <el-date-picker
- v-model="logFilter.dateRange"
- type="daterange"
- range-separator="至"
- start-placeholder="开始日期"
- end-placeholder="结束日期"
- value-format="YYYY-MM-DD"
- @change="fetchLogsData"
- />
- <el-button @click="fetchLogsData"><el-icon><Refresh /></el-icon></el-button>
- </div>
- <el-table :data="logList" v-loading="logLoading" stripe border style="width: 100%">
- <el-table-column prop="id" label="ID" width="80" />
- <el-table-column prop="operator_mobile" label="操作人" width="120" />
- <el-table-column prop="target_mobile" label="目标用户" width="120" />
- <el-table-column prop="action_type" label="操作类型" width="120">
- <template #default="scope">
- {{ getActionLabel(scope.row.action_type) }}
- </template>
- </el-table-column>
- <el-table-column prop="ip_address" label="IP地址" width="130" />
- <el-table-column prop="created_at" label="时间" width="170">
- <template #default="scope">
- {{ formatDate(scope.row.created_at) }}
- </template>
- </el-table-column>
- <el-table-column prop="details" label="详情" min-width="150">
- <template #default="scope">
- <el-tooltip effect="dark" placement="top">
- <template #content>
- <div style="max-width: 500px; max-height: 300px; overflow: auto; white-space: pre-wrap;">{{ JSON.stringify(scope.row.details, null, 2) }}</div>
- </template>
- <div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 12px; color: #666; cursor: pointer;">
- {{ JSON.stringify(scope.row.details) }}
- </div>
- </el-tooltip>
- </template>
- </el-table-column>
- </el-table>
-
- <div class="pagination" style="margin-top: 20px; display: flex; justify-content: flex-end;">
- <el-pagination
- v-model:current-page="logPage.current"
- v-model:page-size="logPage.size"
- :page-sizes="[10, 20, 50]"
- layout="total, sizes, prev, pager, next"
- :total="logPage.total"
- @size-change="handleLogSizeChange"
- @current-change="handleLogCurrentChange"
- />
- </div>
- </el-drawer>
- <UserImportDialog v-model="showImportDialog" @success="fetchUsers" />
- </div>
- </template>
- <script setup lang="ts">
- import { ref, onMounted, reactive } from 'vue'
- import { ElMessage, FormInstance, FormRules } from 'element-plus'
- import { Refresh, ArrowDown, Search, Plus, List, Upload, EditPen, Warning } from '@element-plus/icons-vue'
- import api from '../utils/request'
- import { getLogs, OperationLog } from '../api/logs'
- import UserImportDialog from '../components/UserImportDialog.vue'
- interface User {
- id: number
- mobile: string
- name?: string
- english_name?: string
- status: string
- role: string
- created_at: string
- }
- const users = ref<User[]>([])
- const loading = ref(false)
- const statusFilter = ref('')
- const roleFilter = ref('')
- const searchQuery = ref('')
- const currentPage = ref(1)
- const pageSize = ref(10)
- const total = ref(0)
- // Create User
- const createDialogVisible = ref(false)
- const showImportDialog = ref(false)
- const creating = ref(false)
- const createFormRef = ref<FormInstance>()
- const createForm = reactive({
- mobile: '',
- name: '',
- english_name: '',
- password: '',
- role: 'ORDINARY_USER',
- admin_password: ''
- })
- // Edit User Logic
- const editDialogVisible = ref(false)
- const editing = ref(false)
- const editFormRef = ref<FormInstance>()
- const editForm = reactive({
- id: 0,
- mobile: '',
- name: '',
- english_name: '',
- admin_password: ''
- })
- const editRules = reactive<FormRules>({
- mobile: [
- { required: true, message: '请输入手机号', trigger: 'blur' },
- { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
- ],
- name: [
- { required: true, message: '请输入姓名', trigger: 'blur' }
- ],
- admin_password: [
- { required: true, message: '请输入管理员密码验证', trigger: 'blur' }
- ]
- })
- const handleEditUser = (user: User) => {
- editForm.id = user.id
- editForm.mobile = user.mobile
- editForm.name = user.name || ''
- editForm.english_name = user.english_name || ''
- editForm.admin_password = ''
- refreshDynamicField()
- editDialogVisible.value = true
- }
- const submitEditUser = async () => {
- if (!editFormRef.value) return
- await editFormRef.value.validate(async (valid) => {
- if (valid) {
- editing.value = true
- try {
- await api.put(`/users/${editForm.id}`, {
- mobile: editForm.mobile,
- name: editForm.name,
- english_name: editForm.english_name,
- admin_password: editForm.admin_password
- })
- ElMessage.success('用户信息更新成功')
- editDialogVisible.value = false
- fetchUsers()
- } catch (e) {
- // handled
- } finally {
- editing.value = false
- }
- }
- })
- }
- // Dynamic Field Name logic
- const dynamicPwdField = ref('admin_pwd_' + Date.now())
- const refreshDynamicField = () => {
- dynamicPwdField.value = 'admin_pwd_' + Math.random().toString(36).slice(2)
- }
- const createRules = reactive<FormRules>({
- mobile: [
- { required: true, message: '请输入手机号', trigger: 'blur' },
- { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
- ],
- name: [
- { required: true, message: '请输入姓名', trigger: 'blur' }
- ],
- password: [
- { required: true, message: '请输入密码', trigger: 'blur' },
- { min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
- ],
- role: [
- { required: true, message: '请选择角色', trigger: 'change' }
- ],
- admin_password: [
- { required: true, message: '请输入管理员密码验证', trigger: 'blur' }
- ]
- })
- const handleCreateUser = () => {
- createForm.mobile = ''
- createForm.name = ''
- createForm.english_name = ''
- createForm.password = ''
- createForm.role = 'ORDINARY_USER'
- createForm.admin_password = ''
- refreshDynamicField()
- createDialogVisible.value = true
- }
- const submitCreateUser = async () => {
- if (!createFormRef.value) return
- await createFormRef.value.validate(async (valid) => {
- if (valid) {
- creating.value = true
- try {
- await api.post('/users/', createForm)
- ElMessage.success('用户创建成功')
- createDialogVisible.value = false
- fetchUsers()
- } catch (e) {
- // handled
- } finally {
- creating.value = false
- }
- }
- })
- }
- const fetchUsers = async () => {
- loading.value = true
- try {
- const params: any = {
- skip: (currentPage.value - 1) * pageSize.value,
- limit: pageSize.value
- }
- if (statusFilter.value) params.status = statusFilter.value
- if (roleFilter.value) params.role = roleFilter.value
- if (searchQuery.value) params.keyword = searchQuery.value
-
- const res = await api.get('/users/', { params })
- if (res.data && Array.isArray(res.data.items)) {
- users.value = res.data.items
- total.value = res.data.total
- } else if (Array.isArray(res.data)) {
- users.value = res.data
- total.value = res.data.length
- }
- } catch (e) {
- // handled
- } finally {
- loading.value = false
- }
- }
- const handleSearch = () => {
- currentPage.value = 1
- fetchUsers()
- }
- const handleSizeChange = (val: number) => {
- pageSize.value = val
- fetchUsers()
- }
- const handleCurrentChange = (val: number) => {
- currentPage.value = val
- fetchUsers()
- }
- const getStatusType = (status: string) => {
- switch(status) {
- case 'ACTIVE': return 'success'
- case 'PENDING': return 'warning'
- case 'DISABLED': return 'danger'
- default: return 'info'
- }
- }
- const getStatusLabel = (status: string) => {
- switch(status) {
- case 'ACTIVE': return '正常'
- case 'PENDING': return '待审核'
- case 'DISABLED': return '已禁用'
- default: return status
- }
- }
- const getRoleLabel = (role: string) => {
- switch(role) {
- case 'SUPER_ADMIN': return '超级管理员'
- case 'DEVELOPER': return '开发者'
- case 'ORDINARY_USER': return '普通用户'
- default: return role
- }
- }
- const formatDate = (dateStr: string) => {
- if (!dateStr) return ''
- return new Date(dateStr).toLocaleString()
- }
- const handleStatus = (user: User, newStatus: string) => {
- verifyTargetUser.value = user
- verifyNextStatus.value = newStatus
- verifyActionType.value = 'STATUS'
- adminPasswordVerify.value = ''
- refreshDynamicField()
- verifyDialogVisible.value = true
- }
- const doHandleStatus = async (user: User, newStatus: string, adminPwd: string) => {
- try {
- await api.put(`/users/${user.id}`, {
- status: newStatus,
- admin_password: adminPwd
- })
- ElMessage.success('状态已更新')
- fetchUsers()
- } catch (e) {
- // handled
- }
- }
- // Verify Dialog Logic
- const verifyDialogVisible = ref(false)
- const adminPasswordVerify = ref('')
- const verifyTargetUser = ref<User | null>(null)
- const verifyActionType = ref('') // 'STATUS' | 'RESET'
- const verifyNextStatus = ref('')
- const verifying = ref(false)
- const confirmVerify = async () => {
- if (!adminPasswordVerify.value) {
- ElMessage.warning('请输入管理员密码')
- return
- }
-
- verifying.value = true
- try {
- if (verifyActionType.value === 'STATUS' && verifyTargetUser.value) {
- await doHandleStatus(verifyTargetUser.value, verifyNextStatus.value, adminPasswordVerify.value)
- } else if (verifyActionType.value === 'RESET' && verifyTargetUser.value) {
- await doHandleResetPassword(verifyTargetUser.value, adminPasswordVerify.value)
- }
- verifyDialogVisible.value = false
- } catch (e) {
- // handled
- } finally {
- verifying.value = false
- }
- }
- // Reset Password Logic
- const resetPasswordDialogVisible = ref(false)
- const newPassword = ref('')
- const resetPasswordUserMobile = ref('')
- const handleResetPassword = (user: User) => {
- verifyTargetUser.value = user
- verifyActionType.value = 'RESET'
- adminPasswordVerify.value = ''
- refreshDynamicField()
- verifyDialogVisible.value = true
- }
- const doHandleResetPassword = async (user: User, adminPwd: string) => {
- try {
- const res = await api.post('/simple/admin/reset-password', {
- user_id: user.id,
- admin_password: adminPwd
- })
- newPassword.value = res.data.new_password
- resetPasswordUserMobile.value = user.mobile
- resetPasswordDialogVisible.value = true
- } catch (e) {
- // handled
- }
- }
- const copyPassword = async () => {
- try {
- await navigator.clipboard.writeText(newPassword.value)
- ElMessage.success('密码已复制到剪贴板')
- } catch (err) {
- ElMessage.error('复制失败,请手动复制')
- }
- }
- // Change Role Logic
- const changeRoleDialogVisible = ref(false)
- const roleTarget = ref<User | null>(null)
- const changingRole = ref(false)
- const roleForm = reactive({
- role: '',
- admin_password: ''
- })
- const handleChangeRole = (user: User) => {
- roleTarget.value = user
- roleForm.role = user.role
- roleForm.admin_password = ''
- refreshDynamicField()
- changeRoleDialogVisible.value = true
- }
- const confirmChangeRole = async () => {
- if (!roleTarget.value) return
- if (!roleForm.role) {
- ElMessage.warning('请选择角色')
- return
- }
- if (!roleForm.admin_password) {
- ElMessage.warning('请输入管理员密码')
- return
- }
- changingRole.value = true
- try {
- await api.put(`/users/${roleTarget.value.id}`, {
- role: roleForm.role,
- admin_password: roleForm.admin_password
- })
- ElMessage.success('角色变更成功')
- changeRoleDialogVisible.value = false
- fetchUsers()
- } catch (e) {
- // handled
- } finally {
- changingRole.value = false
- }
- }
- // Batch Reset Logic
- const batchResetDialogVisible = ref(false)
- const batchAdminPassword = ref('')
- const batchResetting = ref(false)
- const selectedUsers = ref<User[]>([])
- const handleSelectionChange = (val: User[]) => {
- selectedUsers.value = val
- }
- const handleBatchResetClick = () => {
- if (selectedUsers.value.length === 0) {
- ElMessage.warning('请先选择用户')
- return
- }
- batchAdminPassword.value = ''
- refreshDynamicField()
- batchResetDialogVisible.value = true
- }
- const confirmBatchReset = async () => {
- if (!batchAdminPassword.value) {
- ElMessage.warning('请输入管理员密码')
- return
- }
-
- batchResetting.value = true
- try {
- const res = await api.post('/users/batch/reset-english-name', {
- user_ids: selectedUsers.value.map(u => u.id),
- admin_password: batchAdminPassword.value
- })
- ElMessage.success(`操作成功,已重置 ${res.data.count} 位用户的英文名`)
- batchResetDialogVisible.value = false
- fetchUsers()
- selectedUsers.value = []
- } catch (e) {
- // handled
- } finally {
- batchResetting.value = false
- }
- }
- // Log Logic
- const logDrawerVisible = ref(false)
- const logLoading = ref(false)
- const logList = ref<OperationLog[]>([])
- const logFilter = reactive({
- action_type: '',
- keyword: '',
- dateRange: [] as string[]
- })
- const logPage = reactive({
- current: 1,
- size: 20,
- total: 0
- })
- const openLogDrawer = () => {
- logDrawerVisible.value = true
- fetchLogsData()
- }
- const fetchLogsData = async () => {
- logLoading.value = true
- try {
- const params: any = {
- skip: (logPage.current - 1) * logPage.size,
- limit: logPage.size,
- action_type: logFilter.action_type || undefined,
- keyword: logFilter.keyword || undefined,
- }
- if (logFilter.dateRange && logFilter.dateRange.length === 2) {
- params.start_date = logFilter.dateRange[0]
- params.end_date = logFilter.dateRange[1] + ' 23:59:59'
- }
- const res = await getLogs(params)
- logList.value = res.data.items
- logPage.total = res.data.total
- } catch (e) {
- // handled
- } finally {
- logLoading.value = false
- }
- }
- const handleLogSizeChange = (val: number) => {
- logPage.size = val
- fetchLogsData()
- }
- const handleLogCurrentChange = (val: number) => {
- logPage.current = val
- fetchLogsData()
- }
- const getActionLabel = (type: string) => {
- const map: Record<string, string> = {
- 'MANUAL_ADD': '新增用户',
- 'DELETE': '删除用户',
- 'UPDATE': '更新信息',
- 'IMPORT': '批量导入',
- 'DISABLE': '禁用',
- 'ENABLE': '启用',
- 'RESET_PASSWORD': '重置密码',
- 'CHANGE_ROLE': '变更角色',
- 'SYNC_M2M': 'M2M 同步'
- }
- return map[type] || type
- }
- onMounted(() => {
- fetchUsers()
- })
- </script>
- <style scoped>
- .user-list-container {
- padding: 20px;
- background: #fff;
- border-radius: 4px;
- }
- .header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 24px;
- flex-wrap: wrap;
- gap: 16px;
- }
- .header-left h2 {
- margin: 0;
- font-size: 24px;
- color: #303133;
- }
- .header-actions {
- display: flex;
- align-items: center;
- gap: 12px;
- flex-wrap: wrap;
- }
- .action-divider {
- height: 24px;
- margin: 0 4px;
- }
- .action-buttons {
- display: flex;
- align-items: center;
- justify-content: flex-start;
- }
- .el-dropdown-link {
- cursor: pointer;
- color: #409eff;
- display: flex;
- align-items: center;
- font-size: 14px;
- margin-left: 10px;
- }
- .text-gray {
- color: #909399;
- font-size: 12px;
- }
- .pagination {
- margin-top: 20px;
- display: flex;
- justify-content: flex-end;
- }
- .captcha-item {
- margin-bottom: 0;
- }
- </style>
|