feat(log、post、user、project、work-report): 添加用户姓名字段并实现岗位用户导出功能
- 在API访问日志、API错误日志中新增用户姓名字段显示 - 优化日志管理页面布局样式,修复高度自适应问题 - 添加系统岗位数据导出功能及相应API接口 - 添加系统用户数据导出功能及相应API接口 - 新增通用文件下载和导出文件名生成工具函数 - 修复工作报表删除确认提示文案 - 调整项目列表默认展开状态逻辑 - 完善岗位和用户管理页面导出按钮权限控制 - 更新日志实体类型定义,增加用户昵称字段
This commit is contained in:
@@ -18,6 +18,29 @@ const POST_PREFIX = `${SYSTEM_SERVICE_PREFIX}/post`;
|
||||
const ORG_LEADER_PREFIX = `${SYSTEM_SERVICE_PREFIX}/org-leader`;
|
||||
const USER_MANAGEMENT_RELATION_PREFIX = `${SYSTEM_SERVICE_PREFIX}/user-management-relation`;
|
||||
|
||||
function appendQueryValue(query: URLSearchParams, key: string, value: unknown) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(item => appendQueryValue(query, key, item));
|
||||
return;
|
||||
}
|
||||
|
||||
query.append(key, String(value));
|
||||
}
|
||||
|
||||
function buildQuery(params: object = {}) {
|
||||
const query = new URLSearchParams();
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
appendQueryValue(query, key, value);
|
||||
});
|
||||
|
||||
return query.toString();
|
||||
}
|
||||
|
||||
function createRolePageQuery(params?: Api.SystemManage.RoleSearchParams) {
|
||||
const query = new URLSearchParams();
|
||||
|
||||
@@ -469,6 +492,17 @@ export function fetchBatchDeletePost(ids: number[]) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 导出岗位 */
|
||||
export function fetchExportPost(params: Api.SystemManage.PostSearchParams) {
|
||||
const query = buildQuery(params);
|
||||
|
||||
return request<Blob, 'blob'>({
|
||||
url: query ? `${POST_PREFIX}/export-excel?${query}` : `${POST_PREFIX}/export-excel`,
|
||||
method: 'get',
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取用户简单列表(用于用户选择下拉框) */
|
||||
export async function fetchGetUserSimpleList() {
|
||||
return request<UserSimpleResponse[]>({
|
||||
@@ -579,6 +613,17 @@ export function fetchBatchDeleteUser(ids: number[]) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 导出用户 */
|
||||
export function fetchExportUser(params: Api.SystemManage.UserSearchParams) {
|
||||
const query = buildQuery(params);
|
||||
|
||||
return request<Blob, 'blob'>({
|
||||
url: query ? `${USER_PREFIX}/export-excel?${query}` : `${USER_PREFIX}/export-excel`,
|
||||
method: 'get',
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取菜单列表 */
|
||||
export async function fetchGetMenuList(params?: Api.SystemManage.MenuSearchParams) {
|
||||
const result = await request<MenuResponse[]>({
|
||||
|
||||
2
src/typings/api/system-log.d.ts
vendored
2
src/typings/api/system-log.d.ts
vendored
@@ -77,6 +77,7 @@ declare namespace Api {
|
||||
id: string;
|
||||
traceId?: string | null;
|
||||
userId: string;
|
||||
userNickname?: string | null;
|
||||
userType: number;
|
||||
applicationName: string;
|
||||
requestMethod: string;
|
||||
@@ -114,6 +115,7 @@ declare namespace Api {
|
||||
id: string;
|
||||
traceId?: string | null;
|
||||
userId: string;
|
||||
userNickname?: string | null;
|
||||
userType: number;
|
||||
applicationName: string;
|
||||
requestMethod: string;
|
||||
|
||||
@@ -80,6 +80,7 @@ const detailSections: LogDetailSection[] = [
|
||||
{
|
||||
title: '业务上下文',
|
||||
fields: [
|
||||
{ label: '用户姓名', key: 'userNickname' },
|
||||
{ label: '用户编号', key: 'userId' },
|
||||
{ label: '操作模块', key: 'operateModule' },
|
||||
{ label: '操作名', key: 'operateName' },
|
||||
@@ -114,6 +115,13 @@ const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagi
|
||||
columns: () => [
|
||||
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||
{ prop: 'applicationName', label: '应用名', minWidth: 140, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'userNickname',
|
||||
label: '用户姓名',
|
||||
minWidth: 120,
|
||||
showOverflowTooltip: true,
|
||||
formatter: row => row.userNickname || '--'
|
||||
},
|
||||
{ prop: 'requestMethod', label: '请求方式', width: 100, align: 'center' },
|
||||
{ prop: 'requestUrl', label: '请求地址', minWidth: 220, showOverflowTooltip: true },
|
||||
{ prop: 'operateModule', label: '操作模块', minWidth: 140, showOverflowTooltip: true },
|
||||
@@ -182,7 +190,7 @@ async function handleExport() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||
<div class="h-full min-h-0 flex-col-stretch gap-16px overflow-hidden">
|
||||
<ApiAccessLogSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
||||
|
||||
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
||||
|
||||
@@ -71,6 +71,7 @@ const detailSections: LogDetailSection[] = [
|
||||
{
|
||||
title: '上下文',
|
||||
fields: [
|
||||
{ label: '用户姓名', key: 'userNickname' },
|
||||
{ label: '用户编号', key: 'userId' },
|
||||
{ label: '用户IP', key: 'userIp' },
|
||||
{ label: '浏览器UA', key: 'userAgent', type: 'multiline' },
|
||||
@@ -109,6 +110,13 @@ const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagi
|
||||
columns: () => [
|
||||
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||
{ prop: 'applicationName', label: '应用名', minWidth: 140, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'userNickname',
|
||||
label: '用户姓名',
|
||||
minWidth: 120,
|
||||
showOverflowTooltip: true,
|
||||
formatter: row => row.userNickname || '--'
|
||||
},
|
||||
{ prop: 'requestMethod', label: '请求方式', width: 100, align: 'center' },
|
||||
{ prop: 'requestUrl', label: '请求地址', minWidth: 220, showOverflowTooltip: true },
|
||||
{ prop: 'exceptionName', label: '异常名', minWidth: 180, showOverflowTooltip: true },
|
||||
@@ -174,7 +182,7 @@ async function handleExport() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||
<div class="h-full min-h-0 flex-col-stretch gap-16px overflow-hidden">
|
||||
<ApiErrorLogSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
||||
|
||||
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
||||
|
||||
@@ -67,11 +67,13 @@ watch(
|
||||
</ElCard>
|
||||
|
||||
<div v-if="activeTabMeta" class="log-management-page__content">
|
||||
<div class="log-management-page__pane">
|
||||
<KeepAlive>
|
||||
<component :is="componentMap[activeTab]" />
|
||||
<component :is="componentMap[activeTab]" class="log-management-page__tab" />
|
||||
</KeepAlive>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -157,8 +159,26 @@ watch(
|
||||
}
|
||||
|
||||
.log-management-page__content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.log-management-page__pane {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.log-management-page__tab) {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@media (width <= 1200px) {
|
||||
|
||||
@@ -187,7 +187,7 @@ async function handleExport() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||
<div class="h-full min-h-0 flex-col-stretch gap-16px overflow-hidden">
|
||||
<LoginLogSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
||||
|
||||
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
||||
|
||||
@@ -183,7 +183,7 @@ async function handleExport() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||
<div class="h-full min-h-0 flex-col-stretch gap-16px overflow-hidden">
|
||||
<OperateLogSearch
|
||||
v-model:model="searchParams"
|
||||
:user-options="userOptions"
|
||||
|
||||
@@ -331,7 +331,7 @@ async function handleSubmitReport(row: Api.WorkReport.Monthly.MonthlyReport) {
|
||||
|
||||
async function handleDelete(row: Api.WorkReport.Monthly.MonthlyReport) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除 ${formatPeriod(row)} 吗?`, '删除确认', {
|
||||
await ElMessageBox.confirm(`确认删除该报告吗?`, '删除确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消'
|
||||
|
||||
@@ -298,7 +298,7 @@ async function handleSubmitReport(row: Api.WorkReport.Project.ProjectReport) {
|
||||
|
||||
async function handleDelete(row: Api.WorkReport.Project.ProjectReport) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除 ${formatPeriod(row)} 吗?`, '删除确认', {
|
||||
await ElMessageBox.confirm(`确认删除该报告吗?`, '删除确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消'
|
||||
|
||||
@@ -420,6 +420,11 @@ async function refreshDraft() {
|
||||
|
||||
if (result.error || !result.data) return;
|
||||
|
||||
baseInfo.value = {
|
||||
...(baseInfo.value || {}),
|
||||
...result.data
|
||||
} as WorkReportRow;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
applyWeeklyEditableFields(result.data as Api.WorkReport.Weekly.WeeklyReport);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
|
||||
@@ -347,7 +347,7 @@ async function handleSubmitReport(row: Api.WorkReport.Weekly.WeeklyReport) {
|
||||
|
||||
async function handleDelete(row: Api.WorkReport.Weekly.WeeklyReport) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除 ${formatPeriod(row)} 吗?`, '删除确认', {
|
||||
await ElMessageBox.confirm(`确认删除该报告吗?`, '删除确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消'
|
||||
|
||||
@@ -45,7 +45,7 @@ const searchParams = reactive(getInitSearchParams());
|
||||
const navKey = ref<ProjectListNavKey>('active');
|
||||
const groupPage = ref<Api.Project.ProjectGroupPageResult>(createEmptyGroupPage());
|
||||
const loading = ref(false);
|
||||
const allCollapsed = ref(true);
|
||||
const allCollapsed = ref(false);
|
||||
/** 状态看板项(overview-summary items,状态机动态下发),左栏与"全部"口径派生均以此为源 */
|
||||
const statusBoardItems = ref<Api.Project.OverviewStatusItem[]>([]);
|
||||
/** "全部"口径总数(后端 total,前端不自行求和) */
|
||||
@@ -171,7 +171,7 @@ async function handleResetSearch() {
|
||||
|
||||
async function handleNavChange(key: ProjectListNavKey) {
|
||||
navKey.value = key;
|
||||
allCollapsed.value = true;
|
||||
allCollapsed.value = false;
|
||||
await Promise.all([loadOverviewData(), loadGroupPage(1)]);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ import type { TableInstance } from 'element-plus';
|
||||
import { ElButton, ElPopconfirm, ElTag } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import { useBoolean } from '@sa/hooks';
|
||||
import { fetchBatchDeletePost, fetchDeletePost, fetchGetPostPage } from '@/service/api';
|
||||
import { fetchBatchDeletePost, fetchDeletePost, fetchExportPost, fetchGetPostPage } from '@/service/api';
|
||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||
import BusinessTableActionCell from '@/components/custom/business-table-action-cell';
|
||||
import { downloadBlob, getSystemExportFileName } from '@/views/system/shared/export';
|
||||
import PostOperateDialog from './modules/post-operate-dialog.vue';
|
||||
import PostSearch from './modules/post-search.vue';
|
||||
|
||||
@@ -74,6 +75,7 @@ function getPostTypeLabel(type?: Api.SystemManage.PostType | null) {
|
||||
const searchParams = reactive(getInitSearchParams());
|
||||
const postTableRef = ref<TableInstance>();
|
||||
const postCheckedRowKeys = ref<number[]>([]);
|
||||
const exporting = ref(false);
|
||||
|
||||
const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagination } = useUIPaginatedTable({
|
||||
paginationProps: {
|
||||
@@ -222,6 +224,18 @@ async function reloadPostTable(page = searchParams.pageNo) {
|
||||
postTableRef.value?.clearSelection();
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
exporting.value = true;
|
||||
const { error, data: blob } = await fetchExportPost(searchParams);
|
||||
exporting.value = false;
|
||||
|
||||
if (error || !blob) {
|
||||
return;
|
||||
}
|
||||
|
||||
downloadBlob(blob, getSystemExportFileName('岗位数据'));
|
||||
}
|
||||
|
||||
function resetSearchParams() {
|
||||
Object.assign(searchParams, getInitSearchParams());
|
||||
reloadPostTable(1);
|
||||
@@ -255,6 +269,12 @@ function handleSubmitted(postId: number) {
|
||||
@refresh="reloadPostTable"
|
||||
>
|
||||
<template #default>
|
||||
<ElButton v-auth="'system:post:export'" plain type="primary" :loading="exporting" @click="handleExport">
|
||||
<template #icon>
|
||||
<icon-mdi-download class="text-icon" />
|
||||
</template>
|
||||
导出
|
||||
</ElButton>
|
||||
<ElButton plain type="primary" @click="openAdd">
|
||||
<template #icon>
|
||||
<icon-ic-round-plus class="text-icon" />
|
||||
|
||||
16
src/views/system/shared/export.ts
Normal file
16
src/views/system/shared/export.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export function downloadBlob(blob: Blob, fileName: string) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
link.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export function getSystemExportFileName(label: string) {
|
||||
return `${label}_${dayjs().format('YYYY-MM-DD')}.xls`;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
fetchBatchDeleteUser,
|
||||
fetchDeleteDept,
|
||||
fetchDeleteUser,
|
||||
fetchExportUser,
|
||||
fetchGetDeptList,
|
||||
fetchGetDictDataByCode,
|
||||
fetchGetPostSimpleList,
|
||||
@@ -23,6 +24,7 @@ import BusinessTableActionCell from '@/components/custom/business-table-action-c
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import { $t } from '@/locales';
|
||||
import { buildMenuTree } from '@/views/system/shared/menu-tree';
|
||||
import { downloadBlob, getSystemExportFileName } from '@/views/system/shared/export';
|
||||
import UserManagementRelation from '@/views/system/user-management-relation/index.vue';
|
||||
import UserOperateDialog from './modules/user-operate-dialog.vue';
|
||||
import UserOrgLeaderDialog from './modules/user-org-leader-dialog.vue';
|
||||
@@ -130,6 +132,7 @@ const deptLoading = ref(false);
|
||||
const userTableRef = ref<TableInstance>();
|
||||
const userCheckedRowKeys = ref<number[]>([]);
|
||||
const statusLoadingIds = ref<number[]>([]);
|
||||
const exporting = ref(false);
|
||||
const deptList = ref<Api.SystemManage.Dept[]>([]);
|
||||
const currentDeptId = ref<number | null>(null);
|
||||
const operateVisible = ref(false);
|
||||
@@ -392,6 +395,25 @@ async function reloadUserTable(page = searchParams.pageNo) {
|
||||
userTableRef.value?.clearSelection();
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
if (!currentDeptId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
exporting.value = true;
|
||||
const { error, data: blob } = await fetchExportUser({
|
||||
...searchParams,
|
||||
deptId: currentDeptId.value
|
||||
});
|
||||
exporting.value = false;
|
||||
|
||||
if (error || !blob) {
|
||||
return;
|
||||
}
|
||||
|
||||
downloadBlob(blob, getSystemExportFileName('用户数据'));
|
||||
}
|
||||
|
||||
function handleDeptSelect(nodeData: Api.SystemManage.Dept) {
|
||||
currentDeptId.value = nodeData.id;
|
||||
}
|
||||
@@ -669,6 +691,19 @@ onMounted(async () => {
|
||||
</div>
|
||||
<TableHeaderOperation v-model:columns="columnChecks" :loading="loading" @refresh="reloadUserTable">
|
||||
<template #default>
|
||||
<ElButton
|
||||
v-auth="'system:user:export'"
|
||||
plain
|
||||
type="primary"
|
||||
:loading="exporting"
|
||||
:disabled="!currentDept"
|
||||
@click="handleExport"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-mdi-download class="text-icon" />
|
||||
</template>
|
||||
导出
|
||||
</ElButton>
|
||||
<ElButton plain type="primary" :disabled="!currentDept" @click="openAdd">
|
||||
<template #icon>
|
||||
<icon-ic-round-plus class="text-icon" />
|
||||
|
||||
Reference in New Issue
Block a user