feat(admin): polish badge, OAuth and payment gateway settings UX

- Badge page: rewrite as collapsible sortable category/item cards with
  preset medal icon picker, dirty-tracked save bar and duplicate slug check;
  drop user_badge_* dual-read and legacy name/icon_url fallbacks
- OAuth: provider config groups collapsible, expanded when provider is on;
  clogin visibility moved to group-level dependsOn
- Gateway: expandWhen accepts dependsOn-style rules; select-gated groups no
  longer always expand ('null' was truthy); mark five gateway secrets as
  secret/password in JS and PHP; class-based notice list styling
- Remove unused UserRuntime::parse_badge_dictionary

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-10-28 15:33:49 +08:00
1 parent 116b6b2223
commit 3179754670
13 files changed
+502 -239

No files matched your search

+95 -168
View File
@@ -1,9 +1,6 @@
<template>
<div class="aksrt-page-container">
<PageHeader
title="用户徽章"
desc="配置勋章分类、自动授予条件(get_type / get_val)与公开展示。路由 /users/badge,存储键 user_medal_*。"
>
<PageHeader title="用户徽章" desc="配置勋章分类、自动授予条件与前台公开展示。">
<template #actions>
<n-space>
<n-button quaternary @click="go('/users/notifications')">消息通知</n-button>
@@ -13,23 +10,6 @@
</PageHeader>
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<h3>当前摘要</h3>
</div>
<n-space>
<n-tag round :type="form.user_medal_s ? 'success' : 'default'">
徽章系统:{{ form.user_medal_s ? '已启用' : '已关闭' }}
</n-tag>
<n-tag round :type="form.user_medal_public_display ? 'info' : 'default'">
公开展示:{{ form.user_medal_public_display ? '开启' : '关闭' }}
</n-tag>
<n-tag round type="info">最多显示:{{ form.user_medal_limit }} 枚</n-tag>
<n-tag round type="info">分类:{{ form.user_medal_args.length }}</n-tag>
<n-tag round type="info">条目:{{ itemCount }}</n-tag>
</n-space>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<h3>徽章展示</h3>
@@ -38,81 +18,57 @@
<n-form-item label="启用徽章/勋章系统">
<n-switch v-model:value="form.user_medal_s" />
</n-form-item>
<n-form-item v-if="form.user_medal_s" label="公开页显示徽章">
<n-switch v-model:value="form.user_medal_public_display" />
</n-form-item>
<n-form-item v-if="form.user_medal_s" label="最多显示数量">
<n-input-number v-model:value="form.user_medal_limit" :min="1" :max="8" style="width: 180px;" />
</n-form-item>
<template v-if="form.user_medal_s">
<n-form-item label="公开页显示徽章">
<n-switch v-model:value="form.user_medal_public_display" />
</n-form-item>
<n-form-item label="最多显示数量">
<n-input-number v-model:value="form.user_medal_limit" :min="1" :max="8" style="width: 180px;">
<template #suffix>枚</template>
</n-input-number>
</n-form-item>
</template>
</n-form>
</n-card>
<n-card v-if="form.user_medal_s" class="aksrt-section-card">
<div class="aksrt-section-header" style="display:flex;justify-content:space-between;align-items:center;">
<h3>徽章明细(分类 → 条目)</h3>
<n-button size="small" @click="addCategory">添加分类</n-button>
<div class="aksrt-section-header users-badge__header">
<h3>徽章明细</h3>
<span class="users-badge__stats">{{ form.user_medal_args.length }} 个分类 · {{ itemCount }} 枚徽章</span>
</div>
<p class="users-badge__hint">
拖动手柄调整分类与条目顺序,点击行头展开编辑。获取条件选择「仅手动授予」时不自动发放。
</p>
<n-empty v-if="!form.user_medal_args.length" description="暂无分类,请添加" />
<div v-for="(cat, ci) in form.user_medal_args" :key="'cat-' + ci" class="medal-cat-block">
<n-card size="small" embedded>
<n-space vertical :size="12">
<n-space align="center">
<n-input v-model:value="cat.cat_name" placeholder="分类名称" style="width: 220px;" />
<n-button size="small" @click="addItem(ci)">添加条目</n-button>
<n-button size="small" quaternary type="error" @click="removeCategory(ci)">删除分类</n-button>
</n-space>
<n-empty v-if="!(cat.items && cat.items.length)" description="该分类暂无条目" />
<div v-for="(item, ii) in cat.items || []" :key="'item-' + ci + '-' + ii" class="medal-item-row">
<n-grid cols="1 s:2 m:3" :x-gap="12" :y-gap="8" responsive="screen">
<n-grid-item>
<n-input v-model:value="item.slug" placeholder="slug" />
</n-grid-item>
<n-grid-item>
<n-input v-model:value="item.name" placeholder="名称" />
</n-grid-item>
<n-grid-item>
<n-select v-model:value="item.get_type" :options="getTypeOptions" placeholder="获取条件" />
</n-grid-item>
<n-grid-item>
<n-input-number
v-model:value="item.get_val"
:min="0"
:disabled="item.get_type === 'manually_add'"
placeholder="阈值"
style="width: 100%;"
/>
</n-grid-item>
<n-grid-item>
<n-input v-model:value="item.icon" placeholder="图标 URL" />
</n-grid-item>
<n-grid-item>
<n-input v-model:value="item.desc" placeholder="描述" />
</n-grid-item>
</n-grid>
<div style="margin-top: 8px; text-align: right;">
<n-button size="tiny" quaternary type="error" @click="removeItem(ci, ii)">删除条目</n-button>
</div>
</div>
</n-space>
</n-card>
<div v-if="!form.user_medal_args.length" class="aksrt-sortable__empty">
<i class="fa fas fa-trophy" aria-hidden="true" />
<span>暂无分类,请添加</span>
</div>
</n-card>
<draggable
v-else
v-model="form.user_medal_args"
v-bind="SORTABLE_LIST_ATTRS"
:item-key="rowKey"
class="aksrt-sortable"
>
<template #item="{ element, index }">
<MedalCategoryCard
:category="element"
:index="index"
:open="cats.isOpen(rowKey(element))"
:head-attrs="cats.headAttrs(rowKey(element))"
@remove="form.user_medal_args.splice(index, 1)"
/>
</template>
</draggable>
<n-card class="aksrt-section-card">
<n-alert type="info" :show-icon="false">
前台已授予徽章保存在用户 meta aksrt_user_badges;佩戴为 aksrt_wear_badge(slug)。
字典来自 user_medal_args;运行时 AKSRT_User_Badges 读取 identity badge.dictionary 派生列表。
</n-alert>
<n-button class="users-badge__add" dashed block @click="addCategory">
<i class="fa fas fa-plus" aria-hidden="true" />&nbsp;添加分类
</n-button>
</n-card>
<div class="aksrt-form-actions">
<n-button type="primary" :loading="saving" @click="handleSave">保存配置</n-button>
</div>
</n-spin>
<AksSaveBar :dirty="dirty" :saving="saving" @save="handleSave" @reload="load" />
</div>
</template>
@@ -120,16 +76,22 @@
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useMessage } from 'naive-ui'
import draggable from 'vuedraggable'
import api from '@/api'
import PageHeader from '@/components/PageHeader.vue'
import { MEDAL_GET_TYPES, defaultMedalArgs } from '@/schema/sections/users/badge.js'
import AksSaveBar from '@/components/form/AksSaveBar.vue'
import { SORTABLE_LIST_ATTRS } from '@/components/form/sortable-options.js'
import { useCollapsibleItems } from '@/components/form/useCollapsibleItems.js'
import { defaultMedalArgs } from '@/schema/sections/users/badge.js'
import { emptyCategory, findInvalidItem, normalizeArgs, rowKey } from './partials/badge/badge-model.js'
import MedalCategoryCard from './partials/badge/MedalCategoryCard.vue'
const router = useRouter()
const message = useMessage()
const cats = useCollapsibleItems()
const loading = ref(true)
const saving = ref(false)
const getTypeOptions = MEDAL_GET_TYPES
const form = reactive({
user_medal_s: false,
@@ -138,82 +100,38 @@ const form = reactive({
user_medal_args: defaultMedalArgs(),
})
const itemCount = computed(() =>
form.user_medal_args.reduce((n, c) => n + (Array.isArray(c.items) ? c.items.length : 0), 0),
)
// 与全局保存栏一致:仅在有未保存更改时显示。以最近一次加载/保存的快照为基准。
const snapshot = ref('')
const serialize = () => JSON.stringify(form)
const dirty = computed(() => !loading.value && snapshot.value !== '' && serialize() !== snapshot.value)
function markClean() {
snapshot.value = serialize()
}
const itemCount = computed(() => form.user_medal_args.reduce((n, c) => n + c.items.length, 0))
function go(path) {
router.push(path)
}
function extractData(response) {
return response?.data ?? response ?? {}
}
function emptyItem() {
return {
slug: '',
name: '',
desc: '',
icon: '',
get_type: 'manually_add',
get_val: 0,
}
}
function normalizeArgs(raw) {
if (!Array.isArray(raw)) {
return defaultMedalArgs()
}
return raw.map((cat) => ({
cat_name: String(cat?.cat_name || cat?.name || '未命名').trim() || '未命名',
items: Array.isArray(cat?.items)
? cat.items.map((it) => ({
slug: String(it?.slug || '').trim(),
name: String(it?.name || '').trim(),
desc: String(it?.desc || '').trim(),
icon: String(it?.icon || it?.icon_url || '').trim(),
get_type: String(it?.get_type || 'manually_add').trim() || 'manually_add',
get_val: Math.max(0, Number(it?.get_val ?? 0) || 0),
}))
: [],
}))
}
function addCategory() {
form.user_medal_args.push({ cat_name: '新分类', items: [emptyItem()] })
form.user_medal_args.push(emptyCategory())
cats.toggle(rowKey(form.user_medal_args[form.user_medal_args.length - 1]))
}
function removeCategory(ci) {
form.user_medal_args.splice(ci, 1)
}
function addItem(ci) {
if (!form.user_medal_args[ci].items) {
form.user_medal_args[ci].items = []
}
form.user_medal_args[ci].items.push(emptyItem())
}
function removeItem(ci, ii) {
form.user_medal_args[ci].items.splice(ii, 1)
function applySettings(data) {
form.user_medal_s = !!data.user_medal_s
form.user_medal_public_display = data.user_medal_public_display !== false
form.user_medal_limit = Number(data.user_medal_limit ?? 3)
form.user_medal_args = normalizeArgs(data.user_medal_args ?? null)
}
async function load() {
loading.value = true
try {
const schemaRes = await api.getSchemaPage('users', 'badge')
const schemaData = extractData(schemaRes)
form.user_medal_s = !!(schemaData.user_medal_s ?? schemaData.user_badge_enabled)
form.user_medal_public_display =
schemaData.user_medal_public_display !== undefined
? schemaData.user_medal_public_display !== false
: schemaData.user_badge_public_display !== false
form.user_medal_limit = Number(
schemaData.user_medal_limit ?? schemaData.user_badge_limit ?? 3,
)
form.user_medal_args = normalizeArgs(schemaData.user_medal_args ?? null)
const res = await api.getSchemaPage('users', 'badge')
applySettings(res?.data ?? res ?? {})
markClean()
} catch (error) {
message.error(error.message || '加载用户徽章配置失败')
} finally {
@@ -222,18 +140,14 @@ async function load() {
}
async function handleSave() {
const args = normalizeArgs(form.user_medal_args)
const invalid = form.user_medal_s ? findInvalidItem(args) : ''
if (invalid) {
message.error(invalid)
return
}
saving.value = true
try {
const args = normalizeArgs(form.user_medal_args)
for (const cat of args) {
for (const it of cat.items) {
if (!it.slug || !it.name || !it.get_type) {
message.error('每个条目需要 slug、名称和获取条件')
saving.value = false
return
}
}
}
await api.saveSchemaPage('users', 'badge', {
user_medal_s: !!form.user_medal_s,
user_medal_public_display: !!form.user_medal_public_display,
@@ -241,6 +155,7 @@ async function handleSave() {
user_medal_args: args,
})
form.user_medal_args = args
markClean()
message.success('用户徽章配置已保存')
} catch (error) {
message.error(error.message || '保存失败')
@@ -253,13 +168,25 @@ onMounted(load)
</script>
<style scoped>
.medal-cat-block {
margin-bottom: 16px;
.users-badge__header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}
.medal-item-row {
padding: 12px;
border: 1px solid var(--n-border-color, #efeff5);
border-radius: 8px;
margin-bottom: 8px;
.users-badge__stats {
color: var(--aksrt-color-muted, #6F7A72);
font-size: 12px;
}
.users-badge__hint {
margin: 0 0 12px;
color: var(--aksrt-color-muted, #6F7A72);
font-size: 13px;
}
.users-badge__add {
margin-top: 12px;
}
</style>
@@ -0,0 +1,97 @@
<template>
<div class="aksrt-sortable__item">
<div class="aksrt-sortable__head" v-bind="headAttrs">
<AksSortHandle @click.stop />
<span class="aksrt-sortable__index">{{ index + 1 }}</span>
<div class="aksrt-sortable__main">
<div class="aksrt-sortable__title-row">
<span class="aksrt-sortable__title">{{ category.cat_name || '未命名分类' }}</span>
<n-tag size="small" :bordered="false">{{ category.items.length }} 枚</n-tag>
</div>
<span class="aksrt-sortable__desc">{{ previewNames || '该分类暂无条目' }}</span>
</div>
<div class="aksrt-sortable__actions">
<n-popconfirm @positive-click="emit('remove')">
<template #trigger>
<n-button size="tiny" quaternary type="error" title="删除分类" @click.stop>
<i class="fa fas fa-trash-alt" aria-hidden="true" />
</n-button>
</template>
删除分类「{{ category.cat_name || '未命名分类' }}」及其全部条目?
</n-popconfirm>
</div>
<i class="fa fas aksrt-sortable__toggle" :class="open ? 'fa-chevron-down' : 'fa-chevron-right'" aria-hidden="true" />
</div>
<div v-if="open" class="aksrt-sortable__body">
<n-form label-placement="left" label-width="96" class="aksrt-form">
<n-form-item label="分类名称">
<n-input v-model:value="category.cat_name" placeholder="如:成长" style="max-width: 320px;" />
</n-form-item>
</n-form>
<div v-if="!category.items.length" class="aksrt-sortable__empty">
<i class="fa fas fa-medal" aria-hidden="true" />
<span>该分类暂无条目</span>
</div>
<draggable
v-else
v-model="category.items"
v-bind="SORTABLE_LIST_ATTRS"
:item-key="rowKey"
class="aksrt-sortable aksrt-sortable--compact"
>
<template #item="{ element, index: ii }">
<MedalItemCard
:item="element"
:open="items.isOpen(rowKey(element))"
:head-attrs="items.headAttrs(rowKey(element))"
@remove="category.items.splice(ii, 1)"
/>
</template>
</draggable>
<n-button class="medal-category-card__add" size="small" dashed block @click="addItem">
<i class="fa fas fa-plus" aria-hidden="true" />&nbsp;添加条目
</n-button>
</div>
</div>
</template>
<script setup>
/** 徽章分类卡片:行头显示条目数与名称预览,展开后编辑分类名并拖拽排序条目。 */
import { computed } from 'vue'
import draggable from 'vuedraggable'
import AksSortHandle from '@/components/form/AksSortHandle.vue'
import { SORTABLE_LIST_ATTRS } from '@/components/form/sortable-options.js'
import { useCollapsibleItems } from '@/components/form/useCollapsibleItems.js'
import { emptyItem, rowKey } from './badge-model.js'
import MedalItemCard from './MedalItemCard.vue'
const props = defineProps({
category: { type: Object, required: true },
index: { type: Number, required: true },
open: { type: Boolean, default: false },
headAttrs: { type: Object, required: true },
})
const emit = defineEmits(['remove'])
const items = useCollapsibleItems()
const previewNames = computed(() =>
props.category.items.map((it) => it.name || it.slug).filter(Boolean).slice(0, 4).join('、'),
)
function addItem() {
const item = emptyItem()
props.category.items.push(item)
// 新条目直接展开,方便立即填写。
items.toggle(rowKey(props.category.items[props.category.items.length - 1]))
}
</script>
<style scoped>
.medal-category-card__add {
margin-top: 10px;
}
</style>
@@ -0,0 +1,82 @@
<template>
<div class="medal-icon-picker">
<div class="medal-icon-picker__presets" role="listbox" aria-label="预设勋章图标">
<button
v-for="(src, i) in MEDAL_PRESET_ICONS"
:key="src"
type="button"
role="option"
class="medal-icon-picker__preset"
:class="{ 'is-active': src === modelValue }"
:aria-selected="src === modelValue ? 'true' : 'false'"
:title="`预设图标 ${i + 1}`"
@click="emit('update:modelValue', src)"
>
<img :src="src" :alt="`预设图标 ${i + 1}`" loading="lazy" />
</button>
</div>
<AksMediaField
:model-value="modelValue"
placeholder="或从媒体库选择自定义图标"
@update:model-value="emit('update:modelValue', $event)"
/>
</div>
</template>
<script setup>
/** 勋章图标选择:主题预设 SVG 一键选用,或走媒体库 / 直接填 URL。 */
import AksMediaField from '@/components/form/AksMediaField.vue'
import { MEDAL_PRESET_ICONS } from '@/schema/sections/users/badge.js'
defineProps({
modelValue: { type: String, default: '' },
})
const emit = defineEmits(['update:modelValue'])
</script>
<style scoped>
.medal-icon-picker {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
}
.medal-icon-picker__presets {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(36px, 1fr));
gap: 6px;
max-width: 520px;
}
.medal-icon-picker__preset {
display: inline-flex;
align-items: center;
justify-content: center;
height: 36px;
padding: 0;
border: 1px solid var(--aksrt-color-border, #E2E8E3);
border-radius: 8px;
background: var(--aksrt-color-surface, #fff);
cursor: pointer;
transition: border-color .15s ease, background-color .15s ease;
}
.medal-icon-picker__preset img {
width: 22px;
height: 22px;
object-fit: contain;
}
.medal-icon-picker__preset:hover,
.medal-icon-picker__preset:focus-visible {
border-color: var(--aksrt-color-primary, #18A058);
outline: none;
}
.medal-icon-picker__preset.is-active {
border-color: var(--aksrt-color-primary, #18A058);
background: var(--aksrt-color-primary-soft, #EAF7EF);
box-shadow: 0 0 0 1px var(--aksrt-color-primary, #18A058);
}
</style>
@@ -0,0 +1,79 @@
<template>
<div class="aksrt-sortable__item">
<div class="aksrt-sortable__head" v-bind="headAttrs">
<AksSortHandle @click.stop />
<span class="aksrt-sortable__icon">
<img v-if="item.icon" :src="item.icon" alt="" />
<i v-else class="fa fas fa-medal" aria-hidden="true" />
</span>
<div class="aksrt-sortable__main">
<div class="aksrt-sortable__title-row">
<span class="aksrt-sortable__title">{{ item.name || '未命名徽章' }}</span>
<n-tag size="small" :bordered="false" :type="item.get_type === 'manually_add' ? 'default' : 'info'">
{{ conditionSummary(item) }}
</n-tag>
<n-tag v-if="!item.slug" size="small" type="warning" :bordered="false">缺少 slug</n-tag>
</div>
<span class="aksrt-sortable__desc">{{ item.slug || '—' }}{{ item.desc ? ` · ${item.desc}` : '' }}</span>
</div>
<div class="aksrt-sortable__actions">
<n-button size="tiny" quaternary type="error" title="删除条目" @click.stop="emit('remove')">
<i class="fa fas fa-trash-alt" aria-hidden="true" />
</n-button>
</div>
<i class="fa fas aksrt-sortable__toggle" :class="open ? 'fa-chevron-down' : 'fa-chevron-right'" aria-hidden="true" />
</div>
<div v-if="open" class="aksrt-sortable__body">
<n-form label-placement="left" label-width="96" class="aksrt-form medal-item-card__form">
<n-grid cols="1 m:2" :x-gap="16" responsive="screen">
<n-form-item-gi label="名称">
<n-input v-model:value="item.name" placeholder="如:签到达人" />
</n-form-item-gi>
<n-form-item-gi label="slug">
<n-input v-model:value="item.slug" placeholder="唯一标识,如 checkin-30" />
</n-form-item-gi>
<n-form-item-gi label="获取条件">
<n-select v-model:value="item.get_type" :options="MEDAL_GET_TYPES" />
</n-form-item-gi>
<n-form-item-gi label="达成阈值">
<n-input-number
v-model:value="item.get_val"
:min="0"
:disabled="item.get_type === 'manually_add'"
:placeholder="item.get_type === 'manually_add' ? '手动授予无需阈值' : '达到该数值自动授予'"
style="width: 100%;"
/>
</n-form-item-gi>
<n-form-item-gi :span="2" label="描述">
<n-input v-model:value="item.desc" placeholder="前台展示的获取说明" />
</n-form-item-gi>
<n-form-item-gi :span="2" label="图标">
<MedalIconPicker v-model="item.icon" />
</n-form-item-gi>
</n-grid>
</n-form>
</div>
</div>
</template>
<script setup>
/** 徽章条目卡片:行头摘要(图标 / 名称 / 条件),展开后编辑字段。条目对象由父级直接持有并原地修改。 */
import AksSortHandle from '@/components/form/AksSortHandle.vue'
import { MEDAL_GET_TYPES } from '@/schema/sections/users/badge.js'
import { conditionSummary } from './badge-model.js'
import MedalIconPicker from './MedalIconPicker.vue'
defineProps({
item: { type: Object, required: true },
open: { type: Boolean, default: false },
headAttrs: { type: Object, required: true },
})
const emit = defineEmits(['remove'])
</script>
<style scoped>
.medal-item-card__form :deep(.n-form-item) {
margin-bottom: 4px;
}
</style>
@@ -0,0 +1,63 @@
/**
* 用户徽章编辑页的数据模型:空条目、规范化与拖拽用的稳定行 key。
*/
import { MEDAL_GET_TYPES, defaultMedalArgs } from '@/schema/sections/users/badge.js'
const GET_TYPE_LABELS = Object.fromEntries(MEDAL_GET_TYPES.map((t) => [t.value, t.label]))
export function getTypeLabel(type) {
return GET_TYPE_LABELS[type] || '未设置条件'
}
export function conditionSummary(item) {
if (!item.get_type) return '未设置条件'
if (item.get_type === 'manually_add') return getTypeLabel(item.get_type)
return `${getTypeLabel(item.get_type)} ≥ ${Number(item.get_val || 0)}`
}
export function emptyItem() {
return { slug: '', name: '', desc: '', icon: '', get_type: 'manually_add', get_val: 0 }
}
export function emptyCategory() {
return { cat_name: '新分类', items: [emptyItem()] }
}
export function normalizeArgs(raw) {
if (!Array.isArray(raw)) return defaultMedalArgs()
return raw.map((cat) => ({
cat_name: String(cat?.cat_name || '').trim() || '未命名',
items: Array.isArray(cat?.items)
? cat.items.map((it) => ({
slug: String(it?.slug || '').trim(),
name: String(it?.name || '').trim(),
desc: String(it?.desc || '').trim(),
icon: String(it?.icon || '').trim(),
get_type: String(it?.get_type || 'manually_add').trim() || 'manually_add',
get_val: Math.max(0, Number(it?.get_val ?? 0) || 0),
}))
: [],
}))
}
/** 首个不完整条目的位置描述;全部完整时返回空字符串。 */
export function findInvalidItem(args) {
const slugs = new Set()
for (const cat of args) {
for (const it of cat.items) {
const where = `「${cat.cat_name}」中的条目「${it.name || it.slug || '未命名'}」`
if (!it.slug || !it.name || !it.get_type) return `${where}缺少 slug、名称或获取条件`
if (slugs.has(it.slug)) return `${where}的 slug「${it.slug}」重复`
slugs.add(it.slug)
}
}
return ''
}
// 对象 → 稳定 key:拖拽换位后展开状态与 DOM 跟随对象本身。
const rowKeys = new WeakMap()
let rowSeq = 0
export function rowKey(obj) {
if (!rowKeys.has(obj)) rowKeys.set(obj, `row-${++rowSeq}`)
return rowKeys.get(obj)
}