更新代码

This commit is contained in:
2025-11-11 16:34:49 +08:00
parent 8b8b0c54a1
commit 65b281fe35
21 changed files with 3536 additions and 1414 deletions
@@ -0,0 +1,335 @@
<template>
<el-dialog
:title="isEditing ? '编辑字典项' : '添加字典项'"
v-model="visible"
width="600px"
:close-on-click-modal="false"
@close="handleClose"
>
<el-form
:model="dictItemForm"
:rules="formRules"
ref="dictItemFormRef"
label-width="100px"
>
<el-form-item label="字典类型" prop="dict_type_id">
<el-input
:value="dictTypeDisplayName"
disabled
style="width: 100%"
/>
<div class="form-tip">当前字典类型{{ dictTypeDisplayName }}</div>
</el-form-item>
<el-form-item label="字典标签" prop="dict_label">
<el-input
v-model="dictItemForm.dict_label"
placeholder="请输入字典标签(显示值)"
/>
</el-form-item>
<el-form-item label="字典值" prop="dict_value">
<el-input
v-model="dictItemForm.dict_value"
placeholder="请输入字典值(存储值)"
/>
</el-form-item>
<el-form-item label="父级字典项" prop="parent_id">
<el-select
v-model="dictItemForm.parent_id"
placeholder="选择父级字典项(可选)"
clearable
filterable
style="width: 100%"
>
<el-option
v-for="item in parentDictItems"
:key="item.id"
:label="item.dict_label"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="dictItemForm.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number
v-model="dictItemForm.sort"
:min="0"
style="width: 100%"
placeholder="数字越小越靠前"
/>
</el-form-item>
<el-form-item label="颜色" prop="color">
<el-input
v-model="dictItemForm.color"
placeholder="请输入颜色值(如:#FF0000 或 red"
>
<template #append>
<el-color-picker
v-model="dictItemForm.color"
:predefine="predefineColors"
/>
</template>
</el-input>
<div class="form-tip">支持十六进制颜色值#FF0000或颜色名称red</div>
</el-form-item>
<el-form-item label="图标" prop="icon">
<el-input
v-model="dictItemForm.icon"
placeholder="请输入图标(如:✓、✗、⭐)"
/>
<div class="form-tip">支持emoji或图标字符</div>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="dictItemForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
保存
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch, computed } from 'vue';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { addDictItem, updateDictItem, getDictItems } from '@/api/dict';
interface Props {
modelValue: boolean;
dictItem?: any;
dictTypeId?: number;
dictTypeName?: string;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
dictItem: null,
dictTypeId: undefined,
dictTypeName: '',
});
const emit = defineEmits<{
'update:modelValue': [value: boolean];
success: [];
}>();
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
});
const submitting = ref(false);
const dictItemFormRef = ref<FormInstance>();
const parentDictItems = ref<any[]>([]);
// 预定义颜色
const predefineColors = [
'#ff4500',
'#ff8c00',
'#ffd700',
'#90ee90',
'#00ced1',
'#1e90ff',
'#c71585',
'#ff0000',
'#00ff00',
'#0000ff',
];
// 判断是否为编辑模式
const isEditing = computed(() => {
return !!(props.dictItem && props.dictItem.id);
});
// 表单数据
const dictItemForm = reactive({
id: null as number | null,
dict_type_id: 0,
dict_label: '',
dict_value: '',
parent_id: 0,
status: 1,
sort: 0,
color: '',
icon: '',
remark: '',
});
// 表单验证规则
const formRules: FormRules = {
dict_label: [
{ required: true, message: '请输入字典标签', trigger: 'blur' },
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' },
],
dict_value: [
{ required: true, message: '请输入字典值', trigger: 'blur' },
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' },
],
sort: [
{ required: true, message: '请输入排序', trigger: 'blur' },
{ type: 'number', min: 0, message: '排序必须大于等于 0', trigger: 'blur' },
],
color: [
{ max: 20, message: '颜色值长度不能超过 20 个字符', trigger: 'blur' },
],
icon: [
{ max: 50, message: '图标长度不能超过 50 个字符', trigger: 'blur' },
],
remark: [
{ max: 500, message: '备注长度不能超过 500 个字符', trigger: 'blur' },
],
};
// 获取父级字典项列表(排除自己)
async function fetchParentDictItems() {
if (!props.dictTypeId) return;
try {
const res = await getDictItems({ dict_type_id: props.dictTypeId });
if (res.success && res.data) {
// 如果是编辑模式,排除当前字典项
if (isEditing.value && props.dictItem) {
parentDictItems.value = res.data.filter(
(item: any) => item.id !== props.dictItem.id
);
} else {
parentDictItems.value = res.data;
}
}
} catch (error) {
console.error('获取父级字典项失败:', error);
}
}
// 计算字典类型显示名称
const dictTypeDisplayName = computed(() => {
if (props.dictTypeName) {
return props.dictTypeName;
}
if (props.dictTypeId) {
return `字典类型 ID: ${props.dictTypeId}`;
}
return '未知字典类型';
});
// 监听 dictItem 变化,填充表单数据
watch(
() => props.dictItem,
(newDictItem) => {
if (newDictItem) {
dictItemForm.id = newDictItem.id || null;
dictItemForm.dict_type_id = newDictItem.dict_type_id || props.dictTypeId || 0;
dictItemForm.dict_label = newDictItem.dict_label || '';
dictItemForm.dict_value = newDictItem.dict_value || '';
dictItemForm.parent_id = newDictItem.parent_id || 0;
dictItemForm.status = newDictItem.status !== undefined ? newDictItem.status : 1;
dictItemForm.sort = newDictItem.sort || 0;
dictItemForm.color = newDictItem.color || '';
dictItemForm.icon = newDictItem.icon || '';
dictItemForm.remark = newDictItem.remark || '';
} else {
resetForm();
}
},
{ immediate: true }
);
// 监听弹窗打开,获取父级字典项
watch(visible, (val) => {
if (val) {
if (props.dictTypeId) {
dictItemForm.dict_type_id = props.dictTypeId;
}
fetchParentDictItems();
}
});
// 重置表单
function resetForm() {
dictItemForm.id = null;
dictItemForm.dict_type_id = props.dictTypeId || 0;
dictItemForm.dict_label = '';
dictItemForm.dict_value = '';
dictItemForm.parent_id = 0;
dictItemForm.status = 1;
dictItemForm.sort = 0;
dictItemForm.color = '';
dictItemForm.icon = '';
dictItemForm.remark = '';
dictItemFormRef.value?.clearValidate();
}
// 关闭对话框
function handleClose() {
visible.value = false;
resetForm();
}
// 提交表单
async function handleSubmit() {
if (!dictItemFormRef.value) return;
try {
await dictItemFormRef.value.validate();
} catch (error) {
return;
}
if (!dictItemForm.dict_type_id) {
ElMessage.error('字典类型ID不能为空');
return;
}
submitting.value = true;
try {
const submitData: any = {
dict_type_id: dictItemForm.dict_type_id,
dict_label: dictItemForm.dict_label,
dict_value: dictItemForm.dict_value,
parent_id: dictItemForm.parent_id || 0,
status: dictItemForm.status,
sort: dictItemForm.sort,
color: dictItemForm.color || '',
icon: dictItemForm.icon || '',
remark: dictItemForm.remark || '',
};
if (isEditing.value) {
await updateDictItem(dictItemForm.id!, submitData);
ElMessage.success('字典项更新成功');
} else {
await addDictItem(submitData);
ElMessage.success('字典项添加成功');
}
emit('success');
handleClose();
} catch (error: any) {
ElMessage.error(error.message || '操作失败,请重试');
} finally {
submitting.value = false;
}
}
</script>
<style scoped lang="less">
.form-tip {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-top: 4px;
line-height: 1.4;
}
</style>
@@ -0,0 +1,316 @@
<template>
<el-dialog
:title="isEditing ? '编辑字典项' : '添加字典项'"
v-model="visible"
width="600px"
:close-on-click-modal="false"
@close="handleClose"
>
<el-form
:model="dictItemForm"
:rules="formRules"
ref="dictItemFormRef"
label-width="100px"
>
<el-form-item label="字典类型" prop="dict_type_id">
<el-select
v-model="dictItemForm.dict_type_id"
placeholder="请选择字典类型"
filterable
style="width: 100%"
:disabled="isEditing || !!dictTypeId"
>
<el-option
v-for="type in dictTypes"
:key="type.id"
:label="`${type.dict_name} (${type.dict_code})`"
:value="type.id"
/>
</el-select>
</el-form-item>
<el-form-item label="字典标签" prop="dict_label">
<el-input
v-model="dictItemForm.dict_label"
placeholder="请输入字典标签(显示值)"
/>
</el-form-item>
<el-form-item label="字典值" prop="dict_value">
<el-input
v-model="dictItemForm.dict_value"
placeholder="请输入字典值(存储值)"
/>
</el-form-item>
<!-- <el-form-item label="父级ID" prop="parent_id">
<el-input-number
v-model="dictItemForm.parent_id"
:min="0"
style="width: 100%"
placeholder="0表示顶级"
/>
<div class="form-tip">0表示顶级字典项</div>
</el-form-item> -->
<el-form-item label="状态" prop="status">
<el-radio-group v-model="dictItemForm.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序序号" prop="sort">
<el-input-number
v-model="dictItemForm.sort"
:min="0"
style="width: 100%"
placeholder="数字越小越靠前"
/>
</el-form-item>
<el-form-item label="颜色标记" prop="color">
<el-input
v-model="dictItemForm.color"
placeholder="请输入颜色值(如:#FF0000 或 red"
maxlength="20"
/>
<div class="form-tip">用于前端显示时的颜色标记</div>
</el-form-item>
<el-form-item label="图标" prop="icon">
<el-input
v-model="dictItemForm.icon"
placeholder="请输入图标名称或类名"
maxlength="50"
/>
<div class="form-tip">用于前端显示时的图标</div>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="dictItemForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
保存
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch, computed, onMounted } from 'vue'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { getDictTypes, addDictItem, updateDictItem } from '@/api/dict'
interface Props {
modelValue: boolean
dictItem?: any
dictTypeId?: number | null
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
dictItem: null,
dictTypeId: null,
})
const emit = defineEmits<{
'update:modelValue': [value: boolean]
success: []
}>()
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
const submitting = ref(false)
const dictItemFormRef = ref<FormInstance>()
const dictTypes = ref<any[]>([])
// 判断是否为编辑模式
const isEditing = computed(() => {
return !!(props.dictItem && props.dictItem.id)
})
// 表单数据
const dictItemForm = reactive({
id: null as number | null,
dict_type_id: null as number | null,
dict_label: '',
dict_value: '',
parent_id: 0,
status: 1,
sort: 0,
color: '',
icon: '',
remark: '',
})
// 表单验证规则
const formRules: FormRules = {
dict_type_id: [
{ required: true, message: '请选择字典类型', trigger: 'change' },
],
dict_label: [
{ required: true, message: '请输入字典标签', trigger: 'blur' },
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' },
],
dict_value: [
{ required: true, message: '请输入字典值', trigger: 'blur' },
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' },
],
parent_id: [
{ type: 'number', min: 0, message: '父级ID必须大于等于 0', trigger: 'blur' },
],
sort: [
{ type: 'number', min: 0, message: '排序序号必须大于等于 0', trigger: 'blur' },
],
color: [
{ max: 20, message: '颜色值长度不能超过 20 个字符', trigger: 'blur' },
],
icon: [
{ max: 50, message: '图标长度不能超过 50 个字符', trigger: 'blur' },
],
remark: [
{ max: 500, message: '备注长度不能超过 500 个字符', trigger: 'blur' },
],
}
// 获取字典类型列表
async function fetchDictTypes() {
try {
const res = await getDictTypes()
if (res.success === true && res.data) {
dictTypes.value = Array.isArray(res.data) ? res.data : []
} else if (Array.isArray(res)) {
dictTypes.value = res
} else if (res && res.data) {
dictTypes.value = Array.isArray(res.data) ? res.data : []
}
} catch (err: any) {
console.error('获取字典类型列表失败:', err)
}
}
// 监听 dictItem 变化,填充表单数据
watch(
() => props.dictItem,
(newDictItem) => {
if (newDictItem) {
dictItemForm.id = newDictItem.id || null
dictItemForm.dict_type_id = newDictItem.dict_type_id || null
dictItemForm.dict_label = newDictItem.dict_label || ''
dictItemForm.dict_value = newDictItem.dict_value || ''
dictItemForm.parent_id = newDictItem.parent_id || 0
dictItemForm.status = newDictItem.status !== undefined ? newDictItem.status : 1
dictItemForm.sort = newDictItem.sort || 0
dictItemForm.color = newDictItem.color || ''
dictItemForm.icon = newDictItem.icon || ''
dictItemForm.remark = newDictItem.remark || ''
} else {
resetForm()
}
},
{ immediate: true }
)
// 监听 dictTypeId 变化
watch(
() => props.dictTypeId,
(newTypeId) => {
if (newTypeId && !isEditing.value) {
dictItemForm.dict_type_id = newTypeId
}
},
{ immediate: true }
)
// 重置表单
function resetForm() {
dictItemForm.id = null
dictItemForm.dict_type_id = props.dictTypeId || null
dictItemForm.dict_label = ''
dictItemForm.dict_value = ''
dictItemForm.parent_id = 0
dictItemForm.status = 1
dictItemForm.sort = 0
dictItemForm.color = ''
dictItemForm.icon = ''
dictItemForm.remark = ''
dictItemFormRef.value?.clearValidate()
}
// 关闭对话框
function handleClose() {
visible.value = false
resetForm()
}
// 提交表单
async function handleSubmit() {
if (!dictItemFormRef.value) return
try {
await dictItemFormRef.value.validate()
} catch (error) {
return
}
submitting.value = true
try {
const submitData: any = {
dict_type_id: dictItemForm.dict_type_id,
dict_label: dictItemForm.dict_label,
dict_value: dictItemForm.dict_value,
parent_id: dictItemForm.parent_id,
status: dictItemForm.status,
sort: dictItemForm.sort,
color: dictItemForm.color || '',
icon: dictItemForm.icon || '',
remark: dictItemForm.remark || '',
}
if (isEditing.value) {
const res = await updateDictItem(dictItemForm.id!, submitData)
if (res.success === true || res.code === 0) {
ElMessage.success('字典项更新成功')
emit('success')
handleClose()
} else {
ElMessage.error(res.message || '更新失败,请重试')
}
} else {
const res = await addDictItem(submitData)
if (res.success === true || res.code === 0) {
ElMessage.success('字典项添加成功')
emit('success')
handleClose()
} else {
ElMessage.error(res.message || '添加失败,请重试')
}
}
} catch (error: any) {
ElMessage.error(error.message || '操作失败,请重试')
} finally {
submitting.value = false
}
}
onMounted(() => {
fetchDictTypes()
})
</script>
<style scoped lang="less">
.form-tip {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-top: 4px;
line-height: 1.4;
}
</style>
@@ -0,0 +1,422 @@
<template>
<div class="dict-item-list">
<div class="toolbar">
<div class="toolbar-left">
<el-select
v-model="selectedDictTypeId"
placeholder="选择字典类型"
clearable
filterable
style="width: 250px"
@change="handleTypeChange"
>
<el-option
v-for="type in dictTypes"
:key="type.id"
:label="`${type.dict_name} (${type.dict_code})`"
:value="type.id"
/>
</el-select>
<el-input
v-model="searchKeyword"
placeholder="搜索字典项标签或值"
clearable
style="width: 300px"
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-select
v-model="statusFilter"
placeholder="状态筛选"
clearable
style="width: 120px"
@change="handleFilter"
>
<el-option label="全部" value="" />
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</div>
<div class="toolbar-right">
<el-button
type="primary"
:disabled="!selectedDictTypeId"
@click="handleAdd"
>
<el-icon><Plus /></el-icon>
添加字典项
</el-button>
</div>
</div>
<!-- 提示信息 -->
<el-alert
v-if="!selectedDictTypeId"
title="请先选择字典类型"
type="info"
:closable="false"
style="margin-bottom: 16px"
/>
<!-- 错误状态 -->
<div v-if="error" class="error-state">
<el-alert title="加载失败" :message="error" type="error" show-icon />
<el-button type="primary" @click="fetchDictItems">重试</el-button>
</div>
<!-- 字典项列表 -->
<div v-else-if="selectedDictTypeId">
<el-table
:data="filteredDictItems"
stripe
style="width: 100%"
v-loading="loading"
>
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="dict_label" label="字典标签" min-width="150" align="center" />
<el-table-column prop="dict_value" label="字典值" min-width="150" align="center">
<template #default="{ row }">
<el-tag size="small">{{ row.dict_value }}</el-tag>
</template>
</el-table-column>
<!-- <el-table-column prop="parent_id" label="父级ID" width="100" align="center">
<template #default="{ row }">
<span>{{ row.parent_id || '-' }}</span>
</template>
</el-table-column> -->
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column prop="color" label="颜色" width="100" align="center">
<template #default="{ row }">
<div v-if="row.color" class="color-display">
<span
class="color-dot"
:style="{ backgroundColor: row.color }"
></span>
<span>{{ row.color }}</span>
</div>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="icon" label="图标" width="100" align="center">
<template #default="{ row }">
<span v-if="row.icon">{{ row.icon }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="200" align="center" show-overflow-tooltip />
<el-table-column label="创建时间" width="180" align="center">
<template #default="{ row }">
{{ formatDate(row.create_time) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right" align="center">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="handleEdit(row)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button size="small" type="danger" link @click="handleDelete(row)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 编辑对话框 -->
<DictItemEditDialog
v-model="editDialogVisible"
:dict-item="currentDictItem"
:dict-type-id="selectedDictTypeId"
@success="handleEditSuccess"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import {
ElMessage,
ElMessageBox,
} from 'element-plus'
import { Plus, Search, Edit, Delete } from '@element-plus/icons-vue'
import {
getDictTypes,
getDictItems,
deleteDictItem,
} from '@/api/dict'
import DictItemEditDialog from './DictItemEditDialog.vue'
interface Props {
selectedTypeId?: number | null
}
const props = withDefaults(defineProps<Props>(), {
selectedTypeId: null,
})
const emit = defineEmits<{
'refresh': []
}>()
const dictTypes = ref<any[]>([])
const dictItems = ref<any[]>([])
const loading = ref(false)
const error = ref('')
const searchKeyword = ref('')
const statusFilter = ref<number | ''>('')
const selectedDictTypeId = ref<number | null>(null)
const editDialogVisible = ref(false)
const currentDictItem = ref<any>(null)
// 过滤后的字典项
const filteredDictItems = computed(() => {
let result = dictItems.value
// 搜索过滤
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase()
result = result.filter(
(item) =>
(item.dict_label && item.dict_label.toLowerCase().includes(keyword)) ||
(item.dict_value && item.dict_value.toLowerCase().includes(keyword)) ||
(item.remark && item.remark.toLowerCase().includes(keyword))
)
}
// 状态过滤
if (statusFilter.value !== '') {
result = result.filter((item) => item.status === statusFilter.value)
}
return result
})
// 监听外部传入的selectedTypeId
watch(
() => props.selectedTypeId,
(newId) => {
if (newId) {
selectedDictTypeId.value = newId
fetchDictItems()
}
},
{ immediate: true }
)
// 格式化日期
function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return ''
try {
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
} catch {
return dateStr as string
}
}
// 获取字典类型列表(用于下拉选择)
async function fetchDictTypes() {
try {
const res = await getDictTypes()
if (res.success === true && res.data) {
dictTypes.value = Array.isArray(res.data) ? res.data : []
} else if (Array.isArray(res)) {
dictTypes.value = res
} else if (res && res.data) {
dictTypes.value = Array.isArray(res.data) ? res.data : []
}
} catch (err: any) {
console.error('获取字典类型列表失败:', err)
}
}
// 获取字典项列表
async function fetchDictItems() {
if (!selectedDictTypeId.value) {
dictItems.value = []
return
}
loading.value = true
error.value = ''
try {
const res = await getDictItems({ dict_type_id: selectedDictTypeId.value })
if (res.success === true && res.data) {
dictItems.value = Array.isArray(res.data) ? res.data : []
} else if (Array.isArray(res)) {
dictItems.value = res
} else if (res && res.data) {
dictItems.value = Array.isArray(res.data) ? res.data : []
} else {
error.value = res.message || '获取字典项列表失败'
dictItems.value = []
}
} catch (err: any) {
error.value = err.message || '获取字典项列表失败'
dictItems.value = []
} finally {
loading.value = false
}
}
// 字典类型改变
const handleTypeChange = () => {
fetchDictItems()
}
// 搜索
const handleSearch = () => {
// 搜索逻辑在computed中处理
}
// 筛选
const handleFilter = () => {
// 筛选逻辑在computed中处理
}
// 设置字典类型ID(供外部调用)
function setTypeId(typeId: number) {
selectedDictTypeId.value = typeId
}
// 添加字典项
function handleAdd() {
if (!selectedDictTypeId.value) {
ElMessage.warning('请先选择字典类型')
return
}
currentDictItem.value = null
editDialogVisible.value = true
}
// 编辑字典项
function handleEdit(row: any) {
currentDictItem.value = { ...row }
editDialogVisible.value = true
}
// 删除字典项
async function handleDelete(row: any) {
try {
await ElMessageBox.confirm(
`确定要删除字典项「${row.dict_label}」吗?删除后不可恢复。`,
'警告',
{ type: 'warning' }
)
loading.value = true
try {
const res = await deleteDictItem(row.id)
if (res.success === true || res.code === 0) {
ElMessage.success('删除成功')
fetchDictItems()
emit('refresh')
} else {
ElMessage.error(res.message || '删除失败')
}
} catch (err: any) {
ElMessage.error(err.message || '删除失败')
} finally {
loading.value = false
}
} catch {
// 取消删除
}
}
// 编辑成功回调
function handleEditSuccess() {
fetchDictItems()
emit('refresh')
}
// 刷新
function refresh() {
fetchDictTypes()
if (selectedDictTypeId.value) {
fetchDictItems()
}
}
defineExpose({
refresh,
setTypeId,
})
onMounted(() => {
fetchDictTypes()
if (selectedDictTypeId.value) {
fetchDictItems()
}
})
</script>
<style scoped lang="less">
.dict-item-list {
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
gap: 12px;
.toolbar-left {
display: flex;
gap: 12px;
align-items: center;
}
.toolbar-right {
display: flex;
gap: 12px;
}
}
.color-display {
display: flex;
align-items: center;
gap: 6px;
justify-content: center;
.color-dot {
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid #ddd;
}
}
}
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 220px;
padding: 32px 0 16px 0;
background: var(--el-bg-color-page);
border-radius: 5px;
gap: 16px;
}
</style>
@@ -0,0 +1,222 @@
<template>
<el-dialog
:title="isEditing ? '编辑字典类型' : '添加字典类型'"
v-model="visible"
width="600px"
:close-on-click-modal="false"
@close="handleClose"
>
<el-form
:model="dictTypeForm"
:rules="formRules"
ref="dictTypeFormRef"
label-width="100px"
>
<el-form-item label="字典编码" prop="dict_code">
<el-input
v-model="dictTypeForm.dict_code"
placeholder="请输入字典编码(如:user_status"
:disabled="isEditing"
/>
<div class="form-tip">只能包含字母数字或下划线且必须唯一</div>
</el-form-item>
<el-form-item label="字典名称" prop="dict_name">
<el-input
v-model="dictTypeForm.dict_name"
placeholder="请输入字典名称"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="dictTypeForm.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序序号" prop="sort">
<el-input-number
v-model="dictTypeForm.sort"
:min="0"
style="width: 100%"
placeholder="数字越小越靠前"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="dictTypeForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
保存
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch, computed, onMounted } from 'vue';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { addDictType, updateDictType, getDictTypes } from '@/api/dict';
interface Props {
modelValue: boolean;
dictType?: any;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
dictType: null,
});
const emit = defineEmits<{
'update:modelValue': [value: boolean];
success: [];
}>();
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
});
const submitting = ref(false);
const dictTypeFormRef = ref<FormInstance>();
// 已移除父级选择
// 判断是否为编辑模式
const isEditing = computed(() => {
return !!(props.dictType && props.dictType.id);
});
// 表单数据
const dictTypeForm = reactive({
id: null as number | null,
dict_code: '',
dict_name: '',
status: 1,
sort: 0,
remark: '',
});
// 表单验证规则
const formRules: FormRules = {
dict_code: [
{ required: true, message: '请输入字典编码', trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9_]+$/,
message: '只能包含字母、数字或下划线',
trigger: 'blur',
},
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' },
],
dict_name: [
{ required: true, message: '请输入字典名称', trigger: 'blur' },
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' },
],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
sort: [
{ required: true, message: '请输入排序序号', trigger: 'blur' },
{ type: 'number', min: 0, message: '排序序号必须大于等于 0', trigger: 'blur' },
],
remark: [
{ max: 500, message: '备注长度不能超过 500 个字符', trigger: 'blur' },
],
};
// 已移除父级选择相关数据获取
// 监听 dictType 变化,填充表单数据
watch(
() => props.dictType,
(newDictType) => {
if (newDictType) {
dictTypeForm.id = newDictType.id || null;
dictTypeForm.dict_code = newDictType.dict_code || '';
dictTypeForm.dict_name = newDictType.dict_name || '';
dictTypeForm.status = newDictType.status !== undefined ? newDictType.status : 1;
dictTypeForm.sort = newDictType.sort || 0;
dictTypeForm.remark = newDictType.remark || '';
} else {
resetForm();
}
},
{ immediate: true }
);
// 已移除父级选择监听
// 重置表单
function resetForm() {
dictTypeForm.id = null;
dictTypeForm.dict_code = '';
dictTypeForm.dict_name = '';
dictTypeForm.status = 1;
dictTypeForm.sort = 0;
dictTypeForm.remark = '';
dictTypeFormRef.value?.clearValidate();
}
// 关闭对话框
function handleClose() {
visible.value = false;
resetForm();
}
// 提交表单
async function handleSubmit() {
if (!dictTypeFormRef.value) return;
try {
await dictTypeFormRef.value.validate();
} catch (error) {
return;
}
submitting.value = true;
try {
const submitData: any = {
dict_code: dictTypeForm.dict_code,
dict_name: dictTypeForm.dict_name,
status: dictTypeForm.status,
sort: dictTypeForm.sort,
remark: dictTypeForm.remark || '',
};
if (isEditing.value) {
await updateDictType(dictTypeForm.id!, submitData);
ElMessage.success('字典类型更新成功');
} else {
await addDictType(submitData);
ElMessage.success('字典类型添加成功');
}
emit('success');
handleClose();
} catch (error: any) {
ElMessage.error(error.message || '操作失败,请重试');
} finally {
submitting.value = false;
}
}
onMounted(() => {
// 无需加载父级数据
});
</script>
<style scoped lang="less">
.form-tip {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-top: 4px;
line-height: 1.4;
}
</style>
@@ -0,0 +1,316 @@
<template>
<div class="dict-type-list">
<div class="toolbar">
<div class="toolbar-left">
<el-input
v-model="searchKeyword"
placeholder="搜索字典类型名称或编码"
clearable
style="width: 300px"
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-select
v-model="statusFilter"
placeholder="状态筛选"
clearable
style="width: 120px"
@change="handleFilter"
>
<el-option label="全部" value="" />
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</div>
<div class="toolbar-right">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
添加字典类型
</el-button>
</div>
</div>
<!-- 错误状态 -->
<div v-if="error" class="error-state">
<el-alert title="加载失败" :message="error" type="error" show-icon />
<el-button type="primary" @click="fetchDictTypes">重试</el-button>
</div>
<!-- 字典类型列表 -->
<div v-else>
<el-table
:data="filteredDictTypes"
stripe
style="width: 100%"
v-loading="loading"
@row-click="handleRowClick"
>
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="dict_name" label="字典名称" min-width="150" align="center" />
<el-table-column prop="dict_code" label="字典编码" min-width="150" align="center">
<template #default="{ row }">
<el-tag size="small">{{ row.dict_code }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="tenant_id" label="租户ID" width="100" align="center">
<template #default="{ row }">
<span>{{ row.tenant_id || 0 }}</span>
</template>
</el-table-column>
<!-- <el-table-column prop="parent_id" label="父级ID" width="100" align="center">
<template #default="{ row }">
<span>{{ row.parent_id || '-' }}</span>
</template>
</el-table-column> -->
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column prop="remark" label="备注" min-width="200" align="center" show-overflow-tooltip />
<el-table-column label="创建时间" width="180" align="center">
<template #default="{ row }">
{{ formatDate(row.create_time) }}
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button size="small" type="primary" link @click.stop="handleViewItems(row)">
<el-icon><Menu /></el-icon>
查看字典项
</el-button>
<el-button size="small" type="primary" link @click.stop="handleEdit(row)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button size="small" type="danger" link @click.stop="handleDelete(row)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 编辑对话框 -->
<DictTypeEditDialog
v-model="editDialogVisible"
:dict-type="currentDictType"
@success="handleEditSuccess"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import {
ElMessage,
ElMessageBox,
} from 'element-plus'
import { Plus, Search, Edit, Delete, Menu } from '@element-plus/icons-vue'
import {
getDictTypes,
deleteDictType,
} from '@/api/dict'
import DictTypeEditDialog from './DictTypeEditDialog.vue'
const emit = defineEmits<{
'select-type': [typeId: number]
'refresh': []
}>()
const dictTypes = ref<any[]>([])
const loading = ref(false)
const error = ref('')
const searchKeyword = ref('')
const statusFilter = ref<number | ''>('')
const editDialogVisible = ref(false)
const currentDictType = ref<any>(null)
// 过滤后的字典类型
const filteredDictTypes = computed(() => {
let result = dictTypes.value
// 搜索过滤
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase()
result = result.filter(
(type) =>
(type.dict_name && type.dict_name.toLowerCase().includes(keyword)) ||
(type.dict_code && type.dict_code.toLowerCase().includes(keyword))
)
}
// 状态过滤
if (statusFilter.value !== '') {
result = result.filter((type) => type.status === statusFilter.value)
}
return result
})
// 格式化日期
function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return ''
try {
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
} catch {
return dateStr as string
}
}
// 获取字典类型列表
async function fetchDictTypes() {
loading.value = true
error.value = ''
try {
const res = await getDictTypes()
if (res.success === true && res.data) {
dictTypes.value = Array.isArray(res.data) ? res.data : []
} else if (Array.isArray(res)) {
dictTypes.value = res
} else if (res && res.data) {
dictTypes.value = Array.isArray(res.data) ? res.data : []
} else {
error.value = res.message || '获取字典类型列表失败'
dictTypes.value = []
}
} catch (err: any) {
error.value = err.message || '获取字典类型列表失败'
dictTypes.value = []
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
// 搜索逻辑在computed中处理
}
// 筛选
const handleFilter = () => {
// 筛选逻辑在computed中处理
}
// 添加字典类型
function handleAdd() {
currentDictType.value = null
editDialogVisible.value = true
}
// 编辑字典类型
function handleEdit(row: any) {
currentDictType.value = { ...row }
editDialogVisible.value = true
}
// 查看字典项
function handleViewItems(row: any) {
emit('select-type', row.id)
}
// 行点击
function handleRowClick(row: any) {
// 可以在这里处理行点击事件
}
// 删除字典类型
async function handleDelete(row: any) {
try {
await ElMessageBox.confirm(
`确定要删除字典类型「${row.dict_name}」吗?删除后不可恢复。`,
'警告',
{ type: 'warning' }
)
loading.value = true
try {
const res = await deleteDictType(row.id)
if (res.success === true || res.code === 0) {
ElMessage.success('删除成功')
fetchDictTypes()
emit('refresh')
} else {
ElMessage.error(res.message || '删除失败')
}
} catch (err: any) {
ElMessage.error(err.message || '删除失败')
} finally {
loading.value = false
}
} catch {
// 取消删除
}
}
// 编辑成功回调
function handleEditSuccess() {
fetchDictTypes()
emit('refresh')
}
// 刷新
function refresh() {
fetchDictTypes()
}
defineExpose({
refresh,
})
onMounted(() => {
fetchDictTypes()
})
</script>
<style scoped lang="less">
.dict-type-list {
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
gap: 12px;
.toolbar-left {
display: flex;
gap: 12px;
align-items: center;
}
.toolbar-right {
display: flex;
gap: 12px;
}
}
}
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 220px;
padding: 32px 0 16px 0;
background: var(--el-bg-color-page);
border-radius: 5px;
gap: 16px;
}
</style>
+249 -522
View File
@@ -1,12 +1,8 @@
<template>
<div class="dict-container">
<div class="container-box">
<div class="header-bar">
<h2>字典管理</h2>
<div class="header-actions">
<el-button type="primary" @click="handleAddType">
<el-icon><Plus /></el-icon>
添加字典类型
</el-button>
<el-button @click="refresh">
<el-icon><Refresh /></el-icon>
刷新
@@ -15,602 +11,333 @@
</div>
<el-divider></el-divider>
<div class="dict-content">
<!-- 左侧字典类型列表 -->
<div class="dict-type-panel">
<div class="panel-header">
<h3>字典类型</h3>
<el-input
v-model="typeSearchKeyword"
placeholder="搜索字典类型"
clearable
@input="handleTypeSearch"
style="width: 200px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<el-table
:data="filteredDictTypes"
stripe
highlight-current-row
@current-change="handleTypeSelect"
v-loading="typeLoading"
style="width: 100%"
height="calc(100vh - 280px)"
>
<el-table-column prop="dict_code" label="编码" width="150" />
<el-table-column prop="dict_name" label="名称" min-width="120" />
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right" align="center">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="handleEditType(row)">
<el-icon><Edit /></el-icon>
</el-button>
<el-button size="small" type="danger" link @click="handleDeleteType(row)">
<el-icon><Delete /></el-icon>
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 右侧字典项列表 -->
<div class="dict-item-panel">
<div class="panel-header">
<h3>字典项</h3>
<div class="panel-actions">
<el-button
type="primary"
size="small"
@click="handleAddItem"
:disabled="!currentDictType"
>
<!-- 内容区域根据当前视图切换 -->
<div v-if="currentView === 'types'">
<div class="tab-content">
<div class="toolbar">
<el-button type="primary" @click="handleAddType">
<el-icon><Plus /></el-icon>
添加字典
添加字典类型
</el-button>
<div class="search-box">
<el-input
v-model="typeSearchKeyword"
placeholder="搜索字典类型名称或编码"
clearable
style="width: 300px"
@input="handleTypeSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
</div>
<el-table
:data="filteredDictTypes"
stripe
style="width: 100%"
v-loading="typeLoading"
>
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="dict_name" label="字典名称" min-width="150" align="center" />
<el-table-column prop="dict_code" label="字典编码" min-width="150" align="center">
<template #default="{ row }">
<el-tag size="small">{{ row.dict_code }}</el-tag>
</template>
</el-table-column>
<!-- <el-table-column prop="parent_id" label="父级ID" width="100" align="center">
<template #default="{ row }">
<span v-if="row.parent_id > 0">{{ row.parent_id }}</span>
<span v-else style="color: #999">-</span>
</template>
</el-table-column> -->
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
<el-table-column prop="create_time" label="创建时间" width="180" align="center">
<template #default="{ row }">
{{ formatDate(row.create_time) }}
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="handleEditType(row)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button size="small" type="primary" link @click="handleViewItems(row)">
<el-icon><List /></el-icon>
查看字典项
</el-button>
<el-button size="small" type="danger" link @click="handleDeleteType(row)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div v-else-if="currentView === 'items'">
<div class="tab-content">
<div class="toolbar">
<div>
<el-button @click="goBackToTypes">
返回
</el-button>
</div>
<div class="search-box">
<span v-if="selectedDictType">
当前字典类型{{ selectedDictType.dict_name }}{{ selectedDictType.dict_code }}
</span>
</div>
</div>
<div v-if="!currentDictType" class="empty-state">
<el-empty description="请先选择字典类型" />
</div>
<el-table
v-else
:data="dictItems"
stripe
v-loading="itemLoading"
style="width: 100%"
height="calc(100vh - 280px)"
>
<el-table-column prop="dict_label" label="标签" min-width="120" />
<el-table-column prop="dict_value" label="值" min-width="120" />
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="color" label="颜色" width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.color" :color="row.color" size="small">{{ row.color }}</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right" align="center">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="handleEditItem(row)">
<el-icon><Edit /></el-icon>
</el-button>
<el-button size="small" type="danger" link @click="handleDeleteItem(row)">
<el-icon><Delete /></el-icon>
</el-button>
</template>
</el-table-column>
</el-table>
<DictItemList :selected-type-id="selectedDictTypeId || undefined" @refresh="refreshTypesSilently" />
</div>
</div>
<!-- 字典类型编辑对话框 -->
<el-dialog
<DictTypeEditDialog
v-model="typeDialogVisible"
:title="typeDialogTitle"
width="600px"
@close="handleTypeDialogClose"
>
<el-form :model="typeForm" :rules="typeRules" ref="typeFormRef" label-width="100px">
<el-form-item label="字典编码" prop="dict_code">
<el-input
v-model="typeForm.dict_code"
placeholder="请输入字典编码(字母、数字、下划线)"
:disabled="typeForm.id > 0"
/>
</el-form-item>
<el-form-item label="字典名称" prop="dict_name">
<el-input v-model="typeForm.dict_name" placeholder="请输入字典名称" />
</el-form-item>
<el-form-item label="父级字典" prop="parent_id">
<el-select v-model="typeForm.parent_id" placeholder="请选择父级字典" clearable>
<el-option
v-for="type in dictTypes"
:key="type.id"
:label="type.dict_name"
:value="type.id"
:disabled="type.id === typeForm.id"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="typeForm.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="typeForm.sort" :min="0" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="typeForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="typeDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleTypeSubmit">确定</el-button>
</template>
</el-dialog>
:dict-type="currentDictType"
@success="handleTypeSuccess"
/>
<!-- 字典项编辑对话框 -->
<el-dialog
v-model="itemDialogVisible"
:title="itemDialogTitle"
width="600px"
@close="handleItemDialogClose"
>
<el-form :model="itemForm" :rules="itemRules" ref="itemFormRef" label-width="100px">
<el-form-item label="字典标签" prop="dict_label">
<el-input v-model="itemForm.dict_label" placeholder="请输入字典标签(显示值)" />
</el-form-item>
<el-form-item label="字典值" prop="dict_value">
<el-input
v-model="itemForm.dict_value"
placeholder="请输入字典值(存储值)"
:disabled="itemForm.id > 0"
/>
</el-form-item>
<el-form-item label="父级项" prop="parent_id">
<el-select v-model="itemForm.parent_id" placeholder="请选择父级项" clearable>
<el-option
v-for="item in dictItems"
:key="item.id"
:label="item.dict_label"
:value="item.id"
:disabled="item.id === itemForm.id"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="itemForm.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="itemForm.sort" :min="0" />
</el-form-item>
<el-form-item label="颜色" prop="color">
<el-input v-model="itemForm.color" placeholder="如:#1890ff" />
</el-form-item>
<el-form-item label="图标" prop="icon">
<el-input v-model="itemForm.icon" placeholder="如:el-icon-success" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="itemForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="itemDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleItemSubmit">确定</el-button>
</template>
</el-dialog>
<!-- 字典项独立页面内操作通过 DictItemList 完成 -->
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Edit, Delete, Refresh, Search } from '@element-plus/icons-vue'
import {
Plus,
Edit,
Delete,
Refresh,
Search,
List,
} from '@element-plus/icons-vue'
import {
getDictTypes,
addDictType,
updateDictType,
deleteDictType,
getDictItems,
addDictItem,
updateDictItem,
deleteDictItem
} from '@/api/dict'
import DictTypeEditDialog from './components/DictTypeEdit.vue'
import DictItemList from './components/DictItemList.vue'
// 数据
const dictTypes = ref([])
const filteredDictTypes = ref([])
const dictItems = ref([])
const currentDictType = ref(null)
const typeSearchKeyword = ref('')
const currentView = ref<'types' | 'items'>('types')
const typeLoading = ref(false)
const itemLoading = ref(false)
// 对话框
// 字典类型相关
const dictTypes = ref<any[]>([])
const typeSearchKeyword = ref('')
const typeDialogVisible = ref(false)
const itemDialogVisible = ref(false)
const typeFormRef = ref(null)
const itemFormRef = ref(null)
const currentDictType = ref<any>(null)
// 表单数据
const typeForm = reactive({
id: 0,
dict_code: '',
dict_name: '',
parent_id: 0,
status: 1,
sort: 0,
remark: ''
const selectedDictTypeId = ref<number | null>(null)
const selectedDictType = computed(() => {
if (!selectedDictTypeId.value) {
return null
}
return dictTypes.value.find((type) => type.id === selectedDictTypeId.value) || null
})
const itemForm = reactive({
id: 0,
dict_type_id: 0,
dict_label: '',
dict_value: '',
parent_id: 0,
status: 1,
sort: 0,
color: '',
icon: '',
remark: ''
// 过滤后的字典类型
const filteredDictTypes = computed(() => {
if (!typeSearchKeyword.value) {
return dictTypes.value
}
const keyword = typeSearchKeyword.value.toLowerCase()
return dictTypes.value.filter(
(type) =>
(type.dict_name && type.dict_name.toLowerCase().includes(keyword)) ||
(type.dict_code && type.dict_code.toLowerCase().includes(keyword))
)
})
// 表单验证规则
const typeRules = {
dict_code: [
{ required: true, message: '请输入字典编码', trigger: 'blur' },
{ pattern: /^[A-Za-z0-9_]+$/, message: '字典编码只能包含字母、数字、下划线', trigger: 'blur' }
],
dict_name: [{ required: true, message: '请输入字典名称', trigger: 'blur' }]
// 格式化日期
function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return ''
try {
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
} catch {
return dateStr as string
}
}
const itemRules = {
dict_label: [{ required: true, message: '请输入字典标签', trigger: 'blur' }],
dict_value: [{ required: true, message: '请输入字典值', trigger: 'blur' }]
}
// 计算属性
const typeDialogTitle = computed(() => {
return typeForm.id > 0 ? '编辑字典类型' : '添加字典类型'
})
const itemDialogTitle = computed(() => {
return itemForm.id > 0 ? '编辑字典项' : '添加字典项'
})
// 方法
const fetchDictTypes = async () => {
// 获取字典类型列表
async function fetchDictTypes() {
typeLoading.value = true
try {
const res = await getDictTypes()
if (res.success) {
dictTypes.value = res.data || []
filteredDictTypes.value = dictTypes.value
if (res.success === true && res.data) {
dictTypes.value = Array.isArray(res.data) ? res.data : []
} else if (Array.isArray(res)) {
dictTypes.value = res
} else {
dictTypes.value = []
ElMessage.error(res.message || '获取字典类型失败')
}
} catch (error) {
ElMessage.error('获取字典类型失败: ' + error.message)
} catch (error: any) {
ElMessage.error(error.message || '获取字典类型失败')
dictTypes.value = []
} finally {
typeLoading.value = false
}
}
const fetchDictItems = async (dictTypeId) => {
if (!dictTypeId) {
dictItems.value = []
return
}
itemLoading.value = true
try {
const res = await getDictItems({ dict_type_id: dictTypeId })
if (res.success) {
dictItems.value = res.data || []
} else {
ElMessage.error(res.message || '获取字典项失败')
}
} catch (error) {
ElMessage.error('获取字典项失败: ' + error.message)
} finally {
itemLoading.value = false
}
// 标签式视图切换已移除
// 字典类型搜索
function handleTypeSearch() {
// 搜索逻辑已在computed中处理
}
const handleTypeSearch = () => {
if (!typeSearchKeyword.value) {
filteredDictTypes.value = dictTypes.value
return
}
const keyword = typeSearchKeyword.value.toLowerCase()
filteredDictTypes.value = dictTypes.value.filter(
(type) =>
type.dict_code.toLowerCase().includes(keyword) ||
type.dict_name.toLowerCase().includes(keyword)
)
}
// 字典项视图内由子组件处理
const handleTypeSelect = (row) => {
currentDictType.value = row
if (row) {
fetchDictItems(row.id)
} else {
dictItems.value = []
}
}
const handleAddType = () => {
Object.assign(typeForm, {
id: 0,
dict_code: '',
dict_name: '',
parent_id: 0,
status: 1,
sort: 0,
remark: ''
})
// 添加字典类型
function handleAddType() {
currentDictType.value = null
typeDialogVisible.value = true
}
const handleEditType = (row) => {
Object.assign(typeForm, {
id: row.id,
dict_code: row.dict_code,
dict_name: row.dict_name,
parent_id: row.parent_id || 0,
status: row.status,
sort: row.sort || 0,
remark: row.remark || ''
})
// 编辑字典类型
function handleEditType(row: any) {
currentDictType.value = { ...row }
typeDialogVisible.value = true
}
const handleDeleteType = async (row) => {
try {
await ElMessageBox.confirm('确定要删除该字典类型吗?删除后该类型下的所有字典项也将无法使用。', '提示', {
type: 'warning'
})
const res = await deleteDictType(row.id)
if (res.success) {
ElMessage.success('删除成功')
if (currentDictType.value && currentDictType.value.id === row.id) {
currentDictType.value = null
dictItems.value = []
}
fetchDictTypes()
} else {
ElMessage.error(res.message || '删除失败')
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败: ' + error.message)
}
}
// 查看字典项
function handleViewItems(row: any) {
selectedDictTypeId.value = row.id
currentView.value = 'items'
}
const handleTypeSubmit = async () => {
if (!typeFormRef.value) return
await typeFormRef.value.validate(async (valid) => {
if (!valid) return
// 删除字典类型
async function handleDeleteType(row: any) {
try {
await ElMessageBox.confirm(
`确定要删除字典类型「${row.dict_name}」吗?删除后不可恢复。`,
'警告',
{ type: 'warning' }
)
typeLoading.value = true
try {
let res
if (typeForm.id > 0) {
res = await updateDictType(typeForm.id, typeForm)
} else {
res = await addDictType(typeForm)
}
if (res.success) {
ElMessage.success(typeForm.id > 0 ? '更新成功' : '添加成功')
typeDialogVisible.value = false
const res = await deleteDictType(row.id)
if (res.success === true) {
ElMessage.success('删除成功')
fetchDictTypes()
if (currentDictType.value && currentDictType.value.id === typeForm.id) {
fetchDictItems(typeForm.id)
}
} else {
ElMessage.error(res.message || '操作失败')
ElMessage.error(res.message || '删除失败')
}
} catch (error) {
ElMessage.error('操作失败: ' + error.message)
}
})
}
const handleTypeDialogClose = () => {
typeFormRef.value?.resetFields()
}
const handleAddItem = () => {
if (!currentDictType.value) {
ElMessage.warning('请先选择字典类型')
return
}
Object.assign(itemForm, {
id: 0,
dict_type_id: currentDictType.value.id,
dict_label: '',
dict_value: '',
parent_id: 0,
status: 1,
sort: 0,
color: '',
icon: '',
remark: ''
})
itemDialogVisible.value = true
}
const handleEditItem = (row) => {
Object.assign(itemForm, {
id: row.id,
dict_type_id: row.dict_type_id,
dict_label: row.dict_label,
dict_value: row.dict_value,
parent_id: row.parent_id || 0,
status: row.status,
sort: row.sort || 0,
color: row.color || '',
icon: row.icon || '',
remark: row.remark || ''
})
itemDialogVisible.value = true
}
const handleDeleteItem = async (row) => {
try {
await ElMessageBox.confirm('确定要删除该字典项吗?', '提示', {
type: 'warning'
})
const res = await deleteDictItem(row.id)
if (res.success) {
ElMessage.success('删除成功')
fetchDictItems(currentDictType.value.id)
} else {
ElMessage.error(res.message || '删除失败')
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败: ' + error.message)
} catch (error: any) {
ElMessage.error(error.message || '删除失败')
} finally {
typeLoading.value = false
}
} catch {
// 取消删除
}
}
const handleItemSubmit = async () => {
if (!itemFormRef.value) return
await itemFormRef.value.validate(async (valid) => {
if (!valid) return
try {
let res
if (itemForm.id > 0) {
res = await updateDictItem(itemForm.id, itemForm)
} else {
res = await addDictItem(itemForm)
}
if (res.success) {
ElMessage.success(itemForm.id > 0 ? '更新成功' : '添加成功')
itemDialogVisible.value = false
fetchDictItems(currentDictType.value.id)
} else {
ElMessage.error(res.message || '操作失败')
}
} catch (error) {
ElMessage.error('操作失败: ' + error.message)
}
})
}
const handleItemDialogClose = () => {
itemFormRef.value?.resetFields()
}
const refresh = () => {
// 字典类型操作成功回调
function handleTypeSuccess() {
fetchDictTypes()
if (currentDictType.value) {
fetchDictItems(currentDictType.value.id)
}
}
// 生命周期
// 从字典项页返回
function goBackToTypes() {
currentView.value = 'types'
}
// 子页面刷新后,静默刷新类型列表
function refreshTypesSilently() {
fetchDictTypes()
}
// 刷新
async function refresh() {
await fetchDictTypes()
ElMessage.success('刷新成功')
}
onMounted(() => {
fetchDictTypes()
})
</script>
<style scoped lang="less">
.dict-container {
.container-box {
padding: 20px;
height: 100%;
}
.header-bar {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
margin: 0;
}
.header-actions {
display: flex;
gap: 10px;
}
h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
}
.dict-content {
.header-actions {
display: flex;
gap: 20px;
flex: 1;
min-height: 0;
gap: 10px;
}
}
.dict-type-panel,
.dict-item-panel {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 15px;
background: #fff;
.tab-content {
padding: 20px 0;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h3 {
margin: 0;
font-size: 16px;
}
.search-box {
display: flex;
align-items: center;
}
.panel-actions {
display: flex;
gap: 10px;
}
}
.filter-box {
display: flex;
align-items: center;
}
}
.empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
}
.color-display {
display: flex;
align-items: center;
gap: 8px;
.color-dot {
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid #e5e5e5;
display: inline-block;
}
}
</style>
@@ -32,8 +32,8 @@
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="tenantData.status === 'enabled' ? 'success' : 'info'">
{{ tenantData.status === 'enabled' ? '启用' : '禁用' }}
<el-tag :type="tenantData.status === 1 ? 'success' : 'info'">
{{ tenantData.status === 1 ? '启用' : '禁用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="存储容量">
@@ -29,8 +29,8 @@
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="tenantForm.status" placeholder="请选择状态" style="width: 100%">
<el-option label="启用" value="enabled" />
<el-option label="禁用" value="disabled" />
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="存储容量" prop="capacity">
@@ -103,7 +103,7 @@ const tenantForm = reactive({
owner: '',
phone: '',
email: '',
status: 'enabled',
status: 1,
capacity: 0,
remark: '',
});
@@ -131,7 +131,7 @@ function resetForm() {
tenantForm.owner = '';
tenantForm.phone = '';
tenantForm.email = '';
tenantForm.status = 'enabled';
tenantForm.status = 1;
tenantForm.capacity = 0;
tenantForm.remark = '';
tenantFormRef.value?.resetFields();
@@ -146,7 +146,7 @@ function initFormData() {
tenantForm.owner = props.tenant.owner || '';
tenantForm.phone = props.tenant.phone || '';
tenantForm.email = props.tenant.email || '';
tenantForm.status = props.tenant.status || 'enabled';
tenantForm.status = props.tenant.status != null ? Number(props.tenant.status) : 1;
// capacity 后端返回的是 MB,直接使用
tenantForm.capacity = props.tenant.capacity != null ? Number(props.tenant.capacity) : 0;
tenantForm.remark = props.tenant.remark || '';
@@ -168,7 +168,7 @@ async function handleSubmit() {
owner: tenantForm.owner,
phone: tenantForm.phone || '',
email: tenantForm.email || '',
status: tenantForm.status,
status: Number(tenantForm.status),
// capacity 前后端都使用 MB
capacity: tenantForm.capacity || 0,
remark: tenantForm.remark || '',
+2 -2
View File
@@ -70,8 +70,8 @@
</el-table-column>
<el-table-column label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === 'enabled' ? 'success' : 'info'">
{{ row.status === 'enabled' ? '启用' : '禁用' }}
<el-tag :type="row.status == 1 ? 'success' : 'info'">
{{ row.status == 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
@@ -0,0 +1,210 @@
<template>
<el-dialog v-model="visible" title="修改密码" width="400px" @close="handleClose">
<el-form :model="form">
<!-- 旧密码 -->
<el-form-item label="旧密码">
<el-input
v-model="passwordForm.oldPassword"
type="password"
autocomplete="current-password"
show-password
/>
</el-form-item>
<!-- 新密码 -->
<el-form-item label="新密码">
<el-input
v-model="passwordForm.newPassword"
type="password"
autocomplete="new-password"
show-password
/>
</el-form-item>
<!-- 确认密码 -->
<el-form-item label="确认密码">
<el-input
v-model="passwordForm.confirmPassword"
type="password"
autocomplete="new-password"
show-password
/>
</el-form-item>
<!-- 错误提示 -->
<el-form-item v-if="passwordError">
<el-alert :title="passwordError" type="error" :closable="false" style="color: #f56c6c;" />
</el-form-item>
</el-form>
<!-- 对话框脚部 -->
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">修改</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { ElMessage } from "element-plus";
import { changePassword } from "@/api/user";
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
userId: {
type: Number,
default: null,
},
});
const emit = defineEmits(['update:modelValue', 'submit', 'close']);
const visible = ref(false);
const passwordError = ref("");
const form = ref<any>({
id: null,
username: "",
});
const passwordForm = ref<any>({
oldPassword: "",
newPassword: "",
confirmPassword: "",
});
// 监听 modelValue
watch(() => props.modelValue, (newVal) => {
visible.value = newVal;
if (newVal && props.userId) {
form.value.id = props.userId;
}
});
// 监听 userId 变化
watch(() => props.userId, (newVal) => {
if (newVal) {
form.value.id = newVal;
}
});
// 监听 visible 变化
watch(visible, (newVal) => {
if (!newVal) {
emit('update:modelValue', false);
}
});
// 校验密码
const validatePassword = (password: string) => {
if (!password) {
return "请输入密码";
}
if (password.length < 6) {
return "密码长度不能小于6位";
}
if (password.length > 16) {
return "密码长度不能大于16位";
}
return true;
};
// 校验确认密码
const validateConfirmPassword = (password: string, confirmPassword: string) => {
if (!confirmPassword) {
return "请再次输入密码";
}
if (confirmPassword !== password) {
return "两次输入的密码不一致";
}
return true;
};
const handleCancel = () => {
visible.value = false;
};
const handleClose = () => {
passwordForm.value = {
oldPassword: "",
newPassword: "",
confirmPassword: "",
};
passwordError.value = "";
emit('close');
};
const handleSubmit = async () => {
// 清除之前的错误
passwordError.value = "";
try {
if (!form.value.id) {
passwordError.value = "用户ID不能为空";
return;
}
// 校验旧密码
if (!passwordForm.value.oldPassword) {
passwordError.value = "请输入旧密码";
return;
}
// 校验新密码格式
const passwordCheck = validatePassword(passwordForm.value.newPassword);
if (passwordCheck !== true) {
passwordError.value = passwordCheck;
return;
}
// 校验确认密码
const confirmCheck = validateConfirmPassword(
passwordForm.value.newPassword,
passwordForm.value.confirmPassword
);
if (confirmCheck !== true) {
passwordError.value = confirmCheck;
return;
}
// 调用接口修改密码
const res = await changePassword(form.value.id, passwordForm.value);
if (res.code === 0 || res.message === '修改成功') {
ElMessage.success("密码修改成功");
visible.value = false;
emit('update:modelValue', false);
emit('submit');
} else {
passwordError.value = res.message || "密码修改失败";
}
} catch (e: any) {
const errorMsg = e?.response?.data?.message || e?.message || "操作失败";
passwordError.value = errorMsg;
}
};
// 暴露方法给父组件
defineExpose({
open: (userId: number, username: string) => {
form.value = {
id: userId,
username: username,
};
passwordForm.value = {
oldPassword: "",
newPassword: "",
confirmPassword: "",
};
passwordError.value = "";
visible.value = true;
},
});
</script>
<style lang="less" scoped>
</style>
@@ -0,0 +1,402 @@
<template>
<el-dialog v-model="visible" :title="dialogTitle" width="500px" @close="handleClose">
<el-form :model="form" ref="formRef">
<!-- 用户名 -->
<el-form-item label="用户名">
<el-input v-model="form.username" />
</el-form-item>
<!-- 昵称 -->
<el-form-item label="昵称">
<el-input v-model="form.nickname" />
</el-form-item>
<!-- 密码添加时必填 -->
<el-form-item label="密码" v-if="isAdd">
<el-input
v-model="form.password"
type="password"
autocomplete="new-password"
/>
</el-form-item>
<!-- 邮箱 -->
<el-form-item label="邮箱">
<el-input v-model="form.email" />
</el-form-item>
<!-- 部门 -->
<el-form-item label="部门">
<el-select
v-model="form.department_id"
placeholder="请选择部门"
style="width: 100%"
:loading="loadingDepartments"
clearable
@change="handleDepartmentChange"
>
<el-option
v-for="dept in departmentList"
:key="dept.id"
:label="dept.name"
:value="dept.id"
/>
</el-select>
</el-form-item>
<!-- 职位 -->
<el-form-item label="职位">
<el-select
v-model="form.position_id"
placeholder="请选择职位"
style="width: 100%"
:loading="loadingPositions"
clearable
>
<el-option
v-for="pos in positionList"
:key="pos.id"
:label="pos.name"
:value="pos.id"
/>
</el-select>
</el-form-item>
<!-- 角色 -->
<el-form-item label="角色">
<el-select
v-model="form.role"
placeholder="请选择角色"
style="width: 100%"
:loading="loadingRoles"
>
<el-option
v-for="role in roleList"
:key="role.roleId"
:label="role.roleName"
:value="role.roleId"
>
<span>{{ role.roleName }}</span>
<span style="color: #8492a6; font-size: 13px; margin-left: 8px;">
({{ role.roleCode }})
</span>
</el-option>
</el-select>
</el-form-item>
<!-- 状态 -->
<el-form-item label="状态">
<el-select v-model="form.status">
<el-option
v-for="item in statusDict"
:key="item.dict_value"
:label="item.dict_label"
:value="item.dict_value"
/>
</el-select>
</el-form-item>
</el-form>
<!-- 对话框脚部 -->
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">保存</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch } from "vue";
import { ElMessage } from "element-plus";
import {
addUser,
editUser,
getUserInfo,
} from "@/api/user";
import { getTenantPositions, getPositionsByDepartment } from "@/api/position";
import { useAuthStore } from "@/stores/auth";
const authStore = useAuthStore();
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
isEdit: {
type: Boolean,
default: false,
},
roleList: {
type: Array,
default: () => [],
},
departmentList: {
type: Array,
default: () => [],
},
positionList: {
type: Array,
default: () => [],
},
loadingRoles: {
type: Boolean,
default: false,
},
loadingDepartments: {
type: Boolean,
default: false,
},
loadingPositions: {
type: Boolean,
default: false,
},
statusDict: {
type: Array,
default: () => [],
},
tenantId: {
type: Number,
default: 0,
},
});
const emit = defineEmits(['update:modelValue', 'submit', 'close', 'fetch-positions']);
const visible = ref(false);
const formRef = ref(null);
const loadingRoles = ref(false);
const loadingDepartments = ref(false);
const loadingPositions = ref(false);
const isAdd = ref(false);
const form = ref<any>({
id: null,
username: "",
nickname: "",
password: "",
email: "",
role: null,
status: "active",
tenant_id: null,
department_id: null,
position_id: null,
});
const dialogTitle = computed(() => {
return isAdd.value ? "添加用户" : "编辑用户";
});
// 监听 modelValue
watch(() => props.modelValue, (newVal) => {
visible.value = newVal;
});
// 监听 visible 变化
watch(visible, (newVal) => {
if (!newVal) {
emit('update:modelValue', false);
}
});
const getCurrentTenantId = () => {
return props.tenantId || 0;
};
const loadUserData = async (user: any) => {
try {
// 处理两种调用方式:传递用户对象或用户 ID
const userId = typeof user === 'number' ? user : (user?.id || user?.userId);
if (!userId) {
throw new Error('未提供有效的用户 ID');
}
console.log('Loading user data for ID:', userId);
const res = await getUserInfo(userId);
console.log('User data response:', res);
const data = res.data || res;
const tenantId = getCurrentTenantId();
let roleValue = data.role || null;
let statusStr = "active";
if (typeof data.status === 'number') {
statusStr = data.status === 1 ? "active" : "inactive";
} else if (typeof data.status === 'string') {
statusStr = data.status;
}
form.value = {
id: data.id,
username: data.username,
nickname: data.nickname,
password: "",
email: data.email,
role: roleValue,
department_id: data.department_id || null,
position_id: data.position_id || null,
status: statusStr,
tenant_id: data.tenant_id || tenantId,
};
// 如果用户有部门,根据部门加载职位
if (form.value.department_id) {
await loadPositions(form.value.department_id);
}
} catch (e: any) {
console.error('Failed to load user data:', e);
const errorMsg = e?.response?.data?.message || e?.message || "加载用户失败";
ElMessage.error(errorMsg);
throw e;
}
};
const loadPositions = async (departmentId?: number) => {
loadingPositions.value = true;
try {
const tenantId = getCurrentTenantId();
let res;
if (departmentId && departmentId > 0) {
res = await getPositionsByDepartment(departmentId);
} else {
res = await getTenantPositions(tenantId);
}
// 职位列表由父组件通过 prop 传入,这里仅用于加载示例
} catch (error: any) {
console.error('获取职位列表失败:', error);
} finally {
loadingPositions.value = false;
}
};
const handleDepartmentChange = (departmentId: number | null) => {
form.value.position_id = null;
if (departmentId && departmentId > 0) {
// 让父组件加载对应部门的职位
emit('fetch-positions', departmentId);
}
};
const handleCancel = () => {
visible.value = false;
};
const handleClose = () => {
form.value = {
id: null,
username: "",
nickname: "",
password: "",
email: "",
role: null,
status: "active",
tenant_id: null,
department_id: null,
position_id: null,
};
isAdd.value = false;
emit('close');
};
const handleSubmit = async () => {
try {
if (isAdd.value) {
// 新增用户
const submitData: any = {
username: form.value.username,
nickname: form.value.nickname,
password: form.value.password,
email: form.value.email,
status: form.value.status,
};
if (form.value.role) {
submitData.role = form.value.role;
}
if (form.value.department_id) {
submitData.department_id = form.value.department_id;
}
if (form.value.position_id) {
submitData.position_id = form.value.position_id;
}
if (form.value.tenant_id) {
submitData.tenant_id = form.value.tenant_id;
}
await addUser(submitData);
ElMessage.success("添加成功");
} else {
// 编辑用户
if (!form.value.id || form.value.id === 0) {
ElMessage.error("用户ID不能为空");
return;
}
const submitData: any = {
id: form.value.id,
username: form.value.username,
nickname: form.value.nickname,
email: form.value.email,
status: form.value.status,
};
if (form.value.role) {
submitData.role = form.value.role;
}
if (form.value.department_id) {
submitData.department_id = form.value.department_id;
}
if (form.value.position_id) {
submitData.position_id = form.value.position_id;
}
if (form.value.tenant_id) {
submitData.tenant_id = form.value.tenant_id;
}
await editUser(form.value.id, submitData);
ElMessage.success("更新成功");
}
visible.value = false;
emit('submit');
} catch (e: any) {
const errorMsg = e?.response?.data?.message || e?.message || "操作失败";
ElMessage.error(errorMsg);
}
};
// 暴露方法给父组件
defineExpose({
loadUserData,
openAdd: (tenantId?: number) => {
isAdd.value = true;
form.value = {
id: 0,
username: "",
nickname: "",
password: "",
email: "",
role: null,
status: "active",
tenant_id: tenantId || getCurrentTenantId(),
department_id: null,
position_id: null,
};
visible.value = true;
},
openEdit: (user: any) => {
isAdd.value = false;
visible.value = true;
// 异步加载用户详细信息
loadUserData(user);
},
});
</script>
<style lang="less" scoped>
</style>
@@ -0,0 +1,358 @@
<template>
<div class="user-list-container">
<!-- 列表操作栏 -->
<div class="list-header">
<el-button type="primary" @click="handleAddUser">
<el-icon><Plus /></el-icon>
添加用户
</el-button>
<el-button @click="refresh">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
<!-- 用户列表表格 -->
<el-table :data="users" style="width: 100%" v-loading="loading">
<el-table-column
prop="username"
label="用户名"
width="150"
align="center"
/>
<el-table-column
prop="nickname"
label="昵称"
width="150"
align="center"
/>
<el-table-column
prop="email"
label="邮箱"
align="center"
min-width="200"
/>
<el-table-column prop="department" label="部门" width="150" align="center">
<template #default="scope">
<span>{{ scope.row.departmentName || '未分配' }}</span>
</template>
</el-table-column>
<el-table-column prop="position" label="职位" width="150" align="center">
<template #default="scope">
<span>{{ scope.row.positionName || '未分配' }}</span>
</template>
</el-table-column>
<el-table-column prop="role" label="角色" width="150" align="center">
<template #default="scope">
<el-tag :type="getRoleTagType(scope.row.roleName)">
{{ scope.row.roleName || '未分配角色' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="120">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)">
{{ getStatusLabel(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="lastLoginTime"
label="最后登录"
width="180"
align="center"
/>
<el-table-column
prop="lastLoginIp"
label="最后登录IP"
width="150"
align="center"
>
<template #default="scope">
<span>{{ scope.row.lastLoginIp || '未知' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="240" align="center" fixed="right">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">
编辑
</el-button>
<el-button size="small" type="warning" @click="handleChangePassword(scope.row)">
修改密码
</el-button>
<el-button
v-if="scope.row.username !== 'admin'"
size="small"
type="danger"
@click="handleDelete(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-bar">
<el-pagination
background
:current-page="page"
:page-size="pageSize"
:total="total"
@current-change="handlePageChange"
layout="total, prev, pager, next"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Refresh } from "@element-plus/icons-vue";
import {
getTenantUsers,
deleteUser,
getUserInfo,
} from "@/api/user";
import { getRoleByTenantId, getAllRoles } from "@/api/role";
import { getTenantDepartments } from "@/api/department";
import { getTenantPositions } from "@/api/position";
import { getDictItemsByCode } from '@/api/dict'
import { useAuthStore } from "@/stores/auth";
interface User {
id: number;
username: string;
nickname: string;
email: string;
role: string;
status: string;
lastLogin: string;
tenant_id: number;
}
const authStore = useAuthStore();
// 声明 props 和 emit
const props = defineProps({
roleList: {
type: Array,
default: () => [],
},
departmentList: {
type: Array,
default: () => [],
},
positionList: {
type: Array,
default: () => [],
},
statusDict: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['add-user', 'edit-user', 'change-password', 'refresh', 'delete-user']);
const page = ref(1);
const pageSize = ref(10);
const total = ref(0);
const users = ref<any[]>([]);
const loading = ref(false);
// 获取当前登录用户的租户ID
const getCurrentTenantId = () => {
if (authStore.user && authStore.user.tenant_id) {
return authStore.user.tenant_id;
}
const userInfo = localStorage.getItem('userInfo');
if (userInfo) {
try {
const user = JSON.parse(userInfo);
return user.tenant_id || user.tenantId || 0;
} catch (e) {
console.error('Failed to parse user info:', e);
}
}
return 0;
};
// 获取角色标签类型
const getRoleTagType = (roleName: string) => {
if (!roleName) return 'info';
if (roleName.includes('管理员')) return 'danger';
if (roleName.includes('用户')) return 'primary';
return 'success';
};
// 根据状态值获取字典显示文本
const getStatusLabel = (status: any) => {
const sval = status !== undefined && status !== null ? String(status) : '';
const item = props.statusDict.find((d: any) => String(d.dict_value) === sval || d.dict_value === status);
if (item && item.dict_label) return item.dict_label;
// 兼容旧逻辑:数字 1 表示启用
if (status === 1 || sval === '1' || sval === 'active') return '启用';
return '禁用';
}
// 根据状态字典或文本返回 el-tag 的 type
const getStatusTagType = (status: any) => {
const label = getStatusLabel(status);
if (!label) return 'info';
const l = label.toString();
if (l.includes('启用') || l.includes('正常') || l.includes('active')) return 'success';
if (l.includes('禁用') || l.includes('停用') || l.includes('inactive')) return 'danger';
return 'info';
}
const fetchUsers = async () => {
loading.value = true;
const tenantId = getCurrentTenantId();
try {
const res = await getTenantUsers(tenantId);
// 兼容接口返回的数据结构
let userList: any[] = [];
if (Array.isArray(res)) {
userList = res;
} else if (res?.data && Array.isArray(res.data)) {
userList = res.data;
} else if (res?.data?.data && Array.isArray(res.data.data)) {
userList = res.data.data;
} else if (res?.data) {
userList = res.data;
}
// 映射接口字段到表格所需结构
users.value = userList.map((item: any) => {
// 查找角色名称
let roleName = '';
let roleValue = item.role || null;
if (roleValue) {
const roleInfo = (props.roleList as any[]).find(r => r.roleId === roleValue);
roleName = roleInfo ? roleInfo.roleName : '';
}
// 查找部门名称
let departmentName = '';
const departmentId = item.department_id || null;
if (departmentId) {
const deptInfo = (props.departmentList as any[]).find(d => d.id === departmentId);
departmentName = deptInfo ? deptInfo.name : '';
}
// 查找职位名称
let positionName = '';
const positionId = item.position_id || null;
if (positionId) {
const posInfo = (props.positionList as any[]).find(p => p.id === positionId);
positionName = posInfo ? posInfo.name : '';
}
// 处理时间字段
const lastLoginTime = item.last_login_time || item.lastLoginTime || null;
const lastLoginIp = item.last_login_ip || item.lastLoginIp || null;
return {
id: item.id,
username: item.username,
nickname: item.nickname,
email: item.email,
role: roleValue,
roleName: roleName,
department_id: departmentId,
departmentName: departmentName,
position_id: positionId,
positionName: positionName,
status: item.status,
lastLoginTime: lastLoginTime
? new Date(lastLoginTime).toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
})
: "从未登录",
lastLoginIp: lastLoginIp || null,
};
});
total.value = users.value.length;
} catch (e) {
users.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
const handlePageChange = (p: number) => {
page.value = p;
};
const handleAddUser = () => {
emit('add-user');
};
const handleEdit = (user: User) => {
emit('edit-user', user);
};
const handleChangePassword = (user: User) => {
emit('change-password', user);
};
const handleDelete = (user: User) => {
ElMessageBox.confirm("确认删除该用户?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(async () => {
try {
await deleteUser(user.id);
ElMessage.success("删除成功");
emit('delete-user', user);
await fetchUsers();
} catch (e) {
ElMessage.error("删除失败");
}
});
};
const refresh = async () => {
await fetchUsers();
emit('refresh');
};
onMounted(async () => {
await fetchUsers();
});
// 暴露 fetchUsers 方法给父组件
defineExpose({
fetchUsers,
});
</script>
<style lang="less" scoped>
.user-list-container {
display: flex;
flex-direction: column;
gap: 16px;
.list-header {
display: flex;
gap: 8px;
}
.pagination-bar {
display: flex;
justify-content: flex-end;
padding: 16px 0;
}
}
</style>
+131 -434
View File
@@ -7,15 +7,16 @@
<el-icon><Plus /></el-icon>
添加用户
</el-button>
<el-button @click="refresh">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
<el-button @click="refresh">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
<el-divider></el-divider>
<!-- 用户列表表格 -->
<el-table :data="users" style="width: 100%" v-loading="loading">
<el-table-column
prop="username"
@@ -52,10 +53,10 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<el-table-column prop="status" label="状态" width="120">
<template #default="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
{{ scope.row.status === 1 ? "启用" : "禁用" }}
<el-tag :type="getStatusTagType(scope.row.status)">
{{ getStatusLabel(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
@@ -93,6 +94,8 @@
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-bar">
<el-pagination
background
@@ -104,137 +107,52 @@
/>
</div>
<!-- Dialog for add/edit -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
<el-form :model="currentForm">
<el-form-item label="用户名" v-show="dialogTitle !== '修改密码'">
<el-input v-model="form.username" />
</el-form-item>
<el-form-item label="昵称" v-if="dialogTitle !== '修改密码'">
<el-input v-model="form.nickname" />
</el-form-item>
<el-form-item label="密码" v-if="dialogTitle === '添加用户'">
<el-input
v-model="form.password"
type="password"
autocomplete="new-password"
/>
</el-form-item>
<el-form-item label="旧密码" v-if="dialogTitle === '修改密码'">
<el-input
v-model="passwordForm.oldPassword"
type="password"
autocomplete="current-password"
show-password
/>
</el-form-item>
<el-form-item label="新密码" v-if="dialogTitle === '修改密码'">
<el-input
v-model="passwordForm.newPassword"
type="password"
autocomplete="new-password"
show-password
/>
</el-form-item>
<el-form-item label="确认密码" v-if="dialogTitle === '修改密码'">
<el-input
v-model="passwordForm.confirmPassword"
type="password"
autocomplete="new-password"
show-password
/>
</el-form-item>
<el-form-item v-if="dialogTitle === '修改密码' && passwordError">
<el-alert :title="passwordError" type="error" :closable="false" style="color: #f56c6c;" />
</el-form-item>
<el-form-item label="邮箱" v-if="dialogTitle !== '修改密码'">
<el-input v-model="form.email" />
</el-form-item>
<el-form-item label="部门" v-if="dialogTitle !== '修改密码'">
<el-select
v-model="form.department_id"
placeholder="请选择部门"
style="width: 100%"
:loading="loadingDepartments"
clearable
@change="handleDepartmentChange"
>
<el-option
v-for="dept in departmentList"
:key="dept.id"
:label="dept.name"
:value="dept.id"
/>
</el-select>
</el-form-item>
<el-form-item label="职位" v-if="dialogTitle !== '修改密码'">
<el-select
v-model="form.position_id"
placeholder="请选择职位"
style="width: 100%"
:loading="loadingPositions"
clearable
>
<el-option
v-for="pos in positionList"
:key="pos.id"
:label="pos.name"
:value="pos.id"
/>
</el-select>
</el-form-item>
<el-form-item label="角色" v-if="dialogTitle !== '修改密码'">
<el-select
v-model="form.role"
placeholder="请选择角色"
style="width: 100%"
:loading="loadingRoles"
>
<el-option
v-for="role in roleList"
:key="role.roleId"
:label="role.roleName"
:value="role.roleId"
>
<span>{{ role.roleName }}</span>
<span style="color: #8492a6; font-size: 13px; margin-left: 8px;">
({{ role.roleCode }})
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="状态" v-if="dialogTitle !== '修改密码'">
<el-select v-model="form.status">
<el-option label="启用" value="active" />
<el-option label="禁用" value="disabled" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">保存</el-button>
</template>
</el-dialog>
<!-- 编辑用户对话框组件 -->
<UserEditDialog
ref="userEditRef"
:modelValue="editDialogVisible"
@update:modelValue="editDialogVisible = $event"
:is-edit="isEdit"
:role-list="roleList"
:department-list="departmentList"
:position-list="positionList"
:loading-roles="loadingRoles"
:loading-departments="loadingDepartments"
:loading-positions="loadingPositions"
:status-dict="statusDict"
:tenant-id="getCurrentTenantId()"
@submit="handleEditSuccess"
@close="editDialogVisible = false"
/>
<!-- 修改密码对话框组件 -->
<ChangePasswordDialog
ref="changePasswordRef"
:modelValue="passwordDialogVisible"
@update:modelValue="passwordDialogVisible = $event"
:user-id="currentUserId"
@submit="handlePasswordChangeSuccess"
@close="passwordDialogVisible = false"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Refresh } from "@element-plus/icons-vue";
import {
getAllUsers,
getTenantUsers,
addUser,
editUser,
deleteUser,
getUserInfo,
changePassword,
} from "@/api/user";
import { getRoleByTenantId, getAllRoles } from "@/api/role";
import { getTenantDepartments } from "@/api/department";
import { getTenantPositions, getPositionsByDepartment } from "@/api/position";
import { getDictItemsByCode } from '@/api/dict'
import { useAuthStore } from "@/stores/auth";
import UserEditDialog from './components/UserEdit.vue'
import ChangePasswordDialog from './components/ChangePassword.vue'
interface User {
id: number;
@@ -262,6 +180,32 @@ const positionList = ref<any[]>([]);
const loadingPositions = ref(false);
const loading = ref(false);
// 状态字典
const statusDict = ref<any[]>([]);
// 编辑/密码对话框状态
const editDialogVisible = ref(false);
const passwordDialogVisible = ref(false);
const editDialogTitle = ref("添加用户");
const isEdit = ref(false);
const currentUserId = ref<number | undefined>(undefined);
// 子组件 ref
const userEditRef = ref()
const changePasswordRef = ref()
const fetchStatusDict = async () => {
try {
const res = await getDictItemsByCode('user_status');
let items: any[] = [];
if (res?.data && Array.isArray(res.data)) items = res.data;
else if (Array.isArray(res)) items = res;
statusDict.value = items;
} catch (err) {
statusDict.value = [];
}
}
// 获取当前登录用户的租户ID
const getCurrentTenantId = () => {
if (authStore.user && authStore.user.tenant_id) {
@@ -300,8 +244,6 @@ const isPlatformUser = () => {
const fetchRoles = async () => {
loadingRoles.value = true;
try {
// 如果是平台用户,使用 getAllRoles 获取所有角色(包括平台角色和租户角色)
// 如果是租户员工,使用 getRoleByTenantId 获取当前租户的角色
let res;
if (isPlatformUser()) {
res = await getAllRoles();
@@ -349,10 +291,8 @@ const fetchPositions = async (departmentId?: number) => {
const tenantId = getCurrentTenantId();
let res;
if (departmentId && departmentId > 0) {
// 根据部门获取职位
res = await getPositionsByDepartment(departmentId);
} else {
// 获取所有职位
res = await getTenantPositions(tenantId);
}
if (res.code === 0 && res.data) {
@@ -368,16 +308,6 @@ const fetchPositions = async (departmentId?: number) => {
}
};
// 部门改变时,重新加载职位列表
const handleDepartmentChange = (departmentId: number | null) => {
form.value.position_id = null; // 清空职位选择
if (departmentId && departmentId > 0) {
fetchPositions(departmentId);
} else {
fetchPositions(); // 加载所有职位
}
};
// 获取角色标签类型
const getRoleTagType = (roleName: string) => {
if (!roleName) return 'info';
@@ -386,38 +316,31 @@ const getRoleTagType = (roleName: string) => {
return 'success';
};
//校验密码
const validatePassword = (password: string) => {
if (!password) {
return "请输入密码";
}
if (password.length < 6) {
return "密码长度不能小于6位";
}
if (password.length > 16) {
return "密码长度不能大于16位";
}
return true;
};
// 根据状态值获取字典显示文本
const getStatusLabel = (status: any) => {
const sval = status !== undefined && status !== null ? String(status) : '';
const item = statusDict.value.find((d: any) => String(d.dict_value) === sval || d.dict_value === status);
if (item && item.dict_label) return item.dict_label;
// 兼容旧逻辑
if (status === 1 || sval === '1' || sval === 'active') return '启用';
return '禁用';
}
// 校验确认密码,需传递新密码和确认密码
const validateConfirmPassword = (password: string, confirmPassword: string) => {
if (!confirmPassword) {
return "请再次输入密码";
}
if (confirmPassword !== password) {
return "两次输入的密码不一致";
}
return true;
};
// 根据状态字典返回 el-tag 的 type
const getStatusTagType = (status: any) => {
const label = getStatusLabel(status);
if (!label) return 'info';
const l = label.toString();
if (l.includes('启用') || l.includes('正常') || l.includes('active')) return 'success';
if (l.includes('禁用') || l.includes('停用') || l.includes('inactive')) return 'danger';
return 'info';
}
const fetchUsers = async () => {
loading.value = true;
// 传tenantid给接口
let tenantId = getCurrentTenantId ? getCurrentTenantId() : null;
try {
const res = await getTenantUsers(tenantId);
// 兼容接口返回的数据结构
let userList: any[] = [];
if (Array.isArray(res)) {
userList = res;
@@ -428,19 +351,16 @@ const fetchUsers = async () => {
} else if (res?.data) {
userList = res.data;
}
// 映射接口字段到表格所需结构
users.value = userList.map((item: any) => {
// 查找角色名称
let roleName = '';
let roleValue = item.role || null; // role 字段存储的是角色ID(数字)
let roleValue = item.role || null;
if (roleValue) {
// 通过角色ID查找角色信息
const roleInfo = roleList.value.find(r => r.roleId === roleValue);
roleName = roleInfo ? roleInfo.roleName : '';
}
// 查找部门名称
let departmentName = '';
const departmentId = item.department_id || null;
if (departmentId) {
@@ -448,7 +368,6 @@ const fetchUsers = async () => {
departmentName = deptInfo ? deptInfo.name : '';
}
// 查找职位名称
let positionName = '';
const positionId = item.position_id || null;
if (positionId) {
@@ -456,9 +375,7 @@ const fetchUsers = async () => {
positionName = posInfo ? posInfo.name : '';
}
// 处理最后登录时间,后端返回的是 last_login_time
const lastLoginTime = item.last_login_time || item.lastLoginTime || null;
// 处理最后登录IP,后端返回的是 last_login_ip
const lastLoginIp = item.last_login_ip || item.lastLoginIp || null;
return {
@@ -497,48 +414,19 @@ const fetchUsers = async () => {
};
onMounted(async () => {
// 先加载角色、部门、职位列表,再加载用户列表
await Promise.all([
fetchRoles(),
fetchDepartments(),
fetchPositions(),
fetchStatusDict(),
]);
fetchUsers();
});
const handlePageChange = (p: number) => {
page.value = p;
// 分页仅前端做演示
};
// 为添加 / 编辑对话框而添加
const dialogVisible = ref(false);
const dialogTitle = ref("");
const isEdit = ref(false);
const form = ref<any>({
id: null,
username: "",
nickname: "",
password: "",
email: "",
role: null, // role 字段存储角色ID
status: "active",
tenant_id: null,
});
const passwordForm = ref<any>({
oldPassword: "",
newPassword: "",
confirmPassword: "",
});
const passwordError = ref("");
// 根据对话框类型返回对应的表单数据
const currentForm = computed(() => {
return dialogTitle.value === '修改密码' ? passwordForm.value : form.value;
});
//刷新界面
const refresh = async () => {
loading.value = true;
try {
@@ -546,6 +434,7 @@ const refresh = async () => {
fetchRoles(),
fetchDepartments(),
fetchPositions(),
fetchStatusDict(),
]);
await fetchUsers();
ElMessage.success('刷新成功');
@@ -556,237 +445,51 @@ const refresh = async () => {
}
};
// 添加用户
const handleAddUser = () => {
dialogTitle.value = "添加用户";
editDialogTitle.value = "添加用户";
isEdit.value = false;
// 从本地缓存读取tenant_id
let tenantId = null;
const cachedUser = localStorage.getItem("userInfo");
if (cachedUser) {
try {
const userInfo = JSON.parse(cachedUser);
tenantId = userInfo.tenant_id || null;
} catch (e) {
tenantId = null;
}
// 清空编辑对话框的数据,让子组件处理
if (userEditRef.value) {
userEditRef.value.openAdd(getCurrentTenantId());
}
form.value = {
id: 0, // 添加用户时ID为0
username: "",
nickname: "",
password: "",
email: "",
role: null, // role 字段存储角色ID
department_id: null,
position_id: null,
status: "active",
tenant_id: tenantId,
};
// 重新加载职位列表(所有职位)
fetchPositions();
dialogVisible.value = true;
editDialogVisible.value = true;
};
// 编辑用户
const handleEdit = async (user: User) => {
dialogTitle.value = "编辑用户";
editDialogTitle.value = "编辑用户";
isEdit.value = true;
try {
const res = await getUserInfo(user.id);
const data = res.data || res;
// 获取当前租户ID
const tenantId = getCurrentTenantId();
// 处理角色:data.role 存储的是角色ID(数字)
let roleValue = data.role || null;
// 处理状态:后端返回的是数字(0或1),前端使用字符串("active"或"inactive"
let statusStr = "active";
if (typeof data.status === 'number') {
statusStr = data.status === 1 ? "active" : "inactive";
} else if (typeof data.status === 'string') {
statusStr = data.status;
}
form.value = {
id: data.id,
username: data.username,
nickname: data.nickname,
password: "",
email: data.email,
role: roleValue, // role 字段存储角色ID
department_id: data.department_id || null,
position_id: data.position_id || null,
status: statusStr,
tenant_id: data.tenant_id || tenantId,
};
// 如果用户有部门,根据部门加载职位列表
if (form.value.department_id) {
fetchPositions(form.value.department_id);
} else {
fetchPositions(); // 加载所有职位
}
} catch (e) {
ElMessage.error("加载用户失败");
return;
}
dialogVisible.value = true;
};
const submitForm = async () => {
// 清除之前的错误
passwordError.value = "";
try {
if (dialogTitle.value === "修改密码") {
// 校验旧密码
if (!passwordForm.value.oldPassword) {
passwordError.value = "请输入旧密码";
return;
}
// 校验密码格式
const passwordCheck = validatePassword(passwordForm.value.newPassword);
if (passwordCheck !== true) {
passwordError.value = passwordCheck;
return;
}
// 校验确认密码
const confirmCheck = validateConfirmPassword(
passwordForm.value.newPassword,
passwordForm.value.confirmPassword
);
if (confirmCheck !== true) {
passwordError.value = confirmCheck;
return;
}
const res = await changePassword(form.value.id, passwordForm.value);
// 检查返回结果
if (res.code === 0) {
// 先显示成功消息
ElMessage.success({
message: "密码修改成功",
type: "success",
customClass: "my-el-message-success",
showClose: true,
center: true,
offset: 60,
});
// 延迟关闭弹窗,确保消息已经渲染
setTimeout(() => {
dialogVisible.value = false;
// 重置密码表单
passwordForm.value = {
oldPassword: "",
newPassword: "",
confirmPassword: "",
};
}, 100);
} else {
passwordError.value = res.message || "密码修改失败";
}
} else if (isEdit.value) {
// 检查ID是否存在
if (!form.value.id || form.value.id === 0) {
ElMessage.error("用户ID不能为空,请重新选择用户");
return;
}
// 构建提交数据
const submitData: any = {
id: form.value.id,
username: form.value.username,
nickname: form.value.nickname,
email: form.value.email,
status: form.value.status,
};
// role 字段存储角色ID
if (form.value.role) {
submitData.role = form.value.role; // 后端接收 role 参数
}
// 部门和职位ID
if (form.value.department_id) {
submitData.department_id = form.value.department_id;
}
if (form.value.position_id) {
submitData.position_id = form.value.position_id;
}
if (form.value.tenant_id) {
submitData.tenant_id = form.value.tenant_id;
}
console.log('编辑用户提交数据:', submitData); // 调试日志
await editUser(form.value.id, submitData);
ElMessage.success({
message: "更新成功",
type: "success",
customClass: "my-el-message-success",
showClose: true,
center: true,
offset: 60,
});
dialogVisible.value = false;
fetchUsers();
} else {
// 构建提交数据
const submitData: any = {
username: form.value.username,
nickname: form.value.nickname,
password: form.value.password,
email: form.value.email,
status: form.value.status,
};
// role 字段存储角色ID
if (form.value.role) {
submitData.role = form.value.role; // 后端接收 role 参数
}
// 部门和职位ID
if (form.value.department_id) {
submitData.department_id = form.value.department_id;
}
if (form.value.position_id) {
submitData.position_id = form.value.position_id;
}
if (form.value.tenant_id) {
submitData.tenant_id = form.value.tenant_id;
}
console.log('添加用户提交数据:', submitData); // 调试日志
await addUser(submitData);
ElMessage.success({
message: "添加成功",
type: "success",
customClass: "my-el-message-success",
showClose: true,
center: true,
offset: 60,
});
dialogVisible.value = false;
fetchUsers();
}
} catch (e: any) {
// 显示具体的错误信息
const errorMsg = e?.response?.data?.message || e?.message || "操作失败";
if (dialogTitle.value === "修改密码") {
passwordError.value = errorMsg;
} else {
ElMessage.error(errorMsg);
// 让子组件 UserEdit 通过 loadUserData 加载用户信息
if (userEditRef.value) {
try {
await userEditRef.value.loadUserData(user.id);
} catch (e) {
console.error("加载用户详情失败:", e);
ElMessage.error("加载用户失败");
return;
}
}
editDialogVisible.value = true;
};
// 编辑成功回调
const handleEditSuccess = () => {
fetchUsers();
};
// 职位加载回调
const handleFetchPositions = (departmentId: number | null) => {
if (departmentId && departmentId > 0) {
fetchPositions(departmentId);
} else {
fetchPositions();
}
};
// 删除用户
const handleDelete = async (user: User) => {
ElMessageBox.confirm("确认删除该用户?", "提示", {
confirmButtonText: "确定",
@@ -803,21 +506,15 @@ const handleDelete = async (user: User) => {
});
};
//修改密码
const handleChangePassword = async (user: User) => {
dialogTitle.value = "修改密码";
isEdit.value = true;
passwordError.value = "";
form.value = {
id: user.id,
username: user.username,
};
passwordForm.value = {
oldPassword: "",
newPassword: "",
confirmPassword: "",
};
dialogVisible.value = true;
// 修改密码
const handleChangePassword = (user: User) => {
currentUserId.value = user.id;
passwordDialogVisible.value = true;
};
// 修改密码成功回调
const handlePasswordChangeSuccess = () => {
fetchUsers();
};
</script>