|
@@ -1,194 +1,192 @@
|
|
|
<script setup lang="ts" name="SelTable">
|
|
<script setup lang="ts" name="SelTable">
|
|
|
|
|
+import { ref, reactive, watch, onUnmounted, nextTick } from 'vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
import { ElMessage } from 'element-plus'
|
|
|
import { formatNumber } from '@/utils/tools'
|
|
import { formatNumber } from '@/utils/tools'
|
|
|
import { useFiltersStore } from '@/stores/modules/filtersList'
|
|
import { useFiltersStore } from '@/stores/modules/filtersList'
|
|
|
-import { cloneDeep } from 'lodash'
|
|
|
|
|
|
|
+import { cloneDeep, debounce } from 'lodash'
|
|
|
import { useRoute } from 'vue-router'
|
|
import { useRoute } from 'vue-router'
|
|
|
|
|
+import axios from 'axios'
|
|
|
|
|
|
|
|
const filtersStore = useFiltersStore()
|
|
const filtersStore = useFiltersStore()
|
|
|
const route = useRoute()
|
|
const route = useRoute()
|
|
|
const searchMode = route.path.includes('booking') ? 'booking' : 'tracking'
|
|
const searchMode = route.path.includes('booking') ? 'booking' : 'tracking'
|
|
|
|
|
|
|
|
-const emit = defineEmits(['check', 'input'])
|
|
|
|
|
|
|
+const emit = defineEmits(['input'])
|
|
|
const props = defineProps({
|
|
const props = defineProps({
|
|
|
- poverWidth: {
|
|
|
|
|
- type: Number,
|
|
|
|
|
- default: 380
|
|
|
|
|
- },
|
|
|
|
|
-
|
|
|
|
|
- data: {
|
|
|
|
|
- type: Array,
|
|
|
|
|
- default: () => []
|
|
|
|
|
- },
|
|
|
|
|
- needKey: {
|
|
|
|
|
- type: String,
|
|
|
|
|
- default: 'city'
|
|
|
|
|
- },
|
|
|
|
|
- isError: {
|
|
|
|
|
- type: Boolean,
|
|
|
|
|
- default: false
|
|
|
|
|
- },
|
|
|
|
|
- title: {
|
|
|
|
|
- type: String,
|
|
|
|
|
- default: ''
|
|
|
|
|
- },
|
|
|
|
|
- disabled: {
|
|
|
|
|
- type: Boolean,
|
|
|
|
|
- default: false
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ poverWidth: { type: Number, default: 380 },
|
|
|
|
|
+ data: { type: Array, default: () => [] },
|
|
|
|
|
+ needKey: { type: String, default: 'city' },
|
|
|
|
|
+ isError: { type: Boolean, default: false },
|
|
|
|
|
+ title: { type: String, default: '' },
|
|
|
|
|
+ disabled: { type: Boolean, default: false }
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-const searchVal = ref(null)
|
|
|
|
|
-const innerTags: any = ref([])
|
|
|
|
|
-const tagInputRef: any = ref(null)
|
|
|
|
|
|
|
+const searchVal = ref('')
|
|
|
|
|
+const innerTags = ref<string[]>([])
|
|
|
|
|
+const tagInputRef = ref<HTMLInputElement | null>(null)
|
|
|
|
|
+
|
|
|
|
|
+// 用于取消上一次搜索请求
|
|
|
|
|
+let searchAbortController: AbortController | null = null
|
|
|
|
|
|
|
|
watch(
|
|
watch(
|
|
|
() => props.data,
|
|
() => props.data,
|
|
|
(current) => {
|
|
(current) => {
|
|
|
- innerTags.value = cloneDeep(current)
|
|
|
|
|
|
|
+ innerTags.value = cloneDeep(current as string[])
|
|
|
},
|
|
},
|
|
|
- {
|
|
|
|
|
- deep: true,
|
|
|
|
|
- immediate: true
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ { deep: true, immediate: true }
|
|
|
)
|
|
)
|
|
|
-// 响应数据
|
|
|
|
|
-const state: any = reactive({
|
|
|
|
|
|
|
+
|
|
|
|
|
+const state = reactive({
|
|
|
poverShow: false,
|
|
poverShow: false,
|
|
|
currentPage: 1,
|
|
currentPage: 1,
|
|
|
pageSize: 10,
|
|
pageSize: 10,
|
|
|
total: 0,
|
|
total: 0,
|
|
|
- activeRowIndex: '',
|
|
|
|
|
tableData: [],
|
|
tableData: [],
|
|
|
loading: false
|
|
loading: false
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-// 点击行
|
|
|
|
|
-const handleRowClick = (row: any) => {
|
|
|
|
|
- state.poverShow = false
|
|
|
|
|
- if (!innerTags.value.includes(row[props.needKey])) {
|
|
|
|
|
- innerTags.value.push(row[props.needKey])
|
|
|
|
|
- state.activeRowIndex = row.id
|
|
|
|
|
- emit('input', innerTags.value, props.title)
|
|
|
|
|
- } else {
|
|
|
|
|
- ElMessage({
|
|
|
|
|
- message: 'Cannot add duplicate cities.',
|
|
|
|
|
- type: 'success'
|
|
|
|
|
- })
|
|
|
|
|
|
|
+// —————— 获取数据 ——————
|
|
|
|
|
+const fetchData = async (query: string, page: number, isPageChange = false) => {
|
|
|
|
|
+ if (!isPageChange) {
|
|
|
|
|
+ if (searchAbortController) {
|
|
|
|
|
+ searchAbortController.abort()
|
|
|
|
|
+ }
|
|
|
|
|
+ searchAbortController = new AbortController()
|
|
|
}
|
|
}
|
|
|
- searchVal.value = null
|
|
|
|
|
-}
|
|
|
|
|
-const handleSearch = (val?) => {
|
|
|
|
|
|
|
+
|
|
|
state.loading = true
|
|
state.loading = true
|
|
|
- let curTableData: any = []
|
|
|
|
|
- state.tableData = []
|
|
|
|
|
- let rc = '-1'
|
|
|
|
|
- if (val === 'pageChange') {
|
|
|
|
|
- rc = state.total
|
|
|
|
|
- } else {
|
|
|
|
|
- searchVal.value = val ? val.target.value : ''
|
|
|
|
|
- state.total = 0
|
|
|
|
|
- }
|
|
|
|
|
- const queryData = filtersStore.getQueryData
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- $api
|
|
|
|
|
- .getMoreFiltersTableData({
|
|
|
|
|
- term: searchVal.value ? searchVal.value : '',
|
|
|
|
|
- cp: state.currentPage,
|
|
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const rc = isPageChange ? String(state.total) : '-1'
|
|
|
|
|
+ const queryData = filtersStore.getQueryData
|
|
|
|
|
+
|
|
|
|
|
+ const res = await $api.getMoreFiltersTableData(
|
|
|
|
|
+ {
|
|
|
|
|
+ term: query?.trim(),
|
|
|
|
|
+ cp: page,
|
|
|
ps: state.pageSize,
|
|
ps: state.pageSize,
|
|
|
rc,
|
|
rc,
|
|
|
search_field: props.title,
|
|
search_field: props.title,
|
|
|
search_mode: searchMode,
|
|
search_mode: searchMode,
|
|
|
...queryData
|
|
...queryData
|
|
|
- })
|
|
|
|
|
- .then((res: any) => {
|
|
|
|
|
- if (res.code == 200) {
|
|
|
|
|
- curTableData = res.data.searchData
|
|
|
|
|
- curTableData = res.data.searchData.filter((p: any) => {
|
|
|
|
|
- return (
|
|
|
|
|
- p.country.toLowerCase().indexOf(searchVal.value.toLowerCase()) > -1 ||
|
|
|
|
|
- p.city.toLowerCase().indexOf(searchVal.value.toLowerCase()) > -1 ||
|
|
|
|
|
- p.uncode.toLowerCase().indexOf(searchVal.value.toLowerCase()) > -1
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
- state.tableData = curTableData
|
|
|
|
|
- state.total = Number(res.data.rc)
|
|
|
|
|
- state.loading = false
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
- }, 800)
|
|
|
|
|
|
|
+ },
|
|
|
|
|
+ { signal: searchAbortController?.signal }
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if (searchAbortController?.signal.aborted) return
|
|
|
|
|
+
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ state.tableData = res.data.searchData || []
|
|
|
|
|
+ state.total = Number(res.data.rc || 0)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ state.tableData = []
|
|
|
|
|
+ state.total = 0
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ if (!axios.isCancel(err) && !searchAbortController?.signal.aborted) {
|
|
|
|
|
+ ElMessage.error('Failed to load data')
|
|
|
|
|
+ state.tableData = []
|
|
|
|
|
+ state.total = 0
|
|
|
|
|
+ }
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (!searchAbortController?.signal.aborted) {
|
|
|
|
|
+ state.loading = false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
-// 分页 请求接口
|
|
|
|
|
-const handleCurrentChange = () => {
|
|
|
|
|
- state.loading = true
|
|
|
|
|
- let tableData: any = []
|
|
|
|
|
- const queryData = filtersStore.getQueryData
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- $api
|
|
|
|
|
- .getMoreFiltersTableData({
|
|
|
|
|
- term: searchVal.value ? searchVal.value : '',
|
|
|
|
|
- cp: state.currentPage,
|
|
|
|
|
- ps: state.pageSize,
|
|
|
|
|
- rc: state.total,
|
|
|
|
|
- search_field: props.title,
|
|
|
|
|
- search_mode: 'booking',
|
|
|
|
|
- ...queryData
|
|
|
|
|
- })
|
|
|
|
|
- .then((res: any) => {
|
|
|
|
|
- if (res.code == 200) {
|
|
|
|
|
- tableData = res.data.searchData
|
|
|
|
|
- state.loading = false
|
|
|
|
|
- tableData = res.data.searchData.filter((p: any) => {
|
|
|
|
|
- return (
|
|
|
|
|
- p.country.toLowerCase().indexOf(searchVal.value.toLowerCase()) > -1 ||
|
|
|
|
|
- p.city.toLowerCase().indexOf(searchVal.value.toLowerCase()) > -1 ||
|
|
|
|
|
- p.uncode.toLowerCase().indexOf(searchVal.value.toLowerCase()) > -1
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
- state.tableData = tableData
|
|
|
|
|
- state.total = Number(res.data.rc)
|
|
|
|
|
- state.loading = false
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
- }, 800)
|
|
|
|
|
|
|
+
|
|
|
|
|
+// —————— 防抖搜索 ——————
|
|
|
|
|
+const debouncedSearch = debounce((query: string) => {
|
|
|
|
|
+ state.currentPage = 1
|
|
|
|
|
+ fetchData(query, 1, false)
|
|
|
|
|
+}, 300)
|
|
|
|
|
+
|
|
|
|
|
+// —————— 事件处理 ——————
|
|
|
|
|
+const handleSearchInput = () => {
|
|
|
|
|
+ const val = searchVal.value
|
|
|
|
|
+ if (val.trim() === '') {
|
|
|
|
|
+ state.tableData = []
|
|
|
|
|
+ state.total = 0
|
|
|
|
|
+ state.currentPage = 1
|
|
|
|
|
+ } else {
|
|
|
|
|
+ debouncedSearch(val)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handlePageChange = () => {
|
|
|
|
|
+ fetchData(searchVal.value, state.currentPage, true)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleRowClick = (row: any) => {
|
|
|
|
|
+ const keyVal = row[props.needKey]
|
|
|
|
|
+ if (innerTags.value.includes(keyVal)) {
|
|
|
|
|
+ ElMessage.success('Cannot add duplicate cities.')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ innerTags.value.push(keyVal)
|
|
|
|
|
+ emit('input', innerTags.value, props.title)
|
|
|
|
|
+
|
|
|
|
|
+ // 关闭弹窗并重置搜索
|
|
|
|
|
+ state.poverShow = false
|
|
|
|
|
+ searchVal.value = ''
|
|
|
|
|
+ state.tableData = []
|
|
|
|
|
+ state.currentPage = 1
|
|
|
|
|
+ state.total = 0
|
|
|
}
|
|
}
|
|
|
-// 删除
|
|
|
|
|
|
|
+
|
|
|
const remove = (idx: number) => {
|
|
const remove = (idx: number) => {
|
|
|
innerTags.value.splice(idx, 1)
|
|
innerTags.value.splice(idx, 1)
|
|
|
emit('input', innerTags.value, props.title)
|
|
emit('input', innerTags.value, props.title)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const onInput = () => {
|
|
|
|
|
- tagInputRef.value.focus()
|
|
|
|
|
|
|
+// 👇 仅在点击 reference 区域时打开弹窗
|
|
|
|
|
+const openPopover = () => {
|
|
|
|
|
+ if (props.disabled) return
|
|
|
|
|
+ state.poverShow = true
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ tagInputRef.value?.focus()
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 在切换title时,可以用来清空搜索框
|
|
|
|
|
const clearSearchInput = () => {
|
|
const clearSearchInput = () => {
|
|
|
searchVal.value = ''
|
|
searchVal.value = ''
|
|
|
state.tableData = []
|
|
state.tableData = []
|
|
|
state.currentPage = 1
|
|
state.currentPage = 1
|
|
|
state.total = 0
|
|
state.total = 0
|
|
|
|
|
+ if (searchAbortController) {
|
|
|
|
|
+ searchAbortController.abort()
|
|
|
|
|
+ searchAbortController = null
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
-defineExpose({
|
|
|
|
|
- clearSearchInput
|
|
|
|
|
|
|
+
|
|
|
|
|
+defineExpose({ clearSearchInput })
|
|
|
|
|
+
|
|
|
|
|
+onUnmounted(() => {
|
|
|
|
|
+ if (searchAbortController) {
|
|
|
|
|
+ searchAbortController.abort()
|
|
|
|
|
+ }
|
|
|
})
|
|
})
|
|
|
</script>
|
|
</script>
|
|
|
|
|
+
|
|
|
<template>
|
|
<template>
|
|
|
<div>
|
|
<div>
|
|
|
|
|
+ <!-- 手动控制弹窗 -->
|
|
|
<el-popover
|
|
<el-popover
|
|
|
v-model:visible="state.poverShow"
|
|
v-model:visible="state.poverShow"
|
|
|
placement="bottom-start"
|
|
placement="bottom-start"
|
|
|
:teleported="false"
|
|
:teleported="false"
|
|
|
:width="props.poverWidth"
|
|
:width="props.poverWidth"
|
|
|
- trigger="click"
|
|
|
|
|
|
|
+ trigger="manual"
|
|
|
>
|
|
>
|
|
|
<template #reference>
|
|
<template #reference>
|
|
|
|
|
+ <!-- 点击此区域才打开弹窗 -->
|
|
|
<div
|
|
<div
|
|
|
class="el-input el-input--suffix el-tooltip__trigger"
|
|
class="el-input el-input--suffix el-tooltip__trigger"
|
|
|
- @click="disabled ? null : onInput"
|
|
|
|
|
|
|
+ @click="openPopover"
|
|
|
:class="{ is_error: props.isError }"
|
|
:class="{ is_error: props.isError }"
|
|
|
>
|
|
>
|
|
|
- <div class="el-input__wrapper" :class="{ 'is-disabled': disabled }">
|
|
|
|
|
|
|
+ <div class="el-input__wrapper" :class="{ 'is-disabled': props.disabled }">
|
|
|
<el-space :class="[innerTags.length ? 'custom-el-spaceno' : 'custom-el-space']">
|
|
<el-space :class="[innerTags.length ? 'custom-el-spaceno' : 'custom-el-space']">
|
|
|
<template v-for="(item, idx) in innerTags" :key="item">
|
|
<template v-for="(item, idx) in innerTags" :key="item">
|
|
|
<template v-if="idx <= 2">
|
|
<template v-if="idx <= 2">
|
|
@@ -197,7 +195,7 @@ defineExpose({
|
|
|
</el-tag>
|
|
</el-tag>
|
|
|
</template>
|
|
</template>
|
|
|
</template>
|
|
</template>
|
|
|
- <template v-if="innerTags.length && innerTags.length > 3">
|
|
|
|
|
|
|
+ <template v-if="innerTags.length > 3">
|
|
|
<el-popover placement="bottom" trigger="hover">
|
|
<el-popover placement="bottom" trigger="hover">
|
|
|
<template #reference>
|
|
<template #reference>
|
|
|
<el-tag type="info" size="small" round :disable-transitions="false">
|
|
<el-tag type="info" size="small" round :disable-transitions="false">
|
|
@@ -228,19 +226,20 @@ defineExpose({
|
|
|
</el-popover>
|
|
</el-popover>
|
|
|
</template>
|
|
</template>
|
|
|
<input
|
|
<input
|
|
|
- :value="searchVal"
|
|
|
|
|
|
|
+ v-model="searchVal"
|
|
|
ref="tagInputRef"
|
|
ref="tagInputRef"
|
|
|
:placeholder="innerTags.length ? '' : 'Please input country/city/uncode'"
|
|
:placeholder="innerTags.length ? '' : 'Please input country/city/uncode'"
|
|
|
class="el-input__inner"
|
|
class="el-input__inner"
|
|
|
- :disabled="disabled"
|
|
|
|
|
|
|
+ :disabled="props.disabled"
|
|
|
type="text"
|
|
type="text"
|
|
|
- @input="handleSearch"
|
|
|
|
|
- @click="handleSearch"
|
|
|
|
|
|
|
+ @input="handleSearchInput"
|
|
|
|
|
+ @keydown.enter.stop.prevent
|
|
|
|
|
+ @focus="debouncedSearch('')"
|
|
|
/>
|
|
/>
|
|
|
</el-space>
|
|
</el-space>
|
|
|
- <div class="el-input__suffix">
|
|
|
|
|
- <div class="el-input__suffix-inner" v-if="!disabled">
|
|
|
|
|
- <el-icon :class="state.poverShow ? 'reverse' : ''">
|
|
|
|
|
|
|
+ <div class="el-input__suffix" v-if="!props.disabled">
|
|
|
|
|
+ <div class="el-input__suffix-inner">
|
|
|
|
|
+ <el-icon :class="{ reverse: state.poverShow }">
|
|
|
<CaretBottom />
|
|
<CaretBottom />
|
|
|
</el-icon>
|
|
</el-icon>
|
|
|
</div>
|
|
</div>
|
|
@@ -248,6 +247,8 @@ defineExpose({
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 数据表格 -->
|
|
|
<el-table
|
|
<el-table
|
|
|
:data="state.tableData"
|
|
:data="state.tableData"
|
|
|
border
|
|
border
|
|
@@ -255,11 +256,14 @@ defineExpose({
|
|
|
v-loading="state.loading"
|
|
v-loading="state.loading"
|
|
|
@row-click="handleRowClick"
|
|
@row-click="handleRowClick"
|
|
|
header-row-class-name="cus-header"
|
|
header-row-class-name="cus-header"
|
|
|
|
|
+ style="width: 100%"
|
|
|
>
|
|
>
|
|
|
<el-table-column property="country" label="Country" width="75" />
|
|
<el-table-column property="country" label="Country" width="75" />
|
|
|
<el-table-column property="city" label="City" />
|
|
<el-table-column property="city" label="City" />
|
|
|
<el-table-column property="uncode" label="Uncode" width="80" />
|
|
<el-table-column property="uncode" label="Uncode" width="80" />
|
|
|
</el-table>
|
|
</el-table>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 分页 -->
|
|
|
<div class="pagination">
|
|
<div class="pagination">
|
|
|
<span>Total {{ formatNumber(state.total) }}</span>
|
|
<span>Total {{ formatNumber(state.total) }}</span>
|
|
|
<el-pagination
|
|
<el-pagination
|
|
@@ -269,8 +273,8 @@ defineExpose({
|
|
|
layout="prev, pager, next"
|
|
layout="prev, pager, next"
|
|
|
:pager-count="5"
|
|
:pager-count="5"
|
|
|
:total="state.total"
|
|
:total="state.total"
|
|
|
- @current-change="handleSearch('pageChange')"
|
|
|
|
|
- @size-change="handleSearch('pageChange')"
|
|
|
|
|
|
|
+ @current-change="handlePageChange"
|
|
|
|
|
+ @size-change="handlePageChange"
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
</el-popover>
|
|
</el-popover>
|
|
@@ -306,22 +310,16 @@ defineExpose({
|
|
|
background-color: var(--color-table-header-bg) !important;
|
|
background-color: var(--color-table-header-bg) !important;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-:deep(.el-table__row) {
|
|
|
|
|
- td {
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+:deep(.el-table__row) td {
|
|
|
|
|
+ cursor: pointer;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-:deep(.el-table__row:not(.current-row):hover) {
|
|
|
|
|
- td {
|
|
|
|
|
- background-color: var(--color-btn-default-bg-hover) !important;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+:deep(.el-table__row:not(.current-row):hover) td {
|
|
|
|
|
+ background-color: var(--color-btn-default-bg-hover) !important;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-:deep(.current-row) {
|
|
|
|
|
- td {
|
|
|
|
|
- background-color: #ffe3cd !important;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+:deep(.current-row) td {
|
|
|
|
|
+ background-color: #ffe3cd !important;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.pagination {
|
|
.pagination {
|