20260907172356

This commit is contained in:
oneao committed 2026-09-07 17:23:56 +08:00
1 parent 78825797e7
commit 1b596ffa5b
4 files changed
+1123 -261

No files matched your search

+150 -95
View File
@@ -13,6 +13,7 @@ import {
Trash2,
Upload,
LockKeyhole,
SlidersHorizontal,
Settings2,
} from '@lucide/vue'
import { Button, Dropdown, Input, Pagination, RangePicker, Select, Tooltip } from '@/components/ui'
@@ -247,12 +248,14 @@ const queryApplied = ref(false)
const activeQueryMode = ref('quick')
const queryMode = ref('quick')
const advancedLogic = ref('and')
const advancedNodes = reactive([
{ id: 1, field: 'status', operator: 'eq', value: 'pending', label: '状态', source: 'advanced' },
{ id: 2, field: 'invoiceStatus', operator: 'eq', value: 'unissued', label: '发票状态', source: 'advanced' },
])
const advancedNodes = reactive([])
const advancedGroups = reactive([])
const advancedSnapshot = ref(null)
const draggingFieldKey = ref('')
const allAdvancedNodes = computed(() => advancedNodes.concat(advancedGroups.flatMap((group) => group.nodes)))
function hasNodeValue(value) {
return Array.isArray(value) ? value.some((item) => item !== '' && item !== null && item !== undefined) : value !== '' && value !== null && value !== undefined
}
/* 渲染用的字段列表:标签 / 选项文案按当前语言解析(含「全部」空值项) */
const searchFields = computed(() =>
@@ -270,7 +273,7 @@ const searchFields = computed(() =>
const quickSearchFields = computed(() =>
searchFields.value
.filter((field) => field.queryable !== false && queryPrefs[field.key]?.area === 'quick')
.sort((a, b) => queryPrefs[a.key].order - queryPrefs[b.key].order),
.toSorted((a, b) => queryPrefs[a.key].order - queryPrefs[b.key].order),
)
const extraQuickSearchFields = computed(() => quickSearchFields.value.slice(QUICK_VISIBLE_LIMIT))
const extraQuickConditionCount = computed(() => extraQuickSearchFields.value.filter((field) => {
@@ -289,18 +292,33 @@ function getFieldOptions(key) {
}
function getNodeOperators(node) {
const type = fieldByKey.value[node.field]?.type
if (type === 'input') return [{ value: 'contains', label: '包含(like)' }, { value: 'eq', label: '等于(eq)' }, { value: 'not_contains', label: '不包含' }]
if (type === 'numrange') return [{ value: 'gt', label: '大于(gt)' }, { value: 'ge', label: '大于等于(ge)' }, { value: 'lt', label: '小于(lt)' }, { value: 'le', label: '小于等于(le)' }, { value: 'between', label: '介于(between)' }]
if (type === 'daterange') return [{ value: 'between', label: '介于(between)' }, { value: 'ge', label: '不早于(ge)' }, { value: 'le', label: '不晚于(le)' }]
if (node.field === 'invoiceStatus' || node.field === 'paymentStatus') return [{ value: 'eq', label: '等于(eq)' }, { value: 'not_eq', label: '不等于(neq)' }, { value: 'exists', label: '已存在(exists)' }]
return [{ value: 'eq', label: '等于(eq)' }, { value: 'not_eq', label: '不等于(neq)' }, { value: 'in', label: '属于(in)' }]
if (type === 'input') return [{ value: 'contains', label: '包含' }, { value: 'eq', label: '等于' }, { value: 'not_contains', label: '不包含' }]
if (type === 'numrange') return [{ value: 'gt', label: '大于' }, { value: 'ge', label: '大于等于' }, { value: 'lt', label: '小于' }, { value: 'le', label: '小于等于' }, { value: 'between', label: '介于' }]
if (type === 'daterange') return [{ value: 'between', label: '介于' }, { value: 'ge', label: '不早于' }, { value: 'le', label: '不晚于' }]
if (node.field === 'invoiceStatus' || node.field === 'paymentStatus') return [{ value: 'eq', label: '等于' }, { value: 'not_eq', label: '不等于' }, { value: 'exists', label: '已存在' }]
return [{ value: 'eq', label: '等于' }, { value: 'not_eq', label: '不等于' }, { value: 'in', label: '属于' }]
}
function emptyNodeValue(field, operator) {
if (field?.type === 'daterange' || (field?.type === 'numrange' && operator === 'between')) return ['', '']
return ''
}
function createAdvancedNode(fieldKey) {
const field = fieldByKey.value[fieldKey]
const node = { id: Date.now() + Math.random(), field: fieldKey, operator: 'eq', value: '', label: field?.label || fieldKey, source: field?.relation ? 'relation' : 'advanced' }
node.operator = getNodeOperators(node)[0]?.value || 'eq'
node.value = emptyNodeValue(field, node.operator)
return node
}
function onNodeFieldChange(node) {
node.operator = getNodeOperators(node)[0]?.value || 'eq'
node.value = ''
node.value = emptyNodeValue(fieldByKey.value[node.field], node.operator)
node.label = getFieldLabel(node.field)
}
function onNodeOperatorChange(node) {
node.value = emptyNodeValue(fieldByKey.value[node.field], node.operator)
}
function getNodeValueLabel(node) {
if (Array.isArray(node.value)) return node.value.filter(Boolean).join(' - ')
const option = getFieldOptions(node.field).find((item) => item.value === node.value)
return option?.label || node.value
}
@@ -317,17 +335,15 @@ const quickQueryAst = computed(() => {
})
const advancedQueryAst = computed(() => {
const children = []
const advanced = advancedNodes.filter((node) => node.value !== '').map((node) => ({ type: 'condition', field: fieldByKey.value[node.field]?.path || node.field, operator: node.operator, value: node.value }))
const advanced = advancedNodes.filter((node) => hasNodeValue(node.value)).map((node) => ({ type: 'condition', field: fieldByKey.value[node.field]?.path || node.field, operator: node.operator, value: node.value }))
if (advanced.length) children.push({ type: 'group', logic: advancedLogic.value, children: advanced })
advancedGroups.forEach((group) => {
const groupChildren = group.nodes.filter((node) => node.value !== '').map((node) => ({ type: 'condition', field: fieldByKey.value[node.field]?.path || node.field, operator: node.operator, value: node.value }))
const groupChildren = group.nodes.filter((node) => hasNodeValue(node.value)).map((node) => ({ type: 'condition', field: fieldByKey.value[node.field]?.path || node.field, operator: node.operator, value: node.value }))
if (groupChildren.length) children.push({ type: 'group', logic: group.logic, children: groupChildren })
})
return { type: 'group', logic: 'and', children }
})
const queryAst = computed(() => activeQueryMode.value === 'advanced' ? advancedQueryAst.value : quickQueryAst.value)
const queryAstPreview = computed(() => JSON.stringify(queryAst.value, null, 2))
const advancedConditionCount = computed(() => allAdvancedNodes.value.filter((node) => node.value !== '').length)
const advancedConditionCount = computed(() => allAdvancedNodes.value.filter((node) => hasNodeValue(node.value)).length)
const querySummary = computed(() => {
const items = []
if (activeQueryMode.value === 'quick') {
@@ -336,7 +352,7 @@ const querySummary = computed(() => {
if (value) items.push({ id: `quick:${field.key}`, kind: 'quick', field: field.key, text: `${field.label} ${field.type === 'input' ? '包含' : '='} ${value}` })
})
} else {
allAdvancedNodes.value.filter((node) => node.value).forEach((node) => items.push({ id: `advanced:${node.id}`, kind: 'advanced', nodeId: node.id, text: `${getFieldLabel(node.field)} ${node.operator === 'eq' ? '=' : node.operator} ${getNodeValueLabel(node)}` }))
allAdvancedNodes.value.filter((node) => Array.isArray(node.value) ? node.value.some(Boolean) : node.value).forEach((node) => items.push({ id: `advanced:${node.id}`, kind: 'advanced', nodeId: node.id, text: `${getFieldLabel(node.field)} ${getNodeOperators(node).find((operator) => operator.value === node.operator)?.label || node.operator} ${getNodeValueLabel(node)}` }))
}
return items
})
@@ -355,9 +371,26 @@ 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 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')
@@ -386,12 +419,36 @@ function removeAdvancedNode(id) {
}
}
function addAdvancedNode() {
const field = advancedSearchFields.value[0]
advancedNodes.push({ id: Date.now(), field: field?.key || 'creator', operator: getNodeOperators({ field: field?.key || 'creator' })[0]?.value || 'eq', value: '', label: field?.label || '创建人', source: field?.relation ? 'relation' : 'advanced' })
advancedNodes.push(createAdvancedNode(advancedSearchFields.value[0]?.key || 'creator'))
}
function addAdvancedGroup() {
const field = advancedSearchFields.value[0]
advancedGroups.push({ id: Date.now(), logic: 'or', nodes: [{ id: Date.now() + 1, field: field?.key || 'creator', operator: 'eq', value: '', label: field?.label || '创建人', source: 'advanced' }] })
advancedGroups.push({ id: Date.now() + Math.random(), logic: 'or', nodes: [createAdvancedNode(advancedSearchFields.value[0]?.key || 'creator')] })
}
function resetAdvancedQuery() {
advancedNodes.splice(0, advancedNodes.length)
advancedGroups.splice(0, advancedGroups.length)
advancedLogic.value = 'and'
if (activeQueryMode.value === 'advanced') queryApplied.value = false
}
function resetQuickQuery() {
Object.assign(searchForm, makeEmptyForm())
if (activeQueryMode.value === 'quick') queryApplied.value = false
}
function startFieldDrag(field) {
draggingFieldKey.value = field.key
}
function dropField(field, area) {
const sourceKey = draggingFieldKey.value
draggingFieldKey.value = ''
if (!sourceKey || sourceKey === field.key || queryPrefs[sourceKey]?.area !== area) return
const rows = searchFields.value.filter((item) => item.queryable !== false && queryPrefs[item.key]?.area === area)
.toSorted((a, b) => queryPrefs[a.key].order - queryPrefs[b.key].order)
const from = rows.findIndex((item) => item.key === sourceKey)
const to = rows.findIndex((item) => item.key === field.key)
if (from < 0 || to < 0) return
const [moved] = rows.splice(from, 1)
rows.splice(to, 0, moved)
rows.forEach((item, index) => { queryPrefs[item.key].order = (index + 1) * 10 })
}
function onSearch() {
activeQueryMode.value = 'quick'
@@ -402,12 +459,17 @@ function applyAdvancedQuery() {
activeQueryMode.value = 'advanced'
queryMode.value = 'advanced'
queryApplied.value = true
advancedSnapshot.value = null
drawerOpen.value = false
}
function onReset() {
Object.assign(searchForm, makeEmptyForm())
advancedNodes.splice(0, advancedNodes.length)
advancedGroups.splice(0, advancedGroups.length)
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
@@ -545,20 +607,8 @@ function onMoreAction({ key }) {
<div class="dashboard-page">
<!-- 页面常用查询:字段来自模块默认配置,并可由用户个性化调整 -->
<section class="table-card search-card">
<div class="query-mode-bar">
<div class="query-mode-switch" role="tablist" aria-label="查询模式">
<span class="query-mode-label">查询模式</span>
<button type="button" :class="{ active: queryMode === 'quick' }" role="tab" :aria-selected="queryMode === 'quick'" @click="selectQueryMode('quick')">普通查询</button>
<button type="button" :class="{ active: queryMode === 'advanced' }" role="tab" :aria-selected="queryMode === 'advanced'" @click="selectQueryMode('advanced')">高级查询</button>
</div>
<div class="query-mode-status">
当前生效:<strong>{{ activeQueryMode === 'quick' ? '普通查询' : '高级查询' }}</strong>
<span v-if="queryMode !== activeQueryMode" class="query-mode-draft">正在编辑{{ queryMode === 'quick' ? '普通' : '高级' }}查询</span>
</div>
</div>
<div class="list-search">
<div v-if="queryMode === 'advanced'" class="advanced-mode-note">高级查询条件在右侧 Drawer 中配置。普通查询条件仍会保留,但当前不会参与结果。</div>
<template v-else>
<template v-if="activeQueryMode === 'quick'">
<div v-for="field in (quickSearchExpanded ? quickSearchFields : quickSearchFields.slice(0, QUICK_VISIBLE_LIMIT))" :key="field.key" class="search-field">
<label class="search-field__label">{{ field.label }}</label>
<Input
@@ -587,25 +637,35 @@ function onMoreAction({ key }) {
</div>
</template>
<div class="list-search__actions">
<Button v-if="queryMode === 'quick'" type="primary" @click="onSearch">{{ t('demo.action.search') }}</Button>
<Button v-else type="primary" @click="openQueryDrawer('conditions')">编辑高级查询</Button>
<Button type="outline" @click="onReset">{{ t('demo.action.reset') }}</Button>
<Button v-if="queryMode === 'quick' && extraQuickSearchFields.length" type="ghost" class="quick-more-button" @click="quickSearchExpanded = !quickSearchExpanded">
<template v-if="activeQueryMode === 'quick'">
<Button type="primary" @click="onSearch">{{ t('demo.action.search') }}</Button>
<Button type="outline" @click="resetQuickQuery">{{ t('demo.action.reset') }}</Button>
<Button v-if="extraQuickSearchFields.length" type="ghost" class="quick-more-button" @click="quickSearchExpanded = !quickSearchExpanded">
{{ quickSearchExpanded ? '收起常用条件' : `更多常用条件(${extraQuickSearchFields.length})` }}
<span v-if="!quickSearchExpanded && extraQuickConditionCount" class="query-count">{{ extraQuickConditionCount }}</span>
<ChevronDown :size="14" :class="{ 'is-up': quickSearchExpanded }" />
</Button>
<Button type="ghost" class="search-toggle" @click="openQueryDrawer('conditions')">
{{ queryMode === 'advanced' ? '编辑高级查询' : '高级查询' }} <span v-if="advancedConditionCount" class="query-count">{{ advancedConditionCount }}</span>
<ChevronDown :size="14" />
<Button type="ghost" class="advanced-query-button" @click="selectQueryMode('advanced')">
<SlidersHorizontal :size="14" />
高级查询
<span v-if="advancedConditionCount" class="query-count">{{ advancedConditionCount }}</span>
</Button>
</template>
<template v-else>
<Button type="ghost" class="advanced-query-button" @click="openQueryDrawer('conditions')">
<SlidersHorizontal :size="14" /> 编辑高级查询
<span v-if="advancedConditionCount" class="query-count">{{ advancedConditionCount }}</span>
</Button>
<Button type="ghost" class="return-quick-button" @click="returnToQuickQuery">改用普通查询</Button>
<Button type="outline" @click="resetAllQueries">清除查询</Button>
</template>
<Button type="ghost" class="settings-button" title="设置常用查询" @click="openQueryDrawer('settings')"><Settings2 :size="15" /> 设置</Button>
</div>
</div>
<div v-if="queryApplied && querySummary.length" class="active-query-bar">
<span class="active-query-label">{{ activeQueryMode === 'quick' ? '普通查询生效' : '高级查询生效' }}</span>
<span v-for="item in querySummary" :key="item.id" class="active-query-item"><span>{{ item.text }}</span><button type="button" aria-label="移除条件" @click="removeSummaryItem(item)">×</button></span>
<button class="clear-query" @click="onReset">清除全部</button>
<button class="clear-query" @click="resetAllQueries">清除全部</button>
</div>
</section>
@@ -681,21 +741,20 @@ function onMoreAction({ key }) {
</section>
</div>
<!-- 高级查询 Drawer:条件与查询设置使用同一入口,配置不展开时不占列表空间 -->
<div v-if="drawerOpen" class="query-drawer-mask" @click.self="drawerOpen = false">
<!-- 查询 Drawer:高级条件与查询设置使用独立入口,共用 Drawer 外壳 -->
<div v-if="drawerOpen" class="query-drawer-mask" @click.self="closeQueryDrawer">
<aside class="query-drawer">
<div class="query-drawer__head">
<div><strong>查询</strong><span class="query-drawer__sub">普通查询与高级查询共用同一 Query AST</span></div>
<button class="query-drawer__close" aria-label="关闭" @click="drawerOpen = false">×</button>
</div>
<div class="query-drawer__tabs">
<button :class="{ active: drawerTab === 'conditions' }" @click="drawerTab = 'conditions'">查询条件</button>
<button :class="{ active: drawerTab === 'settings' }" @click="drawerTab = 'settings'">查询设置</button>
<div>
<strong>{{ drawerTab === 'conditions' ? '高级查询' : '查询设置' }}</strong>
<span class="query-drawer__sub">{{ drawerTab === 'conditions' ? '设置本次查询需要满足的条件' : '调整常用条件的显示和顺序' }}</span>
</div>
<button class="query-drawer__close" aria-label="关闭" @click="closeQueryDrawer">×</button>
</div>
<div v-if="drawerTab === 'conditions'" class="query-drawer__body">
<div class="mode-notice"><span class="mode-notice__dot"></span>当前为高级查询编辑模式。点击“应用高级查询”后,只使用本 Drawer 条件;普通查询条件会保留但暂不生效。</div>
<div class="drawer-section-title">当前条件</div>
<div class="ast-root"><span>满足</span><select v-model="advancedLogic"><option value="and">全部条件(AND)</option><option value="or">任一条件(OR)</option></select></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">暂无条件,请从下方添加</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)">
@@ -703,11 +762,15 @@ function onMoreAction({ key }) {
</select>
<span v-if="fieldByKey[node.field]?.relation" class="relation-badge">关联</span>
</div>
<select v-model="node.operator" class="ast-op">
<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="请输入值" />
<div v-else-if="fieldByKey[node.field]?.type === 'numrange'" class="ast-range-value"><Input v-model="node.value" placeholder="数值" /></div>
<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>
@@ -715,29 +778,41 @@ function onMoreAction({ key }) {
<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">全部满足(AND)</option><option value="or">任一满足(OR)</option></select><button type="button" @click="advancedGroups.splice(advancedGroups.indexOf(group), 1)">删除组</button></div>
<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"><option v-for="operator in getNodeOperators(node)" :key="operator.value" :value="operator.value">{{ operator.label }}</option></select>
<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({ id: Date.now(), field: advancedSearchFields[0]?.key || 'creator', operator: 'eq', value: '', label: '创建人', source: 'advanced' })">+ 条件</Button>
<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></div>
<details class="ast-preview"><summary>查看统一 Query AST</summary><pre>{{ queryAstPreview }}</pre></details>
</div>
<div v-else class="query-drawer__body settings-body">
<div class="drawer-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')" :key="field.key" class="setting-row"><span class="drag-handle">⋮⋮</span><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')" :key="field.key" class="setting-row"><span class="drag-handle">⋮⋮</span><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>
<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" draggable="true" @dragstart="startFieldDrag(field)" @dragover.prevent @drop="dropField(field, 'quick')"><span class="drag-handle">⋮⋮</span><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" draggable="true" @dragstart="startFieldDrag(field)" @dragover.prevent @drop="dropField(field, 'advanced')"><span class="drag-handle">⋮⋮</span><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>
<div class="setting-group"><div class="setting-group__head"><strong>已隐藏字段</strong><span>可恢复到高级查询</span></div><div v-for="field in searchFields.filter((item) => item.queryable !== false && queryPrefs[item.key].area === 'hidden')" :key="field.key" class="setting-row"><span class="drag-handle">⋮⋮</span><span class="setting-label">{{ field.label }}</span><button class="setting-first" @click="setFieldArea(field,'advanced')">恢复到高级</button></div><div v-if="!searchFields.some((item) => item.queryable !== false && queryPrefs[item.key].area === 'hidden')" class="setting-empty">暂无隐藏字段</div></div>
<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">高级查询应用后只使用高级条件,当前共 {{ advancedQueryAst.children.length }} 个条件</span><Button type="ghost" @click="drawerTab === 'settings' ? resetQueryPrefs() : onReset()">{{ drawerTab === 'settings' ? '恢复默认' : '重置条件' }}</Button><Button type="primary" @click="drawerTab === 'settings' ? (drawerOpen = false) : applyAdvancedQuery()">{{ drawerTab === 'settings' ? '保存设置' : '应用高级查询' }}</Button></div>
<div class="query-drawer__foot">
<span v-if="drawerTab === 'conditions'" class="ast-foot-note">已配置 {{ advancedConditionCount }} 个条件</span>
<template v-if="drawerTab === 'conditions'">
<Button type="ghost" @click="resetAdvancedQuery">清空条件</Button>
<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>
</div>
</aside>
</div>
</template>
@@ -784,16 +859,11 @@ function onMoreAction({ key }) {
padding: 12px 16px 8px;
container-type: inline-size;
}
.query-mode-bar { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 12px; }
.query-mode-switch { display: flex; align-items: center; gap: 2px; }
.query-mode-label { margin-right: 8px; color: var(--fms-text-secondary); font-size: 12px; }
.query-mode-switch button { min-height: 30px; padding: 0 12px; border: 1px solid var(--fms-border); background: var(--fms-card); color: var(--fms-text-secondary); font-size: 12px; cursor: pointer; }
.query-mode-switch button:first-of-type { border-radius: 4px 0 0 4px; }
.query-mode-switch button:last-of-type { border-left: 0; border-radius: 0 4px 4px 0; }
.query-mode-switch button.active { border-color: var(--fms-primary); background: var(--fms-primary); color: #fff; font-weight: 600; }
.query-mode-status { color: var(--fms-text-secondary); font-size: 12px; }
.query-mode-status strong { color: var(--fms-text); font-weight: 600; }
.query-mode-draft { margin-left: 8px; color: #8a6414; }
.advanced-query-button { gap: 5px; color: var(--fms-text-secondary); }
.advanced-query-button:hover { color: var(--fms-primary); }
.advanced-query-button .query-count { margin-left: 2px; }
.return-quick-button { color: var(--fms-text-secondary); }
.return-quick-button:hover { color: var(--fms-primary); }
/* ---- ① 搜索区:固定格宽 + 顶部标签 ---- */
.list-search {
@@ -880,16 +950,6 @@ function onMoreAction({ key }) {
.quick-more-button { gap: 5px; }
.quick-more-button svg { transition: transform 160ms ease; }
.quick-more-button svg.is-up { transform: rotate(180deg); }
.advanced-mode-note { grid-column: 1 / -1; padding: 7px 9px; border-radius: 4px; background: #fff8e6; color: #8a6414; font-size: 12px; }
.search-toggle__icon {
transition: transform 160ms ease;
}
.search-toggle__icon.is-up {
transform: rotate(180deg);
}
/* ---- ② 工具条:左「改数据」,中 状态,右「用数据」 ---- */
.list-toolbar {
display: flex;
@@ -1055,10 +1115,6 @@ function onMoreAction({ key }) {
.query-drawer__sub { display: block; margin-top: 4px; color: var(--fms-text-secondary); font-size: 12px; }
.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__tabs { display: flex; flex: none; gap: 22px; padding: 0 20px; border-bottom: 1px solid var(--fms-border-soft, #f0f1f3); }
.query-drawer__tabs button { position: relative; height: 42px; padding: 0; border: 0; background: transparent; color: var(--fms-text-secondary); font-size: 13px; }
.query-drawer__tabs button.active { color: var(--fms-primary); font-weight: 600; }
.query-drawer__tabs button.active::after { position: absolute; right: 0; bottom: -1px; left: 0; height: 2px; background: var(--fms-primary); content: ''; }
.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); }
@@ -1066,6 +1122,7 @@ function onMoreAction({ key }) {
.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 { margin: 8px 0 12px; padding: 18px 12px; border: 1px dashed var(--fms-border); border-radius: 5px; color: var(--fms-text-secondary); font-size: 12px; text-align: center; }
.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-group { margin: 12px 0; padding: 10px; border: 1px solid #dfe6ef; 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; }
@@ -1076,15 +1133,13 @@ function onMoreAction({ key }) {
.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 { 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-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); }
.ast-preview { margin-top: 16px; border-top: 1px solid var(--fms-border-soft); color: var(--fms-text-secondary); font-size: 12px; }
.ast-preview summary { padding-top: 12px; cursor: pointer; }
.ast-preview pre { max-height: 210px; margin: 10px 0 0; overflow: auto; padding: 10px; border-radius: 4px; background: #f7f9fc; color: #526176; font: 11px/1.55 ui-monospace, SFMono-Regular, Consolas, monospace; white-space: pre-wrap; }
.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; }
@@ -1095,5 +1150,5 @@ function onMoreAction({ key }) {
.setting-row { display: flex; align-items: center; gap: 8px; min-height: 42px; padding: 0 12px; border-bottom: 1px solid var(--fms-border-soft); }.setting-row:last-child { border-bottom: 0; }
.setting-empty { padding: 12px; color: var(--fms-text-secondary); font-size: 12px; }
.drag-handle { color: #a3acb8; letter-spacing: -2px; }.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__tabs, .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; }.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; } }
</style>