feat(admin): 新增首页导航管理页面

- 新增 navigation/index.vue 管理页面(CRUD + 状态切换)
- content.ts 新增 Navigation API 类型定义和接口
- business.ts 路由新增首页导航管理菜单
- 数据库 menus 表插入菜单记录并关联管理员角色
This commit is contained in:
zpc 2026-02-23 12:59:01 +08:00
parent 7154d7eb01
commit a5f8deb6e2
3 changed files with 660 additions and 0 deletions

View File

@ -283,3 +283,118 @@ export function updatePromotionStatus(data: UpdateStatusRequest): Promise<ApiRes
data
})
}
// ==================== HomeNavigation 类型定义 ====================
/**
*
*/
export interface NavigationItem {
/** 导航ID */
id: number
/** 导航名称 */
name: string
/** 图标图片URL */
imageUrl: string
/** 跳转链接URL */
linkUrl: string
/** 排序值 */
sort: number
/** 状态 (0: 即将上线, 1: 已上线) */
status: number
/** 状态名称 */
statusName: string
/** 创建时间 */
createTime: string
}
/**
*
*/
export interface NavigationQuery extends PagedRequest {
/** 名称(模糊搜索) */
name?: string
/** 状态筛选 */
status?: number
}
/**
*
*/
export interface CreateNavigationRequest {
/** 导航名称 */
name: string
/** 图标图片URL */
imageUrl?: string
/** 跳转链接URL */
linkUrl?: string
/** 排序值 */
sort: number
/** 状态 */
status: number
}
/**
*
*/
export interface UpdateNavigationRequest extends CreateNavigationRequest {
/** 导航ID */
id: number
}
// ==================== HomeNavigation API ====================
/**
*
*/
export function getNavigationList(params: NavigationQuery): Promise<ApiResponse<PagedResult<NavigationItem>>> {
return request<PagedResult<NavigationItem>>({
url: '/admin/content/navigation/getList',
method: 'get',
params
})
}
/**
*
*/
export function createNavigation(data: CreateNavigationRequest): Promise<ApiResponse<number>> {
return request<number>({
url: '/admin/content/navigation/create',
method: 'post',
data
})
}
/**
*
*/
export function updateNavigation(data: UpdateNavigationRequest): Promise<ApiResponse<boolean>> {
return request<boolean>({
url: '/admin/content/navigation/update',
method: 'post',
data
})
}
/**
*
*/
export function deleteNavigation(id: number): Promise<ApiResponse<boolean>> {
return request<boolean>({
url: '/admin/content/navigation/delete',
method: 'post',
data: { id }
})
}
/**
*
*/
export function updateNavigationStatus(data: UpdateStatusRequest): Promise<ApiResponse<boolean>> {
return request<boolean>({
url: '/admin/content/navigation/updateStatus',
method: 'post',
data
})
}

View File

