xiaochan пре 1 година
родитељ
комит
bb416bddbe

+ 152 - 0
src/components/cy/combo-grid/ComboGridTest.vue

@@ -0,0 +1,152 @@
+<template>
+
+  表单模式:
+  <el-form :model="a" :rules="rules" inline>
+    <el-form-item prop="b" label="select模式">
+      <cy-combo-grid v-model="a.b"
+                     clearable
+                     :data="data"
+                     style="width: 240px"
+                     :collapse-tags="collapseTags"
+                     :remote-method="method2"
+      >
+      </cy-combo-grid>
+    </el-form-item>
+    <el-form-item prop="a">
+      <el-input style="width: 120px" v-model="a.a"/>
+    </el-form-item>
+  </el-form>
+
+  绑定对象:
+  <cy-combo-grid v-model="a.c"
+                 value="code"
+                 :data="data"
+                 label="name"/>
+
+  绑定数组:
+  <cy-combo-grid v-model="a.d"
+                 multiple
+                 :data="data"
+  />
+  绑定数组可排序:
+  <cy-combo-grid v-model="a.d"
+                 multiple
+                 sortable
+                 :data="data"
+  />
+
+  <div style="height: 600px ; width: 100%" ref="monacoRef">
+
+  </div>
+
+
+</template>
+
+<script setup lang="ts">
+import CyComboGrid from "@/components/cy/combo-grid/src/CyComboGrid.vue";
+import UseCreateMonaco from "@/utils/monaco-utlis/useCreateMonaco";
+import sleep from "@/utils/sleep";
+import {ElForm, ElFormItem, ElInput} from "element-plus";
+
+const collapseTags = ref(false)
+
+const a = ref({
+  b: '',
+  a: '',
+  c: {
+    code: '',
+    name: ''
+  },
+  d: [],
+  e: []
+})
+
+const monacoRef = ref<HTMLDivElement>()
+
+const data = ref<any[]>([])
+
+const rules = {
+  b: [{required: true, message: '不能为空', trigger: 'blur'}],
+  a: [{required: true, message: '不能为空', trigger: 'blur'}],
+}
+
+let monaco: any = null
+
+function method(val: any) {
+  console.log(val)
+}
+
+function method2(val: any) {
+  return new Promise(resolve => {
+    setTimeout(() => {
+      const b = [];
+      for (let i = 0; i < 10000; i++) {
+        b.push({
+          value: val + i,
+          label: val + i,
+        })
+      }
+      resolve(b)
+    }, 500)
+  })
+}
+
+watch(() => a.value, async () => {
+  monaco && monaco.setValue(JSON.stringify(a.value))
+  monaco && monaco.formatDocument()
+}, {flush: 'post', deep: true})
+
+function forEachaa<D>(list: D[], iterate: (item: D, index: number, list: D[]) => boolean | null) {
+  if (!list) {
+    return
+  }
+
+  for (let i = 0, len = list.length; i < len; i++) {
+    const a = iterate(list[i], i, list)
+    if (a) {
+      return;
+    }
+  }
+
+}
+
+onMounted(async () => {
+  const b = [];
+  for (let i = 0; i < 10000; i++) {
+    b.push({
+      value: 'code' + i,
+      label: 'name' + i,
+    })
+  }
+  data.value = b
+
+  await nextTick();
+  // @ts-ignore
+  monaco = new UseCreateMonaco(monacoRef!.value, {
+    language: 'json',
+    value: JSON.stringify(a.value),
+  })
+
+  await sleep(500)
+  monaco.formatDocument()
+
+  monaco!.onDidBlurEditorText(() => {
+    a.value = JSON.parse(monaco?.getValue())
+  });
+
+
+  forEachaa(b, (item, index) => {
+    console.log(index)
+    if (index === 100) {
+      return true
+    }
+  })
+
+})
+
+
+</script>
+
+<style lang="scss">
+
+</style>

+ 300 - 0
src/components/cy/combo-grid/src/CyComboGrid.vue

