Explorar el Código

1、用户列表新增用户解锁功能
2、新增公式编辑器工具

MY hace 1 mes
padre
commit
beac35d0eb

+ 3 - 0
JLHWEB/src/api/interface/index.ts

@@ -1189,6 +1189,9 @@ export namespace Basicinfo {
     successMsg: string;
     taskcodeList: any[];
   }
+  export interface ReqFormulaCheck {
+    formula: string;
+  }
 }
 
 export namespace SalePrice {

+ 18 - 0
JLHWEB/src/api/modules/basicinfo.ts

@@ -15,6 +15,10 @@ export const delUserList = (params: Basicinfo.ResUserList) => {
   return http.post<Basicinfo.ResUserList>(PORT1 + `/DelUserList`, params);
 };
 
+export const UnLockUser = (params: Basicinfo.ResUserList) => {
+  return http.post<Basicinfo.ResUserList>(PORT1 + `/UnLockUser`, params);
+};
+
 export const getSysFuncPwr = () => {
   return http.post<SalePrice.ResPriceList>(PORT1 + `/GetSysFuncPwr`, {});
 };
@@ -508,3 +512,17 @@ export const SaveMattressExtraType = (params: Basicinfo.ReqSaveMattressExtra) =>
 export const DeleteMattressExtraType = (params: Basicinfo.ReqDeleteBasicinfo) => {
   return http.post(PORT1 + `/DeleteMattressExtraType`, params);
 };
+
+/**
+ * @name 获取 公式
+ */
+export const GetFormulaVarList = () => {
+  return http.post(PORT1 + `/GetFormulaVarList`, {});
+};
+
+/**
+ * @name 检查 公式
+ */
+export const FormulaCheck = (params: Basicinfo.ReqFormulaCheck) => {
+  return http.post(PORT1 + `/FormulaCheck`, params);
+};

+ 15 - 1
JLHWEB/src/views/baseinfo/bednetvar/index.vue

@@ -20,6 +20,7 @@
           <el-button-group>
             <el-button @click="handleOpenNewTable">{{ $t("common.add") }}</el-button>
             <el-button @click="fDelete">{{ $t("common.delText") }}</el-button>
+            <el-button @click="handleClickFormula">编辑公式</el-button>
           </el-button-group>
           <el-tabs v-model="initParams.varkind" class="tableheader-tabs">
             <el-tab-pane label="床网" :name="0"></el-tab-pane>
@@ -66,11 +67,13 @@
       <Detail class="flx-1" :data="mainData" :status="orderStatus" :enum="enumMap" />
     </div>
   </LjDialog>
+
+  <FormulaEditorDialog ref="formulaEditorRef" @confirm="handleFormulaConfirm" />
 </template>
 
 <script setup lang="ts" name="baseinfo_bednetvarlist">
 import { ref, onMounted, provide } from "vue";
-import { getBedNetVarList, getBedNetVarMxList } from "@/api/modules/basicinfo";
+import { getBedNetVarList, getBedNetVarMxList, GetFormulaVarList } from "@/api/modules/basicinfo";
 import { ColumnProps } from "@/components/LjVxeTable/interface";
 import LjDrawer from "@/components/LjDrawer/index.vue";
 import Detail from "./detail.vue";
@@ -81,6 +84,7 @@ import { useI18n } from "vue-i18n";
 import { useAuthButtons } from "@/hooks/useAuthButtons";
 import { cloneDeep } from "lodash-es";
 import LjFoldLayout from "@/components/LjFoldLayout/index.vue";
+import FormulaEditorDialog from "@/views/system/formula-editor/index.vue";
 
 const dwname = "web_bednetvarlist";
 const dwname_mx = "web_bednetvarmxlist";
@@ -109,6 +113,8 @@ const layoutSetting = {
     hidden: false
   }
 };
+const showEditor = ref(false);
+const formulaEditorRef = ref();
 
 const { t } = useI18n();
 const { VxeTableRef, LjDetailRef, VxeTableMxRef, columns, columns_mx, fDelete, fSave, fChangeBedNetType } = useHooks(t, null);
@@ -153,6 +159,10 @@ const orderEditAction = [
   })
 ];
 
+const handleClickFormula = () => {
+  formulaEditorRef.value.show(true);
+};
+
 /*
  * @description 抽屉默认属性
  */
