- 添加 .dockerignore 和 .env.example 配置文件 - 添加 .gitignore 忽略规则配置 - 创建服务端代理API路由(_file、_modules、_server) - 集成 Ant Design Vue 组件库并配置SSR样式提取 - 定义API响应类型封装 - 创建基础布局组件(blank、console) - 实现应用中心页面和组件(AppsCenter) - 添加文章列表测试页面 - 配置控制台导航菜单结构 - 实现控制台头部组件 - 创建联系页面表单
443 lines
14 KiB
Vue
443 lines
14 KiB
Vue
<template>
|
||
<div class="space-y-4">
|
||
<a-page-header title="成员管理" sub-title="成员邀请、角色与权限">
|
||
<template #extra>
|
||
<a-space>
|
||
<a-input
|
||
v-model:value="keywords"
|
||
allow-clear
|
||
placeholder="搜索账号/昵称/手机号"
|
||
class="w-64"
|
||
@press-enter="doSearch"
|
||
/>
|
||
<a-button :loading="loading" @click="reload">刷新</a-button>
|
||
<a-button type="primary" @click="openInvite">邀请成员</a-button>
|
||
</a-space>
|
||
</template>
|
||
</a-page-header>
|
||
|
||
<a-row :gutter="[16, 16]">
|
||
<a-col :xs="24" :lg="12">
|
||
<a-card :bordered="false" class="card" title="成员配额">
|
||
<a-descriptions :column="2" size="small" bordered>
|
||
<a-descriptions-item label="成员上限">
|
||
{{ company?.members ?? '-' }}
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="当前人数">
|
||
{{ company?.users ?? '-' }}
|
||
</a-descriptions-item>
|
||
</a-descriptions>
|
||
<div class="mt-4 text-sm text-gray-500">
|
||
成员数据来自系统用户(租户维度),可进行邀请、禁用、重置密码与角色设置。
|
||
</div>
|
||
</a-card>
|
||
</a-col>
|
||
<a-col :xs="24" :lg="12">
|
||
<a-card :bordered="false" class="card" title="快速操作">
|
||
<a-space wrap>
|
||
<a-button @click="openInvite">邀请成员</a-button>
|
||
<a-button @click="reload">刷新列表</a-button>
|
||
</a-space>
|
||
</a-card>
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<a-card :bordered="false" class="card">
|
||
<a-alert v-if="error" class="mb-4" show-icon type="error" :message="error" />
|
||
|
||
<a-table
|
||
:data-source="list"
|
||
:loading="loading"
|
||
:pagination="false"
|
||
size="middle"
|
||
:row-key="(r: any) => r.userId ?? r.username"
|
||
>
|
||
<a-table-column title="ID" data-index="userId" width="90" />
|
||
<a-table-column title="账号" data-index="username" width="180" />
|
||
<a-table-column title="昵称" data-index="nickname" width="160" />
|
||
<a-table-column title="手机号" data-index="phone" width="140" />
|
||
<a-table-column title="角色" key="roleName" width="160">
|
||
<template #default="{ record }">
|
||
<span>{{ resolveRoleName(record) }}</span>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="状态" key="status" width="120">
|
||
<template #default="{ record }">
|
||
<a-tag v-if="record.status === 0 || record.status === undefined" color="green">正常</a-tag>
|
||
<a-tag v-else color="default">冻结</a-tag>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="创建时间" data-index="createTime" width="180" />
|
||
<a-table-column title="操作" key="actions" width="260" fixed="right">
|
||
<template #default="{ record }">
|
||
<a-space>
|
||
<a-button size="small" @click="openRole(record)" :disabled="!record.userId">设置角色</a-button>
|
||
<a-button size="small" @click="openReset(record)" :disabled="!record.userId">重置密码</a-button>
|
||
<a-button
|
||
size="small"
|
||
:loading="busyUserId === record.userId"
|
||
@click="toggleStatus(record)"
|
||
:disabled="!record.userId"
|
||
>
|
||
{{ record.status === 1 ? '解冻' : '冻结' }}
|
||
</a-button>
|
||
<a-popconfirm
|
||
title="确定删除该成员?"
|
||
ok-text="删除"
|
||
cancel-text="取消"
|
||
@confirm="remove(record)"
|
||
>
|
||
<a-button size="small" danger :disabled="!record.userId">删除</a-button>
|
||
</a-popconfirm>
|
||
</a-space>
|
||
</template>
|
||
</a-table-column>
|
||
</a-table>
|
||
|
||
<div class="mt-4 flex items-center justify-end">
|
||
<a-pagination
|
||
:current="page"
|
||
:page-size="limit"
|
||
:total="total"
|
||
show-size-changer
|
||
:page-size-options="['10', '20', '50', '100']"
|
||
@change="onPageChange"
|
||
@show-size-change="onPageSizeChange"
|
||
/>
|
||
</div>
|
||
</a-card>
|
||
|
||
<a-modal
|
||
v-model:open="inviteOpen"
|
||
title="邀请成员"
|
||
ok-text="创建账号"
|
||
cancel-text="取消"
|
||
:confirm-loading="inviting"
|
||
@ok="submitInvite"
|
||
>
|
||
<a-form ref="inviteFormRef" layout="vertical" :model="inviteForm" :rules="inviteRules">
|
||
<a-form-item label="账号" name="username">
|
||
<a-input v-model:value="inviteForm.username" placeholder="例如:tom / tom@example.com" />
|
||
</a-form-item>
|
||
<a-form-item label="昵称" name="nickname">
|
||
<a-input v-model:value="inviteForm.nickname" placeholder="例如:Tom" />
|
||
</a-form-item>
|
||
<a-form-item label="手机号" name="phone">
|
||
<a-input v-model:value="inviteForm.phone" placeholder="例如:13800000000" />
|
||
</a-form-item>
|
||
<a-form-item label="角色" name="roleId">
|
||
<a-select
|
||
v-model:value="inviteForm.roleId"
|
||
placeholder="请选择角色"
|
||
allow-clear
|
||
:options="roleOptions"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="初始密码" name="password">
|
||
<a-input-password v-model:value="inviteForm.password" placeholder="请输入初始密码" />
|
||
</a-form-item>
|
||
<a-form-item label="确认密码" name="password2">
|
||
<a-input-password v-model:value="inviteForm.password2" placeholder="再次输入密码" />
|
||
</a-form-item>
|
||
</a-form>
|
||
<a-alert
|
||
class="mt-2"
|
||
type="info"
|
||
show-icon
|
||
message="创建后可在本页进行冻结/解冻、重置密码与角色设置。"
|
||
/>
|
||
</a-modal>
|
||
|
||
<a-modal
|
||
v-model:open="roleOpen"
|
||
title="设置角色"
|
||
ok-text="保存"
|
||
cancel-text="取消"
|
||
:confirm-loading="savingRole"
|
||
@ok="submitRole"
|
||
>
|
||
<a-form layout="vertical">
|
||
<a-form-item label="成员">
|
||
<a-input :value="selectedUser?.nickname || selectedUser?.username || ''" disabled />
|
||
</a-form-item>
|
||
<a-form-item label="角色">
|
||
<a-select v-model:value="selectedRoleId" placeholder="请选择角色" :options="roleOptions" />
|
||
</a-form-item>
|
||
</a-form>
|
||
</a-modal>
|
||
|
||
<a-modal
|
||
v-model:open="resetOpen"
|
||
title="重置密码"
|
||
ok-text="确认重置"
|
||
cancel-text="取消"
|
||
:confirm-loading="resetting"
|
||
@ok="submitReset"
|
||
>
|
||
<a-form layout="vertical">
|
||
<a-form-item label="成员">
|
||
<a-input :value="selectedUser?.nickname || selectedUser?.username || ''" disabled />
|
||
</a-form-item>
|
||
<a-form-item label="新密码">
|
||
<a-input-password v-model:value="resetPassword" placeholder="请输入新密码" />
|
||
</a-form-item>
|
||
</a-form>
|
||
<a-alert class="mt-2" type="warning" show-icon message="重置后请尽快通知成员修改密码。" />
|
||
</a-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, onMounted, reactive, ref } from 'vue'
|
||
import { message, type FormInstance } from 'ant-design-vue'
|
||
import { getTenantInfo } from '@/api/layout'
|
||
import { listRoles } from '@/api/system/role'
|
||
import { addUser, pageUsers, removeUser, updateUserPassword, updateUserStatus } from '@/api/system/user'
|
||
import { addUserRole, listUserRole, updateUserRole } from '@/api/system/userRole'
|
||
import type { Company } from '@/api/system/company/model'
|
||
import type { Role } from '@/api/system/role/model'
|
||
import type { User } from '@/api/system/user/model'
|
||
|
||
definePageMeta({ layout: 'console' })
|
||
|
||
const loading = ref(false)
|
||
const error = ref<string>('')
|
||
|
||
const company = ref<Company | null>(null)
|
||
const roles = ref<Role[]>([])
|
||
|
||
const list = ref<User[]>([])
|
||
const total = ref(0)
|
||
const page = ref(1)
|
||
const limit = ref(10)
|
||
const keywords = ref('')
|
||
|
||
const roleOptions = computed(() =>
|
||
roles.value.map((r) => ({ label: r.roleName ?? String(r.roleId ?? ''), value: r.roleId }))
|
||
)
|
||
|
||
function resolveRoleName(user: User) {
|
||
const direct = typeof user.roleName === 'string' ? user.roleName.trim() : ''
|
||
if (direct) return direct
|
||
const hit = roles.value.find((r) => r.roleId === user.roleId)
|
||
return hit?.roleName ?? '-'
|
||
}
|
||
|
||
async function loadCompany() {
|
||
try {
|
||
company.value = await getTenantInfo()
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
async function loadRolesOnce() {
|
||
if (roles.value.length) return
|
||
try {
|
||
roles.value = await listRoles()
|
||
} catch {
|
||
roles.value = []
|
||
}
|
||
}
|
||
|
||
async function loadMembers() {
|
||
loading.value = true
|
||
error.value = ''
|
||
try {
|
||
const res = await pageUsers({
|
||
page: page.value,
|
||
limit: limit.value,
|
||
keywords: keywords.value || undefined
|
||
})
|
||
list.value = res?.list ?? []
|
||
total.value = res?.count ?? 0
|
||
} catch (e) {
|
||
error.value = e instanceof Error ? e.message : '成员列表加载失败'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function reload() {
|
||
await Promise.all([loadCompany(), loadRolesOnce(), loadMembers()])
|
||
}
|
||
|
||
function doSearch() {
|
||
page.value = 1
|
||
loadMembers()
|
||
}
|
||
|
||
function onPageChange(nextPage: number) {
|
||
page.value = nextPage
|
||
loadMembers()
|
||
}
|
||
|
||
function onPageSizeChange(_current: number, nextSize: number) {
|
||
limit.value = nextSize
|
||
page.value = 1
|
||
loadMembers()
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await reload()
|
||
})
|
||
|
||
const busyUserId = ref<number | null>(null)
|
||
async function toggleStatus(user: User) {
|
||
if (!user.userId) return
|
||
const next = user.status === 1 ? 0 : 1
|
||
busyUserId.value = user.userId
|
||
try {
|
||
await updateUserStatus(user.userId, next)
|
||
message.success(next === 0 ? '已解冻' : '已冻结')
|
||
await loadMembers()
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '操作失败')
|
||
} finally {
|
||
busyUserId.value = null
|
||
}
|
||
}
|
||
|
||
async function remove(user: User) {
|
||
if (!user.userId) return
|
||
busyUserId.value = user.userId
|
||
try {
|
||
await removeUser(user.userId)
|
||
message.success('已删除')
|
||
await loadMembers()
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '删除失败')
|
||
} finally {
|
||
busyUserId.value = null
|
||
}
|
||
}
|
||
|
||
const inviteOpen = ref(false)
|
||
const inviting = ref(false)
|
||
const inviteFormRef = ref<FormInstance>()
|
||
const inviteForm = reactive<{ username: string; nickname: string; phone: string; roleId?: number; password: string; password2: string }>({
|
||
username: '',
|
||
nickname: '',
|
||
phone: '',
|
||
roleId: undefined,
|
||
password: '',
|
||
password2: ''
|
||
})
|
||
const inviteRules = reactive({
|
||
username: [{ required: true, type: 'string', message: '请输入账号' }],
|
||
nickname: [{ required: true, type: 'string', message: '请输入昵称' }],
|
||
password: [{ required: true, type: 'string', message: '请输入初始密码' }],
|
||
password2: [{ required: true, type: 'string', message: '请再次输入密码' }]
|
||
})
|
||
|
||
function openInvite() {
|
||
inviteForm.username = ''
|
||
inviteForm.nickname = ''
|
||
inviteForm.phone = ''
|
||
inviteForm.roleId = undefined
|
||
inviteForm.password = ''
|
||
inviteForm.password2 = ''
|
||
inviteOpen.value = true
|
||
}
|
||
|
||
async function submitInvite() {
|
||
try {
|
||
await inviteFormRef.value?.validate()
|
||
} catch {
|
||
return
|
||
}
|
||
if (inviteForm.password !== inviteForm.password2) {
|
||
message.error('两次输入的密码不一致')
|
||
return
|
||
}
|
||
|
||
inviting.value = true
|
||
try {
|
||
await addUser({
|
||
username: inviteForm.username.trim(),
|
||
nickname: inviteForm.nickname.trim(),
|
||
phone: inviteForm.phone.trim() || undefined,
|
||
password: inviteForm.password,
|
||
password2: inviteForm.password2,
|
||
roleId: inviteForm.roleId
|
||
})
|
||
message.success('成员已创建')
|
||
inviteOpen.value = false
|
||
await loadMembers()
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '创建失败')
|
||
} finally {
|
||
inviting.value = false
|
||
}
|
||
}
|
||
|
||
const roleOpen = ref(false)
|
||
const savingRole = ref(false)
|
||
const selectedUser = ref<User | null>(null)
|
||
const selectedRoleId = ref<number | undefined>(undefined)
|
||
function openRole(user: User) {
|
||
selectedUser.value = user
|
||
selectedRoleId.value = user.roleId
|
||
roleOpen.value = true
|
||
}
|
||
|
||
async function submitRole() {
|
||
if (!selectedUser.value?.userId) return
|
||
if (!selectedRoleId.value) {
|
||
message.error('请选择角色')
|
||
return
|
||
}
|
||
savingRole.value = true
|
||
try {
|
||
const mappings = await listUserRole({ userId: selectedUser.value.userId })
|
||
const first = Array.isArray(mappings) ? mappings[0] : undefined
|
||
if (first?.id) {
|
||
await updateUserRole({ ...first, roleId: selectedRoleId.value })
|
||
} else {
|
||
await addUserRole({ userId: selectedUser.value.userId, roleId: selectedRoleId.value })
|
||
}
|
||
message.success('角色已更新')
|
||
roleOpen.value = false
|
||
await loadMembers()
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '更新失败')
|
||
} finally {
|
||
savingRole.value = false
|
||
}
|
||
}
|
||
|
||
const resetOpen = ref(false)
|
||
const resetting = ref(false)
|
||
const resetPassword = ref('')
|
||
function openReset(user: User) {
|
||
selectedUser.value = user
|
||
resetPassword.value = ''
|
||
resetOpen.value = true
|
||
}
|
||
|
||
async function submitReset() {
|
||
if (!selectedUser.value?.userId) return
|
||
const pwd = resetPassword.value.trim()
|
||
if (!pwd) {
|
||
message.error('请输入新密码')
|
||
return
|
||
}
|
||
resetting.value = true
|
||
try {
|
||
await updateUserPassword(selectedUser.value.userId, pwd)
|
||
message.success('密码已重置')
|
||
resetOpen.value = false
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '重置失败')
|
||
} finally {
|
||
resetting.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.card {
|
||
border-radius: 12px;
|
||
}
|
||
</style>
|