|
|
@@ -11,10 +11,11 @@
|
|
|
v-wave
|
|
|
class="operator-btn"
|
|
|
:title="op.name"
|
|
|
- @click="insertOperator(op.value)"
|
|
|
+ @click="() => insertOperator(op.value)"
|
|
|
>
|
|
|
<span class="symbol">{{ op.symbol }}</span>
|
|
|
</el-button>
|
|
|
+
|
|
|
<el-dropdown @command="insertFunction" trigger="click">
|
|
|
<el-button type="primary" class="func-btn">
|
|
|
<span>函数列表</span>
|
|
|
@@ -38,43 +39,33 @@
|
|
|
resize="none"
|
|
|
class="editor-input"
|
|
|
spellcheck="false"
|
|
|
- >
|
|
|
- </el-input>
|
|
|
-
|
|
|
- <!-- <div class="action-buttons">
|
|
|
- <el-button type="default" @click="handleReset">清空</el-button>
|
|
|
- <el-button type="warning" @click="handleCheck">检查公式</el-button>
|
|
|
- <el-button type="primary" @click="handleConfirm">确认</el-button>
|
|
|
- </div> -->
|
|
|
+ />
|
|
|
</div>
|
|
|
</el-col>
|
|
|
|
|
|
<!-- 右侧参数面板 -->
|
|
|
<el-col :xs="24" :span="8" class="right-panel">
|
|
|
<div class="param-tree">
|
|
|
- <!-- <div class="header">
|
|
|
- <span>参数选择</span>
|
|
|
- </div> -->
|
|
|
-
|
|
|
<el-input
|
|
|
v-model="filterText"
|
|
|
+ size="default"
|
|
|
placeholder="搜索参数..."
|
|
|
clearable
|
|
|
class="search-box"
|
|
|
- @input="handleSearchInput"
|
|
|
- @keyup.enter="handleSearchEnter"
|
|
|
- @clear="handleSearchClear"
|
|
|
+ @input="onFilterInput"
|
|
|
+ @keyup.enter="onFilterEnter"
|
|
|
+ @clear="onFilterClear"
|
|
|
/>
|
|
|
|
|
|
<div class="tree-container">
|
|
|
- <el-tree
|
|
|
+ <el-tree-v2
|
|
|
ref="treeRef"
|
|
|
- :data="props.mockParameters"
|
|
|
+ :data="treeData"
|
|
|
:props="defaultProps"
|
|
|
:filter-node-method="filterNode"
|
|
|
@node-click="handleNodeClick"
|
|
|
- default-expand-all
|
|
|
node-key="text"
|
|
|
+ :height="treeHeight"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -83,201 +74,231 @@
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
-<script setup lang="ts">
|
|
|
-import { ref, computed, watch, nextTick } from "vue";
|
|
|
+<script lang="ts" setup>
|
|
|
+import { ref, watch, nextTick, computed, onMounted, onUnmounted } from "vue";
|
|
|
import { ElMessage, ElTree } from "element-plus";
|
|
|
-import { debounce, cloneDeepWith, isObject } from "lodash-es";
|
|
|
+import { debounce } from "lodash-es";
|
|
|
import { pinyin } from "pinyin-pro";
|
|
|
import { FormulaCheck } from "@/api/modules/basicinfo";
|
|
|
|
|
|
+/**
|
|
|
+ *
|
|
|
+ */
|
|
|
interface TreeNode {
|
|
|
text: string;
|
|
|
- value?: number;
|
|
|
+ value?: number | string | null;
|
|
|
children?: TreeNode[];
|
|
|
}
|
|
|
-interface FormulaEditorProps {
|
|
|
- /** 初始公式值 */
|
|
|
+
|
|
|
+interface Props {
|
|
|
modelValue: string;
|
|
|
- /** 参数树数据 */
|
|
|
- mockParameters: TreeNode[];
|
|
|
+ treeData: TreeNode[];
|
|
|
}
|
|
|
|
|
|
-// Props定义
|
|
|
-const props = withDefaults(defineProps<FormulaEditorProps>(), {
|
|
|
+const props = withDefaults(defineProps<Props>(), {
|
|
|
modelValue: "",
|
|
|
- mockParameters: () => [] as TreeNode[]
|
|
|
+ treeData: () => []
|
|
|
});
|
|
|
|
|
|
-// Emits定义
|
|
|
const emits = defineEmits<{
|
|
|
- "update:modelValue": [value: string]; // v-model标准事件
|
|
|
+ "update:modelValue": [v: string];
|
|
|
}>();
|
|
|
|
|
|
-// 响应式状态
|
|
|
+// 引用与状态
|
|
|
+const editorRef = ref<any | null>(null);
|
|
|
+const treeRef = ref<InstanceType<typeof ElTree> | null>(null);
|
|
|
const formulaValue = ref<string>(props.modelValue);
|
|
|
-const filterText = ref("");
|
|
|
-const treeRef = ref<InstanceType<typeof ElTree>>();
|
|
|
-// 调整默认props配置
|
|
|
-const defaultProps = {
|
|
|
- children: "children",
|
|
|
- label: "text" // 与ElementUI Tree组件默认字段对齐
|
|
|
-};
|
|
|
-// 监听本地修改
|
|
|
-watch(formulaValue, newVal => {
|
|
|
- emits("update:modelValue", newVal);
|
|
|
+const filterText = ref<string>("");
|
|
|
+const treeHeight = ref<number>(200); // 默认高度
|
|
|
+
|
|
|
+// 监听v-model同步
|
|
|
+watch(formulaValue, val => {
|
|
|
+ emits("update:modelValue", val);
|
|
|
});
|
|
|
|
|
|
-// 常量
|
|
|
+/**
|
|
|
+ * 左边区域
|
|
|
+ */
|
|
|
const operatorMap = ref([
|
|
|
- {
|
|
|
- value: "+",
|
|
|
- symbol: "+",
|
|
|
- name: "加"
|
|
|
- },
|
|
|
- {
|
|
|
- value: "-",
|
|
|
- symbol: "-",
|
|
|
- name: "减"
|
|
|
- },
|
|
|
- {
|
|
|
- value: "*",
|
|
|
- symbol: "×",
|
|
|
- name: "乘"
|
|
|
- },
|
|
|
- {
|
|
|
- value: "/",
|
|
|
- symbol: "÷",
|
|
|
- name: "除"
|
|
|
- },
|
|
|
- {
|
|
|
- value: "(",
|
|
|
- symbol: "(",
|
|
|
- name: "左括号"
|
|
|
- },
|
|
|
- {
|
|
|
- value: ")",
|
|
|
- symbol: ")",
|
|
|
- name: "右括号"
|
|
|
- },
|
|
|
- {
|
|
|
- value: "**",
|
|
|
- symbol: "^",
|
|
|
- name: "幂"
|
|
|
- },
|
|
|
- {
|
|
|
- value: "%",
|
|
|
- symbol: "%",
|
|
|
- name: "模"
|
|
|
- }
|
|
|
+ { value: "+", symbol: "+", name: "加" },
|
|
|
+ { value: "-", symbol: "-", name: "减" },
|
|
|
+ { value: "*", symbol: "×", name: "乘" },
|
|
|
+ { value: "/", symbol: "÷", name: "除" },
|
|
|
+ { value: "(", symbol: "(", name: "左括号" },
|
|
|
+ { value: ")", symbol: ")", name: "右括号" },
|
|
|
+ { value: "**", symbol: "^", name: "幂" },
|
|
|
+ { value: "%", symbol: "%", name: "模" }
|
|
|
]);
|
|
|
const functions = ["SUM", "AVG", "IF", "MAX", "MIN"];
|
|
|
|
|
|
+// 插入运算符
|
|
|
+const insertOperator = (op: string) => handleInsertFormula(op);
|
|
|
+// 插入函数(光标定位到括号内)
|
|
|
+const insertFunction = (func: string) => handleInsertFormula(`${func}()`, { cursorOffset: -1 });
|
|
|
+
|
|
|
+// 重置公式
|
|
|
+const reset = () => {
|
|
|
+ formulaValue.value = "";
|
|
|
+};
|
|
|
+
|
|
|
+// 校验公式
|
|
|
+const handleCheck = async (): Promise<boolean> => {
|
|
|
+ if (!formulaValue.value?.trim()) {
|
|
|
+ // ElMessage.warning("请先输入公式");
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const result = await FormulaCheck({
|
|
|
+ formula: formulaValue.value
|
|
|
+ });
|
|
|
+
|
|
|
+ if (result) {
|
|
|
+ ElMessage.success("公式检查通过");
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ } catch (error) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 右边区域
|
|
|
+ */
|
|
|
+const defaultProps = {
|
|
|
+ value: "value",
|
|
|
+ label: "text",
|
|
|
+ children: "children"
|
|
|
+};
|
|
|
+
|
|
|
+// 搜索优化:扁平索引 + 拼音缓存
|
|
|
+const pinyinCache = new Map<string, string>();
|
|
|
+// 构建扁平索引(提升搜索性能)
|
|
|
+const buildIndex = (list: TreeNode[], parentPath: string[] = []) => {
|
|
|
+ const result: { node: TreeNode; path: string[] }[] = [];
|
|
|
+ for (const n of list) {
|
|
|
+ const currentPath = parentPath.concat(n.text);
|
|
|
+ result.push({ node: n, path: currentPath });
|
|
|
+ if (n.children && n.children.length) {
|
|
|
+ result.push(...buildIndex(n.children, currentPath));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+};
|
|
|
+
|
|
|
+// 节点过滤方法(使用缓存提升性能)
|
|
|
const filterNode = (value: string, data: TreeNode, node: any): boolean => {
|
|
|
- // 空搜索时保留有效结构
|
|
|
+ // 空搜索:保留有效节点(有值或有子节点)
|
|
|
if (!value) {
|
|
|
- // 空搜索时:保留所有有效节点及其祖先路径
|
|
|
- const selfValid = !!data.value || data.children !== undefined;
|
|
|
- const keepForStructure = node.parent?.expanded || selfValid;
|
|
|
- return keepForStructure;
|
|
|
+ const selfValid = !!data.value || !!(data.children && data.children.length);
|
|
|
+ return selfValid;
|
|
|
}
|
|
|
|
|
|
- // 核心匹配逻辑(不依赖pinyinFilterTree的树裁剪)
|
|
|
- const isSelfMatch = [data.text].some(
|
|
|
- text =>
|
|
|
- text &&
|
|
|
- (text.includes(value) ||
|
|
|
- pinyin(text, { toneType: "none", type: "array" }).join("").includes(value.toLowerCase()) ||
|
|
|
- pinyin(text, { pattern: "first", type: "array", toneType: "none" }).join("").includes(value.toLowerCase()))
|
|
|
- );
|
|
|
+ const q = value.trim().toLowerCase();
|
|
|
|
|
|
- // 子节点匹配状态(需要展开父节点)
|
|
|
- const hasChildMatch = node.childNodes?.some(child => child.data && filterNode(value, child.data, child));
|
|
|
+ // 文本与拼音匹配检查
|
|
|
+ const text = data.text || "";
|
|
|
+ const cachedPinyin = pinyinCache.get(text) ?? pinyin(text, { toneType: "none", type: "array" }).join("").toLowerCase();
|
|
|
+ if (!pinyinCache.has(text)) pinyinCache.set(text, cachedPinyin);
|
|
|
|
|
|
- // 保留逻辑:自身匹配或子节点匹配
|
|
|
- const shouldShow = isSelfMatch || hasChildMatch;
|
|
|
+ const selfMatch = (text && text.toLowerCase().includes(q)) || (cachedPinyin && cachedPinyin.includes(q));
|
|
|
|
|
|
- return shouldShow;
|
|
|
-};
|
|
|
+ // 子节点匹配检查
|
|
|
+ if (selfMatch) return true;
|
|
|
+ if (data.children && data.children.length) {
|
|
|
+ return data.children.some(child => filterNode(value, child, node));
|
|
|
+ }
|
|
|
|
|
|
-// 优化有效性判断
|
|
|
-const isNodeValid = (node: TreeNode): boolean => {
|
|
|
- // 有效条件:有code 或 有有效子节点
|
|
|
- return !!node.value || (node.children?.some(isNodeValid) ?? false);
|
|
|
+ return false;
|
|
|
};
|
|
|
|
|
|
-// 搜索处理逻辑
|
|
|
-const handleSearchInput = debounce((value: string) => {
|
|
|
- treeRef.value?.filter(value);
|
|
|
-}, 300);
|
|
|
+// 防抖处理搜索输入
|
|
|
+const doFilter = debounce((val: string) => {
|
|
|
+ treeRef.value?.filter?.(val ?? "");
|
|
|
+}, 200);
|
|
|
|
|
|
-// 回车键立即搜索
|
|
|
-const handleSearchEnter = () => {
|
|
|
- treeRef.value?.filter(filterText.value);
|
|
|
+const onFilterInput = (val: string) => {
|
|
|
+ doFilter(val);
|
|
|
};
|
|
|
-
|
|
|
-// 清空时立即刷新
|
|
|
-const handleSearchClear = () => {
|
|
|
- treeRef.value?.filter("");
|
|
|
+const onFilterEnter = () => {
|
|
|
+ treeRef.value?.filter?.(filterText.value ?? "");
|
|
|
+};
|
|
|
+const onFilterClear = () => {
|
|
|
+ filterText.value = "";
|
|
|
+ treeRef.value?.filter?.("");
|
|
|
};
|
|
|
|
|
|
-const insertOperator = (op: string) => {
|
|
|
- handleInsertFormula(op);
|
|
|
+// 插入公式辅助方法(精确控制光标位置)
|
|
|
+const getTextarea = (): HTMLTextAreaElement | null => {
|
|
|
+ const el = (editorRef.value as any)?.$el ?? null;
|
|
|
+ if (!el) return null;
|
|
|
+ return el.querySelector("textarea");
|
|
|
};
|
|
|
|
|
|
-const insertFunction = (func: string) => {
|
|
|
- handleInsertFormula(`${func}()`, -1);
|
|
|
+const handleInsertFormula = async (insertText: string, options?: { cursorOffset?: number }) => {
|
|
|
+ if (!insertText) return;
|
|
|
+ await nextTick();
|
|
|
+ const textarea = getTextarea();
|
|
|
+ if (!textarea) {
|
|
|
+ // 降级处理:直接拼接到末尾
|
|
|
+ formulaValue.value = formulaValue.value + insertText;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const start = textarea.selectionStart ?? textarea.value.length;
|
|
|
+ const end = textarea.selectionEnd ?? textarea.value.length;
|
|
|
+
|
|
|
+ // 插入文本
|
|
|
+ formulaValue.value = formulaValue.value.slice(0, start) + insertText + formulaValue.value.slice(end);
|
|
|
+
|
|
|
+ await nextTick();
|
|
|
+ // 调整光标位置
|
|
|
+ const offset = options?.cursorOffset ?? 0;
|
|
|
+ const newPos = start + insertText.length + offset;
|
|
|
+ try {
|
|
|
+ textarea.setSelectionRange(newPos, newPos);
|
|
|
+ textarea.focus();
|
|
|
+ } catch (err) {
|
|
|
+ textarea.focus();
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
+// 点击树节点插入参数
|
|
|
const handleNodeClick = (data: TreeNode) => {
|
|
|
- if (!data.value) return;
|
|
|
+ if (data.children?.length < 100) return;
|
|
|
handleInsertFormula(`【${data.text}】`);
|
|
|
};
|
|
|
|
|
|
-const handleInsertFormula = (data: string, pm: number = 0) => {
|
|
|
- if (!data) return;
|
|
|
- const textarea = document.querySelector(".editor-input textarea") as HTMLTextAreaElement;
|
|
|
- if (!textarea) return;
|
|
|
- const start = textarea.selectionStart;
|
|
|
- const end = textarea.selectionEnd;
|
|
|
- // 更新公式值
|
|
|
- formulaValue.value = formulaValue.value.slice(0, start) + data + formulaValue.value.slice(end);
|
|
|
- // 更新光标位置
|
|
|
+// 动态计算树高度
|
|
|
+const calculateTreeHeight = () => {
|
|
|
nextTick(() => {
|
|
|
- // 计算新光标位置(插入内容末尾)
|
|
|
- const newPos = start + data.length;
|
|
|
- textarea.setSelectionRange(newPos + pm, newPos + pm);
|
|
|
- textarea.focus();
|
|
|
+ const treeContainer = document.querySelector(".tree-container") as HTMLElement;
|
|
|
+ if (treeContainer) {
|
|
|
+ // 获取tree-container的实际高度,减去可能的padding/margin
|
|
|
+ const containerHeight = treeContainer.clientHeight;
|
|
|
+ // 设置一个最小高度,确保树至少有200px高
|
|
|
+ treeHeight.value = Math.max(containerHeight, 200);
|
|
|
+ }
|
|
|
});
|
|
|
};
|
|
|
|
|
|
-const handleReset = () => {
|
|
|
- formulaValue.value = "";
|
|
|
+// 监听窗口大小变化
|
|
|
+const handleResize = () => {
|
|
|
+ calculateTreeHeight();
|
|
|
};
|
|
|
|
|
|
-const handleCheck = async (): Promise<boolean> => {
|
|
|
- if (!formulaValue.value) {
|
|
|
- ElMessage.warning("请先输入公式");
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- const result = await FormulaCheck({
|
|
|
- formula: formulaValue.value
|
|
|
- });
|
|
|
+onMounted(() => {
|
|
|
+ calculateTreeHeight();
|
|
|
+ window.addEventListener("resize", handleResize);
|
|
|
+});
|
|
|
|
|
|
- if (result) {
|
|
|
- ElMessage.success("公式检查通过");
|
|
|
- return true;
|
|
|
- }
|
|
|
- return false;
|
|
|
- } catch (error) {
|
|
|
- return false;
|
|
|
- }
|
|
|
-};
|
|
|
+onUnmounted(() => {
|
|
|
+ window.removeEventListener("resize", handleResize);
|
|
|
+});
|
|
|
|
|
|
-// 暴露方法供父组件调用
|
|
|
+// 暴露方法给父组件
|
|
|
defineExpose({
|
|
|
- reset: handleReset,
|
|
|
+ reset,
|
|
|
check: handleCheck
|
|
|
});
|
|
|
</script>
|
|
|
@@ -286,111 +307,100 @@ defineExpose({
|
|
|
.formula-container {
|
|
|
height: 100%;
|
|
|
width: 100%;
|
|
|
- min-height: 510px;
|
|
|
--editor-border: #dcdfe6;
|
|
|
--panel-bg: #f8f9fa;
|
|
|
|
|
|
.formula-editor {
|
|
|
height: calc(100% - 20px);
|
|
|
margin-top: 12px;
|
|
|
+ display: flex;
|
|
|
|
|
|
.left-panel {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+
|
|
|
.editor-area {
|
|
|
- // border-color: var(--editor-border);
|
|
|
- // box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ padding: 8px;
|
|
|
}
|
|
|
|
|
|
.operator-toolbar {
|
|
|
gap: 12px;
|
|
|
- .operator-btn {
|
|
|
- min-width: 36px;
|
|
|
- padding: 8px 12px;
|
|
|
- }
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ padding-bottom: 12px;
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- .right-panel {
|
|
|
- .param-tree {
|
|
|
- border-color: var(--editor-border);
|
|
|
- padding: 8px;
|
|
|
-
|
|
|
- .header {
|
|
|
- padding: 12px 16px;
|
|
|
- font-size: 14px;
|
|
|
- background: white;
|
|
|
- }
|
|
|
-
|
|
|
- .el-tree-node__content:hover {
|
|
|
- background: rgba(64, 158, 255, 0.08);
|
|
|
- }
|
|
|
+ .operator-btn {
|
|
|
+ min-width: 36px;
|
|
|
+ padding: 8px 12px;
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- .right-panel {
|
|
|
- .search-box {
|
|
|
- padding: 0 12px;
|
|
|
- flex-shrink: 0;
|
|
|
- }
|
|
|
+ .editor-input {
|
|
|
+ border-radius: 4px;
|
|
|
+ transition: border-color 0.2s;
|
|
|
|
|
|
- .tree-container {
|
|
|
- .el-tree {
|
|
|
- padding: 0px 12px;
|
|
|
+ &:focus-within {
|
|
|
+ border-color: #4096ff;
|
|
|
+ box-shadow: 0 0 0 2px rgba(64, 150, 255, 0.2);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- .editor-area {
|
|
|
- flex: 1;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- // border: 1px solid var(--el-border-color);
|
|
|
- // border-radius: 4px;
|
|
|
- // background: var(--el-bg-color);
|
|
|
- padding: 8px;
|
|
|
- }
|
|
|
+ .right-panel {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
|
|
|
- .param-tree {
|
|
|
- flex: 1;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- // border: 1px solid var(--el-border-color);
|
|
|
- // border-radius: 4px;
|
|
|
- // background: var(--el-bg-color);
|
|
|
- height: 100%;
|
|
|
-
|
|
|
- .header {
|
|
|
- padding: 16px;
|
|
|
- font-weight: 500;
|
|
|
- }
|
|
|
+ .param-tree {
|
|
|
+ padding: 8px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100%;
|
|
|
|
|
|
- .tree-container {
|
|
|
- flex: 1;
|
|
|
- min-height: 0; /* 关键修复 */
|
|
|
- overflow: hidden;
|
|
|
+ .search-box {
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ }
|
|
|
|
|
|
- .el-tree {
|
|
|
- height: 100%;
|
|
|
- :deep(.el-tree-node__content) {
|
|
|
- padding: 6px 0;
|
|
|
+ .tree-container {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ /* 允许垂直滚动,禁止水平滚动 */
|
|
|
+ overflow-y: auto;
|
|
|
+ overflow-x: hidden;
|
|
|
+ /* 添加边框样式 */
|
|
|
+ border: 1px solid var(--editor-border);
|
|
|
+ border-radius: 4px;
|
|
|
+
|
|
|
+ /* 优化滚动条样式 */
|
|
|
+ &::-webkit-scrollbar {
|
|
|
+ width: 6px;
|
|
|
+ }
|
|
|
+ &::-webkit-scrollbar-thumb {
|
|
|
+ background-color: #ddd;
|
|
|
+ border-radius: 3px;
|
|
|
+ }
|
|
|
+ &::-webkit-scrollbar-track {
|
|
|
+ background-color: #f5f5f5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-tree {
|
|
|
+ padding: 0 12px;
|
|
|
+ height: 100%;
|
|
|
+
|
|
|
+ :deep(.el-tree-node__content) {
|
|
|
+ padding: 6px 0;
|
|
|
+ cursor: pointer;
|
|
|
+ &:hover {
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- .operator-toolbar {
|
|
|
- display: flex;
|
|
|
- gap: 8px;
|
|
|
- flex-wrap: wrap;
|
|
|
- padding-bottom: 16px;
|
|
|
- }
|
|
|
-
|
|
|
- .action-buttons {
|
|
|
- margin-top: auto;
|
|
|
- padding-top: 16px;
|
|
|
- display: flex;
|
|
|
- gap: 12px;
|
|
|
- justify-content: flex-end;
|
|
|
- }
|
|
|
}
|
|
|
</style>
|