@@ -243,6 +253,10 @@ const handleClickTable = ({ row, rowIndex, $rowIndex, column, columnIndex, $colu
   fChangeBedNetType(parseInt(row.varclass));
 };
 
+const handleFormulaConfirm = (formula: string) => {
+  console.log(formula);
+};
+
 // 返回绑定的事件
 const tableEvents = {
   "cell-dblclick": handleDBlClickTable,

+ 11 - 1
JLHWEB/src/views/baseinfo/user/detail.vue

@@ -212,7 +212,7 @@ const deptData = inject("deptData", ref([]));
 const priceListData = inject("priceListData", ref([]));
 const userListData = inject("userListData", ref([]));
 
-const emit = defineEmits(["edit", "del", "cancel", "goto"]);
+const emit = defineEmits(["edit", "del", "cancel", "goto", "unlock"]);
 
 /**
  * @description 是否可编辑
@@ -250,6 +250,16 @@ const orderDefaultAction = [
 
       emit("del", mainData.value[0]);
     }
+  }),
+  buttonDefault({
+    label: "解锁",
+    icon: "iconUnlock_light",
+    clickFunc: () => {
+      emit("unlock", mainData.value[0]);
+    },
+    limited: () => {
+      return !mainData.value[0].isLocked;
+    }
   })
 ];
 

+ 23 - 2
JLHWEB/src/views/baseinfo/user/index.vue

@@ -45,6 +45,7 @@
         @cancel="handleCancel"
         @del="handleDel"
         @goto="autoGotoUser"
+        @unlock="handleUnLock"
       />
     </el-col>
   </el-row>
@@ -52,7 +53,7 @@
 
 <script setup lang="tsx" name="saleprice_priclist">
 import { ref, onMounted, provide, nextTick } from "vue";
-import { getUserList, getSysFuncPwr, getDept, delUserList } from "@/api/modules/basicinfo";
+import { getUserList, getSysFuncPwr, getDept, delUserList, UnLockUser } from "@/api/modules/basicinfo";
 import { getPriceList } from "@/api/modules/saleprice";
 import { ColumnProps } from "@/components/LjVxeTable/interface";
 import LjDrawer from "@/components/LjDrawer/index.vue";
@@ -134,7 +135,10 @@ provide("priceListData", priceListData);
 const fileClassListLayout = ref<any>({
   header: [
     {
-      value: "username"
+      value: "username",
+      icon: (item: any) => {
+        return item.isLocked ? <i class={["iconfont", "iconLock_light"]} /> : "";
+      }
     },
     {
       value: "descrp",
@@ -316,6 +320,23 @@ const searchEvent = () => {
     showUserList.value = userListData.value;
   }
 };
+
+const handleUnLock = (data: any) => {
+  if (data.empid == 0) {
+    ElMessage.error("系统管理员账号,不需要解锁");
+    return;
+  }
+  ElMessageBox.confirm(`确认需要解锁用户:${data.username}?`, t("sys.app.warning"), {
+    confirmButtonText: t("common.okText"),
+    cancelButtonText: t("common.cancelText"),
+    type: "error"
+  }).then(async () => {
+    await UnLockUser({ useridList: [data.empid] });
+
+    await getData();
+    handleSelectItem(userListData.value[0]);
+  });
+};
 </script>
 
 <style lang="scss">

+ 396 - 0
JLHWEB/src/views/system/formula-editor/editor.vue

@@ -0,0 +1,396 @@
+<template>
+  <div class="formula-container">
+    <el-row class="formula-editor">
+      <!-- 左侧编辑面板 -->
+      <el-col :xs="24" :span="16" class="left-panel">
+        <div class="editor-area">
+          <div class="operator-toolbar">
+            <el-button
+              v-for="op in operatorMap"
+              :key="op.value"
+              v-wave
+              class="operator-btn"
+              :title="op.name"
+              @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>
+              </el-button>
+              <template #dropdown>
+                <el-dropdown-menu class="func-menu">
+                  <el-dropdown-item v-for="func in functions" :key="func" :command="func" class="func-item">
+                    <span class="func-name">{{ func }}</span>
+                  </el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </div>
+
+          <el-input
+            ref="editorRef"
+            v-model="formulaValue"
+            type="textarea"
+            :rows="8"
+            placeholder="请输入公式,例如:SUM(【基础工资】*1.2 + 【绩效奖金】)"
+            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"
+            placeholder="搜索参数..."
+            clearable
+            class="search-box"
+            @input="handleSearchInput"
+            @keyup.enter="handleSearchEnter"
+            @clear="handleSearchClear"
+          />
+
+          <div class="tree-container">
+            <el-tree
+              ref="treeRef"
+              :data="props.mockParameters"
+              :props="defaultProps"
+              :filter-node-method="filterNode"
+              @node-click="handleNodeClick"
+              default-expand-all
+              node-key="text"
+            />
+          </div>
+        </div>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch, nextTick } from "vue";
+import { ElMessage, ElTree } from "element-plus";
+import { debounce, cloneDeepWith, isObject } from "lodash-es";
+import { pinyin } from "pinyin-pro";
+import { FormulaCheck } from "@/api/modules/basicinfo";
+
+interface TreeNode {
+  text: string;
+  value?: number;
+  children?: TreeNode[];
+}
+interface FormulaEditorProps {
+  /** 初始公式值 */
+  modelValue: string;
+  /** 参数树数据 */
+  mockParameters: TreeNode[];
+}
+
+// Props定义
+const props = withDefaults(defineProps<FormulaEditorProps>(), {
+  modelValue: "",
+  mockParameters: () => [] as TreeNode[]
+});
+
+// Emits定义
+const emits = defineEmits<{
+  "update:modelValue": [value: string]; // v-model标准事件
+}>();
+
+// 响应式状态
+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 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: "模"
+  }
+]);
+const functions = ["SUM", "AVG", "IF", "MAX", "MIN"];
+
+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;
+  }
+
+  // 核心匹配逻辑(不依赖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 hasChildMatch = node.childNodes?.some(child => child.data && filterNode(value, child.data, child));
+
+  // 保留逻辑:自身匹配或子节点匹配
+  const shouldShow = isSelfMatch || hasChildMatch;
+
+  return shouldShow;
+};
+
+// 优化有效性判断
+const isNodeValid = (node: TreeNode): boolean => {
+  // 有效条件:有code 或 有有效子节点
+  return !!node.value || (node.children?.some(isNodeValid) ?? false);
+};
+
+// 搜索处理逻辑
+const handleSearchInput = debounce((value: string) => {
+  treeRef.value?.filter(value);
+}, 300);
+
+// 回车键立即搜索
+const handleSearchEnter = () => {
+  treeRef.value?.filter(filterText.value);
+};
+
+// 清空时立即刷新
+const handleSearchClear = () => {
+  treeRef.value?.filter("");
+};
+
+const insertOperator = (op: string) => {
+  handleInsertFormula(op);
+};
+
+const insertFunction = (func: string) => {
+  handleInsertFormula(`${func}()`, -1);
+};
+
+const handleNodeClick = (data: TreeNode) => {
+  if (!data.value) 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);
+  // 更新光标位置
+  nextTick(() => {
+    // 计算新光标位置(插入内容末尾)
+    const newPos = start + data.length;
+    textarea.setSelectionRange(newPos + pm, newPos + pm);
+    textarea.focus();
+  });
+};
+
+const handleReset = () => {
+  formulaValue.value = "";
+};
+
+const handleCheck = async (): Promise<boolean> => {
+  if (!formulaValue.value) {
+    ElMessage.warning("请先输入公式");
+    return false;
+  }
+
+  try {
+    const result = await FormulaCheck({
+      formula: formulaValue.value
+    });
+
+    if (result) {
+      ElMessage.success("公式检查通过");
+      return true;
+    }
+    return false;
+  } catch (error) {
+    return false;
+  }
+};
+
+// 暴露方法供父组件调用
+defineExpose({
+  reset: handleReset,
+  check: handleCheck
+});
+</script>
+
+<style scoped lang="scss">
+.formula-container {
+  height: 100%;
+  width: 100%;
+  min-height: 510px;
+  --editor-border: #dcdfe6;
+  --panel-bg: #f8f9fa;
+
+  .formula-editor {
+    height: calc(100% - 20px);
+    margin-top: 12px;
+
+    .left-panel {
+      .editor-area {
+        // border-color: var(--editor-border);
+        // box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+      }
+
+      .operator-toolbar {
+        gap: 12px;
+        .operator-btn {
+          min-width: 36px;
+          padding: 8px 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);
+        }
+      }
+    }
+
+    .right-panel {
+      .search-box {
+        padding: 0 12px;
+        flex-shrink: 0;
+      }
+
+      .tree-container {
+        .el-tree {
+          padding: 0px 12px;
+        }
+      }
+    }
+  }
+
+  .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;
+  }
+
+  .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;
+    }
+
+    .tree-container {
+      flex: 1;
+      min-height: 0; /* 关键修复 */
+      overflow: hidden;
+
+      .el-tree {
+        height: 100%;
+        :deep(.el-tree-node__content) {
+          padding: 6px 0;
+        }
+      }
+    }
+  }
+
+  .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>

