20260908170359

This commit is contained in:
oneao committed 2026-09-08 17:03:59 +08:00
1 parent 2f5eac079e
commit 8734b43a2a
14 files changed
+1956 -1063

No files matched your search

@@ -110,14 +110,17 @@
}
/* 加载态 —— spinner 在前,隐藏自带图标 */
.button-loading {
cursor: default;
}
.button-loading :deep(svg):not(.button-spinner) {
display: none;
}
/* 加载态虽因防重复提交而原生 disabled,但视觉是“进行中”而非“禁用”:
保持原配色(不降透明度、悬停不变色),光标用纯加载样式 */
.button-loading:disabled {
opacity: 1;
cursor: wait;
}
:deep(.button-spinner) {
width: 1em;
height: 1em;
+380 -168
View File
@@ -243,15 +243,12 @@ const searchForm = reactive(makeEmptyForm())
const queryPrefs = reactive(Object.fromEntries(SEARCH_FIELD_DEFS.map((field) => [field.key, { area: field.area || 'advanced', order: field.order || 999 }])))
const QUICK_VISIBLE_LIMIT = 4
const quickSearchExpanded = ref(false)
const drawerOpen = ref(false)
const drawerTab = ref('conditions')
const queryApplied = ref(false)
const activeQueryMode = ref('quick')
const queryMode = ref('quick')
const activeQueryMode = ref('quick') // 'quick' 搜索卡 / 'advanced' 左侧常驻高级面板
const settingsOpen = ref(false) // 查询设置右侧抽屉
const advancedLogic = ref('and')
const advancedNodes = reactive([])
const advancedGroups = reactive([])
const advancedSnapshot = ref(null)
const draggingFieldKey = ref('')
const dragOverFieldKey = ref('')
const allAdvancedNodes = computed(() => advancedNodes.concat(advancedGroups.flatMap((group) => group.nodes)))
@@ -282,13 +279,8 @@ const extraQuickConditionCount = computed(() => extraQuickSearchFields.value.fil
if (field.type === 'numrange') return searchForm[`${field.key}Min`] || searchForm[`${field.key}Max`]
return searchForm[field.key] !== '' && searchForm[field.key] !== null && searchForm[field.key] !== undefined
}).length)
// 常用区已填条件总数:高级模式的说明条要用 —— 告诉用户这些值还在,只是不参与
const quickConditionCount = computed(() => quickSearchFields.value.filter((field) => {
if (field.type === 'numrange') return searchForm[`${field.key}Min`] || searchForm[`${field.key}Max`]
return searchForm[field.key] !== '' && searchForm[field.key] !== null && searchForm[field.key] !== undefined
}).length)
// 高级模式下常用区强制收起:保留上下文(知道有哪几个常用条件),但不占版面
const showAllQuickFields = computed(() => quickSearchExpanded.value && activeQueryMode.value === 'quick')
// 高级模式下面板即状态呈现,不再需要「常用条件暂不参与」的说明
const showAllQuickFields = computed(() => quickSearchExpanded.value)
const advancedSearchFields = computed(() =>
searchFields.value.filter((field) => field.queryable !== false && queryPrefs[field.key]?.area !== 'hidden'),
)
@@ -379,30 +371,11 @@ function removeSummaryItem(item) {
}
if (!querySummary.value.length) queryApplied.value = false
}
function openQueryDrawer(tab = 'conditions') {
if (tab === 'conditions' && !drawerOpen.value) {
advancedSnapshot.value = {
logic: advancedLogic.value,
nodes: JSON.parse(JSON.stringify(advancedNodes)),
groups: JSON.parse(JSON.stringify(advancedGroups)),
}
}
drawerTab.value = tab
drawerOpen.value = true
function openSettings() {
settingsOpen.value = true
}
function closeQueryDrawer() {
if (advancedSnapshot.value) {
advancedLogic.value = advancedSnapshot.value.logic
advancedNodes.splice(0, advancedNodes.length, ...advancedSnapshot.value.nodes)
advancedGroups.splice(0, advancedGroups.length, ...advancedSnapshot.value.groups)
advancedSnapshot.value = null
queryMode.value = activeQueryMode.value
}
drawerOpen.value = false
}
function selectQueryMode(mode) {
queryMode.value = mode
if (mode === 'advanced') openQueryDrawer('conditions')
function closeSettings() {
settingsOpen.value = false
}
function setFieldArea(field, area) {
if (field.queryable === false) return
@@ -443,6 +416,11 @@ function resetQuickQuery() {
Object.assign(searchForm, makeEmptyForm())
if (activeQueryMode.value === 'quick') queryApplied.value = false
}
function resetAllQueries() {
resetQuickQuery()
resetAdvancedQuery()
queryApplied.value = false
}
function startFieldDrag(field, event) {
draggingFieldKey.value = field.key
// Firefox 下不写 dataTransfer 不会触发 drop,同时声明「移动」语义拿到正确的光标
@@ -476,28 +454,52 @@ function dropField(field, area) {
}
function onSearch() {
activeQueryMode.value = 'quick'
queryMode.value = 'quick'
queryApplied.value = true
}
function applyAdvancedQuery() {
// 进入高级模式:滑出左侧常驻面板(无遮罩),搜索卡隐藏、表格占满剩余宽高
function enterAdvancedMode() {
activeQueryMode.value = 'advanced'
queryMode.value = 'advanced'
queryApplied.value = true
advancedSnapshot.value = null
drawerOpen.value = false
}
function resetAllQueries() {
resetQuickQuery()
resetAdvancedQuery()
queryApplied.value = false
advancedSnapshot.value = null
}
// 回到简单模式:收起面板、恢复搜索卡
function returnToQuickQuery() {
if (drawerOpen.value && drawerTab.value === 'conditions') closeQueryDrawer()
activeQueryMode.value = 'quick'
queryMode.value = 'quick'
queryApplied.value = false
}
// 面板内应用:编辑即暂存,应用只固化生效态
function applyAdvancedQuery() {
queryApplied.value = true
}
/* ============================================================
* 布局原型扩展(模拟验证用,正式页面删除):
* pageLayout = 'flat' 纯列表 | 'tree' 树+表格。
* 验证「左栏槽位」约定:树(快速模式)与高级面板(高级模式)轮班占同一列,
* 表格宽度开销恒等于一列;树选中条件以 chip 呈现,两种模式都可见可清除。
* ============================================================ */
const pageLayout = ref('flat')
const TREE_NODES = [
{ id: 'all', label: '全部分类', children: [
{ id: 'c1', label: '华东区', children: [
{ id: 'c11', label: '上海' },
{ id: 'c12', label: '杭州' },
] },
{ id: 'c2', label: '华南区', children: [
{ id: 'c21', label: '广州' },
{ id: 'c22', label: '深圳' },
] },
{ id: 'c3', label: '华北区' },
] },
]
function findTreeLabel(nodes, id) {
for (const node of nodes) {
if (node.id === id) return node.label
const childLabel = findTreeLabel(node.children || [], id)
if (childLabel) return childLabel
}
return ''
}
// 树选中条件(页面固定条件):chips 条数据源之一,两种模式都渲染
const selectedTreeNodeId = ref('')
const selectedTreeNodeLabel = computed(() => findTreeLabel(TREE_NODES, selectedTreeNodeId.value) || '')
/* ---------- fms-table 大数据量 + 单行编辑 demo ---------- */
const TYPE_OPTIONS = [
@@ -614,7 +616,7 @@ const selectedText = computed(() =>
? t('demo.statusbar.selected').replace('{n}', selectedRowKeys.value.length)
: '',
)
const totalText = computed(() => t('demo.footer.total').replace('{n}', allRows.value.length))
const totalText = computed(() => t('demo.footer.total').replace('{n}', filteredRows.value.length))
function onRefresh() {
allRows.value = makeRows(DEFAULT_ROW_COUNT)
@@ -625,25 +627,138 @@ function onMoreAction({ key }) {
// 打印 / 密度 / 全屏:占位,后续接入
void key
}
/* 树选中过滤表格行(模拟;正式页面由 SQL 条件承担) */
const filteredRows = computed(() => {
if (!selectedTreeNodeLabel.value || selectedTreeNodeLabel.value === '全部分类') return allRows.value
return allRows.value.filter((row) => String(row.name).length % 2 === Number(selectedTreeNodeId.value.slice(-1)) % 2)
})
/* 左栏是否有内容占位:树(快速模式)或高级面板(高级模式) */
const leftColumnOccupied = computed(() => pageLayout.value === 'tree' || activeQueryMode.value === 'advanced')
/* 树扁平化渲染(模拟;正式页面用 ui/tree 组件) */
function flattenTree(nodes, depth = 0) {
const result = []
for (const node of nodes) {
result.push({ id: node.id, label: node.label, depth })
if (node.children?.length) result.push(...flattenTree(node.children, depth + 1))
}
return result
}
</script>
<template>
<div class="dashboard-page">
<!-- 搜索区:常用条件来自模块默认配置,并可由用户个性化调整 -->
<section class="table-card search-card">
<!-- 左栏槽位有内容(树 / 高级面板)时页面横排,右侧主列纵排搜索卡+表格卡 -->
<div class="dashboard-page" :class="{ 'dashboard-page--row': leftColumnOccupied }">
<!-- ===== 原型布局切换(正式页面删除):flat 纯列表 / tree 树+表格 ===== -->
<div class="layout-switch">
<Button :type="pageLayout === 'flat' ? 'primary' : 'outline'" size="small" @click="pageLayout = 'flat'">纯列表</Button>
<Button :type="pageLayout === 'tree' ? 'primary' : 'outline'" size="small" @click="pageLayout = 'tree'">树+表格</Button>
</div>
<!-- ===== 左栏槽位:树(快速模式)与高级面板(高级模式)轮班占位 ===== -->
<!-- 树+表格页面、快速模式:树占据左栏 -->
<aside v-if="pageLayout === 'tree' && activeQueryMode === 'quick'" class="tree-panel">
<div class="tree-panel__head">
<strong>物料分类</strong>
<button class="panel-icon-button" title="调整常用条件与高级条件的显示和顺序" @click="openSettings">
<Settings2 :size="15" />
</button>
</div>
<div class="tree-panel__body">
<button
v-for="node in flattenTree(TREE_NODES)"
:key="node.id"
class="tree-node"
:class="{ 'is-active': selectedTreeNodeId === node.id }"
:style="{ paddingLeft: `${8 + node.depth * 16}px` }"
@click="selectedTreeNodeId = selectedTreeNodeId === node.id ? '' : node.id"
>
{{ node.label }}
</button>
</div>
</aside>
<!-- 高级模式:高级面板占据左栏(树隐藏,树条件以 chip 呈现在生效条件条上) -->
<aside v-if="activeQueryMode === 'advanced'" class="advanced-panel">
<div class="advanced-panel__head">
<div>
<strong>高级查询</strong>
</div>
<div class="advanced-panel__head-actions">
<button class="panel-icon-button" title="调整常用条件与高级条件的显示和顺序" @click="openSettings">
<Settings2 :size="15" />
</button>
<button class="panel-icon-button" title="改用简单查询" @click="returnToQuickQuery">×</button>
</div>
</div>
<div class="advanced-panel__body">
<div class="ast-root"><span>满足</span><select v-model="advancedLogic"><option value="and">全部条件</option><option value="or">任一条件</option></select></div>
<div v-if="!advancedNodes.length && !advancedGroups.length" class="ast-empty">
<p>暂无条件</p>
<Button type="primary" @click="addAdvancedNode">+ 添加条件</Button>
</div>
<div v-for="node in advancedNodes" :key="node.id" class="ast-row">
<div class="ast-row__top">
<div class="ast-field-wrap">
<select v-model="node.field" class="ast-field" @change="onNodeFieldChange(node)">
<option v-for="field in advancedSearchFields" :key="field.key" :value="field.key">{{ field.label }}</option>
</select>
<span v-if="fieldByKey[node.field]?.relation" class="relation-badge">关联</span>
</div>
<button class="ast-remove" title="删除条件" @click="removeAdvancedNode(node.id)">×</button>
</div>
<div class="ast-row__bottom">
<select v-model="node.operator" class="ast-op" @change="onNodeOperatorChange(node)">
<option v-for="operator in getNodeOperators(node)" :key="operator.value" :value="operator.value">{{ operator.label }}</option>
</select>
<Input v-if="fieldByKey[node.field]?.type === 'input'" v-model="node.value" class="ast-value" placeholder="请输入值" />
<RangePicker v-else-if="fieldByKey[node.field]?.type === 'daterange'" v-model="node.value" class="ast-value" />
<div v-else-if="fieldByKey[node.field]?.type === 'numrange'" class="ast-range-value">
<template v-if="node.operator === 'between'"><Input v-model="node.value[0]" placeholder="最小值" /><span>至</span><Input v-model="node.value[1]" placeholder="最大值" /></template>
<Input v-else v-model="node.value" placeholder="请输入数值" />
</div>
<select v-else v-model="node.value" class="ast-value">
<option value="">请选择</option>
<option v-for="option in getFieldOptions(node.field)" :key="option.value" :value="option.value">{{ option.label }}</option>
</select>
</div>
</div>
<div v-for="group in advancedGroups" :key="group.id" class="ast-group">
<div class="ast-group__head"><span>条件组</span><select v-model="group.logic"><option value="and">全部满足</option><option value="or">任一满足</option></select><button type="button" @click="advancedGroups.splice(advancedGroups.indexOf(group), 1)">删除组</button></div>
<div v-for="node in group.nodes" :key="node.id" class="ast-row ast-row--nested">
<div class="ast-row__top">
<div class="ast-field-wrap"><select v-model="node.field" class="ast-field" @change="onNodeFieldChange(node)"><option v-for="field in advancedSearchFields" :key="field.key" :value="field.key">{{ field.label }}</option></select><span v-if="fieldByKey[node.field]?.relation" class="relation-badge">关联</span></div>
<button class="ast-remove" title="删除条件" @click="removeAdvancedNode(node.id)">×</button>
</div>
<div class="ast-row__bottom">
<select v-model="node.operator" class="ast-op" @change="onNodeOperatorChange(node)"><option v-for="operator in getNodeOperators(node)" :key="operator.value" :value="operator.value">{{ operator.label }}</option></select>
<Input v-if="fieldByKey[node.field]?.type === 'input'" v-model="node.value" class="ast-value" placeholder="请输入值" />
<RangePicker v-else-if="fieldByKey[node.field]?.type === 'daterange'" v-model="node.value" class="ast-value" />
<div v-else-if="fieldByKey[node.field]?.type === 'numrange'" class="ast-range-value"><template v-if="node.operator === 'between'"><Input v-model="node.value[0]" placeholder="最小值" /><span>至</span><Input v-model="node.value[1]" placeholder="最大值" /></template><Input v-else v-model="node.value" placeholder="请输入数值" /></div>
<select v-else v-model="node.value" class="ast-value"><option value="">请选择</option><option v-for="option in getFieldOptions(node.field)" :key="option.value" :value="option.value">{{ option.label }}</option></select>
</div>
</div>
<Button type="ghost" @click="group.nodes.push(createAdvancedNode(advancedSearchFields[0]?.key || 'creator'))">+ 条件</Button>
</div>
<div class="ast-actions"><Button type="outline" @click="addAdvancedNode">+ 添加条件</Button><Button type="outline" @click="addAdvancedGroup">+ 添加条件组</Button><Button type="ghost" class="clear-conditions" @click="resetAdvancedQuery">清空条件</Button></div>
</div>
<div class="advanced-panel__foot">
<span class="ast-foot-note">已配置 {{ advancedConditionCount }} 个条件</span>
<Button type="outline" @click="returnToQuickQuery">取消</Button>
<Button type="primary" @click="applyAdvancedQuery">应用查询</Button>
</div>
</aside>
<!-- 右侧主列:搜索卡 + 表格卡纵向堆叠;横排模式(树/高级面板占左栏)时占满剩余宽度 -->
<div class="dashboard-main">
<!-- 搜索区:常用条件来自模块默认配置,并可由用户个性化调整(高级模式隐藏,表格占满剩余宽高) -->
<section v-if="activeQueryMode === 'quick'" class="table-card search-card">
<!-- 查询设置:卡片级配置,绝对定位在右上角 —— 不占流内高度,
也不随字段增减、模式切换位移 -->
<Button type="ghost" class="settings-button search-card__settings" title="调整常用条件与高级条件的显示和顺序" @click="openQueryDrawer('settings')">
<Button type="ghost" class="settings-button search-card__settings" title="调整常用条件与高级条件的显示和顺序" @click="openSettings">
<Settings2 :size="15" /> 查询设置
</Button>
<!-- 高级模式:常用条件「降权」而不是消失,也不加遮罩 ——
遮罩在交互语义上是「阻塞/锁定」,而这里只是「不参与本次查询」。
只在你真的填了常用条件时才占这一行:没填值时降权区是空的,不解释也不会困惑 -->
<div v-if="activeQueryMode === 'advanced' && quickConditionCount" class="mode-notice">
<span class="mode-notice__dot" />
<span>顶部已填的 {{ quickConditionCount }} 个常用条件暂不参与本次高级查询,改用简单查询后重新生效。</span>
</div>
<div class="list-search" :class="{ 'list-search--muted': activeQueryMode === 'advanced' }">
<div class="list-search">
<div v-for="field in (showAllQuickFields ? quickSearchFields : quickSearchFields.slice(0, QUICK_VISIBLE_LIMIT))" :key="field.key" class="search-field">
<label class="search-field__label">{{ field.label }}</label>
<Input
@@ -670,8 +785,7 @@ function onMoreAction({ key }) {
<Input v-model="searchForm[`${field.key}Max`]" class="search-field__control--mini" />
</div>
</div>
<!-- 操作区只放「执行」类动作;模式切换统一在卡片头的分段控件上,
不再让按钮在不同模式下变形 -->
<!-- 操作区只放「执行」类动作;模式切换由「高级查询」入口承担 -->
<div class="list-search__actions">
<template v-if="activeQueryMode === 'quick'">
<div class="action-group">
@@ -684,32 +798,27 @@ function onMoreAction({ key }) {
<span v-if="!quickSearchExpanded && extraQuickConditionCount" class="query-count">{{ extraQuickConditionCount }}</span>
<ChevronDown :size="14" :class="{ 'is-up': quickSearchExpanded }" />
</Button>
<Button type="ghost" class="advanced-query-button advanced-query-button--ghost" @click="selectQueryMode('advanced')">
<Button type="ghost" class="advanced-query-button advanced-query-button--ghost" @click="enterAdvancedMode">
<SlidersHorizontal :size="14" />
高级查询
<span v-if="advancedConditionCount" class="query-count">{{ advancedConditionCount }}</span>
</Button>
</div>
</template>
<template v-else>
<div class="action-group">
<Button type="outline" class="advanced-query-button" @click="openQueryDrawer('conditions')">
<SlidersHorizontal :size="14" /> 编辑高级查询
<span v-if="advancedConditionCount" class="query-count">{{ advancedConditionCount }}</span>
</Button>
</div>
<div class="action-group">
<Button type="ghost" @click="returnToQuickQuery">改用简单查询</Button>
<Button type="ghost" @click="resetAllQueries">清除查询</Button>
</div>
</template>
</div>
</div>
<div v-if="queryApplied && querySummary.length" class="active-query-bar">
<span class="active-query-label">{{ activeQueryMode === 'quick' ? '简单查询生效' : '高级查询生效' }}</span>
<!-- 树条件 chip:高级模式下树面板被轮换隐藏,靠这枚 chip 保持可见、可清除 -->
<span v-if="selectedTreeNodeLabel && selectedTreeNodeLabel !== '全部分类'" class="active-query-item" :title="`分类 ${selectedTreeNodeLabel}`"><span>分类 {{ selectedTreeNodeLabel }}</span><button type="button" aria-label="清除分类条件" @click="selectedTreeNodeId = ''">×</button></span>
<span v-for="item in querySummary" :key="item.id" class="active-query-item" :title="item.text"><span>{{ item.text }}</span><button type="button" aria-label="移除条件" @click="removeSummaryItem(item)">×</button></span>
<button class="clear-query" @click="resetAllQueries">清除全部</button>
</div>
<!-- 未应用查询时也保持条位(空态):树条件始终有处可看,不随模式切换跳动 -->
<div v-else-if="selectedTreeNodeLabel && selectedTreeNodeLabel !== '全部分类'" class="active-query-bar">
<span class="active-query-label">固定条件</span>
<span class="active-query-item" :title="`分类 ${selectedTreeNodeLabel}`"><span>分类 {{ selectedTreeNodeLabel }}</span><button type="button" aria-label="清除分类条件" @click="selectedTreeNodeId = ''">×</button></span>
</div>
</section>
<!-- ②③④ 表格卡:工具条 + 表格 + 分页条,撑满剩余高度 -->
@@ -769,7 +878,7 @@ function onMoreAction({ key }) {
<div class="list-table">
<FmsTable
:columns="columns"
:data-source="allRows"
:data-source="filteredRows"
:row-class-name="rowClassName"
:row-selection="rowSelection"
row-key="id"
@@ -779,68 +888,25 @@ function onMoreAction({ key }) {
<!-- 底部分页条:仅展示,不与表格数据联动(demo 阶段表格恒为 1w 行全量) -->
<div class="list-footer">
<span class="footer-total">{{ totalText }}</span>
<Pagination :total="allRows.length" show-size-changer />
<Pagination :total="filteredRows.length" show-size-changer />
</div>
</section>
</div>
</div>
<!-- 查询 Drawer:高级条件与查询设置使用独立入口,共用 Drawer 外壳 -->
<div v-if="drawerOpen" class="query-drawer-mask" :class="{ 'query-drawer-mask--light': drawerTab === 'settings' }" @click.self="closeQueryDrawer">
<aside class="query-drawer" :class="{ 'query-drawer--narrow': drawerTab === 'settings' }">
<!-- 查询设置:右侧抽屉(浅遮罩),两个模式共用 -->
<div v-if="settingsOpen" class="query-drawer-mask--light" @click.self="closeSettings">
<aside class="query-drawer query-drawer--narrow">
<div class="query-drawer__head">
<div>
<strong>{{ drawerTab === 'conditions' ? '高级查询' : '查询设置' }}</strong>
<span class="query-drawer__sub">{{ drawerTab === 'conditions' ? '设置本次查询需要满足的条件,将替代常用条件' : '调整常用条件的显示和顺序,改动即时生效' }}</span>
<strong>查询设置</strong>
<span class="query-drawer__sub">调整常用条件的显示和顺序,改动即时生效</span>
</div>
<button class="query-drawer__close" aria-label="关闭" @click="closeQueryDrawer">×</button>
<button class="query-drawer__close" aria-label="关闭" @click="closeSettings">×</button>
</div>
<div v-if="drawerTab === 'conditions'" class="query-drawer__body">
<div class="drawer-section-title">当前条件</div>
<div class="ast-root"><span>满足</span><select v-model="advancedLogic"><option value="and">全部条件</option><option value="or">任一条件</option></select></div>
<div v-if="!advancedNodes.length && !advancedGroups.length" class="ast-empty">
<p>暂无条件</p>
<Button type="primary" @click="addAdvancedNode">+ 添加条件</Button>
</div>
<div v-for="node in advancedNodes" :key="node.id" class="ast-row">
<div class="ast-field-wrap">
<select v-model="node.field" class="ast-field" @change="onNodeFieldChange(node)">
<option v-for="field in advancedSearchFields" :key="field.key" :value="field.key">{{ field.label }}</option>
</select>
<span v-if="fieldByKey[node.field]?.relation" class="relation-badge">关联</span>
</div>
<select v-model="node.operator" class="ast-op" @change="onNodeOperatorChange(node)">
<option v-for="operator in getNodeOperators(node)" :key="operator.value" :value="operator.value">{{ operator.label }}</option>
</select>
<Input v-if="fieldByKey[node.field]?.type === 'input'" v-model="node.value" class="ast-value" placeholder="请输入值" />
<RangePicker v-else-if="fieldByKey[node.field]?.type === 'daterange'" v-model="node.value" class="ast-value" />
<div v-else-if="fieldByKey[node.field]?.type === 'numrange'" class="ast-range-value">
<template v-if="node.operator === 'between'"><Input v-model="node.value[0]" placeholder="最小值" /><span>至</span><Input v-model="node.value[1]" placeholder="最大值" /></template>
<Input v-else v-model="node.value" placeholder="请输入数值" />
</div>
<select v-else v-model="node.value" class="ast-value">
<option value="">请选择</option>
<option v-for="option in getFieldOptions(node.field)" :key="option.value" :value="option.value">{{ option.label }}</option>
</select>
<button class="ast-remove" title="删除条件" @click="removeAdvancedNode(node.id)">×</button>
</div>
<div v-for="group in advancedGroups" :key="group.id" class="ast-group">
<div class="ast-group__head"><span>条件组</span><select v-model="group.logic"><option value="and">全部满足</option><option value="or">任一满足</option></select><button type="button" @click="advancedGroups.splice(advancedGroups.indexOf(group), 1)">删除组</button></div>
<div v-for="node in group.nodes" :key="node.id" class="ast-row ast-row--nested">
<div class="ast-field-wrap"><select v-model="node.field" class="ast-field" @change="onNodeFieldChange(node)"><option v-for="field in advancedSearchFields" :key="field.key" :value="field.key">{{ field.label }}</option></select><span v-if="fieldByKey[node.field]?.relation" class="relation-badge">关联</span></div>
<select v-model="node.operator" class="ast-op" @change="onNodeOperatorChange(node)"><option v-for="operator in getNodeOperators(node)" :key="operator.value" :value="operator.value">{{ operator.label }}</option></select>
<Input v-if="fieldByKey[node.field]?.type === 'input'" v-model="node.value" class="ast-value" placeholder="请输入值" />
<RangePicker v-else-if="fieldByKey[node.field]?.type === 'daterange'" v-model="node.value" class="ast-value" />
<div v-else-if="fieldByKey[node.field]?.type === 'numrange'" class="ast-range-value"><template v-if="node.operator === 'between'"><Input v-model="node.value[0]" placeholder="最小值" /><span>至</span><Input v-model="node.value[1]" placeholder="最大值" /></template><Input v-else v-model="node.value" placeholder="请输入数值" /></div>
<select v-else v-model="node.value" class="ast-value"><option value="">请选择</option><option v-for="option in getFieldOptions(node.field)" :key="option.value" :value="option.value">{{ option.label }}</option></select>
<button class="ast-remove" title="删除条件" @click="removeAdvancedNode(node.id)">×</button>
</div>
<Button type="ghost" @click="group.nodes.push(createAdvancedNode(advancedSearchFields[0]?.key || 'creator'))">+ 条件</Button>
</div>
<div class="ast-hint"><LockKeyhole :size="14" /> 无权限字段不会出现在字段选择器中</div>
<div class="drawer-actions"><Button type="outline" @click="addAdvancedNode">+ 添加条件</Button><Button type="outline" @click="addAdvancedGroup">+ 添加条件组</Button><Button type="ghost" class="clear-conditions" @click="resetAdvancedQuery">清空条件</Button></div>
</div>
<div v-else class="query-drawer__body settings-body">
<div class="drawer-section-title">我的查询布局 <span>只影响当前用户</span></div>
<div class="query-drawer__body settings-body">
<div class="settings-section-title">我的查询布局 <span>只影响当前用户</span></div>
<div class="settings-note">拖动调整常用条件顺序,也可以把字段移到高级条件或隐藏。</div>
<div class="setting-group"><div class="setting-group__head"><strong>页面常用条件</strong><span>直接显示在列表顶部</span></div><div v-for="field in searchFields.filter((item) => queryPrefs[item.key].area === 'quick').toSorted((a, b) => queryPrefs[a.key].order - queryPrefs[b.key].order)" :key="field.key" class="setting-row" :class="rowDragClass(field)" draggable="true" @dragstart="startFieldDrag(field, $event)" @dragover.prevent="dragOverFieldKey = field.key" @dragleave="dragOverFieldKey = ''" @drop="dropField(field, 'quick')" @dragend="endFieldDrag"><GripVertical :size="14" class="drag-handle" /><span class="setting-label">{{ field.label }}</span><button class="setting-first" @click="setFirstQuick(field)">设为第一个</button><button class="setting-more" @click="setFieldArea(field,'advanced')">移到高级</button></div></div>
<div class="setting-group"><div class="setting-group__head"><strong>仅高级条件</strong><span>不占页面空间</span></div><div v-for="field in searchFields.filter((item) => queryPrefs[item.key].area === 'advanced').toSorted((a, b) => queryPrefs[a.key].order - queryPrefs[b.key].order)" :key="field.key" class="setting-row" :class="rowDragClass(field)" draggable="true" @dragstart="startFieldDrag(field, $event)" @dragover.prevent="dragOverFieldKey = field.key" @dragleave="dragOverFieldKey = ''" @drop="dropField(field, 'advanced')" @dragend="endFieldDrag"><GripVertical :size="14" class="drag-handle" /><span class="setting-label">{{ field.label }}</span><button class="setting-first" @click="setFieldArea(field,'quick')">移到常用</button><button class="setting-more" @click="setFieldArea(field,'hidden')">隐藏</button></div></div>
@@ -848,15 +914,8 @@ function onMoreAction({ key }) {
<div class="setting-group setting-group--muted"><div class="setting-group__head"><strong>不可用字段</strong><span>字段权限限制</span></div><div v-for="field in searchFields.filter((item) => item.queryable === false)" :key="field.key" class="setting-row is-disabled"><LockKeyhole :size="14" /><span class="setting-label">{{ field.label }}</span><span class="setting-disabled">无查询权限</span></div></div>
</div>
<div class="query-drawer__foot">
<span v-if="drawerTab === 'conditions'" class="ast-foot-note">已配置 {{ advancedConditionCount }} 个条件</span>
<template v-if="drawerTab === 'conditions'">
<Button type="outline" @click="closeQueryDrawer">取消</Button>
<Button type="primary" @click="applyAdvancedQuery">应用查询</Button>
</template>
<template v-else>
<Button type="ghost" @click="resetQueryPrefs">恢复默认</Button>
<Button type="primary" @click="drawerOpen = false">完成设置</Button>
</template>
<Button type="ghost" @click="resetQueryPrefs">恢复默认</Button>
<Button type="primary" @click="closeSettings">完成设置</Button>
</div>
</aside>
</div>
@@ -879,6 +938,171 @@ function onMoreAction({ key }) {
color: var(--fms-text);
}
/* ---- 查询设置抽屉的节标题 ---- */
.settings-section-title { display: flex; align-items: baseline; gap: 8px; margin-bottom: 10px; color: var(--fms-text); font-size: 13px; font-weight: 600; }
.settings-section-title span { color: var(--fms-text-secondary); font-size: 12px; font-weight: 400; }
/* 高级模式骨架调整:改为横排(左栏 + 右主列)、取消固定 min-width(表格自己横向滚) */
.dashboard-page--row {
flex-direction: row;
min-width: 0;
}
/* 横排时搜索卡允许收缩(树占掉一列后,搜索卡定宽 4 列会把表格挤没),
容器查询随之降列数;纯列表(纵排)模式不受影响 */
.dashboard-page--row .search-card {
flex: 0 1 auto;
min-width: 0;
}
/* 右侧主列:搜索卡 + 表格卡纵向堆叠,占满左栏以外的全部宽度 */
.dashboard-main {
display: flex;
flex: 1;
min-width: 0;
min-height: 0;
flex-direction: column;
gap: 8px;
}
/* ---- 左侧常驻高级面板:无遮罩,全高,表格保持可交互 ---- */
.advanced-panel {
display: flex;
flex: none;
flex-direction: column;
width: 360px;
min-height: 0;
border-radius: 6px;
background: var(--fms-card);
box-shadow: 0 2px 6px rgb(31 35 41 / 4%);
}
/* ---- 原型布局切换(正式页面删除):右下角浮动小工具条 ---- */
.layout-switch {
position: fixed;
right: 16px;
bottom: 16px;
z-index: 60;
display: flex;
gap: 6px;
padding: 6px;
border-radius: 8px;
background: var(--fms-card);
box-shadow: 0 4px 16px rgb(31 35 41 / 18%);
}
/* ---- 树面板(模拟树+表格页):与高级面板同宽同位,轮班占左栏 ---- */
.tree-panel {
display: flex;
flex: none;
flex-direction: column;
width: 360px;
min-height: 0;
border-radius: 6px;
background: var(--fms-card);
box-shadow: 0 2px 6px rgb(31 35 41 / 4%);
}
.tree-panel__head {
display: flex;
flex: none;
align-items: center;
justify-content: space-between;
padding: 14px 14px 10px;
}
.tree-panel__head strong {
font-size: 15px;
color: var(--fms-text);
}
.tree-panel__body {
flex: 1;
min-height: 0;
overflow: auto;
padding: 0 8px 10px;
}
.tree-node {
display: block;
width: 100%;
padding: 7px 10px;
border: 0;
border-radius: 5px;
background: transparent;
color: var(--fms-text);
font-size: 13px;
text-align: left;
cursor: pointer;
}
.tree-node:hover {
background: var(--fms-background, #f5f6f8);
}
.tree-node.is-active {
background: color-mix(in srgb, var(--fms-primary) 10%, transparent);
color: var(--fms-primary);
font-weight: 500;
}
.advanced-panel__head {
display: flex;
flex: none;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
padding: 14px 14px 10px;
}
.advanced-panel__head strong {
display: block;
font-size: 15px;
color: var(--fms-text);
}
.advanced-panel__head-actions {
display: flex;
flex: none;
gap: 4px;
}
.panel-icon-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
border: 0;
border-radius: 5px;
background: transparent;
color: var(--fms-text-secondary);
font-size: 16px;
cursor: pointer;
}
.panel-icon-button:hover {
background: var(--fms-background, #f5f6f8);
color: var(--fms-text);
}
.advanced-panel__body {
flex: 1;
min-height: 0;
overflow: auto;
padding: 4px 14px 10px;
}
.advanced-panel__foot {
display: flex;
flex: none;
align-items: center;
gap: 8px;
padding: 12px 14px;
border-top: 1px solid var(--fms-border-soft, #f0f1f3);
}
/* 白卡公共样式。表格卡:撑满剩余高度、内部滚动;
留白统一由卡的 padding + gap 承担,各段之间不用 border 分隔 */
.table-card {
@@ -908,16 +1132,6 @@ function onMoreAction({ key }) {
/* 右上角固定位:绝对定位不占流内高度(查询区本身就不宽裕),
右缘与内容区(padding 16)对齐 */
.search-card__settings { position: absolute; top: 10px; right: 16px; z-index: 1; }
/* 高级模式:常用条件降权(低透明度 + 去饱和 + 禁用交互)而不是消失、也不是遮罩。
遮罩的语义是「阻塞/锁定」,这里只是「不参与本次查询」。
只降权 .search-field,操作区按钮保持可用 */
.list-search--muted .search-field {
opacity: 0.45;
filter: saturate(0.5);
pointer-events: none;
}
/* 说明条压到最薄:它只在有已填常用条件时才出现,不该再抢版面 */
.search-card .mode-notice { margin: 0; padding: 6px 10px; }
.advanced-query-button { gap: 5px; }
/* ghost(常用模式下的入口)用次要色,不与「查询/重置」抢视觉;
outline(高级模式下的主操作)保持主色 */
@@ -1168,10 +1382,7 @@ function onMoreAction({ key }) {
.active-query-item button { width: 16px; height: 16px; padding: 0; border: 0; background: transparent; color: #7c8da6; font-size: 15px; line-height: 14px; cursor: pointer; }
.active-query-item button:hover { color: var(--fms-danger); }
.clear-query { flex: none; border: 0; background: transparent; color: var(--fms-primary); font-size: 12px; }
.query-drawer-mask { position: fixed; z-index: 50; inset: 0; background: rgb(31 35 41 / 32%); }
/* 设置面板:遮罩压到最浅,让左侧搜索区保持可见 ——
调顺序、移分组时能直接看到常用区的实时变化,不用关掉才知道结果 */
.query-drawer-mask--light { background: rgb(31 35 41 / 8%); }
.query-drawer-mask--light { position: fixed; z-index: 50; inset: 0; background: rgb(31 35 41 / 8%); }
.query-drawer {
display: flex;
width: min(760px, 100%);
@@ -1189,35 +1400,36 @@ function onMoreAction({ key }) {
.query-drawer__close { width: 30px; height: 30px; border: 0; border-radius: 5px; background: transparent; color: var(--fms-text-secondary); font-size: 22px; }
.query-drawer__close:hover { background: var(--fms-background, #f5f6f8); }
.query-drawer__body { flex: 1; min-height: 0; overflow: auto; padding: 18px 20px; }
.mode-notice { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 16px; padding: 9px 10px; border: 1px solid #d8e5f7; border-radius: 5px; background: #f7faff; color: #526176; font-size: 12px; line-height: 1.5; }
.mode-notice__dot { flex: none; width: 7px; height: 7px; margin-top: 5px; border-radius: 50%; background: var(--fms-primary); }
.drawer-section-title { display: flex; align-items: baseline; gap: 8px; margin-bottom: 10px; color: var(--fms-text); font-size: 13px; font-weight: 600; }
.drawer-section-title span { color: var(--fms-text-secondary); font-size: 12px; font-weight: 400; }
/* ---- 高级面板:条件行窄版两行堆叠(第一行字段+删除,第二行操作符+值) ---- */
.ast-root { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; color: var(--fms-text-secondary); font-size: 12px; }
.ast-root select, .ast-row select { height: 34px; border: 1px solid var(--fms-border); border-radius: 4px; background: var(--fms-card); color: var(--fms-text); padding: 0 9px; outline: none; }
.ast-empty { display: flex; flex-direction: column; align-items: center; gap: 10px; margin: 8px 0 12px; padding: 22px 12px; border: 1px dashed var(--fms-border); border-radius: 5px; color: var(--fms-text-secondary); font-size: 12px; text-align: center; }
.ast-empty p { margin: 0; }
.ast-row { display: grid; grid-template-columns: minmax(160px, 1.25fr) 150px minmax(130px, 1fr) 30px; gap: 8px; align-items: center; margin-bottom: 8px; }
.ast-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; padding: 8px; border: 1px solid var(--fms-border-soft, #f0f1f3); border-radius: 6px; background: var(--fms-background, #fafbfc); }
.ast-row__top { display: flex; align-items: center; gap: 6px; min-width: 0; }
.ast-row__top .ast-field-wrap { flex: 1; min-width: 0; }
.ast-row__bottom { display: flex; align-items: center; gap: 6px; min-width: 0; }
.ast-row__bottom .ast-op { flex: none; width: 96px; }
.ast-row__bottom .ast-value { flex: 1; min-width: 0; }
/* 条件组:左侧粗竖线表达嵌套层级,让「这一组是一个整体」一眼可辨 */
.ast-group { margin: 12px 0; padding: 10px; border: 1px solid #dfe6ef; border-left: 3px solid #b9d3f5; border-radius: 6px; background: #fbfcfe; }
.ast-group__head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; color: var(--fms-text-secondary); font-size: 12px; }
.ast-group__head select { height: 30px; }
.ast-group__head button { margin-left: auto; border: 0; background: transparent; color: var(--fms-danger); font-size: 12px; cursor: pointer; }
.ast-row--nested { margin-left: 8px; }
.ast-row--nested { margin-left: 4px; }
.ast-field-wrap { position: relative; display: flex; min-width: 0; align-items: center; gap: 5px; }
.ast-field-wrap .ast-field { min-width: 0; flex: 1; }
.relation-badge { flex: none; padding: 2px 5px; border-radius: 3px; background: #f3edff; color: #7048b8; font-size: 10px; }
.ast-value { width: 100%; min-width: 0; }
.ast-range-value { display: flex; min-width: 0; align-items: center; gap: 5px; }
.ast-range-value :deep(input) { min-width: 0; flex: 1; }
.ast-range-value :deep(input) { width: 100%; }
.ast-remove { width: 28px; height: 28px; border: 0; border-radius: 4px; background: transparent; color: var(--fms-text-secondary); font-size: 18px; }
.ast-range-value :deep(input) { min-width: 0; flex: 1; width: 100%; }
.ast-remove { flex: none; width: 28px; height: 28px; border: 0; border-radius: 4px; background: transparent; color: var(--fms-text-secondary); font-size: 18px; cursor: pointer; }
.ast-remove:hover { background: #fff1f0; color: var(--fms-danger); }
.ast-hint { display: flex; align-items: center; gap: 6px; margin: 12px 0; color: var(--fms-text-secondary); font-size: 12px; }
.drawer-actions { display: flex; gap: 8px; padding-top: 12px; border-top: 1px dashed var(--fms-border); }
/* 清空条件:从 footer 挪到条件列表底部并靠右,避免和「取消/应用」两个放弃类动作挤在一起误点 */
.ast-actions { display: flex; gap: 8px; padding-top: 12px; border-top: 1px dashed var(--fms-border); }
/* 清空条件:靠右,避免和「添加」类动作挤在一起误点 */
.clear-conditions { margin-left: auto; color: var(--fms-text-secondary) !important; }
.clear-conditions:hover { color: var(--fms-danger) !important; }
.ast-foot-note { flex: 1; color: var(--fms-text-secondary); font-size: 12px; }
.query-drawer__foot { display: flex; flex: none; align-items: center; justify-content: flex-end; gap: 8px; padding: 12px 20px; border-top: 1px solid var(--fms-border-soft, #f0f1f3); }
.ast-foot-note { flex: 1; color: var(--fms-text-secondary); font-size: 12px; }
.settings-note { margin-bottom: 14px; padding: 9px 10px; border: 1px solid #e8edf3; border-radius: 5px; background: #f7f9fc; color: var(--fms-text-secondary); font-size: 12px; }
@@ -1232,5 +1444,5 @@ function onMoreAction({ key }) {
否则用户拖的时候不知道会插到哪 */
.setting-row.is-dragging { opacity: 0.45; background: #f2f7ff; }
.setting-row.is-drop-target { box-shadow: inset 0 2px 0 0 var(--fms-primary, #1677ff); }.setting-label { flex: 1; min-width: 0; color: var(--fms-text); }.setting-row button { border: 0; background: transparent; color: var(--fms-primary); font-size: 12px; }.setting-row button:hover { text-decoration: underline; }.setting-more { color: var(--fms-text-secondary) !important; }.setting-row.is-disabled { color: var(--fms-disabled-text); }.setting-row.is-disabled .setting-label { color: var(--fms-disabled-text); }.setting-disabled { color: var(--fms-disabled-text); font-size: 12px; }
@media (max-width: 760px) { .query-drawer__body { padding-inline: 12px; }.query-drawer__head, .query-drawer__foot { padding-inline: 12px; }.ast-row { grid-template-columns: 1fr 1fr; }.ast-row .ast-value { grid-column: 1 / span 2; }.ast-remove { justify-self: end; margin-top: -38px; } .active-query-bar { flex-wrap: wrap; } }
@media (max-width: 760px) { .query-drawer__body { padding-inline: 12px; }.query-drawer__head, .query-drawer__foot { padding-inline: 12px; }.active-query-bar { flex-wrap: wrap; } }
</style>
+1 -8
View File
@@ -161,7 +161,6 @@ function onFailed() {
id="orgId"
v-model="formModel.orgId"
placeholder="请输入机构码"
:disabled="submitting"
>
<template #prefix><Building2 /></template>
</Input>
@@ -173,7 +172,6 @@ function onFailed() {
v-model="formModel.account"
autocomplete="username"
placeholder="请输入账号"
:disabled="submitting"
>
<template #prefix><User /></template>
</Input>
@@ -186,18 +184,13 @@ function onFailed() {
type="password"
autocomplete="current-password"
placeholder="请输入密码"
:disabled="submitting"
>
<template #prefix><KeyRound /></template>
</Input>
</FormItem>
<FormItem name="captcha">
<SliderCaptcha
ref="captchaRef"
v-model="formModel.captcha"
:disabled="submitting"
/>
<SliderCaptcha ref="captchaRef" v-model="formModel.captcha" />
</FormItem>
<Button type="primary" block :loading="submitting" html-type="submit" class="login-submit">