feat: Optimize frontend structure (#12475)

This commit is contained in:
CityFun
2026-04-13 05:48:42 +00:00
committed by GitHub
parent 4d06f444ac
commit 89888cef73
221 changed files with 2740 additions and 1291 deletions
+5 -1
View File
@@ -173,7 +173,11 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { computeSize, dateFormat, downloadFile, newUUID, transferTimeToSecond } from '@/utils/util';
import { computeSize } from '@/utils/size';
import { dateFormat } from '@/utils/date';
import { downloadFile } from '@/utils/file';
import { newUUID } from '@/utils/id';
import { transferTimeToSecond } from '@/utils/validate';
import {
getLocalBackupDir,
handleBackup,
@@ -4,7 +4,7 @@
</template>
<script lang="ts" setup>
import { copyText } from '@/utils/util';
import { copyText } from '@/utils/clipboard';
defineOptions({ name: 'CopyButton' });
defineProps({
@@ -17,7 +17,7 @@
</template>
<script setup lang="ts" name="404">
import { copyText } from '@/utils/util';
import { copyText } from '@/utils/clipboard';
</script>
<style scoped lang="scss">
@@ -17,7 +17,7 @@
</template>
<script setup lang="ts" name="404">
import { copyText } from '@/utils/util';
import { copyText } from '@/utils/clipboard';
</script>
<style scoped lang="scss">
@@ -17,7 +17,7 @@
</template>
<script setup lang="ts" name="404">
import { copyText } from '@/utils/util';
import { copyText } from '@/utils/clipboard';
</script>
<style scoped lang="scss">
+1 -2
View File
@@ -30,8 +30,7 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { computeSize } from '@/utils/util';
import { computeSize } from '@/utils/size';
const dialogVisible = ref();
const existFiles = ref<DialogProps[]>([]);
+2 -2
View File
@@ -178,8 +178,8 @@ import { nextTick, onUpdated, reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgSuccess, MsgWarning } from '@/utils/message';
import { useSearchableForSelect } from '@/views/host/file-management/hooks/searchable';
import { computeSize, debounce } from '@/utils/util';
import { computeSize } from '@/utils/size';
import { debounce } from '@/utils/misc';
const data = ref([]);
const loading = ref(false);
const paths = ref<string[]>([]);
@@ -0,0 +1,92 @@
<template>
<div class="fu-input-rw-switch">
<div
v-if="!isWrite"
class="fu-input-rw-switch__read"
@click.stop="handleReadClick"
@dblclick.stop="handleReadDblClick"
>
<span class="fu-input-rw-switch__text">{{ displayValue }}</span>
</div>
<el-input
v-else
ref="inputRef"
v-bind="$attrs"
:model-value="modelValue"
@update:model-value="handleUpdate"
@input="handleInput"
@blur="handleBlur"
@keyup.enter="handleEnter"
@click.stop
/>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, ref } from 'vue';
defineOptions({ name: 'FuInputRwSwitch', inheritAttrs: false });
const props = defineProps({
modelValue: {
type: [String, Number],
default: '',
},
writeTrigger: {
type: String,
default: 'onClick',
},
});
const emit = defineEmits(['update:modelValue', 'input', 'blur', 'enter']);
const inputRef = ref();
const isWrite = ref(false);
const displayValue = computed(() => {
return props.modelValue === '' || props.modelValue === undefined || props.modelValue === null
? '-'
: String(props.modelValue);
});
const openWrite = async () => {
isWrite.value = true;
await nextTick();
inputRef.value?.focus?.();
inputRef.value?.select?.();
};
const closeWrite = () => {
isWrite.value = false;
};
const handleReadClick = () => {
if (props.writeTrigger === 'onClick') {
openWrite();
}
};
const handleReadDblClick = () => {
if (props.writeTrigger === 'onDblclick') {
openWrite();
}
};
const handleUpdate = (value: string | number) => {
emit('update:modelValue', value);
};
const handleInput = (value: string) => {
emit('input', value);
};
const handleBlur = (event: FocusEvent) => {
emit('blur', event);
closeWrite();
};
const handleEnter = (event: KeyboardEvent) => {
emit('enter', event);
closeWrite();
};
</script>
@@ -0,0 +1,76 @@
<template>
<div class="fu-read-write-switch">
<div
v-if="!isWrite"
class="fu-read-write-switch__read"
@click.stop="handleReadClick"
@dblclick.stop="handleReadDblClick"
>
<slot name="read" :write="openWrite">
<span class="fu-read-write-switch__text">{{ displayValue }}</span>
</slot>
</div>
<div v-else class="fu-read-write-switch__write" @click.stop>
<slot :read="closeWrite"></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
defineOptions({ name: 'FuReadWriteSwitch' });
const props = defineProps({
modelValue: {
type: [String, Number, Boolean, Object, Array],
default: '',
},
data: {
type: [String, Number, Boolean, Object, Array],
default: '',
},
writeTrigger: {
type: String,
default: 'onClick',
},
});
const emit = defineEmits(['update:modelValue', 'change']);
const isWrite = ref(false);
const displayValue = computed(() => {
const value = props.modelValue !== '' && props.modelValue !== undefined ? props.modelValue : props.data;
return value === '' || value === undefined || value === null ? '-' : String(value);
});
const openWrite = () => {
isWrite.value = true;
};
const closeWrite = (value?: any) => {
if (value !== undefined) {
emit('update:modelValue', value);
emit('change', value);
}
isWrite.value = false;
};
const handleReadClick = () => {
if (props.writeTrigger === 'onClick') {
openWrite();
}
};
const handleReadDblClick = () => {
if (props.writeTrigger === 'onDblclick') {
openWrite();
}
};
defineExpose({
write: openWrite,
read: closeWrite,
});
</script>
@@ -0,0 +1,104 @@
<template>
<div class="fu-select-rw-switch">
<div
v-if="!isWrite"
class="fu-select-rw-switch__read"
@click.stop="handleReadClick"
@dblclick.stop="handleReadDblClick"
>
<slot name="read">
<span class="fu-select-rw-switch__text">{{ displayValue }}</span>
</slot>
</div>
<el-select
v-else
ref="selectRef"
v-bind="$attrs"
:model-value="modelValue"
@update:model-value="handleUpdate"
@change="handleChange"
@blur="handleBlur"
@visible-change="handleVisibleChange"
@click.stop
>
<slot></slot>
</el-select>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, ref } from 'vue';
defineOptions({ name: 'FuSelectRwSwitch', inheritAttrs: false });
const props = defineProps({
modelValue: {
type: [String, Number, Boolean],
default: '',
},
blurTime: {
type: Number,
default: 150,
},
writeTrigger: {
type: String,
default: 'onClick',
},
});
const emit = defineEmits(['update:modelValue', 'input', 'blur', 'change']);
const selectRef = ref();
const isWrite = ref(false);
const displayValue = computed(() => {
return props.modelValue === '' || props.modelValue === undefined || props.modelValue === null
? '-'
: String(props.modelValue);
});
const openWrite = async () => {
isWrite.value = true;
await nextTick();
selectRef.value?.focus?.();
};
const closeWrite = () => {
window.setTimeout(() => {
isWrite.value = false;
}, props.blurTime);
};
const handleReadClick = () => {
if (props.writeTrigger === 'onClick') {
openWrite();
}
};
const handleReadDblClick = () => {
if (props.writeTrigger === 'onDblclick') {
openWrite();
}
};
const handleUpdate = (value: string | number | boolean) => {
emit('update:modelValue', value);
emit('input', value);
};
const handleChange = (value: string | number | boolean) => {
emit('change', value);
closeWrite();
};
const handleBlur = (event: FocusEvent) => {
emit('blur', event);
closeWrite();
};
const handleVisibleChange = (visible: boolean) => {
if (!visible) {
closeWrite();
}
};
</script>
+18
View File
@@ -0,0 +1,18 @@
import { defineComponent, h } from 'vue';
export default defineComponent({
name: 'FuStep',
props: {
id: {
type: String,
default: '',
},
title: {
type: String,
default: '',
},
},
setup(_props, { slots }) {
return () => h('div', slots.default?.());
},
});
+213
View File
@@ -0,0 +1,213 @@
import { computed, defineComponent, h, ref, watch, type PropType, type VNode } from 'vue';
import { flattenVNodes, getVNodeComponentName } from './shared';
interface FuStepItem {
id: string;
index: number;
title: string;
vnode: VNode;
}
const buildStepChildren = (vnode: VNode) => {
const children = vnode.children as Record<string, (() => VNode[]) | undefined> | null;
return children?.default?.() || [];
};
export default defineComponent({
name: 'FuSteps',
props: {
active: {
type: Number,
default: 0,
},
direction: {
type: String,
default: 'horizontal',
},
space: {
type: Number,
default: 0,
},
isLoading: {
type: Boolean,
default: false,
},
finishButtonText: {
type: String,
default: '',
},
beforeLeave: {
type: Function as PropType<
(step: { id: string; index: number; title: string }) => boolean | Promise<boolean>
>,
default: undefined,
},
},
emits: ['change'],
setup(props, { emit, slots, expose }) {
const activeIndex = ref(props.active);
const isRunningBeforeLeave = ref(false);
const stepItems = computed<FuStepItem[]>(() =>
flattenVNodes(slots.default?.() || [])
.filter((vnode) => getVNodeComponentName(vnode) === 'FuStep')
.map((vnode, index) => {
const vnodeProps = (vnode.props || {}) as Record<string, any>;
return {
id: String(vnodeProps.id || index),
index,
title: String(vnodeProps.title || ''),
vnode,
};
}),
);
const emitChange = () => {
const currentStep = stepItems.value[activeIndex.value];
if (!currentStep) {
return;
}
emit('change', {
id: currentStep.id,
index: currentStep.index,
title: currentStep.title,
});
};
watch(
() => props.active,
(value) => {
activeIndex.value = value;
},
);
watch(
stepItems,
(items) => {
if (items.length === 0) {
activeIndex.value = 0;
return;
}
if (activeIndex.value > items.length - 1) {
activeIndex.value = items.length - 1;
}
emitChange();
},
{ immediate: true, deep: true },
);
const runBeforeLeave = async () => {
if (!props.beforeLeave) {
return true;
}
const currentStep = stepItems.value[activeIndex.value];
if (!currentStep) {
return true;
}
if (isRunningBeforeLeave.value) {
return true;
}
isRunningBeforeLeave.value = true;
try {
return (
(await props.beforeLeave({
id: currentStep.id,
index: currentStep.index,
title: currentStep.title,
})) !== false
);
} finally {
isRunningBeforeLeave.value = false;
}
};
const changeTo = async (nextIndex: number, runGuard = true) => {
if (nextIndex < 0 || nextIndex >= stepItems.value.length || nextIndex === activeIndex.value) {
return false;
}
const currentIndex = activeIndex.value;
if (runGuard) {
const canLeave = await runBeforeLeave();
if (activeIndex.value !== currentIndex) {
return true;
}
if (!canLeave) {
return false;
}
}
activeIndex.value = nextIndex;
emitChange();
return true;
};
const next = async () => {
if (isRunningBeforeLeave.value) {
const nextIndex = Math.min(activeIndex.value + 1, stepItems.value.length - 1);
if (nextIndex !== activeIndex.value) {
activeIndex.value = nextIndex;
emitChange();
}
return true;
}
return changeTo(activeIndex.value + 1, true);
};
const prev = async () => {
return changeTo(activeIndex.value - 1, false);
};
expose({
next,
prev,
active: activeIndex,
});
return () => {
const currentStep = stepItems.value[activeIndex.value];
return h('div', { class: ['fu-steps', `fu-steps--${props.direction}`] }, [
h(
'div',
{
class: 'fu-steps__nav',
style: props.direction === 'vertical' && props.space ? { gap: `${props.space}px` } : undefined,
},
stepItems.value.map((step, index) =>
h(
'button',
{
key: step.id,
class: [
'fu-steps__item',
{
'is-active': index === activeIndex.value,
'is-finished': index < activeIndex.value,
},
],
type: 'button',
disabled: props.isLoading,
onClick: () => changeTo(step.index, step.index > activeIndex.value),
},
[
h('span', { class: 'fu-steps__index' }, index + 1),
h('span', { class: 'fu-steps__title' }, step.title),
],
),
),
),
h('div', { class: 'fu-steps__content' }, currentStep ? buildStepChildren(currentStep.vnode) : []),
slots.footer
? h(
'div',
{ class: 'fu-steps__footer' },
slots.footer({
active: currentStep,
next,
prev,
}),
)
: null,
]);
};
},
});
+213
View File
@@ -0,0 +1,213 @@
import { ElTable } from 'element-plus';
import { computed, defineComponent, h, onMounted, ref, watch, type PropType, type VNode } from 'vue';
import {
FU_TABLE_STORAGE_PREFIX,
flattenVNodes,
isElTableColumnVNode,
type FuTableColumnConfig,
updateArrayInPlace,
} from './shared';
const buildColumnKey = (vnode: VNode, index: number) => {
const props = (vnode.props || {}) as Record<string, any>;
return String(props.columnKey || props.prop || props.label || `column-${index}`);
};
const buildColumnConfig = (vnode: VNode, index: number): FuTableColumnConfig => {
const props = (vnode.props || {}) as Record<string, any>;
return {
key: buildColumnKey(vnode, index),
label: String(props.label || props.prop || `column-${index}`),
prop: props.prop,
show: true,
fixed: props.fixed ?? props.fix,
};
};
const isConfigurableColumn = (vnode: VNode) => {
if (!isElTableColumnVNode(vnode)) {
return false;
}
const props = (vnode.props || {}) as Record<string, any>;
if (['selection', 'index', 'expand'].includes(props.type)) {
return false;
}
return Boolean(props.label || props.prop);
};
export default defineComponent({
name: 'FuTable',
props: {
columns: {
type: Array as PropType<FuTableColumnConfig[]>,
default: undefined,
},
localKey: {
type: String,
default: '',
},
refresh: {
type: Boolean,
default: true,
},
refTable: {
type: Object as PropType<{ value?: unknown } | null>,
default: null,
},
},
setup(props, { attrs, slots, expose }) {
const refElTable = ref();
const storageLoaded = ref(false);
const storedColumns = ref<FuTableColumnConfig[]>([]);
const slotChildren = computed(() => flattenVNodes(slots.default?.() || []));
const slotColumns = computed(() =>
slotChildren.value.filter(isConfigurableColumn).map((vnode, index) => buildColumnConfig(vnode, index)),
);
const ensureStoredColumns = () => {
if (storageLoaded.value || !props.localKey || typeof window === 'undefined') {
storageLoaded.value = true;
return;
}
storageLoaded.value = true;
try {
const raw = localStorage.getItem(`${FU_TABLE_STORAGE_PREFIX}${props.localKey}`);
if (!raw) {
return;
}
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) {
storedColumns.value = parsed;
}
} catch {
storedColumns.value = [];
}
};
const syncColumns = (nextColumns: FuTableColumnConfig[]) => {
if (!props.columns) {
return;
}
ensureStoredColumns();
const currentColumns = props.columns as FuTableColumnConfig[];
const sourceColumns = currentColumns.length > 0 ? currentColumns : storedColumns.value;
const sourceByKey = new Map(sourceColumns.map((column) => [column.key, column]));
const sourceOrder = new Map(sourceColumns.map((column, index) => [column.key, index]));
const mergedColumns = nextColumns
.map((column, index) => ({
...column,
show: sourceByKey.get(column.key)?.show ?? true,
fixed: sourceByKey.get(column.key)?.fixed ?? column.fixed,
__order: sourceOrder.get(column.key) ?? index,
}))
.sort((a, b) => a.__order - b.__order)
.map((column) => {
const normalizedColumn = { ...column };
delete (normalizedColumn as Partial<typeof normalizedColumn> & { __order?: number }).__order;
return normalizedColumn;
});
updateArrayInPlace(currentColumns, mergedColumns);
};
const resolvedColumns = computed(() => {
if (props.columns && props.columns.length > 0) {
return props.columns;
}
if (storedColumns.value.length > 0) {
const storageMap = new Map(storedColumns.value.map((column) => [column.key, column]));
const orderMap = new Map(storedColumns.value.map((column, index) => [column.key, index]));
return [...slotColumns.value]
.map((column, index) => ({
...column,
show: storageMap.get(column.key)?.show ?? true,
__order: orderMap.get(column.key) ?? index,
}))
.sort((a, b) => a.__order - b.__order)
.map((column) => {
const normalizedColumn = { ...column };
delete (normalizedColumn as Partial<typeof normalizedColumn> & { __order?: number }).__order;
return normalizedColumn;
});
}
return slotColumns.value;
});
const orderedConfigurableChildren = computed(() => {
const columnNodeMap = new Map(
slotChildren.value
.filter(isConfigurableColumn)
.map((vnode, index) => [buildColumnKey(vnode, index), vnode]),
);
return resolvedColumns.value
.filter((column) => column.show !== false)
.map((column) => columnNodeMap.get(column.key))
.filter((column): column is VNode => Boolean(column));
});
const renderedChildren = computed(() => {
if (orderedConfigurableChildren.value.length === 0) {
return slotChildren.value;
}
const children: VNode[] = [];
let hasInsertedConfigurableColumns = false;
for (const child of slotChildren.value) {
if (isConfigurableColumn(child)) {
if (!hasInsertedConfigurableColumns) {
children.push(...orderedConfigurableChildren.value);
hasInsertedConfigurableColumns = true;
}
continue;
}
children.push(child);
}
return children;
});
watch(slotColumns, (nextColumns) => syncColumns(nextColumns), {
immediate: true,
deep: true,
});
watch(
() => props.columns,
(nextColumns) => {
if (!props.localKey || !nextColumns || typeof window === 'undefined') {
return;
}
localStorage.setItem(`${FU_TABLE_STORAGE_PREFIX}${props.localKey}`, JSON.stringify(nextColumns));
},
{
deep: true,
},
);
onMounted(() => {
ensureStoredColumns();
if (props.refTable) {
props.refTable.value = refElTable.value;
}
});
expose({ refElTable });
return () =>
h(
ElTable as any,
{
...attrs,
ref: refElTable,
class: ['fu-table', attrs.class],
headerRowClassName: 'fu-table-header',
},
{
...slots,
default: () => renderedChildren.value,
},
);
},
});
@@ -0,0 +1,125 @@
<template>
<div class="fu-search-bar fu-table-column-select">
<el-popover
v-model:visible="visible"
:trigger="trigger"
:popper-class="resolvedPopperClass"
placement="bottom-end"
:width="260"
>
<template #reference>
<el-button class="fu-search-bar-button" :icon="iconComponent" @click.stop>
<span v-if="!onlyIcon">{{ resolvedTitle }}</span>
</el-button>
</template>
<div class="fu-table-column-select__panel" @click.stop>
<div class="fu-table-column-select__title">{{ resolvedTitle }}</div>
<div ref="listRef" class="fu-table-column-select__list">
<div
v-for="column in columns"
:key="column.key"
class="fu-table-column-select__item"
:data-key="column.key"
>
<el-icon class="fu-table-column-select__drag"><Rank /></el-icon>
<el-checkbox v-model="column.show" :disabled="isFixedColumn(column)">
{{ column.label }}
</el-checkbox>
</div>
</div>
</div>
</el-popover>
</div>
</template>
<script setup lang="ts">
import Sortable from 'sortablejs';
import { Rank, Setting } from '@element-plus/icons-vue';
import { computed, nextTick, onBeforeUnmount, ref, watch, type PropType } from 'vue';
import { useI18n } from 'vue-i18n';
import type { FuTableColumnConfig } from './shared';
defineOptions({ name: 'FuTableColumnSelect' });
const { t } = useI18n();
const props = defineProps({
columns: {
type: Array as PropType<FuTableColumnConfig[]>,
default: () => [],
},
title: {
type: String,
default: '',
},
icon: {
type: [String, Object],
default: undefined,
},
onlyIcon: {
type: Boolean,
default: false,
},
trigger: {
type: String,
default: 'click',
},
popperClass: {
type: String,
default: '',
},
});
const visible = ref(false);
const listRef = ref<HTMLElement | null>(null);
let sortable: Sortable | undefined;
const resolvedTitle = computed(() => props.title || t('fu.table.custom_table_rows'));
const resolvedPopperClass = computed(() =>
['fu-table-column-select__popover', props.popperClass].filter(Boolean).join(' '),
);
const iconComponent = computed(() => props.icon || Setting);
const initSortable = async () => {
await nextTick();
sortable?.destroy();
if (!listRef.value) {
return;
}
sortable = Sortable.create(listRef.value, {
animation: 150,
handle: '.fu-table-column-select__drag',
onEnd: ({ oldIndex, newIndex }) => {
if (oldIndex === undefined || newIndex === undefined || oldIndex === newIndex) {
return;
}
const nextColumns = [...props.columns];
const [movedColumn] = nextColumns.splice(oldIndex, 1);
if (!movedColumn) {
return;
}
nextColumns.splice(newIndex, 0, movedColumn);
props.columns.splice(0, props.columns.length, ...nextColumns);
},
});
};
const isFixedColumn = (column: FuTableColumnConfig) => {
return Boolean(column.fixed);
};
watch(visible, (value) => {
if (value) {
initSortable();
return;
}
sortable?.destroy();
sortable = undefined;
});
onBeforeUnmount(() => {
sortable?.destroy();
sortable = undefined;
});
</script>
@@ -0,0 +1,225 @@
<template>
<el-table-column v-bind="$attrs" :label="label" :width="resolvedWidth" :align="align" :fixed="resolvedFixed">
<template #default="{ row }">
<div class="fu-table-operations" @click.stop>
<span
v-for="button in getInlineButtons(row)"
:key="buildButtonKey(button)"
class="fu-table-operations__action"
>
<el-button
class="fu-table-operations__button"
link
:type="button.type || 'primary'"
:disabled="isButtonDisabled(button, row)"
@click.stop="handleButtonClick(button, row)"
>
<el-icon v-if="button.icon" class="mr-1">
<component :is="button.icon" />
</el-icon>
<span>{{ button.label }}</span>
</el-button>
</span>
<span v-if="getMoreButtons(row).length > 0" class="fu-table-operations__action">
<el-dropdown
class="fu-table-more-button"
:trigger="trigger"
@command="(button) => handleButtonClick(button, row)"
>
<span class="fu-table-operations__dropdown-trigger">
<el-button class="fu-table-operations__button" link type="primary" @click.stop>
{{ t('fu.table.more') }}
</el-button>
</span>
<template #dropdown>
<el-dropdown-menu :style="dropdownStyle">
<el-dropdown-item
v-for="button in getMoreButtons(row)"
:key="buildButtonKey(button)"
:command="button"
:disabled="isButtonDisabled(button, row)"
:divided="button.divided"
>
<el-icon v-if="button.icon" class="mr-1">
<component :is="button.icon" />
</el-icon>
<span>{{ button.label }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</div>
</template>
</el-table-column>
</template>
<script setup lang="ts">
import { computed, type PropType } from 'vue';
import { useI18n } from 'vue-i18n';
import { resolveMaybeFn, type FuTableOperationButton } from './shared';
defineOptions({ name: 'FuTableOperations' });
const { t } = useI18n();
const normalizeWidth = (value?: string | number) => {
if (value === undefined || value === null || value === '') {
return undefined;
}
if (typeof value === 'number') {
return value;
}
const trimmed = value.trim();
if (!trimmed || trimmed === 'auto') {
return undefined;
}
return trimmed;
};
const props = defineProps({
buttons: {
type: Array as PropType<FuTableOperationButton[]>,
default: () => [],
},
label: {
type: String,
default: '',
},
width: {
type: [Number, String],
default: undefined,
},
minWidth: {
type: [Number, String],
default: undefined,
},
align: {
type: String,
default: 'center',
},
ellipsis: {
type: Number,
default: 2,
},
trigger: {
type: String,
default: 'hover',
},
fixed: {
type: [Boolean, String],
default: undefined,
},
fix: {
type: [Boolean, String],
default: undefined,
},
maxHeight: {
type: [Number, String],
default: undefined,
},
});
const resolvedFixed = computed(() => {
if (props.fixed !== undefined) {
return props.fixed;
}
if (typeof props.fix === 'string') {
return props.fix;
}
return props.fix ? 'right' : false;
});
const hasDynamicShow = computed(() => {
return props.buttons.some((button) => typeof button.show === 'function');
});
const staticVisibleCount = computed(() => {
return props.buttons.filter((button) => button.show !== false).length;
});
const hasMoreStatic = computed(() => {
return staticVisibleCount.value > props.ellipsis + 1;
});
const estimatedButtonCount = computed(() => {
if (staticVisibleCount.value === 0) {
return 0;
}
if (props.ellipsis <= 0) {
return 1;
}
if (hasDynamicShow.value || hasMoreStatic.value) {
return props.ellipsis + 1;
}
return staticVisibleCount.value;
});
const estimatedWidth = computed(() => {
const buttonCount = Math.max(estimatedButtonCount.value, 1);
const buttonsWidth = 24 + buttonCount * 56 + Math.max(buttonCount - 1, 0) * 5;
const minWidth = normalizeWidth(props.minWidth);
if (typeof minWidth === 'number') {
return Math.max(buttonsWidth, minWidth);
}
if (typeof minWidth === 'string') {
const parsed = Number(minWidth.replace('px', ''));
if (!Number.isNaN(parsed)) {
return Math.max(buttonsWidth, parsed);
}
}
return buttonsWidth;
});
const resolvedWidth = computed(() => {
return normalizeWidth(props.width) ?? estimatedWidth.value;
});
const dropdownStyle = computed(() => {
if (props.maxHeight === undefined || props.maxHeight === null || props.maxHeight === '') {
return undefined;
}
const maxHeight = typeof props.maxHeight === 'number' ? `${props.maxHeight}px` : props.maxHeight;
return {
maxHeight,
overflowY: 'auto',
};
});
const getVisibleButtons = (row: any) => {
return props.buttons.filter((button) => resolveMaybeFn(button.show ?? true, row));
};
const hasMore = (row: any) => {
return getVisibleButtons(row).length > props.ellipsis + 1;
};
const getInlineButtons = (row: any) => {
const visibleButtons = getVisibleButtons(row);
if (props.ellipsis <= 0) {
return [];
}
return hasMore(row) ? visibleButtons.slice(0, props.ellipsis) : visibleButtons;
};
const getMoreButtons = (row: any) => {
const visibleButtons = getVisibleButtons(row);
return hasMore(row) ? visibleButtons.slice(props.ellipsis) : [];
};
const isButtonDisabled = (button: FuTableOperationButton, row: any) => {
return Boolean(resolveMaybeFn(button.disabled ?? false, row));
};
const handleButtonClick = (button: FuTableOperationButton, row: any) => {
if (isButtonDisabled(button, row)) {
return;
}
button.click?.(row);
};
const buildButtonKey = (button: FuTableOperationButton) => {
return String(button.label || button.command || button.type || button.icon?.name || 'button');
};
</script>
@@ -0,0 +1,51 @@
<template>
<el-pagination
v-bind="$attrs"
class="fu-table-pagination"
:current-page="currentPage"
:page-size="pageSize"
:page-sizes="pageSizes"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</template>
<script setup lang="ts">
import { type PropType } from 'vue';
defineOptions({ name: 'FuTablePagination' });
const props = defineProps({
currentPage: {
type: Number,
default: 1,
},
pageSize: {
type: Number,
default: 10,
},
pageSizes: {
type: Array as PropType<number[]>,
default: () => [10, 20, 50, 100],
},
total: {
type: Number,
default: 0,
},
});
const emit = defineEmits(['update:pageSize', 'update:currentPage', 'size-change', 'current-change', 'change']);
const handleSizeChange = (pageSize: number) => {
emit('update:pageSize', pageSize);
emit('size-change', pageSize);
emit('change', { currentPage: props.currentPage, pageSize });
};
const handleCurrentChange = (currentPage: number) => {
emit('update:currentPage', currentPage);
emit('current-change', currentPage);
emit('change', { currentPage, pageSize: props.pageSize });
};
</script>
@@ -0,0 +1,3 @@
import FuCompatComponents from '@/components/fu';
void FuCompatComponents;
+31
View File
@@ -0,0 +1,31 @@
import { type App } from 'vue';
import FuInputRwSwitch from './FuInputRwSwitch.vue';
import FuReadWriteSwitch from './FuReadWriteSwitch.vue';
import FuSelectRwSwitch from './FuSelectRwSwitch.vue';
import FuStep from './FuStep';
import FuSteps from './FuSteps';
import FuTable from './FuTable';
import FuTableColumnSelect from './FuTableColumnSelect.vue';
import FuTableOperations from './FuTableOperations.vue';
import FuTablePagination from './FuTablePagination.vue';
const components = [
FuTable,
FuTableOperations,
FuTablePagination,
FuInputRwSwitch,
FuReadWriteSwitch,
FuSelectRwSwitch,
FuSteps,
FuStep,
FuTableColumnSelect,
];
export default {
install(app: App) {
components.forEach((component) => {
app.component((component as any).name, component as any);
});
},
};
+68
View File
@@ -0,0 +1,68 @@
import { Comment, Fragment, Text, type VNode } from 'vue';
export interface FuTableColumnConfig {
key: string;
label: string;
prop?: string;
show: boolean;
fixed?: boolean | string;
}
export interface FuTableOperationButton {
label?: string | number;
click?: (row: any) => void;
disabled?: boolean | ((row: any) => boolean);
show?: boolean | ((row: any) => boolean);
type?: string;
icon?: any;
divided?: boolean;
[key: string]: any;
}
export const FU_TABLE_STORAGE_PREFIX = 'FU-T-';
export const flattenVNodes = (nodes: VNode[] = []): VNode[] => {
const flattened: VNode[] = [];
for (const node of nodes) {
if (!node) {
continue;
}
if (Array.isArray(node)) {
flattened.push(...flattenVNodes(node));
continue;
}
if (node.type === Fragment) {
flattened.push(...flattenVNodes((node.children as VNode[]) || []));
continue;
}
if (node.type === Comment || node.type === Text) {
continue;
}
flattened.push(node);
}
return flattened;
};
export const getVNodeComponentName = (vnode: VNode) => {
const type = vnode.type as any;
return type?.name || type?.__name || type;
};
export const isElTableColumnVNode = (vnode: VNode) => {
const type = vnode.type as any;
return type === 'el-table-column' || type?.name === 'ElTableColumn' || type?.__name === 'ElTableColumn';
};
export const resolveMaybeFn = <T, R>(value: R | ((row: T) => R), row: T): R => {
if (typeof value === 'function') {
return (value as (row: T) => R)(row);
}
return value;
};
export const updateArrayInPlace = <T>(target: T[], next: T[]) => {
if (target.length === next.length && target.every((item, index) => item === next[index])) {
return;
}
target.splice(0, target.length, ...next);
};
+2
View File
@@ -10,8 +10,10 @@ import CopyButton from '@/components/copy-button/index.vue';
import MsgInfo from '@/components/msg-info/index.vue';
import DrawerPro from '@/components/drawer-pro/index.vue';
import DialogPro from '@/components/dialog-pro/index.vue';
import FuComponents from '@/components/fu';
export default {
install(app: App) {
app.use(FuComponents);
app.component(LayoutContent.name, LayoutContent);
app.component(RouterButton.name, RouterButton);
app.component(ComplexTable.name, ComplexTable);
+1 -1
View File
@@ -32,7 +32,7 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { copyText } from '@/utils/util';
import { copyText } from '@/utils/clipboard';
import FileList from '@/components/file-list/index.vue';
const em = defineEmits(['update:tags']);
@@ -75,7 +75,7 @@ import { FitAddon } from '@xterm/addon-fit';
import { Terminal } from '@xterm/xterm';
import '@xterm/xterm/css/xterm.css';
import i18n from '@/lang';
import { dateFormatForName } from '@/utils/util';
import { dateFormatForName } from '@/utils/date';
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { ElMessageBox } from 'element-plus';
import { MsgError, MsgSuccess } from '@/utils/message';
+1 -1
View File
@@ -50,7 +50,7 @@
</template>
<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, reactive, ref, computed } from 'vue';
import { downloadFile } from '@/utils/util';
import { downloadFile } from '@/utils/file';
import { readByLine } from '@/api/modules/files';
import { GlobalStore } from '@/store';
import bus from '@/global/bus';
+1 -1
View File
@@ -19,7 +19,7 @@ import { ref } from 'vue';
import { getAgentSettingInfo } from '@/api/modules/setting';
import i18n from '@/lang';
import { MsgError, MsgWarning } from '@/utils/message';
import { jumpToPath } from '@/utils/util';
import { jumpToPath } from '@/utils/router';
import { useRouter } from 'vue-router';
import { GlobalStore } from '@/store';
const globalStore = GlobalStore();
+1 -1
View File
@@ -50,7 +50,7 @@
import TaskLog from '@/components/log/task/index.vue';
import NodeSelect from '@/components/node-select/index.vue';
import { dateFormat } from '@/utils/util';
import { dateFormat } from '@/utils/date';
import { searchTasks } from '@/api/modules/log';
import { reactive, ref } from 'vue';
import { Log } from '@/api/interface/log';
+3 -1
View File
@@ -135,7 +135,9 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { computeSize, newUUID, transferTimeToSecond } from '@/utils/util';
import { computeSize } from '@/utils/size';
import { newUUID } from '@/utils/id';
import { transferTimeToSecond } from '@/utils/validate';
import i18n from '@/lang';
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile, genFileId } from 'element-plus';
import { File } from '@/api/interface/file';
@@ -5,7 +5,7 @@
import { onMounted, nextTick, watch, onBeforeUnmount, ref } from 'vue';
import echarts from '@/utils/echarts';
import { GlobalStore } from '@/store';
import { computeSizeFromKBs, computeSizeFromKB, computeSizeFromMB } from '@/utils/util';
import { computeSizeFromKBs, computeSizeFromKB, computeSizeFromMB } from '@/utils/size';
import i18n from '@/lang';
const globalStore = GlobalStore();
const isDarkTheme = ref(false);