Files
pc-10584/app/pages/console/account/members.vue
赵忠林 775841eed3 feat(core): 初始化项目基础架构和CMS功能模块
- 添加Docker相关配置文件(.dockerignore, .env.example, .gitignore)
- 实现服务端API代理功能,支持文件、模块和服务器API转发
- 创建文章详情页、栏目文章列表页和单页内容展示页面
- 集成Ant Design Vue组件库并实现SSR样式提取功能
- 定义API响应数据结构类型和应用布局组件
- 开发开发者应用中心和文章管理页面
- 实现CMS导航菜单获取和多租户切换功能
2026-01-27 00:14:08 +08:00

443 lines
14 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.

<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>