feat: Enhance responsive design and layout adjustments for mobile devices (#13739)

This commit is contained in:
2026-09-08 14:20:16 +08:00
committed by GitHub
parent 8eac9a1808
commit fac4aec680
37 changed files with 1313 additions and 208 deletions

View File

@@ -547,7 +547,6 @@ func (f FileOp) DownloadFileWithProcess(rawURL, dst, key string, options Downloa
partInfo, runErr = out.Stat()
}
if runErr == nil {
// Remember umask-derived permissions, but keep incomplete content private.
runErr = out.Chmod(0600)
}
if runErr == nil {

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="robots" content="noindex,nofollow" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>loading...</title>
</head>
<body>

View File

@@ -59,11 +59,14 @@ defineProps({
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
min-width: 0;
.header-left {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
.header-span {
position: relative;
@@ -72,6 +75,8 @@ defineProps({
margin-left: 18px;
display: flex;
align-items: center;
min-width: 0;
overflow-wrap: anywhere;
&::before {
position: absolute;
@@ -90,6 +95,7 @@ defineProps({
.header-right {
display: flex;
align-items: center;
min-width: 0;
}
}
@@ -97,4 +103,26 @@ defineProps({
margin-top: 20px;
}
}
@media (max-width: 767px) {
.home-card .header {
flex-wrap: wrap;
align-items: flex-start;
.header-left {
flex: 1 1 200px;
max-width: 100%;
}
.header-right {
flex: 0 1 auto;
max-width: 100%;
margin-left: auto;
}
.header-right:empty {
display: none;
}
}
}
</style>

View File

@@ -19,7 +19,7 @@
<span v-if="actions.more.length" class="fu-table-operations__action">
<el-dropdown class="fu-table-more-button" :trigger="trigger" @command="handleButtonClick">
<span class="fu-table-operations__dropdown-trigger">
<el-button class="fu-table-operations__button" link type="primary" @click.stop>
<el-button class="fu-table-operations__button" link type="primary">
{{ t('fu.table.more') }}
</el-button>
</span>
@@ -50,13 +50,19 @@ import { isOperationDisabled, isOperationVisible, type FuTableOperationButton }
defineOptions({ name: 'FuTableOperationActions' });
type DropdownTrigger = 'hover' | 'click' | 'contextmenu';
type DropdownTriggerValue = DropdownTrigger | DropdownTrigger[];
const { t } = useI18n();
const props = defineProps({
buttons: { type: Array as PropType<FuTableOperationButton[]>, default: () => [] },
row: { type: Object, required: true },
ellipsis: { type: Number, default: 2 },
extra: { type: Number, default: 0 },
trigger: { type: String, default: 'hover' },
trigger: {
type: [String, Array] as PropType<DropdownTriggerValue>,
default: 'hover',
},
dropdownStyle: { type: Object as PropType<Record<string, string> | undefined>, default: undefined },
});

View File

@@ -5,7 +5,7 @@
:row="cardRow"
:ellipsis="ellipsis"
:extra="2"
:trigger="trigger"
:trigger="resolvedTrigger"
:dropdown-style="dropdownStyle"
/>
<el-table-column
@@ -22,7 +22,7 @@
:buttons="buttons"
:row="row"
:ellipsis="ellipsis"
:trigger="trigger"
:trigger="resolvedTrigger"
:dropdown-style="dropdownStyle"
/>
</template>
@@ -31,12 +31,16 @@
<script setup lang="ts">
import { computed, type PropType } from 'vue';
import { useMediaQuery } from '@vueuse/core';
import FuTableOperationActions from './TableOperationActions.vue';
import type { FuTableOperationButton } from './shared';
defineOptions({ name: 'FuTableOperations' });
type DropdownTrigger = 'hover' | 'click' | 'contextmenu';
type DropdownTriggerValue = DropdownTrigger | DropdownTrigger[];
const normalizeWidth = (value?: string | number) => {
if (value === undefined || value === null || value === '') {
return undefined;
@@ -77,8 +81,8 @@ const props = defineProps({
default: 2,
},
trigger: {
type: String,
default: 'hover',
type: [String, Array] as PropType<DropdownTriggerValue>,
default: undefined,
},
fixed: {
type: [Boolean, String],
@@ -98,6 +102,11 @@ const props = defineProps({
},
});
const hasFinePointer = useMediaQuery('(hover: hover) and (pointer: fine)');
const resolvedTrigger = computed<DropdownTriggerValue>(
() => props.trigger ?? (hasFinePointer.value ? 'hover' : 'click'),
);
const resolvedFixed = computed(() => {
if (props.fixed !== undefined) {
return props.fixed;

View File

@@ -729,4 +729,43 @@ onBeforeUnmount(() => {
min-width: 100px;
}
}
@media (max-width: 767px) {
.table-footer-container {
flex-direction: column;
align-items: stretch;
gap: 12px;
.footer-left {
width: 100%;
max-width: 100%;
min-width: 0;
flex-shrink: 1;
flex-wrap: wrap;
&:empty {
display: none;
}
:deep(.footer-left-button) {
width: 100%;
max-width: 100%;
min-width: 0;
}
:deep(.footer-left-button .el-select) {
flex: 1 1 auto;
max-width: 100%;
min-width: 0;
}
}
}
.complex-table__pagination {
flex: 0 1 auto;
width: 100%;
max-width: 100%;
min-width: 0;
}
}
</style>

View File

@@ -72,6 +72,9 @@ const aiNotice = ref({
message: '',
});
let aiNoticeTimer: ReturnType<typeof setTimeout> | null = null;
let resizeFrame: number | undefined;
let lastResizeColumns = 0;
let lastResizeRows = 0;
const readyWatcher = watch(
() => webSocketReady.value && termReady.value,
@@ -214,6 +217,12 @@ function onClose(isKeepShow: boolean = false) {
closing = true;
stopReconnect();
window.removeEventListener('resize', changeTerminalSize);
if (resizeFrame !== undefined) {
cancelAnimationFrame(resizeFrame);
resizeFrame = undefined;
}
lastResizeColumns = 0;
lastResizeRows = 0;
clearAINotice();
webSocketReady.value = false;
try {
@@ -239,6 +248,8 @@ function onClose(isKeepShow: boolean = false) {
const initTerminal = (online: boolean = false): boolean => {
newTerm();
lastResizeColumns = 0;
lastResizeRows = 0;
if (terminalElement.value) {
term.value.open(terminalElement.value);
applyTerminalBackground(terminalStore.backgroundColor);
@@ -253,6 +264,16 @@ const initTerminal = (online: boolean = false): boolean => {
};
function changeTerminalSize() {
if (resizeFrame !== undefined) {
return;
}
resizeFrame = requestAnimationFrame(() => {
resizeFrame = undefined;
resizeTerminal();
});
}
function resizeTerminal() {
if (!terminalElement.value || !term.value) return;
if (terminalElement.value.clientWidth <= 0 || terminalElement.value.clientHeight <= 0) {
return;
@@ -261,6 +282,11 @@ function changeTerminalSize() {
fitAddon.fit();
if (isWsOpen()) {
const { cols, rows } = term.value;
if (cols === lastResizeColumns && rows === lastResizeRows) {
return;
}
lastResizeColumns = cols;
lastResizeRows = rows;
terminalSocket.value!.send(
JSON.stringify({
type: 'resize',
@@ -335,6 +361,7 @@ const showWebSocketAuthError = (message: string) => {
const runRealTerminal = () => {
webSocketReady.value = true;
changeTerminalSize();
term.value?.focus();
// a reattached shell already ran its init command
if (initCmd.value !== '' && !sessionId.value) {
@@ -443,7 +470,7 @@ const closeRealTerminal = (ev: CloseEvent) => {
}
terminalSocket.value = undefined;
if (closing || !sessionId.value) {
term.value?.write('The connection has been disconnected.');
term.value?.write('The connection has been disconnected.');
term.value?.write(ev.reason);
return;
}

View File

@@ -7,7 +7,10 @@ import echarts from '@/utils/echarts';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { themeConfig } = useGlobalStore();
const isDarkTheme = ref(false);
const PieChartRef = ref<HTMLElement>();
let mediaQuery: MediaQueryList;
let resizeObserver: ResizeObserver | undefined;
let resizeFrame: number | undefined;
const props = defineProps({
id: {
@@ -28,7 +31,20 @@ const props = defineProps({
},
});
function changeChartSize() {
echarts.getInstanceByDom(document.getElementById(props.id) as HTMLElement)?.resize();
if (resizeFrame !== undefined) {
return;
}
resizeFrame = requestAnimationFrame(() => {
resizeFrame = undefined;
resizeChart();
});
}
function resizeChart() {
if (!PieChartRef.value) {
return;
}
echarts.getInstanceByDom(PieChartRef.value)?.resize();
}
function getThemeColors() {
return {
@@ -45,14 +61,17 @@ function getThemeColors() {
}
function initChart() {
if (!PieChartRef.value) {
return;
}
if (themeConfig.value.theme === 'auto') {
isDarkTheme.value = window.matchMedia('(prefers-color-scheme: dark)').matches;
} else {
isDarkTheme.value = themeConfig.value.theme === 'dark';
}
let myChart = echarts?.getInstanceByDom(document.getElementById(props.id) as HTMLElement);
let myChart = echarts?.getInstanceByDom(PieChartRef.value);
if (myChart === null || myChart === undefined) {
myChart = echarts.init(document.getElementById(props.id) as HTMLElement);
myChart = echarts.init(PieChartRef.value);
}
let percentText = String(props.option.data).split('.');
const { primaryLight2, primaryLight1, pieBgColor, textColor, subtextColor, shadowColor, backgroundStyleColor } =
@@ -180,13 +199,24 @@ onMounted(() => {
mediaQuery.addEventListener('change', handleThemeChange);
initChart();
window.addEventListener('resize', changeChartSize);
if (PieChartRef.value) {
resizeObserver = new ResizeObserver(changeChartSize);
resizeObserver.observe(PieChartRef.value);
}
});
});
onBeforeUnmount(() => {
echarts.getInstanceByDom(document.getElementById(props.id) as HTMLElement).dispose();
if (resizeFrame !== undefined) {
cancelAnimationFrame(resizeFrame);
resizeFrame = undefined;
}
if (PieChartRef.value) {
echarts.getInstanceByDom(PieChartRef.value)?.dispose();
}
window.removeEventListener('resize', changeChartSize);
mediaQuery.removeEventListener('change', handleThemeChange);
mediaQuery?.removeEventListener('change', handleThemeChange);
resizeObserver?.disconnect();
});
</script>
<style lang="scss" scoped></style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="footer" :class="{ 'footer--mobile': isMobile }">
<div class="flex w-full flex-col gap-4 md:justify-between md:flex-row">
<div class="flex flex-wrap gap-4">
<div class="footer-content">
<div class="footer-copyright">
<a v-if="!isIntl && !isFxplay" href="https://fit2cloud.com/" target="_blank">
Copyright © 2014-{{ year }} {{ $t('commons.fit2cloud') }}
</a>
@@ -9,10 +9,8 @@
Copyright © {{ year }} {{ $t('commons.lingxia') }}
</a>
</div>
<div class="footer-actions">
<FooterNavigation />
<SystemUpgrade />
</div>
<FooterNavigation class="footer-navigation-panel" />
<SystemUpgrade class="footer-upgrade" />
</div>
</div>
</template>
@@ -37,7 +35,7 @@ const year = new Date().getFullYear();
background: var(--panel-footer-bg);
border-top: 1px solid var(--panel-footer-border);
box-sizing: border-box;
padding: 10px 20px;
padding: 10px 12px;
a {
font-size: 12px;
color: #858585;
@@ -53,20 +51,52 @@ const year = new Date().getFullYear();
}
.footer--mobile {
min-height: 108px;
min-height: 76px;
}
.footer-actions {
.footer-content {
display: flex;
width: 100%;
flex-wrap: wrap;
align-items: center;
row-gap: 8px;
justify-content: center;
gap: 8px 12px;
}
@media (max-width: 767px) {
.footer-actions {
column-gap: 8px;
justify-content: center;
.footer-copyright {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.footer-navigation-panel {
order: -1;
flex-basis: 100%;
}
.footer-upgrade {
white-space: nowrap;
}
@media (min-width: 768px) {
.footer-content {
flex-wrap: nowrap;
justify-content: flex-start;
gap: 0;
}
.footer {
padding: 10px 20px;
}
.footer-copyright {
justify-content: flex-start;
}
.footer-navigation-panel {
order: 0;
flex-basis: auto;
margin-left: auto;
}
}
</style>

View File

@@ -28,7 +28,7 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import { ref, computed, onBeforeUnmount, onMounted, watch } from 'vue';
import { RouteRecordRaw, useRoute, useRouter } from 'vue-router';
import { loadingSvg } from '@/utils/svg';
import Logo from './components/Logo.vue';
@@ -78,16 +78,12 @@ function buildRegisteredMenuList(source: RouteRecordRaw[]): RouteRecordRaw[] {
}
const screenWidth = ref(0);
const listeningWindow = () => {
window.onresize = () => {
return (() => {
screenWidth.value = document.body.clientWidth;
if (!isCollapse.value && screenWidth.value < 1200) menuStore.setCollapse();
if (isCollapse.value && screenWidth.value > 1200) menuStore.setCollapse();
})();
};
const handleWindowResize = () => {
screenWidth.value = document.body.clientWidth;
if (!isCollapse.value && screenWidth.value < 1200) menuStore.setCollapse();
if (isCollapse.value && screenWidth.value > 1200) menuStore.setCollapse();
};
listeningWindow();
window.addEventListener('resize', handleWindowResize);
const emit = defineEmits(['menuClick', 'openTask']);
const handleMenuClick = (path) => {
emit('menuClick', path);
@@ -285,12 +281,20 @@ function adjustAndCleanMenu(menuItem, list) {
}
onMounted(() => {
screenWidth.value = document.body.clientWidth;
if (!isCollapse.value && screenWidth.value < 1200) {
menuStore.setCollapse();
}
if (!menuStore.menuList || menuStore.menuList.length === 0) {
menuStore.setMenuList(buildAuthVisibleMenuList(menuList));
}
search();
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleWindowResize);
});
watch(
() => [currentNode.value, isAdmin.value, permissions.value.join('|')],
() => {

View File

@@ -3,8 +3,8 @@ import { useRoute } from 'vue-router';
import { MenuStore } from '@/store';
import { DeviceType } from '@/enums/app';
import { useGlobalStore } from '@/composables/useGlobalStore';
/** 参考 Bootstrap 的响应式设计 WIDTH = 600 */
const WIDTH = 600;
/** 与 Tailwind CSS 的 md 断点保持一致 */
const MOBILE_BREAKPOINT = 768;
/** 根据大小变化重新布局 */
export default () => {
@@ -13,16 +13,20 @@ export default () => {
const menuStore = MenuStore();
const _isMobile = () => {
const rect = document.body.getBoundingClientRect();
return rect.width - 1 < WIDTH;
return rect.width < MOBILE_BREAKPOINT;
};
const _syncLayout = () => {
const isMobileScreen = _isMobile();
globalStore.toggleDevice(isMobileScreen ? DeviceType.Mobile : DeviceType.Desktop);
if (isMobileScreen) {
menuStore.closeSidebar(true);
}
};
const _resizeHandler = () => {
if (!document.hidden) {
const isMobileScreen = _isMobile();
globalStore.toggleDevice(isMobileScreen ? DeviceType.Mobile : DeviceType.Desktop);
if (isMobileScreen) {
menuStore.closeSidebar(true);
}
_syncLayout();
}
};
@@ -40,10 +44,7 @@ export default () => {
});
onMounted(() => {
if (_isMobile()) {
globalStore.toggleDevice(DeviceType.Mobile);
menuStore.closeSidebar(true);
}
_syncLayout();
});
onBeforeUnmount(() => {

View File

@@ -233,6 +233,8 @@ onMounted(() => {
flex-direction: column;
position: relative;
height: 100vh;
height: 100dvh;
min-width: 0;
transition: margin-left 0.3s;
margin-left: var(--panel-menu-width);
background-color: var(--panel-main-bg-color-9);
@@ -241,6 +243,8 @@ onMounted(() => {
.app-main {
padding: 7px 20px;
flex: 1;
min-width: 0;
min-height: 0;
overflow: auto;
}
.app-sidebar {
@@ -309,4 +313,11 @@ onMounted(() => {
transition: none;
}
}
@media (max-width: 767px) {
.app-main {
padding-right: 12px;
padding-left: 12px;
}
}
</style>

View File

@@ -107,7 +107,7 @@ const TerminalSessionStore = defineStore('TerminalSessionStore', () => {
if (r.status !== 'fulfilled') return;
const fromLocalNode = i === 1 || node === 'local';
for (const s of r.value.data || []) {
if (s.kind !== 'local' && s.kind !== 'ssh') continue;
if (s.kind !== 'local' && s.kind !== 'ssh') continue;
// attached elsewhere = another browser tab is using it; do not steal it
if (s.attached || entries.value.some((e) => e.sessionId === s.id)) continue;
if (s.hostId > 0 && !fromLocalNode) continue; // ssh sessions are served by the local node

View File

@@ -78,7 +78,8 @@ html {
.input-help {
font-size: 12px;
word-break: keep-all;
overflow-wrap: anywhere;
word-break: break-word;
color: #adb0bc;
width: 100%;
display: inline-block;
@@ -112,8 +113,10 @@ html {
left: 45%;
transform: translate(-50%, -50%);
width: 400px;
max-width: calc(100% - 24px);
text-align: center;
font-size: 14px;
overflow-wrap: anywhere;
.bt {
margin-top: -2px;
}
@@ -313,6 +316,22 @@ html {
justify-content: flex-end;
}
@media only screen and (max-width: 767px) {
.mask-prompt {
left: 50%;
width: calc(100% - 24px);
}
.dialog-footer {
flex-wrap: wrap;
gap: 8px;
.el-button + .el-button {
margin-left: 0;
}
}
}
.monaco-editor-tree-light .el-tree-node__content:hover {
background-color: #e5eefd;
}

View File

@@ -1,11 +1,4 @@
.mobile {
.monitor-tags {
position: inherit;
top: 13px;
}
.mobile-monitor-chart {
margin-top: 20px !important;
}
.search-button {
float: none !important;
.table-button {
@@ -36,23 +29,28 @@
.router_card_button {
padding: 2px 0;
}
.el-drawer.rtl {
width: 80% !important;
}
.site-form-wrapper {
box-sizing: border-box;
width: 90% !important;
max-width: 100%;
min-width: auto !important;
.el-form-item__label {
width: auto !important;
}
}
.moblie-form {
overflow: auto;
.el-input {
width: 350px;
width: 100%;
min-width: 0;
.el-input,
.el-textarea {
width: 100%;
max-width: 100%;
}
.el-textarea__inner {
width: 350px;
width: 100%;
max-width: 100%;
}
}
@@ -78,12 +76,76 @@
}
}
@media only screen and (max-width: 768px) {
@media only screen and (max-width: 767px) {
.el-col-xs-24 {
margin-bottom: 10px;
}
}
.el-dialog {
--el-dialog-width: 80% !important;
}
@media only screen and (max-width: 1024px) {
.el-dialog:not(.is-fullscreen) {
min-width: min(420px, calc(100vw - 24px));
max-width: calc(100vw - 24px);
max-height: calc(100vh - 24px);
max-height: calc(100dvh - 24px);
display: flex;
flex-direction: column;
overflow: hidden;
.el-dialog__header,
.el-dialog__footer {
flex: 0 0 auto;
}
.el-dialog__body {
min-width: 0;
min-height: 0;
overflow: auto;
overflow-wrap: anywhere;
}
}
.el-drawer {
max-width: 100vw;
max-height: 100vh;
max-height: 100dvh;
&.rtl,
&.ltr {
min-width: min(100vw, 480px);
max-width: 100vw;
}
.el-drawer__header,
.el-drawer__footer {
flex: 0 0 auto;
min-width: 0;
}
.el-drawer__body {
min-width: 0;
min-height: 0;
overflow: auto;
overflow-wrap: anywhere;
}
}
}
@media only screen and (max-width: 767px) {
.el-dialog:not(.is-fullscreen) {
width: calc(100vw - 24px) !important;
min-width: 0;
max-width: calc(100vw - 24px);
margin: 12px auto;
}
.el-picker__popper {
max-width: calc(100vw - 24px);
.el-picker-panel {
max-width: calc(100vw - 24px);
overflow-x: auto;
}
}
}

View File

@@ -111,12 +111,12 @@
<el-tab-pane :label="$t('commons.table.port')">
<div class="mt-1.5">
<el-row :gutter="20">
<el-col :span="8">
<el-col :span="8" :xs="24">
<el-form-item :label="$t('commons.table.port')" prop="port">
<el-input v-model.number="mcpServer.port" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :span="6" :xs="24">
<el-form-item :label="$t('app.allowPort')" prop="hostIP">
<el-switch
v-model="mcpServer.hostIP"

View File

@@ -1,15 +1,15 @@
<template>
<div class="install-card">
<el-card class="e-card">
<el-row :gutter="10">
<el-col :xs="3" :sm="3" :md="3" :lg="4" :xl="3">
<el-row :gutter="10" class="install-card-row">
<el-col class="install-card-icon-col" :xs="3" :sm="3" :md="3" :lg="4" :xl="3">
<AppIcon
@open-detail="$emit('openDetail')"
:appKey="installed.appKey"
:currentNode="currentNode"
></AppIcon>
</el-col>
<el-col :xs="21" :sm="21" :md="21" :lg="20" :xl="21">
<el-col class="install-card-detail-col" :xs="21" :sm="21" :md="21" :lg="20" :xl="21">
<div class="a-detail">
<AppHeader
:installed="installed"
@@ -73,4 +73,90 @@ defineEmits([
<style scoped lang="scss">
@use '@/views/app-store/index.scss';
@media only screen and (max-width: 1023px) {
.install-card-row {
display: grid;
grid-template-columns: 87px minmax(0, 1fr);
}
.install-card-icon-col,
.install-card-detail-col {
width: auto;
max-width: none;
min-width: 0;
flex: initial;
}
}
@media only screen and (max-width: 767px) {
.install-card-row {
grid-template-columns: 74px minmax(0, 1fr);
}
.install-card-icon-col {
:deep(.el-avatar) {
max-width: 64px;
max-height: 64px;
}
}
.install-card-row .install-card-detail-col .a-detail {
min-width: 0;
:deep(.d-name .d-name-row) {
min-width: 0;
align-items: flex-start;
flex-direction: column;
}
:deep(.d-name .d-name-row .name-actions) {
width: 100%;
min-width: 0;
flex: 0 1 auto;
flex-wrap: wrap;
overflow: visible;
}
:deep(.d-name .d-name-row .name-wrap) {
min-width: min(100px, 100%);
max-width: calc(100% - 56px);
flex: 1 1 100px;
}
:deep(.d-name .d-name-row .operate-actions) {
width: 100%;
min-width: 0;
flex-shrink: 1;
justify-content: flex-start;
.el-button + .el-button {
margin-left: 0;
}
.h-button {
margin: 0 !important;
}
}
:deep(.d-description),
:deep(.d-button) {
width: 100%;
max-width: 100%;
min-width: 0;
}
:deep(.d-description .el-button) {
max-width: 100%;
height: auto;
min-height: 24px;
white-space: normal;
> span {
min-width: 0;
overflow-wrap: anywhere;
}
}
}
}
</style>

View File

@@ -32,12 +32,17 @@
<template #main>
<div>
<MainDiv :heightDiff="mode === 'upgrade' ? 280 : 300">
<el-alert type="info" :closable="false" v-if="mode === 'installed' && !isIntl">
<el-alert
class="app-install-alert"
type="info"
:closable="false"
v-if="mode === 'installed' && !isIntl"
>
<template #title>
<span class="flx-align-center">
{{ $t('app.installHelper') }}
<span class="flx-align-center app-install-helper">
<span class="app-install-helper-text">{{ $t('app.installHelper') }}</span>
<el-link
class="ml-5"
class="app-install-helper-link"
icon="Position"
@click="jumpToPath(router, '/containers/setting')"
type="primary"
@@ -565,4 +570,40 @@ onUnmounted(() => {
margin-left: 0;
}
}
.app-install-helper {
min-width: 0;
flex-wrap: wrap;
gap: 4px 8px;
}
.app-install-helper-text {
min-width: 0;
overflow-wrap: break-word;
}
.app-install-helper-link {
flex: 0 0 auto;
}
@media only screen and (max-width: 767px) {
.app-install-alert {
:deep(.el-alert__content),
:deep(.el-alert__title) {
width: 100%;
min-width: 0;
}
}
.app-install-helper {
width: 100%;
align-items: flex-start;
}
.app-install-helper-text {
flex: 0 0 100%;
width: 100%;
text-wrap: balance;
}
}
</style>

View File

@@ -39,15 +39,15 @@
</el-tag>
</template>
</CardWithHeader>
<el-row :gutter="7" class="card-interval">
<el-col :span="8">
<el-row :gutter="7" class="card-interval container-stat-row">
<el-col :span="8" :xs="24">
<CardWithHeader :header="$t('container.compose')">
<template #body>
<span class="count" @click="routerToName('Compose')">{{ countItem.composeCount }}</span>
</template>
</CardWithHeader>
</el-col>
<el-col :span="8">
<el-col :span="8" :xs="24">
<CardWithHeader :header="$t('container.composeTemplate')">
<template #body>
<span class="count" @click="routerToName('ComposeTemplate')">
@@ -56,7 +56,7 @@
</template>
</CardWithHeader>
</el-col>
<el-col :span="8">
<el-col :span="8" :xs="24">
<CardWithHeader :header="$t('container.image')">
<template #body>
<span class="count" @click="routerToName('Image')">{{ countItem.imageCount }}</span>
@@ -64,22 +64,22 @@
</CardWithHeader>
</el-col>
</el-row>
<el-row :gutter="7" class="card-interval">
<el-col :span="8">
<el-row :gutter="7" class="card-interval container-stat-row">
<el-col :span="8" :xs="24">
<CardWithHeader :header="$t('container.imageRepo')">
<template #body>
<span class="count" @click="routerToName('Repo')">{{ countItem.repoCount }}</span>
</template>
</CardWithHeader>
</el-col>
<el-col :span="8">
<el-col :span="8" :xs="24">
<CardWithHeader :header="$t('container.network')">
<template #body>
<span class="count" @click="routerToName('Network')">{{ countItem.networkCount }}</span>
</template>
</CardWithHeader>
</el-col>
<el-col :span="8">
<el-col :span="8" :xs="24">
<CardWithHeader :header="$t('container.volume')">
<template #body>
<span class="count" @click="routerToName('Volume')">{{ countItem.volumeCount }}</span>
@@ -94,10 +94,11 @@
direction="vertical"
align="center"
v-loading="usageLoading"
:column="4"
:column="isMobile ? 2 : 4"
:label-width="isMobile ? '50%' : '25%'"
class="mt-2"
>
<el-descriptions-item label-width="25%" align="center" :label="$t('container.image')">
<el-descriptions-item align="center" :label="$t('container.image')">
{{
$t('container.usage', [
computeSize2(countItem.imageUsage),
@@ -115,7 +116,7 @@
{{ $t('container.clean') }}
</el-button>
</el-descriptions-item>
<el-descriptions-item label-width="25%" align="center" :label="$t('menu.container')">
<el-descriptions-item align="center" :label="$t('menu.container')">
{{
$t('container.usage', [
computeSize2(countItem.containerUsage),
@@ -133,7 +134,7 @@
{{ $t('container.clean') }}
</el-button>
</el-descriptions-item>
<el-descriptions-item label-width="25%" align="center" :label="$t('container.localVolume')">
<el-descriptions-item align="center" :label="$t('container.localVolume')">
{{
$t('container.usage', [
computeSize2(countItem.volumeUsage),
@@ -151,7 +152,7 @@
{{ $t('container.clean') }}
</el-button>
</el-descriptions-item>
<el-descriptions-item label-width="25%" align="center" :label="$t('container.buildCache')">
<el-descriptions-item align="center" :label="$t('container.buildCache')">
{{
$t('container.usage', [
computeSize2(countItem.buildCacheUsage),
@@ -207,8 +208,10 @@ import TaskLog from '@/components/log/task/index.vue';
import { routerToName } from '@/utils/router';
import { onMounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { useGlobalStore } from '@/composables/useGlobalStore';
const taskLogRef = ref();
const { isMobile } = useGlobalStore();
const loading = ref();
const usageLoading = ref(false);
@@ -353,4 +356,10 @@ onMounted(() => {
line-height: 32px;
cursor: pointer;
}
@media (max-width: 767px) {
.container-stat-row {
row-gap: 7px;
}
}
</style>

View File

@@ -49,19 +49,22 @@
label-width="auto"
>
<el-form-item :label="$t('container.mirrors')" prop="mirrors">
<div class="w-full" v-if="form.mirrors">
<div
class="flex w-full min-w-0 flex-col items-start gap-2 md:flex-row md:gap-0"
v-if="form.mirrors"
>
<el-input
type="textarea"
:rows="5"
disabled
v-model="form.mirrors"
style="width: calc(100% - 80px)"
class="w-full min-w-0 md:flex-1"
/>
<el-button
v-permission
@click="onChangeMirrors"
icon="Setting"
class="custom-input-textarea"
class="custom-input-textarea w-full md:w-auto"
>
{{ $t('commons.button.set') }}
</el-button>
@@ -74,7 +77,7 @@
</template>
</el-input>
<span class="input-help">{{ $t('container.mirrorsHelper') }}</span>
<span class="input-help flex flx-align-center" v-if="!isFxplay">
<span class="input-help flex min-w-0 flex-wrap flx-align-center" v-if="!isFxplay">
{{ $t('container.mirrorsHelper2') }}
<el-link class="p-ml-5 text-xs" icon="Position" @click="toDoc()" type="primary">
{{ $t('firewall.quickJump') }}
@@ -82,15 +85,23 @@
</span>
</el-form-item>
<el-form-item :label="$t('container.registries')" prop="registries">
<div class="w-full" v-if="form.registries">
<div
class="flex w-full min-w-0 flex-col items-start gap-2 md:flex-row md:gap-0"
v-if="form.registries"
>
<el-input
type="textarea"
:rows="5"
disabled
v-model="form.registries"
style="width: calc(100% - 80px)"
class="w-full min-w-0 md:flex-1"
/>
<el-button v-permission @click="onChangeRegistries" icon="Setting">
<el-button
v-permission
class="w-full md:w-auto"
@click="onChangeRegistries"
icon="Setting"
>
{{ $t('commons.button.set') }}
</el-button>
</div>

View File

@@ -7,7 +7,7 @@
<el-popover
v-if="dialogData.rowData.name.length >= 15"
placement="top-start"
trigger="hover"
:trigger="hasFinePointer ? 'hover' : 'click'"
width="250"
:content="$t('cronjob.' + dialogData.rowData.type) + ' - ' + dialogData.rowData.name"
>
@@ -80,7 +80,7 @@
<LayoutContent :title="$t('cronjob.record')" :reload="true">
<template #rightToolBar>
<el-date-picker
class="mr-2.5"
class="mr-2.5 record-time-range"
@change="search(true)"
v-model="timeRangeLoad"
type="datetimerange"
@@ -257,6 +257,7 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { useMediaQuery } from '@vueuse/core';
import { Cronjob } from '@/api/interface/cronjob';
import { searchRecords, handleOnce, updateStatus, cleanRecords, stopCronjob } from '@/api/modules/cronjob';
import { dateFormat } from '@/utils/date';
@@ -269,6 +270,8 @@ import { listAppInstalled } from '@/api/modules/app';
import { shortcuts } from '@/utils/shortcuts';
import { hasBackup } from '../helper';
const hasFinePointer = useMediaQuery('(hover: hover) and (pointer: fine)');
const loading = ref();
const hasRecords = ref();
@@ -480,6 +483,7 @@ defineExpose({
<style lang="scss" scoped>
.infinite-list {
height: calc(100vh - 320px);
height: calc(100dvh - 320px);
.select-sign {
&::before {
float: left;
@@ -518,4 +522,46 @@ defineExpose({
min-width: 1200px;
}
}
@media only screen and (max-width: 1024px) {
.mainClass {
overflow: visible;
}
.mainRowClass {
min-width: 0;
flex-direction: column;
> .el-col {
flex: 0 0 100%;
width: 100%;
max-width: 100%;
}
}
}
@media only screen and (max-width: 767px) {
.infinite-list {
height: 320px;
height: clamp(220px, 38dvh, 360px);
}
.descriptionWide,
.description {
width: 100%;
min-width: 0;
}
.page-item {
float: none;
max-width: 100%;
overflow-x: auto;
}
:global(.record-time-range.el-date-editor) {
width: 100%;
max-width: 100%;
margin-right: 0;
}
}
</style>

View File

@@ -10,12 +10,17 @@
]"
/>
<el-alert v-if="!isSafety && showEntranceWarn" class="card-interval" type="warning" @close="hideEntrance">
<el-alert
v-if="!isSafety && showEntranceWarn"
class="card-interval dashboard-entrance-alert"
type="warning"
@close="hideEntrance"
>
<template #title>
<span class="flx-align-center">
<span class="flx-align-center dashboard-entrance-alert-title">
<span>{{ $t('home.entranceHelper') }}</span>
<el-link
style="font-size: 12px; margin-left: 5px"
class="dashboard-entrance-alert-link"
icon="Position"
v-if="isAdmin"
@click="jumpToPath(router, '/settings/safe')"
@@ -123,7 +128,7 @@
</el-select>
</template>
<template #body>
<div style="position: relative; margin-top: 60px">
<div class="monitor-chart-content">
<div class="monitor-tags" :style="monitorTagsStyle" v-if="chartOption === 'network'">
<el-tag>
{{ $t('monitor.up') }}: {{ computeSizeFromKBs(currentChartInfo.netBytesSent) }}
@@ -144,7 +149,7 @@
<el-tag>{{ $t('home.ioDelay') }}: {{ currentChartInfo.ioTime }} ms</el-tag>
</div>
<div v-if="chartOption === 'io'" style="margin-top: 40px" class="mobile-monitor-chart">
<div v-if="chartOption === 'io'" class="mobile-monitor-chart">
<v-charts
height="383px"
id="ioChart"
@@ -153,7 +158,7 @@
:dataZoom="true"
/>
</div>
<div v-if="chartOption === 'network'" style="margin-top: 40px" class="mobile-monitor-chart">
<div v-if="chartOption === 'network'" class="mobile-monitor-chart">
<v-charts
height="383px"
id="networkChart"
@@ -1160,6 +1165,55 @@ onBeforeUnmount(() => {
</script>
<style lang="scss" scoped>
.dashboard-entrance-alert-title {
min-width: 0;
flex-wrap: wrap;
gap: 4px 8px;
> span {
min-width: 0;
overflow-wrap: anywhere;
}
}
.dashboard-entrance-alert-link {
flex: 0 0 auto;
font-size: 12px;
}
.monitor-chart-content {
position: relative;
margin-top: 60px;
}
.mobile-monitor-chart {
margin-top: 40px;
}
@media only screen and (max-width: 767px) {
.dashboard-entrance-alert {
padding-right: 40px;
:deep(.el-alert__content),
:deep(.el-alert__title) {
width: 100%;
min-width: 0;
}
}
.dashboard-entrance-alert-title {
width: 100%;
align-items: flex-start;
> span {
flex: 0 0 100%;
width: 100%;
overflow-wrap: break-word;
text-wrap: balance;
}
}
}
@media only screen and (min-width: 992px) {
.dashboard-right {
contain: size;
@@ -1285,6 +1339,44 @@ onBeforeUnmount(() => {
gap: 10px;
}
@media only screen and (max-width: 1024px) {
.monitor-chart-content {
margin-top: 20px;
}
.monitor-chart-content .monitor-tags {
position: static;
display: grid;
width: 100%;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
margin-bottom: 12px;
:deep(.el-tag) {
width: 100%;
max-width: 100%;
height: auto;
min-height: 24px;
justify-content: center;
padding-block: 3px;
line-height: 1.3;
text-align: center;
white-space: normal;
overflow-wrap: anywhere;
}
}
.mobile-monitor-chart {
margin-top: 0;
}
}
@media only screen and (min-width: 768px) and (max-width: 1024px) {
.monitor-chart-content .monitor-tags {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
.version {
font-size: 14px;
color: #858585;

View File

@@ -5,6 +5,8 @@
:hide-after="20"
:teleported="false"
:width="320"
popper-class="dashboard-status-popover"
:trigger="hasFinePointer ? 'hover' : 'click'"
v-if="chartsOption['load']"
@hide="onCpuPopoverHide"
>
@@ -55,6 +57,8 @@
:hide-after="20"
:teleported="false"
:width="430"
popper-class="dashboard-status-popover"
:trigger="hasFinePointer ? 'hover' : 'click'"
v-if="chartsOption['cpu']"
@hide="onCpuPopoverHide"
>
@@ -161,6 +165,8 @@
:hide-after="20"
:teleported="false"
:width="480"
popper-class="dashboard-status-popover"
:trigger="hasFinePointer ? 'hover' : 'click'"
v-if="chartsOption['memory']"
@hide="onMemPopoverHide"
>
@@ -249,7 +255,13 @@
</el-col>
<template v-for="(item, index) of currentInfo.diskData" :key="index">
<el-col :xs="6" :sm="6" :md="3" :lg="3" :xl="3" align="center" v-if="isShow('disk', index)">
<el-popover :hide-after="20" :teleported="false" :width="450" v-if="chartsOption[`disk${index}`]">
<el-popover
:hide-after="20"
:teleported="false"
:width="450"
popper-class="dashboard-status-popover"
v-if="chartsOption[`disk${index}`]"
>
<el-descriptions :column="1" size="small">
<el-descriptions-item :label="$t('home.mount')">
{{ item.path }}
@@ -300,7 +312,13 @@
</template>
<template v-for="(item, index) of currentInfo.gpuData" :key="index">
<el-col :xs="6" :sm="6" :md="3" :lg="3" :xl="3" align="center" v-if="isShow('gpu', index)">
<el-popover :hide-after="20" :teleported="false" :width="450" v-if="chartsOption[`gpu${index}`]">
<el-popover
:hide-after="20"
:teleported="false"
:width="450"
popper-class="dashboard-status-popover"
v-if="chartsOption[`gpu${index}`]"
>
<el-descriptions :title="item.productName" direction="vertical" :column="3" size="small">
<el-descriptions-item :label="$t('aiTools.gpu.gpuUtil')">
{{ item.gpuUtil }}
@@ -346,7 +364,13 @@
</template>
<template v-for="(item, index) of currentInfo.npuData" :key="index">
<el-col :xs="6" :sm="6" :md="3" :lg="3" :xl="3" align="center" v-if="isShow('npu', index)">
<el-popover :hide-after="20" :teleported="false" :width="450" v-if="chartsOption[`npu${index}`]">
<el-popover
:hide-after="20"
:teleported="false"
:width="450"
popper-class="dashboard-status-popover"
v-if="chartsOption[`npu${index}`]"
>
<el-descriptions :title="item.productName" direction="vertical" :column="3" size="small">
<el-descriptions-item v-if="hasField(item.aiCore)" label="AICore(%)">
{{ item.aiCore }}
@@ -395,7 +419,13 @@
</template>
<template v-for="(item, index) of currentInfo.xpuData" :key="index">
<el-col :xs="6" :sm="6" :md="3" :lg="3" :xl="3" align="center" v-if="isShow('xpu', index)">
<el-popover :hide-after="20" :teleported="false" :width="400" v-if="chartsOption[`xpu${index}`]">
<el-popover
:hide-after="20"
:teleported="false"
:width="400"
popper-class="dashboard-status-popover"
v-if="chartsOption[`xpu${index}`]"
>
<el-descriptions :title="item.deviceName" direction="vertical" :column="3" size="small">
<el-descriptions-item v-if="hasField(item.gpuUtil)" :label="$t('aiTools.gpu.gpuUtil')">
{{ item.gpuUtil }}
@@ -449,10 +479,13 @@ import { Dashboard } from '@/api/interface/dashboard';
import { computeSize } from '@/utils/size';
import i18n from '@/lang';
import { nextTick, onBeforeUnmount, ref } from 'vue';
import { useMediaQuery } from '@vueuse/core';
import { routerToFileWithPath, routerToName } from '@/utils/router';
import { stopProcess } from '@/api/modules/process';
import { loadTopCPU, loadTopMem } from '@/api/modules/dashboard';
import { MsgSuccess } from '@/utils/message';
const hasFinePointer = useMediaQuery('(hover: hover) and (pointer: fine)');
const showMore = ref(false);
const totalCount = ref();
@@ -852,4 +885,12 @@ defineExpose({
grid-column: span 3;
}
}
@media (max-width: 767px) {
:deep(.dashboard-status-popover) {
max-width: calc(100vw - 24px);
max-height: calc(100vh - 24px);
max-height: calc(100dvh - 24px);
overflow: auto;
}
}
</style>

View File

@@ -1,10 +1,12 @@
<template>
<el-card class="shadow-sm">
<el-card class="disk-card shadow-sm">
<div class="border-b pb-4">
<div class="flex items-center space-x-4">
<div>
<h3 class="text-lg">
{{ $t('home.disk') }}{{ $t('commons.table.name') }}: {{ diskInfo.device }}
<h3 class="disk-title text-lg">
<span class="disk-title__name">
{{ $t('home.disk') }}{{ $t('commons.table.name') }}: {{ diskInfo.device }}
</span>
<el-tag size="small" type="warning" v-if="scope === 'system'">
{{ $t('disk.systemDisk') }}
</el-tag>
@@ -20,26 +22,28 @@
{{ $t('disk.unpartitionedDisk') }}
</el-tag>
</h3>
<div class="flex items-center space-x-6 text-sm">
<el-text type="info">{{ $t('container.size') }}: {{ diskInfo.size }}</el-text>
<el-text type="info">
<div class="disk-summary text-sm">
<el-text type="info" class="disk-summary__item">
{{ $t('container.size') }}: {{ diskInfo.size }}
</el-text>
<el-text type="info" class="disk-summary__item">
{{ $t('disk.partition') }}:
<span v-if="diskInfo.partitions">
{{ diskInfo.partitions?.length }}
</span>
<span v-else>0</span>
</el-text>
<el-text type="info" v-if="diskInfo.diskType" class="flex items-center">
<el-text type="info" v-if="diskInfo.diskType" class="disk-summary__item">
{{ $t('disk.diskType') }}:
<el-tag class="ml-2" size="small" type="info">{{ diskInfo.diskType }}</el-tag>
</el-text>
<el-text type="info" v-if="diskInfo.model" class="flex items-center">
<el-text type="info" v-if="diskInfo.model" class="disk-summary__item">
{{ $t('disk.model') }}:
<span class="ml-2">{{ diskInfo.model }}</span>
<span class="disk-summary__value ml-2">{{ diskInfo.model }}</span>
</el-text>
<el-text type="info" v-if="diskInfo.serial" class="flex items-center">
<el-text type="info" v-if="diskInfo.serial" class="disk-summary__item">
{{ $t('disk.serial') }}:
<span class="ml-2">{{ diskInfo.serial }}</span>
<span class="disk-summary__value ml-2">{{ diskInfo.serial }}</span>
</el-text>
<div
v-if="
@@ -63,16 +67,20 @@
</div>
</div>
<div v-if="diskInfo.partitions && diskInfo.partitions.length > 0">
<el-table :data="diskInfo.partitions" class="w-full">
<el-table-column prop="device" :label="$t('disk.partition') + $t('commons.table.name')" min-width="100">
<el-table :data="diskInfo.partitions" class="w-full" :scrollbar-always-on="isMobile">
<el-table-column
prop="device"
:label="$t('disk.partition') + $t('commons.table.name')"
:min-width="columnMinWidth(100, 120)"
>
<template #default="{ row }">
<span class="font-medium">{{ row.device.split('/').pop() }}</span>
</template>
</el-table-column>
<el-table-column prop="size" :label="$t('container.size')" min-width="40" />
<el-table-column prop="used" :label="$t('home.used')" min-width="40" />
<el-table-column prop="avail" :label="$t('home.available')" min-width="40" />
<el-table-column prop="usePercent" :label="$t('home.percent')" min-width="60">
<el-table-column prop="size" :label="$t('container.size')" :min-width="columnMinWidth(40, 80)" />
<el-table-column prop="used" :label="$t('home.used')" :min-width="columnMinWidth(40, 80)" />
<el-table-column prop="avail" :label="$t('home.available')" :min-width="columnMinWidth(40, 80)" />
<el-table-column prop="usePercent" :label="$t('home.percent')" :min-width="columnMinWidth(60, 120)">
<template #default="{ row }">
<el-progress
:percentage="row.usePercent"
@@ -82,7 +90,7 @@
/>
</template>
</el-table-column>
<el-table-column prop="mountPoint" :label="$t('disk.mountPoint')" min-width="120">
<el-table-column prop="mountPoint" :label="$t('disk.mountPoint')" :min-width="columnMinWidth(120, 160)">
<template #default="{ row }">
<span v-if="row.mountPoint != ''">
{{ row.mountPoint }}
@@ -90,7 +98,7 @@
<el-tag v-else size="small" type="warning">{{ $t('disk.unmounted') }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="filesystem" :label="$t('disk.filesystem')" min-width="80">
<el-table-column prop="filesystem" :label="$t('disk.filesystem')" :min-width="columnMinWidth(80, 100)">
<template #default="{ row }">
<el-tag size="small" type="info" v-if="row.filesystem != ''">{{ row.filesystem }}</el-tag>
</template>
@@ -117,16 +125,20 @@
<el-text v-if="scope === 'system'">{{ $t('disk.systemDiskHelper') }}</el-text>
</div>
<div v-if="diskInfo.partitions == undefined && diskInfo.mountPoint != ''">
<el-table :data="[diskInfo]" class="w-full">
<el-table-column prop="device" :label="$t('disk.partition') + $t('commons.table.name')" min-width="100">
<el-table :data="[diskInfo]" class="w-full" :scrollbar-always-on="isMobile">
<el-table-column
prop="device"
:label="$t('disk.partition') + $t('commons.table.name')"
:min-width="columnMinWidth(100, 120)"
>
<template #default="{ row }">
<span class="font-medium">{{ row.device.split('/').pop() }}</span>
</template>
</el-table-column>
<el-table-column prop="size" :label="$t('container.size')" min-width="40" />
<el-table-column prop="used" :label="$t('home.used')" min-width="40" />
<el-table-column prop="avail" :label="$t('home.available')" min-width="40" />
<el-table-column prop="usePercent" :label="$t('home.percent')" min-width="60">
<el-table-column prop="size" :label="$t('container.size')" :min-width="columnMinWidth(40, 80)" />
<el-table-column prop="used" :label="$t('home.used')" :min-width="columnMinWidth(40, 80)" />
<el-table-column prop="avail" :label="$t('home.available')" :min-width="columnMinWidth(40, 80)" />
<el-table-column prop="usePercent" :label="$t('home.percent')" :min-width="columnMinWidth(60, 120)">
<template #default="{ row }">
<el-progress
:percentage="row.usePercent"
@@ -136,7 +148,7 @@
/>
</template>
</el-table-column>
<el-table-column prop="mountPoint" :label="$t('disk.mountPoint')" min-width="120">
<el-table-column prop="mountPoint" :label="$t('disk.mountPoint')" :min-width="columnMinWidth(120, 160)">
<template #default="{ row }">
<span v-if="row.mountPoint != ''">
{{ row.mountPoint }}
@@ -144,7 +156,7 @@
<el-tag v-else size="small" type="warning">{{ $t('disk.unmounted') }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="filesystem" :label="$t('disk.filesystem')" min-width="80">
<el-table-column prop="filesystem" :label="$t('disk.filesystem')" :min-width="columnMinWidth(80, 100)">
<template #default="{ row }">
<el-tag size="small" type="info" v-if="row.filesystem != ''">{{ row.filesystem }}</el-tag>
</template>
@@ -177,8 +189,10 @@ import { Host } from '@/api/interface/host';
import i18n from '@/lang';
import { unmountDisk } from '@/api/modules/host';
import { MsgSuccess } from '@/utils/message';
import { useGlobalStore } from '@/composables/useGlobalStore';
const emit = defineEmits(['partition', 'search', 'mount']);
const { isMobile } = useGlobalStore();
defineProps({
diskInfo: {
@@ -195,6 +209,8 @@ const handlePartition = (diskInfo: Host.DiskInfo) => {
emit('partition', diskInfo);
};
const columnMinWidth = (desktop: number, mobile: number) => (isMobile.value ? mobile : desktop);
const mount = (diskInfo: Host.DiskInfo) => {
emit('mount', diskInfo);
};
@@ -217,3 +233,49 @@ const unmount = (diskInfo: Host.DiskInfo) => {
});
};
</script>
<style scoped lang="scss">
.disk-title {
display: flex;
min-width: 0;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.disk-title__name,
.disk-summary__value {
min-width: 0;
overflow-wrap: anywhere;
}
.disk-summary {
display: flex;
min-width: 0;
align-items: center;
flex-wrap: wrap;
gap: 8px 24px;
}
.disk-summary__item {
display: inline-flex;
min-width: 0;
max-width: 100%;
align-items: center;
}
@media only screen and (max-width: 767px) {
.disk-card {
--el-card-padding: 12px;
}
.disk-summary {
align-items: flex-start;
gap: 8px 12px;
}
.disk-summary__item {
flex: 1 1 140px;
}
}
</style>

View File

@@ -176,9 +176,9 @@
</el-icon>
<span class="sm:inline hidden pl-1">{{ $t('commons.button.refresh') }}</span>
</el-text>
<el-divider direction="vertical" v-if="!isMobile" class="!mx-0" />
<el-dropdown @command="handleCreate" v-if="!isMobile" trigger="click">
<el-text size="small">
<el-divider direction="vertical" class="!mx-0" />
<el-dropdown @command="handleCreate" trigger="click">
<el-text size="small" class="cursor-pointer">
{{ $t('commons.button.create') }}
<el-icon><arrow-down /></el-icon>
</el-text>
@@ -281,6 +281,69 @@
<small :title="node.label" class="tree-node-label">{{ node.label }}</small>
</template>
</span>
<span
v-if="isMobile && data.id !== 'new-dir' && data.id !== 'new-file'"
class="tree-node-mobile-actions"
@click.stop
>
<el-dropdown
trigger="click"
@command="(command) => handleMobileTreeCommand(command, data, node)"
>
<el-button
class="mobile-tree-node-action"
link
:aria-label="$t('tabs.more')"
>
<el-icon><MoreFilled /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<fu-dropdown-item
v-if="data.isDir"
v-permission
v-node-admin
command="dir"
>
<svg-icon
class="tree-context-menu__icon"
iconName="p-file-folder"
></svg-icon>
{{ $t('file.dir') }}
</fu-dropdown-item>
<fu-dropdown-item
v-if="data.isDir"
v-permission
v-node-admin
command="file"
>
<svg-icon
class="tree-context-menu__icon"
iconName="p-file-normal"
></svg-icon>
{{ $t('menu.files') }}
</fu-dropdown-item>
<el-dropdown-item command="copy">
<el-icon><CopyDocument /></el-icon>
{{ $t('file.copyDir') }}
</el-dropdown-item>
<fu-dropdown-item v-permission v-node-admin command="rename">
<el-icon><Edit /></el-icon>
{{ $t('file.rename') }}
</fu-dropdown-item>
<fu-dropdown-item
v-permission
v-node-admin
command="delete"
divided
>
<el-icon><Delete /></el-icon>
{{ $t('commons.button.delete') }}
</fu-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</template>
</el-tree-v2>
<div
@@ -504,7 +567,7 @@ import { copyText } from '@/utils/clipboard';
import { getIcon } from '@/utils/file';
import { newUUID } from '@/utils/id';
import { TreeNodeData } from 'element-plus/es/components/tree-v2/src/types';
import { CopyDocument, Delete, Edit, Refresh, Top } from '@element-plus/icons-vue';
import { CopyDocument, Delete, Edit, MoreFilled, Refresh, Top } from '@element-plus/icons-vue';
import { loadBaseDir } from '@/api/modules/setting';
import CodeTabs from './tabs/index.vue';
import FileHistoryDrawer from './history/index.vue';
@@ -1841,6 +1904,23 @@ const deleteFromContextMenu = async () => {
}
};
const handleMobileTreeCommand = (command: string, data: any, node: any) => {
treeContextMenu.data = data;
treeContextMenu.node = node;
if (command === 'dir' || command === 'file') {
return createFromContextMenu(command);
}
if (command === 'copy') {
return copyPathFromContextMenu();
}
if (command === 'rename') {
return renameFromContextMenu();
}
if (command === 'delete') {
return deleteFromContextMenu();
}
};
const currentEditingNode = ref<any>(null);
const createNewNode = (command: string) => {
@@ -2157,7 +2237,8 @@ defineExpose({ acceptParams });
.tree-node-content {
display: inline-flex;
align-items: center;
width: 100%;
flex: 1 1 auto;
width: auto;
min-width: 0;
}
@@ -2171,12 +2252,26 @@ defineExpose({ acceptParams });
}
.tree-node-label {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tree-node-mobile-actions {
flex: 0 0 32px;
margin-left: auto;
}
.mobile-tree-node-action {
width: 32px;
height: 32px;
flex: 0 0 32px;
margin: 0;
padding: 0;
}
:deep(.el-tabs) {
--el-tabs-header-height: 29px;
.el-tabs__header {
@@ -2231,7 +2326,7 @@ defineExpose({ acceptParams });
border-left: 3px solid var(--el-color-primary);
}
@media (max-width: 599px) {
@media (max-width: 767px) {
.code-header {
min-height: 44px;
padding-right: 12px !important;

View File

@@ -114,7 +114,7 @@ const onDropdownVisibleChange = (visible: boolean, currentPath: string) => {
white-space: nowrap;
}
@media (max-width: 599px) {
@media (max-width: 767px) {
.el-dropdown-link {
max-width: 112px;
}

View File

@@ -4,7 +4,7 @@
<div class="content-container__search">
<el-card>
<div :class="isMobile ? 'flx-wrap' : 'flex justify-between'">
<div class="monitor-toolbar monitor-toolbar--global">
<el-date-picker
@change="searchGlobal()"
v-model="timeRangeGlobal"
@@ -13,10 +13,10 @@
:start-placeholder="$t('commons.search.timeStart')"
:end-placeholder="$t('commons.search.timeEnd')"
:shortcuts="shortcuts"
style="max-width: 360px; width: 100%"
class="monitor-time-range"
:size="isMobile ? 'small' : 'default'"
></el-date-picker>
<TableRefresh class="float-right" @search="searchGlobal()" />
<TableRefresh class="monitor-refresh float-right" @search="searchGlobal()" />
</div>
</el-card>
</div>
@@ -24,7 +24,7 @@
<el-col :span="24">
<el-card style="overflow: inherit">
<template #header>
<div :class="isMobile ? 'flx-wrap' : 'flex justify-between'">
<div class="monitor-toolbar monitor-toolbar--card">
<span class="title">{{ $t('monitor.avgLoad') }}</span>
<el-date-picker
@change="search('load')"
@@ -34,7 +34,7 @@
:start-placeholder="$t('commons.search.timeStart')"
:end-placeholder="$t('commons.search.timeEnd')"
:shortcuts="shortcuts"
style="max-width: 360px; width: 100%"
class="monitor-time-range"
:size="isMobile ? 'small' : 'default'"
></el-date-picker>
</div>
@@ -56,7 +56,7 @@
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-card style="overflow: inherit">
<template #header>
<div :class="isMobile ? 'flx-wrap' : 'flex justify-between'">
<div class="monitor-toolbar monitor-toolbar--card">
<span class="title">CPU</span>
<el-date-picker
@change="search('cpu')"
@@ -66,7 +66,7 @@
:start-placeholder="$t('commons.search.timeStart')"
:end-placeholder="$t('commons.search.timeEnd')"
:shortcuts="shortcuts"
style="max-width: 360px; width: 100%"
class="monitor-time-range"
:size="isMobile ? 'small' : 'default'"
></el-date-picker>
</div>
@@ -86,7 +86,7 @@
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-card style="overflow: inherit">
<template #header>
<div :class="isMobile ? 'flx-wrap' : 'flex justify-between'">
<div class="monitor-toolbar monitor-toolbar--card">
<span class="title">{{ $t('monitor.memory') }}</span>
<el-date-picker
@change="search('memory')"
@@ -96,7 +96,7 @@
:start-placeholder="$t('commons.search.timeStart')"
:end-placeholder="$t('commons.search.timeEnd')"
:shortcuts="shortcuts"
style="max-width: 360px; width: 100%"
class="monitor-time-range"
:size="isMobile ? 'small' : 'default'"
></el-date-picker>
</div>
@@ -118,7 +118,7 @@
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-card style="overflow: inherit">
<template #header>
<div :class="isMobile ? 'flx-wrap' : 'flex justify-between'">
<div class="monitor-toolbar monitor-toolbar--card">
<div>
<span class="title">{{ $t('monitor.disk') }} I/O{{ $t('commons.colon') }}</span>
<el-dropdown max-height="300px">
@@ -147,7 +147,7 @@
:start-placeholder="$t('commons.search.timeStart')"
:end-placeholder="$t('commons.search.timeEnd')"
:shortcuts="shortcuts"
style="max-width: 360px; width: 100%"
class="monitor-time-range"
:size="isMobile ? 'small' : 'default'"
></el-date-picker>
</div>
@@ -167,7 +167,7 @@
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-card style="overflow: inherit">
<template #header>
<div :class="isMobile ? 'flx-wrap' : 'flex justify-between'">
<div class="monitor-toolbar monitor-toolbar--card">
<div>
<span class="title">{{ $t('monitor.network') }}{{ $t('commons.colon') }}</span>
<el-dropdown max-height="300px">
@@ -196,7 +196,7 @@
:start-placeholder="$t('commons.search.timeStart')"
:end-placeholder="$t('commons.search.timeEnd')"
:shortcuts="shortcuts"
style="max-width: 360px; width: 100%"
class="monitor-time-range"
:size="isMobile ? 'small' : 'default'"
></el-date-picker>
</div>
@@ -218,7 +218,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ref, reactive, onMounted, watch } from 'vue';
import { loadMonitor, getNetworkOptions, getIOOptions } from '@/api/modules/host';
import { computeSize, computeSizeFromKBs } from '@/utils/size';
import { dateFormatWithoutYear } from '@/utils/date';
@@ -241,7 +241,13 @@ const networkChoose = ref();
const netOptions = ref();
const ioChoose = ref();
const ioOptions = ref();
const chartsOption = ref({ loadLoadChart: null, loadCPUChart: null, loadMemoryChart: null, loadNetworkChart: null });
const chartsOption = ref<Record<string, any>>({
loadLoadChart: null,
loadCPUChart: null,
loadMemoryChart: null,
loadIOChart: null,
loadNetworkChart: null,
});
const loadingMap = reactive({
load: false,
cpu: false,
@@ -250,6 +256,62 @@ const loadingMap = reactive({
network: false,
});
const getLoadChartGrid = () =>
isMobile.value ? { left: 8, right: 8, top: 64, bottom: '20%', containLabel: true } : undefined;
const getMultiSeriesChartLegend = () =>
isMobile.value
? { show: true, type: 'scroll', orient: 'horizontal', left: 8, right: 8, top: 0, bottom: null }
: { show: true, type: 'plain', orient: 'horizontal', left: 'center', top: null, bottom: 15 };
const getTransferChartGrid = () =>
isMobile.value
? { left: 8, right: 8, top: 64, bottom: 110, containLabel: true }
: { left: getSideWidth(true), right: getSideWidth(true), bottom: '20%' };
const getSingleSeriesChartGrid = () =>
isMobile.value ? { left: 8, right: 8, top: 40, bottom: 110, containLabel: true } : undefined;
const getSingleSeriesChartLegend = () =>
isMobile.value
? { show: true, type: 'plain', top: 0, bottom: null, left: 'center', right: null }
: { show: true, type: 'plain', top: null, bottom: 15, left: 'center' };
const getMobileChartXAxis = () =>
isMobile.value ? { axisLabel: { interval: 'auto', hideOverlap: true, fontSize: 10 } } : undefined;
watch(isMobile, () => {
const loadOption = chartsOption.value.loadLoadChart;
if (loadOption) {
chartsOption.value.loadLoadChart = {
...loadOption,
grid: getLoadChartGrid(),
legend: getMultiSeriesChartLegend(),
};
}
for (const chartKey of ['loadCPUChart', 'loadMemoryChart']) {
const chartOption = chartsOption.value[chartKey];
if (!chartOption) {
continue;
}
chartsOption.value[chartKey] = {
...chartOption,
grid: getSingleSeriesChartGrid(),
legend: getSingleSeriesChartLegend(),
xAxis: getMobileChartXAxis(),
};
}
for (const chartKey of ['loadIOChart', 'loadNetworkChart']) {
const chartOption = chartsOption.value[chartKey];
if (!chartOption) {
continue;
}
chartsOption.value[chartKey] = {
...chartOption,
grid: getTransferChartGrid(),
legend: getMultiSeriesChartLegend(),
xAxis: getMobileChartXAxis(),
};
}
});
const searchTime = ref();
const searchInfo = reactive<Host.MonitorSearch>({
param: '',
@@ -443,7 +505,8 @@ function initLoadCharts(item: Host.MonitorData) {
alignTicks: true,
},
],
grid: isMobile.value ? { left: '15%', right: '15%', bottom: '20%' } : null,
grid: getLoadChartGrid(),
legend: getMultiSeriesChartLegend(),
tooltip: {
trigger: 'axis',
formatter: function (datas: any) {
@@ -472,7 +535,9 @@ function initCPUCharts(baseDate: any, items: Host.MonitorData) {
return withCPUProcess(datas);
},
},
grid: getSingleSeriesChartGrid(),
legend: getSingleSeriesChartLegend(),
xAxis: getMobileChartXAxis(),
formatStr: '%',
};
}
@@ -496,7 +561,9 @@ function initMemCharts(baseDate: any, items: Host.MonitorData) {
return withMemProcess(datas);
},
},
grid: getSingleSeriesChartGrid(),
legend: getSingleSeriesChartLegend(),
xAxis: getMobileChartXAxis(),
formatStr: '%',
};
}
@@ -537,11 +604,9 @@ function initNetCharts(item: Host.MonitorData) {
return res;
},
},
grid: {
left: getSideWidth(true),
right: getSideWidth(true),
bottom: '20%',
},
grid: getTransferChartGrid(),
legend: getMultiSeriesChartLegend(),
xAxis: getMobileChartXAxis(),
formatStr: 'KB/s',
};
}
@@ -610,7 +675,9 @@ function initIOCharts(item: Host.MonitorData) {
return res;
},
},
grid: { left: getSideWidth(true), right: getSideWidth(true), bottom: '20%' },
grid: getTransferChartGrid(),
legend: getMultiSeriesChartLegend(),
xAxis: getMobileChartXAxis(),
yAxis: [
{ type: 'value', name: '( KB/s )', axisLabel: { fontSize: 10 } },
{
@@ -777,8 +844,54 @@ onMounted(() => {
.chart {
width: 100%;
height: 400px;
min-width: 0;
}
.el-dropdown {
vertical-align: baseline;
}
.monitor-toolbar {
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 12px;
> :first-child {
min-width: 0;
}
}
.monitor-toolbar :deep(.monitor-time-range) {
width: 100%;
min-width: 0;
max-width: 360px;
flex: 0 1 360px;
}
@media only screen and (max-width: 1024px) {
.monitor-toolbar--card {
flex-wrap: wrap;
:deep(.monitor-time-range) {
max-width: none;
flex-basis: 100%;
}
}
}
@media only screen and (max-width: 767px) {
.monitor-toolbar--global {
flex-wrap: wrap;
:deep(.monitor-time-range) {
max-width: none;
flex-basis: 100%;
}
:deep(.monitor-refresh) {
margin-left: auto;
}
}
}
</style>

View File

@@ -3,7 +3,7 @@
<FireRouter />
<LayoutContent :title="$t('menu.network', 2)" v-loading="processStore.netLoading">
<template #rightToolBar>
<div class="w-full flex justify-end items-center gap-5">
<div class="network-toolbar w-full flex justify-end items-center gap-5">
<el-select
v-model="filters"
:placeholder="$t('commons.table.status')"
@@ -13,7 +13,7 @@
collapse-tags-tooltip
:max-collapse-tags="2"
@change="search()"
class="p-w-300"
class="network-toolbar__filter"
>
<el-option
v-for="item in statusOptions"
@@ -23,16 +23,19 @@
/>
</el-select>
<TableSearch
class="network-toolbar__field"
@search="search()"
:placeholder="$t('process.pid')"
v-model:searchName="processStore.netSearch.processID"
/>
<TableSearch
class="network-toolbar__field"
@search="search()"
:placeholder="$t('process.processName')"
v-model:searchName="processStore.netSearch.processName"
/>
<TableSearch
class="network-toolbar__field"
@search="search()"
:placeholder="$t('commons.table.port')"
v-model:searchName="processStore.netSearch.port"
@@ -41,14 +44,16 @@
</template>
<template #main>
<div class="!h-[900px]">
<div class="network-table">
<el-auto-resizer>
<template #default="{ height, width }">
<el-table-v2
:fixed="isCompactTable"
:columns="columns"
:data="data"
:width="width"
:height="height"
:scrollbar-always-on="isCompactTable"
:sort-by="sortState"
@column-sort="changeSort"
/>
@@ -68,6 +73,7 @@ import { SortBy, TableV2SortOrder, ElIcon } from 'element-plus';
import { Filter } from '@element-plus/icons-vue';
import i18n from '@/lang';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { useMediaQuery } from '@vueuse/core';
const statusOptions = [
{ text: 'LISTEN', value: 'LISTEN' },
@@ -78,6 +84,7 @@ const statusOptions = [
];
const { currentNode } = useGlobalStore();
const isCompactTable = useMediaQuery('(max-width: 1024px)');
const processStore = ProcessStore();
const quickSearchName = (name: string) => {
@@ -155,8 +162,8 @@ const columns = ref([
const addr = rowData.localaddr;
const addrStr = addr?.ip ? `${addr.ip}${addr.port > 0 ? ':' + addr.port : ''}` : '';
const hasPort = addr?.port > 0;
return h('div', { class: 'flex items-center gap-1' }, [
h('span', {}, addrStr),
return h('div', { class: 'flex items-center gap-1 min-w-0' }, [
h('span', { class: 'truncate', title: addrStr }, addrStr),
hasPort
? h(
ElIcon,
@@ -181,7 +188,8 @@ const columns = ref([
width: 350,
cellRenderer: ({ rowData }) => {
const addr = rowData.remoteaddr;
return addr?.ip ? `${addr.ip}${addr.port > 0 ? ':' + addr.port : ''}` : '';
const addrStr = addr?.ip ? `${addr.ip}${addr.port > 0 ? ':' + addr.port : ''}` : '';
return h('span', { class: 'truncate', title: addrStr }, addrStr);
},
},
{
@@ -247,3 +255,56 @@ onUnmounted(() => {
processStore.disconnect();
});
</script>
<style scoped lang="scss">
.network-toolbar {
min-width: 0;
}
.network-toolbar__filter {
width: 300px;
}
.network-table {
width: 100%;
min-width: 0;
height: 900px;
overflow: hidden;
}
@media only screen and (max-width: 1024px) {
.network-toolbar {
flex-wrap: wrap;
justify-content: flex-start;
gap: 12px;
}
.network-toolbar__filter,
.network-toolbar__field {
width: auto;
min-width: 0;
max-width: 300px;
flex: 1 1 220px;
}
.network-toolbar__field {
:deep(.search-button) {
width: 100%;
}
}
.network-table {
height: clamp(420px, calc(100vh - 260px), 900px);
height: clamp(420px, calc(100dvh - 260px), 900px);
}
}
@media only screen and (max-width: 767px) {
.network-toolbar__filter,
.network-toolbar__field {
width: 100%;
max-width: none;
flex-basis: 100%;
}
}
</style>

View File

@@ -3,13 +3,13 @@
<FireRouter />
<LayoutContent :title="$t('menu.process', 2)" v-loading="processStore.psLoading">
<template #rightToolBar>
<div class="w-full flex justify-end items-center gap-5">
<div class="process-toolbar w-full flex justify-end items-center gap-5">
<el-select
v-model="filters"
:placeholder="$t('commons.table.status')"
clearable
@change="search()"
class="p-w-300"
class="process-toolbar__filter"
multiple
collapse-tags
collapse-tags-tooltip
@@ -23,16 +23,19 @@
/>
</el-select>
<TableSearch
class="process-toolbar__field"
@search="search()"
:placeholder="$t('process.pid')"
v-model:searchName="processStore.psSearch.pid"
/>
<TableSearch
class="process-toolbar__field"
@search="search()"
:placeholder="$t('commons.table.name')"
v-model:searchName="processStore.psSearch.name"
/>
<TableSearch
class="process-toolbar__field"
@search="search()"
:placeholder="$t('commons.table.user')"
v-model:searchName="processStore.psSearch.username"
@@ -40,15 +43,17 @@
</div>
</template>
<template #main>
<div class="!h-[900px]">
<div class="process-table">
<el-auto-resizer>
<template #default="{ height, width }">
<el-table-v2
:fixed="isCompactTable"
@column-sort="changeSort"
:columns="columns"
:data="data"
:width="width"
:height="height"
:scrollbar-always-on="isCompactTable"
:sort-by="sortState"
></el-table-v2>
</template>
@@ -73,8 +78,10 @@ import { ProcessStore } from '@/store';
import { SortBy, TableV2SortOrder, ElButton } from 'element-plus';
import { useGlobalStore } from '@/composables/useGlobalStore';
import RuntimeDiagnostics from './diagnostics/index.vue';
import { useMediaQuery } from '@vueuse/core';
const { currentNode } = useGlobalStore();
const isCompactTable = useMediaQuery('(max-width: 1024px)');
const processStore = ProcessStore();
const permissionDirective = resolveDirective('permission');
@@ -306,3 +313,56 @@ onUnmounted(() => {
processStore.disconnect();
});
</script>
<style scoped lang="scss">
.process-toolbar {
min-width: 0;
}
.process-toolbar__filter {
width: 300px;
}
.process-table {
width: 100%;
min-width: 0;
height: 900px;
overflow: hidden;
}
@media only screen and (max-width: 1024px) {
.process-toolbar {
flex-wrap: wrap;
justify-content: flex-start;
gap: 12px;
}
.process-toolbar__filter,
.process-toolbar__field {
width: auto;
min-width: 0;
max-width: 300px;
flex: 1 1 220px;
}
.process-toolbar__field {
:deep(.search-button) {
width: 100%;
}
}
.process-table {
height: clamp(420px, calc(100vh - 260px), 900px);
height: clamp(420px, calc(100dvh - 260px), 900px);
}
}
@media only screen and (max-width: 767px) {
.process-toolbar__filter,
.process-toolbar__field {
width: 100%;
max-width: none;
flex-basis: 100%;
}
}
</style>

View File

@@ -30,7 +30,7 @@
/>
<el-date-picker
v-model="timeRange"
class="p-w-360"
class="host-log-time-range"
type="datetimerange"
range-separator="-"
:start-placeholder="$t('commons.search.timeStart')"
@@ -293,4 +293,12 @@ watch([keyword, priority, service], () => {
border: 1px solid var(--el-border-color-darker);
border-radius: 4px;
}
@media only screen and (max-width: 767px) {
:deep(.host-log-time-range.el-date-editor) {
width: 100%;
max-width: 100%;
min-width: 0;
}
}
</style>

View File

@@ -27,10 +27,16 @@
</span>
</template>
</el-alert>
<ComplexTable :pagination-config="paginationConfig" @sort-change="search" @search="search" :data="data">
<ComplexTable
:pagination-config="paginationConfig"
@sort-change="search"
@search="search"
:data="data"
:scrollbar-always-on="isMobile"
>
<el-table-column
:label="$t('commons.table.name')"
:min-width="80"
:min-width="isMobile ? 140 : 80"
prop="name"
show-overflow-tooltip
>
@@ -98,9 +104,9 @@
show-overflow-tooltip
/>
<fu-table-operations
width="300px"
:width="isMobile ? 100 : 300"
:buttons="buttons"
:ellipsis="10"
:ellipsis="isMobile ? 0 : 10"
:label="$t('commons.table.operate')"
fix
/>
@@ -125,7 +131,7 @@ import { MsgSuccess } from '@/utils/message';
import { Base64 } from 'js-base64';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { isProductPro, isFxplay, docsUrl } = useGlobalStore();
const { isProductPro, isFxplay, docsUrl, isMobile } = useGlobalStore();
const loading = ref();
const data = ref();
const paginationConfig = reactive({

View File

@@ -2,7 +2,7 @@
<div>
<LayoutContent v-loading="loading" v-if="!isRecordShow && !isSettingShow" :title="$t('toolbox.clam.clam')">
<template #prompt>
<el-alert type="info" :closable="false">
<el-alert class="clam-helper-alert" type="info" :closable="false">
<template #title>
{{ $t('toolbox.clam.clamHelper') }}
<el-link class="ml-1 text-xs" v-if="!isFxplay" @click="toDoc()" type="primary">
@@ -52,6 +52,7 @@
:class="{ mask: !clamStatus.isRunning }"
v-if="!isSettingShow"
:pagination-config="paginationConfig"
:scrollbar-always-on="isMobile"
v-model:selects="selects"
@sort-change="search"
@search="search"
@@ -60,7 +61,7 @@
<el-table-column type="selection" fix />
<el-table-column
:label="$t('commons.table.name')"
:min-width="60"
:min-width="isMobile ? 140 : 60"
prop="name"
sortable
show-overflow-tooltip
@@ -163,6 +164,7 @@
:buttons="buttons"
:ellipsis="10"
:label="$t('commons.table.operate')"
:fixed="isMobile ? false : 'right'"
fix
/>
</ComplexTable>
@@ -202,7 +204,7 @@ import { routerToFileWithPath, routerToName } from '@/utils/router';
const loading = ref();
const selects = ref<any>([]);
const { docsUrl, isFxplay, isProductPro } = useGlobalStore();
const { docsUrl, isFxplay, isProductPro, isMobile } = useGlobalStore();
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'clam-page-size',
@@ -394,3 +396,23 @@ onMounted(() => {
search();
});
</script>
<style scoped lang="scss">
@media only screen and (max-width: 767px) {
.clam-helper-alert {
:deep(.el-alert__content),
:deep(.el-alert__title) {
width: 100%;
min-width: 0;
}
:deep(.el-alert__title) {
display: block;
}
:deep(.el-link) {
white-space: nowrap;
}
}
}
</style>

View File

@@ -10,7 +10,7 @@
<el-popover
v-if="dialogData.rowData.path.length >= 35"
placement="top-start"
trigger="hover"
:trigger="hasFinePointer ? 'hover' : 'click'"
width="250"
:content="dialogData.rowData.path"
>
@@ -59,7 +59,7 @@
<LayoutContent :title="$t('cronjob.record')" :reload="true">
<template #rightToolBar>
<el-date-picker
class="mr-2.5"
class="mr-2.5 record-time-range"
@change="search(true)"
v-model="timeRangeLoad"
type="datetimerange"
@@ -194,6 +194,7 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { useMediaQuery } from '@vueuse/core';
import i18n from '@/lang';
import { ElMessageBox } from 'element-plus';
import { MsgSuccess } from '@/utils/message';
@@ -204,6 +205,8 @@ import LogFile from '@/components/log/file/index.vue';
import { cleanClamRecord, handleClamScan, searchClamRecord } from '@/api/modules/toolbox';
import { routerToFileWithPath } from '@/utils/router';
const hasFinePointer = useMediaQuery('(hover: hover) and (pointer: fine)');
const loading = ref();
const refresh = ref(false);
const hasRecords = ref();
@@ -344,6 +347,7 @@ defineExpose({
<style lang="scss" scoped>
.infinite-list {
height: calc(100vh - 318px);
height: calc(100dvh - 318px);
.select-sign {
&::before {
float: left;
@@ -392,4 +396,46 @@ defineExpose({
min-width: 1200px;
}
}
@media only screen and (max-width: 1024px) {
.mainClass {
overflow: visible;
}
.mainRowClass {
min-width: 0;
flex-direction: column;
> .el-col {
flex: 0 0 100%;
width: 100%;
max-width: 100%;
}
}
}
@media only screen and (max-width: 767px) {
.infinite-list {
height: 320px;
height: clamp(220px, 38dvh, 360px);
}
.descriptionWide,
.description {
width: 100%;
min-width: 0;
}
.page-item {
float: none;
max-width: 100%;
overflow-x: auto;
}
:global(.record-time-range.el-date-editor) {
width: 100%;
max-width: 100%;
margin-right: 0;
}
}
</style>

View File

@@ -1,23 +1,23 @@
<template>
<el-form-item :label="$t('app.app')" prop="appDetailID" :rules="Rules.requiredSelect">
<el-row :gutter="20">
<el-col :span="12">
<el-row :gutter="20" class="w-[calc(100%+20px)] gap-y-3 lg:w-auto lg:gap-y-0">
<el-col :span="12" :xs="24" :sm="24" :md="12" class="min-w-0">
<el-select
v-model="runtime.appID"
:disabled="mode === 'edit' || loadingVersion"
@change="changeApp(runtime.appID)"
class="p-w-200"
class="w-full min-w-0 lg:!w-[200px]"
>
<el-option v-for="(app, index) in apps" :key="index" :label="app.name" :value="app.id"></el-option>
</el-select>
</el-col>
<el-col :span="12">
<el-col :span="12" :xs="24" :sm="24" :md="12" class="min-w-0">
<el-select
v-model="runtime.version"
:disabled="loadingVersion"
:loading="loadingVersion"
@change="changeVersion()"
class="p-w-200"
class="w-full min-w-0 lg:!w-[200px]"
>
<el-option
v-for="(version, index) in appVersions"

View File

@@ -15,7 +15,7 @@
</el-form-item>
<div v-if="appKey == 'node'">
<el-row :gutter="20">
<el-col :span="18">
<el-col :span="18" :xs="24" :sm="24" :md="18">
<el-form-item :label="$t('runtime.runScript')" prop="params.EXEC_SCRIPT">
<el-select
v-model="runtime.params['EXEC_SCRIPT']"
@@ -43,7 +43,7 @@
</span>
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :span="6" :xs="24" :sm="24" :md="6">
<el-form-item :label="$t('runtime.customScript')" prop="params.CUSTOM_SCRIPT">
<el-switch
v-model="runtime.params['CUSTOM_SCRIPT']"

View File

@@ -34,13 +34,13 @@
</el-form-item>
<div v-if="runtime.resource === 'appstore'">
<el-form-item :label="$t('app.app')" prop="appDetailID" :rules="Rules.requiredSelect">
<el-row :gutter="20">
<el-col :span="12">
<el-row :gutter="20" class="w-[calc(100%+20px)] gap-y-3 lg:w-auto lg:gap-y-0">
<el-col :span="12" :xs="24" :sm="24" :md="12" class="min-w-0">
<el-select
v-model="runtime.appID"
:disabled="mode === 'edit'"
@change="changeApp(runtime.appID)"
class="p-w-200"
class="w-full min-w-0 lg:!w-[200px]"
>
<el-option
v-for="(app, index) in apps"
@@ -50,13 +50,14 @@
></el-option>
</el-select>
</el-col>
<el-col :span="12">
<el-col :span="12" :xs="24" :sm="24" :md="12" class="min-w-0">
<el-select v-model="runtime.version" @change="changeVersion()" class="p-w-200">
<el-option
v-for="(version, index) in appVersions"
:key="index"
:label="version"
:value="version"
class="w-full min-w-0 lg:!w-[200px]"
></el-option>
</el-select>
</el-col>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<el-row :gutter="22" v-for="(domain, index) of create.domains" :key="index">
<el-col :span="6">
<el-row class="domain-create-row" :gutter="22" v-for="(domain, index) of create.domains" :key="index">
<el-col class="domain-create-field domain-create-domain" :span="6" :xs="24">
<el-form-item
:label="index == 0 ? $t('website.domain') : ''"
:prop="`domains.${index}.domain`"
@@ -16,7 +16,7 @@
<span class="input-help" v-if="domainWarnings[index]">{{ $t('website.domainNotFQDN') }}</span>
</el-form-item>
</el-col>
<el-col :span="6">
<el-col class="domain-create-field domain-create-host" :span="6" :xs="24">
<el-form-item :label="index == 0 ? $t('toolbox.device.hostname') : ''">
<el-input
type="string"
@@ -26,7 +26,7 @@
></el-input>
</el-form-item>
</el-col>
<el-col :span="4">
<el-col class="domain-create-field domain-create-port" :span="4" :xs="12">
<el-form-item
:label="index == 0 ? $t('commons.table.port') : ''"
:prop="`domains.${index}.port`"
@@ -35,24 +35,25 @@
<el-input type="number" v-model.number="create.domains[index].port"></el-input>
</el-form-item>
</el-col>
<el-col :span="2">
<el-col class="domain-create-field domain-create-ssl" :span="2" :xs="6">
<el-form-item :label="index == 0 ? 'SSL' : ''" :prop="`domains.${index}.ssl`">
<el-checkbox
class="domain-create-ssl-control"
v-model="create.domains[index].ssl"
:disabled="create.domains[index].port == 80"
></el-checkbox>
</el-form-item>
</el-col>
<el-col :span="4" v-if="index == 0">
<el-col class="domain-create-field domain-create-operation" :span="4" :xs="6" v-if="index == 0">
<el-form-item :label="$t('commons.table.operate')">
<el-button v-permission @click="addDomain">
<el-button v-permission :aria-label="$t('commons.button.add')" @click="addDomain">
<el-icon><Plus /></el-icon>
</el-button>
</el-form-item>
</el-col>
<el-col :span="4" v-else>
<el-col class="domain-create-field domain-create-operation" :span="4" :xs="6" v-else>
<el-form-item>
<el-button v-permission @click="removeDomain(index)">
<el-button v-permission :aria-label="$t('commons.button.delete')" @click="removeDomain(index)">
<el-icon><Delete /></el-icon>
</el-button>
</el-form-item>
@@ -381,3 +382,42 @@ onMounted(() => {
handleParams();
});
</script>
<style lang="scss" scoped>
@media only screen and (max-width: 1024px) {
.domain-create-row {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(64px, 84px) minmax(64px, 84px);
}
.domain-create-field {
flex: none;
width: auto;
min-width: 0;
max-width: none;
}
.domain-create-domain,
.domain-create-host {
grid-column: 1 / -1;
}
.domain-create-port {
grid-column: 1;
}
.domain-create-ssl {
grid-column: 2;
}
.domain-create-operation {
grid-column: 3;
}
.domain-create-ssl-control {
min-width: 40px;
min-height: 32px;
margin-right: 0;
}
}
</style>