- 将系统管理中的用户、角色和权限管理移动到新的 auth 目录 - 更新路由配置,将 system 和 auth 路由分离 - 调整权限管理相关组件的导入和路由配置 - 优化路由和组件的命名和结构 - 移除系统管理中的用户相关管理功能
663 lines
17 KiB
Vue
663 lines
17 KiB
Vue
<script setup>
|
||
import { ref, onMounted, computed, nextTick } 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'
|
||
import request from '@/utils/request'
|
||
|
||
// 定义表格数据结构
|
||
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,
|
||
imageInputType: 'upload' // 默认选择上传图片
|
||
})
|
||
|
||
// 表单校验规则
|
||
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' }
|
||
]
|
||
}
|
||
|
||
// 添加计算属性用于处理图片URL
|
||
const imageUrl = computed(() => {
|
||
const url = formData.value.cover_image
|
||
if (!url) return ''
|
||
if (url.startsWith('data:')) return url
|
||
if (url.startsWith('http')) return url
|
||
// 使用后端服务器URL
|
||
return `http://localhost:3000${url}`
|
||
})
|
||
|
||
// 获取上传请求头
|
||
const uploadHeaders = computed(() => {
|
||
const token = localStorage.getItem('token')
|
||
return {
|
||
Authorization: token ? `Bearer ${token}` : ''
|
||
}
|
||
})
|
||
|
||
// 获取列表数据
|
||
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,
|
||
imageInputType: 'upload' // 默认选择上传图片
|
||
}
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
// 处理编辑
|
||
const handleEdit = (row) => {
|
||
dialogTitle.value = '编辑简介'
|
||
formData.value = {
|
||
...row,
|
||
imageInputType: row.cover_image?.startsWith('http') ? 'url' : 'upload' // 根据图片地址类型设置默认值
|
||
}
|
||
// 手动触发表单验证
|
||
nextTick(() => {
|
||
formRef.value?.validateField('cover_image')
|
||
})
|
||
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 handleImageTypeChange = () => {
|
||
formData.value.cover_image = ''
|
||
}
|
||
|
||
// 处理上传成功
|
||
const handleUploadSuccess = (response) => {
|
||
console.log('上传响应:', response)
|
||
try {
|
||
if (response.success) {
|
||
const imageUrl = response.data?.url
|
||
if (!imageUrl) {
|
||
ElMessage.error('上传成功但未获取到图片地址')
|
||
return
|
||
}
|
||
// 直接使用完整的URL
|
||
formData.value.cover_image = imageUrl
|
||
ElMessage.success('图片上传成功')
|
||
} else {
|
||
ElMessage.error(response.message || '图片上传失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('处理上传响应错误:', error)
|
||
ElMessage.error('处理上传响应失败')
|
||
}
|
||
}
|
||
|
||
// 处理上传错误
|
||
const handleUploadError = (error) => {
|
||
console.error('上传失败:', error)
|
||
formData.value.cover_image = ''
|
||
}
|
||
|
||
// 处理上传前检查
|
||
const beforeUpload = (file) => {
|
||
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 handleSubmit = async () => {
|
||
if (!formRef.value) return
|
||
|
||
try {
|
||
await formRef.value.validate()
|
||
|
||
// 准备提交的数据
|
||
const submitData = { ...formData.value }
|
||
delete submitData.imageInputType // 删除不需要提交的字段
|
||
|
||
// 确保cover_image字段存在且有值
|
||
if (!submitData.cover_image) {
|
||
ElMessage.error('请先上传封面图片或输入图片地址')
|
||
return
|
||
}
|
||
|
||
if (submitData.id) {
|
||
const res = await updateProject(submitData.id, submitData)
|
||
if (res.success) {
|
||
ElMessage.success('更新成功')
|
||
dialogVisible.value = false
|
||
getList()
|
||
} else {
|
||
ElMessage.error(res.message || '更新失败')
|
||
}
|
||
} else {
|
||
const res = await createProject(submitData)
|
||
if (res.success) {
|
||
ElMessage.success('创建成功')
|
||
dialogVisible.value = false
|
||
getList()
|
||
} else {
|
||
ElMessage.error(res.message || '创建失败')
|
||
}
|
||
}
|
||
} catch (error) {
|
||
if (error.message) {
|
||
ElMessage.error(error.message)
|
||
} else {
|
||
console.error('提交失败:', error)
|
||
ElMessage.error('提交失败,请检查表单数据')
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理弹窗关闭
|
||
const handleDialogClose = () => {
|
||
formRef.value?.resetFields()
|
||
dialogVisible.value = false
|
||
}
|
||
|
||
// 处理图片加载错误
|
||
const handleImageError = (e) => {
|
||
console.error('图片加载失败:', {
|
||
src: e.target.src,
|
||
error: e
|
||
})
|
||
ElMessage.error('图片加载失败,请检查图片地址是否正确')
|
||
formData.value.cover_image = ''
|
||
}
|
||
|
||
// 页面加载时获取数据
|
||
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">
|
||
<div class="image-input-container">
|
||
<el-radio-group v-model="formData.imageInputType" class="mb-4" @change="handleImageTypeChange">
|
||
<el-radio value="url">输入图片地址</el-radio>
|
||
<el-radio value="upload">上传图片</el-radio>
|
||
</el-radio-group>
|
||
|
||
<template v-if="formData.imageInputType === 'url'">
|
||
<el-input
|
||
v-model="formData.cover_image"
|
||
placeholder="请输入图片URL地址"
|
||
/>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<el-upload
|
||
class="avatar-uploader"
|
||
action="http://localhost:3000/api/admin/projects/upload"
|
||
:headers="uploadHeaders"
|
||
:show-file-list="false"
|
||
accept="image/*"
|
||
:on-success="handleUploadSuccess"
|
||
:on-error="handleUploadError"
|
||
:before-upload="beforeUpload"
|
||
name="file"
|
||
>
|
||
<img
|
||
v-if="formData.cover_image"
|
||
:src="imageUrl"
|
||
class="avatar"
|
||
@error="handleImageError"
|
||
crossorigin="anonymous"
|
||
/>
|
||
<el-icon v-else class="avatar-uploader-icon">
|
||
<Plus />
|
||
</el-icon>
|
||
</el-upload>
|
||
<div class="el-upload__tip">
|
||
只能上传 jpg/png/gif 格式图片,且不超过 2MB
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</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;
|
||
}
|
||
}
|
||
}
|
||
|
||
.content-cell {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
max-width: 300px;
|
||
}
|
||
|
||
.image-input-container {
|
||
.mb-4 {
|
||
margin-bottom: 16px;
|
||
display: block;
|
||
}
|
||
|
||
.el-upload__tip {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-top: 8px;
|
||
}
|
||
}
|
||
|
||
.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;
|
||
object-fit: cover;
|
||
}
|
||
</style> |