feat(dict): apply dynamic dictionaries across user, menu and log pages

This commit is contained in:
Leo 2025-09-27 00:54:38 +08:00
parent d50611c05b
commit 7c1bf63133
5 changed files with 153 additions and 187 deletions

View File

@ -51,10 +51,7 @@
v-model:value="searchForm.loginStatus" v-model:value="searchForm.loginStatus"
placeholder="请选择登录状态" placeholder="请选择登录状态"
clearable clearable
:options="[ :options="loginStatusOptions"
{ label: '成功', value: '0' },
{ label: '失败', value: '1' },
]"
/> />
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
@ -196,12 +193,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { h, nextTick, onMounted, ref } from 'vue' import { computed, h, nextTick, onMounted, ref } from 'vue'
import type { DataTableColumns, FormInst } from 'naive-ui' import type { DataTableColumns, FormInst } from 'naive-ui'
import { NButton, NIcon, NPopconfirm, NSpace, NTag } from 'naive-ui' import { NButton, NIcon, NPopconfirm, NSpace } from 'naive-ui'
import IconParkOutlineDelete from '~icons/icon-park-outline/delete' import IconParkOutlineDelete from '~icons/icon-park-outline/delete'
import CoiEmpty from '@/components/common/CoiEmpty.vue' import CoiEmpty from '@/components/common/CoiEmpty.vue'
import CoiPagination from '@/components/common/CoiPagination.vue' import CoiPagination from '@/components/common/CoiPagination.vue'
import DictTag from '@/components/common/DictTag.vue'
import { import {
batchDeleteLoginLog, batchDeleteLoginLog,
deleteLoginLog, deleteLoginLog,
@ -210,10 +208,21 @@ import {
import type { LoginLogVo } from '@/service/api/system/loginlog' import type { LoginLogVo } from '@/service/api/system/loginlog'
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi' import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
import { PERMISSIONS } from '@/constants/permissions' import { PERMISSIONS } from '@/constants/permissions'
import { usePermission } from '@/hooks/usePermission' import { useDict, usePermission } from '@/hooks'
// //
const { hasPermission } = usePermission() const { hasPermission } = usePermission()
const { dictOptions } = useDict(['sys_common_status'])
const loginStatusOptions = computed(() => {
const options = dictOptions.sys_common_status || []
return options
.filter(option => option.dictValue === '1' || option.dictValue === '2')
.map(option => ({
label: option.dictLabel,
value: option.dictValue === '1' ? '0' : '1',
}))
})
// //
interface LoginLogSearchForm { interface LoginLogSearchForm {
@ -320,17 +329,9 @@ const columns: DataTableColumns<LoginLogVo> = [
{ {
title: '登录状态', title: '登录状态',
key: 'loginStatus', key: 'loginStatus',
width: 100, width: 110,
align: 'center', align: 'center',
render: (row) => { render: row => h(DictTag, { dictType: 'sys_common_status', value: row.loginStatus === '0' ? '1' : '2' }),
const isSuccess = row.loginStatus === '0'
return h(NTag, {
type: isSuccess ? 'success' : 'error',
size: 'small',
}, {
default: () => isSuccess ? '操作成功' : '操作失败',
})
},
}, },
{ {
title: '登录信息', title: '登录信息',

View File

@ -40,10 +40,7 @@
v-model:value="searchForm.menuStatus" v-model:value="searchForm.menuStatus"
placeholder="请选择菜单状态" placeholder="请选择菜单状态"
clearable clearable
:options="[ :options="getSelectOptions('sys_switch_status')"
{ label: '启用', value: '0' },
{ label: '停用', value: '1' },
]"
/> />
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
@ -235,7 +232,7 @@
:style="formData.parentId === '0' ? { color: themeColors.primary, fontWeight: '500' } : {}" :style="formData.parentId === '0' ? { color: themeColors.primary, fontWeight: '500' } : {}"
@click="handleSelectParent('0', '最顶级菜单')" @click="handleSelectParent('0', '最顶级菜单')"
> >
<n-radio <NRadio
:checked="formData.parentId === '0'" :checked="formData.parentId === '0'"
style="pointer-events: none;" style="pointer-events: none;"
/> />
@ -263,7 +260,7 @@
@click="handleSelectParent(menu.value, menu.label)" @click="handleSelectParent(menu.value, menu.label)"
@mouseenter="handleMenuHover(menu)" @mouseenter="handleMenuHover(menu)"
> >
<n-radio <NRadio
:checked="formData.parentId === menu.value" :checked="formData.parentId === menu.value"
style="pointer-events: none;" style="pointer-events: none;"
/> />
@ -303,7 +300,7 @@
:style="formData.parentId === submenu.value ? { color: themeColors.primary, fontWeight: '500' } : {}" :style="formData.parentId === submenu.value ? { color: themeColors.primary, fontWeight: '500' } : {}"
@click="handleSelectParent(submenu.value, submenu.label)" @click="handleSelectParent(submenu.value, submenu.label)"
> >
<n-radio <NRadio
:checked="formData.parentId === submenu.value" :checked="formData.parentId === submenu.value"
style="pointer-events: none;" style="pointer-events: none;"
/> />
@ -325,20 +322,18 @@
<!-- 菜单类型 --> <!-- 菜单类型 -->
<n-form-item label="菜单类型" path="menuType" class="mb-2"> <n-form-item label="菜单类型" path="menuType" class="mb-2">
<n-radio-group <NRadioGroup
v-model:value="formData.menuType" v-model:value="formData.menuType"
@update:value="handleMenuTypeChange" @update:value="handleMenuTypeChange"
> >
<n-radio value="1"> <NRadio
目录 v-for="item in dictOptions.sys_menu_type || []"
</n-radio> :key="item.dictValue"
<n-radio value="2"> :value="item.dictValue"
菜单 >
</n-radio> {{ item.dictLabel }}
<n-radio value="3"> </NRadio>
按钮 </NRadioGroup>
</n-radio>
</n-radio-group>
</n-form-item> </n-form-item>
<!-- 菜单图标 --> <!-- 菜单图标 -->
@ -392,14 +387,14 @@
<n-grid :cols="2" :x-gap="10" class="mb-2"> <n-grid :cols="2" :x-gap="10" class="mb-2">
<n-grid-item> <n-grid-item>
<n-form-item label="是否隐藏" path="isHide"> <n-form-item label="是否隐藏" path="isHide">
<n-radio-group v-model:value="formData.isHide"> <NRadioGroup v-model:value="formData.isHide">
<n-radio value="0"> <NRadio value="0">
</n-radio> </NRadio>
<n-radio value="1"> <NRadio value="1">
</n-radio> </NRadio>
</n-radio-group> </NRadioGroup>
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
<n-grid-item> <n-grid-item>
@ -438,14 +433,14 @@
<n-grid :cols="2" :x-gap="10" class="mb-2"> <n-grid :cols="2" :x-gap="10" class="mb-2">
<n-grid-item> <n-grid-item>
<n-form-item label="是否缓存" path="isKeepAlive"> <n-form-item label="是否缓存" path="isKeepAlive">
<n-radio-group v-model:value="formData.isKeepAlive"> <NRadioGroup v-model:value="formData.isKeepAlive">
<n-radio value="0"> <NRadio value="0">
</n-radio> </NRadio>
<n-radio value="1"> <NRadio value="1">
</n-radio> </NRadio>
</n-radio-group> </NRadioGroup>
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
<n-grid-item> <n-grid-item>
@ -462,26 +457,26 @@
<n-grid :cols="2" :x-gap="10" class="mb-2"> <n-grid :cols="2" :x-gap="10" class="mb-2">
<n-grid-item> <n-grid-item>
<n-form-item label="是否展开" path="isSpread"> <n-form-item label="是否展开" path="isSpread">
<n-radio-group v-model:value="formData.isSpread"> <NRadioGroup v-model:value="formData.isSpread">
<n-radio value="0"> <NRadio value="0">
</n-radio> </NRadio>
<n-radio value="1"> <NRadio value="1">
</n-radio> </NRadio>
</n-radio-group> </NRadioGroup>
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
<n-grid-item> <n-grid-item>
<n-form-item label="是否固钉" path="isAffix"> <n-form-item label="是否固钉" path="isAffix">
<n-radio-group v-model:value="formData.isAffix"> <NRadioGroup v-model:value="formData.isAffix">
<n-radio value="0"> <NRadio value="0">
</n-radio> </NRadio>
<n-radio value="1"> <NRadio value="1">
</n-radio> </NRadio>
</n-radio-group> </NRadioGroup>
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
</n-grid> </n-grid>
@ -492,26 +487,26 @@
<n-grid :cols="2" :x-gap="10" class="mb-2"> <n-grid :cols="2" :x-gap="10" class="mb-2">
<n-grid-item> <n-grid-item>
<n-form-item label="是否展开" path="isSpread"> <n-form-item label="是否展开" path="isSpread">
<n-radio-group v-model:value="formData.isSpread"> <NRadioGroup v-model:value="formData.isSpread">
<n-radio value="0"> <NRadio value="0">
</n-radio> </NRadio>
<n-radio value="1"> <NRadio value="1">
</n-radio> </NRadio>
</n-radio-group> </NRadioGroup>
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
<n-grid-item> <n-grid-item>
<n-form-item label="是否固钉" path="isAffix"> <n-form-item label="是否固钉" path="isAffix">
<n-radio-group v-model:value="formData.isAffix"> <NRadioGroup v-model:value="formData.isAffix">
<n-radio value="0"> <NRadio value="0">
</n-radio> </NRadio>
<n-radio value="1"> <NRadio value="1">
</n-radio> </NRadio>
</n-radio-group> </NRadioGroup>
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
</n-grid> </n-grid>
@ -535,7 +530,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, h, nextTick, onMounted, ref } from 'vue' import { computed, h, nextTick, onMounted, ref } from 'vue'
import type { DataTableColumns, FormInst, FormRules } from 'naive-ui' import type { DataTableColumns, FormInst, FormRules } from 'naive-ui'
import { NButton, NIcon, NPopconfirm, NSpace, NSwitch, NTag } from 'naive-ui' import { NButton, NIcon, NPopconfirm, NRadio, NRadioGroup, NSpace, NSwitch, NTag } from 'naive-ui'
import IconParkOutlineDelete from '~icons/icon-park-outline/delete' import IconParkOutlineDelete from '~icons/icon-park-outline/delete'
import IconParkOutlineEdit from '~icons/icon-park-outline/edit' import IconParkOutlineEdit from '~icons/icon-park-outline/edit'
import IconParkOutlinePlus from '~icons/icon-park-outline/plus' import IconParkOutlinePlus from '~icons/icon-park-outline/plus'
@ -543,8 +538,9 @@ import CoiDialog from '@/components/common/CoiDialog.vue'
import CoiEmpty from '@/components/common/CoiEmpty.vue' import CoiEmpty from '@/components/common/CoiEmpty.vue'
import CoiIcon from '@/components/common/CoiIcon.vue' import CoiIcon from '@/components/common/CoiIcon.vue'
import IconSelect from '@/components/common/IconSelect.vue' import IconSelect from '@/components/common/IconSelect.vue'
import DictTag from '@/components/common/DictTag.vue'
import { PERMISSIONS } from '@/constants/permissions' import { PERMISSIONS } from '@/constants/permissions'
import { usePermission } from '@/hooks' import { useDict, usePermission } from '@/hooks'
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi' import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
import { import {
addMenu, addMenu,
@ -587,6 +583,11 @@ const selectedRows = ref<MenuVo[]>([])
const expandedKeys = ref<string[]>([]) const expandedKeys = ref<string[]>([])
const isAllExpanded = ref(true) const isAllExpanded = ref(true)
const { dictOptions, getSelectOptions, getDictLabel } = useDict([
'sys_menu_type',
'sys_switch_status',
])
// //
const menuDialogRef = ref() const menuDialogRef = ref()
const formRef = ref<FormInst>() const formRef = ref<FormInst>()
@ -716,15 +717,7 @@ const columns: DataTableColumns<MenuVo> = [
key: 'menuType', key: 'menuType',
width: 100, width: 100,
align: 'center', align: 'center',
render: (row) => { render: row => h(DictTag, { dictType: 'sys_menu_type', value: row.menuType }),
const typeMap = {
1: { label: '目录', color: 'primary' },
2: { label: '菜单', color: 'info' },
3: { label: '按钮', color: 'warning' },
}
const type = typeMap[Number(row.menuType) as keyof typeof typeMap]
return h(NTag, { type: type.color }, { default: () => type.label })
},
}, },
{ {
title: '展开/折叠', title: '展开/折叠',
@ -818,6 +811,8 @@ const columns: DataTableColumns<MenuVo> = [
render: (row) => { render: (row) => {
return h(NSwitch, { return h(NSwitch, {
value: row.menuStatus === '0', value: row.menuStatus === '0',
checkedChildren: getDictLabel('sys_switch_status', '0', '启用'),
uncheckedChildren: getDictLabel('sys_switch_status', '1', '停用'),
onUpdateValue: value => handleStatusChange(row, value ? '0' : '1'), onUpdateValue: value => handleStatusChange(row, value ? '0' : '1'),
}) })
}, },
@ -1315,7 +1310,8 @@ async function handleStatusChange(menu: MenuVo, status: string) {
// //
updateMenuStatusInData(tableData.value, menu.menuId, status) updateMenuStatusInData(tableData.value, menu.menuId, status)
coiMsgSuccess('状态修改成功') const statusLabel = getDictLabel('sys_switch_status', status, status === '0' ? '启用' : '停用')
coiMsgSuccess(`状态已更新为「${statusLabel}`)
} }
catch { catch {
coiMsgError('状态修改失败') coiMsgError('状态修改失败')

View File

@ -40,10 +40,7 @@
v-model:value="searchForm.operStatus" v-model:value="searchForm.operStatus"
placeholder="请选择操作状态" placeholder="请选择操作状态"
clearable clearable
:options="[ :options="operStatusOptions"
{ label: '操作成功', value: '0' },
{ label: '操作失败', value: '1' },
]"
/> />
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
@ -233,9 +230,7 @@
{{ currentLogDetail.costTime || '-' }} {{ currentLogDetail.costTime || '-' }}
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="操作状态"> <n-descriptions-item label="操作状态">
<NTag :type="currentLogDetail.operStatus === '0' ? 'success' : 'error'" size="small"> <DictTag dict-type="sys_common_status" :value="currentLogDetail.operStatus === '0' ? '1' : '2'" />
{{ currentLogDetail.operStatus === '0' ? '操作成功' : '操作失败' }}
</NTag>
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item v-if="currentLogDetail.operParam" label="请求参数" :span="2"> <n-descriptions-item v-if="currentLogDetail.operParam" label="请求参数" :span="2">
<div class="json-container"> <div class="json-container">
@ -255,7 +250,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { h, nextTick, onMounted, ref } from 'vue' import { computed, h, nextTick, onMounted, ref } from 'vue'
import type { DataTableColumns, FormInst } from 'naive-ui' import type { DataTableColumns, FormInst } from 'naive-ui'
import { NButton, NIcon, NPopconfirm, NSpace, NTag } from 'naive-ui' import { NButton, NIcon, NPopconfirm, NSpace, NTag } from 'naive-ui'
import IconParkOutlineDelete from '~icons/icon-park-outline/delete' import IconParkOutlineDelete from '~icons/icon-park-outline/delete'
@ -263,6 +258,7 @@ import IconParkOutlinePreviewOpen from '~icons/icon-park-outline/preview-open'
import CoiDialog from '@/components/common/CoiDialog.vue' import CoiDialog from '@/components/common/CoiDialog.vue'
import CoiEmpty from '@/components/common/CoiEmpty.vue' import CoiEmpty from '@/components/common/CoiEmpty.vue'
import CoiPagination from '@/components/common/CoiPagination.vue' import CoiPagination from '@/components/common/CoiPagination.vue'
import DictTag from '@/components/common/DictTag.vue'
import { import {
batchDeleteOperLog, batchDeleteOperLog,
clearOperLog, clearOperLog,
@ -273,10 +269,21 @@ import {
import type { OperLogSearchForm, OperLogVo } from '@/service/api/system/operlog' import type { OperLogSearchForm, OperLogVo } from '@/service/api/system/operlog'
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi' import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
import { PERMISSIONS } from '@/constants/permissions' import { PERMISSIONS } from '@/constants/permissions'
import { usePermission } from '@/hooks/usePermission' import { useDict, usePermission } from '@/hooks'
// //
const { hasButton } = usePermission() const { hasButton } = usePermission()
const { dictOptions } = useDict(['sys_oper_type', 'sys_common_status'])
const operStatusOptions = computed(() => {
const options = dictOptions.sys_common_status || []
return options
.filter(option => option.dictValue === '1' || option.dictValue === '2')
.map(option => ({
label: option.dictLabel,
value: option.dictValue === '1' ? '0' : '1',
}))
})
// //
const loading = ref(false) const loading = ref(false)
@ -339,16 +346,9 @@ const columns: DataTableColumns<OperLogVo> = [
{ {
title: '操作类型', title: '操作类型',
key: 'operType', key: 'operType',
width: 90, width: 110,
align: 'center', align: 'center',
render: (row) => { render: row => h(DictTag, { dictType: 'sys_oper_type', value: row.operType }),
return h(NTag, {
type: 'primary',
size: 'small',
}, {
default: () => row.operType,
})
},
}, },
{ {
title: '操作人员[登录名/用户名]', title: '操作人员[登录名/用户名]',
@ -419,15 +419,7 @@ const columns: DataTableColumns<OperLogVo> = [
key: 'operStatus', key: 'operStatus',
width: 90, width: 90,
align: 'center', align: 'center',
render: (row) => { render: row => h(DictTag, { dictType: 'sys_common_status', value: row.operStatus === '0' ? '1' : '2' }),
const isSuccess = row.operStatus === '0'
return h(NTag, {
type: isSuccess ? 'success' : 'error',
size: 'small',
}, {
default: () => isSuccess ? '操作成功' : '操作失败',
})
},
}, },
{ {
title: '操作时间', title: '操作时间',

View File

@ -61,10 +61,7 @@
v-model:value="searchForm.roleStatus" v-model:value="searchForm.roleStatus"
placeholder="请选择角色状态" placeholder="请选择角色状态"
clearable clearable
:options="[ :options="getSelectOptions('sys_switch_status')"
{ label: '启用', value: '0' },
{ label: '停用', value: '1' },
]"
/> />
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
@ -269,14 +266,15 @@
<n-grid :cols="2" :x-gap="10"> <n-grid :cols="2" :x-gap="10">
<n-grid-item> <n-grid-item>
<n-form-item label="角色状态" path="roleStatus" class="mb-2"> <n-form-item label="角色状态" path="roleStatus" class="mb-2">
<n-select <NRadioGroup v-model:value="formData.roleStatus">
v-model:value="formData.roleStatus" <NRadio
placeholder="请选择角色状态" v-for="item in dictOptions.sys_switch_status || []"
:options="[ :key="item.dictValue"
{ label: '启用', value: '0' }, :value="item.dictValue"
{ label: '停用', value: '1' }, >
]" {{ item.dictLabel }}
/> </NRadio>
</NRadioGroup>
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
<n-grid-item> <n-grid-item>
@ -420,13 +418,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { h, nextTick, onMounted, ref, watch } from 'vue' import { h, nextTick, onMounted, ref, watch } from 'vue'
import type { DataTableColumns, FormInst } from 'naive-ui' import type { DataTableColumns, FormInst } from 'naive-ui'
import { NButton, NIcon, NPopconfirm, NSpace, NSwitch, NTag, NTree } from 'naive-ui' import { NButton, NIcon, NPopconfirm, NRadio, NRadioGroup, NSpace, NSwitch, NTag, NTree } from 'naive-ui'
import IconParkOutlineEditOne from '~icons/icon-park-outline/edit-one' import IconParkOutlineEditOne from '~icons/icon-park-outline/edit-one'
import IconParkOutlineDelete from '~icons/icon-park-outline/delete' import IconParkOutlineDelete from '~icons/icon-park-outline/delete'
import IconParkOutlineKey from '~icons/icon-park-outline/key' import IconParkOutlineKey from '~icons/icon-park-outline/key'
import CoiDialog from '@/components/common/CoiDialog.vue' import CoiDialog from '@/components/common/CoiDialog.vue'
import CoiEmpty from '@/components/common/CoiEmpty.vue' import CoiEmpty from '@/components/common/CoiEmpty.vue'
import CoiPagination from '@/components/common/CoiPagination.vue' import CoiPagination from '@/components/common/CoiPagination.vue'
import DictTag from '@/components/common/DictTag.vue'
import { import {
addRole, addRole,
batchDeleteRoles, batchDeleteRoles,
@ -446,10 +445,11 @@ import {
import type { MenuPermissionData, MenuVo } from '@/service/api/system/menu' import type { MenuPermissionData, MenuVo } from '@/service/api/system/menu'
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi' import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
import { PERMISSIONS } from '@/constants/permissions' import { PERMISSIONS } from '@/constants/permissions'
import { usePermission } from '@/hooks/usePermission' import { useDict, usePermission } from '@/hooks'
// //
const { hasButton } = usePermission() const { hasButton } = usePermission()
const { dictOptions, getSelectOptions, getDictLabel } = useDict(['sys_switch_status'])
// //
const loading = ref(false) const loading = ref(false)
@ -546,28 +546,27 @@ const columns: DataTableColumns<RoleVo> = [
key: 'roleCode', key: 'roleCode',
width: 150, width: 150,
align: 'center', align: 'center',
render: (row) => { render: row => h(NTag, { type: 'primary', size: 'small' }, { default: () => row.roleCode }),
return h(NTag, { type: 'primary', size: 'small' }, { default: () => row.roleCode })
},
}, },
{ {
title: '角色状态', title: '角色状态',
key: 'roleStatus', key: 'roleStatus',
width: 100, width: 120,
align: 'center', align: 'center',
render: (row) => { render: (row) => {
return h('div', { class: 'flex items-center justify-center' }, [ return h('div', { class: 'flex items-center justify-center gap-2' }, [
h(DictTag, { dictType: 'sys_switch_status', value: row.roleStatus }),
h(NPopconfirm, { h(NPopconfirm, {
onPositiveClick: () => handleToggleStatus(row), onPositiveClick: () => handleToggleStatus(row),
negativeText: '取消', negativeText: '取消',
positiveText: '确定', positiveText: '确定',
}, { }, {
default: () => `确定要${row.roleStatus === '0' ? '停用' : '启用'}角色「${row.roleName}」吗?`, default: () => `确定要将角色「${row.roleName}」状态切换为「${getDictLabel('sys_switch_status', row.roleStatus === '0' ? '1' : '0')}」吗?`,
trigger: () => h(NSwitch, { trigger: () => h(NSwitch, {
value: row.roleStatus === '0', value: row.roleStatus === '0',
size: 'small', size: 'small',
checkedChildren: '启用', checkedChildren: getDictLabel('sys_switch_status', '0', '启用'),
uncheckedChildren: '停用', uncheckedChildren: getDictLabel('sys_switch_status', '1', '停用'),
loading: false, loading: false,
}), }),
}), }),
@ -966,7 +965,7 @@ async function handleBatchDelete() {
async function handleToggleStatus(role: RoleVo) { async function handleToggleStatus(role: RoleVo) {
try { try {
const newStatus = role.roleStatus === '0' ? '1' : '0' const newStatus = role.roleStatus === '0' ? '1' : '0'
const statusText = newStatus === '0' ? '启用' : '停用' const statusText = getDictLabel('sys_switch_status', newStatus, newStatus === '0' ? '启用' : '停用')
const { isSuccess } = await updateRoleStatus(role.roleId, newStatus) const { isSuccess } = await updateRoleStatus(role.roleId, newStatus)
if (isSuccess) { if (isSuccess) {

View File

@ -72,10 +72,7 @@
v-model:value="searchForm.userStatus" v-model:value="searchForm.userStatus"
placeholder="请选择用户状态" placeholder="请选择用户状态"
clearable clearable
:options="[ :options="getSelectOptions('sys_switch_status')"
{ label: '启用', value: '0' },
{ label: '停用', value: '1' },
]"
/> />
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
@ -313,11 +310,7 @@
<n-select <n-select
v-model:value="formData.userType" v-model:value="formData.userType"
placeholder="请选择用户类型" placeholder="请选择用户类型"
:options="[ :options="getSelectOptions('sys_user_type')"
{ label: '系统用户', value: '1' },
{ label: '注册用户', value: '2' },
{ label: '微信用户', value: '3' },
]"
/> />
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
@ -327,11 +320,7 @@
<n-select <n-select
v-model:value="formData.sex" v-model:value="formData.sex"
placeholder="请选择性别" placeholder="请选择性别"
:options="[ :options="getSelectOptions('sys_user_sex')"
{ label: '男', value: '1' },
{ label: '女', value: '2' },
{ label: '未知', value: '3' },
]"
/> />
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
@ -367,14 +356,15 @@
<n-grid-item> <n-grid-item>
<n-form-item label="用户状态" path="userStatus" class="mb-2"> <n-form-item label="用户状态" path="userStatus" class="mb-2">
<n-radio-group v-model:value="formData.userStatus"> <NRadioGroup v-model:value="formData.userStatus">
<n-radio value="0"> <NRadio
启用 v-for="item in dictOptions.sys_switch_status || []"
</n-radio> :key="item.dictValue"
<n-radio value="1"> :value="item.dictValue"
停用 >
</n-radio> {{ item.dictLabel }}
</n-radio-group> </NRadio>
</NRadioGroup>
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
@ -585,11 +575,8 @@
{{ currentAvatarUser?.avatar ? '用户头像' : '默认头像' }} {{ currentAvatarUser?.avatar ? '用户头像' : '默认头像' }}
</p> </p>
<div class="flex items-center justify-center gap-4 text-xs text-gray-400"> <div class="flex items-center justify-center gap-4 text-xs text-gray-400">
<span>用户类型: {{ <span>用户类型: {{ getDictLabel('sys_user_type', currentAvatarUser?.userType, '--') }}</span>
currentAvatarUser?.userType === '1' ? '系统用户' <span>状态: {{ getDictLabel('sys_switch_status', currentAvatarUser?.userStatus, '--') }}</span>
: currentAvatarUser?.userType === '2' ? '注册用户' : '微信用户'
}}</span>
<span>状态: {{ currentAvatarUser?.userStatus === '0' ? '启用' : '停用' }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -704,7 +691,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { h, nextTick, onBeforeUnmount, onMounted, ref } from 'vue' import { h, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import type { DataTableColumns, FormInst } from 'naive-ui' import type { DataTableColumns, FormInst } from 'naive-ui'
import { NButton, NCheckbox, NDropdown, NIcon, NPopconfirm, NProgress, NSpace, NSwitch, NTag, NUpload, NUploadDragger } from 'naive-ui' import { NButton, NCheckbox, NDropdown, NIcon, NPopconfirm, NProgress, NRadio, NRadioGroup, NSpace, NSwitch, NTag, NUpload, NUploadDragger } from 'naive-ui'
import IconParkOutlineEditOne from '~icons/icon-park-outline/edit-one' import IconParkOutlineEditOne from '~icons/icon-park-outline/edit-one'
import IconParkOutlineDelete from '~icons/icon-park-outline/delete' import IconParkOutlineDelete from '~icons/icon-park-outline/delete'
import IconParkOutlineRefresh from '~icons/icon-park-outline/refresh' import IconParkOutlineRefresh from '~icons/icon-park-outline/refresh'
@ -715,6 +702,7 @@ import IconParkOutlineFileCodeOne from '~icons/icon-park-outline/file-code-one'
import CoiDialog from '@/components/common/CoiDialog.vue' import CoiDialog from '@/components/common/CoiDialog.vue'
import CoiEmpty from '@/components/common/CoiEmpty.vue' import CoiEmpty from '@/components/common/CoiEmpty.vue'
import CoiPagination from '@/components/common/CoiPagination.vue' import CoiPagination from '@/components/common/CoiPagination.vue'
import DictTag from '@/components/common/DictTag.vue'
import { import {
addUser, addUser,
batchDeleteUsers, batchDeleteUsers,
@ -739,7 +727,7 @@ import {
import type { RoleVo } from '@/service/api/system/role' import type { RoleVo } from '@/service/api/system/role'
import { coiMsgBox, coiMsgError, coiMsgInfo, coiMsgSuccess, coiMsgWarning } from '@/utils/coi' import { coiMsgBox, coiMsgError, coiMsgInfo, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
import { PERMISSIONS } from '@/constants/permissions' import { PERMISSIONS } from '@/constants/permissions'
import { usePermission } from '@/hooks/usePermission' import { useDict, usePermission } from '@/hooks'
// //
const { hasButton } = usePermission() const { hasButton } = usePermission()
@ -790,6 +778,12 @@ const uploadProgress = ref(0)
const progressInterval = ref<NodeJS.Timeout | null>(null) const progressInterval = ref<NodeJS.Timeout | null>(null)
const createdBlobUrls = ref<string[]>([]) const createdBlobUrls = ref<string[]>([])
const { dictOptions, getSelectOptions, getDictLabel } = useDict([
'sys_user_type',
'sys_user_sex',
'sys_switch_status',
])
// //
const pagination = ref({ const pagination = ref({
page: 1, page: 1,
@ -956,33 +950,14 @@ const columns: DataTableColumns<UserVo> = [
key: 'userType', key: 'userType',
width: 100, width: 100,
align: 'center', align: 'center',
render: (row) => { render: row => h(DictTag, { dictType: 'sys_user_type', value: row.userType }),
const typeMap: Record<string, { label: string, type: any }> = {
1: { label: '系统用户', type: 'primary' },
2: { label: '注册用户', type: 'info' },
3: { label: '微信用户', type: 'warning' },
}
const config = typeMap[row.userType] || { label: '未知', type: 'default' }
return h(NTag, { type: config.type, size: 'small' }, { default: () => config.label })
},
}, },
{ {
title: '性别', title: '性别',
key: 'sex', key: 'sex',
width: 80, width: 80,
align: 'center', align: 'center',
render: (row) => { render: row => h(DictTag, { dictType: 'sys_user_sex', value: row.sex }),
const sexMap: Record<string, { label: string, icon: string, color: string }> = {
1: { label: '男', icon: '♂', color: 'text-blue-500' },
2: { label: '女', icon: '♀', color: 'text-pink-500' },
3: { label: '未知', icon: '?', color: 'text-gray-400' },
}
const config = sexMap[row.sex || '3']
return h('div', { class: `flex items-center justify-center gap-1 ${config.color}` }, [
h('span', { class: 'text-lg' }, config.icon),
h('span', { class: 'text-xs' }, config.label),
])
},
}, },
{ {
title: '用户状态', title: '用户状态',
@ -990,18 +965,21 @@ const columns: DataTableColumns<UserVo> = [
width: 100, width: 100,
align: 'center', align: 'center',
render: (row) => { render: (row) => {
const nextStatus = row.userStatus === '0' ? '1' : '0'
const enableLabel = getDictLabel('sys_switch_status', '0', '启用')
const disableLabel = getDictLabel('sys_switch_status', '1', '停用')
return h('div', { class: 'flex items-center justify-center' }, [ return h('div', { class: 'flex items-center justify-center' }, [
h(NPopconfirm, { h(NPopconfirm, {
onPositiveClick: () => handleToggleStatus(row), onPositiveClick: () => handleToggleStatus(row),
negativeText: '取消', negativeText: '取消',
positiveText: '确定', positiveText: '确定',
}, { }, {
default: () => `确定要${row.userStatus === '0' ? '停用' : '启用'}用户「${row.userName}」吗?`, default: () => `确定要将用户「${row.userName}」状态切换为「${getDictLabel('sys_switch_status', nextStatus)}」吗?`,
trigger: () => h(NSwitch, { trigger: () => h(NSwitch, {
value: row.userStatus === '0', value: row.userStatus === '0',
size: 'small', size: 'small',
checkedChildren: '启用', checkedChildren: enableLabel,
uncheckedChildren: '停用', uncheckedChildren: disableLabel,
loading: false, loading: false,
}), }),
}), }),
@ -1357,7 +1335,7 @@ async function handleBatchDelete() {
async function handleToggleStatus(user: UserVo) { async function handleToggleStatus(user: UserVo) {
try { try {
const newStatus = user.userStatus === '0' ? '1' : '0' const newStatus = user.userStatus === '0' ? '1' : '0'
const statusText = newStatus === '0' ? '启用' : '停用' const statusText = getDictLabel('sys_switch_status', newStatus, newStatus === '0' ? '启用' : '停用')
const { isSuccess } = await updateUserStatus(user.userId, newStatus) const { isSuccess } = await updateUserStatus(user.userId, newStatus)
if (isSuccess) { if (isSuccess) {