@@ -0,0 +1,300 @@
+<script setup lang="ts">
+import UesComboGrid from "./index";
+import '../style/index.scss'
+import {VxeTable} from "vxe-table";
+import {CaretBottom, CircleClose} from "@element-plus/icons-vue";
+import XEUtils, {isArray} from "xe-utils";
+import {ElTag} from "element-plus";
+
+export declare type TableHeader =
+    { code: string, name: string, width?: string | number | undefined }
+
+export declare type CyComboGridOptions = {
+  modelValue: string | number | any[] | object;
+  data?: any;
+  placement?: string;
+  teleported?: boolean;
+  select?: boolean;
+  effect?: 'light' | 'dark';
+  tableHeader?: TableHeader[];
+  remoteMethod?: (value: string) => null | Promise<any[] | unknown>;
+  debounce?: number;
+  disabled?: boolean;
+  clearable?: boolean;
+  value?: string;
+  label?: string;
+  valueLabel?: string;
+  placeholder?: string;
+  multiple?: boolean;
+  collapseTags?: boolean;
+  maxCollapseTags?: number;
+  keys?: boolean;
+  sortable?: boolean
+}
+
+const props = withDefaults(defineProps<CyComboGridOptions>(), {
+  placement: 'bottom-start',
+  teleported: true,
+  select: true,
+  effect: 'light',
+  tableHeader: () => [
+    {code: 'value', name: '编码', width: '125px'},
+    {code: 'label', name: '名称', width: '135px'}
+  ],
+  debounce: 300,
+  disabled: false,
+  clearable: false,
+  placeholder: "可编码,拼音,五笔,名称搜索",
+  multiple: false,
+  collapseTags: false,
+  maxCollapseTags: 1,
+  keys: false,
+  sortable: false
+})
+
+const emits = defineEmits<{
+  (e: "update:modelValue", value: any): void,
+  (e: 'focus', value: any): void,
+  (e: 'blur', value: any): void,
+  (e: 'update:data', value: any): void,
+  (e: 'visible-change', value: boolean): void,
+}>()
+
+const {
+  nsSelect,
+  handleFocus,
+  inputRef,
+  selectionRef,
+  calculatorRef,
+  handleBlur,
+  isFocused,
+  handleClickOutside,
+  tooltipRef,
+  popperRef,
+  dropdownMenuVisible,
+  tableRef,
+  states,
+  onInput,
+  handleCompositionStart,
+  handleCompositionUpdate,
+  handleCompositionEnd,
+  toggleMenu,
+  iconReverse,
+  showClear,
+  handleClearClick,
+  handleTableClick,
+  inputStyle,
+  wrapperRef,
+  currentPlaceholder,
+  shouldShowPlaceholder,
+  isTransparent,
+  collapseItemRef,
+  tagStyle,
+  wrapperHeightGoBeyond,
+  deleteTag,
+  showTagList,
+  collapseTagStyle,
+  detailsTableRaf,
+  tableData,
+  inputId,
+  selectDisabled
+} = UesComboGrid(<any>props, emits);
+
+
+function modelValueIsArr(length: number = 0): boolean {
+  return isArray(props.modelValue) && props.modelValue.length > length
+}
+
+</script>
+
+<template>
+  <div :class="[nsSelect.b()]"
+       class="cy-combo-grid--small"
+       v-click-outside:[popperRef]="handleClickOutside"
+       @mouseenter="states.inputHovering = true"
+       @mouseleave="states.inputHovering = false"
+       @click.stop="toggleMenu"
+  >
+    <el-tooltip
+        ref="tooltipRef"
+        :placement="placement"
+        :teleported="teleported"
+        :fallback-placements="['bottom-start', 'top-start', 'right', 'left']"
+        :effect="effect"
+        trigger="click"
+        :visible="dropdownMenuVisible"
+        :stop-popper-mouse-event="false"
+        :gpu-acceleration="false"
+        :persistent="true"
+    >
+      <template #default>
+        <div
+            ref="wrapperRef"
+            :class="[
+              nsSelect.e('wrapper'),
+              nsSelect.is('focused',isFocused),
+              nsSelect.is('filterable', true),
+              nsSelect.is('padding2' , wrapperHeightGoBeyond),
+              nsSelect.is('disabled', selectDisabled),
+        ]"
+        >
+          <div
+              ref="selectionRef"
+              :class="[
+                  nsSelect.e('selection') ,
+                  nsSelect.is(
+                      'near' ,
+                      multiple && modelValueIsArr()
+                  )
+              ]"
+          >
+            <div
+                v-if="multiple"
+                v-for="(item,index) in showTagList"
+                :key="item.value"
+                :class="nsSelect.e('selected-item')"
+            >
+              <el-tag
+                  closable
+                  size="small"
+                  type="info"
+                  disable-transitions
+                  :style="tagStyle"
+                  @close.stop="deleteTag(item,index)"
+              >
+                <span :class="nsSelect.e('tags-text')">
+                  {{ item.label }}
+                </span>
+              </el-tag>
+            </div>
+
+            <div
+                ref="collapseItemRef"
+                v-if="collapseTags && modelValueIsArr( 1)"
+                :class="nsSelect.e('selected-item')"
+            >
+              <el-tag size="small"
+                      type="info"
+                      :style="collapseTagStyle"
+                      disable-transitions>
+                <span :class="nsSelect.e('tags-text')">
+                  + {{ (modelValue as []).length - 1 }}
+                </span>
+              </el-tag>
+            </div>
+            <div
+                :class="[
+                nsSelect.e('selected-item'),
+                nsSelect.e('input-wrapper'),
+              ]"
+            >
+              <input :class="nsSelect.e('input')"
+                     ref="inputRef"
+                     type="text"
+                     :disabled="selectDisabled"
+                     :id="inputId"
+                     autocomplete="off"
+                     :style="inputStyle"
+                     v-model="states.inputValue"
+                     @compositionstart="handleCompositionStart"
+                     @compositionupdate="handleCompositionUpdate"
+                     @compositionend="handleCompositionEnd"
+                     @blur="handleBlur"
+                     @focus="handleFocus"
+                     @input="onInput"
+                     @click.stop="toggleMenu"
+              />
+              <span
+                  ref="calculatorRef"
+                  aria-hidden="true"
+                  :class="nsSelect.e('input-calculator')"
+                  v-text="states.inputValue"
+              />
+            </div>
+            <div
+                v-if="shouldShowPlaceholder"
+                :class="[
+                  nsSelect.e('selected-item'),
+                  nsSelect.e('placeholder'),
+                  nsSelect.is('transparent',isTransparent)
+                ]"
+            >
+              <span>{{ currentPlaceholder }}</span>
+            </div>
+          </div>
+          <div
+              :class="[nsSelect.e('suffix')]"
+          >
+            <el-icon v-if="showClear"
+                     style="cursor: pointer"
+                     @click.stop="handleClearClick">
+              <CircleClose/>
+            </el-icon>
+            <el-icon
+                v-else
+                :class="[nsSelect.e('caret'),nsSelect.is('reverse' , iconReverse)]">
+              <CaretBottom/>
+            </el-icon>
+          </div>
+        </div>
+      </template>
+
+      <template #content>
+        <div :class="nsSelect.e('table_content')">
+          <div>
+            <vxe-table :class="nsSelect.e('table')"
+                       ref="tableRef"
+                       :data="tableData"
+                       @cell-click="handleTableClick"
+                       show-overflow-tooltip
+                       show-overflow
+                       max-height="250px"
+                       :row-config="{ height: 24,isCurrent: true,isHover:true, useKey: true,keyField: 'value' }"
+                       :scroll-y="{enabled: true}"
+            >
+              <slot v-if="$slots.default"/>
+              <vxe-column v-for="item in tableHeader"
+                          v-else
+                          :key="item"
+                          :title="item.name"
+                          :field="item.code"
+                          :width="item.width"
+              />
+            </vxe-table>
+          </div>
+          <div v-if="props.multiple">
+            <vxe-table
+                ref="detailsTableRaf"
+                :row-class-name="nsSelect.is('row-move' , sortable)"
+                :class="nsSelect.e('table')"
+                :data="XEUtils.clone(states.selected , true)"
+                max-height="250px"
+                show-overflow-tooltip
+                show-overflow
+                :row-config="{ height: 30 , isCurrent: true,isHover:true , useKey: true , keyField: 'value' }"
+            >
+              <vxe-column field="value" title="编码" width="80">
+                <template #default="{row,$rowIndex}" v-if="sortable">
+                  <el-tag v-if="$rowIndex === 0" effect="dark" disable-transitions>
+                    主
+                  </el-tag>
+                  <el-tag v-else effect="dark" type="info" disable-transitions>
+                    次
+                  </el-tag>
+                  {{ row.value }}
+                </template>
+              </vxe-column>
+              <vxe-column field="label" title="名称" width="80"/>
+              <vxe-column title="操作" width="40">
+                <template #default="{row , $rowIndex}">
+                  <el-button type="danger" icon="Delete" circle @click="deleteTag(row , $rowIndex)"/>
+                </template>
+              </vxe-column>
+            </vxe-table>
+          </div>
+        </div>
+
+      </template>
+    </el-tooltip>
+  </div>
+</template>

