添加系统管理、登录模块的接口、标准化开发流程
This commit is contained in:
205
src/views/system/dict/index.vue
Normal file
205
src/views/system/dict/index.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<div class="dict-container">
|
||||
<a-page-header title="字典管理" sub-title="管理系统数据字典">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增字典
|
||||
</a-button>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<a-card>
|
||||
<!-- 搜索区域 -->
|
||||
<a-form layout="inline" class="search-form">
|
||||
<a-form-item label="字典名称">
|
||||
<a-input v-model:value="searchForm.name" placeholder="请输入字典名称" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="字典编码">
|
||||
<a-input v-model:value="searchForm.code" placeholder="请输入字典编码" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'red'">
|
||||
{{ record.status === 1 ? '正常' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button type="link" size="small" @click="handleViewItems(record)">字典项</a-button>
|
||||
<a-popconfirm
|
||||
title="确定删除此字典吗?"
|
||||
@confirm="handleDelete(record.id)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 新增/编辑字典弹窗 -->
|
||||
<DictFormModal
|
||||
v-model:visible="modalVisible"
|
||||
:edit-data="editData"
|
||||
@success="loadData"
|
||||
/>
|
||||
|
||||
<!-- 字典项管理抽屉 -->
|
||||
<DictItemDrawer
|
||||
v-model:visible="itemDrawerVisible"
|
||||
:dict-data="currentDict"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import DictFormModal from '@/components/system/dict/DictFormModal.vue'
|
||||
import DictItemDrawer from '@/components/system/dict/DictItemDrawer.vue'
|
||||
|
||||
// 类型定义
|
||||
interface DictRecord {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
remark?: string
|
||||
status: number
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
// 表格
|
||||
const loading = ref(false)
|
||||
const tableData = ref<DictRecord[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ title: '字典名称', dataIndex: 'name', width: 150 },
|
||||
{ title: '字典编码', dataIndex: 'code', width: 150 },
|
||||
{ title: '备注', dataIndex: 'remark', ellipsis: true },
|
||||
{ title: '状态', dataIndex: 'status', width: 80 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', width: 170 },
|
||||
{ title: '操作', dataIndex: 'action', width: 160, fixed: 'right' }
|
||||
]
|
||||
|
||||
// 字典弹窗
|
||||
const modalVisible = ref(false)
|
||||
const editData = ref<DictRecord | null>(null)
|
||||
|
||||
// 字典项抽屉
|
||||
const itemDrawerVisible = ref(false)
|
||||
const currentDict = ref<DictRecord | null>(null)
|
||||
|
||||
// 加载字典数据
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
// TODO: 接入真实API
|
||||
// const res = await getDictList({ ...searchForm, page: pagination.current, pageSize: pagination.pageSize })
|
||||
// tableData.value = res.data.data.records
|
||||
// pagination.total = res.data.data.total
|
||||
|
||||
// 模拟数据
|
||||
tableData.value = [
|
||||
{ id: 1, name: '性别', code: 'gender', remark: '用户性别', status: 1, createdAt: '2026-01-08 10:00:00' },
|
||||
{ id: 2, name: '状态', code: 'status', remark: '通用状态', status: 1, createdAt: '2026-01-08 10:00:00' },
|
||||
{ id: 3, name: '审批状态', code: 'approval_status', remark: '审批流程状态', status: 1, createdAt: '2026-01-08 10:00:00' }
|
||||
]
|
||||
pagination.total = 3
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.name = ''
|
||||
searchForm.code = ''
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
editData.value = null
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(record: DictRecord) {
|
||||
editData.value = record
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
function handleViewItems(record: DictRecord) {
|
||||
currentDict.value = record
|
||||
itemDrawerVisible.value = true
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
// TODO: 接入真实API
|
||||
// await deleteDict(id)
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dict-container {
|
||||
padding: 0;
|
||||
}
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
166
src/views/system/menus/index.vue
Normal file
166
src/views/system/menus/index.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 操作栏 -->
|
||||
<a-card class="table-card" :bordered="false">
|
||||
<template #title>
|
||||
<a-button type="primary" @click="handleAdd()">
|
||||
<PlusOutlined /> 新增菜单
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 表格(树形) -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="treeData"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
:default-expand-all-rows="true"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'icon'">
|
||||
<span v-if="record.icon" class="menu-icon"><component :is="record.icon" /> {{ record.icon }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'type'">
|
||||
<a-tag :color="getTypeColor(record.type)">{{ getTypeName(record.type) }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'red'">
|
||||
{{ record.status === 1 ? '正常' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'hidden'">
|
||||
<a-tag :color="record.hidden === 1 ? 'orange' : 'default'">
|
||||
{{ record.hidden === 1 ? '隐藏' : '显示' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleAdd(record.id)">添加子菜单</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record as MenuRecord)">编辑</a-button>
|
||||
<a-popconfirm title="确定删除该菜单吗?" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<MenuFormModal
|
||||
v-model:visible="modalVisible"
|
||||
:record="currentRecord"
|
||||
:menu-tree="treeData"
|
||||
@success="loadData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import type { MenuRecord, MenuFormData } from '@/types/system/menu'
|
||||
import { getMenuList, deleteMenu } from '@/api/system/menu'
|
||||
import MenuFormModal from '@/components/system/menu/MenuFormModal.vue'
|
||||
import { buildMenuTree } from '@/utils/route'
|
||||
// Note: buildMenuTree expects SysMenu, MenuRecord extends SysMenu so it's fine.
|
||||
// Using 'any' cast if strict checking fails due to optional properties.
|
||||
|
||||
const loading = ref(false)
|
||||
const treeData = ref<MenuRecord[]>([])
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{ title: '菜单名称', dataIndex: 'name', key: 'name', width: 200 },
|
||||
{ title: '编码', dataIndex: 'code', key: 'code', width: 150 },
|
||||
{ title: '图标', dataIndex: 'icon', key: 'icon', width: 120 },
|
||||
{ title: '类型', dataIndex: 'type', key: 'type', width: 100 },
|
||||
{ title: '路径', dataIndex: 'path', key: 'path' },
|
||||
{ title: '排序', dataIndex: 'sort', key: 'sort', width: 80 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 80 },
|
||||
{ title: '显示', dataIndex: 'hidden', key: 'hidden', width: 80 },
|
||||
{ title: '操作', key: 'action', width: 220, fixed: 'right' as const }
|
||||
]
|
||||
|
||||
// 弹窗相关
|
||||
const modalVisible = ref(false)
|
||||
const currentRecord = ref<MenuFormData | undefined>(undefined)
|
||||
|
||||
function getTypeColor(type: string): string {
|
||||
const colorMap: Record<string, string> = {
|
||||
directory: 'blue',
|
||||
menu: 'green',
|
||||
button: 'orange'
|
||||
}
|
||||
return colorMap[type] || 'default'
|
||||
}
|
||||
|
||||
function getTypeName(type: string): string {
|
||||
const nameMap: Record<string, string> = {
|
||||
directory: '目录',
|
||||
menu: '菜单',
|
||||
button: '按钮'
|
||||
}
|
||||
return nameMap[type] || type
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getMenuList()
|
||||
// res.data.data is the array
|
||||
const list = res.data.data || []
|
||||
// Cast to any because buildMenuTree typings might be strict about SysMenu properties
|
||||
treeData.value = buildMenuTree(list as any[]) as unknown as MenuRecord[]
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd(parentId?: number) {
|
||||
currentRecord.value = {
|
||||
parentId: parentId || 0
|
||||
} as MenuFormData
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(record: MenuRecord) {
|
||||
currentRecord.value = { ...record }
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
await deleteMenu(id)
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
199
src/views/system/roles/index.vue
Normal file
199
src/views/system/roles/index.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 操作栏 -->
|
||||
<a-card class="table-card" :bordered="false">
|
||||
<template #title>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<PlusOutlined /> 新增角色
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'red'">
|
||||
{{ record.status === 1 ? '正常' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record as RoleRecord)">编辑</a-button>
|
||||
<a-button type="link" size="small" @click="handleAssignMenus(record as RoleRecord)">分配权限</a-button>
|
||||
<a-popconfirm
|
||||
title="确定删除该角色吗?"
|
||||
@confirm="handleDelete(record.id)"
|
||||
:disabled="['super_admin', 'admin'].includes(record.code)"
|
||||
>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
:disabled="['super_admin', 'admin'].includes(record.code)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<RoleFormModal
|
||||
v-model:visible="modalVisible"
|
||||
:record="currentRecord"
|
||||
@success="loadData"
|
||||
/>
|
||||
|
||||
<!-- 分配权限弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="assignMenusVisible"
|
||||
title="分配菜单权限"
|
||||
:confirm-loading="assignMenusLoading"
|
||||
width="500px"
|
||||
@ok="handleAssignMenusOk"
|
||||
>
|
||||
<a-spin :spinning="menuTreeLoading">
|
||||
<a-tree
|
||||
v-model:checkedKeys="checkedMenuIds"
|
||||
:tree-data="menuTree"
|
||||
checkable
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
default-expand-all
|
||||
/>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
import { buildMenuTree } from '@/utils/route'
|
||||
|
||||
import { getRoleList, getRolePage, deleteRole, getRoleMenuIds, assignRoleMenus } from '@/api/system/role'
|
||||
import { getMenuList } from '@/api/system/menu'
|
||||
import type { RoleRecord, RoleFormData } from '@/types/system/role'
|
||||
import type { SysMenu } from '@/types/api/auth'
|
||||
|
||||
import RoleFormModal from '@/components/system/role/RoleFormModal.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<RoleRecord[]>([])
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{ title: '角色编码', dataIndex: 'code', key: 'code' },
|
||||
{ title: '角色名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '描述', dataIndex: 'description', key: 'description' },
|
||||
{ title: '排序', dataIndex: 'sort', key: 'sort', width: 80 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 180 },
|
||||
{ title: '操作', key: 'action', width: 220, fixed: 'right' as const }
|
||||
]
|
||||
|
||||
// 弹窗相关
|
||||
const modalVisible = ref(false)
|
||||
const currentRecord = ref<RoleFormData | undefined>(undefined)
|
||||
|
||||
// 分配权限相关
|
||||
const assignMenusVisible = ref(false)
|
||||
const assignMenusLoading = ref(false)
|
||||
const menuTreeLoading = ref(false)
|
||||
const menuTree = ref<any[]>([])
|
||||
const checkedMenuIds = ref<number[]>([])
|
||||
const currentRoleId = ref<number>()
|
||||
|
||||
// 加载数据
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getRolePage({ pageSize: 100 })
|
||||
tableData.value = res.data.data.records || []
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
currentRecord.value = undefined
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(record: RoleRecord) {
|
||||
currentRecord.value = { ...record }
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
await deleteRole(id)
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAssignMenus(record: RoleRecord) {
|
||||
currentRoleId.value = record.id
|
||||
checkedMenuIds.value = []
|
||||
assignMenusVisible.value = true
|
||||
menuTreeLoading.value = true
|
||||
|
||||
try {
|
||||
// 加载菜单树
|
||||
// 加载菜单树
|
||||
const menuRes = await getMenuList()
|
||||
menuTree.value = buildMenuTree(menuRes.data.data || [])
|
||||
|
||||
// 加载角色已有的菜单
|
||||
const roleMenuRes = await getRoleMenuIds(record.id)
|
||||
checkedMenuIds.value = roleMenuRes.data.data || []
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
} finally {
|
||||
menuTreeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAssignMenusOk() {
|
||||
if (!currentRoleId.value) return
|
||||
assignMenusLoading.value = true
|
||||
try {
|
||||
await assignRoleMenus(currentRoleId.value, checkedMenuIds.value)
|
||||
message.success('权限分配成功')
|
||||
assignMenusVisible.value = false
|
||||
} catch (error) {
|
||||
console.error('权限分配失败:', error)
|
||||
} finally {
|
||||
assignMenusLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
277
src/views/system/users/index.vue
Normal file
277
src/views/system/users/index.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 搜索栏 -->
|
||||
<a-card class="search-card" :bordered="false">
|
||||
<a-form layout="inline" :model="searchForm" @finish="handleSearch">
|
||||
<a-form-item label="关键词">
|
||||
<a-input v-model:value="searchForm.keyword" placeholder="用户名/昵称/手机号" allow-clear style="width: 200px" />
|
||||
</a-form-item>
|
||||
<a-form-item label="角色">
|
||||
<a-select v-model:value="searchForm.role" placeholder="请选择角色" allow-clear style="width: 150px">
|
||||
<a-select-option v-for="role in roleOptions" :key="role.code" :value="role.code">
|
||||
{{ role.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态">
|
||||
<a-select v-model:value="searchForm.status" placeholder="请选择状态" allow-clear style="width: 120px">
|
||||
<a-select-option :value="1">正常</a-select-option>
|
||||
<a-select-option :value="0">禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" html-type="submit">
|
||||
<SearchOutlined /> 查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<ReloadOutlined /> 重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<a-card class="table-card" :bordered="false">
|
||||
<template #title>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<PlusOutlined /> 新增用户
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'avatar'">
|
||||
<a-avatar :src="record.avatar" :size="40">
|
||||
<template #icon><UserOutlined /></template>
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'role'">
|
||||
<a-tag :color="getRoleColor(record.role)">{{ getRoleName(record.role) }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-switch
|
||||
:checked="record.status === 1"
|
||||
checked-children="正常"
|
||||
un-checked-children="禁用"
|
||||
@change="(checked: boolean) => handleStatusChange(record.id, checked ? 1 : 0)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button type="link" size="small" @click="handleResetPassword(record)">重置密码</a-button>
|
||||
<a-popconfirm title="确定删除该用户吗?" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 组件 -->
|
||||
<UserFormModal
|
||||
v-model:visible="modalVisible"
|
||||
:record="currentRecord"
|
||||
:role-options="roleOptions"
|
||||
@success="loadData"
|
||||
/>
|
||||
|
||||
<ResetPasswordModal
|
||||
v-model:visible="resetPasswordVisible"
|
||||
:user-id="currentUserId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
UserOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
import { getUserList, deleteUser, updateUserStatus } from '@/api/system/user'
|
||||
import { getRoleList } from '@/api/system/role'
|
||||
import type { UserRecord, UserFormData } from '@/types/system/user'
|
||||
import type { RoleRecord } from '@/types/system/role'
|
||||
|
||||
// 引入组件
|
||||
import UserFormModal from '@/components/system/user/UserFormModal.vue'
|
||||
import ResetPasswordModal from '@/components/system/user/ResetPasswordModal.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<UserRecord[]>([])
|
||||
const roleOptions = ref<{ code: string; name: string }[]>([]) // Role List for display/select
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
role: undefined as string | undefined,
|
||||
status: undefined as number | undefined
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
})
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{ title: '头像', dataIndex: 'avatar', key: 'avatar', width: 80 },
|
||||
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
||||
{ title: '昵称', dataIndex: 'nickname', key: 'nickname' },
|
||||
{ title: '手机号', dataIndex: 'phone', key: 'phone' },
|
||||
{ title: '邮箱', dataIndex: 'email', key: 'email' },
|
||||
{ title: '角色', dataIndex: 'role', key: 'role', width: 120 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 180 },
|
||||
{ title: '操作', key: 'action', width: 200, fixed: 'right' }
|
||||
]
|
||||
|
||||
// Modal State
|
||||
const modalVisible = ref(false)
|
||||
const currentRecord = ref<UserFormData | undefined>(undefined)
|
||||
|
||||
const resetPasswordVisible = ref(false)
|
||||
const currentUserId = ref<number>()
|
||||
|
||||
// 角色颜色
|
||||
function getRoleColor(role: string): string {
|
||||
const colorMap: Record<string, string> = {
|
||||
super_admin: 'red',
|
||||
admin: 'blue',
|
||||
finance: 'green',
|
||||
customer_service: 'orange'
|
||||
}
|
||||
return colorMap[role] || 'default'
|
||||
}
|
||||
|
||||
function getRoleName(role: string): string {
|
||||
const option = roleOptions.value.find(r => r.code === role)
|
||||
return option?.name || role
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getUserList({
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
...searchForm
|
||||
})
|
||||
tableData.value = res.data.data.records || []
|
||||
pagination.total = res.data.data.total || 0
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载角色列表
|
||||
async function loadRoles() {
|
||||
try {
|
||||
// 这里如果后端没有 list-all,我们可以用 list 且不传参(默认第一页),或者传大 pageSize
|
||||
// 假设后端支持 list 并返回 records
|
||||
const res = await getRoleList()
|
||||
// Mapping RoleRecord to Option { code, name }
|
||||
roleOptions.value = (res.data.data || []).map((r: RoleRecord) => ({ code: r.code, name: r.name }))
|
||||
} catch (error) {
|
||||
console.error('加载角色列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.role = undefined
|
||||
searchForm.status = undefined
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handleTableChange(pag: { current?: number; pageSize?: number }) {
|
||||
pagination.current = pag.current || 1
|
||||
pagination.pageSize = pag.pageSize || 10
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
currentRecord.value = undefined
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(record: UserRecord) {
|
||||
currentRecord.value = {
|
||||
...record,
|
||||
password: '' // Password usually not editable directly here, treated separately or optional
|
||||
}
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
async function handleStatusChange(id: number, status: number) {
|
||||
try {
|
||||
await updateUserStatus(id, status)
|
||||
message.success('状态更新成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('状态更新失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
await deleteUser(id)
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function handleResetPassword(record: UserRecord) {
|
||||
currentUserId.value = record.id
|
||||
resetPasswordVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRoles()
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user