@ -62,6 +62,12 @@ export const businessRoutes: RouteRecordRaw[] = [
component: () => import('@/views/business/content/banner/index.vue'),
meta: { title: '轮播图管理', permission: 'banner:view', keepAlive: true }
},
{
path: 'navigation',
name: 'Navigation',
component: () => import('@/views/business/content/navigation/index.vue'),
meta: { title: '首页导航管理', permission: 'content:view', keepAlive: true }
},
{
path: 'promotion',
name: 'Promotion',

View File

@ -0,0 +1,539 @@
<template>
<div class="navigation-container">
<!-- 页面标题和操作栏 -->
<el-card class="page-header">
<div class="header-content">
<div class="header-left">
<h2 class="page-title">首页导航管理</h2>
<span class="page-description">管理小程序首页导航入口卡片支持图标上传跳转配置和状态管理</span>
</div>
<div class="header-right">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增导航
</el-button>
</div>
</div>
</el-card>
<!-- 搜索表单 -->
<el-card class="search-card">
<el-form :model="queryParams" inline>
<el-form-item label="名称">
<el-input
v-model="queryParams.name"
placeholder="请输入导航名称"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option label="已上线" :value="1" />
<el-option label="即将上线" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 数据表格 -->
<el-card v-loading="state.loading" class="table-card">
<el-table :data="state.tableData" row-key="id" stripe>
<!-- 图标预览 -->
<el-table-column label="图标" width="100" align="center">
<template #default="{ row }">
<el-image
v-if="row.imageUrl"
:src="row.imageUrl"
:preview-src-list="[row.imageUrl]"
fit="cover"
style="width: 50px; height: 50px; border-radius: 8px;"
preview-teleported
>
<template #error>
<div class="image-error">
<el-icon><Picture /></el-icon>
</div>
</template>
</el-image>
<span v-else class="text-placeholder">无图标</span>
</template>
</el-table-column>
<!-- 名称 -->
<el-table-column prop="name" label="名称" min-width="120" show-overflow-tooltip />
<!-- 跳转链接 -->
<el-table-column prop="linkUrl" label="跳转链接" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
{{ row.linkUrl || '-' }}
</template>
</el-table-column>
<!-- 状态 -->
<el-table-column label="状态" width="120" align="center">
<template #default="{ row }">
<el-switch
v-model="row.status"
:active-value="1"
:inactive-value="0"
active-text="已上线"
inactive-text="即将上线"
:loading="row._statusLoading"
@change="(val: number) => handleStatusChange(row, val)"
/>
</template>
</el-table-column>
<!-- 排序 -->
<el-table-column prop="sort" label="排序" width="80" align="center" />
<!-- 创建时间 -->
<el-table-column prop="createTime" label="创建时间" width="180" align="center" />
<!-- 操作 -->
<el-table-column label="操作" width="150" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="handleEdit(row)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-popconfirm
title="确定要删除这条导航吗?"
confirm-button-text="确定"
cancel-button-text="取消"
@confirm="handleDelete(row)"
>
<template #reference>
<el-button type="danger" link size="small">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50]"
:total="state.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="state.dialogVisible"
:title="state.dialogTitle"
width="550px"
:close-on-click-modal="false"
@closed="handleDialogClosed"
>
<el-form
ref="formRef"
:model="state.formData"
:rules="formRules"
label-width="100px"
label-position="right"
>
<el-form-item label="导航名称" prop="name">
<el-input
v-model="state.formData.name"
placeholder="请输入导航名称"
maxlength="50"
show-word-limit
/>
</el-form-item>
<el-form-item label="图标图片" prop="imageUrl">
<ImageUpload
v-model="state.formData.imageUrl"
placeholder="点击上传图标"
tip="建议尺寸200x200支持 jpg、png 格式"
:max-size="5"
/>
</el-form-item>
<el-form-item label="跳转链接" prop="linkUrl">
<el-input
v-model="state.formData.linkUrl"
placeholder="请输入跳转链接,如:/pages/assessment/info/index"
clearable
/>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number
v-model="state.formData.sort"
:min="0"
:max="9999"
placeholder="数值越大越靠前"
/>
</el-form-item>
<el-form-item label="状态" prop="status" required>
<el-select v-model="state.formData.status" placeholder="请选择状态">
<el-option label="已上线" :value="1" />
<el-option label="即将上线" :value="0" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="state.dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="state.formLoading" @click="handleSubmit">
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
/**
* 首页导航管理页面
* @description 管理小程序首页导航入口卡片支持图标上传跳转配置和状态管理
*/
import { reactive, ref, onMounted } from 'vue'
import { Plus, Search, Refresh, Edit, Delete, Picture } from '@element-plus/icons-vue'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import {
getNavigationList,
createNavigation,
updateNavigation,
deleteNavigation,
updateNavigationStatus,
type NavigationItem,
type NavigationQuery,
type CreateNavigationRequest,
type UpdateNavigationRequest
} from '@/api/business/content'
import { ImageUpload } from '@/components'
// ============ Types ============
interface NavigationFormData {
id?: number
name: string
imageUrl: string
linkUrl: string
sort: number
status: number
}
interface PageState {
loading: boolean
tableData: (NavigationItem & { _statusLoading?: boolean })[]
total: number
dialogVisible: boolean
dialogTitle: string
formData: NavigationFormData
formLoading: boolean
isEdit: boolean
}
// ============ Refs ============
const formRef = ref<FormInstance>()
// ============ State ============
const queryParams = reactive<NavigationQuery>({
page: 1,
pageSize: 10,
name: '',
status: undefined
})
const state = reactive<PageState>({
loading: false,
tableData: [],
total: 0,
dialogVisible: false,
dialogTitle: '新增导航',
formData: getDefaultFormData(),
formLoading: false,
isEdit: false
})
// ============ Form Rules ============
const formRules: FormRules = {
name: [
{ required: true, message: '请输入导航名称', trigger: 'blur' }
],
status: [
{ required: true, message: '请选择状态', trigger: 'change' }
]
}
// ============ Helper Functions ============
function getDefaultFormData(): NavigationFormData {
return {
name: '',
imageUrl: '',
linkUrl: '',
sort: 0,
status: 1
}
}
// ============ API Functions ============
async function loadList() {
state.loading = true
try {
const params: NavigationQuery = {
page: queryParams.page,
pageSize: queryParams.pageSize
}
if (queryParams.name) params.name = queryParams.name
if (queryParams.status !== undefined && queryParams.status !== '') {
params.status = Number(queryParams.status)
}
const res = await getNavigationList(params)
if (res.code === 0) {
state.tableData = res.data?.list || []
state.total = res.data?.total || 0
} else {
throw new Error(res.message || '获取导航列表失败')
}
} catch (error) {
const message = error instanceof Error ? error.message : '获取导航列表失败'
ElMessage.error(message)
} finally {
state.loading = false
}
}
// ============ Event Handlers ============
function handleSearch() {
queryParams.page = 1
loadList()
}
function handleReset() {
queryParams.name = ''
queryParams.status = undefined
queryParams.page = 1
loadList()
}
function handleSizeChange(size: number) {
queryParams.pageSize = size
queryParams.page = 1
loadList()
}
function handleCurrentChange(page: number) {
queryParams.page = page
loadList()
}
function handleAdd() {
state.isEdit = false
state.dialogTitle = '新增导航'
state.formData = getDefaultFormData()
state.dialogVisible = true
}
function handleEdit(row: NavigationItem) {
state.isEdit = true
state.dialogTitle = '编辑导航'
state.formData = {
id: row.id,
name: row.name,
imageUrl: row.imageUrl || '',
linkUrl: row.linkUrl || '',
sort: row.sort,
status: row.status
}
state.dialogVisible = true
}
async function handleStatusChange(row: NavigationItem & { _statusLoading?: boolean }, status: number) {
row._statusLoading = true
try {
const res = await updateNavigationStatus({ id: row.id, status })
if (res.code === 0) {
ElMessage.success(status === 1 ? '已上线' : '已设为即将上线')
} else {
row.status = status === 1 ? 0 : 1
throw new Error(res.message || '状态更新失败')
}
} catch (error) {
const message = error instanceof Error ? error.message : '状态更新失败'
ElMessage.error(message)
} finally {
row._statusLoading = false
}
}
async function handleDelete(row: NavigationItem) {
try {
const res = await deleteNavigation(row.id)
if (res.code === 0) {
ElMessage.success('删除成功')
if (state.tableData.length === 1 && queryParams.page > 1) {
queryParams.page--
}
await loadList()
} else {
throw new Error(res.message || '删除失败')
}
} catch (error) {
const message = error instanceof Error ? error.message : '删除失败'
ElMessage.error(message)
}
}
async function handleSubmit() {
if (!formRef.value) return
try {
await formRef.value.validate()
} catch {
return
}
state.formLoading = true
try {
const formData = state.formData
const requestData: CreateNavigationRequest | UpdateNavigationRequest = {
name: formData.name,
imageUrl: formData.imageUrl || undefined,
linkUrl: formData.linkUrl || undefined,
sort: formData.sort,
status: formData.status
}
let res
if (state.isEdit && formData.id) {
res = await updateNavigation({ ...requestData, id: formData.id } as UpdateNavigationRequest)
} else {
res = await createNavigation(requestData)
}
if (res.code === 0) {
ElMessage.success(state.isEdit ? '更新成功' : '创建成功')
state.dialogVisible = false
await loadList()
} else {
throw new Error(res.message || (state.isEdit ? '更新失败' : '创建失败'))
}
} catch (error) {
const message = error instanceof Error ? error.message : (state.isEdit ? '更新失败' : '创建失败')
ElMessage.error(message)
} finally {
state.formLoading = false
}
}
function handleDialogClosed() {
formRef.value?.resetFields()
state.formData = getDefaultFormData()
}
// ============ Lifecycle ============
onMounted(() => {
loadList()
})
</script>
<style scoped>
.navigation-container {
padding: 0;
}
.page-header {
margin-bottom: 16px;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left {
display: flex;
align-items: baseline;
gap: 16px;
}
.page-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--text-primary, #303133);
}
.page-description {
font-size: 14px;
color: var(--text-secondary, #909399);
}
.search-card {
margin-bottom: 16px;
}
.search-card :deep(.el-card__body) {
padding-bottom: 2px;
}
.table-card {
min-height: 400px;
}
.image-error {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: var(--el-fill-color-light);
color: var(--el-text-color-placeholder);
}
.text-placeholder {
color: var(--el-text-color-placeholder);
font-size: 12px;
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
:deep(.el-table th.el-table__cell) {
background-color: var(--bg-light, #f5f7fa);
font-weight: 500;
}
:deep(.el-dialog__body) {
padding-top: 20px;
}
</style>