feat(core): 初始化项目基础架构和CMS功能模块
- 添加Docker相关配置文件(.dockerignore, .env.example, .gitignore) - 实现服务端API代理功能,支持文件、模块和服务器API转发 - 创建文章详情页、栏目文章列表页和单页内容展示页面 - 集成Ant Design Vue组件库并实现SSR样式提取功能 - 定义API响应数据结构类型和应用布局组件 - 开发开发者应用中心和文章管理页面 - 实现CMS导航菜单获取和多租户切换功能
This commit is contained in:
442
app/pages/console/account/members.vue
Normal file
442
app/pages/console/account/members.vue
Normal file
@@ -0,0 +1,442 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user