更新模块市场功能

This commit is contained in:
李志强 2026-01-30 16:19:46 +08:00
parent 992b7e571b
commit df0507ed2f
7 changed files with 841 additions and 421 deletions

View File

@ -0,0 +1,11 @@
//拼接接口路径
const getEnvUrl = (path: string) => {
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
return `${API_BASE_URL}${path}`;
};
用例:
<img :src="getEnvUrl(module.thumb)" :alt="module.title" />
const url = getEnvUrl('/admin/moduleCenter/modules');

View File

@ -1,39 +1,39 @@
<el-form-item label="默认图片" prop="image"> <el-form-item label="默认图片" prop="image">
<div class="uploads"> <div class="uploads">
<el-upload v-model:file-list="fileList" :action="uploadUrl" list-type="picture-card" :headers="uploadHeaders" <el-upload v-model:file-list="fileList" :action="uploadUrl" list-type="picture-card"
:limit="1" :auto-upload="false" :before-upload="beforeImgUpload" :on-success="handleImgSuccess"> :headers="uploadHeaders" :limit="1" :auto-upload="false" :before-upload="beforeImgUpload"
<el-icon> :on-success="handleImgSuccess">
<Plus /> <el-icon>
</el-icon> <Plus />
</el-icon>
<template #file="{ file }"> <template #file="{ file }">
<div> <div>
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" /> <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
<span class="el-upload-list__item-actions"> <span class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)"> <span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
<el-icon> <el-icon>
<ZoomIn /> <ZoomIn />
</el-icon> </el-icon>
</span>
<span class="el-upload-list__item-delete" @click="handleRemove(file)">
<el-icon>
<Delete />
</el-icon>
</span>
</span> </span>
<span class="el-upload-list__item-delete" @click="handleRemove(file)"> </div>
<el-icon> </template>
<Delete /> </el-upload>
</el-icon> <el-dialog v-model="dialogVisible">
</span> <img w-full :src="dialogImageUrl" alt="Preview Image" />
</span> </el-dialog>
</div>
</template>
</el-upload>
<el-dialog v-model="dialogVisible">
<img w-full :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
<div class="upload-tip"> <div class="upload-tip">
<span>建议尺寸250px × 140px</span> <span>建议尺寸250px × 140px</span>
</div>
</div> </div>
</div> </el-form-item>
</el-form-item>
import { uploadFile } from '@/api/file.js'; import { uploadFile } from '@/api/file.js';
import { ElMessage, ElUpload } from 'element-plus' import { ElMessage, ElUpload } from 'element-plus'

57
src/api/moduleCenter.js Normal file
View File

@ -0,0 +1,57 @@
import request from "@/utils/request";
/**
* 获取模块中心分类
* @returns {Promise}
*/
export function getModuleCategory() {
return request({
url: "/admin/moduleCategory",
method: "get",
});
}
/**
* 获取模块中心列表
* @param {number} cid 分类id
* @returns {Promise}
*/
export function getModules(cid) {
return request({
url: "/admin/moduleCenter/modules",
method: "get",
params: { cid }
});
}
/**
* 编辑模块分类
* @param {Object} data 分类数据
* @param {number} data.id 分类id编辑时必填新增时不填
* @param {string} data.title 分类名称
* @param {number} data.status 分类状态
* @returns {Promise}
*/
export function editModuleCategory(data) {
return request({
url: "/admin/moduleCenter/editCategory",
method: "post",
data
});
}
/**
* 编辑模块
* @param {Object} data 模块数据
* @param {number} data.id 模块id编辑时必填新增时不填
* @param {string} data.title 模块名称
* @param {number} data.status 模块状态
* @returns {Promise}
*/
export function editModules(data) {
return request({
url: "/admin/moduleCenter/editModules",
method: "post",
data
});
}

View File