+ 635 - 0
src/components/cy/combo-grid/src/index.ts

@@ -0,0 +1,635 @@
+import {ElTooltip, useFocusController, useFormItem, useFormItemInputId, useNamespace} from "element-plus";
+import {useCompRef} from "@/utils/useCompRef";
+import XEUtils, {filter, isArray, isEqual, isFunction, isNumber, isObject, isString} from "xe-utils";
+import type {CyComboGridOptions} from './CyComboGrid.vue'
+import {useResizeObserver} from "@vueuse/core";
+import {stringNotBlank} from "@/utils/blank-utils";
+import {listFilter} from "@/utils/list-utlis";
+//@ts-ignore
+import Sortable from 'sortablejs'
+import {WritableComputedRef} from "vue";
+
+const isPromise = (val: Promise<any | unknown>) => {
+    if (val === null || typeof val === 'undefined') {
+        return false
+    }
+    return isObject(val) && isFunction(val.then) && isFunction(val.catch);
+};
+
+const isKorean = (text: string) =>
+    /([\uAC00-\uD7AF\u3130-\u318F])+/gi.test(text)
+
+export function useInputV2(handleInput: (event: InputEvent) => void) {
+    const isComposing = ref(false)
+    const handleCompositionStart = () => {
+        isComposing.value = true
+    }
+    const handleCompositionUpdate = (event: any) => {
+        const text = event.target.value
+        const lastCharacter = text[text.length - 1] || ''
+        isComposing.value = !isKorean(lastCharacter)
+    }
+
+    const handleCompositionEnd = (event: any) => {
+        if (isComposing.value) {
+            isComposing.value = false
+            if (isFunction(handleInput)) {
+                handleInput(event)
+            }
+        }
+    }
+
+    return {
+        handleCompositionStart,
+        handleCompositionUpdate,
+        handleCompositionEnd,
+        isComposing
+    }
+}
+
+declare type CyComboGridOptionsV2 = Readonly<Required<CyComboGridOptions>>
+
+declare type ValueLabel = { value: string, label: string, [key: string]: any }
+
+const prefix = ref('cy')
+
+export default function UesComboGrid(props: CyComboGridOptionsV2, emits: any) {
+    const nsSelect = useNamespace('combo-grid', prefix)
+    const inputRef = ref<HTMLInputElement | null>(null)
+    const tooltipRef = useCompRef(ElTooltip)
+    const expanded = ref(false)
+    const tableRef = ref(null)
+    const calculatorRef = ref<HTMLElement | null>(null)
+    const selectionRef = ref<HTMLElement | null>(null)
+    const collapseItemRef = ref<HTMLElement | null>(null)
+    const detailsTableRaf = ref(null)
+    const localSearchData = ref<any[]>([])
+    const internalData = isFunction(props.remoteMethod) && typeof props.data === 'undefined'
+    const wrapperHeightGoBeyond = ref(false)
+
+    const {form, formItem} = useFormItem()
+    const {inputId} = useFormItemInputId(props, {
+        formItemContext: formItem,
+    })
+
+    const selectDisabled = computed(() => props.disabled || form?.disabled)
+
+    const propsData: WritableComputedRef<ValueLabel[]> = computed({
+        get() {
+            return internalData ? states.data : props.data
+        },
+        set(val) {
+            if (internalData) {
+                states.data = val
+            } else {
+                emits('update:data', val)
+            }
+        }
+    })
+
+    const dropdownMenuVisible = computed({
+        get() {
+            return expanded.value
+        },
+        set(val: boolean) {
+            expanded.value = val
+        },
+    })
+
+    const states = reactive({
+        menuVisibleOnFocus: false,
+        inputValue: '',
+        selectedLabel: '',
+        inputHovering: false,
+        selectionWidth: 0,
+        calculatorWidth: 0,
+        collapseItemWidth: 0,
+        previousQuery: '',
+        data: [],
+        // 缓存点击的选项
+        selected: props.multiple ? ([] as ValueLabel[]) : ({} as any),
+    })
+
+    const popperRef = computed(() => {
+        return tooltipRef.value?.popperRef?.contentRef
+    })
+
+    const {wrapperRef, isFocused, handleFocus, handleBlur} = useFocusController(
+        // @ts-ignore
+        inputRef, {
+            afterFocus() {
+                expanded.value = true
+                states.menuVisibleOnFocus = true
+            },
+            beforeBlur(event) {
+                return (tooltipRef.value?.isFocusInsideContent(event))
+            },
+            afterBlur() {
+                expanded.value = false
+                states.menuVisibleOnFocus = false
+                formItem?.validate('blur').catch((err) => console.warn(err))
+            }
+        })
+
+    function handleClickOutside(event: Event) {
+        expanded.value = false
+        if (isFocused.value) {
+            const _event = new FocusEvent('focus', event)
+            nextTick().then(r => {
+                handleBlur(_event)
+            })
+        }
+    }
+
+    const tableData = computed(() => {
+        return localSearchData.value.length > 0 ? localSearchData.value : propsData.value;
+    })
+
+    function localSearch() {
+        localSearchData.value = listFilter(propsData.value, states.inputValue)
+    }
+
+    function handleQueryChange(val: string) {
+        if (states.previousQuery === val) {
+            return
+        }
+        if (isComposing.value) {
+            return;
+        }
+        if (val === '') {
+            localSearchData.value = []
+            return;
+        }
+        states.previousQuery = val
+        if (isFunction(props.remoteMethod)) {
+            const remoteMethod = props.remoteMethod(val)
+            if (remoteMethod != null && isPromise(remoteMethod)) {
+                remoteMethod.then(data => {
+                    propsData.value = data as any
+                })
+            }
+            return;
+        }
+
+        // 本地搜索
+        localSearch()
+    }
+
+    const onInputChange = () => {
+        if (states.inputValue.length > 0 && !expanded.value) {
+            expanded.value = true
+        }
+        handleQueryChange(states.inputValue)
+    }
+
+    const onInput = (event: any) => {
+        states.inputValue = event.target.value
+        debouncedOnInputChange()
+    }
+
+    const debouncedOnInputChange = XEUtils.debounce(() => {
+        onInputChange()
+    }, props.debounce)
+
+    const {
+        handleCompositionStart,
+        handleCompositionUpdate,
+        handleCompositionEnd,
+        isComposing
+    } = useInputV2((e) => onInput(e))
+
+    const iconReverse = computed(() => {
+        return dropdownMenuVisible.value
+    })
+
+    const showClear = computed(() => {
+        return props.clearable && hasModelValue.value && states.inputHovering
+    })
+
+    const bindObj = computed(() => {
+        return (stringNotBlank(props.value) && stringNotBlank(props.label)) || stringNotBlank(props.valueLabel)
+    })
+
+    const updateModel = (val: any) => {
+        localSearchData.value = []
+        states.inputValue = ''
+        const select = {
+            value: val.value,
+            label: val.label,
+        }
+
+        if (props.multiple) {
+            let index = -1
+            if (props.keys) {
+                index = (props.modelValue as string[]).indexOf(select.value)
+            } else {
+                index = (props.modelValue as any[]).findIndex(item => {
+                    return item.value === val.value
+                })
+            }
+            if (index > -1) {
+                (props.modelValue as any[]).splice(index, 1)
+                states.selected.splice(index, 1)
+            } else {
+                (props.modelValue as any[]).push(props.keys ? select.value : select)
+                states.selected.push(select)
+            }
+
+        } else {
+            states.selected = select
+        }
+
+        if (bindObj.value) {
+            // @ts-ignore
+            props.modelValue[props.value] = val.value
+            // @ts-ignore
+            props.modelValue[props.label] = val.label
+            states.selectedLabel = val.label
+            return
+        }
+
+        if (isString(props.modelValue)) {
+            emits('update:modelValue', val.value)
+            states.selectedLabel = val.label
+            return
+        }
+        if (isNumber(props.modelValue)) {
+            emits('update:modelValue', val.value)
+            states.selectedLabel = val.label
+            return
+        }
+
+
+    }
+
+    const handleClearClick = () => {
+        states.selectedLabel = ''
+        states.selected = props.multiple ? [] : ({} as any)
+        if (props.multiple) {
+            emits('update:modelValue', [])
+            return;
+        }
+
+        if (bindObj.value) {
+            // @ts-ignore
+            props.modelValue[props.value] = ''
+            // @ts-ignore
+            props.modelValue[props.label] = ''
+            return;
+        }
+        if (isString(props.modelValue)) {
+            emits('update:modelValue', '')
+            return
+        }
+        if (isNumber(props.modelValue)) {
+            emits('update:modelValue', null)
+            return
+        }
+
+    }
+
+    const handleTableClick = (val: any) => {
+        updateModel(val.row)
+        if (!props.multiple)
+            toggleMenu()
+    }
+
+    function toggleMenu() {
+        if (props.disabled) return
+        if (states.menuVisibleOnFocus) {
+            // controlled by automaticDropdown
+            states.menuVisibleOnFocus = false
+        } else {
+            expanded.value = !expanded.value
+        }
+    }
+
+    const getGapWidth = () => {
+        if (!selectionRef.value) return 0
+        const style = window.getComputedStyle(selectionRef.value)
+        return Number.parseFloat(style.gap || '6px')
+    }
+
+    const inputStyle = computed(() => ({
+        width: `${Math.max(states.calculatorWidth, 11)}px`
+    }))
+
+    const collapseTagStyle = computed(() => {
+        return {maxWidth: `${states.selectionWidth}px`}
+    })
+
+    // computed style
+    const tagStyle = computed(() => {
+        const gapWidth = getGapWidth()
+        const maxWidth =
+            collapseItemRef.value && props.maxCollapseTags === 1
+                ? states.selectionWidth - states.collapseItemWidth - gapWidth
+                : states.selectionWidth
+        return {maxWidth: `${maxWidth}px`}
+    })
+
+    const hasModelValue = computed(() => {
+        if (isString(props.modelValue) && stringNotBlank(props.modelValue)) {
+            return true
+        }
+        if (isNumber(props.modelValue) && stringNotBlank(props.modelValue)) {
+            return true
+        }
+        // @ts-ignore
+        if (bindObj.value && stringNotBlank(props.modelValue[props!.value])) {
+            return true
+        }
+        return isArray(props.modelValue) && props.modelValue.length > 0;
+    })
+
+    const shouldShowPlaceholder = computed(() => {
+        return !states.inputValue
+    })
+
+    const currentPlaceholder = computed(() => {
+        if (props.multiple) {
+            return (props.modelValue as any[]).length > 0 ? '' : props.placeholder;
+        }
+        return states.selectedLabel || props.placeholder
+    })
+
+
+    const isTransparent = computed(() => {
+        return !hasModelValue.value || (expanded.value && !states.inputValue)
+    })
+
+    const showTagList = computed(() => {
+        if (props.multiple) {
+            const temp = states.selected.length > 0 ? [states.selected[0]] : []
+            return props.collapseTags ? temp : states.selected
+        }
+        return []
+    })
+
+    const deleteTag = (item: any, index: number) => {
+        (props.modelValue as ValueLabel[]).splice(index, 1)
+        states.selected.splice(index, 1)
+    }
+
+    function getObjValue(): string {
+        // @ts-ignore
+        return props.modelValue[props.value]
+    }
+
+    function getObjLabel(): string {
+        // @ts-ignore
+        return props.modelValue[props.label]
+    }
+
+    function getModelArr() {
+        return (props.modelValue as ValueLabel[])
+    }
+
+    function handleObj() {
+        states.selected = {
+            value: getObjValue(),
+            label: getObjLabel(),
+        }
+        states.selectedLabel = getObjLabel()
+    }
+
+    function handleSelect() {
+        if (props.modelValue === states.selected.value) {
+            states.selectedLabel = states.selected.label
+            return
+        }
+
+        const data = filter(propsData.value, (item) => {
+            return item.value === props.modelValue
+        })
+        if (data.length > 0) {
+            states.selected = {
+                value: data[0].value,
+                label: data[0].label,
+            }
+            states.selectedLabel = states.selected.label
+            return
+        }
+        states.selectedLabel = XEUtils.toString(props.modelValue)
+    }
+
+    function handleMultiple() {
+        let completelyEqualTo = true
+        if (states.selected.length === getModelArr().length) {
+            for (let i = 0, len = states.selected.length; i < len; i++) {
+                const item = states.selected[i];
+                const modItem = getModelArr()[i];
+                if (item.value !== modItem.value) {
+                    completelyEqualTo = false
+                    break;
+                }
+            }
+        } else {
+            completelyEqualTo = false
+        }
+
+        if (!completelyEqualTo) {
+            states.selected = XEUtils.clone(getModelArr(), true)
+        }
+    }
+
+    function handleKeys() {
+        let completelyEqualTo = true
+
+        if (getModelArr().length !== states.selected.length) {
+            completelyEqualTo = false
+        } else {
+            for (let i = 0; i < states.selected.length; i++) {
+                const item = states.selected[i];
+                if (!getModelArr().includes(item.value)) {
+                    completelyEqualTo = false
+                    break;
+                }
+            }
+        }
+
+        if (completelyEqualTo) {
+            return;
+        }
+
+        const tempKey = []
+        const data: {
+            [key: number]: ValueLabel
+        } = {};
+
+        for (let i = 0, len = propsData.value.length; i < len; i++) {
+            const item = propsData.value[i];
+            // @ts-ignore
+            const index = (getModelArr() as string[]).indexOf(item.value);
+            if (index > -1) {
+                tempKey.push(item.value)
+                data[index] = {
+                    value: item.value,
+                    label: item.label
+                }
+            }
+            if (tempKey.length === getModelArr().length) {
+                break;
+            }
+        }
+
+        if (tempKey.length > 0) {
+            states.selected = XEUtils.toArray(data)
+            if (tempKey.length !== getModelArr().length) {
+                const selectedLength = states.selected.length
+                // @ts-ignore
+                for (let i = selectedLength; i < (getModelArr() as string[]).length; i++) {
+                    const item = getModelArr()[i]
+                    states.selected.push({
+                        value: item,
+                        label: item,
+                    })
+                }
+            }
+        } else {
+            states.selected = []
+            // @ts-ignore
+            for (let i = 0; i < (getModelArr() as string[]).length; i++) {
+                const item = getModelArr()[i]
+                states.selected.push({
+                    value: item,
+                    label: item,
+                })
+            }
+        }
+
+    }
+
+
+    function handleChangeModel() {
+        if (bindObj.value) {
+            handleObj()
+        } else if (props.multiple) {
+            if (props.keys) {
+                handleKeys()
+            } else {
+                handleMultiple()
+            }
+        } else {
+            handleSelect()
+        }
+    }
+
+
+    watch(
+        () => props.modelValue,
+        (val, oldVal) => {
+            handleChangeModel()
+            if (!isEqual(val, oldVal)) {
+                formItem?.validate('change').then(r => console.log(r)).catch((err) => console.error(err))
+            }
+        }, {
+            flush: 'post',
+            deep: true,
+        }
+    )
+
+    watch(
+        () => expanded.value,
+        (val) => {
+            if (val) {
+                handleQueryChange(states.inputValue)
+            } else {
+                states.inputValue = ''
+                states.previousQuery = ''
+            }
+            emits('visible-change', val)
+        }
+    )
+
+    const resetCalculatorWidth = () => {
+        states.calculatorWidth = calculatorRef.value!.getBoundingClientRect().width
+    }
+
+    const updateTooltip = () => {
+        tooltipRef.value?.updatePopper?.()
+    }
+
+    const resetSelectionWidth = () => {
+        states.selectionWidth = selectionRef.value!.getBoundingClientRect().width
+    }
+
+    const resetCollapseItemWidth = () => {
+        states.collapseItemWidth =
+            collapseItemRef.value!.getBoundingClientRect().width
+    }
+
+    function columnsTableSortable() {
+        const el = (detailsTableRaf.value!['$el'] as HTMLDivElement).querySelector('.vxe-table--body tbody')
+        const ops = {
+            handle: '.vxe-body--row',
+            onEnd: function ({newIndex, oldIndex}: { newIndex: number, oldIndex: number }) {
+                const currRow = getModelArr().splice(oldIndex, 1)[0];
+                getModelArr().splice(newIndex, 0, currRow);
+
+                const currRow2 = states.selected.splice(oldIndex, 1)[0];
+                states.selected.splice(newIndex, 0, currRow2);
+            }
+        }
+        Sortable.create(el, ops)
+    }
+
+    useResizeObserver(calculatorRef, resetCalculatorWidth)
+    useResizeObserver(wrapperRef, () => {
+        updateTooltip()
+        wrapperHeightGoBeyond.value = wrapperRef.value!.getBoundingClientRect().height > 28
+    })
+    useResizeObserver(selectionRef, resetSelectionWidth)
+    useResizeObserver(collapseItemRef, resetCollapseItemWidth)
+
+    onMounted(() => {
+        if (props.multiple) {
+            if (!isArray(props.modelValue)) {
+                emits('update:modelValue', [])
+            }
+            props.sortable && columnsTableSortable()
+        }
+    })
+
+    return {
+        inputId,
+        nsSelect,
+        handleFocus,
+        inputRef,
+        handleBlur,
+        isFocused,
+        handleClickOutside,
+        tooltipRef,
+        popperRef,
+        dropdownMenuVisible,
+        tableRef,
+        states,
+        onInput,
+        handleCompositionStart,
+        handleCompositionUpdate,
+        handleCompositionEnd,
+        toggleMenu,
+        iconReverse,
+        showClear,
+        handleClearClick,
+        handleTableClick,
+        wrapperRef,
+        currentPlaceholder,
+        calculatorRef,
+        shouldShowPlaceholder,
+        isTransparent,
+        selectionRef,
+        collapseItemRef,
+        deleteTag,
+        showTagList,
+        detailsTableRaf,
+        tableData,
+        wrapperHeightGoBeyond,
+        selectDisabled,
+
+        inputStyle,
+        tagStyle,
+        collapseTagStyle,
+    }
+}
+

