feat(项目日志): 项目日志&项目选择器
This commit is contained in:
parent
064894fa3b
commit
509804dc06
|
@ -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>
|
||||
|
|
|
@ -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) {
|
|
@ -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 } });
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export const ProjectListUrl = '/project/list/options';
|
||||
|
||||
export default {};
|
|
@ -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'; // 搜索操作用户
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export const ProjectListUrl = '/system/project/list';
|
||||
|
||||
export default {};
|
|
@ -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 {
|
||||
|
|
|
@ -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'],
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue