feat: add full search for set/zset
This commit is contained in:
parent
eca640fc87
commit
6a048037b0
|
@ -762,6 +762,12 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
|
||||||
}
|
}
|
||||||
|
|
||||||
case "set":
|
case "set":
|
||||||
|
if !strings.HasPrefix(matchPattern, "*") {
|
||||||
|
matchPattern = "*" + matchPattern
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(matchPattern, "*") {
|
||||||
|
matchPattern = matchPattern + "*"
|
||||||
|
}
|
||||||
loadSetHandle := func() ([]types.SetEntryItem, bool, bool, error) {
|
loadSetHandle := func() ([]types.SetEntryItem, bool, bool, error) {
|
||||||
var items []types.SetEntryItem
|
var items []types.SetEntryItem
|
||||||
var cursor uint64
|
var cursor uint64
|
||||||
|
@ -769,11 +775,11 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
|
||||||
var subErr error
|
var subErr error
|
||||||
var loadedKey []string
|
var loadedKey []string
|
||||||
scanSize := int64(Preferences().GetScanSize())
|
scanSize := int64(Preferences().GetScanSize())
|
||||||
if param.Full {
|
if param.Full || matchPattern != "*" {
|
||||||
// load all
|
// load all
|
||||||
cursor, reset = 0, true
|
cursor, reset = 0, true
|
||||||
for {
|
for {
|
||||||
loadedKey, cursor, subErr = client.SScan(ctx, key, cursor, param.MatchPattern, scanSize).Result()
|
loadedKey, cursor, subErr = client.SScan(ctx, key, cursor, matchPattern, scanSize).Result()
|
||||||
if subErr != nil {
|
if subErr != nil {
|
||||||
return items, reset, false, subErr
|
return items, reset, false, subErr
|
||||||
}
|
}
|
||||||
|
@ -797,7 +803,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
|
||||||
} else {
|
} else {
|
||||||
cursor, _, reset = getEntryCursor()
|
cursor, _, reset = getEntryCursor()
|
||||||
}
|
}
|
||||||
loadedKey, cursor, subErr = client.SScan(ctx, key, cursor, param.MatchPattern, scanSize).Result()
|
loadedKey, cursor, subErr = client.SScan(ctx, key, cursor, matchPattern, scanSize).Result()
|
||||||
items = make([]types.SetEntryItem, len(loadedKey))
|
items = make([]types.SetEntryItem, len(loadedKey))
|
||||||
for i, val := range loadedKey {
|
for i, val := range loadedKey {
|
||||||
items[i].Value = val
|
items[i].Value = val
|
||||||
|
@ -820,17 +826,23 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
|
||||||
}
|
}
|
||||||
|
|
||||||
case "zset":
|
case "zset":
|
||||||
|
if !strings.HasPrefix(matchPattern, "*") {
|
||||||
|
matchPattern = "*" + matchPattern
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(matchPattern, "*") {
|
||||||
|
matchPattern = matchPattern + "*"
|
||||||
|
}
|
||||||
loadZSetHandle := func() ([]types.ZSetEntryItem, bool, bool, error) {
|
loadZSetHandle := func() ([]types.ZSetEntryItem, bool, bool, error) {
|
||||||
var items []types.ZSetEntryItem
|
var items []types.ZSetEntryItem
|
||||||
var reset bool
|
var reset bool
|
||||||
var cursor uint64
|
var cursor uint64
|
||||||
scanSize := int64(Preferences().GetScanSize())
|
scanSize := int64(Preferences().GetScanSize())
|
||||||
var loadedVal []string
|
var loadedVal []string
|
||||||
if param.Full {
|
if param.Full || matchPattern != "*" {
|
||||||
// load all
|
// load all
|
||||||
cursor, reset = 0, true
|
cursor, reset = 0, true
|
||||||
for {
|
for {
|
||||||
loadedVal, cursor, err = client.ZScan(ctx, key, cursor, param.MatchPattern, scanSize).Result()
|
loadedVal, cursor, err = client.ZScan(ctx, key, cursor, matchPattern, scanSize).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return items, reset, false, err
|
return items, reset, false, err
|
||||||
}
|
}
|
||||||
|
@ -858,7 +870,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
|
||||||
} else {
|
} else {
|
||||||
cursor, _, reset = getEntryCursor()
|
cursor, _, reset = getEntryCursor()
|
||||||
}
|
}
|
||||||
loadedVal, cursor, err = client.ZScan(ctx, key, cursor, param.MatchPattern, scanSize).Result()
|
loadedVal, cursor, err = client.ZScan(ctx, key, cursor, matchPattern, scanSize).Result()
|
||||||
loadedLen := len(loadedVal)
|
loadedLen := len(loadedVal)
|
||||||
items = make([]types.ZSetEntryItem, loadedLen/2)
|
items = make([]types.ZSetEntryItem, loadedLen/2)
|
||||||
var score float64
|
var score float64
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, reactive } from 'vue'
|
import { computed, reactive } from 'vue'
|
||||||
import { isEmpty, trim } from 'lodash'
|
import { debounce, isEmpty, trim } from 'lodash'
|
||||||
import { NButton, NInput } from 'naive-ui'
|
import { NButton, NInput } from 'naive-ui'
|
||||||
|
|
||||||
const emit = defineEmits(['filterChanged', 'matchChanged'])
|
const emit = defineEmits(['filterChanged', 'matchChanged'])
|
||||||
|
@ -31,9 +31,10 @@ const onFullSearch = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onInput = () => {
|
const _onInput = () => {
|
||||||
emit('filterChanged', inputData.filter)
|
emit('filterChanged', inputData.filter)
|
||||||
}
|
}
|
||||||
|
const onInput = debounce(_onInput, 500, { leading: true, trailing: true })
|
||||||
|
|
||||||
const onClearFilter = () => {
|
const onClearFilter = () => {
|
||||||
inputData.filter = ''
|
inputData.filter = ''
|
||||||
|
@ -70,7 +71,7 @@ defineExpose({
|
||||||
{{ inputData.match }}
|
{{ inputData.match }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
</template>
|
</template>
|
||||||
{{ $t('interface.full_search') }}
|
{{ $t('interface.full_search_result', { pattern: inputData.match }) }}
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
</template>
|
</template>
|
||||||
</n-input>
|
</n-input>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { computed, h, reactive, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import ContentToolbar from './ContentToolbar.vue'
|
import ContentToolbar from './ContentToolbar.vue'
|
||||||
import AddLink from '@/components/icons/AddLink.vue'
|
import AddLink from '@/components/icons/AddLink.vue'
|
||||||
import { NButton, NCode, NIcon, NInput, useThemeVars } from 'naive-ui'
|
import { NButton, NCode, NIcon, useThemeVars } from 'naive-ui'
|
||||||
import { isEmpty, size } from 'lodash'
|
import { isEmpty, size } from 'lodash'
|
||||||
import useDialogStore from 'stores/dialog.js'
|
import useDialogStore from 'stores/dialog.js'
|
||||||
import { types, types as redisTypes } from '@/consts/support_redis_type.js'
|
import { types, types as redisTypes } from '@/consts/support_redis_type.js'
|
||||||
|
@ -17,6 +17,7 @@ import IconButton from '@/components/common/IconButton.vue'
|
||||||
import Edit from '@/components/icons/Edit.vue'
|
import Edit from '@/components/icons/Edit.vue'
|
||||||
import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue'
|
import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue'
|
||||||
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
||||||
|
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
||||||
|
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
|
@ -51,7 +52,7 @@ const props = defineProps({
|
||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete'])
|
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete', 'match'])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -246,13 +247,13 @@ const onAddValue = (value) => {
|
||||||
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.SET)
|
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.SET)
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterValue = ref('')
|
|
||||||
const onFilterInput = (val) => {
|
const onFilterInput = (val) => {
|
||||||
valueFilterOption.value = val
|
valueFilterOption.value = val
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearFilter = () => {
|
const onMatchInput = (matchVal, filterVal) => {
|
||||||
valueFilterOption.value = null
|
valueFilterOption.value = filterVal
|
||||||
|
emit('match', matchVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onUpdateFilter = (filters, sourceColumn) => {
|
const onUpdateFilter = (filters, sourceColumn) => {
|
||||||
|
@ -263,10 +264,11 @@ const onFormatChanged = (selDecode, selFormat) => {
|
||||||
emit('reload', selDecode, selFormat)
|
emit('reload', selDecode, selFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const searchInputRef = ref(null)
|
||||||
defineExpose({
|
defineExpose({
|
||||||
reset: () => {
|
reset: () => {
|
||||||
clearFilter()
|
|
||||||
resetEdit()
|
resetEdit()
|
||||||
|
searchInputRef.value?.reset()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -287,12 +289,10 @@ defineExpose({
|
||||||
@rename="emit('rename')" />
|
@rename="emit('rename')" />
|
||||||
<div class="tb2 value-item-part flex-box-h">
|
<div class="tb2 value-item-part flex-box-h">
|
||||||
<div class="flex-box-h">
|
<div class="flex-box-h">
|
||||||
<n-input
|
<content-search-input
|
||||||
v-model:value="filterValue"
|
ref="searchInputRef"
|
||||||
:placeholder="$t('interface.search')"
|
@filter-changed="onFilterInput"
|
||||||
clearable
|
@match-changed="onMatchInput" />
|
||||||
@clear="clearFilter"
|
|
||||||
@update:value="onFilterInput" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-item-expand"></div>
|
<div class="flex-item-expand"></div>
|
||||||
<n-button-group>
|
<n-button-group>
|
||||||
|
|
|
@ -100,13 +100,14 @@ const loadData = async (reset, full, selMatch) => {
|
||||||
*/
|
*/
|
||||||
const onReload = async (selDecode, selFormat) => {
|
const onReload = async (selDecode, selFormat) => {
|
||||||
try {
|
try {
|
||||||
const { name, db, keyCode, keyPath, decode, format } = data.value
|
const { name, db, keyCode, keyPath, decode, format, matchPattern } = data.value
|
||||||
await browserStore.reloadKey({
|
await browserStore.reloadKey({
|
||||||
server: name,
|
server: name,
|
||||||
db,
|
db,
|
||||||
key: keyCode || keyPath,
|
key: keyCode || keyPath,
|
||||||
decode: selDecode || decode,
|
decode: selDecode || decode,
|
||||||
format: selFormat || format,
|
format: selFormat || format,
|
||||||
|
matchPattern,
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { computed, h, reactive, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import ContentToolbar from './ContentToolbar.vue'
|
import ContentToolbar from './ContentToolbar.vue'
|
||||||
import AddLink from '@/components/icons/AddLink.vue'
|
import AddLink from '@/components/icons/AddLink.vue'
|
||||||
import { NButton, NCode, NIcon, NInput, useThemeVars } from 'naive-ui'
|
import { NButton, NCode, NIcon, useThemeVars } from 'naive-ui'
|
||||||
import { types, types as redisTypes } from '@/consts/support_redis_type.js'
|
import { types, types as redisTypes } from '@/consts/support_redis_type.js'
|
||||||
import EditableTableColumn from '@/components/common/EditableTableColumn.vue'
|
import EditableTableColumn from '@/components/common/EditableTableColumn.vue'
|
||||||
import { isEmpty, size } from 'lodash'
|
import { isEmpty, size } from 'lodash'
|
||||||
|
@ -17,6 +17,7 @@ import IconButton from '@/components/common/IconButton.vue'
|
||||||
import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue'
|
import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue'
|
||||||
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
||||||
import Edit from '@/components/icons/Edit.vue'
|
import Edit from '@/components/icons/Edit.vue'
|
||||||
|
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
||||||
|
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
|
@ -51,7 +52,7 @@ const props = defineProps({
|
||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete'])
|
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete', 'match'])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -61,18 +62,6 @@ const keyName = computed(() => {
|
||||||
return !isEmpty(props.keyCode) ? props.keyCode : props.keyPath
|
return !isEmpty(props.keyCode) ? props.keyCode : props.keyPath
|
||||||
})
|
})
|
||||||
|
|
||||||
const filterOption = [
|
|
||||||
{
|
|
||||||
value: 1,
|
|
||||||
label: i18n.t('common.value'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 2,
|
|
||||||
label: i18n.t('common.score'),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
const filterType = ref(1)
|
|
||||||
|
|
||||||
const browserStore = useBrowserStore()
|
const browserStore = useBrowserStore()
|
||||||
const dialogStore = useDialogStore()
|
const dialogStore = useDialogStore()
|
||||||
const keyType = redisTypes.ZSET
|
const keyType = redisTypes.ZSET
|
||||||
|
@ -89,46 +78,46 @@ const inEdit = computed(() => {
|
||||||
})
|
})
|
||||||
const fullEdit = ref(false)
|
const fullEdit = ref(false)
|
||||||
|
|
||||||
const scoreFilterOption = ref(null)
|
// const scoreFilterOption = ref(null)
|
||||||
const scoreColumn = computed(() => ({
|
const scoreColumn = computed(() => ({
|
||||||
key: 'score',
|
key: 'score',
|
||||||
title: i18n.t('common.score'),
|
title: i18n.t('common.score'),
|
||||||
align: 'center',
|
align: 'center',
|
||||||
titleAlign: 'center',
|
titleAlign: 'center',
|
||||||
resizable: true,
|
resizable: true,
|
||||||
filterOptionValue: scoreFilterOption.value,
|
// filterOptionValue: scoreFilterOption.value,
|
||||||
filter(value, row) {
|
// filter(value, row) {
|
||||||
const score = parseFloat(row.s)
|
// const score = parseFloat(row.s)
|
||||||
if (isNaN(score)) {
|
// if (isNaN(score)) {
|
||||||
return true
|
// return true
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
const regex = /^(>=|<=|>|<|=|!=)?(\d+(\.\d*)?)?$/
|
// const regex = /^(>=|<=|>|<|=|!=)?(\d+(\.\d*)?)?$/
|
||||||
const matches = value.match(regex)
|
// const matches = value.match(regex)
|
||||||
if (matches) {
|
// if (matches) {
|
||||||
const operator = matches[1] || ''
|
// const operator = matches[1] || ''
|
||||||
const filterScore = parseFloat(matches[2] || '')
|
// const filterScore = parseFloat(matches[2] || '')
|
||||||
if (!isNaN(filterScore)) {
|
// if (!isNaN(filterScore)) {
|
||||||
switch (operator) {
|
// switch (operator) {
|
||||||
case '>=':
|
// case '>=':
|
||||||
return score >= filterScore
|
// return score >= filterScore
|
||||||
case '<=':
|
// case '<=':
|
||||||
return score <= filterScore
|
// return score <= filterScore
|
||||||
case '>':
|
// case '>':
|
||||||
return score > filterScore
|
// return score > filterScore
|
||||||
case '<':
|
// case '<':
|
||||||
return score < filterScore
|
// return score < filterScore
|
||||||
case '=':
|
// case '=':
|
||||||
return score === filterScore
|
// return score === filterScore
|
||||||
case '!=':
|
// case '!=':
|
||||||
return score !== filterScore
|
// return score !== filterScore
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
return !!~row.v.indexOf(value.toString())
|
// return !!~row.v.indexOf(value.toString())
|
||||||
}
|
// }
|
||||||
return true
|
// return true
|
||||||
},
|
// },
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
return row.s
|
return row.s
|
||||||
},
|
},
|
||||||
|
@ -294,52 +283,28 @@ const onAddRow = () => {
|
||||||
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.ZSET)
|
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.ZSET)
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterValue = ref('')
|
|
||||||
const onFilterInput = (val) => {
|
const onFilterInput = (val) => {
|
||||||
switch (filterType.value) {
|
|
||||||
case filterOption[0].value:
|
|
||||||
// filter value
|
|
||||||
scoreFilterOption.value = null
|
|
||||||
valueFilterOption.value = val
|
valueFilterOption.value = val
|
||||||
break
|
|
||||||
case filterOption[1].value:
|
|
||||||
// filter score
|
|
||||||
valueFilterOption.value = null
|
|
||||||
scoreFilterOption.value = val
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChangeFilterType = (type) => {
|
const onMatchInput = (matchVal, filterVal) => {
|
||||||
onFilterInput(filterValue.value)
|
valueFilterOption.value = filterVal
|
||||||
}
|
emit('match', matchVal)
|
||||||
|
|
||||||
const clearFilter = () => {
|
|
||||||
valueFilterOption.value = null
|
|
||||||
scoreFilterOption.value = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onUpdateFilter = (filters, sourceColumn) => {
|
const onUpdateFilter = (filters, sourceColumn) => {
|
||||||
switch (filterType.value) {
|
|
||||||
case filterOption[0].value:
|
|
||||||
// filter value
|
|
||||||
valueFilterOption.value = filters[sourceColumn.key]
|
valueFilterOption.value = filters[sourceColumn.key]
|
||||||
break
|
|
||||||
case filterOption[1].value:
|
|
||||||
// filter score
|
|
||||||
scoreFilterOption.value = filters[sourceColumn.key]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onFormatChanged = (selDecode, selFormat) => {
|
const onFormatChanged = (selDecode, selFormat) => {
|
||||||
emit('reload', selDecode, selFormat)
|
emit('reload', selDecode, selFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const searchInputRef = ref(null)
|
||||||
defineExpose({
|
defineExpose({
|
||||||
reset: () => {
|
reset: () => {
|
||||||
clearFilter()
|
|
||||||
resetEdit()
|
resetEdit()
|
||||||
|
searchInputRef.value?.reset()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -360,25 +325,10 @@ defineExpose({
|
||||||
@rename="emit('rename')" />
|
@rename="emit('rename')" />
|
||||||
<div class="tb2 value-item-part flex-box-h">
|
<div class="tb2 value-item-part flex-box-h">
|
||||||
<div class="flex-box-h">
|
<div class="flex-box-h">
|
||||||
<n-input-group>
|
<content-search-input
|
||||||
<n-select
|
ref="searchInputRef"
|
||||||
v-model:value="filterType"
|
@filter-changed="onFilterInput"
|
||||||
:consistent-menu-width="false"
|
@match-changed="onMatchInput" />
|
||||||
:options="filterOption"
|
|
||||||
style="width: 120px"
|
|
||||||
@update:value="onChangeFilterType" />
|
|
||||||
<n-tooltip :delay="500" :disabled="filterType !== 2">
|
|
||||||
<template #trigger>
|
|
||||||
<n-input
|
|
||||||
v-model:value="filterValue"
|
|
||||||
:placeholder="$t('interface.search')"
|
|
||||||
clearable
|
|
||||||
@clear="clearFilter"
|
|
||||||
@update:value="onFilterInput" />
|
|
||||||
</template>
|
|
||||||
<div class="text-block">{{ $t('interface.score_filter_tip') }}</div>
|
|
||||||
</n-tooltip>
|
|
||||||
</n-input-group>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-item-expand"></div>
|
<div class="flex-item-expand"></div>
|
||||||
<n-button-group>
|
<n-button-group>
|
||||||
|
|
|
@ -82,6 +82,7 @@
|
||||||
"unpin_edit": "Cancel Pin",
|
"unpin_edit": "Cancel Pin",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"full_search": "Full Search",
|
"full_search": "Full Search",
|
||||||
|
"full_search_result": "The content has been matched as '*{pattern}*'",
|
||||||
"filter_field": "Filter Field",
|
"filter_field": "Filter Field",
|
||||||
"filter_value": "Filter Value",
|
"filter_value": "Filter Value",
|
||||||
"length": "Length",
|
"length": "Length",
|
||||||
|
|
|
@ -81,7 +81,8 @@
|
||||||
"pin_edit": "固定编辑框(保存后不关闭)",
|
"pin_edit": "固定编辑框(保存后不关闭)",
|
||||||
"unpin_edit": "取消固定",
|
"unpin_edit": "取消固定",
|
||||||
"search": "搜索",
|
"search": "搜索",
|
||||||
"full_search": "全文搜索",
|
"full_search": "全文匹配",
|
||||||
|
"full_search_result": "内容已匹配为 *{pattern}*",
|
||||||
"filter_field": "筛选字段",
|
"filter_field": "筛选字段",
|
||||||
"filter_value": "筛选值",
|
"filter_value": "筛选值",
|
||||||
"length": "长度",
|
"length": "长度",
|
||||||
|
|
Loading…
Reference in New Issue