675 lines
19 KiB
Vue
675 lines
19 KiB
Vue
<script setup>
|
||
import { computed, nextTick, onBeforeUnmount, ref, shallowRef, useSlots, watch } from "vue";
|
||
import { useRafFn, useResizeObserver } from "@vueuse/core";
|
||
import { StkTable } from "stk-table-vue";
|
||
import { createSelectionColumn, isSelectionColumn } from "./utils/createSelectionCellRenderers";
|
||
import { resolveCustomCell } from "./utils/createCellRenderer";
|
||
|
||
defineOptions({ inheritAttrs: false });
|
||
|
||
const props = defineProps({
|
||
virtual: { type: Boolean, default: true },
|
||
virtualX: { type: Boolean, default: true },
|
||
colResizable: { type: Boolean, default: true },
|
||
bordered: { type: [Boolean, String], default: true },
|
||
noDataFull: { type: Boolean, default: true },
|
||
showOverflow: { type: Boolean, default: true },
|
||
columns: { type: Array, default: () => [] },
|
||
// 序号列默认显示;false=关闭;{ title, width }=自定义
|
||
seq: { type: [Boolean, Object], default: true },
|
||
// 列头拖拽默认开启;false=关闭;{ mode, disabled }=自定义
|
||
columnDraggable: { type: [Boolean, Object], default: true },
|
||
// 行多选:{ selectedRowKeys, onChange },提供时在最前列插入复选框列
|
||
rowSelection: { type: Object, default: null },
|
||
dataSource: { type: Array, default: () => [] },
|
||
rowKey: { type: [String, Function], default: "id" },
|
||
activeRowKey: { type: [String, Number], default: null },
|
||
// 行拖拽排序:开启后显示拖拽手柄,并通过 row-order-change 回传排序后的完整数据。
|
||
rowDraggable: { type: Boolean, default: false },
|
||
// 存在 editor 配置的列默认可编辑;设为 false 可整体禁用编辑。
|
||
editable: { type: Boolean, default: true },
|
||
});
|
||
|
||
const emit = defineEmits(["update:columns", "change", "row-order-change"]);
|
||
|
||
const rootRef = ref(null);
|
||
const tableRef = ref(null);
|
||
const slots = useSlots();
|
||
|
||
// Tab 面板从隐藏恢复时,等到容器拥有有效尺寸再刷新虚拟滚动布局。
|
||
// RAF 将同一帧内的多次 ResizeObserver 通知合并,避免重复重排。
|
||
const { resume: refreshLayout } = useRafFn(
|
||
async () => {
|
||
await nextTick();
|
||
tableRef.value?.initVirtualScroll?.();
|
||
await nextTick();
|
||
},
|
||
{ immediate: false, once: true },
|
||
);
|
||
|
||
useResizeObserver(rootRef, ([entry]) => {
|
||
const { width, height } = entry.contentRect;
|
||
if (width > 0 && height > 0) refreshLayout();
|
||
});
|
||
|
||
const NAVIGATION_KEYS = new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Tab"]);
|
||
const rowEditingStates = new WeakMap();
|
||
let activeEditingRow = null;
|
||
let pendingEditingTarget = null;
|
||
const pressedNavigationKeys = new Set();
|
||
|
||
function getRowKey(row) {
|
||
return typeof props.rowKey === "function" ? props.rowKey(row) : row?.[props.rowKey];
|
||
}
|
||
|
||
function getRowEditingState(row) {
|
||
if (!row || typeof row !== "object") return null;
|
||
let state = rowEditingStates.get(row);
|
||
if (!state) {
|
||
state = shallowRef({ editing: false, focusCol: null, initialValues: null });
|
||
rowEditingStates.set(row, state);
|
||
}
|
||
return state;
|
||
}
|
||
|
||
function captureEditableValues(row) {
|
||
return new Map(
|
||
props.columns.filter((col) => col.editor).map((col) => [col.dataIndex, row[col.dataIndex]]),
|
||
);
|
||
}
|
||
|
||
function clearActiveEditing() {
|
||
if (!activeEditingRow) return;
|
||
const state = rowEditingStates.get(activeEditingRow);
|
||
activeEditingRow = null;
|
||
if (state) state.value = { editing: false, focusCol: null, initialValues: null };
|
||
}
|
||
|
||
// 父级可能在 change 后回灌同 rowKey 的新对象,编辑状态需要随行对象迁移。
|
||
function rebindActiveEditingRow(rows) {
|
||
if (!activeEditingRow || rows.includes(activeEditingRow)) return;
|
||
|
||
const previousRow = activeEditingRow;
|
||
const previousState = rowEditingStates.get(previousRow)?.value;
|
||
const previousKey = getRowKey(previousRow);
|
||
const replacement =
|
||
previousKey == null ? null : rows.find((row) => String(getRowKey(row)) === String(previousKey));
|
||
|
||
clearActiveEditing();
|
||
if (!replacement || !previousState?.editing) return;
|
||
|
||
const state = getRowEditingState(replacement);
|
||
activeEditingRow = replacement;
|
||
state.value = {
|
||
editing: true,
|
||
focusCol: previousState.focusCol,
|
||
initialValues: captureEditableValues(replacement),
|
||
};
|
||
}
|
||
|
||
function activateEditing(row, focusCol) {
|
||
if (!props.editable || !row) return;
|
||
const state = getRowEditingState(row);
|
||
if (!state) return;
|
||
|
||
if (activeEditingRow !== row) {
|
||
clearActiveEditing();
|
||
activeEditingRow = row;
|
||
state.value = {
|
||
editing: true,
|
||
focusCol,
|
||
initialValues: captureEditableValues(row),
|
||
};
|
||
return;
|
||
}
|
||
|
||
const current = state.value;
|
||
if (current.editing && current.focusCol === focusCol) return;
|
||
state.value = { ...current, editing: true, focusCol };
|
||
}
|
||
|
||
function editorValuesEqual(col, previous, current) {
|
||
if (col.editor?.type === "select" || col.editor?.type === "checkbox") {
|
||
return Object.is(previous, current);
|
||
}
|
||
return String(previous ?? "") === String(current ?? "");
|
||
}
|
||
|
||
const selectOnFocus = ref(true);
|
||
const editingCtx = {
|
||
isEditing: (row) => props.editable && getRowEditingState(row)?.value.editing,
|
||
shouldFocus: (row, col) => getRowEditingState(row)?.value.focusCol === col.dataIndex,
|
||
shouldSelectOnFocus: selectOnFocus,
|
||
shouldNotify: (row, col, newValue) => {
|
||
const values = getRowEditingState(row)?.value.initialValues;
|
||
if (!values?.has(col.dataIndex)) return true;
|
||
if (editorValuesEqual(col, values.get(col.dataIndex), newValue)) return false;
|
||
values.set(col.dataIndex, newValue);
|
||
return true;
|
||
},
|
||
onChange: (row, col, newValue) => {
|
||
emit("change", row, col, newValue);
|
||
},
|
||
};
|
||
|
||
// 由 seq 派生:enabled / title / width
|
||
const seqConfig = computed(() => {
|
||
const enabled = !!props.seq;
|
||
if (!enabled) return { enabled: false, title: "序号", width: 50 };
|
||
const cfg = props.seq === true ? {} : props.seq;
|
||
return {
|
||
enabled: true,
|
||
title: cfg.title ?? "序号",
|
||
width: Number(cfg.width ?? 50),
|
||
};
|
||
});
|
||
const columnDragConfig = computed(() => {
|
||
if (!props.columnDraggable) return false;
|
||
const configured = props.columnDraggable === true ? {} : props.columnDraggable;
|
||
return {
|
||
...configured,
|
||
disabled: (column) => {
|
||
if (
|
||
isSelectionColumn(column) ||
|
||
column.type === "seq" ||
|
||
column.type === "dragRow" ||
|
||
column.type === "expand" ||
|
||
column.dataIndex === "" ||
|
||
column.dataIndex === "__actions__"
|
||
) {
|
||
return true;
|
||
}
|
||
if (typeof configured.disabled === "function") return configured.disabled(column);
|
||
return Boolean(configured.disabled);
|
||
},
|
||
};
|
||
});
|
||
const hasRowSelection = computed(() => Boolean(props.rowSelection));
|
||
const dragRowColumn = {
|
||
type: "dragRow",
|
||
dataIndex: "__drag_row__",
|
||
title: "",
|
||
width: 40,
|
||
resizable: false,
|
||
};
|
||
|
||
function getHeaderAlign(column) {
|
||
if (column.headerAlign) return column.headerAlign;
|
||
if (isSelectionColumn(column)) return "center";
|
||
if (column.dataIndex === "actions") return "center";
|
||
if (column.actions && column.actions.length) return "center";
|
||
if (column.checkbox === true) return "center";
|
||
if (column.editor && column.editor.type === "checkbox") return "center";
|
||
return "left";
|
||
}
|
||
|
||
const displayColumns = computed(() => {
|
||
const cols = props.columns.map((col) => resolveCustomCell(col, editingCtx));
|
||
const seq = seqConfig.value;
|
||
if (seq.enabled) {
|
||
cols.unshift({
|
||
type: "seq",
|
||
width: seq.width,
|
||
dataIndex: "",
|
||
title: seq.title,
|
||
resizable: false,
|
||
});
|
||
}
|
||
if (props.rowDraggable) cols.unshift(dragRowColumn);
|
||
if (hasRowSelection.value) cols.unshift(selectionColumn);
|
||
return cols.map((column) => ({
|
||
...column,
|
||
headerAlign: getHeaderAlign(column),
|
||
}));
|
||
});
|
||
|
||
// 剥离内部列(选择列、序号列、拖拽列)后回传,避免污染父组件 columns
|
||
function onColumnsUpdate(columns) {
|
||
const originalByKey = new Map(
|
||
props.columns.map((col) => [String(col.key ?? col.dataIndex ?? ""), col]),
|
||
);
|
||
const real = (columns || [])
|
||
.filter(
|
||
(c) =>
|
||
!isSelectionColumn(c) && c.type !== "seq" && c.type !== "dragRow" && c.dataIndex !== "",
|
||
)
|
||
.map((col) => {
|
||
const original = originalByKey.get(String(col.key ?? col.dataIndex ?? ""));
|
||
return original ? { ...original, width: col.width ?? original.width } : col;
|
||
});
|
||
emit("update:columns", real);
|
||
}
|
||
|
||
function handleRowOrderChange(sourceIndex, targetIndex) {
|
||
const rows = tableRef.value?.getTableData?.() ?? props.dataSource;
|
||
tableRef.value?.clearSelectedArea?.();
|
||
emit("row-order-change", [...rows], sourceIndex, targetIndex);
|
||
}
|
||
|
||
// 行高同时作为 CSS 变量与 StkTable 的 row-height / header-row-height
|
||
const rowHeight = 34;
|
||
const headerRowHeight = 40;
|
||
const customVars = {
|
||
"--header-row-height": `${headerRowHeight}px`,
|
||
"--row-height": `${rowHeight}px`,
|
||
"--footer-row-height": "36px",
|
||
};
|
||
|
||
// 选择列:已选状态管理 + 渲染逻辑全部内聚在 createSelectionColumn 中
|
||
const selectionColumn = createSelectionColumn({
|
||
getDataSource: () => props.dataSource,
|
||
getRowKey,
|
||
getSelectedRowKeys: () => props.rowSelection?.selectedRowKeys ?? [],
|
||
isRowDisabled: (row) => Boolean(props.rowSelection?.getCheckboxProps?.(row)?.disabled),
|
||
onChange: (keys, rows) => props.rowSelection?.onChange?.(keys, rows),
|
||
});
|
||
|
||
// 滚动到指定行(新增后聚焦)
|
||
async function scrollToRow(rowKey) {
|
||
await nextTick();
|
||
const table = tableRef.value;
|
||
if (!table) return;
|
||
const idx = props.dataSource.findIndex((row) => String(getRowKey(row)) === String(rowKey));
|
||
if (idx < 0) return;
|
||
table.scrollTo(idx * rowHeight, null);
|
||
}
|
||
|
||
// 新增行等程序化场景使用:滚动到目标行,并让该行立即进入编辑状态。
|
||
// activeRowKey 仅控制当前行高亮,不应隐式触发编辑,因此单独暴露此方法。
|
||
async function editRow(rowKey, focusCol = null) {
|
||
await scrollToRow(rowKey);
|
||
await nextTick();
|
||
const row = props.dataSource.find((item) => String(getRowKey(item)) === String(rowKey));
|
||
if (!row) return;
|
||
tableRef.value?.setCurrentRow?.(row, { silent: true });
|
||
activateEditing(row, focusCol);
|
||
}
|
||
|
||
function getSelectedArea() {
|
||
return tableRef.value?.getSelectedArea?.();
|
||
}
|
||
|
||
function clearSelectedArea() {
|
||
tableRef.value?.clearSelectedArea?.();
|
||
}
|
||
|
||
function setRowExpand(...args) {
|
||
return tableRef.value?.setRowExpand?.(...args);
|
||
}
|
||
|
||
function initVirtualScroll() {
|
||
return tableRef.value?.initVirtualScroll?.();
|
||
}
|
||
|
||
let editActivateTimer = null;
|
||
let lastEditActivateAt = 0;
|
||
let isRepeatNavigation = false;
|
||
|
||
function clearEditActivateTimer() {
|
||
if (editActivateTimer) {
|
||
clearTimeout(editActivateTimer);
|
||
editActivateTimer = null;
|
||
}
|
||
}
|
||
|
||
// 键盘导航停止(松开按键或停顿)后再激活编辑器,合并连续/按住按键,
|
||
// 避免滚动过程中每帧创建/销毁编辑器组件导致掉帧。
|
||
function scheduleEditActivation() {
|
||
clearEditActivateTimer();
|
||
editActivateTimer = setTimeout(() => {
|
||
editActivateTimer = null;
|
||
commitPendingEditing();
|
||
}, 100);
|
||
}
|
||
|
||
function commitPendingEditing() {
|
||
const target = pendingEditingTarget;
|
||
pendingEditingTarget = null;
|
||
if (!target) return;
|
||
const row = props.dataSource.find(
|
||
(item) => item === target.row || String(getRowKey(item)) === String(getRowKey(target.row)),
|
||
);
|
||
if (row) {
|
||
activateEditing(row, target.focusCol);
|
||
tableRef.value?.setCurrentRow?.(row, { silent: true });
|
||
lastEditActivateAt = performance.now();
|
||
}
|
||
}
|
||
|
||
function removeNavigationListeners() {
|
||
window.removeEventListener("keyup", handleKeyupCapture, true);
|
||
window.removeEventListener("blur", handleWindowBlur);
|
||
}
|
||
|
||
function handleKeydownCapture(event) {
|
||
if (!NAVIGATION_KEYS.has(event.key)) return;
|
||
if (!pressedNavigationKeys.size) {
|
||
window.addEventListener("keyup", handleKeyupCapture, true);
|
||
window.addEventListener("blur", handleWindowBlur);
|
||
}
|
||
pressedNavigationKeys.add(event.key);
|
||
// event.repeat=true 表示系统自动重复(按住方向键不松),用于区分“按住滚动”与“离散点按”
|
||
if (event.repeat) isRepeatNavigation = true;
|
||
}
|
||
|
||
function handleKeyupCapture(event) {
|
||
if (!NAVIGATION_KEYS.has(event.key)) return;
|
||
if (!pressedNavigationKeys.delete(event.key) || pressedNavigationKeys.size) return;
|
||
|
||
removeNavigationListeners();
|
||
|
||
if (isRepeatNavigation) {
|
||
// 长按已经结束,立即恢复最终目标行,不再额外等待防抖时间。
|
||
isRepeatNavigation = false;
|
||
clearEditActivateTimer();
|
||
commitPendingEditing();
|
||
return;
|
||
}
|
||
|
||
// 快速离散连按仍使用短防抖,避免连续挂载/卸载整行编辑器。
|
||
if (pendingEditingTarget) scheduleEditActivation();
|
||
}
|
||
|
||
function handleWindowBlur() {
|
||
removeNavigationListeners();
|
||
pressedNavigationKeys.clear();
|
||
isRepeatNavigation = false;
|
||
clearEditActivateTimer();
|
||
commitPendingEditing();
|
||
}
|
||
|
||
// 选区变化后,目标行进入编辑,活动单元格获得焦点。
|
||
function handleAreaSelectionChange(ranges) {
|
||
if (!props.editable) return;
|
||
|
||
const target = ranges?.at(-1)?.index.end;
|
||
const row = target ? tableRef.value?.getTableData?.()[target.row] : null;
|
||
const col = target ? displayColumns.value[target.col] : null;
|
||
if (!row) {
|
||
pendingEditingTarget = null;
|
||
clearActiveEditing();
|
||
clearEditActivateTimer();
|
||
return;
|
||
}
|
||
|
||
const focusCol = col?.editor ? col.dataIndex : null;
|
||
// 键盘导航期间(按下方向键等):
|
||
// - 按住方向键滚动(event.repeat=true):全程保持 0 编辑器,避免长按下反复创建组件导致卡顿/闪烁;
|
||
// 松手后再激活到最终行。
|
||
// - 慢速离散点按(非 repeat):立即激活,编辑器逐行跟随、跟手;
|
||
// - 快速连按(间隔 <150ms):防抖合并,避免连按时每键挂载掉帧。
|
||
if (pressedNavigationKeys.size > 0) {
|
||
if (isRepeatNavigation) {
|
||
pendingEditingTarget = { row, focusCol };
|
||
clearActiveEditing();
|
||
} else {
|
||
const now = performance.now();
|
||
if (now - lastEditActivateAt >= 150) {
|
||
pendingEditingTarget = null;
|
||
clearEditActivateTimer();
|
||
activateEditing(row, focusCol);
|
||
tableRef.value?.setCurrentRow?.(row, { silent: true });
|
||
lastEditActivateAt = now;
|
||
} else {
|
||
pendingEditingTarget = { row, focusCol };
|
||
scheduleEditActivation();
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 非键盘导航(鼠标点击等):立即激活。
|
||
// 同一行已编辑时仍更新 focusCol(保证焦点切换到新列),
|
||
// 但暂时禁用 select 全选(避免鼠标点击触发全选文本)。
|
||
pendingEditingTarget = null;
|
||
clearEditActivateTimer();
|
||
if (activeEditingRow === row) {
|
||
selectOnFocus.value = false;
|
||
activateEditing(row, focusCol);
|
||
nextTick(() => { selectOnFocus.value = true; });
|
||
} else {
|
||
activateEditing(row, focusCol);
|
||
}
|
||
lastEditActivateAt = performance.now();
|
||
}
|
||
|
||
watch(
|
||
[() => props.activeRowKey, () => props.dataSource],
|
||
async ([rowKey]) => {
|
||
rebindActiveEditingRow(props.dataSource);
|
||
await nextTick();
|
||
if (rowKey == null) {
|
||
tableRef.value?.setCurrentRow?.(undefined, { silent: true });
|
||
return;
|
||
}
|
||
const row = props.dataSource.find((item) => String(getRowKey(item)) === String(rowKey));
|
||
tableRef.value?.setCurrentRow?.(row, { silent: true });
|
||
},
|
||
{ immediate: true },
|
||
);
|
||
|
||
watch(
|
||
() => props.editable,
|
||
(editable) => {
|
||
if (editable) return;
|
||
pendingEditingTarget = null;
|
||
removeNavigationListeners();
|
||
pressedNavigationKeys.clear();
|
||
isRepeatNavigation = false;
|
||
clearEditActivateTimer();
|
||
clearActiveEditing();
|
||
tableRef.value?.clearSelectedArea?.();
|
||
},
|
||
);
|
||
|
||
onBeforeUnmount(() => {
|
||
removeNavigationListeners();
|
||
clearEditActivateTimer();
|
||
pressedNavigationKeys.clear();
|
||
});
|
||
|
||
defineExpose({
|
||
scrollToRow,
|
||
editRow,
|
||
getSelectedArea,
|
||
clearSelectedArea,
|
||
setRowExpand,
|
||
initVirtualScroll,
|
||
refreshLayout: initVirtualScroll,
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div ref="rootRef" class="fms-table" @keydown.capture="handleKeydownCapture">
|
||
<StkTable
|
||
ref="tableRef"
|
||
class="fms-table__inner"
|
||
v-bind="$attrs"
|
||
:style="customVars"
|
||
:row-height="rowHeight"
|
||
:header-row-height="headerRowHeight"
|
||
:virtual="virtual"
|
||
:virtual-x="virtualX"
|
||
:auto-resize="false"
|
||
:col-resizable="colResizable"
|
||
:header-drag="columnDragConfig"
|
||
:columns="displayColumns"
|
||
@update:columns="onColumnsUpdate"
|
||
:data-source="dataSource"
|
||
:drag-row-config="{ mode: rowDraggable ? 'insert' : 'none' }"
|
||
:row-key="rowKey"
|
||
:bordered="props.editable ? true : 'body-h'"
|
||
:no-data-full="noDataFull"
|
||
:show-overflow="showOverflow"
|
||
:area-selection="editable"
|
||
@area-selection-change="handleAreaSelectionChange"
|
||
@row-order-change="handleRowOrderChange"
|
||
>
|
||
<template v-if="slots.expand" #expand="slotProps">
|
||
<slot name="expand" v-bind="slotProps" />
|
||
</template>
|
||
<template v-if="slots.empty" #empty>
|
||
<slot name="empty" />
|
||
</template>
|
||
<template v-else #empty>
|
||
<a-result status="info" title="暂无数据" sub-title="当前没有可显示的内容" size="small" />
|
||
</template>
|
||
</StkTable>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.fms-table,
|
||
.fms-table__inner {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
</style>
|
||
|
||
<!-- 非 scoped 全局样式:选择列原生 checkbox 外观模拟 antdv-next Checkbox,
|
||
避免每行创建 Checkbox 组件实例导致的滚动性能退化 -->
|
||
<style lang="scss">
|
||
.fms-checkbox {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
height: 100%;
|
||
cursor: pointer;
|
||
line-height: 1;
|
||
margin: 0;
|
||
|
||
&.disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.45;
|
||
}
|
||
|
||
input {
|
||
position: absolute;
|
||
width: 0;
|
||
height: 0;
|
||
opacity: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.fms-checkbox-box {
|
||
display: block;
|
||
position: relative;
|
||
width: 16px;
|
||
height: 16px;
|
||
border: 1px solid #d9d9d9;
|
||
border-radius: 4px;
|
||
background: #fff;
|
||
transition: all 0.2s;
|
||
box-sizing: border-box;
|
||
flex: none;
|
||
}
|
||
|
||
&.checked .fms-checkbox-box {
|
||
background: #1677ff;
|
||
border-color: #1677ff;
|
||
|
||
&::after {
|
||
content: "";
|
||
position: absolute;
|
||
left: 5px;
|
||
top: 2px;
|
||
width: 4px;
|
||
height: 8px;
|
||
border: solid #fff;
|
||
border-width: 0 2px 2px 0;
|
||
transform: rotate(45deg);
|
||
}
|
||
}
|
||
|
||
&.indeterminate .fms-checkbox-box {
|
||
background: #1677ff;
|
||
border-color: #1677ff;
|
||
|
||
&::after {
|
||
content: "";
|
||
position: absolute;
|
||
left: 3px;
|
||
top: 7px;
|
||
width: 8px;
|
||
height: 2px;
|
||
background: #fff;
|
||
border: none;
|
||
border-radius: 2px;
|
||
transform: none;
|
||
}
|
||
}
|
||
}
|
||
|
||
.fms-cell-editor {
|
||
width: 100%;
|
||
height: calc(100% - 4px);
|
||
min-width: 0;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.fms-cell-display {
|
||
display: block;
|
||
min-width: 0;
|
||
user-select: text;
|
||
}
|
||
|
||
.fms-table__inner.text-overflow .fms-cell-display {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.fms-table__inner td:has(> .fms-cell-editor-root) {
|
||
padding-inline: 3px;
|
||
}
|
||
|
||
// actions 列按钮容器
|
||
.fms-actions-cell {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
// 操作按钮基础样式
|
||
.fms-action-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 3px;
|
||
border: none;
|
||
background: transparent;
|
||
cursor: pointer;
|
||
padding: 2px 4px;
|
||
color: #1677ff;
|
||
font-size: 12px;
|
||
line-height: 20px;
|
||
white-space: nowrap;
|
||
|
||
&:hover {
|
||
opacity: 0.75;
|
||
}
|
||
|
||
&:disabled {
|
||
cursor: not-allowed;
|
||
color: #bfbfbf;
|
||
opacity: 1;
|
||
}
|
||
|
||
&--danger {
|
||
color: #ff4d4f;
|
||
}
|
||
}
|
||
|
||
// "更多" 展开按钮样式
|
||
.fms-action-more {
|
||
color: #1677ff;
|
||
font-size: 12px;
|
||
line-height: 20px;
|
||
padding: 2px 4px;
|
||
|
||
&.fms-action-more-open {
|
||
opacity: 0.75;
|
||
}
|
||
}
|
||
</style>
|