init5
This commit is contained in:
743
app/components/UniversalQueryBuilder.vue
Normal file
743
app/components/UniversalQueryBuilder.vue
Normal file
@@ -0,0 +1,743 @@
|
||||
<template>
|
||||
<div class="universal-query-builder bg-white rounded-lg shadow-lg p-6">
|
||||
<!-- 表选择 -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2"> 选择表 </label>
|
||||
<select
|
||||
v-model="selectedTable"
|
||||
@change="onTableChange"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">请选择表</option>
|
||||
<option v-for="table in availableTables" :key="table.key" :value="table.key">
|
||||
{{ table.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 查询条件构造器 -->
|
||||
<div v-if="selectedTable && tableInfo" class="mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-900">查询条件</h3>
|
||||
<button
|
||||
@click="addCondition"
|
||||
class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
添加条件
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 条件列表 -->
|
||||
<div v-if="queryOptions.where && queryOptions.where.length > 0" class="space-y-4">
|
||||
<div
|
||||
v-for="(condition, index) in queryOptions.where"
|
||||
:key="index"
|
||||
class="flex items-center space-x-4 p-4 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<!-- 逻辑操作符 -->
|
||||
<select
|
||||
v-if="index > 0"
|
||||
v-model="condition.logicalOperator"
|
||||
class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="AND">AND</option>
|
||||
<option value="OR">OR</option>
|
||||
</select>
|
||||
|
||||
<!-- 字段选择 -->
|
||||
<select
|
||||
v-model="condition.field"
|
||||
@change="onFieldChange(condition)"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">选择字段</option>
|
||||
<option v-for="field in tableInfo.fields" :key="field.name" :value="field.name">
|
||||
{{ field.name }} ({{ field.type }})
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- 操作符选择 -->
|
||||
<select
|
||||
v-model="condition.operator"
|
||||
@change="onOperatorChange(condition)"
|
||||
class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">选择操作符</option>
|
||||
<option
|
||||
v-for="operator in getAvailableOperators(condition.field)"
|
||||
:key="operator.value"
|
||||
:value="operator.value"
|
||||
>
|
||||
{{ operator.label }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- 值输入 -->
|
||||
<div class="flex-1">
|
||||
<!-- 单值输入 -->
|
||||
<input
|
||||
v-if="
|
||||
!isMultiValueOperator(condition.operator) && !isNullOperator(condition.operator)
|
||||
"
|
||||
v-model="condition.value"
|
||||
:type="getInputType(condition.field, condition.operator)"
|
||||
:placeholder="getInputPlaceholder(condition.field, condition.operator)"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
|
||||
<!-- 多值输入 -->
|
||||
<div v-else-if="isMultiValueOperator(condition.operator)" class="space-y-2">
|
||||
<input
|
||||
v-if="condition.operator === 'between' || condition.operator === 'date_between'"
|
||||
v-model="condition.value[0]"
|
||||
:type="getInputType(condition.field, condition.operator)"
|
||||
placeholder="起始值"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<input
|
||||
v-if="condition.operator === 'between' || condition.operator === 'date_between'"
|
||||
v-model="condition.value[1]"
|
||||
:type="getInputType(condition.field, condition.operator)"
|
||||
placeholder="结束值"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<textarea
|
||||
v-if="condition.operator === 'in' || condition.operator === 'not_in'"
|
||||
v-model="condition.valueText"
|
||||
@input="onMultiValueInput(condition)"
|
||||
placeholder="输入多个值,用逗号分隔"
|
||||
rows="2"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 空值操作符不需要输入 -->
|
||||
<span v-else-if="isNullOperator(condition.operator)" class="text-gray-500 italic">
|
||||
无需输入值
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 删除按钮 -->
|
||||
<button
|
||||
@click="removeCondition(index)"
|
||||
class="px-3 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-gray-500 text-center py-8">
|
||||
暂无查询条件,点击"添加条件"开始构建查询
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div v-if="selectedTable && tableInfo" class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2"> 全文搜索 </label>
|
||||
<input
|
||||
v-model="queryOptions.search"
|
||||
type="text"
|
||||
placeholder="输入搜索关键词..."
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<p class="text-sm text-gray-500 mt-1">搜索字段: {{ tableInfo.searchFields.join(', ') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 排序设置 -->
|
||||
<div v-if="selectedTable && tableInfo" class="mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-900">排序设置</h3>
|
||||
<button
|
||||
@click="addSortCondition"
|
||||
class="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
>
|
||||
添加排序
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="queryOptions.orderBy && queryOptions.orderBy.length > 0" class="space-y-2">
|
||||
<div
|
||||
v-for="(sort, index) in queryOptions.orderBy"
|
||||
:key="index"
|
||||
class="flex items-center space-x-4 p-3 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<select
|
||||
v-model="sort.field"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">选择字段</option>
|
||||
<option v-for="field in tableInfo.sortableFields" :key="field" :value="field">
|
||||
{{ field }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
v-model="sort.direction"
|
||||
class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="ASC">升序</option>
|
||||
<option value="DESC">降序</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
@click="removeSortCondition(index)"
|
||||
class="px-3 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页设置 -->
|
||||
<div v-if="selectedTable" class="mb-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">分页设置</h3>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">页码</label>
|
||||
<input
|
||||
v-model.number="queryOptions.pagination.page"
|
||||
type="number"
|
||||
min="1"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">每页数量</label>
|
||||
<select
|
||||
v-model.number="queryOptions.pagination.limit"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option :value="10">10</option>
|
||||
<option :value="20">20</option>
|
||||
<option :value="50">50</option>
|
||||
<option :value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div v-if="selectedTable" class="flex space-x-4">
|
||||
<button
|
||||
@click="executeQuery"
|
||||
:disabled="loading"
|
||||
class="flex-1 px-6 py-3 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
{{ loading ? '查询中...' : '执行查询' }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="executeAggregateQuery"
|
||||
:disabled="loading"
|
||||
class="px-6 py-3 bg-purple-500 text-white rounded-md hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-500 disabled:opacity-50"
|
||||
>
|
||||
聚合查询
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="clearQuery"
|
||||
class="px-6 py-3 bg-gray-500 text-white rounded-md hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500"
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 查询结果 -->
|
||||
<div v-if="queryResult" class="mt-8">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">查询结果</h3>
|
||||
|
||||
<!-- 结果统计 -->
|
||||
<div class="mb-4 p-4 bg-blue-50 rounded-lg">
|
||||
<p class="text-sm text-blue-800">
|
||||
<span v-if="queryResult.pagination">
|
||||
第 {{ queryResult.pagination.page }} 页,共 {{ queryResult.pagination.totalPages }} 页,
|
||||
总计 {{ queryResult.total }} 条记录
|
||||
</span>
|
||||
<span v-else> 总计 {{ queryResult.total }} 条记录 </span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
v-for="field in getDisplayFields()"
|
||||
:key="field"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
{{ field }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr v-for="(item, index) in queryResult.data" :key="index">
|
||||
<td
|
||||
v-for="field in getDisplayFields()"
|
||||
:key="field"
|
||||
class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"
|
||||
>
|
||||
{{ formatValue(item[field]) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 聚合结果 -->
|
||||
<div v-if="aggregateResult" class="mt-8">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">聚合结果</h3>
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
<pre class="text-sm">{{ JSON.stringify(aggregateResult, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AdvancedQueryOptions, QueryCondition, SortCondition } from '@nuxt4crud/shared/types'
|
||||
|
||||
/**
|
||||
* 组件属性
|
||||
* Component props
|
||||
*/
|
||||
interface Props {
|
||||
modelValue?: AdvancedQueryOptions
|
||||
table?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件事件
|
||||
* Component events
|
||||
*/
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: AdvancedQueryOptions): void
|
||||
(e: 'query', result: any): void
|
||||
(e: 'aggregate', result: any): void
|
||||
(e: 'search', options: AdvancedQueryOptions): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => ({
|
||||
where: [],
|
||||
orderBy: [],
|
||||
pagination: { page: 1, limit: 20 },
|
||||
}),
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 响应式数据
|
||||
const selectedTable = ref(props.table || '')
|
||||
const availableTables = ref<any[]>([])
|
||||
const tableInfo = ref<any>(null)
|
||||
const loading = ref(false)
|
||||
const queryResult = ref<any>(null)
|
||||
const aggregateResult = ref<any>(null)
|
||||
|
||||
// 监听table属性变化
|
||||
watch(() => props.table, (newTable) => {
|
||||
if (newTable) {
|
||||
selectedTable.value = newTable
|
||||
}
|
||||
})
|
||||
|
||||
// 查询选项
|
||||
const queryOptions = ref<AdvancedQueryOptions>({
|
||||
where: [],
|
||||
orderBy: [],
|
||||
search: '',
|
||||
pagination: { page: 1, limit: 20 },
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听查询选项变化
|
||||
* Watch query options changes
|
||||
*/
|
||||
watch(
|
||||
queryOptions,
|
||||
newValue => {
|
||||
emit('update:modelValue', newValue)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
/**
|
||||
* 监听外部传入的值
|
||||
* Watch external value changes
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
newValue => {
|
||||
if (newValue && JSON.stringify(newValue) !== JSON.stringify(queryOptions.value)) {
|
||||
queryOptions.value = { ...newValue }
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取可用表列表
|
||||
* Get available tables
|
||||
*/
|
||||
async function loadAvailableTables() {
|
||||
try {
|
||||
const response = await $fetch('/api/table-info')
|
||||
if (response.success) {
|
||||
availableTables.value = response.data.tables
|
||||
} else {
|
||||
// 提供默认的表列表,确保能够正确加载用户表和文章表
|
||||
availableTables.value = [
|
||||
{
|
||||
key: 'User',
|
||||
name: '用户表'
|
||||
},
|
||||
{
|
||||
key: 'Post',
|
||||
name: '文章表'
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load tables:', error)
|
||||
// 错误时提供默认的表列表
|
||||
availableTables.value = [
|
||||
{
|
||||
key: 'User',
|
||||
name: '用户表'
|
||||
},
|
||||
{
|
||||
key: 'Post',
|
||||
name: '文章表'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 表选择变化处理
|
||||
* Handle table selection change
|
||||
*/
|
||||
async function onTableChange() {
|
||||
if (!selectedTable.value) {
|
||||
tableInfo.value = null
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await $fetch(`/api/table-info?table=${selectedTable.value}`)
|
||||
if (response.success) {
|
||||
tableInfo.value = response.data.table
|
||||
// 清空之前的查询条件
|
||||
clearQuery()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load table info:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加查询条件
|
||||
* Add query condition
|
||||
*/
|
||||
function addCondition() {
|
||||
const condition: QueryCondition & { logicalOperator?: string; valueText?: string } = {
|
||||
field: '',
|
||||
operator: 'equals' as any,
|
||||
value: '',
|
||||
logicalOperator: queryOptions.value.where!.length > 0 ? 'AND' : undefined,
|
||||
valueText: '',
|
||||
}
|
||||
|
||||
if (!queryOptions.value.where) {
|
||||
queryOptions.value.where = []
|
||||
}
|
||||
|
||||
queryOptions.value.where.push(condition as any)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除查询条件
|
||||
* Remove query condition
|
||||
*/
|
||||
function removeCondition(index: number) {
|
||||
if (queryOptions.value.where) {
|
||||
queryOptions.value.where.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段变化处理
|
||||
* Handle field change
|
||||
*/
|
||||
function onFieldChange(condition: any) {
|
||||
// 重置操作符和值
|
||||
condition.operator = 'equals'
|
||||
condition.value = ''
|
||||
condition.valueText = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作符变化处理
|
||||
* Handle operator change
|
||||
*/
|
||||
function onOperatorChange(condition: any) {
|
||||
// 根据操作符类型初始化值
|
||||
if (isMultiValueOperator(condition.operator)) {
|
||||
if (condition.operator === 'between' || condition.operator === 'date_between') {
|
||||
condition.value = ['', '']
|
||||
} else {
|
||||
condition.value = []
|
||||
condition.valueText = ''
|
||||
}
|
||||
} else if (isNullOperator(condition.operator)) {
|
||||
condition.value = null
|
||||
} else {
|
||||
condition.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多值输入处理
|
||||
* Handle multi-value input
|
||||
*/
|
||||
function onMultiValueInput(condition: any) {
|
||||
if (condition.valueText) {
|
||||
condition.value = condition.valueText
|
||||
.split(',')
|
||||
.map((v: string) => v.trim())
|
||||
.filter(Boolean)
|
||||
} else {
|
||||
condition.value = []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用操作符
|
||||
* Get available operators for field
|
||||
*/
|
||||
function getAvailableOperators(fieldName: string) {
|
||||
if (!tableInfo.value || !fieldName) return []
|
||||
|
||||
const field = tableInfo.value.fields.find((f: any) => f.name === fieldName)
|
||||
if (!field) return []
|
||||
|
||||
return tableInfo.value.operators.filter((op: any) => op.types.includes(field.type))
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为多值操作符
|
||||
* Check if operator requires multiple values
|
||||
*/
|
||||
function isMultiValueOperator(operator: string): boolean {
|
||||
return ['in', 'not_in', 'between', 'date_between'].includes(operator)
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为空值操作符
|
||||
* Check if operator is null operator
|
||||
*/
|
||||
function isNullOperator(operator: string): boolean {
|
||||
return ['is_null', 'is_not_null'].includes(operator)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取输入框类型
|
||||
* Get input type for field and operator
|
||||
*/
|
||||
function getInputType(fieldName: string, operator: string): string {
|
||||
if (!tableInfo.value || !fieldName) return 'text'
|
||||
|
||||
const field = tableInfo.value.fields.find((f: any) => f.name === fieldName)
|
||||
if (!field) return 'text'
|
||||
|
||||
if (operator.startsWith('date_') || field.type === 'datetime') {
|
||||
return 'datetime-local'
|
||||
} else if (field.type === 'number') {
|
||||
return 'number'
|
||||
}
|
||||
|
||||
return 'text'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取输入框占位符
|
||||
* Get input placeholder
|
||||
*/
|
||||
function getInputPlaceholder(fieldName: string, operator: string): string {
|
||||
if (!tableInfo.value || !fieldName) return ''
|
||||
|
||||
const field = tableInfo.value.fields.find((f: any) => f.name === fieldName)
|
||||
if (!field) return ''
|
||||
|
||||
return `请输入${field.name}的值`
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加排序条件
|
||||
* Add sort condition
|
||||
*/
|
||||
function addSortCondition() {
|
||||
const sortCondition: SortCondition = {
|
||||
field: '',
|
||||
direction: 'ASC',
|
||||
}
|
||||
|
||||
if (!queryOptions.value.orderBy) {
|
||||
queryOptions.value.orderBy = []
|
||||
}
|
||||
|
||||
queryOptions.value.orderBy.push(sortCondition)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除排序条件
|
||||
* Remove sort condition
|
||||
*/
|
||||
function removeSortCondition(index: number) {
|
||||
if (queryOptions.value.orderBy) {
|
||||
queryOptions.value.orderBy.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行查询
|
||||
* Execute query
|
||||
*/
|
||||
async function executeQuery() {
|
||||
if (!selectedTable.value) return
|
||||
|
||||
loading.value = true
|
||||
queryResult.value = null
|
||||
aggregateResult.value = null
|
||||
|
||||
// 发出搜索事件,传递查询选项
|
||||
emit('search', queryOptions.value)
|
||||
|
||||
try {
|
||||
const response = await $fetch('/api/universal-query', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
table: selectedTable.value,
|
||||
options: queryOptions.value,
|
||||
mode: 'query',
|
||||
},
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
queryResult.value = response.data
|
||||
emit('query', response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Query failed:', error)
|
||||
alert('查询失败: ' + (error as any).data?.message || '未知错误')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行聚合查询
|
||||
* Execute aggregate query
|
||||
*/
|
||||
async function executeAggregateQuery() {
|
||||
if (!selectedTable.value) return
|
||||
|
||||
loading.value = true
|
||||
queryResult.value = null
|
||||
aggregateResult.value = null
|
||||
|
||||
try {
|
||||
// 构建聚合选项
|
||||
const aggregateOptions = {
|
||||
...queryOptions.value,
|
||||
aggregate: {
|
||||
count: true,
|
||||
sum: tableInfo.value.fields.filter((f: any) => f.type === 'number').map((f: any) => f.name),
|
||||
avg: tableInfo.value.fields.filter((f: any) => f.type === 'number').map((f: any) => f.name),
|
||||
min: tableInfo.value.fields.filter((f: any) => f.type === 'number').map((f: any) => f.name),
|
||||
max: tableInfo.value.fields.filter((f: any) => f.type === 'number').map((f: any) => f.name),
|
||||
},
|
||||
}
|
||||
|
||||
const response = await $fetch('/api/universal-query', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
table: selectedTable.value,
|
||||
options: aggregateOptions,
|
||||
mode: 'aggregate',
|
||||
},
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
aggregateResult.value = response.data.result
|
||||
emit('aggregate', response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Aggregate query failed:', error)
|
||||
alert('聚合查询失败: ' + (error as any).data?.message || '未知错误')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空查询
|
||||
* Clear query
|
||||
*/
|
||||
function clearQuery() {
|
||||
queryOptions.value = {
|
||||
where: [],
|
||||
orderBy: tableInfo.value?.defaultOrderBy || [],
|
||||
search: '',
|
||||
pagination: { page: 1, limit: 20 },
|
||||
}
|
||||
queryResult.value = null
|
||||
aggregateResult.value = null
|
||||
// 发出重置事件
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取显示字段
|
||||
* Get display fields
|
||||
*/
|
||||
function getDisplayFields(): string[] {
|
||||
if (!queryResult.value?.data || queryResult.value.data.length === 0) return []
|
||||
return Object.keys(queryResult.value.data[0])
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化值显示
|
||||
* Format value for display
|
||||
*/
|
||||
function formatValue(value: any): string {
|
||||
if (value === null || value === undefined) return '-'
|
||||
if (typeof value === 'object') return JSON.stringify(value)
|
||||
if (typeof value === 'string' && value.includes('T')) {
|
||||
// 可能是日期时间
|
||||
try {
|
||||
return new Date(value).toLocaleString()
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
// 组件挂载时加载表列表
|
||||
onMounted(() => {
|
||||
loadAvailableTables()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.universal-query-builder {
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user