20260915221013
This commit is contained in:
1 parent
1f56cc3efb
commit
dce29f250e
54 files changed
+3732
-574
No files matched your search
@@ -72,6 +72,8 @@ const typeDefaults = computed(() => {
|
||||
case 'number':
|
||||
case 'money':
|
||||
return { type: 'number' }
|
||||
case 'password':
|
||||
return { type: 'password' }
|
||||
case 'date':
|
||||
return { valueFormat: 'YYYY-MM-DD' }
|
||||
case 'datetime':
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { Spin } from '@/components/ui'
|
||||
import FmsTree from '@/components/fms-tree/FmsTree.vue'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -12,15 +11,10 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['select', 'update:expandedKeys'])
|
||||
|
||||
const selectedKeys = computed(() =>
|
||||
props.selectedKey ? [props.selectedKey] : [],
|
||||
)
|
||||
const selectedKeys = computed(() => (props.selectedKey ? [props.selectedKey] : []))
|
||||
|
||||
function onSelect(keys, info) {
|
||||
if (info?.node?.isFmsRoot) {
|
||||
emit('select', '')
|
||||
return
|
||||
}
|
||||
function onSelect(keys) {
|
||||
// FmsTree 已把点中虚拟根归一化成空 keys
|
||||
emit('select', keys.length ? String(keys[0]) : '')
|
||||
}
|
||||
</script>
|
||||
@@ -31,15 +25,13 @@ function onSelect(keys, info) {
|
||||
<FmsTree
|
||||
show-root
|
||||
:tree-data="treeData"
|
||||
:loading="loading"
|
||||
:selected-keys="selectedKeys"
|
||||
:expanded-keys="expandedKeys"
|
||||
:block-node="true"
|
||||
@select="onSelect"
|
||||
@update:expanded-keys="(keys) => emit('update:expandedKeys', keys)"
|
||||
/>
|
||||
<div v-if="loading" class="fc-panel__loading">
|
||||
<Spin />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -56,7 +48,6 @@ function onSelect(keys, info) {
|
||||
}
|
||||
|
||||
.fc-panel__tree {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
@@ -72,12 +63,4 @@ function onSelect(keys, info) {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fc-panel__loading {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
</style>
|
||||
@@ -54,8 +54,13 @@ export function formatDateValue(value, withTime = false) {
|
||||
if (value && typeof value.format === 'function') {
|
||||
return value.format(withTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD')
|
||||
}
|
||||
const text = String(value).replace('T', ' ')
|
||||
if (/^\d{4}-\d{2}-\d{2}/.test(text)) return withTime ? text.slice(0, 19) : text.slice(0, 10)
|
||||
// 后端统一输出「显示格式」的墙钟串:date → "2026-09-15",datetime → "2026-09-15 21:16:47"
|
||||
// (见后端 DbUtils.normalizeJdbcValue,与旧项目读取 ResultSet 时格式化的做法一致)。
|
||||
// 因此这里基本是透传:格式化只兜底非标准输入(前端本地数据、数字时间戳、ISO 串等)。
|
||||
const text = String(value)
|
||||
if (/^\d{4}-\d{2}-\d{2}/.test(text)) {
|
||||
return withTime ? text.slice(0, 19).replace('T', ' ') : text.slice(0, 10)
|
||||
}
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return text
|
||||
const datePart = `${date.getFullYear()}-${padDatePart(date.getMonth() + 1)}-${padDatePart(date.getDate())}`
|
||||
@@ -63,3 +68,13 @@ export function formatDateValue(value, withTime = false) {
|
||||
? `${datePart} ${padDatePart(date.getHours())}:${padDatePart(date.getMinutes())}:${padDatePart(date.getSeconds())}`
|
||||
: datePart
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前本地时间 → "YYYY-MM-DD HH:mm:ss"(写入用)。
|
||||
* 与库中 datetime2、接口输出的语义对齐(本地墙钟值,不走 UTC),
|
||||
* 避免同一张表出现「库里存 21:16、前端却写 13:16」两种时间基准。
|
||||
*/
|
||||
export function formatLocalDateTime(date = new Date()) {
|
||||
const datePart = `${date.getFullYear()}-${padDatePart(date.getMonth() + 1)}-${padDatePart(date.getDate())}`
|
||||
return `${datePart} ${padDatePart(date.getHours())}:${padDatePart(date.getMinutes())}:${padDatePart(date.getSeconds())}`
|
||||
}
|
||||
@@ -61,7 +61,7 @@ function collectFieldNodes(nodes) {
|
||||
|
||||
/**
|
||||
* view JSON → 列表配置行(列表页/表格消费)。
|
||||
* 返回结构与旧 s_field_view 行兼容,便于复用既有表格渲染。
|
||||
* 行结构沿用旧版列表配置行的字段名(b_field / b_width / b_visible / b_xh),便于复用既有表格渲染。
|
||||
*/
|
||||
export function buildListConfig(viewSchema) {
|
||||
return collectFieldNodes(viewSchema?.columns || []).map((node, index) => ({
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// 布局只能调整区域与顺序,不得放宽字段权限——合并后仍按字段权限裁剪(开发规范 通用原则 10)。
|
||||
import { loadDataApi, saveObjectApi } from '@/services/api'
|
||||
import { escapeSqlText } from '@/components/fms-module-common/queryUtils'
|
||||
import { formatLocalDateTime } from '@/components/fms-module-common/fieldUtils'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const SCHEMA_TYPE = 'query'
|
||||
@@ -123,7 +124,7 @@ export async function saveModulePref(userId, moduleId, pref, rowExists) {
|
||||
b_schema_type: SCHEMA_TYPE,
|
||||
b_schema_json: JSON.stringify(pref),
|
||||
b_updated_by: userId,
|
||||
b_updated_at: new Date().toISOString().slice(0, 19).replace('T', ' '),
|
||||
b_updated_at: formatLocalDateTime(),
|
||||
}
|
||||
const request = { table: USER_PREF_TABLE, key_field: USER_PREF_KEY }
|
||||
if (rowExists) request.updates = [row]
|
||||
|
||||
@@ -5,11 +5,18 @@
|
||||
* 渲染、交互、拖拽、右键菜单、键盘导航、无障碍全部由 ui/Tree 承担;本组件只做 FMS 业务约定适配:
|
||||
* 1. 默认字段名 b_id / b_name / children,调用方不必重复书写 fieldNames;
|
||||
* 2. 节点标题按 b_i18n 翻译(translate(b_i18n, b_name || b_code || b_id)),关闭 translateTitle 则直接取原值;
|
||||
* 3. 可选「全部分类」虚拟根节点(showRoot),默认展开、可折叠,调用方无需感知 rootKey。
|
||||
* 3. 可选「全部分类」虚拟根节点(showRoot),默认展开、可折叠;调用方只以「选中键为空」
|
||||
* 表达「选中了根」,不需要知道 rootKey,详见下方「虚拟根的选中约定」;
|
||||
* 4. 加载态(loading)由本组件统一渲染遮罩,调用方不必各自写 Spin 与定位样式。
|
||||
*
|
||||
* 除下表声明的 props 外,其余 props 与事件(draggable / filter / disabled / contextMenu /
|
||||
* selectedKeys / checkedKeys / lazy / v-model:selected-keys …)经 $attrs 原样透传给 ui/Tree,
|
||||
* 事件名与 ui/Tree 保持一致。
|
||||
* 事件名与 ui/Tree 保持一致;其中 select 事件额外做了一层归一化,见下。
|
||||
*
|
||||
* 虚拟根的选中约定(showRoot 时):
|
||||
* - 传入:selectedKeys 为 [] / 空值时,本组件自动把虚拟根当作选中项渲染(高亮「全部分类」);
|
||||
* - 传出:用户点中虚拟根时,select 事件回传的 keys 是 [](不是 [rootKey])。
|
||||
* 于是调用方只需用「falsy 的选中键」表达「全部」这一个概念,无需 import rootKey、也无需再判 isFmsRoot。
|
||||
*
|
||||
* 注意:本组件始终向 ui/Tree 提供 #title 插槽(默认渲染翻译后的标题),因此 ui/Tree 内置的
|
||||
* filter 关键词高亮不再生效(过滤本身照常生效);需要高亮时在 #title 插槽内自行实现。
|
||||
@@ -17,6 +24,7 @@
|
||||
* 用法:
|
||||
* <FmsTree
|
||||
* :tree-data="moduleTree"
|
||||
* :loading="categoryLoading"
|
||||
* :expanded-keys="expandedKeys"
|
||||
* :selected-keys="selectedId ? [selectedId] : []"
|
||||
* :context-menu="buildContextMenu"
|
||||
@@ -29,12 +37,18 @@
|
||||
* </template>
|
||||
* </FmsTree>
|
||||
*/
|
||||
import { computed, ref } from 'vue'
|
||||
import { Tree } from '@/components/ui'
|
||||
import { computed, ref, useAttrs } from 'vue'
|
||||
import { Spin, Tree } from '@/components/ui'
|
||||
import { translate } from '@/stores/i18n'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
/** 未声明的 ui/Tree props(含 selectedKeys)经 $attrs 透传,归一化时需读取原值 */
|
||||
const attrs = useAttrs()
|
||||
|
||||
/** 虚拟根节点 key:纯内部实现细节,调用方用「空选中键」表达选中根,不需要也不应引用它 */
|
||||
const ALL_CATEGORY_KEY = '__fms_tree_root__'
|
||||
|
||||
const props = defineProps({
|
||||
/** 业务树数据(b_id / b_name / children) */
|
||||
treeData: { type: Array, default: () => [] },
|
||||
@@ -45,9 +59,11 @@ const props = defineProps({
|
||||
/** 是否注入「全部分类」虚拟根节点 */
|
||||
showRoot: { type: Boolean, default: false },
|
||||
/** 虚拟根节点 key */
|
||||
rootKey: { type: [String, Number], default: '__fms_tree_root__' },
|
||||
rootKey: { type: [String, Number], default: ALL_CATEGORY_KEY },
|
||||
/** 虚拟根节点标题,空则取多语言键 common.allCategories */
|
||||
rootTitle: { type: String, default: '' },
|
||||
/** 加载态:树区域盖一层居中 Spin 遮罩(懒加载子节点的局部 loading 仍由 ui/Tree 负责) */
|
||||
loading: { type: Boolean, default: false },
|
||||
/**
|
||||
* 展开键。undefined 表示非受控:无虚拟根时原样交给 ui/Tree(default-expand-all 等初始展开配置照常生效),
|
||||
* 有虚拟根时由本组件用内部状态维护,并照常抛出 update:expandedKeys。
|
||||
@@ -55,7 +71,7 @@ const props = defineProps({
|
||||
expandedKeys: { type: Array, default: undefined },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:expandedKeys'])
|
||||
const emit = defineEmits(['update:expandedKeys', 'select'])
|
||||
|
||||
/** showRoot 且调用方未受控时,本组件用内部状态持有展开键(始终不含 rootKey) */
|
||||
const internalExpandedKeys = ref([])
|
||||
@@ -102,6 +118,32 @@ const rootCollapsed = ref(false)
|
||||
|
||||
const hasRootKey = (keys) => keys.some((key) => String(key) === String(props.rootKey))
|
||||
|
||||
/**
|
||||
* 选中键归一化(仅 showRoot 时生效):
|
||||
* 传入 [] / 空值 → 补成 [rootKey],让虚拟根呈现选中态,调用方用「空」表达「选中全部」;
|
||||
* 传入非空数组 → 原样透传,业务节点的选中完全由调用方决定。
|
||||
* 只补虚拟根这一个 key,不会顺手选中任何业务节点。
|
||||
*
|
||||
* 注意 selectedKeys 与其他 ui/Tree props 一样走 $attrs(本组件未声明它),
|
||||
* 因此这里必须从 attrs 取值;若从 props 取会永远拿到 undefined,
|
||||
* 从而把调用方传的真实选中项覆盖成虚拟根。
|
||||
*/
|
||||
const effectiveSelectedKeys = computed(() => {
|
||||
const keys = attrs.selectedKeys
|
||||
if (!props.showRoot) return keys
|
||||
if (keys && keys.length) return keys
|
||||
return [props.rootKey]
|
||||
})
|
||||
|
||||
/**
|
||||
* select 事件归一化:点中虚拟根时回传 [] 而非 [rootKey]。
|
||||
* 与 effectiveSelectedKeys 是一对镜像处理,保证调用方永远看不到 rootKey。
|
||||
*/
|
||||
function onSelect(keys, info) {
|
||||
const isRoot = info?.node?.isFmsRoot || hasRootKey(keys)
|
||||
emit('select', isRoot ? [] : keys, info)
|
||||
}
|
||||
|
||||
/**
|
||||
* 虚拟根默认展开(调用方的展开键里不需要出现 rootKey);
|
||||
* 用户收起根之后不再强制补回,避免箭头点不动。
|
||||
@@ -128,21 +170,46 @@ function onExpandedKeysChange(keys) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Tree
|
||||
v-bind="$attrs"
|
||||
:tree-data="effectiveTreeData"
|
||||
:field-names="fn"
|
||||
:expanded-keys="effectiveExpandedKeys"
|
||||
@update:expanded-keys="onExpandedKeysChange"
|
||||
>
|
||||
<!-- 标题:默认渲染翻译后的文本;调用方可用 #title 覆盖,并从插槽参数取翻译结果 -->
|
||||
<template #title="scope">
|
||||
<slot name="title" :node="scope.node" :title="resolveNodeTitle(scope.node)">
|
||||
{{ resolveNodeTitle(scope.node) }}
|
||||
<div class="fms-tree">
|
||||
<Tree
|
||||
v-bind="$attrs"
|
||||
:tree-data="effectiveTreeData"
|
||||
:field-names="fn"
|
||||
:expanded-keys="effectiveExpandedKeys"
|
||||
:selected-keys="effectiveSelectedKeys"
|
||||
@update:expanded-keys="onExpandedKeysChange"
|
||||
@select="onSelect"
|
||||
>
|
||||
<!-- 标题:默认渲染翻译后的文本;调用方可用 #title 覆盖,并从插槽参数取翻译结果 -->
|
||||
<template #title="scope">
|
||||
<slot name="title" :node="scope.node" :title="resolveNodeTitle(scope.node)">
|
||||
{{ resolveNodeTitle(scope.node) }}
|
||||
</slot>
|
||||
</template>
|
||||
<template v-if="$slots.empty" #empty>
|
||||
<slot name="empty" />
|
||||
</template>
|
||||
</Tree>
|
||||
<!-- 加载遮罩盖住树区域;#loading 插槽可覆盖默认的居中 Spin -->
|
||||
<div v-if="loading" class="fms-tree__loading">
|
||||
<slot name="loading">
|
||||
<Spin />
|
||||
</slot>
|
||||
</template>
|
||||
<template v-if="$slots.empty" #empty>
|
||||
<slot name="empty" />
|
||||
</template>
|
||||
</Tree>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fms-tree {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.fms-tree__loading {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgb(255 255 255 / 60%);
|
||||
}
|
||||
</style>
|
||||
@@ -169,10 +169,20 @@ function toggleCollapse() {
|
||||
grid-template-rows: 0fr;
|
||||
}
|
||||
|
||||
/* 行高被压缩时子项要真能收起来:min-height: 0(允许被压)+ overflow: hidden(裁掉溢出) */
|
||||
/* 行高被压缩时子项要真能收起来:min-height: 0(允许被压)+ 裁剪(裁掉溢出)。
|
||||
展开态用 clip + 外扩 4px 裁剪边界:输入框的聚焦光环是向外扩散 3px 的 box-shadow,
|
||||
紧贴网格边缘的一侧会被 overflow: hidden 削掉(最左列削左边、首末行削上下)。
|
||||
overflow: hidden 在前作降级:不支持 clip 的浏览器仍能正常折叠,只是光环照旧被裁 */
|
||||
.fms-form-group__collapse > * {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
overflow: clip;
|
||||
overflow-clip-margin: 4px;
|
||||
}
|
||||
|
||||
/* 折叠态必须收干净:外扩的裁剪边界会让 0fr 压下去的内容再露出 4px */
|
||||
.fms-form-group__collapse.is-collapsed > * {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ========== 组内宫格 ========== */
|
||||
|
||||
@@ -148,22 +148,9 @@
|
||||
padding: 0 1px;
|
||||
}
|
||||
|
||||
/* ---------- 拖拽手柄(在最左侧) ---------- */
|
||||
.tree-node-grip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: none;
|
||||
width: var(--fms-tree-grip-size);
|
||||
height: var(--fms-tree-grip-size);
|
||||
margin-right: 4px;
|
||||
color: #b0b7c3;
|
||||
cursor: grab;
|
||||
/* 从手柄按下时不选中行文本(拖拽由 mousedown 启动,不能 preventDefault) */
|
||||
user-select: none;
|
||||
}
|
||||
.tree-node-grip:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
/* ---------- 拖拽 ----------
|
||||
不再设手柄:整行长按发起(见 tree-node.vue 的 onRowMouseDown)。
|
||||
行内因此不需要为拖拽预留任何列,横向空间全部留给缩进、箭头与标题。 */
|
||||
|
||||
/* ---------- 拖拽落点指示:圆点 + 连接线 ---------- */
|
||||
.tree-node.drop-before::before,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, inject, onMounted, onBeforeUnmount, ref } from 'vue'
|
||||
import { GripVertical, ChevronRight, LoaderCircle } from '@lucide/vue'
|
||||
import { ChevronRight, LoaderCircle } from '@lucide/vue'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import { getNodeKey, getNodeTitle, getNodeChildren, isLeafNode } from './utils.js'
|
||||
|
||||
@@ -111,30 +111,74 @@ function onContentClick(e) {
|
||||
function onCheckChange() {
|
||||
ctx.handleCheck(key.value, props.node)
|
||||
}
|
||||
/* ---------- 拖拽:仅最左侧 grip 手柄可发起 ---------- */
|
||||
// 行默认 draggable=false。整行可拖时,「按下后手抖几像素」就会误起原生拖拽,
|
||||
// 而松手若没落在合法落点上不会走 drop,行上又没绑 dragend → dragKey 永远清不掉,
|
||||
// 节点一直挂着 is-dragging(opacity .4),表现就是「点了发灰、移开也灰」。
|
||||
const gripArmed = ref(false)
|
||||
/* ---------- 拖拽:长按整行发起(不设手柄) ---------- */
|
||||
// 行默认 draggable=false,长按 HOLD_DELAY_MS 之后才临时置为 true,让两件事同时成立:
|
||||
// 1. 短按 / 双击 / 多选点击不受影响,仍是普通的选中与展开操作;
|
||||
// 2. 不会「按下即拖」—— 行可拖时手抖几像素就会被判成拖拽,而松手若没落在合法落点上
|
||||
// 不会走 drop,dragKey 清不掉,节点会一直挂着 is-dragging(opacity .4)。
|
||||
// 浏览器只在「已按下 + 指针移动」时才发起 dragstart,所以延时到点后再置 draggable=true,
|
||||
// 用户接着移动指针即可开始拖拽;到点后原地不动、或提前松手都不会触发。
|
||||
const HOLD_DELAY_MS = 400
|
||||
// 长按期间的手抖余量(px):位移超过它说明用户在滑动/圈选,放弃本次拖拽
|
||||
const HOLD_SLOP_PX = 6
|
||||
|
||||
const dragArmed = ref(false)
|
||||
const draggableAttr = computed(() =>
|
||||
ctx.props.draggable && !dragDisabled.value && gripArmed.value ? 'true' : 'false',
|
||||
ctx.props.draggable && !dragDisabled.value && dragArmed.value ? 'true' : 'false',
|
||||
)
|
||||
|
||||
/** grip 按下:武装本行拖拽(同步写 DOM,dragstart 可能早于 Vue 的 patch) */
|
||||
let holdTimer = null
|
||||
let holdOrigin = null
|
||||
|
||||
function clearHold() {
|
||||
if (holdTimer) {
|
||||
clearTimeout(holdTimer)
|
||||
holdTimer = null
|
||||
}
|
||||
holdOrigin = null
|
||||
}
|
||||
|
||||
/** 长按到位:武装本行拖拽(同步写 DOM,dragstart 可能早于 Vue 的 patch) */
|
||||
function armDrag() {
|
||||
if (dragDisabled.value || !ctx.props.draggable) return
|
||||
gripArmed.value = true
|
||||
dragArmed.value = true
|
||||
rowRef.value?.setAttribute('draggable', 'true')
|
||||
}
|
||||
|
||||
function disarmDrag() {
|
||||
clearHold()
|
||||
document.removeEventListener('mousemove', onHoldMove)
|
||||
if (!dragArmed.value) return
|
||||
dragArmed.value = false
|
||||
rowRef.value?.setAttribute('draggable', 'false')
|
||||
}
|
||||
|
||||
/** 行按下:延时武装。箭头与复选框有自己的交互,不参与长按拖拽 */
|
||||
function onRowMouseDown(e) {
|
||||
if (!ctx.props.draggable || dragDisabled.value) return
|
||||
if (e.button !== 0) return
|
||||
if (e.target?.closest?.('.tree-node-switcher, .tree-node-checkbox')) return
|
||||
clearHold()
|
||||
holdOrigin = { x: e.clientX, y: e.clientY }
|
||||
holdTimer = setTimeout(() => {
|
||||
holdTimer = null
|
||||
holdOrigin = null
|
||||
armDrag()
|
||||
}, HOLD_DELAY_MS)
|
||||
document.addEventListener('mousemove', onHoldMove)
|
||||
// 按下但没真拖(不会产生 dragend)时的兜底解除
|
||||
document.addEventListener('mouseup', disarmDrag, { once: true })
|
||||
}
|
||||
function disarmDrag() {
|
||||
if (!gripArmed.value) return
|
||||
gripArmed.value = false
|
||||
rowRef.value?.setAttribute('draggable', 'false')
|
||||
|
||||
/** 长按未到位就移动:视为滑动/圈选,放弃本次拖拽 */
|
||||
function onHoldMove(e) {
|
||||
if (!holdOrigin) return
|
||||
const dx = e.clientX - holdOrigin.x
|
||||
const dy = e.clientY - holdOrigin.y
|
||||
if (dx * dx + dy * dy > HOLD_SLOP_PX * HOLD_SLOP_PX) clearHold()
|
||||
}
|
||||
|
||||
function onDragStart(e) {
|
||||
if (dragDisabled.value || !gripArmed.value) {
|
||||
if (dragDisabled.value || !dragArmed.value) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
@@ -145,7 +189,11 @@ function onDragEnd(e) {
|
||||
disarmDrag()
|
||||
ctx.onDragEnd(e)
|
||||
}
|
||||
onBeforeUnmount(() => document.removeEventListener('mouseup', disarmDrag))
|
||||
onBeforeUnmount(() => {
|
||||
clearHold()
|
||||
document.removeEventListener('mousemove', onHoldMove)
|
||||
document.removeEventListener('mouseup', disarmDrag)
|
||||
})
|
||||
|
||||
function onDragOver(e) {
|
||||
ctx.onDragOver(props.node, e)
|
||||
@@ -175,6 +223,7 @@ function onContextMenu(e) {
|
||||
:aria-disabled="disabled || undefined"
|
||||
:tabindex="tabindex"
|
||||
:draggable="draggableAttr"
|
||||
@mousedown="onRowMouseDown"
|
||||
@dragstart="onDragStart"
|
||||
@dragover="onDragOver"
|
||||
@dragleave="onDragLeave"
|
||||
@@ -183,10 +232,9 @@ function onContextMenu(e) {
|
||||
@contextmenu.stop="onContextMenu"
|
||||
>
|
||||
<!-- 展开/收起箭头:非叶子渲染真实箭头,叶子渲染等宽透明占位。
|
||||
占位不是多占地方,而是对齐机制:没有它,叶子标题会左移一个箭头宽,
|
||||
比同层非叶子靠左、并与父级标题齐平(缩进不足),层级关系丢失。
|
||||
位置在拖拽手柄之前:占位是「缩进带」的一部分(叶子行它是空白),
|
||||
放到手柄之后会让手柄与标题之间空出一个箭头宽,读起来像多余空隙。 -->
|
||||
叶子占位是必须的:去掉它,叶子标题会左移一个箭头宽,同层内与非叶子错开。
|
||||
不设拖拽手柄后,本槽位是全行唯一的固定槽 —— 缩进基准稳定,层级才读得出来;
|
||||
拖拽由「长按整行」发起(见 onRowMouseDown),不再需要独立的拖拽列。 -->
|
||||
<span
|
||||
v-if="!leaf"
|
||||
class="tree-node-switcher"
|
||||
@@ -205,16 +253,6 @@ function onContextMenu(e) {
|
||||
</span>
|
||||
<span v-else class="tree-node-switcher is-placeholder" :aria-hidden="true"></span>
|
||||
|
||||
<!-- 拖拽手柄:整行唯一可发起拖拽的区域(按下时才把本行置为 draggable);不可拖拽的节点不渲染 -->
|
||||
<span
|
||||
v-if="ctx.props.draggable && !dragDisabled"
|
||||
class="tree-node-grip"
|
||||
title="拖拽调整顺序"
|
||||
@mousedown="armDrag"
|
||||
>
|
||||
<GripVertical :aria-hidden="true" />
|
||||
</span>
|
||||
|
||||
<span v-if="ctx.props.checkable" class="tree-node-checkbox">
|
||||
<Checkbox
|
||||
:model-value="isChecked"
|
||||
|
||||
Reference in new issue
Block a user