feat(项目日志): 项目日志&项目选择器

This commit is contained in:
baiqi 2023-09-08 15:35:55 +08:00 committed by 刘瑞斌
parent 064894fa3b
commit 509804dc06
14 changed files with 234 additions and 129 deletions

View File

@ -1,9 +1,9 @@
<template>
<a-config-provider :locale="locale">
<router-view />
<template #empty>
<!-- <template #empty>
<MsEmpty />
</template>
</template> -->
<!-- <global-setting /> -->
</a-config-provider>
</template>

View File

@ -1,5 +1,5 @@
import MSR from '@/api/http/index';
import { ProjectListUrl } from '@/api/requrls/setting/project';
import { ProjectListUrl } from '@/api/requrls/project-management/project';
import type { ProjectListItem } from '@/models/setting/project';
export function getProjectList(organizationId: string) {

View File

@ -6,13 +6,15 @@ import {
GetOrgLogListUrl,
GetOrgLogOptionsUrl,
GetOrgLogUserUrl,
GetProjectLogListUrl,
GetProjectLogUserUrl,
} from '@/api/requrls/setting/log';
import type { CommonList } from '@/models/common';
import type { LogOptions, LogItem, UserItem } from '@/models/setting/log';
import type { LogOptions, LogItem, UserItem, LogListParams } from '@/models/setting/log';
// 获取系统日志列表
export function getSystemLogList(data: any) {
export function getSystemLogList(data: LogListParams) {
return MSR.post<CommonList<LogItem>>({ url: GetSystemLogListUrl, data });
}
@ -22,12 +24,12 @@ export function getSystemLogOptions() {
}
// 获取系统日志-操作用户列表
export function getSystemLogUsers() {
return MSR.get<UserItem[]>({ url: GetSystemLogUserUrl });
export function getSystemLogUsers({ keyword }: { keyword: string }) {
return MSR.get<UserItem[]>({ url: GetSystemLogUserUrl, params: { keyword } });
}
// 获取组织日志列表
export function getOrgLogList(data: any) {
export function getOrgLogList(data: LogListParams) {
return MSR.post<CommonList<LogItem>>({ url: GetOrgLogListUrl, data });
}
@ -37,6 +39,16 @@ export function getOrgLogOptions(id: string) {
}
// 获取组织日志-操作用户列表
export function getOrgLogUsers(id: string) {
return MSR.get<UserItem[]>({ url: GetOrgLogUserUrl, params: id });
export function getOrgLogUsers({ id, keyword }: { id: string; keyword: string }) {
return MSR.get<UserItem[]>({ url: `${GetOrgLogUserUrl}/${id}`, params: { keyword } });
}
// 获取项目日志列表
export function getProjectLogList(data: LogListParams) {
return MSR.post<CommonList<LogItem>>({ url: GetProjectLogListUrl, data });
}
// 获取项目日志-操作用户列表
export function getProjectLogUsers({ id, keyword }: { id: string; keyword: string }) {
return MSR.get<UserItem[]>({ url: `${GetProjectLogUserUrl}/${id}`, params: { keyword } });
}

View File

@ -0,0 +1,3 @@
export const ProjectListUrl = '/project/list/options';
export default {};

View File

@ -5,5 +5,9 @@ export const GetSystemLogUserUrl = '/operation/log/user/list'; // 搜索操作
// 组织级别日志
export const GetOrgLogListUrl = '/organization/log/list';
export const GetOrgLogOptionsUrl = '/organization/log/get/options'; // 获取组织/项目级联下拉框选
export const GetOrgLogOptionsUrl = '/organization/log/get/options'; // 获取项目级联下拉框选
export const GetOrgLogUserUrl = '/organization/log/user/list'; // 搜索操作用户
// 项目级别日志
export const GetProjectLogListUrl = '/project/log/list';
export const GetProjectLogUserUrl = '/project/log/user/list'; // 搜索操作用户

View File

@ -1,3 +0,0 @@
export const ProjectListUrl = '/system/project/list';
export default {};

View File

@ -526,6 +526,18 @@
background-color: var(--color-text-input-border);
}
}
.ms-card-container .arco-scrollbar .arco-scrollbar-track-direction-vertical {
right: -10px;
}
.ms-card-container .arco-scrollbar .arco-scrollbar-track-direction-horizontal {
bottom: -10px;
}
.ms-base-table .arco-scrollbar .arco-scrollbar-track-direction-vertical {
right: 0;
}
.ms-base-table .arco-scrollbar .arco-scrollbar-track-direction-horizontal {
bottom: 0;
}
.arco-scrollbar-track-direction-vertical {
width: 6px;
.arco-scrollbar-thumb-bar {

View File

@ -1,19 +1,30 @@
import { watch, ref, h, defineComponent } from 'vue';
import { watch, ref, h, defineComponent, onBeforeMount } from 'vue';
import { debounce } from 'lodash-es';
import { useI18n } from '@/hooks/useI18n';
import type { SelectOptionData } from '@arco-design/web-vue';
export type ModelType = string | number | Record<string, any> | (string | number | Record<string, any>)[];
export type RemoteFieldsMap = {
id: string;
value: string;
label: string;
[key: string]: string;
};
export interface MsSearchSelectProps {
mode?: 'static' | 'remote'; // 静态模式,远程模式。默认为静态模式,需要传入 options 数据;远程模式需要传入请求函数
modelValue: ModelType;
allowClear?: boolean;
placeholder?: string;
prefix?: string;
searchKeys: string[]; // 需要搜索的 key 名,关键字会遍历这个 key 数组,然后取 item[key] 进行模糊匹配
options: SelectOptionData[];
remoteFieldsMap?: RemoteFieldsMap; // 远程模式下的结果 key 映射,例如 { value: 'id' },表示远程请求时,会将返回结果的 id 赋值到 value 字段
remoteExtraParams?: Record<string, any>; // 远程模式下的额外参数
remoteFunc?(params: Record<string, any>): Promise<any>; // 远程模式下的请求函数,返回一个 Promise
optionLabelRender?: (item: SelectOptionData) => string; // 自定义 option 的 label 渲染,返回一个 html 字符串,默认使用 item.label
optionTooltipContent?: (item: SelectOptionData) => string; // 自定义 option 的 tooltip 内容,返回一个字符串,默认使用 item.label
}
export default defineComponent(
@ -21,7 +32,8 @@ export default defineComponent(
const { t } = useI18n();
const innerValue = ref(props.modelValue);
const filterOptions = ref<SelectOptionData[]>([...props.options]);
const filterOptions = ref<SelectOptionData[]>([]);
const remoteOriginOptions = ref<SelectOptionData[]>([...props.options]);
watch(
() => props.modelValue,
(val) => {
@ -36,20 +48,45 @@ export default defineComponent(
}
);
function handleUserSearch(val: string) {
const loading = ref(false);
async function handleUserSearch(val: string) {
try {
loading.value = true;
// 如果是远程模式,则请求接口数据
if (props.mode === 'remote' && typeof props.remoteFunc === 'function') {
remoteOriginOptions.value = (await props.remoteFunc({ ...props.remoteExtraParams, keyword: val })).map(
(e: any) => {
const item = {
...e,
};
// 支持接口字段自定义映射
if (props.remoteFieldsMap) {
const map = props.remoteFieldsMap;
Object.keys(map).forEach((key) => {
item[key] = e[map[key]];
});
}
// 为了避免关键词搜索影响 label 值,这里需要开辟新字段存储 tooltip 内容
item.tooltipContent =
typeof props.optionTooltipContent === 'function' ? props.optionTooltipContent(e) : e.label;
return item;
}
);
}
if (val.trim() === '') {
filterOptions.value = [...props.options];
// 如果搜索关键字为空,则直接返回所有数据
filterOptions.value = [...remoteOriginOptions.value];
return;
}
const highlightedKeyword = `<span class="text-[rgb(var(--primary-4))]">${val}</span>`;
filterOptions.value = props.options
filterOptions.value = remoteOriginOptions.value
.map((e) => {
const item = { ...e };
let hasMatch = false;
for (let i = 0; i < props.searchKeys.length; i++) {
// 遍历传入的搜索字段
const key = props.searchKeys[i];
if (e[key].includes(val)) {
// 是否匹配
hasMatch = true;
@ -62,7 +99,21 @@ export default defineComponent(
return null;
})
.filter((e) => e) as SelectOptionData[];
} catch (error) {
console.log(error);
} finally {
loading.value = false;
}
}
const optionItemLabelRender = (item: SelectOptionData) =>
typeof props.optionLabelRender === 'function'
? h('div', { innerHTML: props.optionLabelRender(item) })
: item.label;
onBeforeMount(() => {
handleUserSearch('');
});
return () => (
<a-select
@ -71,6 +122,7 @@ export default defineComponent(
allow-clear={props.allowClear}
allow-search
filter-option={false}
loading={loading.value}
onUpdate:model-value={(value: ModelType) => emit('update:modelValue', value)}
onInputValueChange={debounce(handleUserSearch, 300)}
>
@ -78,19 +130,32 @@ export default defineComponent(
prefix: () => t(props.prefix || ''),
default: () =>
filterOptions.value.map((item) => (
<a-tooltip content={item.tooltipContent} mouse-enter-delay={500}>
<a-option key={item.id} value={item.value}>
{typeof props.optionLabelRender === 'function'
? h('div', { innerHTML: props.optionLabelRender(item) })
: item.label}
{optionItemLabelRender(item)}
</a-option>
</a-tooltip>
)),
}}
</a-select>
);
},
{
// eslint-disable-next-line vue/require-prop-types
props: ['modelValue', 'allowClear', 'placeholder', 'prefix', 'searchKeys', 'options', 'optionLabelRender'],
/* eslint-disable vue/require-prop-types */
props: [
'mode',
'modelValue',
'allowClear',
'placeholder',
'prefix',
'searchKeys',
'options',
'optionLabelRender',
'remoteFieldsMap',
'remoteExtraParams',
'remoteFunc',
'optionTooltipContent',
],
emits: ['update:modelValue'],
}
);

View File

@ -140,9 +140,6 @@
.arco-divider {
@apply mb-0;
}
.ms-card-container {
padding: 0;
}
}
.card-header {
@apply flex items-center;
@ -160,11 +157,5 @@
}
}
}
:deep(.arco-scrollbar-track-direction-vertical) {
right: -10px;
}
:deep(.arco-scrollbar-track-direction-horizontal) {
bottom: -10px;
}
}
</style>

View File

@ -24,8 +24,9 @@
<a-option
:value="project.id"
:class="project.id === appStore.getCurrentProjectId ? 'arco-select-option-selected' : ''"
>{{ project.name }}</a-option
>
{{ project.name }}
</a-option>
</a-tooltip>
</a-select>
<a-divider direction="vertical" class="mr-0" />
@ -188,7 +189,7 @@
import TopMenu from '@/components/business/ms-top-menu/index.vue';
import MessageBox from '../message-box/index.vue';
import { NOT_SHOW_PROJECT_SELECT_MODULE } from '@/router/constants';
// import { getProjectList } from '@/api/modules/setting/project';
import { getProjectList } from '@/api/modules/project-management/project';
import { useI18n } from '@/hooks/useI18n';
import type { ProjectListItem } from '@/models/setting/project';
@ -207,12 +208,12 @@
const projectList: Ref<ProjectListItem[]> = ref([]);
onBeforeMount(async () => {
// try {
// const res = await getProjectList(appStore.getCurrentOrgId);
// projectList.value = res;
// } catch (error) {
// console.log(error);
// }
try {
const res = await getProjectList(appStore.getCurrentOrgId);
projectList.value = res;
} catch (error) {
console.log(error);
}
});
const showProjectSelect = computed(() => {
@ -364,4 +365,4 @@
}
}
</style>
@/models/setting/project @/api/modules/setting/project
@/models/setting/project @/api/modules/setting/project @/api/modules/project-management/project

View File

@ -1,41 +0,0 @@
<template>
<div class="project-selection">
<a-select v-model="value" placeholder="Please select ...">
<a-option v-for="item of data" :key="item.key" :value="item" :label="item.label" />
</a-select>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
const value = ref('默认工作空间');
const data = [
{
value: '测试项目1',
label: '测试项目1',
key: '1',
},
{
value: '测试项目2',
label: '测试项目2',
key: '2',
},
{
value: '默认工作空间',
label: '默认工作空间',
key: '2',
},
];
</script>
<style lang="less" scoped>
.project-selection {
& .arco-select-view-single {
@apply border-none;
color: #323233;
background-color: var(--color-bg-1);
}
}
</style>

View File

@ -1,5 +1,36 @@
import type { RouteEnum } from '@/enums/routeEnum';
interface Sort {
[key: string]: string;
}
interface Combine {
[key: string]: any;
}
interface Filter {
[key: string]: string[];
}
export interface LogListParams {
keyword: string;
filter: Filter;
combine: Combine;
current: number;
pageSize: number;
sort: Sort;
operUser: string; // 操作人
startTime: number;
endTime: number;
projectIds: string[]; // 项目 id 集合
organizationIds: string[]; // 组织 id 集合
type: string; // 操作类型
module: string; // 操作对象
content: string; // 操作名称
level: string; // 系统/组织/项目级别
sortString: string;
}
export interface OptionsItem {
id: string;
name: string;

View File

@ -61,6 +61,7 @@ const useUserStore = defineStore('user', {
setToken(res.sessionId, res.csrfToken);
const appStore = useAppStore();
appStore.setCurrentOrgId(res.lastOrganizationId || '');
appStore.setCurrentProjectId(res.lastProjectId || '');
this.setInfo(res);
} catch (err) {
clearToken();
@ -96,9 +97,8 @@ const useUserStore = defineStore('user', {
const appStore = useAppStore();
setToken(res.sessionId, res.csrfToken);
this.setInfo(res);
if (appStore.currentOrgId === '') {
appStore.setCurrentOrgId(res.lastOrganizationId || '');
}
appStore.setCurrentProjectId(res.lastProjectId || '');
return true;
} catch (err) {
return false;

View File

@ -1,17 +1,29 @@
<template>
<MsCard simple auto-height>
<div class="filter-box">
<div class="filter-item">
<MsSearchSelect
v-model:model-value="operUser"
mode="remote"
placeholder="system.log.operatorPlaceholder"
prefix="system.log.operator"
:options="userList"
:options="[]"
:remote-func="requestFuncMap[props.mode].usersFunc"
:remote-extra-params="{ id: props.mode === 'PROJECT' ? appStore.currentProjectId : appStore.currentOrgId }"
:remote-fields-map="{
id: 'id',
value: 'value',
label: 'name',
email: 'email',
}"
:search-keys="['label', 'email']"
:option-tooltip-content="(item) => `${item.name}(${item.email})`"
:option-label-render="
(item) => `${item.label}<span class='text-[var(--color-text-2)]'>${item.email}</span>`
"
allow-clear
/>
</div>
<a-range-picker
v-model:model-value="time"
show-time
@ -30,6 +42,7 @@
</template>
</a-range-picker>
<MsCascader
v-if="props.mode !== 'PROJECT'"
v-model:model-value="operateRange"
v-model:level="level"
:options="rangeOptions"
@ -37,6 +50,7 @@
:level-top="[...MENU_LEVEL]"
:virtual-list-props="{ height: 200 }"
:loading="rangeLoading"
class="filter-item"
/>
<a-select v-model:model-value="type" class="filter-item">
<template #prefix>
@ -53,6 +67,7 @@
:placeholder="t('system.log.operateTargetPlaceholder')"
:panel-width="100"
strictly
class="filter-item"
/>
<a-input
v-model:model-value="content"
@ -64,11 +79,19 @@
{{ t('system.log.operateName') }}
</template>
</a-input>
</div>
<div v-if="props.mode === 'PROJECT'">
<a-button type="outline" @click="searchLog">{{ t('system.log.search') }}</a-button>
<a-button type="outline" class="arco-btn-outline--secondary ml-[8px]" @click="resetFilter">
{{ t('system.log.reset') }}
</a-button>
</div>
</div>
<template v-if="props.mode !== 'PROJECT'">
<a-button type="outline" @click="searchLog">{{ t('system.log.search') }}</a-button>
<a-button type="outline" class="arco-btn-outline--secondary ml-[8px]" @click="resetFilter">
{{ t('system.log.reset') }}
</a-button>
</template>
</MsCard>
<div class="log-card">
<div class="log-card-header">
@ -109,6 +132,8 @@
getOrgLogList,
getOrgLogOptions,
getOrgLogUsers,
getProjectLogList,
getProjectLogUsers,
} from '@/api/modules/setting/log';
import MsCascader from '@/components/business/ms-cascader/index.vue';
import useTableStore from '@/store/modules/ms-table';
@ -151,9 +176,9 @@
usersFunc: getOrgLogUsers,
},
[MENU_LEVEL[2]]: {
listFunc: getOrgLogList,
optionsFunc: getOrgLogOptions,
usersFunc: getOrgLogUsers,
listFunc: getProjectLogList,
optionsFunc: getOrgLogOptions, //
usersFunc: getProjectLogUsers,
},
};
@ -198,6 +223,7 @@
* 初始化操作范围级联选项
*/
async function initRangeOptions() {
if (props.mode === 'PROJECT') return;
try {
rangeLoading.value = true;
const res = await requestFuncMap[props.mode].optionsFunc(appStore.currentOrgId);
@ -418,7 +444,7 @@
title: 'system.log.time',
dataIndex: 'createTime',
fixed: 'right',
width: 170,
width: 180,
sortable: {
sortDirections: ['ascend', 'descend'],
},
@ -433,6 +459,7 @@
columns,
selectable: false,
showSelectAll: false,
size: 'default',
}
);
@ -474,7 +501,7 @@
}
onBeforeMount(() => {
initUserList();
// initUserList();
initRangeOptions();
initModuleOptions();
searchLog();
@ -487,6 +514,9 @@
margin-bottom: 16px;
gap: 16px;
.filter-item {
@apply overflow-hidden;
}
}
@media screen and (max-width: 1400px) {
.filter-box {