feat(admin): 用户管理新增分组列、分组筛选与专属分组一键替换
- 新增分组列:展示用户的专属/公开分组,支持 hover 查看详情 - 新增分组筛选:下拉选择或模糊搜索分组名过滤用户 - 专属分组替换:点击专属分组弹出操作菜单,选择目标分组后 自动授予新分组权限、迁移绑定的 Key、移除旧分组权限 - 后端新增 POST /admin/users/:id/replace-group 端点,事务内 完成分组替换并失效认证缓存
This commit is contained in:
@@ -79,7 +79,8 @@
|
||||
'sticky-header-cell py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dark-400',
|
||||
getAdaptivePaddingClass(),
|
||||
{ 'cursor-pointer hover:bg-gray-100 dark:hover:bg-dark-700': column.sortable },
|
||||
getStickyColumnClass(column, index)
|
||||
getStickyColumnClass(column, index),
|
||||
column.class
|
||||
]"
|
||||
@click="column.sortable && handleSort(column.key)"
|
||||
>
|
||||
@@ -168,7 +169,8 @@
|
||||
:class="[
|
||||
'whitespace-nowrap py-4 text-sm text-gray-900 dark:text-gray-100',
|
||||
getAdaptivePaddingClass(),
|
||||
getStickyColumnClass(column, colIndex)
|
||||
getStickyColumnClass(column, colIndex),
|
||||
column.class
|
||||
]"
|
||||
>
|
||||
<slot :name="`cell-${column.key}`"
|
||||
|
||||
@@ -77,7 +77,13 @@
|
||||
]"
|
||||
>
|
||||
<slot name="option" :option="option" :selected="isSelected(option)">
|
||||
<span class="select-option-label">{{ getOptionLabel(option) }}</span>
|
||||
<Icon
|
||||
v-if="option._creatable"
|
||||
name="search"
|
||||
size="sm"
|
||||
class="flex-shrink-0 text-gray-400"
|
||||
/>
|
||||
<span class="select-option-label" :class="option._creatable && 'italic text-gray-500 dark:text-dark-300'">{{ getOptionLabel(option) }}</span>
|
||||
<Icon
|
||||
v-if="isSelected(option)"
|
||||
name="check"
|
||||
@@ -127,6 +133,8 @@ interface Props {
|
||||
emptyText?: string
|
||||
valueKey?: string
|
||||
labelKey?: string
|
||||
creatable?: boolean
|
||||
creatablePrefix?: string
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
@@ -138,6 +146,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
disabled: false,
|
||||
error: false,
|
||||
searchable: false,
|
||||
creatable: false,
|
||||
creatablePrefix: '',
|
||||
valueKey: 'value',
|
||||
labelKey: 'label'
|
||||
})
|
||||
@@ -217,6 +227,10 @@ const selectedLabel = computed(() => {
|
||||
if (selectedOption.value) {
|
||||
return getOptionLabel(selectedOption.value)
|
||||
}
|
||||
// In creatable mode, show the raw value if no matching option
|
||||
if (props.creatable && props.modelValue) {
|
||||
return String(props.modelValue)
|
||||
}
|
||||
return placeholderText.value
|
||||
})
|
||||
|
||||
@@ -231,6 +245,12 @@ const filteredOptions = computed(() => {
|
||||
if (opt.description && String(opt.description).toLowerCase().includes(query)) return true
|
||||
return false
|
||||
})
|
||||
// In creatable mode, always prepend a fuzzy search option
|
||||
if (props.creatable && searchQuery.value.trim()) {
|
||||
const trimmed = searchQuery.value.trim()
|
||||
const prefix = props.creatablePrefix || t('common.search')
|
||||
opts = [{ [props.valueKey]: trimmed, [props.labelKey]: `${prefix} "${trimmed}"`, _creatable: true }, ...opts]
|
||||
}
|
||||
}
|
||||
return opts
|
||||
})
|
||||
|
||||
@@ -6,5 +6,6 @@ export interface Column {
|
||||
key: string
|
||||
label: string
|
||||
sortable?: boolean
|
||||
class?: string
|
||||
formatter?: (value: any, row: any) => string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user