feat: 增加字典管理角色管理

This commit is contained in:
chansee97 2025-08-31 17:25:53 +08:00
parent 3124d0b923
commit 53282f9453
26 changed files with 792 additions and 583 deletions

View File

@ -45,7 +45,7 @@ const propOverrides = {
cols: 4, cols: 4,
}, },
ProModalForm: { ProModalForm: {
labelWidth: 120, labelWidth: 100,
labelPlacement: 'left', labelPlacement: 'left',
preset: 'card', preset: 'card',
}, },

View File

@ -6,7 +6,7 @@ interface UserQueryParams {
pageSize?: number pageSize?: number
username?: string username?: string
gender?: 'male' | 'female' | 'unknown' gender?: 'male' | 'female' | 'unknown'
userStatus?: number status?: number
deptId?: number deptId?: number
} }

View File

@ -8,5 +8,5 @@ export enum Regex {
Email = '^(([^<>()[\\]\\\\.,;:\\s@"]+(\\.[^<>()[\\]\\\\.,;:\\s@"]+)*)|(".+"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$', Email = '^(([^<>()[\\]\\\\.,;:\\s@"]+(\\.[^<>()[\\]\\\\.,;:\\s@"]+)*)|(".+"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$',
RouteName = '^[\\w_!@#$%^&*~-]+$', Phone = '^1[3-9]\d{9}$',
} }

View File

@ -1,5 +0,0 @@
/** Gender */
export enum Gender {
male,
female,
}

View File

@ -1,2 +1 @@
export * from './Regex' export * from './Regex'
export * from './User'

View File

