init:添加了岗位管理中的岗位详情按钮,可以点击查看岗位详情

fix: 修复了角色功能,可以通过角色检索该角色的用户
This commit is contained in:
Maple 2025-07-29 17:53:31 +08:00
parent 9741192bee
commit db047b4e60
12 changed files with 337 additions and 77 deletions

View File

@ -4,6 +4,7 @@ import http from '@/utils/http'
export type * from './type'
const BASE_URL = '/system/role'
const BASE_URL_NEW = '/role'
/** @desc 查询角色列表(已废弃) */
export function listRole(query: T.RoleQuery) {
@ -72,7 +73,7 @@ export function updateRolePermission(id: string, data: any) {
/** @desc 查询角色关联用户 */
export function listRoleUser(id: string, query: T.RoleUserPageQuery) {
return http.get<PageRes<T.RoleUserResp[]>>(`${BASE_URL}/${id}/user`, query)
return http.get<PageRes<T.RoleUserResp[]>>(`${BASE_URL_NEW}/${id}/user`, query)
}
/** @desc 分配角色给用户 */
@ -87,5 +88,5 @@ export function unassignFromUsers(userRoleIds: Array<string | number>) {
/** @desc 查询角色关联用户 ID */
export function listRoleUserId(id: string) {
return http.get(`${BASE_URL}/${id}/user/id`)
return http.get(`${BASE_URL_NEW}/${id}/user`)
}

View File

@ -117,6 +117,7 @@ export const systemRoutes: RouteRecordRaw[] = [
},
],
},
//
// {
// path: '/organization/hr/salary/insurance',
// name: 'HRInsurance',
@ -153,9 +154,10 @@ export const systemRoutes: RouteRecordRaw[] = [
// name: 'HRPersonalInfo',
// component: () => import('@/views/hr/salary/insurance/personal-info/index.vue'),
// meta: { title: '个人信息', icon: 'user', hidden: false },
// }
// ]
// },
// ],
// },
//
{
path: '/organization/hr/salary/system-insurance/health-management',
name: 'HRSystemHealthManagement',
@ -932,6 +934,18 @@ export const systemRoutes: RouteRecordRaw[] = [
// }
],
},
{
path: '/user/profile',
name: 'UserProfile',
component: () => import('@/views/user/profile/index.vue'),
meta: {
title: '个人中心',
icon: 'user',
hidden: false,
sort: 100,
},
children: [],
},
{
path: '/enterprise-settings',
name: 'EnterpriseSettings',
@ -1135,7 +1149,6 @@ export const systemRoutes: RouteRecordRaw[] = [
},
],
},
{
path: '/',
redirect: '/project-management/project-template/project-aproval',
@ -1161,6 +1174,11 @@ export const constantRoutes: RouteRecordRaw[] = [
},
],
},
// {
// path: '/user/profile',
// component: () => import('@/views/user/profile/index.vue'),
// meta: { hidden: true },
// },
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/default/error/404.vue'),

View File

