更新代码
This commit is contained in:
@@ -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
@@ -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 || '',
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user