+ 174 - 0
src/components/cy/combo-grid/style/index.scss

@@ -0,0 +1,174 @@
+.is-error {
+  .cy-combo-grid__wrapper {
+    box-shadow: 0 0 0 1px var(--el-color-danger) inset !important;
+  }
+}
+
+.cy-combo-grid {
+  display: inline-block;
+  position: relative;
+  vertical-align: middle;
+  width: 120px
+}
+
+.cy-combo-grid--small .cy-combo-grid__wrapper {
+  gap: 4px;
+  padding: 0 8px;
+  min-height: 24px;
+  line-height: 20px;
+  font-size: 12px;
+}
+
+.cy-combo-grid__input {
+  border: none;
+  outline: 0;
+  padding: 0;
+  color: var(--el-select-multiple-input-color);
+  font-size: inherit;
+  font-family: inherit;
+  -webkit-appearance: none;
+  -moz-appearance: none;
+  appearance: none;
+  height: 24px;
+  max-width: 100%;
+  background-color: transparent
+}
+
+
+.cy-combo-grid__wrapper {
+  display: flex;
+  align-items: center;
+  position: relative;
+  box-sizing: border-box;
+  cursor: pointer;
+  text-align: left;
+  font-size: 14px;
+  padding: 4px 12px;
+  gap: 6px;
+  min-height: 32px;
+  line-height: 24px;
+  border-radius: var(--el-border-radius-base);
+  background-color: var(--el-fill-color-blank);
+  transition: var(--el-transition-duration);
+  box-shadow: 0 0 0 1px #dcdfe6 inset;
+
+  &.is-disabled {
+    cursor: not-allowed !important;
+    background-color: var(--el-fill-color-light);
+    color: var(--el-text-color-placeholder);
+    box-shadow: 0 0 0 1px #e4e7ed inset
+  }
+
+  &.is-padding2 {
+    transition: none;
+    padding: 2px 8px;
+  }
+
+  &.is-focused {
+    box-shadow: 0 0 0 1px var(--el-color-primary) inset
+  }
+
+  &.is-filterable {
+    cursor: text
+  }
+
+  .cy-combo-grid__input-calculator {
+    position: absolute;
+    left: 0;
+    top: 0;
+    max-width: 100%;
+    visibility: hidden;
+    white-space: pre;
+    overflow: hidden
+  }
+
+  .cy-combo-grid__selected-item {
+    display: flex;
+    flex-wrap: wrap;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none
+  }
+
+  .cy-combo-grid__input-wrapper {
+    max-width: 100%
+  }
+
+  .cy-combo-grid__placeholder {
+    position: absolute;
+    display: block;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 100%;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    color: var(--el-input-text-color, var(--el-text-color-regular));
+
+    &.is-transparent {
+      -webkit-user-select: none;
+      -moz-user-select: none;
+      -ms-user-select: none;
+      user-select: none;
+      color: var(--el-text-color-placeholder)
+    }
+  }
+
+}
+
+.cy-combo-grid__caret {
+  color: var(--el-select-input-color);
+  font-size: var(--el-select-input-font-size);
+  transition: var(--el-transition-duration);
+  transform: rotateZ(0);
+  cursor: pointer;
+
+  &.is-reverse {
+    transform: rotateZ(180deg)
+  }
+
+}
+
+.cy-combo-grid__selection {
+  position: relative;
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  flex: 1;
+  min-width: 0;
+  gap: 6px;
+
+  &.is-near {
+    margin-left: -6px
+  }
+}
+
+.cy-combo-grid__suffix {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+  gap: 6px;
+  color: var(--el-input-icon-color, var(--el-text-color-placeholder));
+}
+
+.cy-combo-grid__table_content {
+  display: flex;
+}
+
+.is-row-move {
+  cursor: move;
+}
+
+.cy-combo-grid__table {
+  --vxe-table-column-padding-default: 5px 0;
+  //--vxe-table-row-height-default: 20px;
+  --vxe-table-cell-padding-left: 5px;
+  --vxe-table-cell-padding-right: 5px;
+
+  .vxe-table--body-wrapper {
+    &::-webkit-scrollbar {
+      width: 15px !important;
+    }
+  }
+}

