admin_new/src/SearchBar.vue

275 lines
7.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
/**
* F006 通用 SearchBar 组件 (W17 设计规范 v2 整改)
*
* 按 ui-preview-v2.html 模板: 紧凑布局 + 无 label + 同行末尾 chip + 简洁按钮
*
* 用法:
* <SearchBar
* v-model:filters="filters"
* :fields="[
* { key: 'name', label: '名称', type: 'text', placeholder: '请输入' },
* { key: 'status', label: '状态', type: 'select', options: [...] }
* ]"
* @search="onSearch"
* @reset="onReset"
* />
*/
import { computed } from 'vue'
export interface SearchFieldOption {
value: any
label: string
}
export interface SearchField {
key: string
label: string
type?: 'text' | 'select' | 'date' | 'daterange' | 'number'
placeholder?: string
options?: SearchFieldOption[]
width?: number | string
clearable?: boolean
showLabel?: boolean
}
interface Props {
filters: Record<string, any>
fields: SearchField[]
showReset?: boolean
showChips?: boolean
showLabel?: boolean
}
const props = withDefaults(defineProps<Props>(), {
showReset: true,
showChips: true,
showLabel: false
})
const emit = defineEmits<{
(e: 'update:filters', filters: Record<string, any>): void
(e: 'search'): void
(e: 'reset'): void
}>()
const localFilters = computed({
get: () => props.filters,
set: (v) => emit('update:filters', v)
})
function onSearch() { emit('search') }
function onReset() {
for (const f of props.fields) {
if (f.type === 'daterange') localFilters.value[f.key] = []
else if (f.type === 'number') localFilters.value[f.key] = undefined
else localFilters.value[f.key] = ''
}
emit('reset')
}
function onEnter() { onSearch() }
function onClearField(key: string) {
const f = props.fields.find(it => it.key === key)
if (!f) return
if (f.type === 'daterange') localFilters.value[key] = []
else if (f.type === 'number') localFilters.value[key] = undefined
else localFilters.value[key] = ''
}
const activeChips = computed(() => {
return props.fields
.map(f => {
const v = props.filters[f.key]
let display: string | null = null
if (f.type === 'daterange') {
if (Array.isArray(v) && v.length === 2) display = `${v[0]} ~ ${v[1]}`
} else if (f.type === 'select' && v !== '' && v !== undefined && v !== null) {
const opt = f.options?.find(o => String(o.value) === String(v))
display = opt ? opt.label : String(v)
} else if (v !== '' && v !== undefined && v !== null) {
display = String(v)
}
if (display) {
return { key: f.key, label: `${f.label}: ${display}` }
}
return null
})
.filter(Boolean) as { key: string; label: string }[]
})
</script>
<template>
<div class="rr-search-bar">
<div class="rr-search-bar__row">
<div class="rr-search-bar__field" v-for="f in fields" :key="f.key">
<span v-if="showLabel || f.showLabel" class="rr-search-bar__label">{{ f.label }}</span>
<el-input
v-if="f.type === 'text' || !f.type"
v-model="localFilters[f.key]"
:placeholder="f.placeholder || `请输入${f.label}`"
:clearable="f.clearable !== false"
:style="{ width: (typeof f.width === 'number' ? f.width + 'px' : (f.width || '180px')) }"
@keyup.enter="onEnter"
/>
<el-select
v-else-if="f.type === 'select'"
v-model="localFilters[f.key]"
:placeholder="f.placeholder || `请选择${f.label}`"
:clearable="f.clearable !== false"
:style="{ width: (typeof f.width === 'number' ? f.width + 'px' : (f.width || '160px')) }"
>
<el-option
v-for="opt in f.options || []"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-date-picker
v-else-if="f.type === 'date'"
v-model="localFilters[f.key]"
type="date"
:placeholder="f.placeholder || `请选择${f.label}`"
:style="{ width: (typeof f.width === 'number' ? f.width + 'px' : (f.width || '180px')) }"
value-format="YYYY-MM-DD"
/>
<el-date-picker
v-else-if="f.type === 'daterange'"
v-model="localFilters[f.key]"
type="daterange"
range-separator="~"
:start-placeholder="f.placeholder || '开始日期'"
:end-placeholder="'结束日期'"
:style="{ width: (typeof f.width === 'number' ? f.width + 'px' : (f.width || '260px')) }"
value-format="YYYY-MM-DD"
/>
<el-input-number
v-else-if="f.type === 'number'"
v-model="localFilters[f.key]"
:placeholder="f.placeholder || `请输入${f.label}`"
:style="{ width: (typeof f.width === 'number' ? f.width + 'px' : (f.width || '180px')) }"
/>
</div>
<div class="rr-search-bar__chips" v-if="showChips && activeChips.length">
<div
v-for="c in activeChips"
:key="c.key"
class="rr-search-bar__chip"
>
{{ c.label }}
<span class="rr-search-bar__chip-close" @click="onClearField(c.key)">×</span>
</div>
</div>
<div class="rr-search-bar__actions">
<el-button type="primary" @click="onSearch">查询</el-button>
<el-button v-if="showReset" @click="onReset">重置</el-button>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
/* === 统一 input/select 圆角 (按 ui-preview-v2.html 模板) === */
:deep(.el-input__wrapper),
:deep(.el-select__wrapper) {
border-radius: var(--radius, 8px);
box-shadow: 0 0 0 1px var(--rr-border-color, #E2E8F0) inset;
transition: box-shadow 0.15s, border-color 0.15s;
min-height: 34px;
}
:deep(.el-input__wrapper:hover),
:deep(.el-select__wrapper:hover) {
box-shadow: 0 0 0 1px var(--ink-3, #94A3B8) inset;
}
:deep(.el-input__wrapper.is-focus),
:deep(.el-select__wrapper.is-focused) {
box-shadow: 0 0 0 1px var(--brand, #2563EB) inset, 0 0 0 3px var(--brand-glow, rgba(37, 99, 235, 0.18));
}
/* === el-select 宽度兜底 (Vue :style 没生效时也保证最小宽度) === */
:deep(.el-select) {
min-width: 140px;
width: auto !important;
}
:deep(.el-select .el-select__wrapper) {
min-width: 140px;
}
:deep(.el-select .el-select__wrapper .el-select__selection) {
min-width: 100px;
}
/* === 下拉弹层圆角 (跟 input 一致) === */
:deep(.el-select-dropdown),
:deep(.el-select__popper),
:deep(.el-popper.is-light) {
border-radius: var(--radius, 8px) !important;
overflow: hidden;
}
:deep(.el-select-dropdown__item) {
border-radius: var(--radius-sm, 6px);
margin: 2px 4px;
padding: 0 12px;
height: 32px;
line-height: 32px;
font-size: 13px;
}
:deep(.el-select-dropdown__item.is-hovering) {
background: var(--brand-soft, rgba(37, 99, 235, 0.08));
color: var(--brand, #2563EB);
}
:deep(.el-select-dropdown__item.is-selected) {
font-weight: 600;
color: var(--brand, #2563EB);
}
.rr-search-bar {
margin-bottom: 16px;
&__row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
&__field {
display: inline-flex;
align-items: center;
}
&__label {
font-size: 13px;
color: var(--ink-2, #334155);
margin-right: 6px;
}
&__chips {
display: inline-flex;
align-items: center;
gap: 6px;
}
&__chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
background: var(--brand-soft, rgba(37, 99, 235, 0.08));
color: var(--brand, #2563EB);
border-radius: var(--radius-pill, 9999px);
font-size: 12px;
font-weight: 500;
&-close {
cursor: pointer;
opacity: 0.6;
font-size: 14px;
line-height: 1;
&:hover { opacity: 1; }
}
}
&__actions {
display: inline-flex;
gap: 8px;
margin-left: auto;
:deep(.el-button) {
padding: 7px 16px;
font-weight: 500;
border-radius: var(--radius, 8px);
}
}
}
</style>