feat(app): 初始化项目配置和页面结构

- 添加 .dockerignore 和 .env.example 配置文件
- 添加 .gitignore 忽略规则配置
- 创建服务端代理API路由(_file、_modules、_server)
- 集成 Ant Design Vue 组件库并配置SSR样式提取
- 定义API响应类型封装
- 创建基础布局组件(blank、console)
- 实现应用中心页面和组件(AppsCenter)
- 添加文章列表测试页面
- 配置控制台导航菜单结构
- 实现控制台头部组件
- 创建联系页面表单
This commit is contained in:
2026-01-17 18:23:37 +08:00
commit 5e26fdc7fb
439 changed files with 56219 additions and 0 deletions

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