+ 5 - 7
src/components/cy/floating-frame/FloatingFrame.vue

@@ -30,16 +30,14 @@ const draggable = computed(() => {
   return true
 })
 
-const isNumberAddUnit = (val: number | string, unit: string = 'px') => {
-  return XEUtils.addUnit(val)
-}
+const addUnit = (val: number | string) => XEUtils.addUnit(val)
 
 const boxStyle = computed(() => {
   return {
-    height: isNumberAddUnit(props.height),
-    width: isNumberAddUnit(props.width),
-    left: isNumberAddUnit(props.x),
-    top: isNumberAddUnit(props.y),
+    height: addUnit(props.height),
+    width: addUnit(props.width),
+    left: addUnit(props.x),
+    top: addUnit(props.y),
     zIndex: zIndex.value
   }
 })

+ 3 - 12
src/components/zhu-yuan-yi-sheng/yi-zhu-lu-ru/BaoCunXinXi.vue

@@ -1,5 +1,6 @@
 <template>
-  <FloatingFrame v-model="errorMsg.dialog" title="错误信息"
+  <FloatingFrame v-model="errorMsg.dialog"
+                 title="错误信息"
                  :x="77"
                  :y="494"
                  width="420px">
@@ -43,7 +44,6 @@
           </div>
         </div>
       </div>