@ -1,4 +1,5 @@
<template>
<GiPageLayout>
<div>
<a-radio-group v-model="viewType" type="button" size="small" style="margin-bottom: 16px;">
@ -51,6 +52,7 @@
<a-link v-permission="['system:dept:update']" title="修改" @click="onUpdate(record)">修改</a-link>
<a-link
v-permission="['system:dept:delete']"
:disabled="record.children && record.children.length > 0"
status="danger"
title="删除"
@click="onDelete(record)"
@ -71,14 +73,14 @@
:collapsable="true"
:horizontal="false"
:define-menus="menus"
:expand-all="true"
:default-expand-level="999"
:props="{ id: 'deptId', parentId: 'parentId', label: 'deptName', children: 'children' }"
center
:node-add="handleAdd"
:node-delete="onDelete"
:node-edit="onUpdate"
@on-expand-all="bool => nodeExpandAll = bool"
:expanded-keys="nodeExpandedKeys"
@node-click="(node) => toggleExpand(node.deptId)"
>
</Vue3TreeOrg>
</a-dropdown>
@ -92,6 +94,7 @@
import 'vue3-tree-org/lib/vue3-tree-org.css'
import { Vue3TreeOrg } from 'vue3-tree-org'
import type { TableInstance } from '@arco-design/web-vue'
import { Message } from '@arco-design/web-vue'
import DeptAddModal from './DeptAddModal.vue'
import { type DeptQuery, type DeptResp, deleteDept, getDeptTree } from '@/apis/system/dept'
import type GiTable from '@/components/GiTable/index.vue'
@ -99,6 +102,8 @@ import { useDownload, useTable } from '@/hooks'
import { isMobile } from '@/utils'
import has from '@/utils/has'
const $message = Message
defineOptions({ name: 'SystemDept' })
const queryForm = reactive<DeptQuery>({})
@ -111,10 +116,21 @@ const {
} = useTable<DeptResp>(() => getDeptTree(queryForm), {
immediate: true,
onSuccess: () => {
// nextTick(() => {
// tableRef.value?.tableRef?.expandAll(true)
// })
//
if (isFirstLoad.value) {
// id
const collectKeys = (nodes: DeptResp[]): string[] => {
let keys: string[] = []
nodes.forEach(node => {
keys.push(node.deptId)
if (node.children && node.children.length) {
keys = keys.concat(collectKeys(node.children))
}
})
return keys
}
expandedKeys.value = collectKeys(tableData.value)
isFirstLoad.value = false
}
},
})
//
@ -126,9 +142,9 @@ const menus = [
{ name: '删除部门', command: 'delete' },
]
//
const nodeExpandAll = ref<boolean>(true)
//
const expandedKeys = ref<string[]>([])
const isFirstLoad = ref(true)
//
const searchData = (name: string) => {
const loop = (data: DeptResp[]) => {
@ -185,6 +201,13 @@ const reset = () => {
//
const onDelete = (record: DeptResp) => {
//
if (record.children && record.children.length > 0) {
//
$message.warning('该部门存在子部门,无法直接删除')
return Promise.reject()
}
return handleDelete(() => deleteDept(record.deptId), {
content: `是否确定删除部门「${record.deptName}」?`,
showModal: true,
@ -220,6 +243,20 @@ const onExpand = (expanded: boolean, record: DeptResp) => {
expandedKeys.value = expandedKeys.value.filter(k => k !== key)
}
}
// /
const toggleExpandAll = () => {
nodeExpandAll.value = !nodeExpandAll.value
}
// :
const nodeExpandedKeys = ref<string[]>([])
const toggleExpand = (deptId: string) => {
const index = nodeExpandedKeys.value.indexOf(deptId)
index > -1
? nodeExpandedKeys.value.splice(index, 1)
: nodeExpandedKeys.value.push(deptId)
}
</script>
<style scoped lang="scss">

View File

@ -27,8 +27,8 @@
<a-radio :value="0">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item field="remark" label="备注">
<a-textarea v-model="formData.remark" placeholder="请输入备注" allow-clear />
<a-form-item field="remark" label="说明">
<a-textarea v-model="formData.remark" placeholder="请输入岗位说明" allow-clear />
</a-form-item>
</a-form>
</a-modal>

View File

@ -2,61 +2,128 @@
<a-drawer
v-model:visible="visible"
title="岗位详情"
width="500px"
:footer="false"
width="620px"
unmount-on-close
class="post-detail-drawer"
@close="() => visible = false"
>
<a-descriptions
:data="detailData"
:column="1"
:align="{ label: 'right' }"
label-style="width: 120px"
size="medium"
:loading="loading"
border
>
<template #label="{ label }">{{ label }}</template>
<template #value="{ value }">
<span v-if="value !== undefined && value !== null">{{ value }}</span>
<span v-else>-</span>
<a-spin :loading="loading" class="detail-container">
<div class="detail-card">
<div class="detail-header">
<div class="post-name">{{ primaryInfo?.name || '-' }}</div>
<a-tag class="status-tag" :color="getStatusColor(primaryInfo?.status)">
{{ getStatusText(primaryInfo?.status) }}
</a-tag>
</div>
<div class="detail-group">
<div class="group-title">基本信息</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">岗位ID</div>
<div class="info-value">{{ primaryInfo?.id || '-' }}</div>
</div>
<div class="info-item">
<div class="info-label">岗位排序</div>
<div class="info-value">{{ primaryInfo?.sort || '-' }}</div>
</div>
</div>
</div>
<div v-if="primaryInfo?.remark" class="detail-group">
<div class="group-title">岗位说明</div>
<div class="remark-container">
<div class="remark-content">{{ primaryInfo.remark }}</div>
</div>
</div>
<div class="detail-group">
<div class="group-title">时间信息</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">创建时间</div>
<div class="info-value">{{ primaryInfo?.createTime || '-' }}</div>
</div>
<div class="info-item">
<div class="info-label">更新时间</div>
<div class="info-value">{{primaryInfo?.updateTime || '-' }}</div>
</div>
</div>
</div>
</div>
</a-spin>
<template #footer>
<div class="footer-actions">
<a-button type="primary" @click="handleClose">关闭详情</a-button>
</div>
</template>
</a-descriptions>
</a-drawer>
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue'
import { getPostDetail } from '@/apis/system/post'
import { useLoading } from '@/hooks'
//import { formatDate } from '@/utils/date'
defineOptions({ name: 'PostDetailDrawer' })
const visible = ref(false)
const { loading, setLoading } = useLoading()
const detailData = ref<Array<{ label: string; value: any }>>([])
const primaryInfo = ref<any>(null)
//
const getDetail = async (id: string) => {
try {
setLoading(true)
const { data } = await getPostDetail(id)
if (data) {
detailData.value = [
{ label: '岗位名称', value: data.postName },
{ label: '岗位排序', value: data.postSort },
{ label: '状态', value: Number(data.status) === 1 ? '正常' : '停用' },
{ label: '备注', value: data.remark },
{ label: '创建时间', value: data.createTime },
{ label: '更新时间', value: data.updateTime },
]
primaryInfo.value = null
const response = await getPostDetail(id)
const data = response?.data || response
if (data && typeof data === 'object') {
primaryInfo.value = {
id: data.postId ?? data.id,
name: data.postName ?? data.name,
sort: data.postSort,
status: data.status,
remark: data.remark,
createTime: data.createTime,
updateTime: data.updateTime,
}
}
} catch (error: any) {
console.error('获取岗位详情失败:', error)
} finally {
setLoading(false)
}
}
//
const getStatusText = (status: number | string) => {
if (status === 1 || status === '1') return '正常'
if (status === 0 || status === '0') return '停用'
return '未知状态'
}
//
const getStatusColor = (status: number | string) => {
if (status === 1 || status === '1') return 'rgb(82, 196, 26)'
if (status === 0 || status === '0') return 'rgb(245, 34, 45)'
return 'rgb(150, 150, 150)'
}
//
const handleClose = () => {
visible.value = false
}
//
const onDetail = async (id: string) => {
if (!id) return
visible.value = true
await nextTick()
await getDetail(id)
}
@ -64,3 +131,147 @@ defineExpose({
onDetail,
})
</script>
<style scoped>
.post-detail-drawer {
--primary-color: #3498db;
--light-bg: #f9fafc;
--border-color: #eaeaea;
--label-color: #666;
--value-color: #333;
--group-title-color: #555;
--group-bg: #f5f7fa;
}
.detail-container {
padding: 16px 24px;
}
.detail-card {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
padding: 20px;
transition: all 0.3s ease;
}
.detail-header {
display: flex;
align-items: center;
margin-bottom: 28px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border-color);
}
.post-name {
font-size: 22px;
font-weight: 600;
color: var(--value-color);
margin-right: 15px;
letter-spacing: 0.5px;
}
.status-tag {
padding: 4px 12px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
border: none;
color: white !important;
}
.detail-group {
margin-bottom: 25px;
position: relative;
background-color: var(--group-bg);
border-radius: 8px;
padding: 12px 16px;
}
.detail-group:last-child {
margin-bottom: 0;
}
.group-title {
font-size: 16px;
font-weight: 600;
color: var(--group-title-color);
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
margin-bottom: 15px;
display: flex;
align-items: center;
}
.group-title::before {
content: '';
display: inline-block;
width: 4px;
height: 16px;
background-color: var(--primary-color);
margin-right: 8px;
border-radius: 2px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 18px;
}
.info-item {
display: flex;
flex-direction: column;
background: white;
padding: 12px 15px;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
border: 1px solid var(--border-color);
transition: transform 0.2s, box-shadow 0.2s;
}
.info-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
}
.info-label {
font-size: 14px;
color: var(--label-color);
margin-bottom: 6px;
font-weight: 500;
display: flex;
align-items: center;
}
.info-label::after {
content: ':';
margin-right: 4px;
}
.info-value {
font-size: 16px;
color: var(--value-color);
font-weight: 500;
word-break: break-word;
}
.remark-container {
padding: 16px;
background: white;
border-radius: 6px;
border: 1px solid var(--border-color);
}
.remark-content {
color: var(--value-color);
line-height: 1.7;
font-size: 15px;
}
.footer-actions {
display: flex;
justify-content: flex-end;
padding: 16px 24px;
}
</style>

View File

@ -26,6 +26,7 @@
</template>
<template #action="{ record }">
<a-space>
<a-link v-permission="['system:post:query']" title="详情" @click="onDetail(record)">详情</a-link>
<a-link v-permission="['system:post:update']" title="修改" @click="onUpdate(record)">修改</a-link>
<a-link
v-permission="['system:post:delete']"
@ -128,7 +129,7 @@ const tableColumns = ref<TableColumnData[]>([
width: 100
},
{
title: '备注',
title: '说明',
dataIndex: 'remark',
minWidth: 180,
ellipsis: true,
@ -148,7 +149,7 @@ const tableColumns = ref<TableColumnData[]>([
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 160,
width: 200,
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['system:post:update', 'system:post:delete']),
},

View File

@ -17,6 +17,7 @@
import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { assignToUsers } from '@/apis/system/role'
//
const emit = defineEmits<{
(e: 'save-success'): void

View File

@ -98,21 +98,11 @@ const columns: TableInstance['columns'] = [
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '昵称',
dataIndex: 'nickname',
slotName: 'nickname',
minWidth: 130,
ellipsis: true,
tooltip: true,
fixed: !isMobile() ? 'left' : undefined,
},
{ title: '用户名', dataIndex: 'username', slotName: 'username', minWidth: 120, ellipsis: true, tooltip: true },
{ title: '状态', dataIndex: 'status', slotName: 'status', align: 'center' },
{ title: '用户名', dataIndex: 'name', slotName: 'name', minWidth: 120, ellipsis: true, tooltip: true },
{ title: '性别', dataIndex: 'gender', slotName: 'gender', align: 'center' },
{ title: '状态', dataIndex: 'status', slotName: 'status', align: 'center' },
{ title: '所属部门', dataIndex: 'deptName', minWidth: 140, ellipsis: true, tooltip: true },
{ title: '角色', dataIndex: 'roleNames', slotName: 'roleNames', minWidth: 165 },
{ title: '描述', dataIndex: 'description', minWidth: 130, ellipsis: true, tooltip: true },
{
title: '操作',
dataIndex: 'action',
@ -142,7 +132,7 @@ const onMulDelete = () => {
content: `是否确定取消分配角色给所选的${selectedKeys.value.length}个用户?`,
hideCancel: false,
onOk: async () => {
await unassignFromUsers(selectedKeys.value)
await unassignFromUsers(selectedKeys.value.map(id => String(id)))
Message.success('取消成功')
search()
},
@ -151,7 +141,7 @@ const onMulDelete = () => {
//
const onDelete = (record: RoleUserResp) => {
return handleDelete(() => unassignFromUsers([record.id]), {
return handleDelete(() => unassignFromUsers([String(record.id)]), {
content: `是否确定取消分配角色给用户「${record.nickname}(${record.username})」?`,
successTip: '取消成功',
showModal: true,

View File

@ -19,6 +19,7 @@
import RoleTree from './tree/index.vue'
import Permission from './components/Permission.vue'
import RoleUser from './components/RoleUser.vue'
import {listRoleUserId} from "@/apis";
defineOptions({ name: 'SystemRole' })

View File

@ -60,7 +60,7 @@ const save = async () => {
try {
const isInvalid = await formRef.value?.formRef?.validate()
if (isInvalid) return false
await updateUserRole({ roleIds: form.roleIds }, dataId.value)
await updateUserRole({ roleIds: form.roleIds.map(id => String(id)) }, dataId.value)
Message.success('分配成功')
emit('save-success')
return true

View File

@ -93,7 +93,7 @@ const handleSubmit = async () => {
await bindUserRole({
userId: props.userData.userId,
roleIds: selectedRoles.value
roleIds: selectedRoles.value.map(id => String(id))
})
Message.success('角色分配成功')

View File

@ -313,17 +313,17 @@ const columns: TableInstance['columns'] = [
},
{ title: '账号', dataIndex: 'account', minWidth: 140, ellipsis: true, tooltip: true },
{ title: '员工编码', dataIndex: 'userCode', minWidth: 120, ellipsis: true, tooltip: true },
{ title: '在职状态', dataIndex: 'userStatus', slotName: 'userStatus', align: 'center', width: 100 },
{ title: '员工性质', dataIndex: 'userType', slotName: 'userType', align: 'center', width: 100 },
{ title: '性别', dataIndex: 'gender', slotName: 'gender', align: 'center', width: 80 },
{ title: '所属部门', dataIndex: 'deptName', minWidth: 180, ellipsis: true, tooltip: true },
{ title: '角色', dataIndex: 'roleIds', slotName: 'roleIds', minWidth: 165 },
{ title: '手机号', dataIndex: 'mobile', minWidth: 170, ellipsis: true, tooltip: true },
{ title: '邮箱', dataIndex: 'email', minWidth: 170, ellipsis: true, tooltip: true },
{ title: '入职日期', dataIndex: 'hiredate', width: 120, ellipsis: true, tooltip: true },
{ title: '出生日期', dataIndex: 'birthdate', width: 120, ellipsis: true, tooltip: true, show: false },
{ title: '所属部门', dataIndex: 'deptName', minWidth: 180, ellipsis: true, tooltip: true },
{ title: '学历', dataIndex: 'educationLabel', width: 100, ellipsis: true, tooltip: true, show: false },
{ title: '专业', dataIndex: 'majorField', width: 120, ellipsis: true, tooltip: true, show: false },
{ title: '在职状态', dataIndex: 'userStatus', slotName: 'userStatus', align: 'center', width: 100 },
{ title: '员工性质', dataIndex: 'userType', slotName: 'userType', align: 'center', width: 100 },
{ title: '角色', dataIndex: 'roleName', slotName: 'roleName', minWidth: 185 },
{ title: '邮箱', dataIndex: 'email', slotName: 'email', minWidth: 170, ellipsis: true, tooltip: true },
{ title: '入职日期', dataIndex: 'hiredate', width: 120, ellipsis: true, tooltip: true },
{ title: '出生日期', dataIndex: 'birthdate', width: 120, ellipsis: true, tooltip: true, show: false },
{ title: '工作方向', dataIndex: 'workField', width: 120, ellipsis: true, tooltip: true, show: false },
{ title: '身份证', dataIndex: 'identityCard', width: 180, ellipsis: true, tooltip: true, show: false },
{