Add project introduction module to admin layout and router

- Add new sidebar menu item for project introduction in AdminLayout
- Create new route for projects management in router configuration
- Update formatDateTime utility function with improved error handling and default return value
This commit is contained in:
wzclm 2025-02-23 15:59:17 +08:00
parent 739aca7edb
commit 777532f396
5 changed files with 627 additions and 13 deletions

56
src/api/projects.js Normal file
View File

@ -0,0 +1,56 @@
import request from '@/utils/request'
/**
* 获取项目简介列表
* @param {Object} params - 查询参数
* @returns {Promise}
*/
export function getProjectList(params) {
return request.get('/api/admin/projects', { params })
}
/**
* 获取项目简介详情
* @param {number|string} id - 项目ID
* @returns {Promise}
*/
export function getProjectDetail(id) {
return request.get(`/api/admin/projects/${id}`)
}
/**
* 创建项目简介
* @param {Object} data - 项目数据
* @returns {Promise}
*/
export function createProject(data) {
return request.post('/api/admin/projects', data)
}
/**
* 更新项目简介
* @param {number|string} id - 项目ID
* @param {Object} data - 更新数据
* @returns {Promise}
*/
export function updateProject(id, data) {
return request.put(`/api/admin/projects/${id}`, data)
}
/**
* 删除项目简介
* @param {number|string} id - 项目ID
* @returns {Promise}
*/
export function deleteProject(id) {
return request.delete(`/api/admin/projects/${id}`)
}
/**
* 批量更新排序
* @param {Object} data - 排序数据
* @returns {Promise}
*/
export function updateProjectSort(data) {
return request.post('/api/admin/projects/sort/batch', data)
}

View File

@ -167,6 +167,14 @@ const handleLogout = () => {
<el-menu-item index="/feedback/suggestions">意见反馈</el-menu-item>
<el-menu-item index="/feedback/satisfaction">满意度调查</el-menu-item>
</el-sub-menu>
<el-sub-menu index="projects">
<template #title>
<el-icon><component :is="icons.DataLine" /></el-icon>
<span>项目简介</span>
</template>
<el-menu-item index="/projects">项目简介</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>

View File

@ -161,6 +161,12 @@ const router = createRouter({
name: 'SystemData',
component: () => import('@/views/system/data/index.vue'),
meta: { title: '数据管理', icon: 'data' }
},
{
path: 'projects',
name: 'Projects',
component: () => import('@/views/projects/index.vue'),
meta: { title: '项目简介管理', icon: 'data' }
}
]
}

View File