-
     </div>
   </FloatingFrame>
 </template>
@@ -66,19 +66,10 @@ const emit = defineEmits<{
 }>()
 
 
-const clickToModify = (key) => {
+const clickToModify = (key: any) => {
   if (props.currentKey === key) return
   emit('clickError', XEUtils.toNumber(key))
 }
-
-const openOrClose = (val = true) => {
-  errorMsg.value.dialog = val
-  errorMsg.value.type = YzErrTypeEnum.确认错误
-}
-
-defineExpose({
-  openOrClose
-})
 </script>
 
 <style scoped lang="scss">

+ 1 - 2
src/components/zhu-yuan-yi-sheng/yi-zhu-lu-ru/yz-edit/YzEditor.vue

@@ -197,6 +197,7 @@
             <el-option label="基数药" value="3"/>
             <el-option :disabled="queryParam.frequCode !== 'takeMedicine'" label="出院带药"
                        value="4"/>
+            <el-option label="GCP自费" value="5"/>
           </el-select>
         </div>
         <div class="div_center__box">
@@ -250,7 +251,6 @@ import {
   toDeleteAnOrder, YaoPingJiLiang
 } from '@/api/zhu-yuan-yi-sheng/yi-zhu-lu-ru'
 import {listNotBlank, listToStr, stringIsBlank, stringNotBlank} from '@/utils/blank-utils'
-import Sleep from '@/utils/sleep'
 import XcComboGrid from "@/components/xiao-chan/combo-grid/XcComboGrid";
 import XcSelectV3 from "@/components/xiao-chan/select-v3/XcSelectV3";
 import XcOption from "@/components/xiao-chan/select/XcOption";
@@ -258,7 +258,6 @@ import XcSelect from "@/components/xiao-chan/select/XcSelect";
 import XcCheckbox from "@/components/xiao-chan/checkbox/XcCheckbox";
 import {BizException, ExceptionEnum} from "@/utils/BizException";
 import BaoCunXinXi from "@/components/zhu-yuan-yi-sheng/yi-zhu-lu-ru/BaoCunXinXi";
-import {clone} from "@/utils/clone";
 import {
   queryParam,
   yiZhuData,

+ 2 - 1
src/main.js

@@ -1,5 +1,5 @@
 import {createApp} from 'vue'
-import ElementPlus, {useZIndex} from 'element-plus'
+import ElementPlus, {useZIndex, ClickOutside} from 'element-plus'
 import 'element-plus/dist/index.css'
 import 'element-plus/theme-chalk/dark/css-vars.css'
 import 'normalize.css' // css初始化
@@ -50,6 +50,7 @@ app.use(router)
 app.use(DataVVue3)
 app.directive('el-btn', VElBtn)
 app.directive('title', VTitle)
+app.directive('ClickOutside', ClickOutside)
 app.use(print)
 app.use(VXETable)
 app.use(JsonViewer);

+ 2 - 1
src/utils/array-utils.ts

@@ -7,6 +7,7 @@ declare type TreeSearch<D> = {
 
 export const treeSearch = <D = any>(data: D[], iterator: (item: TreeSearch<D>) => boolean): D[] => {
     data = XEUtils.clone(data, true);
+
     function filterDataByVisible(tempData: D[]): D[] {
         return XEUtils.filter(tempData, (item: TreeSearch<D>): boolean => {
             if (item.children) {
@@ -23,7 +24,7 @@ export const treeSearch = <D = any>(data: D[], iterator: (item: TreeSearch<D>) =
                 traverse(child.children);
             }
             if (!child.$visible && child.children?.length) {
-                const $visible: boolean = !child.children.some((child: TreeSearch<D>) => child.$visible);
+                const $visible: boolean = !child.children.some((child) => child.$visible);
                 child.$visible = !$visible;
             }
         })

+ 2 - 3
src/utils/blank-utils.js

@@ -7,9 +7,8 @@ export function stringIsBlank(val) {
     return typeof val === 'undefined' || val === null || val === ''
 }
 
-export function stringNotBlank(val) {
-    return typeof val !== 'undefined' && val !== null && val !== ''
-}
+export const stringNotBlank = (val) => !stringIsBlank(val)
+
 
 export function listIsBlank(val) {
     return typeof val === 'undefined' || val === null || val.length === 0

+ 0 - 1
src/utils/emr/emr-init-v2.ts

@@ -110,7 +110,6 @@ export function useEmrInit(
             div.querySelector('iframe').remove()
         }
 
-
         const iframe: HTMLIFrameElement = document.createElement('iframe')
         let editor: EditType, runtime: Runtime
 

+ 2 - 2
src/utils/list-utlis.ts

@@ -8,7 +8,7 @@ const isEnglish = (str: string) => {
     return /^[a-zA-Z]+$/.test(str);
 }
 
-export const notNullAndLike = (data: any, likeValue: string, keyName: string[] = ['pyCode', 'dCode', 'code', 'name']) => {
+export const notNullAndLike = (data: any, likeValue: string, keyName: string[] = ['pyCode', 'dCode', 'code', 'name', 'value', 'label']) => {
     for (let i = 0; i < keyName.length; i++) {
         let itemKey = keyName[i];
         let tempVal = data[itemKey]
@@ -29,7 +29,7 @@ export const notNullAndLike = (data: any, likeValue: string, keyName: string[] =
     return false
 }
 
-export function listFilter(data: any[], likeValue: string, keyName?: string[]) {
+export function listFilter(data: any[], likeValue: string, keyName: string[] = ['pyCode', 'dCode', 'code', 'name', 'value', 'label']) {
     return XEUtils.filter(data, (item) => {
         return notNullAndLike(item, likeValue, keyName)
     });

+ 1 - 2
src/utils/moment-utils.ts

@@ -1,5 +1,4 @@
 import XEUtils from "xe-utils";
-// @ts-ignore
 import moment from 'moment'
 import RequestV2 from "./request-v2";
 
@@ -58,7 +57,7 @@ export const formatDateToStr = (val: string | Date, format: DATEFORMAT = DATEFOR
 /**
  * 获取服务器的时间,如果错误就使用本地的时间
  */
-export const getServerDate = async () => {
+export const getServerDate = async (): Promise<string> => {
     let now: string = ''
     await RequestV2<string>({
         url: '/publicApi/getDate',

+ 8 - 5
src/views/data-base/data-base-api/DataBase.vue

@@ -9,7 +9,7 @@ import {
 import {userInfoStore} from "@/utils/store-public";
 import DataBaseDialog from "@/views/data-base/data-base-api/components/DataBaseDialog.vue";
 
-const iframe = ref<HTMLIFrameElement>()
+const iframe = ref<HTMLIFrameElement | null>(null)
 // @ts-ignore
 const src = import.meta.env.VITE_DATA_BASE + '/magic/web/index.html'
 // const src = 'http://192.168.56.1:8991'
@@ -24,9 +24,9 @@ function user() {
   dialogRef.value!.openDialog()
 }
 
-const getMessage = e => {
+const getMessage = (e: { data: { name: string} }) => {
   const data = JSON.parse(e.data)
-  func[data.name]()
+  func[data.name] && func[data.name]()
 }
 
 onDeactivated(() => {
@@ -39,7 +39,7 @@ onActivated(() => {
 
 onMounted(async () => {
   await nextTick()
-  iframe.value.onload = () => {
+  iframe.value!.onload = () => {
     const data = {
       token: userInfoStore.value.token,
       name: 'setToken'
@@ -51,7 +51,10 @@ onMounted(async () => {
 
 <template>
   <DataBaseDialog ref="dialogRef"/>
-  <iframe :src="src" width="100%" height="100%" style="border: 0"
+  <iframe :src="src"
+          width="100%"
+          height="100%"
+          style="border: 0"
           ref="iframe"/>
 </template>
 

+ 1 - 1
src/views/hospitalization/zhu-yuan-yi-sheng/electronic-medical-record/emr-editor/EmrMain.vue

@@ -836,7 +836,7 @@ const clickSaveData = async () => {
     emrDataElement: null,
     documentData: null
   }
-  data.emrDataElement = editor.getDataElements('business')
+  data.emrDataElement = editor.getDataElements('business', false, true)
 
   objectValuesCannotBeNull(data);
   if (categoryCode.value === emrCodeEnum.courseRecord) {

+ 5 - 6
src/views/hospitalization/zhu-yuan-yi-sheng/public-js/zhu-yuan-yi-sheng.ts

@@ -3,7 +3,6 @@ import {ElMessage} from "element-plus";
 import {getPatientInfo, getDrgPatInfo} from "@/api/inpatient/patient";
 import {BizException, ExceptionEnum} from "@/utils/BizException";
 import {nextTick, Ref, ref, computed, onActivated, onDeactivated} from "vue";
-import {getServerDateApi} from "@/api/public-api";
 import {getFormatDatetime} from "@/utils/date";
 import {isDev} from "@/utils/public";
 import {getFrequency, getSupplyType, huoQuYiZhuShuJu} from "@/api/zhu-yuan-yi-sheng/yi-zhu-lu-ru";
@@ -11,6 +10,7 @@ import EventBus from "@/utils/mitt";
 import XEUtils from 'xe-utils'
 import {getAncillaryInformation} from '@/api/zhu-yuan-yi-sheng/jian-yan-jian-cha-shen-qing'
 import {timeLimitPromptByPatientNo} from "@/api/emr-control/emr-time-limit-prompt";
+import {getServerDate} from "@/utils/moment-utils";
 
 export interface PatInfo {
     inpatientNo?: string | null
@@ -453,7 +453,7 @@ export const jsQueryYzData = async () => {
 }
 
 let newDate = ''
-getServerDateApi().then((res: string) => {
+getServerDate().then((res) => {
     newDate = res
 })
 
@@ -740,7 +740,7 @@ export const clickOnThePatient = async (patNo: string) => {
     changePatientHook.forEach(item => {
         item()
     })
-    getDrgPatInfo(huanZheXinXi.value).then((res: { [x: string]: string | undefined; } | null) => {
+    getDrgPatInfo(huanZheXinXi.value).then((res: any) => {
         if (res != null) {
             huanZheXinXi.value.groupInfoName = res['name']
             huanZheXinXi.value.groupInfoWeight = res['weight']
@@ -856,8 +856,7 @@ export let tableHeader = [
     {label: '医保备注', prop: 'ybComment'},
     {label: '大输液', prop: 'infusionFlagName'},
     {label: '厂家', prop: 'manuName'},
-    {label: '类型', prop: 'orderType'},
-    {label: '毒麻类型', prop: 'drugFlagName'},
+    {label: '类型', prop: 'drugFlagName'},
     {label: '药房', prop: 'groupName'},
 ]
 
@@ -907,7 +906,7 @@ export const addJcCheck = async (data: AddJcParams) => {
         BizException(ExceptionEnum.MESSAGE_ERROR, '请勿重复添加。')
     }
     jyJcRestriction(data)
-    data.startTime = await getServerDateApi();
+    data.startTime = await getServerDate();
     // @ts-ignore
     addCheckList.value.push(data)
 }

+ 1 - 1
src/views/hospitalization/zhu-yuan-yi-sheng/yi-zhu-lu-ru/components/DoctorAuthorization.vue

@@ -3,7 +3,7 @@
 import {doctorAuthorizationLogin} from "@/api/zhu-yuan-yi-sheng/yi-zhu-lu-ru";
 import {xcMessage} from "@/utils/xiaochan-element-plus";
 import {ClosingMethod, dialogEmits} from "@/components/js-dialog-comp/useDialogToJs";
-
+import {ref} from 'vue'
 
 const props = defineProps<{
   drugCode: string

+ 2 - 5
src/views/settings/permissions/UserRoleSettings.vue

@@ -76,7 +76,6 @@
               <el-button text size="small" v-if="competence" @click="viewUserRoles(scope.row)">角色</el-button>
               <el-button size="small"
                          v-if="competence"
-                         @contextmenu.stop.prevent="resetPasswordClick(scope.row, true)"
                          @click="resetPasswordClick(scope.row)">
                 重置密码
               </el-button>
@@ -140,6 +139,7 @@ import PersonnelInformationEditing from '@/components/settings/permissions/Perso
 import {needRule} from '@/utils/public'
 import PageLayer from "@/layout/PageLayer";
 import {CyMessageBox} from "@/components/cy/message-box";
+// import {onMounted, reactive, ref, watchEffect} from "vue";
 
 const windowSize = store.state.app.windowSize
 const tableHeight = windowSize.h - 10
@@ -322,13 +322,10 @@ function resetPasswordClick(row, setNextDate = false) {
     }).then(() => {
       resetPasswordByCode({
         code: row.code,
-        nextTime: 20
+        nextTime: 0
       })
     })
-
-
   }
-
 }
 
 onMounted(() => {