@ -54,37 +54,69 @@
</div> </div>
</div> </div>
<div class="home-wrapper"> <div class="home-wrapper">
<!-- <div class="welcome-section">
<div class="welcome-content">
<h1 class="welcome-title">
<span class="greeting">您好</span>
<span class="username">{{ userName }}</span>
</h1>
<p class="welcome-subtitle">欢迎进入系统管理平台</p>
<p class="welcome-desc">选择下方功能模块开始管理您的系统</p>
</div>
<div class="welcome-decoration">
<div class="decoration-circle circle-1"></div>
<div class="decoration-circle circle-2"></div>
<div class="decoration-circle circle-3"></div>
</div>
</div> -->
<div class="modules-section" v-if="moduleList.length > 0"> <div class="modules-section" v-if="moduleList.length > 0">
<div class="module-grid"> <!-- 功能模块 (type=1) -->
<div <div v-if="basicModules.length > 0" class="category-section">
v-for="module in moduleList" <div class="category-title">功能模块</div>
:key="module.path" <div class="module-grid">
class="module-card" <div
@click="handleNavigate(module.path)" v-for="module in basicModules"
> :key="module.path"
<div class="card-header"> class="module-card"
<span class="card-title">{{ module.name }}</span> @click="handleNavigate(module.path)"
<div class="card-icon-wrapper" v-html="module.icon"></div> >
<div class="card-header">
<span class="card-title">{{ module.name }}</span>
<div class="card-icon-wrapper" v-html="module.icon"></div>
</div>
<div class="card-divider"></div>
<div class="card-content">
<p class="card-desc">{{ module.description || "暂无描述" }}</p>
</div>
</div> </div>
<div class="card-divider"></div> </div>
<div class="card-content"> </div>
<p class="card-desc">{{ module.description || "暂无描述" }}</p>
<!-- 系统设置 (type=2) -->
<div v-if="systemModules.length > 0" class="category-section">
<div class="category-title">系统设置</div>
<div class="module-grid">
<div
v-for="module in systemModules"
:key="module.path"
class="module-card"
@click="handleNavigate(module.path)"
>
<div class="card-header">
<span class="card-title">{{ module.name }}</span>
<div class="card-icon-wrapper" v-html="module.icon"></div>
</div>
<div class="card-divider"></div>
<div class="card-content">
<p class="card-desc">{{ module.description || "暂无描述" }}</p>
</div>
</div>
</div>
</div>
<!-- 未分类 (type=0) -->
<div v-if="uncategorizedModules.length > 0" class="category-section">
<div class="category-title">未分类</div>
<div class="module-grid">
<div
v-for="module in uncategorizedModules"
:key="module.path"
class="module-card"
@click="handleNavigate(module.path)"
>
<div class="card-header">
<span class="card-title">{{ module.name }}</span>
<div class="card-icon-wrapper" v-html="module.icon"></div>
</div>
<div class="card-divider"></div>
<div class="card-content">
<p class="card-desc">{{ module.description || "暂无描述" }}</p>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -127,12 +159,25 @@ interface ModuleItem {
description: string; description: string;
sort: number; sort: number;
status: number; status: number;
type: number;
title?: string;
} }
const router = useRouter(); const router = useRouter();
const authStore = useAuthStore(); const authStore = useAuthStore();
const menuStore = useMenuStore(); const menuStore = useMenuStore();
// type
const basicModules = computed(() =>
moduleList.value.filter((item) => item.type === 1)
);
const systemModules = computed(() =>
moduleList.value.filter((item) => item.type === 2)
);
const uncategorizedModules = computed(() =>
moduleList.value.filter((item) => item.type === 0)
);
const moduleList = ref<ModuleItem[]>([]); const moduleList = ref<ModuleItem[]>([]);
const refreshLoading = ref(false); const refreshLoading = ref(false);
@ -231,7 +276,12 @@ async function loadModules() {
const list = res.data?.list || []; const list = res.data?.list || [];
moduleList.value = list moduleList.value = list
.filter((item: ModuleItem) => item.status === 1 && item.is_show === 1) .filter((item: ModuleItem) => item.status === 1 && item.is_show === 1)
.sort((a, b) => Number(a.sort) - Number(b.sort)); .sort((a, b) => Number(a.sort) - Number(b.sort))
.map((item: ModuleItem) => ({
...item,
type: item.type || 0, // 0
title: item.name // title
}));
} }
} catch (error) { } catch (error) {
console.error("加载模块列表失败:", error); console.error("加载模块列表失败:", error);
@ -416,14 +466,27 @@ $text-regular: #606266;
.modules-section { .modules-section {
margin-top: 20px; margin-top: 20px;
.category-section {
margin-bottom: 32px;
.category-title {
font-size: 16px;
font-weight: 600;
color: $text-main;
margin: 0 0 20px 0;
padding-left: 4px;
border-left: 4px solid #667eea;
}
}
.module-grid { .module-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(5, 1fr);
gap: 20px; gap: 20px;
} }
.module-card { .module-card {
height: 220px; height: 180px;
background: #fff; background: #fff;
border-radius: 12px; border-radius: 12px;
padding: 30px; padding: 30px;
@ -468,7 +531,7 @@ $text-regular: #606266;
margin: 0; margin: 0;
line-height: 1.5; line-height: 1.5;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 3;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
} }

View File

@ -0,0 +1,313 @@
<template>
<div class="category-container">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>分类管理</span>
<el-button type="primary" :icon="Plus" @click="handleCategoryAdd">
新增分类
</el-button>
</div>
</template>
<el-table
:data="categoryList"
v-loading="loading"
stripe
border
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="分类标题" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
link
size="small"
:icon="Edit"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
type="danger"
link
size="small"
:icon="Delete"
@click="handleDelete(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="categoryList.length === 0 && !loading" class="empty-state">
<el-empty description="暂无分类记录" />
</div>
</el-card>
<el-dialog
v-model="categoryDialogVisible"
:title="isEdit ? '编辑分类' : '新增分类'"
width="700px"
:close-on-click-modal="false"
@close="handleCategoryClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
>
<el-form-item label="分类标题" prop="title">
<el-input
v-model="formData.title"
placeholder="请输入分类标题"
clearable
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="categoryDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Edit, Delete } from "@element-plus/icons-vue";
import type { FormInstance, FormRules } from "element-plus";
import { getModuleCategory,editModuleCategory } from "@/api/moduleCenter";
interface Category {
id: number;
title: string;
module_name: string;
module_id?: number;
status: number;
}
interface ModuleOption {
id: number;
name: string;
}
const loading = ref(false);
const categoryList = ref<Category[]>([]);
const categoryDialogVisible = ref(false);
const submitLoading = ref(false);
const isEdit = ref(false);
const editId = ref<number>(0);
const formRef = ref<FormInstance>();
const formData = reactive({
title: "",
module_id: null as number | null,
status: 1
});
const moduleOptions = ref<ModuleOption[]>([]);
const formRules: FormRules = {
title: [
{ required: true, message: "请输入分类标题", trigger: "blur" }
],
module_id: [
{ required: true, message: "请选择所属模块", trigger: "change" }
],
status: [
{ required: true, message: "请选择状态", trigger: "change" }
]
};
const getStatusText = (status: number) => {
const map: Record<number, string> = {
0: "禁用",
1: "启用"
};
return map[status] || "未知";
};
const getStatusType = (status: number) => {
const map: Record<number, any> = {
0: "info",
1: "success",
2: "warning"
};
return map[status] || "info";
};
const formatDate = (date: string) => {
if (!date) return "-";
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hours = String(d.getHours()).padStart(2, "0");
const minutes = String(d.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
const handleCategoryAdd = () => {
isEdit.value = false;
editId.value = 0;
formData.title = "";
formData.module_id = null;
formData.status = 1;
categoryDialogVisible.value = true;
fetchModuleOptions();
};
const handleEdit = (row: Category) => {
isEdit.value = true;
editId.value = row.id;
formData.title = row.title;
formData.module_id = row.module_id || null;
formData.status = row.status;
categoryDialogVisible.value = true;
fetchModuleOptions();
};
const handleView = (row: Category) => {
ElMessage.info("查看分类详情功能开发中");
};
const handleDelete = (row: Category) => {
ElMessageBox.confirm(`确定要删除分类"${row.title}"吗?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
try {
// TODO:
// await deleteCategory(row.id);
ElMessage.success("删除成功");
handleRefresh();
} catch (error) {
console.error("删除失败:", error);
ElMessage.error("删除失败");
}
}).catch(() => {});
};
const handleCategoryClose = () => {
formRef.value?.resetFields();
isEdit.value = false;
editId.value = 0;
categoryDialogVisible.value = false;
};
const handleSubmit = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true;
try {
const data = {
title: formData.title,
status: formData.status
};
if (isEdit.value) {
data.id = editId.value;
await editModuleCategory(data);
ElMessage.success("编辑成功");
} else {
await editModuleCategory(data);
ElMessage.success("添加成功");
}
handleCategoryClose();
handleRefresh();
} catch (error) {
console.error(isEdit.value ? "编辑失败:" : "添加失败:", error);
ElMessage.error(isEdit.value ? "编辑失败" : "添加失败");
} finally {
submitLoading.value = false;
}
}
});
};
const handleRefresh = () => {
fetchCategoryList();
};
const fetchCategoryList = async () => {
loading.value = true;
try {
const res = await getModuleCategory();
if (res.code === 200 && res.data) {
categoryList.value = res.data.list || [];
}
} catch (error) {
console.error("获取分类列表失败:", error);
ElMessage.error("获取分类列表失败");
} finally {
loading.value = false;
}
};
const fetchModuleOptions = async () => {
try {
// TODO:
// const res = await getModules(0);
// if (res.code === 200) {
// moduleOptions.value = res.data.list.map((item: any) => ({
// id: item.id,
// name: item.title
// }));
// }
moduleOptions.value = [
{ id: 1, name: "CMS内容管理" },
{ id: 2, name: "用户管理" },
{ id: 3, name: "数据分析" },
{ id: 4, name: "系统设置" }
];
} catch (error) {
console.error("获取模块列表失败:", error);
}
};
onMounted(() => {
fetchCategoryList();
});
</script>
<style scoped lang="scss">
.category-container {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.empty-state {
padding: 40px 0;
}
}
</style>

View File

@ -2,51 +2,44 @@
<div class="modules-container"> <div class="modules-container">
<div class="header-section"> <div class="header-section">
<div class="header-top"> <div class="header-top">
<div class="category-tabs"> <div class="category-tabs">
<div <div v-for="category in categories" :key="category.id" class="category-item"
v-for="category in categories" :class="{ active: activeCategory === category.id }" @click="handleCategoryChange(category.id)">
:key="category.id" {{ category.title }}
class="category-item" </div>
:class="{ active: activeCategory === category.id }"
@click="handleCategoryChange(category.id)"
>
{{ category.name }}
</div> </div>
</div> </div>
</div>
</div> </div>
<div v-loading="loading" class="modules-grid"> <div v-loading="loading" class="modules-grid">
<div <div v-for="module in moduleList" :key="module.id" class="module-card" :class="{ disabled: module.status === 0 }">
v-for="module in moduleList"
:key="module.id"
class="module-card"
:class="{ disabled: module.status === 0 }"
>
<div class="module-preview"> <div class="module-preview">
<div class="preview-image"> <img v-if="module.thumb" :src="getEnvUrl(module.thumb)" :alt="module.title" />
<el-icon :class="module.icon" :size="40" /> <div v-else class="placeholder"></div>
</div>
</div> </div>
<div class="module-content"> <div class="module-content">
<h3 class="module-name">{{ module.name }}</h3> <h3 class="module-name">{{ module.title }}</h3>
<p class="module-desc">{{ module.description || '暂无描述' }}</p> <p class="module-desc">{{ module.desc || '暂无描述' }}</p>
<div class="module-footer"> <div class="module-footer">
<div class="module-company"> <div class="module-category">
<el-icon><OfficeBuilding /></el-icon> <el-icon>
<span>{{ module.code }}</span> <OfficeBuilding />
</el-icon>
<span>{{ module.cid || '-' }}</span>
</div> </div>
<div class="module-downloads"> <div class="module-downloads">
<el-icon><Download /></el-icon> <el-icon>
<span>{{ module.sort }} </span> <Download />
</el-icon>
<span>{{ module.downloads }} </span>
</div> </div>
</div> </div>
</div> </div>
<div class="module-actions"> <div class="module-actions">
<el-dropdown trigger="click" @command="(cmd) => handleCommand(cmd, module)"> <el-dropdown trigger="click" @command="(cmd) => handleCommand(cmd, module)">
<el-button type="primary" link :icon="MoreFilled" circle /> <el-button type="primary" link :icon="MoreFilled" circle class="action-item" />
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item command="edit" :icon="Edit">编辑</el-dropdown-item> <el-dropdown-item command="edit" :icon="Edit">编辑</el-dropdown-item>
@ -65,80 +58,65 @@
<el-empty description="暂无模块数据" :image-size="120" /> <el-empty description="暂无模块数据" :image-size="120" />
</div> </div>
<create-modules <create-modules v-model="createDialogVisible" @success="handleRefresh" />
v-model="createDialogVisible"
@success="handleRefresh"
/>
<el-dialog <el-dialog v-model="editDialogVisible" title="编辑模块" width="600px" :close-on-click-modal="false"
v-model="editDialogVisible" @close="handleEditClose">
title="编辑模块" <el-form ref="editFormRef" :model="editFormData" :rules="formRules" label-width="100px">
width="600px" <el-form-item label="模块名称" prop="title">
:close-on-click-modal="false" <el-input v-model="editFormData.title" placeholder="请输入模块名称" clearable />
@close="handleEditClose"
>
<el-form
ref="editFormRef"
:model="editFormData"
:rules="formRules"
label-width="100px"
>
<el-form-item label="模块名称" prop="name">
<el-input
v-model="editFormData.name"
placeholder="请输入模块名称"
clearable
/>
</el-form-item> </el-form-item>
<el-form-item label="模块代码" prop="code"> <el-form-item label="模块描述" prop="desc">
<el-input <el-input v-model="editFormData.desc" type="textarea" :rows="3" placeholder="请输入模块描述" />
v-model="editFormData.code"
placeholder="请输入模块代码"
clearable
/>
</el-form-item> </el-form-item>
<el-form-item label="模块图标" prop="icon"> <el-form-item label="缩略图" prop="thumb">
<el-input <div class="uploads">
v-model="editFormData.icon" <el-upload v-model:file-list="fileList" :auto-upload="false" list-type="picture-card"
placeholder="请输入图标类名,如: fa-solid fa-home" :limit="1" :before-upload="beforeImgUpload" :on-change="handleUploadChange">
clearable <el-icon>
> <Plus />
<template #prefix> </el-icon>
<el-icon v-if="editFormData.icon" :class="editFormData.icon" />
</template> <template #file="{ file }">
</el-input> <div>
<img class="el-upload-list__item-thumbnail" :src="getEnvUrl(file.url)" alt="" />
<span class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
<el-icon>
<ZoomIn />
</el-icon>
</span>
<span class="el-upload-list__item-delete" @click="handleRemove(file)">
<el-icon>
<Delete />
</el-icon>
</span>
</span>
</div>
</template>
</el-upload>
<el-dialog v-model="dialogVisible">
<img w-full :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
<div class="upload-tip">
<span>建议尺寸250px × 140px</span>
</div>
</div>
</el-form-item> </el-form-item>
<el-form-item label="模块描述" prop="description">
<el-input
v-model="editFormData.description"
type="textarea"
:rows="3"
placeholder="请输入模块描述"
/>
</el-form-item>
<el-form-item label="模块类型" prop="type"> <el-form-item label="所属分类" prop="cid">
<el-select <el-select v-model="editFormData.cid" placeholder="请选择所属分类">
v-model="editFormData.type" <el-option v-for="cat in categories.filter(c => c.id > 0)" :key="cat.id" :label="cat.title"
placeholder="请选择模块类型" :value="cat.id" />
>
<el-option label="前端模块" :value="1" />
<el-option label="后端模块" :value="2" />
<el-option label="全栈模块" :value="3" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="排序" prop="sort"> <el-form-item label="版本号" prop="version">
<el-input-number <el-input v-model="editFormData.version" placeholder="请输入版本号,如: 1.0.0" clearable />
v-model="editFormData.sort"
:min="0"
:max="999"
controls-position="right"
placeholder="请输入排序"
/>
</el-form-item> </el-form-item>
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
@ -162,21 +140,23 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted } from "vue"; import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox, ElUpload } from "element-plus";
import { Plus, Edit, Delete, Sort, Clock, MoreFilled, OfficeBuilding, Download } from "@element-plus/icons-vue"; import { Plus, Edit, Delete, MoreFilled, OfficeBuilding, Download, ZoomIn } from "@element-plus/icons-vue";
import type { FormInstance, FormRules } from "element-plus"; import type { FormInstance, FormRules } from "element-plus";
import { getModules, getModuleCategory,editModules } from "@/api/moduleCenter";
import CreateModules from "../components/createModules.vue"; import CreateModules from "../components/createModules.vue";
import { uploadFile } from '@/api/file.js';
interface Module { interface Module {
id: number; id: number;
name: string; title: string;
code: string; desc: string;
icon: string; thumb: string;
description: string; cid: number;
type: number; downloads: number;
sort: number;
status: number; status: number;
created_at: string; version: string;
create_time: string;
} }
const loading = ref(false); const loading = ref(false);
@ -185,20 +165,16 @@ const createDialogVisible = ref(false);
const editDialogVisible = ref(false); const editDialogVisible = ref(false);
const editLoading = ref(false); const editLoading = ref(false);
const activeCategory = ref(0); const activeCategory = ref(0);
const isEdit = ref(false);
const editId = ref<number>(0);
// //
const categories = ref([ const categories = ref([
{ id: 0, name: "全部" }, { id: 0, title: "全部" }
{ id: 1, name: "内容管理" },
{ id: 2, name: "用户管理" },
{ id: 3, name: "数据分析" },
{ id: 4, name: "系统工具" },
{ id: 5, name: "营销推广" }
]); ]);
const handleCategoryChange = (categoryId: number) => { const handleCategoryChange = (categoryId: number) => {
activeCategory.value = categoryId; activeCategory.value = categoryId;
// TODO:
fetchModuleList(categoryId); fetchModuleList(categoryId);
}; };
@ -206,48 +182,89 @@ const editFormRef = ref<FormInstance>();
const editFormData = reactive({ const editFormData = reactive({
id: 0, id: 0,
name: "", title: "",
code: "", desc: "",
icon: "", thumb: "",
description: "", cid: 0,
type: 3, downloads: 0,
sort: 0, version: "",
status: 1 status: 1
}); });
const formRules: FormRules = { const formRules: FormRules = {
name: [ title: [
{ required: true, message: "请输入模块名称", trigger: "blur" } { required: true, message: "请输入模块名称", trigger: "blur" }
], ],
code: [ cid: [
{ required: true, message: "请输入模块代码", trigger: "blur" }, { required: true, message: "请选择分类", trigger: "change" }
{ pattern: /^[a-zA-Z][a-zA-Z0-9]*$/, message: "代码必须以字母开头,只能包含字母和数字", trigger: "blur" }
],
type: [
{ required: true, message: "请选择模块类型", trigger: "change" }
], ],
status: [ status: [
{ required: true, message: "请选择状态", trigger: "change" } { required: true, message: "请选择状态", trigger: "change" }
] ]
}; };
const getTypeText = (type: number) => { //
const map: Record<number, string> = { const getEnvUrl = (path: string) => {
1: "前端模块", const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
2: "后端模块", return `${API_BASE_URL}${path}`;
3: "全栈模块"
};
return map[type] || "未知";
}; };
const getTypeTagType = (type: number) => { //
const map: Record<number, any> = { async function getModuleCategoryList() {
1: "", try {
2: "warning", const res = await getModuleCategory();
3: "success" if (res.code === 200 && res.data) {
}; categories.value = [
return map[type] || "info"; { id: 0, title: "全部" },
}; ...res.data.list || []
];
// console.log(":", categories.value);
}
} catch (error) {
console.error("获取分类失败:", error);
}
}
//
const fileList = ref<any[]>([]);
const dialogVisible = ref(false);
const dialogImageUrl = ref('');
function beforeImgUpload(file: File) {
const isImage = file.type.startsWith('image/');
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isImage) ElMessage.error('仅支持图片格式');
if (!isLt10M) ElMessage.error('图片大小不能超过10MB');
return isImage && isLt10M;
}
function handleRemove(file: any) {
fileList.value = [];
editFormData.thumb = '';
}
async function handleUploadChange(file: any) {
if (file.raw) {
const isImage = file.raw.type.startsWith('image/');
const isLt10M = file.raw.size / 1024 / 1024 < 10;
if (!isImage || !isLt10M) {
fileList.value = [];
ElMessage.error(isImage ? '图片大小不能超过10MB' : '仅支持图片格式');
return;
}
}
}
function handlePictureCardPreview(file: any) {
dialogImageUrl.value = file.url;
dialogVisible.value = true;
}
const formatDate = (date: string) => { const formatDate = (date: string) => {
if (!date) return "-"; if (!date) return "-";
@ -273,11 +290,25 @@ const handleCommand = (command: string, row: Module) => {
}; };
const handleEdit = (row: Module) => { const handleEdit = (row: Module) => {
isEdit.value = true;
editId.value = row.id;
Object.assign(editFormData, row); Object.assign(editFormData, row);
//
if (row.thumb) {
fileList.value = [{
name: 'thumb',
url: row.thumb
}];
} else {
fileList.value = [];
}
editDialogVisible.value = true; editDialogVisible.value = true;
}; };
const handleEditClose = () => { const handleEditClose = () => {
isEdit.value = false;
editId.value = 0;
fileList.value = [];
editFormRef.value?.resetFields(); editFormRef.value?.resetFields();
editDialogVisible.value = false; editDialogVisible.value = false;
}; };
@ -289,15 +320,43 @@ const handleEditSubmit = async () => {
if (valid) { if (valid) {
editLoading.value = true; editLoading.value = true;
try { try {
// TODO: let imageUrl = editFormData.thumb;
// await updateModule(editFormData.id, editFormData);
ElMessage.success("编辑成功"); //
editDialogVisible.value = false; if (fileList.value.length > 0 && fileList.value[0].raw) {
const uploadFormData = new FormData();
uploadFormData.append('file', fileList.value[0].raw);
uploadFormData.append('cate', 'module');
const uploadRes = await uploadFile(uploadFormData);
if (uploadRes?.data?.url) {
imageUrl = uploadRes.data.url;
}
}
const data = {
title: editFormData.title,
desc: editFormData.desc,
thumb: imageUrl,
cid: editFormData.cid,
downloads: editFormData.downloads,
version: editFormData.version,
status: editFormData.status
};
if (isEdit.value) {
data.id = editId.value;
await editModules(data);
ElMessage.success("编辑成功");
} else {
await editModules(data);
ElMessage.success("添加成功");
}
handleEditClose();
handleRefresh(); handleRefresh();
} catch (error) { } catch (error) {
console.error("编辑失败:", error); console.error(isEdit.value ? "编辑失败:" : "添加失败:", error);
ElMessage.error("编辑失败"); ElMessage.error(isEdit.value ? "编辑失败" : "添加失败");
} finally { } finally {
editLoading.value = false; editLoading.value = false;
} }
@ -319,7 +378,7 @@ const handleStatusChange = async (row: Module) => {
}; };
const handleDelete = (row: Module) => { const handleDelete = (row: Module) => {
ElMessageBox.confirm(`确定要删除模块"${row.name}"吗?`, "提示", { ElMessageBox.confirm(`确定要删除模块"${row.title}"吗?`, "提示", {
confirmButtonText: "确定", confirmButtonText: "确定",
cancelButtonText: "取消", cancelButtonText: "取消",
type: "warning" type: "warning"
@ -334,7 +393,7 @@ const handleDelete = (row: Module) => {
console.error("删除失败:", error); console.error("删除失败:", error);
ElMessage.error("删除失败"); ElMessage.error("删除失败");
} }
}).catch(() => {}); }).catch(() => { });
}; };
const handleRefresh = () => { const handleRefresh = () => {
@ -344,93 +403,21 @@ const handleRefresh = () => {
const fetchModuleList = async (categoryId?: number) => { const fetchModuleList = async (categoryId?: number) => {
loading.value = true; loading.value = true;
try { try {
// TODO: categoryId //
// const res = await getModuleList({ categoryId }); const res = await getModules(categoryId || 0);
// if (res.code === 200) { if (res.code === 200 && res.data) {
// moduleList.value = res.data || []; //
// } moduleList.value = res.data.list.map((item: any) => ({
id: item.id,
// title: item.title,
const allModules = [ desc: item.desc,
{ thumb: item.thumb,
id: 1, cid: item.cid,
name: "CMS内容管理", downloads: item.downloads,
code: "cms", status: item.status,
icon: "fa-solid fa-newspaper", version: item.version,
description: "内容管理系统模块,支持文章发布、分类管理等功能", create_time: item.create_time
type: 1, }));
sort: 128,
status: 1,
categoryId: 1,
created_at: "2026-01-29 10:00:00"
},
{
id: 2,
name: "用户管理",
code: "user",
icon: "fa-solid fa-user",
description: "用户管理系统模块,支持用户增删改查、角色权限管理",
type: 2,
sort: 256,
status: 1,
categoryId: 2,
created_at: "2026-01-29 10:00:00"
},
{
id: 3,
name: "数据分析",
code: "analytics",
icon: "fa-solid fa-chart-column",
description: "数据分析模块,提供丰富的数据统计和可视化图表",
type: 2,
sort: 89,
status: 1,
categoryId: 3,
created_at: "2026-01-29 10:00:00"
},
{
id: 4,
name: "系统设置",
code: "system",
icon: "fa-solid fa-gear",
description: "系统配置管理模块,包含站点设置、菜单管理等功能",
type: 3,
sort: 45,
status: 0,
categoryId: 4,
created_at: "2026-01-29 10:00:00"
},
{
id: 5,
name: "SEO优化",
code: "seo",
icon: "fa-solid fa-magnifying-glass",
description: "搜索引擎优化模块,提升网站排名和流量",
type: 1,
sort: 67,
status: 1,
categoryId: 5,
created_at: "2026-01-29 10:00:00"
},
{
id: 6,
name: "表单构建器",
code: "form-builder",
icon: "fa-solid fa-wpforms",
description: "可视化表单构建工具,快速创建各类表单",
type: 1,
sort: 156,
status: 1,
categoryId: 4,
created_at: "2026-01-29 10:00:00"
}
];
//
if (categoryId && categoryId !== 0) {
moduleList.value = allModules.filter(m => m.categoryId === categoryId);
} else {
moduleList.value = allModules;
} }
} catch (error) { } catch (error) {
console.error("获取模块列表失败:", error); console.error("获取模块列表失败:", error);
@ -442,6 +429,7 @@ const fetchModuleList = async (categoryId?: number) => {
onMounted(() => { onMounted(() => {
fetchModuleList(); fetchModuleList();
getModuleCategoryList();
}); });
</script> </script>
@ -478,7 +466,7 @@ onMounted(() => {
border-radius: 20px; border-radius: 20px;
font-size: 14px; font-size: 14px;
color: var(--el-text-color-regular); color: var(--el-text-color-regular);
background: var(--el-fill-color-light); background: var(--el-bg-color);
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
border: 1px solid transparent; border: 1px solid transparent;
@ -499,7 +487,7 @@ onMounted(() => {
.modules-grid { .modules-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); grid-template-columns: repeat(5, 1fr);
gap: 20px; gap: 20px;
.module-card { .module-card {
@ -532,6 +520,12 @@ onMounted(() => {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
img{
width: 100%;
height: 100%;
object-fit: cover;
}
&::before { &::before {
content: ''; content: '';
position: absolute; position: absolute;
@ -539,24 +533,9 @@ onMounted(() => {
right: -50%; right: -50%;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: linear-gradient(135deg, rgba(255,255,255,0.3) 0%, transparent 100%); background: linear-gradient(135deg, rgba(255, 255, 255, 0.3) 0%, transparent 100%);
transform: rotate(45deg); transform: rotate(45deg);
} }
.preview-image {
width: 80px;
height: 80px;
border-radius: 16px;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
.el-icon {
color: var(--el-color-primary);
}
}
} }
.module-content { .module-content {
@ -589,7 +568,7 @@ onMounted(() => {
padding-top: 16px; padding-top: 16px;
border-top: 1px solid var(--el-border-color-lighter); border-top: 1px solid var(--el-border-color-lighter);
.module-company, .module-category,
.module-downloads { .module-downloads {
display: flex; display: flex;
align-items: center; align-items: center;
@ -612,6 +591,11 @@ onMounted(() => {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
.action-item {
width: 16px;
height: 16px;
}
.el-button { .el-button {
color: var(--el-text-color-regular); color: var(--el-text-color-regular);
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
@ -634,14 +618,63 @@ onMounted(() => {
padding: 80px 20px; padding: 80px 20px;
text-align: center; text-align: center;
} }
.uploads {
display: flex;
flex-direction: column;
}
.upload-tip {
font-size: 12px;
color: #999;
margin-top: 8px;
}
} }
@media (max-width: 768px) { @media (max-width: 1400px) {
.modules-container { .modules-container {
.modules-grid { .modules-grid {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-template-columns: repeat(4, 1fr);
gap: 16px;
} }
} }
} }
@media (max-width: 1200px) {
.modules-container {
.modules-grid {
grid-template-columns: repeat(3, 1fr);
}
}
}
@media (max-width: 992px) {
.modules-container {
.modules-grid {
grid-template-columns: repeat(2, 1fr);
}
}
}
@media (max-width: 576px) {
.modules-container {
.modules-grid {
grid-template-columns: 1fr;
}
}
}
.uploads{
display: flex;
flex-direction: column;
}
.upload-tip {
font-size: 12px;
color: #999;
}
:deep(.el-upload-list__item-thumbnail) {
width: 100%;
height: 100%;
object-fit: cover;
}
</style> </style>

View File

@ -5,7 +5,7 @@
<div class="card-header"> <div class="card-header">
<span>发布管理</span> <span>发布管理</span>
<el-button type="primary" :icon="Plus" @click="handlePublish"> <el-button type="primary" :icon="Plus" @click="handlePublish">
新增发布 新增模块
</el-button> </el-button>
</div> </div>
</template> </template>
@ -17,16 +17,9 @@
border border
> >
<el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="发布标题" min-width="200" show-overflow-tooltip /> <el-table-column prop="title" label="模块标题" min-width="200" show-overflow-tooltip />
<el-table-column prop="version" label="版本号" width="120" /> <el-table-column prop="version" label="版本号" width="120" />
<el-table-column prop="module_name" label="所属模块" width="150" /> <el-table-column prop="module_name" label="所属分类" width="150" />
<el-table-column prop="type" label="发布类型" width="100">
<template #default="{ row }">
<el-tag :type="getTypeTagType(row.type)">
{{ getTypeText(row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100"> <el-table-column prop="status" label="状态" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="getStatusType(row.status)"> <el-tag :type="getStatusType(row.status)">
@ -34,6 +27,7 @@
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="author" label="作者" width="120" />
<el-table-column prop="publish_time" label="发布时间" width="180"> <el-table-column prop="publish_time" label="发布时间" width="180">
<template #default="{ row }"> <template #default="{ row }">
{{ formatDate(row.publish_time) }} {{ formatDate(row.publish_time) }}
@ -70,7 +64,7 @@
<el-dialog <el-dialog
v-model="publishDialogVisible" v-model="publishDialogVisible"
title="新增发布" title="新增模块"
width="700px" width="700px"
:close-on-click-modal="false" :close-on-click-modal="false"
@close="handlePublishClose" @close="handlePublishClose"
@ -81,10 +75,10 @@
:rules="formRules" :rules="formRules"
label-width="120px" label-width="120px"
> >
<el-form-item label="发布标题" prop="title"> <el-form-item label="模块标题" prop="title">
<el-input <el-input
v-model="formData.title" v-model="formData.title"
placeholder="请输入发布标题" placeholder="请输入模块标题"
clearable clearable
/> />
</el-form-item> </el-form-item>
@ -97,10 +91,10 @@
/> />
</el-form-item> </el-form-item>
<el-form-item label="所属模块" prop="module_id"> <el-form-item label="所属分类" prop="module_id">
<el-select <el-select
v-model="formData.module_id" v-model="formData.module_id"
placeholder="请选择所属模块" placeholder="请选择所属分类"
clearable clearable
> >
<el-option <el-option
@ -112,14 +106,6 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="发布类型" prop="type">
<el-radio-group v-model="formData.type">
<el-radio :value="1">更新</el-radio>
<el-radio :value="2">新增</el-radio>
<el-radio :value="3">修复</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status"> <el-radio-group v-model="formData.status">
<el-radio :value="1">发布</el-radio> <el-radio :value="1">发布</el-radio>
@ -162,12 +148,12 @@
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="发布说明" prop="description"> <el-form-item label="模块说明" prop="description">
<el-input <el-input
v-model="formData.description" v-model="formData.description"
type="textarea" type="textarea"
:rows="4" :rows="4"
placeholder="请输入发布说明" placeholder="请输入模块说明"
/> />
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -188,12 +174,14 @@ import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, View, Delete, UploadFilled, Document } from "@element-plus/icons-vue"; import { Plus, View, Delete, UploadFilled, Document } from "@element-plus/icons-vue";
import type { FormInstance, FormRules, UploadFile } from "element-plus"; import type { FormInstance, FormRules, UploadFile } from "element-plus";
import { getModules, getModuleCategory } from "@/api/moduleCenter";
interface Publish { interface Publish {
id: number; id: number;
title: string; title: string;
version: string; version: string;
author: string;
module_name: string; module_name: string;
type: number;
status: number; status: number;
publish_time: string; publish_time: string;
} }
@ -214,8 +202,8 @@ const uploadRef = ref();
const formData = reactive({ const formData = reactive({
title: "", title: "",
version: "", version: "",
author: "",
module_id: null as number | null, module_id: null as number | null,
type: 2,
status: 1, status: 1,
description: "", description: "",
packageFile: null as File | null packageFile: null as File | null
@ -225,41 +213,20 @@ const moduleOptions = ref<ModuleOption[]>([]);
const formRules: FormRules = { const formRules: FormRules = {
title: [ title: [
{ required: true, message: "请输入发布标题", trigger: "blur" } { required: true, message: "请输入模块标题", trigger: "blur" }
], ],
version: [ version: [
{ required: true, message: "请输入版本号", trigger: "blur" }, { required: true, message: "请输入版本号", trigger: "blur" },
{ pattern: /^\d+\.\d+\.\d+$/, message: "版本号格式不正确,如: 1.0.0", trigger: "blur" } { pattern: /^\d+\.\d+\.\d+$/, message: "版本号格式不正确,如: 1.0.0", trigger: "blur" }
], ],
module_id: [ module_id: [
{ required: true, message: "请选择所属模块", trigger: "change" } { required: true, message: "请选择所属分类", trigger: "change" }
],
type: [
{ required: true, message: "请选择发布类型", trigger: "change" }
], ],
status: [ status: [
{ required: true, message: "请选择状态", trigger: "change" } { required: true, message: "请选择状态", trigger: "change" }
] ]
}; };
const getTypeText = (type: number) => {
const map: Record<number, string> = {
1: "更新",
2: "新增",
3: "修复"
};
return map[type] || "未知";
};
const getTypeTagType = (type: number) => {
const map: Record<number, any> = {
1: "warning",
2: "success",
3: "danger"
};
return map[type] || "info";
};
const getStatusText = (status: number) => { const getStatusText = (status: number) => {
const map: Record<number, string> = { const map: Record<number, string> = {
0: "草稿", 0: "草稿",
@ -295,11 +262,11 @@ const handlePublish = () => {
}; };
const handleView = (row: Publish) => { const handleView = (row: Publish) => {
ElMessage.info("查看发布详情功能开发中"); ElMessage.info("查看模块详情功能开发中");
}; };
const handleDelete = (row: Publish) => { const handleDelete = (row: Publish) => {
ElMessageBox.confirm(`确定要删除发布"${row.title}"吗?`, "提示", { ElMessageBox.confirm(`确定要删除模块"${row.title}"吗?`, "提示", {
confirmButtonText: "确定", confirmButtonText: "确定",
cancelButtonText: "取消", cancelButtonText: "取消",
type: "warning" type: "warning"
@ -355,18 +322,18 @@ const handleSubmit = async () => {
await formRef.value.validate(async (valid) => { await formRef.value.validate(async (valid) => {
if (valid) { if (valid) {
if (!formData.packageFile) { if (!formData.packageFile) {
ElMessage.warning("请上传发布包"); ElMessage.warning("请上传模块包");
return; return;
} }
submitLoading.value = true; submitLoading.value = true;
try { try {
// TODO: // TODO:
const formDataObj = new FormData(); const formDataObj = new FormData();
formDataObj.append("title", formData.title); formDataObj.append("title", formData.title);
formDataObj.append("version", formData.version); formDataObj.append("version", formData.version);
formDataObj.append("author", formData.author);
formDataObj.append("module_id", formData.module_id.toString()); formDataObj.append("module_id", formData.module_id.toString());
formDataObj.append("type", formData.type.toString());
formDataObj.append("status", formData.status.toString()); formDataObj.append("status", formData.status.toString());
formDataObj.append("description", formData.description); formDataObj.append("description", formData.description);
formDataObj.append("package", formData.packageFile); formDataObj.append("package", formData.packageFile);
@ -391,45 +358,21 @@ const handleRefresh = () => {
const fetchPublishList = async () => { const fetchPublishList = async () => {
loading.value = true; loading.value = true;
try { try {
// TODO: const res = await getModules(0);
// const res = await getPublishList(); if (res.code === 200 && res.data) {
// if (res.code === 200) { publishList.value = res.data.list.map((item: any) => ({
// publishList.value = res.data || []; id: item.id,
// } title: item.title,
version: item.version || "-",
// author: item.author || "-",
publishList.value = [ module_name: item.cid || "",
{ status: item.status,
id: 1, publish_time: item.create_time || ""
title: "CMS内容管理 v1.0.0", }));
version: "1.0.0", }
module_name: "CMS内容管理",
type: 2,
status: 1,
publish_time: "2026-01-29 10:00:00"
},
{
id: 2,
title: "用户管理 v1.0.1",
version: "1.0.1",
module_name: "用户管理",
type: 1,
status: 1,
publish_time: "2026-01-28 10:00:00"
},
{
id: 3,
title: "数据分析 v1.0.0",
version: "1.0.0",
module_name: "数据分析",
type: 3,
status: 0,
publish_time: "2026-01-27 10:00:00"
}
];
} catch (error) { } catch (error) {
console.error("获取发布列表失败:", error); console.error("获取模块列表失败:", error);
ElMessage.error("获取发布列表失败"); ElMessage.error("获取模块列表失败");
} finally { } finally {
loading.value = false; loading.value = false;
} }
@ -437,15 +380,15 @@ const fetchPublishList = async () => {
const fetchModuleOptions = async () => { const fetchModuleOptions = async () => {
try { try {
// TODO: const res = await getModuleCategory();
moduleOptions.value = [ if (res.code === 200 && res.data) {
{ id: 1, name: "CMS内容管理" }, moduleOptions.value = res.data.list.map((item: any) => ({
{ id: 2, name: "用户管理" }, id: item.id,
{ id: 3, name: "数据分析" }, name: item.title
{ id: 4, name: "系统设置" } }));
]; }
} catch (error) { } catch (error) {
console.error("获取模块列表失败:", error); console.error("获取分类列表失败:", error);
} }
}; };