Files
order_site/apps/frontend/src/views/admin/AdminUsersView.vue
T

394 lines
11 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 lang="ts">
import { onMounted, ref } from 'vue'
import AdminPaginationBar from '@/components/admin/AdminPaginationBar.vue'
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
import { isFeedbackDismissed, showConfirm, showError, showPrompt, showSuccess } from '@/lib/feedback'
import {
createAdminUser,
fetchAdminUsers,
resetAdminUserPassword,
updateAdminUserRole,
updateAdminUserStatus,
} from '@/services/admin'
import type { AdminPagination, AdminRole, AdminUserListItem } from '@/types/admin'
import { getAdminUserId, hasAdminRole } from '@/utils/admin-auth'
import { adminUserRoleOptions, adminUserStatusOptions } from '@/utils/admin-options'
import { formatAdminDateTime } from '@/utils/admin-time'
const loading = ref(true)
const creating = ref(false)
const actionLoadingId = ref<number | null>(null)
const errorMessage = ref('')
const username = ref('')
const role = ref('')
const status = ref('')
const createUsername = ref('')
const createPassword = ref('')
const createRole = ref<AdminRole>('operator')
const items = ref<AdminUserListItem[]>([])
const pagination = ref<AdminPagination>({
page: 1,
pageSize: 20,
total: 0,
})
function isCurrentUser(userId: number) {
return getAdminUserId() === userId
}
function formatRoleLabel(role: AdminRole) {
if (role === 'admin') {
return '管理员'
}
if (role === 'support') {
return '客服'
}
return '普通运营'
}
async function loadUsers(page = pagination.value.page) {
if (!hasAdminRole('admin')) {
loading.value = false
return
}
loading.value = true
errorMessage.value = ''
try {
const response = await fetchAdminUsers({
page,
pageSize: pagination.value.pageSize,
username: username.value.trim(),
role: role.value.trim(),
status: status.value.trim(),
})
items.value = response.data.items
pagination.value = response.data.pagination
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '读取后台用户失败'
} finally {
loading.value = false
}
}
async function submitCreate() {
if (!createUsername.value.trim() || !createPassword.value.trim()) {
showError('请填写账号和密码')
return
}
creating.value = true
try {
await createAdminUser({
username: createUsername.value.trim(),
password: createPassword.value.trim(),
role: createRole.value,
status: 'active',
})
showSuccess('后台用户已创建')
createUsername.value = ''
createPassword.value = ''
createRole.value = 'operator'
await loadUsers(1)
} catch (error) {
showError(error instanceof Error ? error.message : '创建后台用户失败')
} finally {
creating.value = false
}
}
async function updateRole(item: AdminUserListItem, nextRole: AdminRole) {
if (item.role === nextRole) {
return
}
try {
await showConfirm(`确认将 ${item.username} 调整为${formatRoleLabel(nextRole)}吗?`, '确认操作', {
type: 'warning',
confirmButtonText: '继续执行',
cancelButtonText: '取消',
})
} catch {
return
}
actionLoadingId.value = item.userId
try {
await updateAdminUserRole(item.userId, { role: nextRole })
showSuccess('用户角色已更新')
await loadUsers()
} catch (error) {
showError(error instanceof Error ? error.message : '更新用户角色失败')
} finally {
actionLoadingId.value = null
}
}
async function toggleStatus(item: AdminUserListItem) {
const nextStatus = item.status === 'active' ? 'disabled' : 'active'
try {
await showConfirm(`确认将 ${item.username}${nextStatus === 'active' ? '启用' : '停用'}吗?`, '确认操作', {
type: nextStatus === 'active' ? 'info' : 'warning',
confirmButtonText: '继续执行',
cancelButtonText: '取消',
})
} catch {
return
}
actionLoadingId.value = item.userId
try {
await updateAdminUserStatus(item.userId, { status: nextStatus })
showSuccess('用户状态已更新')
await loadUsers()
} catch (error) {
showError(error instanceof Error ? error.message : '更新用户状态失败')
} finally {
actionLoadingId.value = null
}
}
async function promptResetPassword(item: AdminUserListItem) {
try {
const result = await showPrompt(`请输入 ${item.username} 的新密码`, '重置密码', {
inputType: 'password',
inputPlaceholder: '至少 8 位',
confirmButtonText: '提交',
cancelButtonText: '取消',
inputValidator: (value) => value.trim().length >= 8 || '密码至少 8 位',
})
actionLoadingId.value = item.userId
await resetAdminUserPassword(item.userId, { password: result.value.trim() })
showSuccess('用户密码已重置')
await loadUsers()
} catch (error) {
if (isFeedbackDismissed(error)) {
return
}
showError(error instanceof Error ? error.message : '重置密码失败')
} finally {
actionLoadingId.value = null
}
}
onMounted(loadUsers)
</script>
<template>
<section class="admin-panel">
<header class="panel-header">
<div>
<h1>后台用户</h1>
<p>管理员可维护后台账号角色和启停状态避免继续使用单一口令</p>
</div>
</header>
<div v-if="!hasAdminRole('admin')" class="empty-block">仅管理员可以访问用户管理</div>
<template v-else>
<section class="form-card">
<div class="form-row">
<input v-model="username" class="text-input" placeholder="账号筛选" />
<select v-model="role" class="text-input select-input">
<option v-for="option in adminUserRoleOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
<select v-model="status" class="text-input select-input">
<option v-for="option in adminUserStatusOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
<el-button round @click="() => loadUsers()">查询列表</el-button>
</div>
<div class="form-row form-row-top">
<input v-model="createUsername" class="text-input" placeholder="新账号" />
<input v-model="createPassword" class="text-input" type="password" placeholder="新密码,至少 8 位" />
<select v-model="createRole" class="text-input select-input">
<option value="operator">普通运营</option>
<option value="support">客服</option>
<option value="admin">管理员</option>
</select>
<el-button :loading="creating" round type="primary" @click="submitCreate">新增用户</el-button>
</div>
</section>
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
<div v-if="loading" class="empty-block">用户列表加载中</div>
<div v-else class="table-card">
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>账号</th>
<th>角色</th>
<th>状态</th>
<th>创建时间</th>
<th>更新时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.userId">
<td>{{ item.userId }}</td>
<td>{{ item.username }}</td>
<td>{{ formatRoleLabel(item.role) }}</td>
<td><AdminStatusTag :status="item.status" /></td>
<td>{{ formatAdminDateTime(item.createdAt) }}</td>
<td>{{ formatAdminDateTime(item.updatedAt) }}</td>
<td>
<div class="action-stack">
<el-button
:loading="actionLoadingId === item.userId"
link
:disabled="isCurrentUser(item.userId)"
@click="updateRole(item, 'admin')"
>
设为管理员
</el-button>
<el-button
:loading="actionLoadingId === item.userId"
link
:disabled="isCurrentUser(item.userId)"
@click="updateRole(item, 'operator')"
>
设为运营
</el-button>
<el-button
:loading="actionLoadingId === item.userId"
link
:disabled="isCurrentUser(item.userId)"
@click="updateRole(item, 'support')"
>
设为客服
</el-button>
<el-button
:loading="actionLoadingId === item.userId"
link
:disabled="isCurrentUser(item.userId)"
@click="toggleStatus(item)"
>
{{ item.status === 'active' ? '停用' : '启用' }}
</el-button>
<el-button
:loading="actionLoadingId === item.userId"
link
type="primary"
@click="promptResetPassword(item)"
>
重置密码
</el-button>
<span v-if="isCurrentUser(item.userId)" class="self-copy">当前登录账号</span>
</div>
</td>
</tr>
</tbody>
</table>
<AdminPaginationBar
:page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:loading="loading"
@change="loadUsers"
/>
</div>
</template>
</section>
</template>
<style scoped>
.admin-panel {
display: grid;
gap: 16px;
}
.panel-header h1 {
margin: 0;
color: #1d3555;
}
.panel-header p {
margin: 8px 0 0;
color: #64748b;
}
.form-card,
.table-card,
.empty-block,
.error-copy {
padding: 18px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(86, 108, 138, 0.1);
}
.error-copy {
color: #b42318;
}
.form-row {
display: flex;
gap: 12px;
align-items: center;
}
.form-row-top {
margin-top: 12px;
}
.text-input {
width: 100%;
min-height: 42px;
padding: 0 14px;
border-radius: 14px;
border: 1px solid rgba(86, 108, 138, 0.18);
background: rgba(255, 255, 255, 0.94);
}
.select-input {
appearance: none;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
padding: 12px 10px;
text-align: left;
border-bottom: 1px solid rgba(86, 108, 138, 0.08);
color: #334155;
vertical-align: top;
}
.data-table th {
color: #64748b;
font-size: 13px;
}
.action-stack {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.self-copy {
color: #64748b;
font-size: 12px;
line-height: 30px;
}
</style>