@ -3,14 +3,18 @@
/** 数据库表字段 */ /** 数据库表字段 */
namespace Entity { namespace Entity {
interface DictType { interface DictType {
/**
* ID
*/
id?: number
/** /**
* *
*/ */
dictName: string name: string
/** /**
* *
*/ */
dictType: string type: string
/** /**
* *
*/ */
@ -19,21 +23,29 @@ namespace Entity {
* *
*/ */
status?: number status?: number
/**
*
*/
createTime?: string
/**
*
*/
updateTime?: string
} }
interface DictData { interface DictData {
/** /**
* * ID
*/ */
cssClass?: string id?: number
/** /**
* *
*/ */
dictLabel: string name: string
/** /**
* *
*/ */
dictSort?: number sort?: number
/** /**
* *
*/ */
@ -41,15 +53,7 @@ namespace Entity {
/** /**
* *
*/ */
dictValue: string value: string
/**
*
*/
isDefault?: number
/**
*
*/
listClass?: string
/** /**
* *
*/ */
@ -58,5 +62,13 @@ namespace Entity {
* *
*/ */
status?: number status?: number
/**
*
*/
createTime?: string
/**
*
*/
updateTime?: string
} }
} }

View File

@ -8,7 +8,7 @@ namespace Entity {
/** /**
* ID * ID
*/ */
roleId: number id: number
/** /**
* ID数组 * ID数组
*/ */
@ -28,7 +28,7 @@ namespace Entity {
/** /**
* *
*/ */
roleStatus?: number status?: number
/** /**
* *
*/ */

View File

@ -4,7 +4,7 @@
namespace Entity { namespace Entity {
interface User { interface User {
/** 用户id */ /** 用户id */
userId: number id: number
/** 部门id */ /** 部门id */
deptId?: any deptId?: any
/** 用户名 */ /** 用户名 */
@ -22,13 +22,9 @@ namespace Entity {
/** 头像 */ /** 头像 */
avatar?: string avatar?: string
/** 用户状态 */ /** 用户状态 */
userStatus: number status: number
/** 创建人 */
createBy?: string
/** 创建时间 */ /** 创建时间 */
createTime: string createTime: string
/** 更新人 */
updateBy?: string
/** 更新时间 */ /** 更新时间 */
updateTime: string updateTime: string
/** 备注 */ /** 备注 */

View File

@ -72,24 +72,23 @@ export function createAlovaInstance(
const { status } = response const { status } = response
let errorMessage = '' let errorMessage = ''
const res = await response.clone().json()
if (status === 200) { if (status === 200) {
// 返回blob数据 // 返回blob数据
if (method.meta?.isBlob) if (method.meta?.isBlob)
return response.blob() return response.blob()
// 返回json数据
const apiData = await response.json()
// 请求成功 // 请求成功
if (apiData[_backendConfig.codeKey] === _backendConfig.successCode) if (res[_backendConfig.codeKey] === _backendConfig.successCode)
return apiData return res
// 业务请求失败 // 业务请求失败
errorMessage = apiData[_backendConfig.msgKey] errorMessage = res[_backendConfig.msgKey]
} }
else { else {
// 接口请求失败 // 接口请求失败
const errorCode = response.status as ErrorStatus const errorCode = response.status as ErrorStatus
errorMessage = ERROR_STATUS[errorCode] || ERROR_STATUS.default errorMessage = res[_backendConfig.msgKey] || ERROR_STATUS[errorCode] || ERROR_STATUS.default
} }
window.$message?.error(errorMessage) window.$message?.error(errorMessage)
throw new Error(errorMessage) throw new Error(errorMessage)

View File

@ -49,7 +49,7 @@ const rules = computed(() => {
}) })
const formValue = ref({ const formValue = ref({
account: 'admin', account: 'admin',
pwd: '12345', pwd: '123456',
captcha: '', captcha: '',
}) })
const isRemember = ref(false) const isRemember = ref(false)

View File

@ -185,7 +185,7 @@ function handleAddTable() {
<template #icon> <template #icon>
<icon-park-outline-add-one /> <icon-park-outline-add-one />
</template> </template>
</NButton> </NButton>
<NButton strong secondary> <NButton strong secondary>
<template #icon> <template #icon>

View File

@ -1,144 +1,98 @@
import type { DataTableColumns } from 'naive-ui' import type { DataTableColumns } from 'naive-ui'
import { NButton, NFlex, NPopconfirm } from 'naive-ui' import { NButton, NPopconfirm, NSpace } from 'naive-ui'
import CopyText from '@/components/custom/CopyText.vue' import type { ProSearchFormColumns } from 'pro-naive-ui'
import { renderProCopyableText, renderProTags } from 'pro-naive-ui'
// 字典类型columns配置函数
interface DictTypeColumnActions {
onView: (code: string) => void
onEdit: (row: Entity.DictType) => void
onDelete: (id: number) => void
}
export function createDictTypeColumns(actions: DictTypeColumnActions): DataTableColumns<Entity.DictType> {
return [
{
title: '字典项',
key: 'dictName',
},
{
title: '字典码',
key: 'dictType',
render: (row) => {
return (
<CopyText value={row.dictType} />
)
},
},
{
title: '状态',
key: 'status',
align: 'center',
render: (row) => {
return (
<span>{row.status === 1 ? '正常' : '停用'}</span>
)
},
},
{
title: '操作',
key: 'actions',
align: 'center',
render: (row) => {
return (
<NFlex justify="center">
<NButton
size="small"
onClick={() => actions.onView(row.dictType)}
>
</NButton>
<NButton
size="small"
onClick={() => actions.onEdit(row)}
>
</NButton>
<NPopconfirm onPositiveClick={() => actions.onDelete(row.id!)}>
{{
default: () => (
<span>
<b>{row.dictName}</b>
{' '}
</span>
),
trigger: () => <NButton size="small" type="error"></NButton>,
}}
</NPopconfirm>
</NFlex>
)
},
},
]
}
// 字典数据columns配置函数
interface DictDataColumnActions { interface DictDataColumnActions {
onEdit: (row: Entity.DictData) => void onEdit: (row: Entity.DictData) => void
onDelete: (id: number) => void onDelete: (id: number) => void
} }
export const dictDataSearchColumns: ProSearchFormColumns<Entity.DictData> = [
{
title: '数据名称',
path: 'name',
},
{
title: '数据键值',
path: 'value',
},
{
title: '状态',
path: 'status',
field: 'select',
fieldProps: {
options: [
{ label: '正常', value: 1 },
{ label: '停用', value: 0 },
],
},
},
]
export function createDictDataColumns(actions: DictDataColumnActions): DataTableColumns<Entity.DictData> { export function createDictDataColumns(actions: DictDataColumnActions): DataTableColumns<Entity.DictData> {
return [ return [
{ {
title: '字典名称', key: 'name',
key: 'dictLabel', title: '数据名称',
align: 'left',
minWidth: 120,
ellipsis: {
tooltip: true,
},
},
{
key: 'value',
title: '数据键值',
align: 'center',
width: 120,
render: row => renderProCopyableText(row.value),
}, },
{ {
title: '字典码',
key: 'dictType', key: 'dictType',
title: '字典类型',
align: 'center',
render: row => renderProTags(row.dictType),
}, },
{ {
title: '字典值', key: 'sort',
key: 'dictValue',
},
{
title: '排序', title: '排序',
key: 'dictSort',
align: 'center', align: 'center',
width: '80px', width: 100,
}, },
{ {
title: '状态', key: 'remark',
key: 'status', title: '备注',
align: 'center', align: 'center',
render: (row) => {
return (
<span>{row.status === 1 ? '正常' : '停用'}</span>
)
},
}, },
{ {
title: '操作',
key: 'actions', key: 'actions',
title: '操作',
align: 'center', align: 'center',
width: '15em', width: 150,
render: (row) => { render: (row: Entity.DictData) => (
return ( <NSpace justify="center">
<NFlex justify="center"> <NButton
<NButton type="primary"
size="small" text
onClick={() => actions.onEdit(row)} onClick={() => actions.onEdit(row)}
> >
</NButton> </NButton>
<NPopconfirm onPositiveClick={() => actions.onDelete(row.id!)}> <NPopconfirm
{{ onPositiveClick={() => actions.onDelete(row.id!)}
default: () => ( v-slots={{
<span> trigger: () => (
<NButton type="error" text>
<b>{row.dictLabel}</b>
{' '} </NButton>
),
</span> default: () => `确定删除字典数据"${row.name}"吗?`,
), }}
trigger: () => <NButton size="small" type="error"></NButton>, />
}} </NSpace>
</NPopconfirm> ),
</NFlex>
)
},
}, },
] ]
} }

View File

@ -0,0 +1,143 @@
<script setup lang="ts">
import { useBoolean } from '@/hooks'
import { createDictData, getDictDataById, updateDictData } from '@/api'
import { createProModalForm } from 'pro-naive-ui'
interface Props {
modalName?: string
}
const {
modalName = '',
} = defineProps<Props>()
const emit = defineEmits<{
success: []
}>()
const { bool: submitLoading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
const modalForm = createProModalForm<Partial<Entity.DictData>>({
omitEmptyString: false,
initialValues: {
sort: 0,
status: 1,
},
onSubmit: submitModal,
})
type ModalType = 'add' | 'edit'
const modalType = shallowRef<ModalType>('add')
const modalTitle = computed(() => {
const titleMap: Record<ModalType, string> = {
add: '添加',
edit: '编辑',
}
return `${titleMap[modalType.value]}${modalName}`
})
async function openModal(type: ModalType = 'add', data?: Partial<Entity.DictData>) {
modalType.value = type
modalForm.open()
const handlers = {
async add() {
if (data?.dictType) {
modalForm.values.value.dictType = data.dictType
}
},
async edit() {
if (!data)
return
const { data: dictData } = await getDictDataById(data.id!)
modalForm.values.value = dictData
},
}
await handlers[type]()
}
async function submitModal(filedValues: Partial<Entity.DictData>) {
const handlers = {
async add() {
try {
await createDictData(filedValues)
window.$message.success('字典数据创建成功')
return true
}
catch (error) {
console.error('创建字典数据失败', error)
return false
}
},
async edit() {
try {
await updateDictData(modalForm.values.value.id!, filedValues)
window.$message.success('字典数据更新成功')
return true
}
catch (error) {
console.error('更新字典数据失败', error)
return false
}
},
}
startLoading()
const success = await handlers[modalType.value]()
endLoading()
if (success) {
emit('success')
modalForm.close()
}
}
defineExpose({
openModal,
})
</script>
<template>
<pro-modal-form
:title="modalTitle"
:form="modalForm"
:loading="submitLoading"
width="700px"
>
<div class="grid grid-cols-2 gap-4">
<pro-input
required
title="字典名称"
path="name"
placeholder="请输入字典名称"
/>
<pro-input
required
title="字典键值"
path="value"
placeholder="请输入字典键值"
/>
<pro-input
title="字典类型"
path="dictType"
placeholder="字典类型"
:readonly="true"
/>
<pro-digit
title="排序"
path="sort"
:field-props="{ min: 0, max: 999 }"
/>
<pro-switch
title="状态"
path="status"
:field-props="{ checkedValue: 1, uncheckedValue: 0 }"
/>
<pro-textarea
class="col-span-2"
title="备注"
path="remark"
placeholder="请输入备注信息"
/>
</div>
</pro-modal-form>
</template>

View File

@ -1,202 +0,0 @@
<script setup lang="ts">
import type { FormRules } from 'naive-ui'
import { useBoolean } from '@/hooks'
import { createDictData, createDictType, updateDictData, updateDictType } from '@/api'
interface Props {
modalName?: string
dictCode?: string
isRoot?: boolean
}
const {
modalName = '',
dictCode,
isRoot = false,
} = defineProps<Props>()
const emit = defineEmits<{
open: []
close: []
success: []
}>()
const { bool: modalVisible, setTrue: showModal, setFalse: hiddenModal } = useBoolean(false)
const { bool: submitLoading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
const formDefault: any = {
label: '',
code: '',
}
const formModel = ref<any>({ ...formDefault })
type ModalType = 'add' | 'view' | 'edit'
const modalType = shallowRef<ModalType>('add')
const modalTitle = computed(() => {
const titleMap: Record<ModalType, string> = {
add: '添加',
view: '查看',
edit: '编辑',
}
return `${titleMap[modalType.value]}${modalName}`
})
async function openModal(type: ModalType = 'add', data?: any) {
emit('open')
modalType.value = type
showModal()
const handlers = {
async add() {
formModel.value = { ...formDefault }
formModel.value.isRoot = isRoot ? 1 : 0
if (dictCode) {
formModel.value.code = dictCode
}
},
async view() {
if (!data)
return
formModel.value = { ...data }
},
async edit() {
if (!data)
return
formModel.value = { ...data }
},
}
await handlers[type]()
}
function closeModal() {
hiddenModal()
endLoading()
emit('close')
}
defineExpose({
openModal,
})
const formRef = ref()
async function submitModal() {
const handlers = {
async add() {
try {
if (isRoot) {
//
await createDictType({
dictType: formModel.value.label,
dictName: formModel.value.code,
})
window.$message.success('字典类型创建成功')
}
else {
//
await createDictData({
dictType: formModel.value.label,
dictValue: formModel.value.value,
})
window.$message.success('字典数据创建成功')
}
emit('success')
return true
}
catch {
return false
}
},
async edit() {
try {
if (isRoot) {
//
await updateDictType(formModel.value.id!, {
dictType: formModel.value.label as any,
dictName: formModel.value.code,
})
window.$message.success('字典类型更新成功')
}
else {
//
await updateDictData(formModel.value.id!, {
dictType: formModel.value.label,
dictValue: formModel.value.value,
})
window.$message.success('字典数据更新成功')
}
emit('success')
return true
}
catch {
return false
}
},
async view() {
return true
},
}
await formRef.value?.validate()
startLoading()
const success = await handlers[modalType.value]()
endLoading()
if (success) {
closeModal()
}
}
const rules: FormRules = {
label: {
required: true,
message: '请输入字典名称',
trigger: ['input', 'blur'],
},
code: {
required: true,
message: '请输入字典码',
trigger: ['input', 'blur'],
},
value: {
required: true,
message: '请输入字典值',
type: 'number',
trigger: ['input', 'blur'],
},
}
</script>
<template>
<n-modal
v-model:show="modalVisible"
:mask-closable="false"
preset="card"
:title="modalTitle"
class="w-700px"
:segmented="{
content: true,
action: true,
}"
>
<n-form ref="formRef" :rules="rules" label-placement="left" :model="formModel" :label-width="100" :disabled="modalType === 'view'">
<n-form-item label="字典名称" path="label">
<n-input v-model:value="formModel.label" />
</n-form-item>
<n-form-item label="字典码" path="code">
<n-input v-model:value="formModel.code" :disabled="!isRoot" />
</n-form-item>
<n-form-item v-if="!isRoot" label="字典值" path="value">
<n-input-number v-model:value="formModel.value" :min="0" />
</n-form-item>
</n-form>
<template #action>
<n-space justify="center">
<n-button @click="closeModal">
取消
</n-button>
<n-button type="primary" :loading="submitLoading" @click="submitModal">
提交
</n-button>
</n-space>
</template>
</n-modal>
</template>

View File

@ -0,0 +1,171 @@
<script setup lang="tsx">
import type { DataTableColumns } from 'naive-ui'
import { NButton, NPopconfirm, NSpace } from 'naive-ui'
import type { ProSearchFormColumns } from 'pro-naive-ui'
import { createProSearchForm, renderProCopyableText, useNDataTable } from 'pro-naive-ui'
import { deleteDictType, getDictTypeList } from '@/api'
import DictTypeModal from './DictTypeModal.vue'
defineEmits(['select'])
//
const dictTypeModalRef = ref<InstanceType<typeof DictTypeModal>>()
//
const dictTypeSearchForm = createProSearchForm<Partial<Entity.DictType>>({
initialValues: {},
})
//
const dictTypeSearchColumns: ProSearchFormColumns<Entity.DictType> = [
{
title: '字典名称',
path: 'name',
},
{
title: '字典类型',
path: 'type',
},
]
// 使 useNDataTable
const {
table: {
tableProps: dictTypeTableProps,
},
search: {
proSearchFormProps: dictTypeSearchProps,
},
refresh: refreshDictTypes,
} = useNDataTable(getDictTypePage, {
form: dictTypeSearchForm,
})
//
async function getDictTypePage({ current, pageSize }: any, formData: Partial<Entity.DictType>) {
try {
const { data } = await getDictTypeList({
...formData,
pageNum: current || 1,
pageSize: pageSize || 10,
})
return data
}
catch {
return {
list: [],
total: 0,
}
}
}
//
async function deleteDictTypeData(id: number) {
try {
await deleteDictType(id)
window.$message.success('字典类型删除成功')
refreshDictTypes()
}
catch {
window.$message.error('字典类型删除失败')
}
}
//
const columns: DataTableColumns<Entity.DictType> = [
{
key: 'name',
title: '字典名称',
ellipsis: {
tooltip: true,
},
},
{
key: 'type',
title: '字典类型',
render: row => renderProCopyableText(row.type),
},
{
key: 'actions',
title: '操作',
align: 'center',
render: (row: Entity.DictType) => (
<NSpace size="small" justify="center">
<NButton
text
type="primary"
onClick={(e: Event) => {
e.stopPropagation()
dictTypeModalRef.value?.openModal('edit', row)
}}
>
编辑
</NButton>
<NPopconfirm
onPositiveClick={() => deleteDictTypeData(row.id!)}
v-slots={{
trigger: () => (
<NButton
text
type="error"
onClick={(e: Event) => e.stopPropagation()}
>
删除
</NButton>
),
default: () => `确定删除字典类型"${row.name}"吗?`,
}}
/>
</NSpace>
),
},
]
</script>
<template>
<NSpace vertical>
<!-- 搜索表单 -->
<n-card>
<pro-search-form
:form="dictTypeSearchForm"
:columns="dictTypeSearchColumns"
:collapse-button-props="false"
v-bind="dictTypeSearchProps"
:cols="3"
/>
</n-card>
<!-- 数据表格 -->
<pro-data-table
:columns="columns"
v-bind="dictTypeTableProps"
:row-props="(row:Entity.DictType) => {
return {
onClick: () => $emit('select', row),
style: {
cursor: 'pointer',
},
}
}"
>
<template #title>
<NButton
type="primary"
@click="dictTypeModalRef?.openModal('add')"
>
<template #icon>
<icon-park-outline-plus />
</template>
新增字典类型
</NButton>
</template>
</pro-data-table>
<!-- 字典类型弹窗 -->
<DictTypeModal
ref="dictTypeModalRef"
modal-name="字典类型"
@success="refreshDictTypes"
/>
</NSpace>
</template>

View File

@ -0,0 +1,133 @@
<script setup lang="ts">
import { useBoolean } from '@/hooks'
import { createDictType, getDictTypeById, updateDictType } from '@/api'
import { createProModalForm } from 'pro-naive-ui'
interface Props {
modalName?: string
}
const {
modalName = '',
} = defineProps<Props>()
const emit = defineEmits<{
success: []
}>()
const { bool: submitLoading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
const modalForm = createProModalForm<Partial<Entity.DictType>>({
omitEmptyString: false,
initialValues: {
name: '',
type: '',
remark: '',
status: 1,
},
onSubmit: submitModal,
})
type ModalType = 'add' | 'edit'
const modalType = shallowRef<ModalType>('add')
const modalTitle = computed(() => {
const titleMap: Record<ModalType, string> = {
add: '添加',
edit: '编辑',
}
return `${titleMap[modalType.value]}${modalName}`
})
async function openModal(type: ModalType = 'add', data?: Partial<Entity.DictType>) {
modalType.value = type
modalForm.open()
const handlers = {
async add() {
// 使
},
async edit() {
if (!data)
return
const { data: dictType } = await getDictTypeById(data.id!)
modalForm.values.value = dictType
},
}
await handlers[type]()
}
async function submitModal(filedValues: Partial<Entity.DictType>) {
const handlers = {
async add() {
try {
await createDictType(filedValues)
window.$message.success('字典类型创建成功')
return true
}
catch (error) {
console.error('创建字典类型失败', error)
return false
}
},
async edit() {
try {
await updateDictType(modalForm.values.value.id!, filedValues)
window.$message.success('字典类型更新成功')
return true
}
catch (error) {
console.error('更新字典类型失败', error)
return false
}
},
}
startLoading()
const success = await handlers[modalType.value]()
endLoading()
if (success) {
emit('success')
modalForm.close()
}
}
defineExpose({
openModal,
})
</script>
<template>
<pro-modal-form
:title="modalTitle"
:form="modalForm"
:loading="submitLoading"
width="600px"
>
<div class="grid grid-cols-2 gap-4">
<pro-input
required
title="字典名称"
path="name"
placeholder="请输入字典名称"
/>
<pro-input
required
title="字典类型"
path="type"
placeholder="请输入字典类型"
:readonly="modalType === 'edit'"
/>
<pro-textarea
class="col-span-2"
title="备注"
path="remark"
placeholder="请输入备注信息"
/>
<pro-switch
title="状态"
path="status"
:field-props="{ checkedValue: 1, uncheckedValue: 0 }"
/>
</div>
</pro-modal-form>
</template>

View File

@ -1,144 +1,124 @@
<script setup lang="tsx"> <script setup lang="ts">
import { useBoolean } from '@/hooks' import { createProSearchForm, useNDataTable } from 'pro-naive-ui'
import { deleteDictData, deleteDictType, getDictDataByType, getDictTypeList } from '@/api' import { deleteDictData, getDictDataList } from '@/api'
import { createDictDataColumns, createDictTypeColumns } from './columns' import { createDictDataColumns, dictDataSearchColumns } from './columns'
import DictModal from './components/DictModal.vue' import DictTypeList from './components/DictTypeList.vue'
import DictDataModal from './components/DictDataModal.vue'
const { bool: dictLoading, setTrue: startDictLoading, setFalse: endDictLoading } = useBoolean(false) const dictTypeListRef = ref<InstanceType<typeof DictTypeList>>()
const { bool: contentLoading, setTrue: startContentLoading, setFalse: endContentLoading } = useBoolean(false)
const dictRef = ref<InstanceType<typeof DictModal>>() //
const dictContentRef = ref<InstanceType<typeof DictModal>>() const dictDataSearchForm = createProSearchForm<Partial<Entity.DictData>>({
initialValues: {},
})
const { values } = dictDataSearchForm
onMounted(() => { const {
getDictList() table: {
tableProps: dictDataTableProps,
},
search: {
proSearchFormProps: dictDataSearchProps,
},
refresh: refreshDictData,
} = useNDataTable(getDictDataPage, {
form: dictDataSearchForm,
}) })
const dictData = ref<Entity.DictType[]>([]) const dictDataModalRef = ref<InstanceType<typeof DictDataModal>>()
const dictContentData = ref<Entity.DictData[]>([])
async function getDictList() { //
startDictLoading() const currentDictType = ref<Entity.DictType | null>(null)
function handleDictTypeSelect(dictType: Entity.DictType) {
currentDictType.value = dictType
refreshDictData()
}
//
async function deleteDictDataItem(id: number) {
try { try {
const { data } = await getDictTypeList() await deleteDictData(id)
dictData.value = data.list window.$message.success('字典数据删除成功')
refreshDictData()
} }
catch (error) { catch {
console.error('获取字典类型列表失败', error) window.$message.error('字典数据删除失败')
}
finally {
endDictLoading()
} }
} }
const lastDictCode = ref('') //
async function getDictContent(code: string) { const dictDataColumns = createDictDataColumns({
startContentLoading() onEdit: (row: Entity.DictData) => dictDataModalRef.value?.openModal('edit', row),
try { onDelete: deleteDictDataItem,
const { data } = await getDictDataByType(code)
dictContentData.value = data
lastDictCode.value = code
}
catch (error) {
console.error('获取字典数据失败', error)
}
finally {
endContentLoading()
}
}
// columns
const dictColumns = createDictTypeColumns({
onView: getDictContent,
onEdit: row => dictRef.value!.openModal('edit', row),
onDelete: id => deleteDict(id, true),
}) })
// columns //
const contentColumns = createDictDataColumns({ async function getDictDataPage({ current, pageSize }: any, formData: Partial<Entity.DictData>) {
onEdit: row => dictContentRef.value!.openModal('edit', row), if (!currentDictType.value) {
onDelete: id => deleteDict(id, false), return { list: [], total: 0 }
})
async function deleteDict(id: number, isType: boolean = false) {
try {
if (isType) {
await deleteDictType(id)
window.$message.success('删除字典类型成功')
getDictList() //
}
else {
await deleteDictData(id)
window.$message.success('删除字典数据成功')
if (lastDictCode.value) {
getDictContent(lastDictCode.value) //
}
}
} }
catch (error) {
console.error(`删除${isType ? '字典类型' : '字典数据'}失败`, error) try {
const { data } = await getDictDataList({
...formData,
dictType: currentDictType.value.type,
pageNum: current || 1,
pageSize,
})
return data
}
catch {
return {
list: [],
total: 0,
}
} }
} }
</script> </script>
<template> <template>
<NFlex> <div class="flex h-full gap-2">
<div class="basis-2/5"> <!-- 左侧字典类型列表 -->
<n-card> <div class="w-1/3">
<template #header> <DictTypeList
<NButton type="primary" @click="dictRef!.openModal('add')"> ref="dictTypeListRef"
<template #icon> @select="handleDictTypeSelect"
<icon-park-outline-add-one /> />
</template>
新建
</NButton>
</template>
<template #header-extra>
<NFlex>
<NButton type="primary" secondary @click="getDictList">
<template #icon>
<icon-park-outline-refresh />
</template>
刷新
</NButton>
</NFlex>
</template>
<n-data-table
:columns="dictColumns" :data="dictData" :loading="dictLoading" :pagination="false"
:bordered="false"
/>
</n-card>
</div>
<div class="flex-1">
<n-card>
<template #header>
<NButton type="primary" :disabled="!lastDictCode" @click="dictContentRef!.openModal('add')">
<template #icon>
<icon-park-outline-add-one />
</template>
新建
</NButton>
</template>
<template #header-extra>
<NFlex>
<NButton type="primary" :disabled="!lastDictCode" secondary @click="getDictContent(lastDictCode)">
<template #icon>
<icon-park-outline-refresh />
</template>
刷新
</NButton>
</NFlex>
</template>
<n-data-table
:columns="contentColumns" :data="dictContentData" :loading="contentLoading" :pagination="false"
:bordered="false"
/>
</n-card>
</div> </div>
<DictModal ref="dictRef" modal-name="字典项" is-root @success="getDictList" /> <!-- 右侧字典数据表格 -->
<DictModal ref="dictContentRef" modal-name="字典值" :dict-code="lastDictCode" @success="() => getDictContent(lastDictCode)" /> <n-space class="flex-1" vertical>
</NFlex> <n-card>
<pro-search-form
:form="dictDataSearchForm"
:columns="dictDataSearchColumns"
v-bind="dictDataSearchProps"
:collapse-button-props="false"
/>
</n-card>
<pro-data-table
:columns="dictDataColumns"
v-bind="dictDataTableProps"
>
<template #title>
<n-button
v-if="values.dictType"
type="primary"
@click="dictDataModalRef?.openModal('add', { dictType: values.dictType })"
>
<template #icon>
<icon-park-outline-plus />
</template>
新增字典数据
</n-button>
</template>
</pro-data-table>
</n-space>
<!-- 字典数据弹窗 -->
<DictDataModal
ref="dictDataModalRef"
@success="refreshDictData"
/>
</div>
</template> </template>
<style scoped></style>

View File

@ -88,7 +88,7 @@ export function createMenuColumns(actions: MenuColumnActions): DataTableColumns<
type="primary" type="primary"
onClick={() => onAdd(row)} onClick={() => onAdd(row)}
> >
</NButton> </NButton>
)} )}
<NButton <NButton

View File

@ -71,7 +71,7 @@ async function openModal(type: ModalType = 'add', data?: Partial<Entity.Menu>) {
modalForm.open() modalForm.open()
const handlers = { const handlers = {
async add() { async add() {
// menuId // menuId
if (data?.id) { if (data?.id) {
modalForm.values.value.parentId = data.id modalForm.values.value.parentId = data.id
modalForm.values.value.path = `${data.path}/` modalForm.values.value.path = `${data.path}/`
@ -93,27 +93,34 @@ async function submitModal(filedValues: Partial<Entity.Menu>) {
try { try {
await createMenu(filedValues) await createMenu(filedValues)
window.$message.success('菜单创建成功') window.$message.success('菜单创建成功')
return true
} }
catch (error) { catch (error) {
console.error('创建菜单失败', error) console.error('创建菜单失败', error)
return false
} }
}, },
async edit() { async edit() {
try { try {
await updateMenu(modalForm.values.value.id!, filedValues) await updateMenu(modalForm.values.value.id!, filedValues)
window.$message.success('菜单更新成功') window.$message.success('菜单更新成功')
return true
} }
catch (error) { catch (error) {
console.error('更新菜单失败', error) console.error('更新菜单失败', error)
return false
} }
}, },
} }
startLoading() startLoading()
await handlers[modalType.value]() const success = await handlers[modalType.value]()
emit('success')
modalForm.close()
endLoading() endLoading()
if (success) {
emit('success')
modalForm.close()
}
} }
defineExpose({ defineExpose({
@ -127,6 +134,7 @@ defineExpose({
:form="modalForm" :form="modalForm"
:loading="submitLoading" :loading="submitLoading"
width="700px" width="700px"
label-width="120px"
> >
<pro-field <pro-field
path="menuType" path="menuType"

View File

@ -79,9 +79,9 @@ onMounted(() => {
<template #title> <template #title>
<n-button type="primary" @click="menuModalRef.openModal('add')"> <n-button type="primary" @click="menuModalRef.openModal('add')">
<template #icon> <template #icon>
<icon-park-outline-add-one /> <icon-park-outline-plus />
</template> </template>
</n-button> </n-button>
</template> </template>

View File

@ -9,12 +9,12 @@ export const searchColumns: ProSearchFormColumns<Entity.Role> = [
path: 'roleName', path: 'roleName',
}, },
{ {
title: '角色权限', title: '权限标识',
path: 'roleKey', path: 'roleKey',
}, },
{ {
title: '状态', title: '状态',
path: 'roleStatus', path: 'status',
field: 'select', field: 'select',
fieldProps: { fieldProps: {
options: [ options: [
@ -35,24 +35,18 @@ export const searchColumns: ProSearchFormColumns<Entity.Role> = [
interface RoleColumnActions { interface RoleColumnActions {
onEdit: (row: Entity.Role) => void onEdit: (row: Entity.Role) => void
onDelete: (id: number) => void onDelete: (id: number) => void
onStatusChange: (value: 0 | 1, id: number) => void onStatusChange: (id: number, value: 0 | 1) => void
} }
export function createRoleColumns(actions: RoleColumnActions): DataTableColumns<Entity.Role> { export function createRoleColumns(actions: RoleColumnActions): DataTableColumns<Entity.Role> {
return [ return [
{
title: '角色ID',
align: 'center',
key: 'roleId',
width: 80,
},
{ {
title: '角色名称', title: '角色名称',
align: 'center', align: 'center',
key: 'roleName', key: 'roleName',
}, },
{ {
title: '角色权限', title: '权限标识',
align: 'center', align: 'center',
key: 'roleKey', key: 'roleKey',
render: row => renderProCopyableText(row.roleKey), render: row => renderProCopyableText(row.roleKey),
@ -63,25 +57,18 @@ export function createRoleColumns(actions: RoleColumnActions): DataTableColumns<
key: 'remark', key: 'remark',
width: 200, width: 200,
}, },
{
title: '排序',
align: 'center',
key: 'sort',
width: 80,
},
{ {
title: '状态', title: '状态',
align: 'center', align: 'center',
key: 'roleStatus', key: 'status',
width: 100, width: 100,
render: (row) => { render: (row) => {
return ( return (
<NSwitch <NSwitch
value={row.roleStatus || 1} value={row.status}
checked-value={1} checked-value={0}
unchecked-value={0} unchecked-value={1}
onUpdateValue={(value: 0 | 1) => onUpdateValue={(value: 0 | 1) => actions.onStatusChange(row.id, value)}
actions.onStatusChange(value, row.roleId)}
> >
{{ checked: () => '启用', unchecked: () => '禁用' }} {{ checked: () => '启用', unchecked: () => '禁用' }}
</NSwitch> </NSwitch>
@ -106,19 +93,19 @@ export function createRoleColumns(actions: RoleColumnActions): DataTableColumns<
return ( return (
<NSpace justify="center"> <NSpace justify="center">
<NButton <NButton
size="small" text
type="primary" type="primary"
onClick={() => actions.onEdit(row)} onClick={() => actions.onEdit(row)}
> >
</NButton> </NButton>
<NPopconfirm <NPopconfirm
onPositiveClick={() => actions.onDelete(row.roleId)} onPositiveClick={() => actions.onDelete(row.id)}
> >
{{ {{
default: () => '确认删除该角色?', default: () => '确认删除该角色?',
trigger: () => ( trigger: () => (
<NButton size="small" type="error"> <NButton text type="error">
</NButton> </NButton>
), ),

View File

@ -20,11 +20,7 @@ const { bool: submitLoading, setTrue: startLoading, setFalse: endLoading } = use
const modalForm = createProModalForm<Partial<Entity.Role>>({ const modalForm = createProModalForm<Partial<Entity.Role>>({
omitEmptyString: false, omitEmptyString: false,
initialValues: { initialValues: {
roleName: '', status: 0,
roleKey: '',
remark: '',
sort: 0,
roleStatus: 1,
}, },
onSubmit: submitModal, onSubmit: submitModal,
}) })
@ -51,7 +47,7 @@ async function openModal(type: ModalType = 'add', data?: Partial<Entity.Role>) {
if (!data) if (!data)
return return
const { data: role } = await getRoleById(data.roleId!) const { data: role } = await getRoleById(data.id!)
modalForm.values.value = role modalForm.values.value = role
}, },
} }
@ -64,28 +60,33 @@ async function submitModal(filedValues: Partial<Entity.Role>) {
try { try {
await createRole(filedValues) await createRole(filedValues)
window.$message.success('角色创建成功') window.$message.success('角色创建成功')
return true
} }
catch (error) { catch (error) {
console.error('创建角色失败', error) console.error('创建角色失败', error)
window.$message.error('创建角色失败') return false
} }
}, },
async edit() { async edit() {
try { try {
await updateRole(modalForm.values.value.roleId!, filedValues) await updateRole(modalForm.values.value.id!, filedValues)
window.$message.success('角色更新成功') window.$message.success('角色更新成功')
return true
} }
catch (error) { catch (error) {
console.error('更新角色失败', error) console.error('更新角色失败', error)
window.$message.error('更新角色失败') return false
} }
}, },
} }
startLoading() startLoading()
await handlers[modalType.value]() const success = await handlers[modalType.value]()
endLoading() endLoading()
emit('success')
modalForm.close() if (success) {
emit('success')
modalForm.close()
}
} }
defineExpose({ defineExpose({
@ -105,29 +106,21 @@ defineExpose({
required required
title="角色名称" title="角色名称"
path="roleName" path="roleName"
placeholder="请输入角色名称"
/> />
<pro-input <pro-input
required required
title="角色权限" title="权限标识"
path="roleKey" path="roleKey"
placeholder="请输入角色权限" />
<pro-switch
title="状态"
path="status"
:field-props="{ checkedValue: 0, uncheckedValue: 1 }"
/> />
<pro-textarea <pro-textarea
class="col-span-2" class="col-span-2"
title="备注" title="备注"
path="remark" path="remark"
placeholder="请输入备注信息"
/>
<pro-digit
title="排序"
path="sort"
:field-props="{ min: 0, max: 999 }"
/>
<pro-switch
title="状态"
path="roleStatus"
:field-props="{ checkedValue: 1, uncheckedValue: 0 }"
/> />
</div> </div>
</pro-modal-form> </pro-modal-form>

View File

@ -1,6 +1,6 @@
<script setup lang="tsx"> <script setup lang="tsx">
import { createProSearchForm, useNDataTable } from 'pro-naive-ui' import { createProSearchForm, useNDataTable } from 'pro-naive-ui'
import { deleteRole, getRoleList } from '@/api' import { deleteRole, getRoleList, updateRole } from '@/api'
import { createRoleColumns, searchColumns } from './columns' import { createRoleColumns, searchColumns } from './columns'
import RoleModal from './components/RoleModal.vue' import RoleModal from './components/RoleModal.vue'
@ -34,10 +34,21 @@ async function deleteRoleData(id: number) {
} }
} }
async function updateRoleStatus(id: number, value: 0 | 1) {
try {
await updateRole(id, { status: value })
window.$message.success('角色状态更新成功')
refresh() //
}
catch {
window.$message.error('角色状态更新失败')
}
}
const tablecolumns = createRoleColumns({ const tablecolumns = createRoleColumns({
onEdit: (row: Entity.Role) => modalRef.value?.openModal('edit', row), onEdit: (row: Entity.Role) => modalRef.value?.openModal('edit', row),
onDelete: deleteRoleData, onDelete: deleteRoleData,
onStatusChange: () => {}, onStatusChange: updateRoleStatus,
}) })
async function getRolePage({ current, pageSize }: any, formData: Entity.Role[]) { async function getRolePage({ current, pageSize }: any, formData: Entity.Role[]) {
@ -47,10 +58,7 @@ async function getRolePage({ current, pageSize }: any, formData: Entity.Role[])
pageNum: current, pageNum: current,
pageSize, pageSize,
}) })
return { return data
list: data.list,
total: data.total,
}
} }
catch { catch {
return { return {
@ -81,7 +89,7 @@ async function getRolePage({ current, pageSize }: any, formData: Entity.Role[])
<template #icon> <template #icon>
<icon-park-outline-plus /> <icon-park-outline-plus />
</template> </template>
角色 角色
</n-button> </n-button>
</template> </template>
</pro-data-table> </pro-data-table>

View File

@ -14,17 +14,17 @@ export const searchColumns: ProSearchFormColumns<Entity.User> = [
}, },
{ {
title: '状态', title: '状态',
path: 'userStatus', path: 'status',
field: 'select', field: 'select',
fieldProps: { fieldProps: {
options: [ options: [
{ {
label: '启用', label: '启用',
value: 1, value: 0,
}, },
{ {
label: '禁用', label: '禁用',
value: 0, value: 1,
}, },
], ],
}, },
@ -36,7 +36,7 @@ export const searchColumns: ProSearchFormColumns<Entity.User> = [
interface UserColumnActions { interface UserColumnActions {
onEdit: (row: Entity.User) => void onEdit: (row: Entity.User) => void
onDelete: (id: number) => void onDelete: (id: number) => void
onStatusChange: (value: 0 | 1, id: number) => void onStatusChange: (id: number, value: 0 | 1) => void
} }
export function createUserColumns(actions: UserColumnActions): DataTableColumns<Entity.User> { export function createUserColumns(actions: UserColumnActions): DataTableColumns<Entity.User> {
@ -83,16 +83,15 @@ export function createUserColumns(actions: UserColumnActions): DataTableColumns<
{ {
title: '状态', title: '状态',
align: 'center', align: 'center',
key: 'userStatus', key: 'status',
width: 100, width: 100,
render: (row) => { render: (row) => {
return ( return (
<NSwitch <NSwitch
value={row.userStatus} value={row.status}
checked-value={0} checked-value={0}
unchecked-value={1} unchecked-value={1}
onUpdateValue={(value: 0 | 1) => onUpdateValue={(value: 0 | 1) => actions.onStatusChange(row.id, value)}
actions.onStatusChange(value, row.userId)}
> >
{{ checked: () => '启用', unchecked: () => '禁用' }} {{ checked: () => '启用', unchecked: () => '禁用' }}
</NSwitch> </NSwitch>
@ -122,7 +121,7 @@ export function createUserColumns(actions: UserColumnActions): DataTableColumns<
> >
</NButton> </NButton>
<NPopconfirm onPositiveClick={() => actions.onDelete(row.userId)}> <NPopconfirm onPositiveClick={() => actions.onDelete(row.id)}>
{{ {{
default: () => '确认删除', default: () => '确认删除',
trigger: () => <NButton text type="error"></NButton>, trigger: () => <NButton text type="error"></NButton>,

View File

@ -2,6 +2,7 @@
import { useBoolean } from '@/hooks' import { useBoolean } from '@/hooks'
import { createUser, getRoleOptions, getUserById, updateUser } from '@/api' import { createUser, getRoleOptions, getUserById, updateUser } from '@/api'
import { createProModalForm } from 'pro-naive-ui' import { createProModalForm } from 'pro-naive-ui'
import { Regex } from '@/constants'
interface Props { interface Props {
modalName?: string modalName?: string
@ -17,12 +18,11 @@ const emit = defineEmits<{
const { bool: submitLoading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) const { bool: submitLoading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
const modalForm = createProModalForm<Partial<Entity.User>>({ const modalForm = createProModalForm<Partial<Entity.User> & { roleIds?: number[] }>({
omitEmptyString: false, omitEmptyString: false,
initialValues: { initialValues: {
gender: 'unknown', gender: 'unknown',
userStatus: 1, status: 0,
roles: [],
}, },
onSubmit: submitModal, onSubmit: submitModal,
}) })
@ -51,8 +51,9 @@ async function openModal(type: ModalType = 'add', data?: Partial<Entity.User>) {
if (!data) if (!data)
return return
const { data: user } = await getUserById(data.userId!) const { data: user } = await getUserById(data.id!)
modalForm.values.value = user modalForm.values.value = user
modalForm.values.value.roleIds = user.roles.map(role => role.id)
}, },
} }
await handlers[type]() await handlers[type]()
@ -64,26 +65,33 @@ async function submitModal(filedValues: Partial<Entity.User>) {
try { try {
await createUser(filedValues) await createUser(filedValues)
window.$message.success('用户创建成功') window.$message.success('用户创建成功')
return true
} }
catch (error) { catch (error) {
console.error('创建用户失败', error) console.error('创建用户失败', error)
return false
} }
}, },
async edit() { async edit() {
try { try {
await updateUser(modalForm.values.value.userId!, filedValues) await updateUser(modalForm.values.value.id!, filedValues)
window.$message.success('用户更新成功') window.$message.success('用户更新成功')
return true
} }
catch (error) { catch (error) {
console.error('更新用户失败', error) console.error('更新用户失败', error)
return false
} }
}, },
} }
startLoading() startLoading()
await handlers[modalType.value]() const success = await handlers[modalType.value]()
endLoading() endLoading()
emit('success')
modalForm.close() if (success) {
emit('success')
modalForm.close()
}
} }
defineExpose({ defineExpose({
openModal, openModal,
@ -104,6 +112,12 @@ defineExpose({
path="username" path="username"
:readonly="modalType === 'edit'" :readonly="modalType === 'edit'"
/> />
<pro-input
v-if="modalType === 'add'"
required
title="密码"
path="password"
/>
<pro-input <pro-input
title="昵称" title="昵称"
path="nickName" path="nickName"
@ -112,7 +126,6 @@ defineExpose({
title="性别" title="性别"
path="gender" path="gender"
:field-props="{ :field-props="{
type: 'button',
options: [ options: [
{ label: '男', value: 'male' }, { label: '男', value: 'male' },
{ label: '女', value: 'female' }, { label: '女', value: 'female' },
@ -127,28 +140,41 @@ defineExpose({
title="邮箱" title="邮箱"
path="email" path="email"
placeholder="example@domain.com" placeholder="example@domain.com"
:rule="[
{
pattern: new RegExp(Regex.Email),
message: '请输入正确的邮箱格式',
trigger: 'blur',
},
]"
/> />
<pro-input <pro-input
title="手机号" title="手机号"
path="phone" path="phone"
placeholder="11位手机号" placeholder="11位手机号"
:rule="[
{
pattern: new RegExp(Regex.Phone),
message: '请输入正确的手机号格式',
trigger: 'blur',
},
]"
/>
<pro-select
title="角色"
path="roleIds"
:field-props="{ multiple: true, options: roleOptions }"
/>
<pro-switch
title="用户状态"
path="status"
:field-props="{ checkedValue: 0, uncheckedValue: 1 }"
/> />
<pro-textarea <pro-textarea
class="col-span-2" class="col-span-2"
title="备注" title="备注"
path="remark" path="remark"
/> />
<pro-switch
title="用户状态"
path="userStatus"
:field-props="{ checkedValue: 1, uncheckedValue: 0 }"
/>
<pro-select
class="col-span-2"
title="角色"
path="roles"
:field-props="{ multiple: true, options: roleOptions }"
/>
</div> </div>
</pro-modal-form> </pro-modal-form>
</template> </template>

View File

@ -1,6 +1,6 @@
<script setup lang="tsx"> <script setup lang="tsx">
import { createProSearchForm, useNDataTable } from 'pro-naive-ui' import { createProSearchForm, useNDataTable } from 'pro-naive-ui'
import { deleteUser, getUserList } from '@/api' import { deleteUser, getUserList, updateUser } from '@/api'
import { createUserColumns, searchColumns } from './columns' import { createUserColumns, searchColumns } from './columns'
import UserModal from './components/UserModal.vue' import UserModal from './components/UserModal.vue'
@ -33,10 +33,21 @@ async function delteteUser(id: number) {
} }
} }
async function updateUserStatus(id: number, value: 0 | 1) {
try {
await updateUser(id, { status: value })
window.$message.success('用户状态更新成功')
refresh() //
}
catch {
window.$message.error('角色状态更新失败')
}
}
const tablecolumns = createUserColumns({ const tablecolumns = createUserColumns({
onEdit: row => modalRef.value?.openModal('edit', row), onEdit: row => modalRef.value?.openModal('edit', row),
onDelete: delteteUser, onDelete: delteteUser,
onStatusChange: () => {}, onStatusChange: updateUserStatus,
}) })
async function getUserPage({ current, pageSize }: any, formData: Entity.User[]) { async function getUserPage({ current, pageSize }: any, formData: Entity.User[]) {
@ -46,10 +57,7 @@ async function getUserPage({ current, pageSize }: any, formData: Entity.User[])
pageNum: current, pageNum: current,
pageSize, pageSize,
}) })
return { return data
list: data.list,
total: data.total,
}
} }
catch { catch {
return { return {
@ -116,7 +124,7 @@ const treeData = ref([
<template #icon> <template #icon>
<icon-park-outline-plus /> <icon-park-outline-plus />
</template> </template>
用户 用户
</n-button> </n-button>
</template> </template>
</pro-data-table> </pro-data-table>