初始化

This commit is contained in:
zg
2026-07-15 16:13:27 +08:00
parent 6d6f559793
commit c4023c1623
276 changed files with 42622 additions and 0 deletions

View File

@@ -0,0 +1,309 @@
<template>
<div class="form-page">
<el-page-header @back="handleBack" :title="isEdit ? '编辑用户' : '新增用户'" />
<el-divider />
<el-card shadow="never" class="form-card">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<!-- ===== 基本信息 ===== -->
<el-divider content-position="left"><span class="section-label">基本信息</span></el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名" :disabled="isEdit" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="真实姓名" prop="realName">
<el-input v-model="form.realName" placeholder="请输入真实姓名" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手机号">
<el-input v-model="form.phone" placeholder="请输入手机号" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="岗位">
<el-input v-model="form.post" placeholder="请输入岗位" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="!isEdit" label="密码" prop="password">
<el-input v-model="form.password" type="password" placeholder="请输入密码" show-password />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="状态" prop="status">
<el-switch v-model="form.status" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="禁用" />
</el-form-item>
<!-- ===== 系统 & 角色绑定仅编辑模式 ===== -->
<template v-if="isEdit">
<el-divider content-position="left"><span class="section-label">系统绑定</span></el-divider>
<el-form-item label="绑定系统">
<el-checkbox-group v-model="bindSystemIds" @change="onSystemChange">
<el-checkbox
v-for="sys in allSystems"
:key="sys.systemId"
:label="sys.systemId"
>
{{ sys.systemName }}
<el-tag size="small" type="info" style="margin-left:4px">{{ sys.systemCode }}</el-tag>
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-divider content-position="left"><span class="section-label">角色分配</span></el-divider>
<p class="hint-text">先选择系统再为该用户分配该系统下的角色</p>
<el-form-item label="选择系统">
<el-select
v-model="roleSystemId"
placeholder="请选择系统"
clearable
style="width:280px"
@change="onRoleSystemChange"
>
<el-option
v-for="sys in allSystems"
:key="sys.systemId"
:label="sys.systemName + ' (' + sys.systemCode + ')'"
:value="sys.systemId"
/>
</el-select>
</el-form-item>
<el-form-item v-if="roleSystemId" label="分配角色">
<el-checkbox-group v-model="assignRoleIds">
<el-checkbox
v-for="r in currentRoleList"
:key="r.id"
:label="r.id"
>
{{ r.roleName }}
<span style="color:var(--text-muted);font-size:12px;margin-left:4px">{{ r.roleCode }}</span>
</el-checkbox>
</el-checkbox-group>
<el-empty v-if="currentRoleList.length === 0" description="该系统下暂无角色" :image-size="60" />
</el-form-item>
</template>
<!-- ===== 按钮 ===== -->
<el-divider />
<div class="form-footer">
<el-button @click="handleBack">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</el-button>
</div>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts">
import { computed, reactive, ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import request from '@/utils/request'
const route = useRoute()
const router = useRouter()
const isEdit = ref(false)
const editId = ref<string>()
const submitLoading = ref(false)
const formRef = ref<FormInstance>()
const form = reactive({
username: '',
realName: '',
email: '',
phone: '',
post: '',
password: '',
status: 1,
})
const rules = computed<FormRules>(() => ({
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
realName: [{ required: true, message: '请输入真实姓名', trigger: 'blur' }],
email: [{ required: true, message: '请输入邮箱', trigger: 'blur' }],
password: isEdit.value ? [] : [{ required: true, message: '请输入密码', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
}))
// ===== 系统绑定 =====
const allSystems = ref<any[]>([])
const bindSystemIds = ref<string[]>([])
async function fetchAllSystems() {
try {
const res = await request.get('/zgapi/v1/auth/system/list')
allSystems.value = res.data ?? []
} catch { /* ignore */ }
}
async function fetchUserSystems() {
if (!editId.value) return
try {
const res = await request.get(`/zgapi/v1/admin/user/${editId.value}/system`)
bindSystemIds.value = res.data ?? []
} catch { /* ignore */ }
}
async function onSystemChange(_val: string[]) {
// 用户取消勾选系统时,清除该系统下的角色选择
await saveSystems()
}
async function saveSystems() {
if (!editId.value) return
try {
await request.post(`/zgapi/v1/admin/user/${editId.value}/system`, bindSystemIds.value)
} catch { /* ignore */ }
}
// ===== 角色分配 =====
const roleSystemId = ref<string>()
const assignRoleIds = ref<string[]>([])
const currentRoleList = ref<any[]>([])
async function onRoleSystemChange(sysId: string | undefined) {
assignRoleIds.value = []
if (!sysId) { currentRoleList.value = []; return }
// 加载该系统下的角色列表
try {
const res = await request.post('/zgapi/v1/admin/role/page', { page: 1, pageSize: 999, systemId: sysId })
currentRoleList.value = res.data?.records ?? []
} catch {
currentRoleList.value = []
}
// 加载用户在该系统下的已有角色
if (!editId.value) return
try {
const res = await request.get(`/zgapi/v1/admin/user/${editId.value}/role/${sysId}`)
assignRoleIds.value = res.data ?? []
} catch { /* ignore */ }
}
async function saveRoles() {
if (!editId.value || !roleSystemId.value) return
try {
await request.post(`/zgapi/v1/admin/user/${editId.value}/role`, assignRoleIds.value, {
params: { systemId: roleSystemId.value }
})
ElMessage.success('角色已保存')
} catch { /* ignore */ }
}
// ===== 加载与提交 =====
async function loadData() {
const id = route.params.id as string
if (!id) { isEdit.value = false; return }
isEdit.value = true
editId.value = id
try {
const res = await request.get(`/zgapi/v1/admin/user/${editId.value}`)
const data = res.data
if (data) {
Object.assign(form, {
username: data.username ?? '',
realName: data.realName ?? '',
email: data.email ?? '',
phone: data.phone ?? '',
post: data.post ?? '',
password: '',
status: data.status ?? 1,
})
setTimeout(() => formRef.value?.clearValidate(), 0)
}
} catch {
ElMessage.error('加载用户信息失败')
router.back()
}
}
function handleBack() { router.back() }
async function handleSubmit() {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
submitLoading.value = true
try {
const payload: any = {
username: form.username,
realName: form.realName,
email: form.email,
phone: form.phone,
post: form.post,
status: form.status,
}
if (isEdit.value) {
await request.put('/zgapi/v1/admin/user', { ...payload, id: editId.value })
// 保存系统绑定
await saveSystems()
// 如果有选择角色系统,保存角色
if (roleSystemId.value) await saveRoles()
ElMessage.success('用户信息已保存')
} else {
await request.post('/zgapi/v1/admin/user', { ...payload, password: form.password })
ElMessage.success('新增成功')
}
router.back()
} catch (e: any) {
const msg = e?.response?.data?.msg || e?.message
if (msg && msg !== '请求失败') ElMessage.error(msg)
} finally {
submitLoading.value = false
}
}
onMounted(async () => {
await fetchAllSystems()
await loadData()
if (isEdit.value) {
await fetchUserSystems()
}
})
</script>
<style scoped>
.form-page { display:flex; flex-direction:column; }
:deep(.el-page-header) { margin-bottom:4px; }
:deep(.el-page-header + .el-divider) { margin:0 0 20px; }
.form-card { max-width:860px; margin:0 auto; width:100%; }
.section-label { font-size:15px; font-weight:600; color:var(--text-primary); }
.hint-text { font-size:13px; color:var(--text-muted); margin-bottom:16px; }
.form-footer { display:flex; justify-content:flex-end; gap:12px; }
@media (max-width: 768px) {
.form-card { max-width: 100%; padding: 0 8px; }
.form-footer { flex-direction: column; }
.form-footer .el-button { width: 100%; }
}
</style>

View File

@@ -0,0 +1,250 @@
<template>
<div class="page-wrapper">
<!-- 搜索区域 -->
<el-card shadow="never" class="search-card">
<template #header>
<div class="card-header-title">
<el-icon><Search /></el-icon>
<span>搜索条件</span>
</div>
</template>
<el-form :model="query" inline>
<el-form-item label="用户名">
<el-input v-model="query.username" placeholder="请输入用户名" clearable />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="query.email" placeholder="请输入邮箱" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="query.status" placeholder="全部" clearable style="width: 120px">
<el-option label="全部" value="" />
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button-group>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">重置</el-button>
</el-button-group>
</el-form-item>
</el-form>
</el-card>
<!-- 操作栏 -->
<div class="toolbar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增用户
</el-button>
</div>
<!-- 表格 -->
<el-card shadow="never">
<el-table :data="tableData" border stripe v-loading="loading" style="width: 100%">
<el-table-column prop="username" label="用户名" width="120" />
<el-table-column prop="realName" label="真实姓名" width="100" />
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
<el-table-column prop="phone" label="手机号" width="130" />
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'" effect="dark" size="small">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="lastLoginTime" label="最后登录时间" width="180">
<template #default="{ row }">
{{ row.lastLoginTime ? new Date(row.lastLoginTime).toLocaleString('zh-CN') : '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" align="center" class-name="ops-col">
<template #default="{ row }">
<div class="actions">
<el-button type="primary" size="small" plain @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" size="small" plain @click="handleDelete(row)">删除</el-button>
</div>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无用户数据" :image-size="100" />
</template>
</el-table>
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="query.page"
v-model:page-size="query.pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
background
@current-change="fetchData"
@size-change="fetchData"
/>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus } from '@element-plus/icons-vue'
import request from '@/utils/request'
/** 用户类型 */
interface UserItem {
id?: string
username: string
realName: string
email: string
phone: string
post: string
password?: string
status: number
lastLoginTime?: string
}
interface SystemItem {
id: string
systemName: string
}
interface RoleItem {
id: string
roleName: string
}
// --- 查询 ---
const query = reactive({
page: 1,
pageSize: 10,
username: '',
email: '',
status: '' as string | number,
})
const tableData = ref<UserItem[]>([])
const total = ref(0)
const loading = ref(false)
async function fetchData() {
loading.value = true
try {
const params: Record<string, unknown> = {
page: query.page,
pageSize: query.pageSize,
}
if (query.username) params.username = query.username
if (query.email) params.email = query.email
if (query.status !== '') params.status = query.status
const res = await request.post('/zgapi/v1/admin/user/page', params)
tableData.value = res.data?.records ?? []
total.value = res.data?.total ?? 0
} catch {
// ignore
} finally {
loading.value = false
}
}
function handleSearch() {
query.page = 1
fetchData()
}
function handleReset() {
query.username = ''
query.email = ''
query.status = ''
query.page = 1
fetchData()
}
// --- 新增/编辑(路由跳转到独立表单页)---
const router = useRouter()
function handleAdd() {
router.push('/admin/user/form')
}
function handleEdit(row: UserItem) {
router.push('/admin/user/form/' + row.id)
}
function handleDelete(row: UserItem) {
ElMessageBox.confirm(`确认删除用户"${row.username}"吗?`, '删除确认', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消',
}).then(async () => {
try {
await request.delete(`/zgapi/v1/admin/user/${row.id}`)
ElMessage.success('删除成功')
fetchData()
} catch {
// ignore
}
}).catch(() => {})
}
// 初始化
fetchData()
</script>
<style scoped>
.page-wrapper {
display: flex;
flex-direction: column;
gap: 16px;
}
.search-card {
border-radius: var(--radius-lg);
}
.search-card :deep(.el-card__header) {
padding: 14px 20px;
}
.card-header-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 14px;
color: var(--text-primary);
}
.toolbar {
display: flex;
justify-content: flex-start;
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
.actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
}
@media (max-width: 768px) {
.page-wrapper { gap: 10px; }
.page-toolbar { flex-direction: column; gap: 10px; }
.page-toolbar .el-select,
.page-toolbar .el-input { width: 100% !important; }
}
</style>