@ -1,22 +1,25 @@
/**
* 格式化日期时间
* @param {string|number|Date} time 需要格式化的时间
* @param {string} [format='YYYY-MM-DD HH:mm:ss'] 格式化的格式
* @param {string|number|Date} time 时间
* @param {string} format 格式默认为 'YYYY-MM-DD HH:mm:ss'
* @returns {string} 格式化后的时间字符串
*/
export function formatDateTime(time, format = 'YYYY-MM-DD HH:mm:ss') {
if (!time) return '';
if (!time) {
return '-'
}
const date = new Date(time);
const date = new Date(time)
if (isNaN(date.getTime())) {
return '-'
}
if (isNaN(date.getTime())) return '';
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return format
.replace('YYYY', year)
@ -24,7 +27,7 @@ export function formatDateTime(time, format = 'YYYY-MM-DD HH:mm:ss') {
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds);
.replace('ss', seconds)
}
/**

View File

@ -0,0 +1,541 @@
<script setup>
import { ref, onMounted } from 'vue'
import { Search, Plus, Edit, Delete } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getProjectList, deleteProject, updateProjectSort, createProject, updateProject } from '@/api/projects'
import { formatDateTime } from '@/utils/format'
import { sortArrayByField } from '@/utils/sort'
//
const tableData = ref([])
const originalData = ref([]) //
const loading = ref(false)
//
const pagination = ref({
page: 1,
limit: 10,
total: 0
})
//
const searchForm = ref({
title: '',
type: ''
})
//
const typeOptions = [
{ label: '关于我们', value: 'about' },
{ label: '团队介绍', value: 'team' },
{ label: '环境介绍', value: 'environment' }
]
//
const columns = [
{ prop: 'title', label: '标题', minWidth: 180 },
{ prop: 'content', label: '内容', minWidth: 300 },
{ prop: 'type', label: '类型', width: 120 },
{ prop: 'sort_order', label: '排序', width: 80 },
{ prop: 'status', label: '状态', width: 80 },
{ prop: 'created_at', label: '创建时间', width: 180 },
{ prop: 'creator_name', label: '创建人', width: 120 },
{ prop: 'updated_at', label: '更新时间', width: 180 },
{ prop: 'updater_name', label: '更新人', width: 120 },
{ prop: 'operation', label: '操作', width: 150, fixed: 'right' }
]
//
const dialogVisible = ref(false)
const dialogTitle = ref('新增项目简介')
const formRef = ref(null)
const formData = ref({
title: '',
content: '',
cover_image: '',
type: '',
sort_order: 0,
status: 1
})
//
const rules = {
title: [
{ required: true, message: '请输入标题', trigger: 'blur' },
{ min: 2, max: 200, message: '长度在 2 到 200 个字符', trigger: 'blur' }
],
type: [
{ required: true, message: '请选择类型', trigger: 'change' }
],
content: [
{ required: true, message: '请输入内容', trigger: 'blur' }
],
sort_order: [
{ required: true, message: '请输入排序', trigger: 'blur' },
{ type: 'number', message: '排序必须为数字', trigger: 'blur' }
]
}
//
const getList = async () => {
try {
loading.value = true
const params = {
page: pagination.value.page,
page_size: pagination.value.limit
}
const res = await getProjectList(params)
if (res.success) {
const sortedData = sortArrayByField(res.data.list, 'created_at', 'asc')
originalData.value = sortedData //
//
tableData.value = filterData(sortedData)
pagination.value.total = res.data.pagination.total
pagination.value.page = res.data.pagination.page
pagination.value.limit = res.data.pagination.page_size
} else {
ElMessage.error(res.message || '获取列表失败')
}
} catch (error) {
console.error('获取列表失败:', error)
ElMessage.error('获取列表失败')
} finally {
loading.value = false
}
}
//
const filterData = (data) => {
return data.filter(item => {
const titleMatch = !searchForm.value.title ||
item.title.toLowerCase().includes(searchForm.value.title.toLowerCase())
const typeMatch = !searchForm.value.type ||
item.type === searchForm.value.type
return titleMatch && typeMatch
})
}
//
const handleSearch = () => {
tableData.value = filterData(originalData.value)
}
//
const handleReset = () => {
searchForm.value = {
title: '',
type: ''
}
tableData.value = originalData.value
}
//
const handleSizeChange = (val) => {
pagination.value.limit = val
getList()
}
const handleCurrentChange = (val) => {
pagination.value.page = val
getList()
}
//
const handleAdd = () => {
dialogTitle.value = '新增项目简介'
formData.value = {
title: '',
content: '',
cover_image: '',
type: '',
sort_order: 0,
status: 1
}
dialogVisible.value = true
}
//
const handleEdit = (row) => {
dialogTitle.value = '编辑项目简介'
formData.value = { ...row }
dialogVisible.value = true
}
//
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确定要删除该项目简介吗?', '提示', {
type: 'warning'
})
await deleteProject(row.id)
ElMessage.success('删除成功')
getList()
} catch (error) {
if (error !== 'cancel') {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
}
}
//
const handleSortChange = async (row) => {
try {
await updateProjectSort({
id: row.id,
sort_order: row.sort_order
})
ElMessage.success('更新排序成功')
getList()
} catch (error) {
console.error('更新排序失败:', error)
ElMessage.error('更新排序失败')
}
}
//
const handleUploadSuccess = (res) => {
formData.value.cover_image = res.url
ElMessage.success('上传成功')
}
//
const handleUploadError = () => {
ElMessage.error('上传失败')
}
//
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (formData.value.id) {
await updateProject(formData.value.id, formData.value)
ElMessage.success('更新成功')
} else {
await createProject(formData.value)
ElMessage.success('创建成功')
}
dialogVisible.value = false
getList()
} catch (error) {
console.error('提交失败:', error)
ElMessage.error('提交失败')
}
}
//
const handleDialogClose = () => {
formRef.value?.resetFields()
dialogVisible.value = false
}
//
onMounted(() => {
getList()
})
</script>
<template>
<div class="project-introduction">
<!-- 搜索区域 -->
<div class="search-container">
<el-form :model="searchForm" inline>
<el-form-item label="标题">
<el-input
v-model="searchForm.title"
placeholder="请输入标题"
clearable
style="width: 240px"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="类型">
<el-select
v-model="searchForm.type"
placeholder="请选择类型"
clearable
style="width: 240px"
>
<el-option
v-for="item in typeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 操作按钮区域 -->
<div class="operation-container">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增</el-button>
</div>
<!-- 表格区域 -->
<div class="table-container">
<el-table
:data="tableData"
border
style="width: 100%"
v-loading="loading"
>
<el-table-column
type="index"
label="序号"
width="60"
align="center"
/>
<el-table-column
v-for="col in columns"
:key="col.prop"
:prop="col.prop"
:label="col.label"
:min-width="col.minWidth"
:width="col.width"
:fixed="col.fixed"
align="center"
>
<template #default="scope">
<template v-if="col.prop === 'type'">
<el-tag
:type="scope.row.type === 'about' ? 'info' : scope.row.type === 'team' ? 'success' : 'warning'"
>
{{ typeOptions.find(item => item.value === scope.row.type)?.label }}
</el-tag>
</template>
<template v-else-if="col.prop === 'status'">
<el-tag
:type="scope.row.status === 1 ? 'success' : 'danger'"
effect="plain"
>
{{ scope.row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
<template v-else-if="col.prop === 'content'">
<el-tooltip
:content="scope.row.content"
placement="top"
:hide-after="0"
>
<div class="content-cell">{{ scope.row.content }}</div>
</el-tooltip>
</template>
<template v-else-if="col.prop === 'sort_order'">
{{ scope.row.sort_order }}
</template>
<template v-else-if="col.prop === 'created_at' || col.prop === 'updated_at'">
{{ formatDateTime(scope.row[col.prop]) }}
</template>
<template v-else-if="col.prop === 'operation'">
<el-button
type="primary"
:icon="Edit"
link
@click="handleEdit(scope.row)"
>
编辑
</el-button>
<el-button
type="danger"
:icon="Delete"
link
@click="handleDelete(scope.row)"
>
删除
</el-button>
</template>
<template v-else>
{{ scope.row[col.prop] }}
</template>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-if="pagination.total > pagination.limit"
v-model:current-page="pagination.page"
v-model:page-size="pagination.limit"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
<!-- 表单弹窗 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="700px"
@close="handleDialogClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="80px"
>
<el-form-item label="标题" prop="title">
<el-input
v-model="formData.title"
placeholder="请输入标题"
maxlength="200"
show-word-limit
/>
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select
v-model="formData.type"
placeholder="请选择类型"
style="width: 100%"
>
<el-option
v-for="item in typeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="封面图片" prop="cover_image">
<el-upload
class="avatar-uploader"
action="/api/admin/upload"
:show-file-list="false"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
accept="image/*"
>
<img
v-if="formData.cover_image"
:src="formData.cover_image"
class="avatar"
>
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
</el-form-item>
<el-form-item label="内容" prop="content">
<el-input
v-model="formData.content"
type="textarea"
:rows="6"
placeholder="请输入内容"
/>
</el-form-item>
<el-form-item label="排序" prop="sort_order">
<el-input-number
v-model="formData.sort_order"
:min="0"
:max="999"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch
v-model="formData.status"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleDialogClose">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
.project-introduction {
padding: 20px;
background-color: #fff;
border-radius: 4px;
.search-container {
margin-bottom: 20px;
padding: 20px;
background-color: #f5f7fa;
border-radius: 4px;
}
.operation-container {
margin-bottom: 20px;
display: flex;
justify-content: flex-end;
}
.table-container {
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
}
}
.avatar-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);
}
:deep(.el-upload:hover) {
border-color: var(--el-color-primary);
}
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
line-height: 178px;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
.content-cell {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 300px;
}
</style>