更新架构
This commit is contained in:
@@ -1,332 +0,0 @@
|
||||
<template>
|
||||
<!-- 添加/编辑Banner对话框 -->
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
:title="dialogTitle"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@update:model-value="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
:model="currentBanner"
|
||||
label-width="100px"
|
||||
:rules="formRules"
|
||||
ref="bannerFormRef"
|
||||
>
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input
|
||||
v-model="currentBanner.title"
|
||||
placeholder="请输入Banner标题"
|
||||
maxlength="255"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="简介" prop="desc">
|
||||
<el-input
|
||||
v-model="currentBanner.desc"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入Banner简介"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="跳转地址" prop="url">
|
||||
<el-input
|
||||
v-model="currentBanner.url"
|
||||
placeholder="例如:https://www.example.com 或 /page/detail"
|
||||
/>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary);">
|
||||
支持外部链接(http://)和内部路由(/)
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Banner图片" prop="image">
|
||||
<el-upload
|
||||
class="image-uploader"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleImageSuccess"
|
||||
:on-error="handleImageError"
|
||||
:before-upload="beforeImageUpload"
|
||||
accept="*"
|
||||
>
|
||||
<img
|
||||
v-if="currentBanner.image"
|
||||
:src="getImageUrl(currentBanner.image)"
|
||||
class="image-preview"
|
||||
/>
|
||||
<div v-else class="upload-placeholder">
|
||||
<el-icon class="image-uploader-icon"><Plus /></el-icon>
|
||||
<div class="el-upload__text">点击上传图片</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary);">
|
||||
建议尺寸:1920x600,支持 jpg、png、gif 格式,大小不超过 5MB
|
||||
</div>
|
||||
<el-button
|
||||
v-if="currentBanner.image"
|
||||
size="small"
|
||||
type="danger"
|
||||
text
|
||||
@click="handleRemoveImage"
|
||||
style="margin-top: 8px;"
|
||||
>
|
||||
删除图片
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number
|
||||
v-model="currentBanner.sort"
|
||||
:min="0"
|
||||
placeholder="数字越小越靠前"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary);">
|
||||
数字越小,排序越靠前
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleSave">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { ElMessage, ElForm } from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
|
||||
// 定义Banner数据类型
|
||||
interface Banner {
|
||||
id: number;
|
||||
title: string;
|
||||
desc: string;
|
||||
url: string;
|
||||
image: string;
|
||||
sort: number;
|
||||
create_time?: number;
|
||||
update_time?: number;
|
||||
delete_time?: number;
|
||||
}
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
banner: Partial<Banner> | null;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
banner: null,
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: "update:visible", value: boolean): void;
|
||||
(e: "save", banner: Partial<Banner>): void;
|
||||
(e: "cancel"): void;
|
||||
}>();
|
||||
|
||||
// 表单引用
|
||||
const bannerFormRef = ref<InstanceType<typeof ElForm>>();
|
||||
|
||||
// 当前操作的Banner
|
||||
const currentBanner = ref<Partial<Banner>>({
|
||||
id: 0,
|
||||
title: "",
|
||||
desc: "",
|
||||
url: "",
|
||||
image: "",
|
||||
sort: 0,
|
||||
});
|
||||
|
||||
// 上传配置
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
const uploadUrl = ref(API_BASE_URL + "/admin/uploadfiles");
|
||||
const uploadHeaders = ref({
|
||||
Authorization: "Bearer " + (localStorage.getItem("token") || ""),
|
||||
});
|
||||
|
||||
// 对话框标题
|
||||
const dialogTitle = computed(() => {
|
||||
return props.banner?.id && props.banner.id > 0 ? "编辑Banner" : "添加Banner";
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = ref({
|
||||
title: [{ required: true, message: "请输入Banner标题", trigger: "blur" }],
|
||||
sort: [{ required: true, message: "请输入排序号", trigger: "blur" }],
|
||||
});
|
||||
|
||||
// 监听props变化,更新当前Banner
|
||||
watch(
|
||||
() => props.banner,
|
||||
(newBanner) => {
|
||||
if (newBanner) {
|
||||
currentBanner.value = {
|
||||
...newBanner,
|
||||
};
|
||||
} else {
|
||||
// 重置表单
|
||||
currentBanner.value = {
|
||||
id: 0,
|
||||
title: "",
|
||||
desc: "",
|
||||
url: "",
|
||||
image: "",
|
||||
sort: 0,
|
||||
};
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听props.visible变化
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVisible) => {
|
||||
if (newVisible && (!props.banner || !props.banner.id)) {
|
||||
// 新增时重置表单
|
||||
currentBanner.value = {
|
||||
id: 0,
|
||||
title: "",
|
||||
desc: "",
|
||||
url: "",
|
||||
image: "",
|
||||
sort: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 取消操作
|
||||
const handleCancel = () => {
|
||||
emit("update:visible", false);
|
||||
emit("cancel");
|
||||
};
|
||||
|
||||
// 处理对话框关闭
|
||||
const handleDialogClose = (value: boolean) => {
|
||||
if (!value) {
|
||||
emit("update:visible", false);
|
||||
emit("cancel");
|
||||
}
|
||||
};
|
||||
|
||||
// 图片上传前校验
|
||||
const beforeImageUpload = (file: any) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
// 图片上传成功
|
||||
const handleImageSuccess = (response: any) => {
|
||||
if (response.code === 200) {
|
||||
// 直接保存相对路径,不拼接API_BASE_URL
|
||||
currentBanner.value.image = response.data.url || response.data.path;
|
||||
ElMessage.success("图片上传成功");
|
||||
} else {
|
||||
ElMessage.error(response.msg || "图片上传失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 图片上传失败
|
||||
const handleImageError = () => {
|
||||
ElMessage.error("图片上传失败,请重试");
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const handleRemoveImage = () => {
|
||||
currentBanner.value.image = "";
|
||||
ElMessage.success("图片已删除");
|
||||
};
|
||||
|
||||
// 获取图片完整URL(用于预览)
|
||||
const getImageUrl = (imagePath: string) => {
|
||||
if (!imagePath) return "";
|
||||
// 如果是绝对路径,直接返回
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
// 如果是相对路径,拼接API基础地址
|
||||
return API_BASE_URL + imagePath;
|
||||
};
|
||||
|
||||
// 保存Banner
|
||||
const handleSave = async () => {
|
||||
// 表单验证
|
||||
if (!bannerFormRef.value) return;
|
||||
const valid = await bannerFormRef.value.validate();
|
||||
if (!valid) return;
|
||||
|
||||
// 准备提交数据
|
||||
const payload = { ...currentBanner.value };
|
||||
|
||||
// 触发保存事件
|
||||
emit("save", payload);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
/* 对话框样式精简 */
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 20px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 图片上传样式 */
|
||||
.image-uploader {
|
||||
:deep(.el-upload) {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
height: auto;
|
||||
max-height: 300px;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
.image-uploader-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.el-upload__text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,301 +0,0 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>Banner管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAddBanner">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
添加Banner
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table
|
||||
:data="bannerList"
|
||||
style="width: 100%"
|
||||
border
|
||||
v-loading="loading"
|
||||
element-loading-text="正在加载..."
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
|
||||
<el-table-column prop="title" label="标题" min-width="150" />
|
||||
|
||||
<el-table-column label="图片" width="200" align="center">
|
||||
<template #default="scope">
|
||||
<el-image
|
||||
v-if="scope.row.image"
|
||||
:src="getImageUrl(scope.row.image)"
|
||||
:preview-src-list="[getImageUrl(scope.row.image)]"
|
||||
:preview-teleported="true"
|
||||
fit="cover"
|
||||
style="width: 100px; height: 60px; border-radius: 4px; cursor: pointer;"
|
||||
/>
|
||||
<span v-else style="color: #ccc;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="desc" label="简介" min-width="200" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.desc">{{ scope.row.desc }}</span>
|
||||
<span v-else style="color: #ccc;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="url" label="跳转地址" min-width="200" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-link v-if="scope.row.url" :href="scope.row.url" target="_blank" type="primary">
|
||||
{{ scope.row.url }}
|
||||
</el-link>
|
||||
<span v-else style="color: #ccc;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="sort"
|
||||
label="排序"
|
||||
width="100"
|
||||
align="center"
|
||||
sortable
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag>{{ scope.row.sort || 0 }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<el-button size="small" text @click="handleEditBanner(scope.row)">
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
<span>编辑</span>
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
type="danger"
|
||||
@click="handleDeleteBanner(scope.row)"
|
||||
>
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
<span>删除</span>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 引入编辑组件 -->
|
||||
<BannerEdit
|
||||
v-model:visible="dialogVisible"
|
||||
:banner="dialogBanner"
|
||||
@save="handleBannerSave"
|
||||
@cancel="handleBannerCancel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Edit, Delete, Refresh } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getBanners,
|
||||
createBanner,
|
||||
editBanner,
|
||||
deleteBanner,
|
||||
} from "@/api/banner";
|
||||
import BannerEdit from "./components/edit.vue";
|
||||
|
||||
// 定义Banner数据类型
|
||||
interface Banner {
|
||||
id: number;
|
||||
title: string;
|
||||
desc: string;
|
||||
url: string;
|
||||
image: string;
|
||||
sort: number;
|
||||
create_time?: number;
|
||||
update_time?: number;
|
||||
delete_time?: number;
|
||||
}
|
||||
|
||||
// Banner列表
|
||||
const bannerList = ref<Banner[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 对话框相关变量
|
||||
const dialogVisible = ref(false);
|
||||
const dialogBanner = ref<Partial<Banner> | null>(null);
|
||||
|
||||
// 获取Banner列表
|
||||
const fetchBanners = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await getBanners();
|
||||
if (result.code === 200) {
|
||||
bannerList.value = result.data || [];
|
||||
} else {
|
||||
ElMessage.error("获取Banner列表失败: " + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("获取Banner列表失败: " + (error as Error).message);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新界面
|
||||
async function refresh() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await fetchBanners();
|
||||
ElMessage.success("刷新成功");
|
||||
} catch (error) {
|
||||
ElMessage.error("刷新失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加Banner
|
||||
const handleAddBanner = () => {
|
||||
dialogBanner.value = {
|
||||
id: 0,
|
||||
title: "",
|
||||
desc: "",
|
||||
url: "",
|
||||
image: "",
|
||||
sort: 0,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑Banner
|
||||
const handleEditBanner = (banner: Banner) => {
|
||||
dialogBanner.value = { ...banner };
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除Banner
|
||||
const handleDeleteBanner = (banner: Banner) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除Banner "${banner.title}" 吗?`,
|
||||
"确认删除",
|
||||
{
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
const result = await deleteBanner(banner.id);
|
||||
if (result.code === 200) {
|
||||
ElMessage.success("删除成功");
|
||||
fetchBanners();
|
||||
} else {
|
||||
ElMessage.error("删除失败: " + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("删除失败: " + (error as Error).message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 处理Banner保存
|
||||
const handleBannerSave = async (banner: Partial<Banner>) => {
|
||||
try {
|
||||
const payload = { ...banner };
|
||||
|
||||
// 判断是新增还是编辑
|
||||
if (!banner.id || banner.id === 0) {
|
||||
// 新增Banner
|
||||
const result = await createBanner(payload as Banner);
|
||||
if (result.code === 200) {
|
||||
ElMessage.success(result.msg || "Banner添加成功");
|
||||
dialogVisible.value = false;
|
||||
await fetchBanners();
|
||||
} else {
|
||||
ElMessage.error(result.msg || "添加失败");
|
||||
}
|
||||
} else {
|
||||
// 编辑已存在的Banner
|
||||
const result = await editBanner(banner.id!, payload as Banner);
|
||||
if (result.code === 200) {
|
||||
ElMessage.success(result.msg || "更新成功");
|
||||
dialogVisible.value = false;
|
||||
await fetchBanners();
|
||||
} else {
|
||||
ElMessage.error(result.msg || "更新失败");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("操作失败: " + (error as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理Banner取消
|
||||
const handleBannerCancel = () => {
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
// 获取图片完整URL(用于显示)
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api';
|
||||
const getImageUrl = (imagePath: string) => {
|
||||
if (!imagePath) return '';
|
||||
// 如果是绝对路径,直接返回
|
||||
if (imagePath.startsWith('http')) return imagePath;
|
||||
// 如果是相对路径,拼接API基础地址
|
||||
return API_BASE_URL + imagePath;
|
||||
};
|
||||
|
||||
// 组件挂载时加载Banner列表
|
||||
onMounted(() => {
|
||||
fetchBanners();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.header-bar h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
:deep(.el-table) {
|
||||
border-radius: 0;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body td) {
|
||||
padding: 12px 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
@@ -291,7 +291,7 @@ const handleClose = () => {
|
||||
text-align: center;
|
||||
|
||||
em {
|
||||
color: #409eff;
|
||||
color: #3973ff;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<el-icon><Grid /></el-icon>
|
||||
<span>文件分组</span>
|
||||
</div>
|
||||
<el-button
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleCreateCategory"
|
||||
@@ -160,7 +160,7 @@
|
||||
<video :src="getFileUrl(file.url)" alt="file" />
|
||||
</div>
|
||||
<div v-else-if="isDocument(file)">
|
||||
<el-icon :size="48" color="#409eff">
|
||||
<el-icon :size="48" color="#3973ff">
|
||||
<Document />
|
||||
</el-icon>
|
||||
</div>
|
||||
@@ -584,7 +584,7 @@ const handleRenameCategorySuccess = () => {
|
||||
selectedGroup.value.id === currentRenameCategoryId.value
|
||||
) {
|
||||
const updatedGroup = groups.value.find(
|
||||
(g: any) => g.id === currentRenameCategoryId.value
|
||||
(g: any) => g.id === currentRenameCategoryId.value,
|
||||
);
|
||||
if (updatedGroup) {
|
||||
selectGroup(updatedGroup);
|
||||
@@ -633,7 +633,7 @@ const loadFiles = async () => {
|
||||
cateId,
|
||||
currentPage.value,
|
||||
pageSize.value,
|
||||
fileSearchQuery.value
|
||||
fileSearchQuery.value,
|
||||
);
|
||||
if (res.code === 200 && res.data) {
|
||||
// 更新总数
|
||||
@@ -646,7 +646,7 @@ const loadFiles = async () => {
|
||||
} else {
|
||||
// 更新普通分组的文件数量
|
||||
const group = groups.value.find(
|
||||
(g) => g.id === selectedGroup.value!.id
|
||||
(g) => g.id === selectedGroup.value!.id,
|
||||
);
|
||||
if (group) {
|
||||
group.total = res.data.total || 0;
|
||||
@@ -942,8 +942,8 @@ function handleDelete(row) {
|
||||
res && typeof res.code !== "undefined"
|
||||
? res
|
||||
: res && res.data
|
||||
? res.data
|
||||
: res;
|
||||
? res.data
|
||||
: res;
|
||||
if (resp.code === 200) {
|
||||
ElMessage.success("删除成功");
|
||||
loadFiles();
|
||||
@@ -987,12 +987,12 @@ onMounted(() => {
|
||||
gap: 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1030,13 +1030,13 @@ onMounted(() => {
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 8px;
|
||||
background: #f5f7fa;
|
||||
// background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #e6f0ff;
|
||||
// background: #e6f0ff;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
@@ -1059,7 +1059,7 @@ onMounted(() => {
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
color: var(--el-text-color-primary);
|
||||
margin-bottom: 4px;
|
||||
|
||||
.group-icon {
|
||||
@@ -1254,7 +1254,7 @@ onMounted(() => {
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #f5f7fa;
|
||||
// background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
|
||||
.preview-image-wrapper {
|
||||
|
||||
@@ -1,514 +0,0 @@
|
||||
<template>
|
||||
<!-- 添加/编辑菜单对话框 -->
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
:title="dialogTitle"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
@update:model-value="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
:model="currentMenu"
|
||||
label-width="100px"
|
||||
:rules="formRules"
|
||||
ref="menuFormRef"
|
||||
>
|
||||
<el-form-item label="父级菜单" prop="pid">
|
||||
<el-tree-select
|
||||
v-model="currentMenu.pid"
|
||||
:data="parentMenuOptions"
|
||||
:props="{ value: 'id', label: 'title', children: 'children' }"
|
||||
placeholder="请选择父级菜单"
|
||||
clearable
|
||||
check-strictly
|
||||
:render-after-expand="false"
|
||||
style="width: 100%"
|
||||
@change="handleParentChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="菜单名称" prop="title">
|
||||
<el-input v-model="currentMenu.title" placeholder="请输入菜单名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="菜单类型" prop="type">
|
||||
<el-radio-group v-model="currentMenu.type" style="width: 100%">
|
||||
<el-radio-button :value="1">目录</el-radio-button>
|
||||
<el-radio-button :value="2">页面</el-radio-button>
|
||||
<el-radio-button :value="3">外链</el-radio-button>
|
||||
<el-radio-button :value="4">单页</el-radio-button>
|
||||
</el-radio-group>
|
||||
<div
|
||||
style="
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
"
|
||||
>
|
||||
<div>
|
||||
• 目录:只有路由地址,用于<span
|
||||
style="color: var(--el-color-primary)"
|
||||
>目录管理</span
|
||||
>和<span style="color: var(--el-color-primary)">菜单分组</span>
|
||||
</div>
|
||||
<div>
|
||||
• 页面:有路由和组件地址,用于<span
|
||||
style="color: var(--el-color-primary)"
|
||||
>页面管理</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
• 外链:无路由和组件,用于<span
|
||||
style="color: var(--el-color-primary)"
|
||||
>外链管理</span
|
||||
>和<span style="color: var(--el-color-primary)">权限控制</span>
|
||||
</div>
|
||||
<div>
|
||||
• 单页:根据路由从单页表获取内容显示,无需填写组件路径
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="路由地址" prop="path" v-if="currentMenu.type !== 3">
|
||||
<el-input v-model="currentMenu.path" placeholder="例如:/system" />
|
||||
<div v-if="currentMenu.type === 4" style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary);">
|
||||
单页类型:路由需与单页管理中的路由一致,系统会自动从单页表获取内容
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
label="组件路径"
|
||||
prop="component_path"
|
||||
v-if="currentMenu.type === 2"
|
||||
>
|
||||
<el-input
|
||||
v-model="currentMenu.component_path"
|
||||
placeholder="例如:/apps/knowledge/index.vue"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
label="外链地址"
|
||||
prop="link_url"
|
||||
v-if="currentMenu.type === 3"
|
||||
>
|
||||
<el-input
|
||||
v-model="currentMenu.link_url"
|
||||
required
|
||||
placeholder="例如:https://www.baidu.com"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="菜单图片" prop="image">
|
||||
<el-upload
|
||||
class="image-uploader"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleImageSuccess"
|
||||
:on-error="handleImageError"
|
||||
:before-upload="beforeImageUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<img v-if="currentMenu.image" :src="getImageUrl(currentMenu.image)" class="image-preview" />
|
||||
<div v-else class="upload-placeholder">
|
||||
<el-icon class="image-uploader-icon"><Plus /></el-icon>
|
||||
<div class="el-upload__text">点击上传图片</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary);">
|
||||
建议尺寸:400x300,支持 jpg、png、gif 格式,大小不超过 2MB
|
||||
</div>
|
||||
<el-button
|
||||
v-if="currentMenu.image"
|
||||
size="small"
|
||||
type="danger"
|
||||
text
|
||||
@click="handleRemoveImage"
|
||||
style="margin-top: 8px;"
|
||||
>
|
||||
删除图片
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="描述" prop="desc">
|
||||
<el-input
|
||||
v-model="currentMenu.desc"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="例如:系统管理"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number
|
||||
v-model="currentMenu.sort"
|
||||
:min="0"
|
||||
placeholder="数字越小越靠前"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleSave">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { ElMessage, ElForm } from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
|
||||
// 定义菜单数据类型
|
||||
interface Menu {
|
||||
id: number;
|
||||
pid: number;
|
||||
title: string;
|
||||
type: number;
|
||||
path: string;
|
||||
component_path: string;
|
||||
link_url?: string;
|
||||
image?: string;
|
||||
sort: number;
|
||||
desc: string;
|
||||
children?: Menu[];
|
||||
hasChildren?: boolean;
|
||||
}
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
menu: Partial<Menu> | null;
|
||||
parentMenuOptions: Menu[];
|
||||
dialogType: "add" | "edit" | "addSub";
|
||||
parentTitle?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
menu: null,
|
||||
parentMenuOptions: () => [],
|
||||
dialogType: "add",
|
||||
parentTitle: "",
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: "update:visible", value: boolean): void;
|
||||
(e: "save", menu: Partial<Menu>): void;
|
||||
(e: "cancel"): void;
|
||||
}>();
|
||||
|
||||
// 表单引用
|
||||
const menuFormRef = ref<InstanceType<typeof ElForm>>();
|
||||
|
||||
// 当前操作的菜单
|
||||
const currentMenu = ref<Partial<Menu>>({
|
||||
id: 0,
|
||||
pid: 0,
|
||||
title: "",
|
||||
type: 1,
|
||||
path: "",
|
||||
component_path: "",
|
||||
image: "",
|
||||
sort: 0,
|
||||
desc: "",
|
||||
});
|
||||
|
||||
// 上传配置
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
const uploadUrl = ref(API_BASE_URL + '/admin/uploadfiles');
|
||||
const uploadHeaders = ref({
|
||||
'Authorization': 'Bearer ' + (localStorage.getItem('token') || '')
|
||||
});
|
||||
|
||||
// 查找父级菜单路径的递归函数
|
||||
const findMenuPath = (menuList: Menu[], targetId: number): string => {
|
||||
for (const menu of menuList) {
|
||||
if (menu.id === targetId) {
|
||||
return menu.path || "";
|
||||
}
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const childPath = findMenuPath(menu.children, targetId);
|
||||
if (childPath) return childPath;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// 处理父级菜单变化 - 自动填充父级路径到路由地址
|
||||
const handleParentChange = (value: number) => {
|
||||
if (value === 0) {
|
||||
// 选择顶级菜单,清空路径
|
||||
currentMenu.value.path = "";
|
||||
} else {
|
||||
// 选择子菜单,自动填充父级路径
|
||||
const parentPath = findMenuPath(props.parentMenuOptions, value);
|
||||
if (parentPath) {
|
||||
currentMenu.value.path = parentPath;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 监听props变化,更新当前菜单
|
||||
watch(
|
||||
() => props.menu,
|
||||
(newMenu) => {
|
||||
if (newMenu) {
|
||||
currentMenu.value = {
|
||||
...newMenu,
|
||||
// 确保 pid 有默认值
|
||||
pid: newMenu.pid ?? 0,
|
||||
};
|
||||
} else {
|
||||
// 重置表单
|
||||
currentMenu.value = {
|
||||
id: 0,
|
||||
pid: 0,
|
||||
title: "",
|
||||
path: "",
|
||||
component_path: "",
|
||||
sort: 0,
|
||||
desc: "",
|
||||
type: 1,
|
||||
};
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听props.visible变化
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVisible) => {
|
||||
if (newVisible && props.dialogType === "add") {
|
||||
// 新增时重置表单
|
||||
currentMenu.value = {
|
||||
id: 0,
|
||||
pid: 0,
|
||||
title: "",
|
||||
path: "",
|
||||
component_path: "",
|
||||
image: "",
|
||||
sort: 0,
|
||||
desc: "",
|
||||
type: 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 对话框标题
|
||||
const dialogTitle = computed(() => {
|
||||
switch (props.dialogType) {
|
||||
case "add":
|
||||
return "添加菜单";
|
||||
case "edit":
|
||||
return "编辑菜单";
|
||||
case "addSub":
|
||||
return `添加子菜单 (父菜单: ${props.parentTitle || "顶级菜单"})`;
|
||||
default:
|
||||
return "操作菜单";
|
||||
}
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = ref({
|
||||
title: [{ required: true, message: "请输入菜单名称", trigger: "blur" }],
|
||||
path: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (currentMenu.value.type === 3) {
|
||||
// 外链类型不需要路径
|
||||
callback();
|
||||
} else if (!value || value.trim() === "") {
|
||||
callback(new Error("请输入路由地址"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
component_path: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (currentMenu.value.type === 2) {
|
||||
// 页面类型需要组件路径
|
||||
if (!value || value.trim() === "") {
|
||||
callback(new Error("请输入组件路径"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
// 其他类型(目录、外链、单页)不需要组件路径
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
sort: [{ required: true, message: "请输入排序号", trigger: "blur" }],
|
||||
});
|
||||
|
||||
|
||||
// 监听菜单类型变化,自动清空不相关的字段
|
||||
watch(
|
||||
() => currentMenu.value.type,
|
||||
(newType, oldType) => {
|
||||
if (newType === oldType) return; // 避免初始化时的触发
|
||||
|
||||
if (newType === 1) {
|
||||
// 目录:清空组件路径,保留路径
|
||||
currentMenu.value.component_path = "";
|
||||
} else if (newType === 2) {
|
||||
// 页面:保留路径和组件路径
|
||||
// 不清空,保持现有值
|
||||
} else if (newType === 3) {
|
||||
// 外链:清空路径和组件路径
|
||||
currentMenu.value.path = "";
|
||||
currentMenu.value.component_path = "";
|
||||
} else if (newType === 4) {
|
||||
// 单页:清空组件路径,保留路径(路径用于匹配单页表)
|
||||
currentMenu.value.component_path = "";
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 取消操作
|
||||
const handleCancel = () => {
|
||||
emit("update:visible", false);
|
||||
emit("cancel");
|
||||
};
|
||||
|
||||
// 处理对话框关闭
|
||||
const handleDialogClose = (value: boolean) => {
|
||||
if (!value) {
|
||||
emit("update:visible", false);
|
||||
emit("cancel");
|
||||
}
|
||||
};
|
||||
|
||||
// 图片上传前校验
|
||||
const beforeImageUpload = (file: any) => {
|
||||
const isImage = file.type.startsWith('image/');
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!');
|
||||
return false;
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('图片大小不能超过 2MB!');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 图片上传成功
|
||||
const handleImageSuccess = (response: any) => {
|
||||
if (response.code === 200) {
|
||||
// 直接保存相对路径,不拼接API_BASE_URL
|
||||
currentMenu.value.image = response.data.url || response.data.path;
|
||||
ElMessage.success('图片上传成功');
|
||||
} else {
|
||||
ElMessage.error(response.msg || '图片上传失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 图片上传失败
|
||||
const handleImageError = () => {
|
||||
ElMessage.error('图片上传失败,请重试');
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const handleRemoveImage = () => {
|
||||
currentMenu.value.image = '';
|
||||
ElMessage.success('图片已删除');
|
||||
};
|
||||
|
||||
// 获取图片完整URL(用于预览)
|
||||
const getImageUrl = (imagePath: string) => {
|
||||
if (!imagePath) return '';
|
||||
// 如果是绝对路径,直接返回
|
||||
if (imagePath.startsWith('http')) return imagePath;
|
||||
// 如果是相对路径,拼接API基础地址
|
||||
return API_BASE_URL + imagePath;
|
||||
};
|
||||
|
||||
// 保存菜单
|
||||
const handleSave = async () => {
|
||||
// 表单验证
|
||||
if (!menuFormRef.value) return;
|
||||
const valid = await menuFormRef.value.validate();
|
||||
if (!valid) return;
|
||||
|
||||
// 解决后端时间字段问题:过滤掉不需要的字段
|
||||
const payload = { ...currentMenu.value };
|
||||
|
||||
// 触发保存事件
|
||||
emit("save", payload);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
/* 对话框样式精简 */
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 20px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 图片上传样式 */
|
||||
.image-uploader {
|
||||
:deep(.el-upload) {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
.image-uploader-icon {
|
||||
font-size: 28px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.el-upload__text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,563 +0,0 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>前端导航管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button @click="expandAll">
|
||||
<el-icon>
|
||||
<FolderOpened />
|
||||
</el-icon>
|
||||
全部展开
|
||||
</el-button>
|
||||
<el-button @click="collapseAll">
|
||||
<el-icon>
|
||||
<Folder />
|
||||
</el-icon>
|
||||
全部折叠
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleAddMenu">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
添加菜单
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 树形表格 -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
:data="menuTree"
|
||||
style="width: 100%"
|
||||
row-key="id"
|
||||
border
|
||||
v-loading="loading"
|
||||
element-loading-text="正在加载..."
|
||||
:tree-props="{
|
||||
children: 'children',
|
||||
hasChildren: 'hasChildren',
|
||||
}"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
<el-table-column prop="title" label="菜单名称" width="200">
|
||||
<template #default="scope">
|
||||
<div class="menu-item">
|
||||
<i
|
||||
v-if="scope.row.icon"
|
||||
:class="scope.row.icon"
|
||||
class="menu-icon"
|
||||
></i>
|
||||
<span>{{ scope.row.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="path" label="路由地址"></el-table-column>
|
||||
|
||||
<el-table-column label="图片" width="200" align="center">
|
||||
<template #default="scope">
|
||||
<el-image
|
||||
v-if="scope.row.image"
|
||||
:src="getImageUrl(scope.row.image)"
|
||||
:preview-src-list="[getImageUrl(scope.row.image)]"
|
||||
:preview-teleported="true"
|
||||
fit="cover"
|
||||
style="width: 50px; height: 50px; border-radius: 4px;"
|
||||
/>
|
||||
<span v-else style="color: #ccc;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="MenuType"
|
||||
label="菜单类型"
|
||||
width="120"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag :type="getMenuTypeTagType(scope.row.type)">
|
||||
{{ getMenuTypeTitle(scope.row.type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="sort"
|
||||
label="排序"
|
||||
width="80"
|
||||
align="center"
|
||||
></el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="280" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
@click="handleAddSubMenu(scope.row)"
|
||||
:disabled="scope.row.type === 3"
|
||||
>
|
||||
<el-icon>
|
||||
<CirclePlus />
|
||||
</el-icon>
|
||||
<span>子菜单</span>
|
||||
</el-button>
|
||||
|
||||
<el-button size="small" text @click="handleEditMenu(scope.row)">
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
<span>编辑</span>
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
type="danger"
|
||||
@click="handleDeleteMenu(scope.row)"
|
||||
>
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
<span>删除</span>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 引入编辑组件 -->
|
||||
<MenuEdit
|
||||
v-model:visible="dialogVisible"
|
||||
:menu="dialogMenu"
|
||||
:parent-menu-options="parentMenuOptions"
|
||||
:dialog-type="dialogType"
|
||||
:parent-title="dialogParentTitle"
|
||||
@save="handleMenuSave"
|
||||
@cancel="handleMenuCancel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { ElMessage, ElMessageBox, ElForm } from "element-plus";
|
||||
import {
|
||||
Plus,
|
||||
CirclePlus,
|
||||
Edit,
|
||||
Delete,
|
||||
Refresh,
|
||||
FolderOpened,
|
||||
Folder,
|
||||
} from "@element-plus/icons-vue";
|
||||
import {
|
||||
getFrontMenus,
|
||||
createFrontMenu,
|
||||
editFrontMenu,
|
||||
deleteFrontMenu,
|
||||
} from "@/api/frontMenu";
|
||||
import MenuEdit from "./components/edit.vue";
|
||||
|
||||
// 定义菜单数据类型
|
||||
interface Menu {
|
||||
id: number;
|
||||
pid: number;
|
||||
title: string;
|
||||
type: number;
|
||||
path: string;
|
||||
component_path: string;
|
||||
image?: string;
|
||||
sort: number;
|
||||
desc: string;
|
||||
children?: Menu[];
|
||||
hasChildren?: boolean;
|
||||
}
|
||||
|
||||
// 菜单树形数据
|
||||
const menuTree = ref<Menu[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 表格引用
|
||||
const tableRef = ref<any>(null);
|
||||
|
||||
// 对话框相关变量
|
||||
const dialogVisible = ref(false);
|
||||
const dialogMenu = ref<Partial<Menu> | null>(null);
|
||||
const dialogType = ref<"add" | "edit" | "addSub">("add");
|
||||
const dialogParentTitle = ref("");
|
||||
|
||||
// 父级菜单选项
|
||||
const parentMenuOptions = ref<Menu[]>([]);
|
||||
|
||||
let fetchMenusPromise: Promise<any> | null = null;
|
||||
|
||||
const fetchMenus = async () => {
|
||||
if (fetchMenusPromise) {
|
||||
return fetchMenusPromise;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
fetchMenusPromise = (async () => {
|
||||
try {
|
||||
const result = await getFrontMenus();
|
||||
if (result.code === 200) {
|
||||
menuTree.value = result.data;
|
||||
parentMenuOptions.value = [
|
||||
{
|
||||
id: 0,
|
||||
pid: -1,
|
||||
title: "顶级菜单",
|
||||
type: 1,
|
||||
path: "",
|
||||
component_path: "",
|
||||
sort: 0,
|
||||
desc: "",
|
||||
children: result.data,
|
||||
} as Menu,
|
||||
];
|
||||
} else {
|
||||
ElMessage.error("获取前端导航失败: " + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("获取前端导航数据失败: " + (error as Error).message);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
fetchMenusPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return fetchMenusPromise;
|
||||
};
|
||||
|
||||
// 刷新界面
|
||||
async function refresh() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await fetchMenus();
|
||||
ElMessage.success("刷新成功");
|
||||
} catch (error) {
|
||||
ElMessage.error("刷新失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有前端导航行数据(包括子节点)
|
||||
function getAllMenuRows(menuList: Menu[]): Menu[] {
|
||||
const rows: Menu[] = [];
|
||||
menuList.forEach((frontMenu) => {
|
||||
rows.push(frontMenu);
|
||||
if (frontMenu.children && frontMenu.children.length > 0) {
|
||||
rows.push(...getAllMenuRows(frontMenu.children));
|
||||
}
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
// 全部展开
|
||||
function expandAll() {
|
||||
if (!tableRef.value) return;
|
||||
const allRows = getAllMenuRows(menuTree.value);
|
||||
allRows.forEach((row) => {
|
||||
if (row.children && row.children.length > 0) {
|
||||
tableRef.value.toggleRowExpansion(row, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 全部折叠
|
||||
function collapseAll() {
|
||||
if (!tableRef.value) return;
|
||||
const allRows = getAllMenuRows(menuTree.value);
|
||||
allRows.forEach((row) => {
|
||||
if (row.children && row.children.length > 0) {
|
||||
tableRef.value.toggleRowExpansion(row, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理行点击事件 - 展开/收缩
|
||||
function handleRowClick(row: Menu, column: any, event: Event) {
|
||||
// 如果点击的是操作列,不触发展开/收缩
|
||||
if (column && column.label === '操作') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果该行有子菜单,切换展开/收缩状态
|
||||
if (row.children && row.children.length > 0) {
|
||||
if (tableRef.value) {
|
||||
tableRef.value.toggleRowExpansion(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建菜单树(处理父子关系)
|
||||
const buildMenuTree = (menuList: Menu[]): Menu[] => {
|
||||
return menuList;
|
||||
};
|
||||
|
||||
// 获取菜单类型名称
|
||||
const getMenuTypeTitle = (type: number) => {
|
||||
const typeMap = { 1: "目录", 2: "页面", 3: "外链", 4: "单页" };
|
||||
return typeMap[type as keyof typeof typeMap] || "未知类型";
|
||||
};
|
||||
|
||||
// 获取菜单类型标签样式
|
||||
const getMenuTypeTagType = (type: number) => {
|
||||
const typeMap = { 1: "primary", 2: "success", 3: "info", 4: "warning" };
|
||||
return typeMap[type as keyof typeof typeMap] || "default";
|
||||
};
|
||||
|
||||
// 添加子菜单
|
||||
const handleAddSubMenu = (parentMenu: Menu) => {
|
||||
dialogType.value = "addSub";
|
||||
dialogParentTitle.value = parentMenu.title;
|
||||
dialogMenu.value = {
|
||||
id: 0, // 明确设置为 0,表示是新增
|
||||
pid: parentMenu.id,
|
||||
title: "",
|
||||
path: parentMenu.path || "", // 自动填充父级路径
|
||||
component_path: "",
|
||||
desc: "",
|
||||
sort: 0,
|
||||
type: parentMenu.type === 1 ? 2 : parentMenu.type, // 如果父菜单是目录,子菜单默认为页面
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑菜单
|
||||
const handleEditMenu = (menu: Menu) => {
|
||||
dialogType.value = "edit";
|
||||
dialogMenu.value = { ...menu };
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除菜单
|
||||
const handleDeleteMenu = (menu: Menu) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除菜单 "${menu.title}" 吗?${
|
||||
menu.hasChildren ? "其下所有子菜单也将被删除。" : ""
|
||||
}`,
|
||||
"确认删除",
|
||||
{
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
const result = await deleteFrontMenu(menu.id);
|
||||
if (result.code === 200) {
|
||||
ElMessage.success("删除成功");
|
||||
fetchMenus();
|
||||
} else {
|
||||
ElMessage.error("删除失败: " + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("删除失败: " + (error as Error).message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 添加菜单
|
||||
const handleAddMenu = () => {
|
||||
dialogType.value = "add";
|
||||
dialogMenu.value = {
|
||||
id: 0,
|
||||
pid: 0,
|
||||
title: "",
|
||||
type: 1,
|
||||
path: "",
|
||||
component_path: "",
|
||||
sort: 0,
|
||||
desc: "",
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 处理菜单保存
|
||||
const handleMenuSave = async (menu: Partial<Menu>) => {
|
||||
try {
|
||||
// 解决后端时间字段问题:过滤掉不需要的字段
|
||||
const payload = { ...menu };
|
||||
|
||||
// 确保 pid 是整数类型(后端要求必须是整数)
|
||||
// 处理数组情况:如果 pid 是数组,取第一个元素
|
||||
let pidValue: any = payload.pid;
|
||||
if (Array.isArray(pidValue)) {
|
||||
pidValue = pidValue.length > 0 ? pidValue[0] : null;
|
||||
}
|
||||
|
||||
// 强制转换为整数
|
||||
if (pidValue === null || pidValue === undefined || pidValue === '') {
|
||||
payload.pid = 0;
|
||||
} else {
|
||||
const parsedPid = parseInt(String(pidValue), 10);
|
||||
if (isNaN(parsedPid)) {
|
||||
payload.pid = 0;
|
||||
} else {
|
||||
payload.pid = parsedPid;
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是新增还是编辑:没有 id 或 id 为 0 或 dialogType 为 add/addSub 时为新增
|
||||
if (!menu.id || menu.id === 0 || dialogType.value === 'add' || dialogType.value === 'addSub') {
|
||||
// 新增菜单(包括添加顶级菜单和添加子菜单)
|
||||
const result = await createFrontMenu(payload as Menu);
|
||||
if (result.code === 200) {
|
||||
ElMessage.success(result.msg || "菜单添加成功");
|
||||
dialogVisible.value = false;
|
||||
await fetchMenus();
|
||||
} else {
|
||||
ElMessage.error(result.msg || "添加失败");
|
||||
}
|
||||
} else {
|
||||
// 编辑已存在的菜单
|
||||
const result = await editFrontMenu(menu.id!, payload as Menu);
|
||||
if (result.code === 200) {
|
||||
ElMessage.success(result.msg || "更新成功");
|
||||
dialogVisible.value = false;
|
||||
await fetchMenus();
|
||||
} else {
|
||||
ElMessage.error(result.msg || "更新失败");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("操作失败: " + (error as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理菜单取消
|
||||
const handleMenuCancel = () => {
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
// 获取图片完整URL(用于显示)
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api';
|
||||
const getImageUrl = (imagePath: string) => {
|
||||
if (!imagePath) return '';
|
||||
// 如果是绝对路径,直接返回
|
||||
if (imagePath.startsWith('http')) return imagePath;
|
||||
// 如果是相对路径,拼接API基础地址
|
||||
return API_BASE_URL + imagePath;
|
||||
};
|
||||
|
||||
// 组件挂载时加载菜单
|
||||
onMounted(() => {
|
||||
fetchMenus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.header-bar h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f2f3f5;
|
||||
}
|
||||
|
||||
.card-header span {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 表格核心样式 */
|
||||
:deep(.el-table) {
|
||||
border-radius: 0;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body td) {
|
||||
padding: 12px 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 有子菜单的行显示手型光标 */
|
||||
:deep(.el-table__body tr.el-table__row--level-0) {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr) {
|
||||
&:has(.el-table__expand-icon:not(.el-table__expand-icon--hidden)) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
/* 展开图标与菜单内容对齐 */
|
||||
:deep(.el-table__expand-icon) {
|
||||
margin: 0 !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:deep(.el-table__expand-icon-cell) {
|
||||
padding: 0 8px !important;
|
||||
}
|
||||
|
||||
/* 菜单项样式 */
|
||||
.menu-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 16px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 隐藏无子女菜单的展开图标 */
|
||||
:deep(.el-table__expand-icon--hidden) {
|
||||
visibility: hidden;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
:deep(.el-table__expand-icon) {
|
||||
margin-right: 8px !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 对话框样式精简 */
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 20px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,472 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>模块管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加模块
|
||||
</el-button>
|
||||
<el-button @click="refresh">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<el-table
|
||||
:data="modules"
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column
|
||||
prop="name"
|
||||
label="模块名称"
|
||||
min-width="150"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="module-name">
|
||||
<span class="module-icon" v-html="row.icon"></span>
|
||||
<span>{{ row.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="code"
|
||||
label="模块编码"
|
||||
min-width="120"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="path"
|
||||
label="路由路径"
|
||||
min-width="150"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" type="info">{{ row.path || "-" }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="description"
|
||||
label="描述"
|
||||
min-width="200"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="sort" label="排序" width="100" align="center" />
|
||||
<el-table-column prop="is_show" label="显示" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.is_show"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="handleShowChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="table-footer" v-if="selectedModules.length > 0">
|
||||
<span>已选择 {{ selectedModules.length }} 项</span>
|
||||
<el-button type="danger" size="small" @click="handleBatchDelete"
|
||||
>批量删除</el-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<div class="tips-section">
|
||||
<el-alert title="模块管理说明" type="info" :closable="false" show-icon>
|
||||
<template #default>
|
||||
<p>1. 模块用于管理系统功能单元,每个模块包含独立的路由、图标和描述</p>
|
||||
<p>2. 模块编码用于程序识别,请确保唯一性</p>
|
||||
<p>3. 禁用状态的模块将不会在菜单中显示</p>
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '添加模块' : '编辑模块'"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="模块名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入模块名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模块编码" prop="code">
|
||||
<el-input
|
||||
v-model="formData.code"
|
||||
placeholder="请输入模块编码(英文)"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="路由路径" prop="path">
|
||||
<el-input
|
||||
v-model="formData.path"
|
||||
placeholder="请输入路由路径,如 /system/modules"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="图标" prop="icon">
|
||||
<el-input
|
||||
v-model="formData.icon"
|
||||
placeholder="请输入图标类名,如 Grid"
|
||||
>
|
||||
<template #append>
|
||||
<el-popover placement="bottom-end" :width="400" trigger="click">
|
||||
<template #reference>
|
||||
<el-button link>选择图标</el-button>
|
||||
</template>
|
||||
<div class="icon-grid">
|
||||
<div
|
||||
v-for="icon in iconList"
|
||||
:key="icon"
|
||||
class="icon-item"
|
||||
:class="{ active: formData.icon === icon }"
|
||||
@click="formData.icon = icon"
|
||||
>
|
||||
<el-icon :size="20"><component :is="icon" /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input
|
||||
v-model="formData.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入模块描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number
|
||||
v-model="formData.sort"
|
||||
:min="0"
|
||||
:max="999"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否显示" prop="is_show">
|
||||
<el-switch
|
||||
v-model="formData.is_show"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading"
|
||||
>确定</el-button
|
||||
>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, shallowRef } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import {
|
||||
getModulesList,
|
||||
getModuleDetail,
|
||||
addModule,
|
||||
editModule,
|
||||
deleteModule,
|
||||
batchDeleteModules,
|
||||
changeModuleStatus,
|
||||
} from "@/api/modules";
|
||||
|
||||
const loading = ref(false);
|
||||
const modules = ref([]);
|
||||
const selectedModules = ref([]);
|
||||
const dialogVisible = ref(false);
|
||||
const dialogType = ref("add");
|
||||
const formRef = ref(null);
|
||||
const submitLoading = ref(false);
|
||||
|
||||
const formData = ref({
|
||||
name: "",
|
||||
code: "",
|
||||
path: "",
|
||||
icon: "",
|
||||
description: "",
|
||||
sort: 0,
|
||||
is_show: 1,
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: "请输入模块名称", trigger: "blur" }],
|
||||
code: [{ required: true, message: "请输入模块编码", trigger: "blur" }],
|
||||
};
|
||||
|
||||
function getIconComponent(iconName) {
|
||||
return iconComponents.value[iconName] || Grid;
|
||||
}
|
||||
|
||||
async function fetchModules() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getModulesList();
|
||||
if (res.code === 200 && res.data) {
|
||||
modules.value = res.data.list || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取模块列表失败:", error);
|
||||
ElMessage.error("获取模块列表失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
fetchModules();
|
||||
}
|
||||
|
||||
function handleSelectionChange(selection) {
|
||||
selectedModules.value = selection;
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
dialogType.value = "add";
|
||||
formData.value = {
|
||||
name: "",
|
||||
code: "",
|
||||
path: "",
|
||||
icon: "",
|
||||
description: "",
|
||||
sort: 0,
|
||||
is_show: 1,
|
||||
status: 1,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleEdit(row) {
|
||||
dialogType.value = "edit";
|
||||
try {
|
||||
const res = await getModuleDetail(row.id);
|
||||
if (res.code === 200 && res.data) {
|
||||
formData.value = { ...res.data };
|
||||
dialogVisible.value = true;
|
||||
} else {
|
||||
ElMessage.error(res.msg || "获取模块详情失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取模块详情失败:", error);
|
||||
ElMessage.error("获取模块详情失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
submitLoading.value = true;
|
||||
|
||||
if (dialogType.value === "add") {
|
||||
const res = await addModule(formData.value);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("添加成功");
|
||||
dialogVisible.value = false;
|
||||
fetchModules();
|
||||
} else {
|
||||
ElMessage.error(res.msg || "添加失败");
|
||||
}
|
||||
} else {
|
||||
const res = await editModule(formData.value.id, formData.value);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("编辑成功");
|
||||
dialogVisible.value = false;
|
||||
fetchModules();
|
||||
} else {
|
||||
ElMessage.error(res.msg || "编辑失败");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("提交失败:", error);
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm("确定要删除该模块吗?", "提示", {
|
||||
type: "warning",
|
||||
});
|
||||
const res = await deleteModule(row.id);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("删除成功");
|
||||
fetchModules();
|
||||
} else {
|
||||
ElMessage.error(res.msg || "删除失败");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== "cancel") {
|
||||
console.error("删除失败:", error);
|
||||
ElMessage.error("删除失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除选中的 ${selectedModules.value.length} 个模块吗?`,
|
||||
"提示",
|
||||
{
|
||||
type: "warning",
|
||||
},
|
||||
);
|
||||
const ids = selectedModules.value.map((item) => item.id);
|
||||
const res = await batchDeleteModules(ids);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("批量删除成功");
|
||||
fetchModules();
|
||||
} else {
|
||||
ElMessage.error(res.msg || "批量删除失败");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== "cancel") {
|
||||
console.error("批量删除失败:", error);
|
||||
ElMessage.error("批量删除失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleShowChange(row) {
|
||||
try {
|
||||
const res = await changeModuleStatus(row.id, row.is_show);
|
||||
if (res.code !== 200) {
|
||||
ElMessage.error(res.msg || "状态修改失败");
|
||||
row.is_show = row.is_show ? 0 : 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("状态修改失败:", error);
|
||||
row.is_show = row.is_show ? 0 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchModules();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.container-box {
|
||||
padding: 20px;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.module-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
.module-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
color: #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
.table-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.tips-section {
|
||||
margin-top: 20px;
|
||||
|
||||
p {
|
||||
margin: 4px 0;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
gap: 8px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.icon-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,167 +0,0 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="角色详情" width="600px" @close="handleClose">
|
||||
<el-descriptions :column="1" border v-loading="loading">
|
||||
<el-descriptions-item label="角色ID">
|
||||
{{ roleDetail.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="角色名称">
|
||||
{{ roleDetail.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="roleDetail.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ roleDetail.status === 1 ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="权限列表">
|
||||
<div v-if="menuNames.length > 0" style="max-height: 300px; overflow-y: auto;">
|
||||
<el-tag
|
||||
v-for="(name, index) in menuNames"
|
||||
:key="index"
|
||||
style="margin: 4px;"
|
||||
size="small"
|
||||
>
|
||||
{{ name }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<span v-else style="color: var(--el-text-color-secondary);">
|
||||
暂无权限
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ roleDetail.create_time }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">
|
||||
{{ roleDetail.update_time }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { getRoleById } from "@/api/role";
|
||||
import { getAllMenus } from "@/api/menu";
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
roleId?: number | null;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
roleId: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const roleDetail = ref<any>({});
|
||||
const allMenus = ref<any[]>([]);
|
||||
|
||||
// 监听 modelValue
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
visible.value = val;
|
||||
if (val && props.roleId) {
|
||||
loadRoleDetail();
|
||||
loadMenus();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 visible
|
||||
watch(visible, (val) => {
|
||||
emit("update:modelValue", val);
|
||||
});
|
||||
|
||||
// 解析权限ID
|
||||
const parseRights = (rights: any): number[] => {
|
||||
if (!rights) return [];
|
||||
if (Array.isArray(rights)) return rights;
|
||||
if (typeof rights === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(rights);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// 递归获取所有菜单ID和名称的映射
|
||||
const getMenuMap = (menus: any[]): Map<number, string> => {
|
||||
const map = new Map<number, string>();
|
||||
const traverse = (items: any[]) => {
|
||||
for (const item of items) {
|
||||
map.set(item.id, item.title || item.name || `菜单${item.id}`);
|
||||
if (item.children && item.children.length > 0) {
|
||||
traverse(item.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
traverse(menus);
|
||||
return map;
|
||||
};
|
||||
|
||||
// 计算菜单名称列表
|
||||
const menuNames = computed(() => {
|
||||
if (!roleDetail.value.rights) return [];
|
||||
const rightIds = parseRights(roleDetail.value.rights);
|
||||
const menuMap = getMenuMap(allMenus.value);
|
||||
return rightIds
|
||||
.map((id) => menuMap.get(id))
|
||||
.filter((name) => name !== undefined) as string[];
|
||||
});
|
||||
|
||||
// 加载角色详情
|
||||
const loadRoleDetail = async () => {
|
||||
if (!props.roleId) return;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getRoleById(props.roleId);
|
||||
if (res.code === 200) {
|
||||
roleDetail.value = res.data || {};
|
||||
} else {
|
||||
ElMessage.error(res.msg || "获取角色详情失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载角色详情失败:", error);
|
||||
ElMessage.error("获取角色详情失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载菜单树
|
||||
const loadMenus = async () => {
|
||||
try {
|
||||
const res = await getAllMenus();
|
||||
if (res.code === 200) {
|
||||
allMenus.value = res.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载菜单失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
roleDetail.value = {};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 120px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,217 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="isEdit ? '编辑角色' : '添加角色'"
|
||||
width="600px"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-form-item label="角色名称" prop="name">
|
||||
<el-input
|
||||
v-model="form.name"
|
||||
placeholder="请输入角色名称"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="权限设置" prop="rights">
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="menuTree"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:props="{ children: 'children', label: 'title' }"
|
||||
:default-checked-keys="form.rights"
|
||||
style="width: 100%; border: 1px solid var(--el-border-color); border-radius: 4px; padding: 10px; max-height: 400px; overflow-y: auto;"
|
||||
/>
|
||||
</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, watch, nextTick } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { createRole, updateRole } from "@/api/role";
|
||||
import { getAllMenus } from "@/api/menu";
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
role?: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
role: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "success"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const submitting = ref(false);
|
||||
const formRef = ref();
|
||||
const treeRef = ref();
|
||||
const menuTree = ref<any[]>([]);
|
||||
|
||||
const form = ref({
|
||||
name: "",
|
||||
status: 1,
|
||||
rights: [] as number[],
|
||||
});
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
{ required: true, message: "请输入角色名称", trigger: "blur" },
|
||||
{ min: 2, max: 50, message: "角色名称长度在 2 到 50 个字符", trigger: "blur" },
|
||||
],
|
||||
status: [{ required: true, message: "请选择状态", trigger: "change" }],
|
||||
};
|
||||
|
||||
// 监听 modelValue
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
visible.value = val;
|
||||
if (val) {
|
||||
loadMenus();
|
||||
if (props.role) {
|
||||
isEdit.value = true;
|
||||
form.value = {
|
||||
name: props.role.name,
|
||||
status: props.role.status,
|
||||
rights: parseRights(props.role.rights),
|
||||
};
|
||||
// 等待树加载完成后设置选中状态
|
||||
nextTick(() => {
|
||||
if (treeRef.value) {
|
||||
treeRef.value.setCheckedKeys(form.value.rights);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
isEdit.value = false;
|
||||
resetForm();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 visible
|
||||
watch(visible, (val) => {
|
||||
emit("update:modelValue", val);
|
||||
});
|
||||
|
||||
// 解析权限字符串
|
||||
const parseRights = (rights: any): number[] => {
|
||||
if (!rights) return [];
|
||||
if (Array.isArray(rights)) return rights;
|
||||
if (typeof rights === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(rights);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// 加载菜单树
|
||||
const loadMenus = async () => {
|
||||
try {
|
||||
const res = await getAllMenus();
|
||||
if (res.code === 200) {
|
||||
menuTree.value = res.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载菜单失败:", error);
|
||||
ElMessage.error("加载菜单失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
name: "",
|
||||
status: 1,
|
||||
rights: [],
|
||||
};
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
if (treeRef.value) {
|
||||
treeRef.value.setCheckedKeys([]);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
resetForm();
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
|
||||
// 获取选中的菜单ID
|
||||
const checkedKeys = treeRef.value.getCheckedKeys();
|
||||
const halfCheckedKeys = treeRef.value.getHalfCheckedKeys();
|
||||
const allCheckedKeys = [...checkedKeys, ...halfCheckedKeys];
|
||||
|
||||
const submitData = {
|
||||
name: form.value.name,
|
||||
status: form.value.status,
|
||||
rights: allCheckedKeys,
|
||||
};
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
let res;
|
||||
if (isEdit.value && props.role) {
|
||||
res = await updateRole(props.role.id, submitData);
|
||||
} else {
|
||||
res = await createRole(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(isEdit.value ? "更新成功" : "创建成功");
|
||||
emit("success");
|
||||
handleClose();
|
||||
} else {
|
||||
ElMessage.error(res.msg || "操作失败");
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error !== false) {
|
||||
// 不是表单验证错误
|
||||
ElMessage.error(error.message || "操作失败");
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-tree) {
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
</style>
|
||||
@@ -1,226 +0,0 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>角色管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加角色
|
||||
</el-button>
|
||||
<el-button @click="refresh">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-if="error" class="error-state">
|
||||
<el-alert title="加载失败" :message="error" type="error" show-icon />
|
||||
<el-button type="primary" @click="refresh">重试</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 角色列表 -->
|
||||
<div v-else>
|
||||
<el-table :data="roles" stripe style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column
|
||||
prop="name"
|
||||
label="角色名称"
|
||||
min-width="150"
|
||||
align="center"
|
||||
/>
|
||||
<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="rights"
|
||||
label="权限"
|
||||
min-width="150"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleView(row)"
|
||||
>
|
||||
<el-icon><View /></el-icon>
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.default !== 1 && row.default !== 2"
|
||||
size="small"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.id !== 1"
|
||||
size="small"
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 编辑对话框 -->
|
||||
<RoleEditDialog
|
||||
v-model="editDialogVisible"
|
||||
:role="currentRole"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<RoleDetailDialog
|
||||
v-model="detailDialogVisible"
|
||||
:roleId="currentRoleId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, View, Edit, Delete, Refresh } from "@element-plus/icons-vue";
|
||||
import { getAllRoles, getRoleById, deleteRole } from "@/api/role";
|
||||
import RoleEditDialog from "./components/edit.vue";
|
||||
import RoleDetailDialog from "./components/detail.vue";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
// 使用 auth store 获取用户信息
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const roles = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
|
||||
const editDialogVisible = ref(false);
|
||||
const detailDialogVisible = ref(false);
|
||||
const currentRole = ref<any>(null);
|
||||
const currentRoleId = ref<number | null>(null);
|
||||
|
||||
// 获取用户信息
|
||||
const userInfo = authStore.user;
|
||||
if (userInfo && userInfo.id) {
|
||||
// console.log('用户名:', userInfo.username || userInfo.nickname);
|
||||
// console.log('用户ID:', userInfo.id);
|
||||
// console.log('角色:', userInfo.role);
|
||||
} else {
|
||||
// console.log('未找到用户信息或用户未登录');
|
||||
}
|
||||
|
||||
// 获取角色列表
|
||||
const fetchRoles = async () => {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
const res = await getAllRoles();
|
||||
if (res.code === 200) {
|
||||
roles.value = res.data || [];
|
||||
console.log('角色列表:', roles.value);
|
||||
} else {
|
||||
error.value = res.msg || "获取角色列表失败";
|
||||
ElMessage.error(error.value);
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || "获取角色列表失败";
|
||||
ElMessage.error(error.value);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新界面
|
||||
async function refresh() {
|
||||
await fetchRoles();
|
||||
if (!error.value) {
|
||||
ElMessage.success("刷新成功");
|
||||
}
|
||||
}
|
||||
|
||||
// 添加角色
|
||||
function handleAdd() {
|
||||
currentRole.value = null;
|
||||
editDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
function handleView(row: any) {
|
||||
currentRoleId.value = row.id;
|
||||
detailDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 编辑角色
|
||||
function handleEdit(row: any) {
|
||||
currentRole.value = { ...row };
|
||||
editDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 删除角色
|
||||
async function handleDelete(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除角色「${row.name}」吗?删除后不可恢复。`,
|
||||
"警告",
|
||||
{ type: "warning" }
|
||||
);
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await deleteRole(row.id);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("删除成功");
|
||||
fetchRoles();
|
||||
} else {
|
||||
ElMessage.error(res.msg || "删除失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || "删除失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} catch {
|
||||
// 取消删除
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
function handleEditSuccess() {
|
||||
fetchRoles();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchRoles();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.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>
|
||||
@@ -1,78 +0,0 @@
|
||||
<template>
|
||||
<el-form
|
||||
ref="contactFormRef"
|
||||
:model="contactForm"
|
||||
:rules="contactRules"
|
||||
label-width="120px"
|
||||
style="max-width: 600px"
|
||||
>
|
||||
<el-form-item label="联系电话" prop="phone">
|
||||
<el-input
|
||||
v-model="contactForm.phone"
|
||||
placeholder="请输入联系电话"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系邮箱" prop="email">
|
||||
<el-input
|
||||
v-model="contactForm.email"
|
||||
placeholder="请输入联系邮箱"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="公司地址" prop="address">
|
||||
<el-input
|
||||
v-model="contactForm.address"
|
||||
placeholder="请输入公司地址"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="工作时间" prop="workTime">
|
||||
<el-input
|
||||
v-model="contactForm.workTime"
|
||||
placeholder="如:周一至周五 9:00-18:00"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveContactSettings">保存设置</el-button>
|
||||
<el-button @click="resetContactForm">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import type { FormInstance, FormRules } from "element-plus";
|
||||
|
||||
const contactFormRef = ref<FormInstance>();
|
||||
|
||||
const contactForm = reactive({
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
workTime: ""
|
||||
});
|
||||
|
||||
const contactRules: FormRules = {
|
||||
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: "blur" }]
|
||||
};
|
||||
|
||||
const saveContactSettings = async () => {
|
||||
if (!contactFormRef.value) return;
|
||||
await contactFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
// TODO: 保存联系方式
|
||||
ElMessage.success("联系方式保存成功");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const resetContactForm = () => {
|
||||
contactForm.phone = "";
|
||||
contactForm.email = "";
|
||||
contactForm.address = "";
|
||||
contactForm.workTime = "";
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
contactForm
|
||||
});
|
||||
</script>
|
||||
@@ -1,91 +0,0 @@
|
||||
<template>
|
||||
<el-form
|
||||
ref="legalNoticeFormRef"
|
||||
:model="formData"
|
||||
label-width="120px"
|
||||
style="max-width: 600px"
|
||||
>
|
||||
<el-form-item label="法律声明" prop="legalNotice">
|
||||
<el-input
|
||||
v-model="legalNotice"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
placeholder="请输入法律声明"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="隐私条款" prop="privacyTerms">
|
||||
<el-input
|
||||
v-model="privacyTerms"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
placeholder="请输入隐私条款"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveLegalInfos">保存设置</el-button>
|
||||
<el-button @click="resetLegalNoticeForm">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import type { FormInstance, FormRules } from "element-plus";
|
||||
|
||||
import { getLegalInfos, saveLegalInfos } from "@/api/sitesettings";
|
||||
|
||||
const legalNoticeFormRef = ref<FormInstance>();
|
||||
|
||||
const legalNotice = ref("");
|
||||
const privacyTerms = ref("");
|
||||
|
||||
const seoForm = reactive({
|
||||
legalNotice: "",
|
||||
privacyTerms: "",
|
||||
});
|
||||
|
||||
const formData = {
|
||||
legalNotice,
|
||||
privacyTerms,
|
||||
};
|
||||
|
||||
//调用法律声明和隐私条款数据
|
||||
const initLegalInfos = async () => {
|
||||
const res = await getLegalInfos();
|
||||
if (res.code === 200 && res.data) {
|
||||
const data = res.data;
|
||||
const dataMap: Record<string, string> = {};
|
||||
data.forEach((item: any) => {
|
||||
dataMap[item.label] = item.value;
|
||||
});
|
||||
legalNotice.value = dataMap["legalNotice"] || "";
|
||||
privacyTerms.value = dataMap["privacyTerms"] || "";
|
||||
}
|
||||
};
|
||||
|
||||
const saveLegalInfos = async () => {
|
||||
if (!legalNoticeFormRef.value) return;
|
||||
await legalNoticeFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
// TODO: 保存法律声明和隐私条款
|
||||
ElMessage.success("法律声明和隐私条款保存成功");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const resetLegalNoticeForm = () => {
|
||||
legalNotice.value = "";
|
||||
privacyTerms.value = "";
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
legalNotice,
|
||||
privacyTerms,
|
||||
});
|
||||
|
||||
// 初始化法律声明和隐私条款数据
|
||||
onMounted(() => {
|
||||
initLegalInfos();
|
||||
});
|
||||
</script>
|
||||
@@ -1,195 +0,0 @@
|
||||
<template>
|
||||
<el-form
|
||||
ref="normalFormRef"
|
||||
:model="formData"
|
||||
:rules="normalRules"
|
||||
label-width="120px"
|
||||
style="max-width: 600px"
|
||||
>
|
||||
<el-form-item label="站点名称" prop="sitename">
|
||||
<el-input v-model="sitename" placeholder="请输入站点名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="站点Logo" prop="logo">
|
||||
<el-upload
|
||||
class="logo-uploader"
|
||||
:show-file-list="false"
|
||||
:auto-upload="false"
|
||||
:on-change="handleLogoChange"
|
||||
>
|
||||
<img
|
||||
v-if="logo"
|
||||
:src="API_BASE_URL + logo.replace(/^\//, '/')"
|
||||
class="logo-image"
|
||||
/>
|
||||
<el-icon v-else class="logo-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="企业名称" prop="companyname">
|
||||
<el-input v-model="companyname" placeholder="请输入企业名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="站点SEO描述" prop="description">
|
||||
<el-input v-model="description" placeholder="请输入站点SEO描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="版权信息" prop="copyright">
|
||||
<el-input v-model="copyright" placeholder="如:© 2026 公司名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备案号 " prop="icp">
|
||||
<el-input v-model="icp" placeholder="如:苏ICP备20260000号" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSaveNormalInfos"
|
||||
>保存设置</el-button
|
||||
>
|
||||
<el-button @click="resetnormalForm">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import type { FormInstance, FormRules, UploadFile } from "element-plus";
|
||||
import { getNormalInfos, saveNormalInfos } from "@/api/sitesettings";
|
||||
import { uploadFile } from "@/api/file";
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
const normalFormRef = ref<FormInstance>();
|
||||
|
||||
const sitename = ref("");
|
||||
const companyname = ref("");
|
||||
const logo = ref("");
|
||||
const description = ref("");
|
||||
const copyright = ref("");
|
||||
const icp = ref("");
|
||||
|
||||
const formData = {
|
||||
sitename,
|
||||
companyname,
|
||||
logo,
|
||||
description,
|
||||
copyright,
|
||||
icp,
|
||||
};
|
||||
|
||||
const normalRules: FormRules = {
|
||||
sitename: [{ required: true, message: "请输入站点名称", trigger: "blur" }],
|
||||
};
|
||||
|
||||
//调用基础数据
|
||||
const initNormalInfos = async () => {
|
||||
const res = await getNormalInfos();
|
||||
if (res.code === 200 && res.data) {
|
||||
const data = res.data;
|
||||
const dataMap: Record<string, string> = {};
|
||||
data.forEach((item: any) => {
|
||||
dataMap[item.label] = item.value;
|
||||
});
|
||||
sitename.value = dataMap["sitename"] || "";
|
||||
logo.value = dataMap["logo"] || "";
|
||||
description.value = dataMap["description"] || "";
|
||||
copyright.value = dataMap["copyright"] || "";
|
||||
icp.value = dataMap["icp"] || "";
|
||||
companyname.value = dataMap["companyname"] || "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogoChange = (file: UploadFile) => {
|
||||
const uploadFormData = new FormData();
|
||||
uploadFormData.append("file", file.raw);
|
||||
uploadFormData.append("cate", "site");
|
||||
|
||||
uploadFile(uploadFormData).then((uploadRes) => {
|
||||
if (
|
||||
(uploadRes.code === 200 || uploadRes.code === 201) &&
|
||||
uploadRes.data &&
|
||||
uploadRes.data.url
|
||||
) {
|
||||
logo.value = uploadRes.data.url.replace(/\\/g, "/");
|
||||
} else {
|
||||
ElMessage.error(uploadRes.msg || "上传失败");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveNormalInfos = async () => {
|
||||
if (!normalFormRef.value) return;
|
||||
await normalFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
const data = [
|
||||
{ label: "sitename", value: sitename.value },
|
||||
{ label: "logo", value: logo.value },
|
||||
{ label: "description", value: description.value },
|
||||
{ label: "companyname", value: companyname.value },
|
||||
{ label: "copyright", value: copyright.value },
|
||||
{ label: "icp", value: icp.value },
|
||||
];
|
||||
const res = await saveNormalInfos(data);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("保存成功");
|
||||
} else {
|
||||
ElMessage.error(res.msg || "保存失败");
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const resetnormalForm = () => {
|
||||
sitename.value = "";
|
||||
companyname.value = "";
|
||||
logo.value = "";
|
||||
description.value = "";
|
||||
copyright.value = "";
|
||||
icp.value = "";
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
sitename,
|
||||
companyname,
|
||||
logo,
|
||||
description,
|
||||
copyright,
|
||||
icp,
|
||||
});
|
||||
|
||||
// 初始化基础数据
|
||||
onMounted(() => {
|
||||
initNormalInfos();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.logo-uploader {
|
||||
:deep(.el-upload) {
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<el-form
|
||||
ref="otherFormRef"
|
||||
:model="otherForm"
|
||||
label-width="120px"
|
||||
style="max-width: 600px"
|
||||
>
|
||||
<el-form-item label="开启维护模式">
|
||||
<el-switch v-model="otherForm.maintenanceMode" />
|
||||
<span class="form-tip">开启后,前台将显示维护中页面</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="开启注册">
|
||||
<el-switch v-model="otherForm.allowRegister" />
|
||||
<span class="form-tip">允许用户在前台注册账号</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="验证码">
|
||||
<el-switch v-model="otherForm.captchaEnabled" />
|
||||
<span class="form-tip">登录、注册等操作需要验证码</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveOtherSettings">保存设置</el-button>
|
||||
<el-button @click="resetOtherForm">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import type { FormInstance } from "element-plus";
|
||||
|
||||
const otherFormRef = ref<FormInstance>();
|
||||
|
||||
const otherForm = reactive({
|
||||
maintenanceMode: false,
|
||||
allowRegister: false,
|
||||
captchaEnabled: true
|
||||
});
|
||||
|
||||
const saveOtherSettings = () => {
|
||||
// TODO: 保存其他设置
|
||||
ElMessage.success("其他设置保存成功");
|
||||
};
|
||||
|
||||
const resetOtherForm = () => {
|
||||
otherForm.maintenanceMode = false;
|
||||
otherForm.allowRegister = false;
|
||||
otherForm.captchaEnabled = true;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
otherForm
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-tip {
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
@@ -1,72 +0,0 @@
|
||||
<template>
|
||||
<el-form
|
||||
ref="seoFormRef"
|
||||
:model="seoForm"
|
||||
:rules="seoRules"
|
||||
label-width="120px"
|
||||
style="max-width: 600px"
|
||||
>
|
||||
<el-form-item label="SEO标题" prop="seoTitle">
|
||||
<el-input
|
||||
v-model="seoForm.seoTitle"
|
||||
placeholder="请输入SEO标题"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="SEO关键词" prop="seoKeywords">
|
||||
<el-input
|
||||
v-model="seoForm.seoKeywords"
|
||||
placeholder="多个关键词用逗号分隔"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="SEO描述" prop="seoDescription">
|
||||
<el-input
|
||||
v-model="seoForm.seoDescription"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入SEO描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveSeoSettings">保存设置</el-button>
|
||||
<el-button @click="resetSeoForm">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import type { FormInstance, FormRules } from "element-plus";
|
||||
|
||||
const seoFormRef = ref<FormInstance>();
|
||||
|
||||
const seoForm = reactive({
|
||||
seoTitle: "",
|
||||
seoKeywords: "",
|
||||
seoDescription: ""
|
||||
});
|
||||
|
||||
const seoRules: FormRules = {
|
||||
seoTitle: [{ required: true, message: "请输入SEO标题", trigger: "blur" }]
|
||||
};
|
||||
|
||||
const saveSeoSettings = async () => {
|
||||
if (!seoFormRef.value) return;
|
||||
await seoFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
// TODO: 保存SEO设置
|
||||
ElMessage.success("SEO设置保存成功");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const resetSeoForm = () => {
|
||||
seoForm.seoTitle = "";
|
||||
seoForm.seoKeywords = "";
|
||||
seoForm.seoDescription = "";
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
seoForm
|
||||
});
|
||||
</script>
|
||||
@@ -1,109 +0,0 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>站点设置</h2>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<div class="settings-container">
|
||||
<el-tabs v-model="activeTab" class="settings-tabs">
|
||||
<el-tab-pane label="基本信息" name="basic">
|
||||
<normalSettings ref="normalSettingsRef" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="SEO设置" name="seo">
|
||||
<seoSettings ref="seoSettingsRef" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="联系方式" name="contact">
|
||||
<contactSettings ref="contactSettingsRef" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="法律声明&隐私条款" name="legalNotice">
|
||||
<legalNoticeSettings ref="legalNoticeSettingsRef" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="其他设置" name="other">
|
||||
<otherSettings ref="otherSettingsRef" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import normalSettings from "./components/normalSettings.vue";
|
||||
import seoSettings from "./components/seoSettings.vue";
|
||||
import contactSettings from "./components/contactSettings.vue";
|
||||
import otherSettings from "./components/otherSettings.vue";
|
||||
import legalNoticeSettings from "./components/legalNotice.vue";
|
||||
|
||||
const activeTab = ref("basic");
|
||||
|
||||
const normalSettingsRef = ref();
|
||||
const seoSettingsRef = ref();
|
||||
const contactSettingsRef = ref();
|
||||
const otherSettingsRef = ref();
|
||||
const legalNoticeSettingsRef = ref();
|
||||
|
||||
// 初始化各标签页数据
|
||||
const initSettings = async () => {
|
||||
// TODO: 从后端获取各设置数据并赋值给对应组件
|
||||
if (normalSettingsRef.value) {
|
||||
// normalSettingsRef.value.normalinfos = ...
|
||||
}
|
||||
if (seoSettingsRef.value) {
|
||||
// seoSettingsRef.value.seoForm = ...
|
||||
}
|
||||
if (contactSettingsRef.value) {
|
||||
// contactSettingsRef.value.contactForm = ...
|
||||
}
|
||||
if (otherSettingsRef.value) {
|
||||
// otherSettingsRef.value.otherForm = ...
|
||||
}
|
||||
if (legalNoticeSettingsRef.value) {
|
||||
// legalNoticeSettingsRef.value.legalNoticeForm = ...
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initSettings();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container-box {
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.settings-tabs {
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-wrap::after) {
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,198 +0,0 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="修改密码" width="400px" @close="handleClose">
|
||||
<el-form :model="form">
|
||||
<!-- 用户账号(只读) -->
|
||||
<el-form-item label="账号">
|
||||
<el-input v-model="form.username" disabled />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 新密码 -->
|
||||
<el-form-item label="新密码">
|
||||
<el-input
|
||||
v-model="passwordForm.newPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
placeholder="请输入新密码(6-16位)"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 确认密码 -->
|
||||
<el-form-item label="确认密码">
|
||||
<el-input
|
||||
v-model="passwordForm.confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</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>({
|
||||
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 = {
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
passwordError.value = "";
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// 清除之前的错误
|
||||
passwordError.value = "";
|
||||
|
||||
try {
|
||||
if (!form.value.id) {
|
||||
passwordError.value = "用户ID不能为空";
|
||||
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 === 200 || res.msg === '修改成功') {
|
||||
ElMessage.success("密码修改成功");
|
||||
visible.value = false;
|
||||
emit('update:modelValue', false);
|
||||
emit('submit');
|
||||
} else {
|
||||
passwordError.value = res.msg || "密码修改失败";
|
||||
}
|
||||
} catch (e: any) {
|
||||
const errorMsg = e?.response?.data?.msg || e?.response?.data?.message || e?.message || "操作失败";
|
||||
passwordError.value = errorMsg;
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
open: (userId: number, username: string) => {
|
||||
form.value = {
|
||||
id: userId,
|
||||
username: username,
|
||||
};
|
||||
passwordForm.value = {
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
passwordError.value = "";
|
||||
visible.value = true;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
@@ -1,190 +0,0 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" title="用户信息预览" size="50%">
|
||||
<div class="user-preview" v-if="user">
|
||||
<div class="user-header">
|
||||
<div class="user-avatar">
|
||||
<el-avatar :size="80" :icon="UserFilled" />
|
||||
</div>
|
||||
<h2 class="user-name">{{ user.name || "未知用户" }}</h2>
|
||||
<el-tag :type="user.status === 1 ? 'success' : 'danger'" size="large">
|
||||
{{ user.status === 1 ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<div class="user-info">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="ID">
|
||||
{{ user.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="账号">
|
||||
{{ user.account || "未设置" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">
|
||||
{{ user.name || "未设置" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">
|
||||
{{ user.sex === 1 ? "男" : "女" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">
|
||||
{{ user.phone || "未设置" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="QQ">
|
||||
{{ user.qq || "未设置" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">
|
||||
{{ user.email || "未设置" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="角色">
|
||||
<el-tag :type="getRoleTagType(user.group_id)" size="small">
|
||||
{{ getRoleName(user.group_id) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最后登录IP">
|
||||
{{ user.last_login_ip || "未登录" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="登录次数">
|
||||
{{ user.login_count || 0 }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ user.create_time || "未知" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">
|
||||
{{ user.update_time || "未知" }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-user">
|
||||
<el-empty description="暂无用户信息" />
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { UserFilled } from "@element-plus/icons-vue";
|
||||
import { getAllRoles } from "@/api/role";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
account: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
qq: string;
|
||||
email: string;
|
||||
sex: number;
|
||||
group_id: number;
|
||||
status: number;
|
||||
last_login_ip: string;
|
||||
login_count: number;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
}
|
||||
|
||||
interface Role {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
user: {
|
||||
type: Object as () => User | undefined,
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const roles = ref<Role[]>([]);
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
visible.value = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
// 监听visible变化,同步给父组件
|
||||
watch(visible, (newVal) => {
|
||||
emit("update:modelValue", newVal);
|
||||
});
|
||||
|
||||
// 获取角色列表
|
||||
const fetchRoles = async () => {
|
||||
try {
|
||||
const res = await getAllRoles();
|
||||
roles.value = res.data || [];
|
||||
} catch (e) {
|
||||
roles.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 获取角色tag状态
|
||||
function getRoleTagType(group_id: number): string {
|
||||
const typeMap: Record<number, string> = {
|
||||
1: "primary",
|
||||
2: "success",
|
||||
3: "warning",
|
||||
4: "danger",
|
||||
};
|
||||
return typeMap[group_id] || "primary";
|
||||
}
|
||||
|
||||
// 获取角色名称
|
||||
function getRoleName(group_id: number): string {
|
||||
const role = roles.value.find((r) => r.id === group_id);
|
||||
return role?.name || "未知";
|
||||
}
|
||||
|
||||
// 暴露open方法供父组件调用
|
||||
const open = (userData?: User) => {
|
||||
visible.value = true;
|
||||
fetchRoles();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
});
|
||||
|
||||
// 初始化时获取角色列表
|
||||
fetchRoles();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-preview {
|
||||
padding: 20px;
|
||||
|
||||
.user-header {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
|
||||
.user-avatar {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
margin: 15px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-user {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,408 +0,0 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" :title="dialogTitle" width="500px">
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<div class="form-title">账号信息</div>
|
||||
<!-- 账号 -->
|
||||
<el-form-item label="账号">
|
||||
<el-input v-model="form.account" :disabled="!isAdd" placeholder="请输入账号" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 密码 -->
|
||||
<el-form-item label="密码" prop="password" v-if="isAdd">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
:placeholder="isAdd ? '请输入密码(至少6位)' : '留空则不修改密码'"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 确认密码 -->
|
||||
<el-form-item label="确认密码" prop="confirmPassword" v-if="isAdd">
|
||||
<el-input
|
||||
v-model="form.confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
:placeholder="isAdd ? '请再次输入密码' : '留空则不修改密码'"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider></el-divider>
|
||||
<div class="form-title">个人信息</div>
|
||||
<!-- 姓名 -->
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="form.name" placeholder="请输入姓名" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 电话 -->
|
||||
<el-form-item label="电话">
|
||||
<el-input v-model="form.phone" placeholder="请输入电话" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 邮箱 -->
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- QQ -->
|
||||
<el-form-item label="QQ">
|
||||
<el-input v-model="form.qq" placeholder="请输入QQ" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 性别 -->
|
||||
<el-form-item label="性别">
|
||||
<el-radio-group v-model="form.sex" placeholder="请选择性别">
|
||||
<el-radio-button label="男" :value="1" />
|
||||
<el-radio-button label="女" :value="2" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 状态 -->
|
||||
<el-form-item label="状态">
|
||||
<el-select
|
||||
v-model="form.status"
|
||||
placeholder="请选择状态"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</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-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { addUser, editUser, getUserInfo } from "@/api/user";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
statusDict: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "submit", "close"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const formRef = ref<any>(null);
|
||||
const isAdd = ref(false);
|
||||
|
||||
const form = ref<any>({
|
||||
id: null,
|
||||
account: "",
|
||||
name: "",
|
||||
phone: "",
|
||||
qq: "",
|
||||
sex: 1,
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
email: "",
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
return isAdd.value ? "添加用户" : "编辑用户";
|
||||
});
|
||||
|
||||
// 密码验证规则
|
||||
const validatePassword = (rule: any, value: any, callback: any) => {
|
||||
if (isAdd.value) {
|
||||
// 新增用户时,密码必填
|
||||
if (!value) {
|
||||
callback(new Error("请输入密码"));
|
||||
return;
|
||||
}
|
||||
if (value.length < 6) {
|
||||
callback(new Error("密码长度不能少于6位"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 编辑用户时,如果填写了密码,则必须符合规则
|
||||
if (value && value.length < 6) {
|
||||
callback(new Error("密码长度不能少于6位"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
// 确认密码验证规则
|
||||
const validateConfirmPassword = (rule: any, value: any, callback: any) => {
|
||||
if (isAdd.value) {
|
||||
// 新增用户时,确认密码必填
|
||||
if (!value) {
|
||||
callback(new Error("请再次输入密码"));
|
||||
return;
|
||||
}
|
||||
if (value !== form.value.password) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 编辑用户时,如果填写了密码,则确认密码必须一致
|
||||
if (form.value.password && value !== form.value.password) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
return;
|
||||
}
|
||||
// 如果填写了确认密码但没填密码,提示错误
|
||||
if (value && !form.value.password) {
|
||||
callback(new Error("请先输入密码"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
account: [
|
||||
{ required: true, message: "请输入账号", trigger: "blur" },
|
||||
{ min: 3, max: 20, message: "账号长度在 3 到 20 个字符", trigger: "blur" },
|
||||
],
|
||||
name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
||||
password: [{ validator: validatePassword, trigger: "blur" }],
|
||||
confirmPassword: [{ validator: validateConfirmPassword, trigger: "blur" }],
|
||||
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: "blur" }],
|
||||
};
|
||||
|
||||
// 监听 modelValue
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
visible.value = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
if (!newVal) {
|
||||
emit("update:modelValue", false);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 statusDict 变化,用于调试
|
||||
watch(
|
||||
() => props.statusDict,
|
||||
(newVal) => {},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
const loadUserData = async (user: any) => {
|
||||
try {
|
||||
// 处理两种调用方式:传递用户对象或用户 ID
|
||||
const userId = typeof user === "number" ? user : user?.id || user?.userId;
|
||||
if (!userId) {
|
||||
throw new Error("未提供有效的用户 ID");
|
||||
}
|
||||
|
||||
const res = await getUserInfo(userId);
|
||||
|
||||
const data = res.data || res;
|
||||
|
||||
// 确保 sex 和 status 都是数字类型
|
||||
const sexValue =
|
||||
data.sex !== undefined && data.sex !== null
|
||||
? Number(data.sex)
|
||||
: 1;
|
||||
|
||||
const statusValue =
|
||||
data.status !== undefined && data.status !== null
|
||||
? Number(data.status)
|
||||
: 1;
|
||||
|
||||
form.value = {
|
||||
id: data.id,
|
||||
account: data.account,
|
||||
name: data.name,
|
||||
phone: data.phone,
|
||||
qq: data.qq,
|
||||
sex: sexValue,
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
email: data.email,
|
||||
status: statusValue,
|
||||
};
|
||||
} 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 handleCancel = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// 表单验证
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch (error) {
|
||||
ElMessage.warning("请检查表单填写是否正确");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证密码一致性
|
||||
if (isAdd.value) {
|
||||
// 新增用户时,密码必填
|
||||
if (!form.value.password) {
|
||||
ElMessage.error("请输入密码");
|
||||
return;
|
||||
}
|
||||
if (form.value.password !== form.value.confirmPassword) {
|
||||
ElMessage.error("两次输入的密码不一致");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 编辑用户时,如果填写了密码,则必须填写确认密码且一致
|
||||
if (form.value.password) {
|
||||
if (form.value.password !== form.value.confirmPassword) {
|
||||
ElMessage.error("两次输入的密码不一致");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (isAdd.value) {
|
||||
// 新增用户
|
||||
const submitData: any = {
|
||||
account: form.value.account,
|
||||
name: form.value.name,
|
||||
phone: form.value.phone,
|
||||
qq: form.value.qq,
|
||||
sex: form.value.sex,
|
||||
email: form.value.email,
|
||||
status: form.value.status,
|
||||
password: form.value.password,
|
||||
};
|
||||
|
||||
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,
|
||||
account: form.value.account,
|
||||
name: form.value.name,
|
||||
phone: form.value.phone,
|
||||
qq: form.value.qq,
|
||||
sex: form.value.sex,
|
||||
email: form.value.email,
|
||||
status: form.value.status,
|
||||
};
|
||||
|
||||
// 只有在填写了密码时才添加到提交数据中
|
||||
if (form.value.password) {
|
||||
submitData.password = form.value.password;
|
||||
}
|
||||
|
||||
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: () => {
|
||||
isAdd.value = true;
|
||||
form.value = {
|
||||
id: 0,
|
||||
account: "",
|
||||
name: "",
|
||||
phone: "",
|
||||
qq: "",
|
||||
sex: 1,
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
email: "",
|
||||
status: 1,
|
||||
};
|
||||
visible.value = true;
|
||||
// 清除表单验证
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
},
|
||||
openEdit: (user: any) => {
|
||||
isAdd.value = false;
|
||||
visible.value = true;
|
||||
// 清除表单验证
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
// 异步加载用户详细信息
|
||||
loadUserData(user);
|
||||
},
|
||||
open: (user?: any) => {
|
||||
if (user) {
|
||||
isAdd.value = false;
|
||||
loadUserData(user);
|
||||
} else {
|
||||
isAdd.value = true;
|
||||
form.value = {
|
||||
id: 0,
|
||||
account: "",
|
||||
name: "",
|
||||
phone: "",
|
||||
qq: "",
|
||||
sex: 1,
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
email: "",
|
||||
status: 1,
|
||||
};
|
||||
}
|
||||
visible.value = true;
|
||||
// 清除表单验证
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,342 +0,0 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>用户管理</h2>
|
||||
<div class="header-actions">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 用户列表表格 -->
|
||||
<el-table :data="users" style="width: 100%" v-loading="loading">
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
align="center"
|
||||
fixed="left"
|
||||
/>
|
||||
<el-table-column prop="account" label="账号" align="center" />
|
||||
<el-table-column
|
||||
prop="name"
|
||||
label="姓名"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span class="name-link" @click="handlePreview(scope.row)">{{ scope.row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="group_id" label="角色" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="getRoleTagType(scope.row.group_id)"
|
||||
size="small"
|
||||
>
|
||||
{{ getRoleTagText(roles, scope.row.group_id) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="phone"
|
||||
label="手机号"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column prop="qq" label="QQ" align="center" />
|
||||
<el-table-column
|
||||
prop="last_login_ip"
|
||||
label="最后登录IP"
|
||||
width="120"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="login_count"
|
||||
label="登陆次数"
|
||||
width="120"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column prop="status" label="状态" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">{{
|
||||
scope.row.status === 1 ? "启用" : "禁用"
|
||||
}}</el-tag>
|
||||
</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' && scope.row.id !== 1"
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
@current-change="handlePageChange"
|
||||
layout="total, prev, pager, next"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 编辑用户对话框组件 -->
|
||||
<UserEditDialog
|
||||
ref="userEditRef"
|
||||
:modelValue="editDialogVisible"
|
||||
@update:modelValue="editDialogVisible = $event"
|
||||
:is-edit="isEdit"
|
||||
@submit="handleEditSuccess"
|
||||
@close="editDialogVisible = false"
|
||||
/>
|
||||
|
||||
<!-- 修改密码对话框组件 -->
|
||||
<ChangePasswordDialog
|
||||
ref="changePasswordRef"
|
||||
:modelValue="passwordDialogVisible"
|
||||
@update:modelValue="passwordDialogVisible = $event"
|
||||
:user-id="currentUserId"
|
||||
@submit="handlePasswordChangeSuccess"
|
||||
@close="passwordDialogVisible = false"
|
||||
/>
|
||||
|
||||
<!-- 预览用户对话框组件 -->
|
||||
<PreviewDialog
|
||||
ref="previewDialogRef"
|
||||
:modelValue="previewDialogVisible"
|
||||
@update:modelValue="previewDialogVisible = $event"
|
||||
:user="currentUser"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Refresh } from "@element-plus/icons-vue";
|
||||
import { getAllUsers, deleteUser } from "@/api/user";
|
||||
import { getAllRoles } from "@/api/role";
|
||||
import UserEditDialog from "./components/userEdit.vue";
|
||||
import ChangePasswordDialog from "./components/changePassword.vue";
|
||||
import PreviewDialog from "./components/preview.vue";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
account: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
qq: string;
|
||||
sex: number;
|
||||
group_id: number;
|
||||
status: number;
|
||||
last_login_ip: string;
|
||||
login_count: number;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
}
|
||||
|
||||
interface Role {
|
||||
id: number;
|
||||
name: string;
|
||||
status?: number;
|
||||
rights?: string;
|
||||
create_time?: string;
|
||||
update_time?: string;
|
||||
}
|
||||
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
|
||||
const users = ref<User[]>([]);
|
||||
const roles = ref<Role[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 组件引用
|
||||
const userEditRef = ref();
|
||||
const changePasswordRef = ref();
|
||||
const previewDialogRef = ref();
|
||||
|
||||
// 编辑/密码对话框状态
|
||||
const editDialogVisible = ref(false);
|
||||
const passwordDialogVisible = ref(false);
|
||||
const previewDialogVisible = ref(false);
|
||||
const editDialogTitle = ref("添加用户");
|
||||
const isEdit = ref(false);
|
||||
const currentUserId = ref<number | undefined>(undefined);
|
||||
const currentUser = ref<User | undefined>(undefined);
|
||||
|
||||
//刷新
|
||||
const refresh = async () => {
|
||||
await fetchUsers();
|
||||
};
|
||||
|
||||
// 获取角色列表
|
||||
const fetchRoles = async () => {
|
||||
try {
|
||||
const res = await getAllRoles();
|
||||
roles.value = res.data || [];
|
||||
} catch (e) {
|
||||
roles.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 获取角色tag状态
|
||||
function getRoleTagType(group_id: number): string {
|
||||
const typeMap: Record<number, string> = {
|
||||
1: "primary",
|
||||
2: "success",
|
||||
3: "warning",
|
||||
4: "danger",
|
||||
};
|
||||
return typeMap[group_id] || "primary";
|
||||
}
|
||||
|
||||
// 获取角色tag文本
|
||||
function getRoleTagText(roles: Role[] | undefined, group_id: number): string {
|
||||
if (!roles || !Array.isArray(roles)) {
|
||||
return "未知";
|
||||
}
|
||||
return roles.find((role) => role.id === group_id)?.name || "未知";
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getAllUsers();
|
||||
users.value = res.data.list;
|
||||
} catch (e) {
|
||||
users.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 添加用户
|
||||
const handleAddUser = () => {
|
||||
isEdit.value = false;
|
||||
editDialogVisible.value = true;
|
||||
if (userEditRef.value) {
|
||||
userEditRef.value.open();
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑用户
|
||||
const handleEdit = (user: User) => {
|
||||
isEdit.value = true;
|
||||
editDialogVisible.value = true;
|
||||
if (userEditRef.value) {
|
||||
userEditRef.value.open(user);
|
||||
}
|
||||
};
|
||||
|
||||
// 预览用户
|
||||
const handlePreview = (user: User) => {
|
||||
currentUser.value = user;
|
||||
previewDialogVisible.value = true;
|
||||
if (previewDialogRef.value) {
|
||||
previewDialogRef.value.open(user);
|
||||
}
|
||||
};
|
||||
|
||||
//修改密码
|
||||
const handleChangePassword = async (user: User) => {
|
||||
changePasswordRef.value.open(user.id, user.account);
|
||||
passwordDialogVisible.value = true;
|
||||
currentUserId.value = user.id;
|
||||
};
|
||||
|
||||
// 编辑成功回调
|
||||
const handleEditSuccess = () => {
|
||||
editDialogVisible.value = false;
|
||||
ElMessage.success(isEdit.value ? "编辑成功" : "添加成功");
|
||||
fetchUsers();
|
||||
};
|
||||
|
||||
// 密码修改成功回调
|
||||
const handlePasswordChangeSuccess = () => {
|
||||
passwordDialogVisible.value = false;
|
||||
ElMessage.success("密码修改成功");
|
||||
};
|
||||
|
||||
// 分页改变
|
||||
const handlePageChange = (val: number) => {
|
||||
page.value = val;
|
||||
fetchUsers();
|
||||
};
|
||||
|
||||
// 删除用户
|
||||
const handleDelete = async (user: User) => {
|
||||
ElMessageBox.confirm("确认删除该用户?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteUser(user.id);
|
||||
ElMessage.success("删除成功");
|
||||
fetchUsers();
|
||||
} catch (e) {
|
||||
ElMessage.error("删除失败");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
fetchUsers();
|
||||
fetchRoles();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-alert__title) {
|
||||
color: #f56c6c !important;
|
||||
}
|
||||
|
||||
.name-link {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #66b1ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user