+ 185 - 0
JLHWEB/src/views/system/formula-editor/index.vue

@@ -0,0 +1,185 @@
+<template>
+  <LjDialog
+    ref="LjSelectorRef"
+    class="is-selector lj-selector"
+    v-bind="{
+      ...drawerDefineProp
+    }"
+    :modal="false"
+    :style="{ height: '60%' }"
+  >
+    <template #header>
+      <div class="flx-1">
+        <span class="text-h5-b">{{ props.title }}</span>
+      </div>
+    </template>
+    <div class="flx-col h-full">
+      <LjHeaderMenu :update="dialogVisible" :data="initParams" :action="btnGroup" />
+      <LjFoldLayoutDouble v-bind="showLayout" class="flx-1 overflow-hidden">
+        <template #secondMain>
+          <div class="flx h-full">
+            <div class="flx-col w-full">
+              <FormulaEditor ref="formulaEditorRef" v-model="localFormula" :mock-parameters="mockParameters" />
+            </div>
+          </div>
+        </template>
+      </LjFoldLayoutDouble>
+    </div>
+  </LjDialog>
+</template>
+<script setup lang="ts">
+import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from "vue";
+import LjDialog from "@/components/LjDialog/index.vue";
+import LjFoldLayoutDouble from "@/components/LjFoldLayoutDouble/index.vue";
+import FormulaEditor from "./editor.vue";
+import { GetFormulaVarList, FormulaCheck } from "@/api/modules/basicinfo";
+import { useAuthButtons } from "@/hooks/useAuthButtons";
+import { useI18n } from "vue-i18n";
+
+const { t } = useI18n();
+const { buttonDefault } = useAuthButtons(t);
+
+const props = defineProps({
+  title: {
+    type: String,
+    default: "公式编辑器"
+  }
+});
+
+const emit = defineEmits(["confirm"]);
+const localFormula = ref<string>("");
+const mockParameters = ref([]);
+
+const LjSelectorRef = ref();
+const dialogVisible = ref(false);
+
+const formulaEditorRef = ref();
+/*
+ * @description 抽屉默认属性
+ */
+const drawerDefineProp = {
+  draggable: true,
+  overflow: true,
+  width: "60%",
+  modalClass: "lj-file-dialog"
+};
+const showLayout = ref<any>(false);
+const initParams = ref<any>();
+const btnGroup = ref<any>([
+  buttonDefault({
+    label: "取消",
+    clickFunc: item => {
+      hide();
+    }
+  }),
+  buttonDefault({
+    label: "清空",
+    clickFunc: item => {
+      formulaEditorRef.value.reset();
+    }
+  }),
+  buttonDefault({
+    label: "检查公式",
+    clickFunc: item => {
+      formulaEditorRef.value.check();
+    }
+  }),
+  buttonDefault({
+    label: "确认",
+    clickFunc: item => {
+      handleConfirm();
+    }
+  })
+]);
+
+const handleConfirm = async () => {
+  // 调用子组件检查方法
+  const isValid = await formulaEditorRef.value?.check();
+
+  if (!isValid) {
+    return;
+  }
+
+  // 提交有效公式
+  emit("confirm", localFormula.value);
+  hide();
+};
+
+const getFormulaVarList = async () => {
+  const res = (await GetFormulaVarList()) as any;
+  mockParameters.value = res.recursionList;
+};
+
+/**
+ * @description 展示组件
+ * @param params 入参
+ * @param activeName 多选时 活动面板入参
+ */
+const show = (refresh?: boolean, formula: string = "") => {
+  localFormula.value = formula;
+  LjSelectorRef.value.show(refresh);
+  nextTick(() => {
+    dialogVisible.value = true;
+  });
+};
+
+/**
+ * @description cancel 取消
+ */
+const hide = () => {
+  // 关闭弹窗
+  LjSelectorRef.value.hide();
+  dialogVisible.value = false;
+};
+
+onMounted(() => {
+  getFormulaVarList();
+});
+
+defineExpose({
+  show,
+  hide
+});
+</script>
+
+<style lang="scss">
+.lj-selector {
+  padding: 0;
+  .el-tree {
+    background: unset;
+  }
+}
+.ljselector-aside-collapse {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  // background-color: $color-gray-2;
+
+  .el-collapse-item {
+    display: flex;
+    flex-direction: column;
+    overflow: auto;
+    height: auto;
+
+    &.is-active {
+      flex: 1;
+    }
+    &:not(.is-active) {
+      flex-shrink: 0;
+    }
+    .el-collapse-item__header {
+      flex-shrink: 0;
+    }
+    .el-collapse-item__wrap {
+      flex: 1;
+      overflow: auto;
+      background-color: $color-gray-2;
+      padding: 8px 4px;
+
+      .lj-infor-item {
+        background-color: $color-gray-1;
+      }
+    }
+  }
+}
+</style>