|
@@ -0,0 +1,1023 @@
|
|
|
+<template>
|
|
|
+ <div class="bottom-main-nav">
|
|
|
+ <!-- 搜索条件区域 -->
|
|
|
+ <el-card class="search-card">
|
|
|
+ <el-form :model="queryParams" ref="queryRef" :inline="true" label-width="100px">
|
|
|
+ <el-form-item label="菜单名称" prop="name">
|
|
|
+ <el-input
|
|
|
+ v-model="queryParams.name"
|
|
|
+ placeholder="请输入菜单名称"
|
|
|
+ clearable
|
|
|
+ style="width: 200px"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="链接类型" prop="jumpType">
|
|
|
+ <el-select v-model="queryParams.jumpType" placeholder="请选择链接类型" clearable style="width: 200px">
|
|
|
+ <el-option label="跳转链接" :value="1" />
|
|
|
+ <el-option label="不跳转" :value="2" />
|
|
|
+ <el-option label="小程序内链" :value="3" />
|
|
|
+ <el-option label="小程序外链" :value="4" />
|
|
|
+ <el-option label="H5外链" :value="5" />
|
|
|
+ <el-option label="公众号文章" :value="6" />
|
|
|
+ <el-option label="公众号" :value="7" />
|
|
|
+ <el-option label="电话拨号" :value="8" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="状态" prop="status">
|
|
|
+ <el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 200px">
|
|
|
+ <el-option label="正常" :value="0" />
|
|
|
+ <el-option label="停用" :value="1" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="创建时间" prop="createTime">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="dateRange"
|
|
|
+ type="daterange"
|
|
|
+ range-separator="-"
|
|
|
+ start-placeholder="开始时间"
|
|
|
+ end-placeholder="结束时间"
|
|
|
+ style="width: 240px"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" icon="Search" @click="handleQuery">Q 搜索</el-button>
|
|
|
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 操作按钮区域 -->
|
|
|
+ <el-card class="action-card">
|
|
|
+ <el-row :gutter="10" class="mb8">
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button type="primary" icon="Plus" @click="handleAdd"> 添加菜单</el-button>
|
|
|
+ </el-col>
|
|
|
+ <!-- <el-col :span="1.5">
|
|
|
+ <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()"
|
|
|
+ >修改
|
|
|
+ </el-button>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()"
|
|
|
+ >删除
|
|
|
+ </el-button>
|
|
|
+ </el-col> -->
|
|
|
+ <right-toolbar v-model:showSearch="showSearch" :columns="columns" @queryTable="loadNavData"></right-toolbar>
|
|
|
+ </el-row>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 菜单列表 -->
|
|
|
+ <el-card class="list-card">
|
|
|
+ <el-table
|
|
|
+ v-loading="loading"
|
|
|
+ :data="navItems"
|
|
|
+ @selection-change="handleSelectionChange"
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-table-column type="selection" width="55" align="center" />
|
|
|
+ <el-table-column label="序号" align="center" width="80" v-if="columns[0].visible">
|
|
|
+ <template #default="scope">
|
|
|
+ <span class="sort-number">{{ scope.row.sortNum || '0' }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="菜单名称" align="center" prop="name" width="150" v-if="columns[1].visible" />
|
|
|
+ <el-table-column label="活动类型" align="center" prop="activityType" width="150" v-if="columns[2].visible">
|
|
|
+ <template #default="scope">
|
|
|
+ <dict-tag :options="game_activity_type" :value="scope.row.activityType" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="图标" align="center" prop="pic" width="150" v-if="columns[3].visible">
|
|
|
+ <template #default="scope">
|
|
|
+ <img :src="scope.row.pic" style="width: 50px; height: 50px" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="颜色" align="center" prop="color" width="150" v-if="columns[4].visible" />
|
|
|
+ <el-table-column label="链接类别" align="center" prop="jumpType" width="150" v-if="columns[5].visible">
|
|
|
+ <template #default="scope">
|
|
|
+ {{ getJumpTypeText(scope.row.jumpType) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="链接" align="center" prop="jumpPath" width="150" v-if="columns[6].visible">
|
|
|
+ <template #default="scope">
|
|
|
+ {{ scope.row.jumpPath || '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column label="创建时间" min-width="200" v-if="columns[7].visible">
|
|
|
+ <template #default="scope">
|
|
|
+ <div class="create-info">
|
|
|
+ <div class="status">
|
|
|
+ <span class="label">使用状态:</span>
|
|
|
+ <el-tag :type="scope.row.status === 0 ? 'success' : 'danger'" size="small">
|
|
|
+ {{ scope.row.status === 0 ? '正常' : '停用' }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="create-time">
|
|
|
+ <span class="label">创建时间:</span>
|
|
|
+ <span class="value">{{ formatDate(scope.row.createTime) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column label="操作" align="center" width="180">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-button type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
|
|
+ <el-dropdown @command="(command) => handleCommand(command, scope.row)">
|
|
|
+ <el-button size="small">
|
|
|
+ 更多<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
|
|
+ </el-button>
|
|
|
+ <template #dropdown>
|
|
|
+ <el-dropdown-menu>
|
|
|
+ <el-dropdown-item command="delete">删除</el-dropdown-item>
|
|
|
+ <el-dropdown-item command="view">查看详情</el-dropdown-item>
|
|
|
+ </el-dropdown-menu>
|
|
|
+ </template>
|
|
|
+ </el-dropdown>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 编辑/添加对话框 -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="dialogVisible"
|
|
|
+ :title="dialogTitle"
|
|
|
+ width="720px"
|
|
|
+ class="nav-dialog"
|
|
|
+ >
|
|
|
+ <el-form
|
|
|
+ ref="formRef"
|
|
|
+ :model="form"
|
|
|
+ :rules="rules"
|
|
|
+ label-width="120px"
|
|
|
+ >
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="菜单名称" prop="name">
|
|
|
+ <el-input v-model="form.name" placeholder="请输入菜单名称" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="排序" prop="sortNum">
|
|
|
+ <el-input-number v-model="form.sortNum" :min="1" :max="999" style="width: 100%" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="18">
|
|
|
+ <el-form-item label="活动类型" prop="activityType">
|
|
|
+ <el-radio-group v-model="form.activityType">
|
|
|
+ <el-radio v-for="dict in game_activity_type" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="功能图标" prop="pic">
|
|
|
+ <el-upload
|
|
|
+ class="icon-uploader"
|
|
|
+ :action="uploadUrl"
|
|
|
+ :headers="headers"
|
|
|
+ :show-file-list="false"
|
|
|
+ :before-upload="beforeIconUpload"
|
|
|
+ :on-success="handleIconSuccess"
|
|
|
+ >
|
|
|
+ <img v-if="form.pic" :src="form.pic" class="icon-preview" />
|
|
|
+ <el-icon v-else class="icon-uploader-icon"><Plus /></el-icon>
|
|
|
+ </el-upload>
|
|
|
+ <div class="upload-tip">建议尺寸 100x100</div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="颜色" required>
|
|
|
+ <div class="color-input-group">
|
|
|
+ <el-color-picker
|
|
|
+ v-model="form.color"
|
|
|
+ show-alpha
|
|
|
+ size="large"
|
|
|
+ class="color-picker"
|
|
|
+ @change="handleColorChange"
|
|
|
+ />
|
|
|
+ <el-input
|
|
|
+ v-model="colorInput"
|
|
|
+ placeholder="#RRGGBB"
|
|
|
+ class="color-input"
|
|
|
+ @input="handleColorInput"
|
|
|
+ @blur="validateColorInput"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+
|
|
|
+ <el-form-item label="跳转类型" prop="jumpType">
|
|
|
+ <el-radio-group v-model="form.jumpType" @change="handleJumpTypeChange">
|
|
|
+ <el-radio :value="1">跳转链接</el-radio>
|
|
|
+ <el-radio :value="2">不跳转</el-radio>
|
|
|
+ <el-radio :value="3">小程序内链</el-radio>
|
|
|
+ <el-radio :value="4">小程序外链</el-radio>
|
|
|
+ <el-radio :value="5">H5外链</el-radio>
|
|
|
+ <el-radio :value="6">公众号文章</el-radio>
|
|
|
+ <el-radio :value="7">公众号</el-radio>
|
|
|
+ <el-radio :value="8">电话拨号</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="跳转链接" prop="jumpPath">
|
|
|
+ <el-input v-model="form.jumpPath" placeholder="请输入跳转链接" :disabled="form.jumpType === 2" />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="小程序AppID" prop="appId" v-if="form.jumpType === 4">
|
|
|
+ <el-input v-model="form.appId" placeholder="请输入对方小程序的AppID" />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="创建时间">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="form.createTime"
|
|
|
+ type="datetime"
|
|
|
+ placeholder="请选择创建时间"
|
|
|
+ style="width: 100%"
|
|
|
+ format="YYYY-MM-DD HH:mm:ss"
|
|
|
+ value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="更新时间">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="form.updateTime"
|
|
|
+ type="datetime"
|
|
|
+ placeholder="请选择更新时间"
|
|
|
+ style="width: 100%"
|
|
|
+ format="YYYY-MM-DD HH:mm:ss"
|
|
|
+ value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-form-item label="状态" prop="status">
|
|
|
+ <el-radio-group v-model="form.status">
|
|
|
+ <el-radio :value="0">正常</el-radio>
|
|
|
+ <el-radio :value="1">停用</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="备注">
|
|
|
+ <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <template #footer>
|
|
|
+ <span class="dialog-footer">
|
|
|
+ <el-button @click="dialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="handleSubmit" :loading="submitting">
|
|
|
+ 确定
|
|
|
+ </el-button>
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 详情对话框 -->
|
|
|
+ <el-dialog title="菜单详情" v-model="detailOpen" width="600px" append-to-body>
|
|
|
+ <div class="detail-content">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">菜单名称:</span>
|
|
|
+ <span class="detail-value">{{ detailData.name || '-' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">排序:</span>
|
|
|
+ <span class="detail-value">{{ detailData.sortNum || '-' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">活动类型:</span>
|
|
|
+ <span class="detail-value">
|
|
|
+ <dict-tag :options="game_activity_type" :value="detailData.activityType" />
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">图标:</span>
|
|
|
+ <div class="detail-image">
|
|
|
+ <img v-if="detailData.pic" :src="detailData.pic" :alt="detailData.pic" />
|
|
|
+ <span v-else class="no-image-text">暂无图标</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">颜色:</span>
|
|
|
+ <span class="detail-value">{{ detailData.color }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">跳转类型:</span>
|
|
|
+ <span class="detail-value">{{ getJumpTypeText(detailData.jumpType) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">跳转链接:</span>
|
|
|
+ <span class="detail-value">{{ detailData.jumpPath || '-' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="detail-item" v-if="detailData.appId">
|
|
|
+ <span class="detail-label">小程序AppID:</span>
|
|
|
+ <span class="detail-value">{{ detailData.appId }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">状态:</span>
|
|
|
+ <el-tag :type="detailData.status === 0 ? 'success' : 'danger'" size="small">
|
|
|
+ {{ detailData.status === 0 ? '正常' : '停用' }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">创建时间:</span>
|
|
|
+ <span class="detail-value">{{ formatDateTime(detailData.createTime) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">更新时间:</span>
|
|
|
+ <span class="detail-value">{{ formatDateTime(detailData.updateTime) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="detail-item" v-if="detailData.remark">
|
|
|
+ <span class="detail-label">备注:</span>
|
|
|
+ <span class="detail-value">{{ detailData.remark }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <template #footer>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button @click="detailOpen = false">关 闭</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, reactive, onMounted, computed } from 'vue'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
+import { HomeFilled, Plus } from '@element-plus/icons-vue'
|
|
|
+import {
|
|
|
+ getEnabledNavigator,
|
|
|
+ getNavigator,
|
|
|
+ addNavigator,
|
|
|
+ updateNavigator,
|
|
|
+ delNavigator,
|
|
|
+ type GameNavigatorVo,
|
|
|
+ type GameNavigatorBo,
|
|
|
+ listNavigator
|
|
|
+} from '@/api/system/common/nav/gameNavigator'
|
|
|
+
|
|
|
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
+const { game_activity_type } = toRefs<any>(proxy?.useDict('game_activity_type'));
|
|
|
+
|
|
|
+// 计算下一个排序值
|
|
|
+const nextSortNum = computed(() => getMaxSortNum() + 1);
|
|
|
+
|
|
|
+const showSearch = ref(true);
|
|
|
+const single = ref(true);
|
|
|
+const multiple = ref(true);
|
|
|
+const total = ref(0);
|
|
|
+
|
|
|
+// 列显隐数据
|
|
|
+const columns = ref<FieldOption[]>([
|
|
|
+ { key: 0, label: '序号', visible: true },
|
|
|
+ { key: 1, label: '菜单名称', visible: true },
|
|
|
+ { key: 2, label: '活动类型', visible: true },
|
|
|
+ { key: 3, label: '图标', visible: true },
|
|
|
+ { key: 4, label: '颜色', visible: true },
|
|
|
+ { key: 5, label: '链接类别', visible: true },
|
|
|
+ { key: 6, label: '链接', visible: true },
|
|
|
+ { key: 7, label: '创建时间', visible: true },
|
|
|
+]);
|
|
|
+
|
|
|
+// 菜单项接口
|
|
|
+interface NavItem extends GameNavigatorVo {
|
|
|
+ navId?: number;
|
|
|
+ name?: string;
|
|
|
+ pic?: string;
|
|
|
+ color?: string;
|
|
|
+ jumpType?: number;
|
|
|
+ jumpPath?: string;
|
|
|
+ activityType?: number;
|
|
|
+ sortNum?: number;
|
|
|
+ status?: number;
|
|
|
+ remark?: string;
|
|
|
+ createTime?: string;
|
|
|
+ updateTime?: string;
|
|
|
+ appId?: string;
|
|
|
+}
|
|
|
+
|
|
|
+// 表单数据
|
|
|
+const navItems = ref<NavItem[]>([]);
|
|
|
+const formRef = ref()
|
|
|
+const form = reactive<GameNavigatorBo & {
|
|
|
+ createTime?: string;
|
|
|
+ updateTime?: string;
|
|
|
+}>({
|
|
|
+ navId: undefined,
|
|
|
+ name: '',
|
|
|
+ pic: '',
|
|
|
+ color: '#409EFF',
|
|
|
+ jumpType: 1,
|
|
|
+ jumpPath: '',
|
|
|
+ activityType: 1,
|
|
|
+ appId: '',
|
|
|
+ sortNum: 1, // 初始值,会在使用时动态更新
|
|
|
+ status: 0,
|
|
|
+ remark: '',
|
|
|
+ createTime: '',
|
|
|
+ updateTime: '',
|
|
|
+});
|
|
|
+
|
|
|
+// 响应式数据
|
|
|
+const loading = ref(false);
|
|
|
+const dialogVisible = ref(false);
|
|
|
+const detailOpen = ref(false);
|
|
|
+const dialogTitle = ref('');
|
|
|
+const submitting = ref(false);
|
|
|
+const selectedIds = ref<number[]>([]);
|
|
|
+const dateRange = ref<[Date, Date] | null>(null);
|
|
|
+const detailData = ref<NavItem>({} as NavItem);
|
|
|
+
|
|
|
+// 查询参数
|
|
|
+const queryParams = reactive({
|
|
|
+ name: '',
|
|
|
+ jumpType: undefined as number | undefined,
|
|
|
+ status: undefined as number | undefined,
|
|
|
+ createTime: ''
|
|
|
+});
|
|
|
+
|
|
|
+// 表单验证规则
|
|
|
+const rules = {
|
|
|
+ name: [
|
|
|
+ { required: true, message: '请输入菜单名称', trigger: 'blur' }
|
|
|
+ ],
|
|
|
+ pic: [
|
|
|
+ { required: true, message: '请上传功能图标', trigger: 'change' }
|
|
|
+ ],
|
|
|
+ activityType: [
|
|
|
+ { required: true, message: '请选择活动类型', trigger: 'change' }
|
|
|
+ ],
|
|
|
+ jumpType: [
|
|
|
+ { required: true, message: '请选择跳转方式', trigger: 'change' }
|
|
|
+ ],
|
|
|
+ sortNum: [
|
|
|
+ { required: true, message: '请输入排序', trigger: 'blur' }
|
|
|
+ ]
|
|
|
+}
|
|
|
+
|
|
|
+// 添加跳转链接验证函数
|
|
|
+function validateJumpPath(rule: any, value: string, callback: Function) {
|
|
|
+ if (value && !/^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/.test(value)) {
|
|
|
+ callback(new Error('请输入有效的URL'))
|
|
|
+ } else {
|
|
|
+ callback()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 上传相关
|
|
|
+import { globalHeaders } from '@/utils/request';
|
|
|
+
|
|
|
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload'
|
|
|
+const headers = globalHeaders()
|
|
|
+
|
|
|
+// 格式化日期
|
|
|
+function formatDate(date: string | Date | null | undefined): string {
|
|
|
+ if (!date) return '-'
|
|
|
+ const d = new Date(date)
|
|
|
+ if (isNaN(d.getTime())) return '-'
|
|
|
+ return d.toLocaleDateString('zh-CN')
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化日期时间
|
|
|
+function formatDateTime(date: string | Date | null | undefined): string {
|
|
|
+ if (!date) return '-'
|
|
|
+ const d = new Date(date)
|
|
|
+ if (isNaN(d.getTime())) return '-'
|
|
|
+ return d.toLocaleString('zh-CN', {
|
|
|
+ year: 'numeric',
|
|
|
+ month: '2-digit',
|
|
|
+ day: '2-digit',
|
|
|
+ hour: '2-digit',
|
|
|
+ minute: '2-digit',
|
|
|
+ second: '2-digit'
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 获取跳转类型文本
|
|
|
+function getJumpTypeText(jumpType: number | null | undefined): string {
|
|
|
+ if (!jumpType) return '-'
|
|
|
+ const typeMap: Record<number, string> = {
|
|
|
+ 1: '跳转链接',
|
|
|
+ 2: '不跳转',
|
|
|
+ 3: '小程序内链',
|
|
|
+ 4: '小程序外链',
|
|
|
+ 5: 'H5外链',
|
|
|
+ 6: '公众号文章',
|
|
|
+ 7: '公众号',
|
|
|
+ 8: '电话拨号'
|
|
|
+ }
|
|
|
+ return typeMap[jumpType] || '-'
|
|
|
+}
|
|
|
+
|
|
|
+// 获取最大排序值
|
|
|
+function getMaxSortNum(): number {
|
|
|
+ if (navItems.value.length === 0) {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+ const maxSort = Math.max(...navItems.value.map(item => item.sortNum || 0))
|
|
|
+ return maxSort
|
|
|
+}
|
|
|
+
|
|
|
+// 加载菜单数据
|
|
|
+async function loadNavData() {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const response = await getEnabledNavigator()
|
|
|
+ if (response.code === 200 && response.data) {
|
|
|
+ navItems.value = response.data as unknown as NavItem[]
|
|
|
+ } else {
|
|
|
+ navItems.value = []
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载数据失败:', error)
|
|
|
+ navItems.value = []
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ // 数据加载完成后,更新排序值
|
|
|
+ if (!form.navId) {
|
|
|
+ form.sortNum = nextSortNum.value
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 搜索按钮操作 */
|
|
|
+function handleQuery() {
|
|
|
+ loadNavData();
|
|
|
+}
|
|
|
+
|
|
|
+/** 重置按钮操作 */
|
|
|
+function resetQuery() {
|
|
|
+ dateRange.value = null;
|
|
|
+ Object.assign(queryParams, {
|
|
|
+ name: '',
|
|
|
+ jumpType: undefined,
|
|
|
+ status: undefined,
|
|
|
+ createTime: ''
|
|
|
+ });
|
|
|
+ loadNavData();
|
|
|
+}
|
|
|
+
|
|
|
+/** 多选框选中数据 */
|
|
|
+function handleSelectionChange(selection: NavItem[]) {
|
|
|
+ selectedIds.value = selection.map(item => item.navId!);
|
|
|
+}
|
|
|
+
|
|
|
+/** 排序变更 */
|
|
|
+async function handleSortChange(row: NavItem) {
|
|
|
+ try {
|
|
|
+ const response = await updateNavigator({
|
|
|
+ navId: row.navId,
|
|
|
+ sortNum: row.sortNum,
|
|
|
+ // type: '4'
|
|
|
+ });
|
|
|
+ if (response.code === 200) {
|
|
|
+ ElMessage.success('排序更新成功');
|
|
|
+ } else {
|
|
|
+ ElMessage.error(response.msg || '排序更新失败');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ ElMessage.error('排序更新失败');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 批量操作 */
|
|
|
+async function handleCommand(command: string, row?: NavItem) {
|
|
|
+ if (command === 'delete') {
|
|
|
+ if (row) {
|
|
|
+ await handleDelete(row);
|
|
|
+ } else if (selectedIds.value.length > 0) {
|
|
|
+ await handleBatchDelete();
|
|
|
+ }
|
|
|
+ } else if (command === 'export') {
|
|
|
+ handleExport();
|
|
|
+ } else if (command === 'view') {
|
|
|
+ handleView(row!);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 批量删除 */
|
|
|
+async function handleBatchDelete() {
|
|
|
+ await ElMessageBox.confirm(`是否确认删除选中的${selectedIds.value.length}个菜单?`, '提示');
|
|
|
+ // 这里需要实现批量删除接口
|
|
|
+ ElMessage.success('批量删除成功');
|
|
|
+ loadNavData();
|
|
|
+}
|
|
|
+
|
|
|
+/** 导出 */
|
|
|
+function handleExport() {
|
|
|
+ ElMessage.info('导出功能待实现');
|
|
|
+}
|
|
|
+
|
|
|
+/** 查看详情 */
|
|
|
+function handleView(row: NavItem) {
|
|
|
+ detailData.value = { ...row };
|
|
|
+ detailOpen.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+// 处理编辑
|
|
|
+function handleEdit(item: NavItem) {
|
|
|
+ reset()
|
|
|
+ const formData = {
|
|
|
+ navId: item.navId,
|
|
|
+ name: item.name,
|
|
|
+ pic: item.pic,
|
|
|
+ color: item.color || '#409EFF',
|
|
|
+ jumpType: item.jumpType || 1,
|
|
|
+ jumpPath: item.jumpType === 2 ? item.jumpPath || '#' : item.jumpPath,
|
|
|
+ activityType: item.activityType || 1,
|
|
|
+ appId: (item as any).appId,
|
|
|
+ sortNum: item.sortNum,
|
|
|
+ status: item.status,
|
|
|
+ remark: item.remark || '',
|
|
|
+ createTime: item.createTime,
|
|
|
+ updateTime: item.updateTime
|
|
|
+ }
|
|
|
+ Object.assign(form, formData)
|
|
|
+ colorInput.value = item.color || '#409EFF'
|
|
|
+ dialogVisible.value = true
|
|
|
+ dialogTitle.value = '修改菜单'
|
|
|
+}
|
|
|
+
|
|
|
+// 处理添加
|
|
|
+function handleAdd() {
|
|
|
+ reset()
|
|
|
+ // 设置排序值为当前最大值+1
|
|
|
+ form.sortNum = nextSortNum.value
|
|
|
+ dialogVisible.value = true
|
|
|
+ dialogTitle.value = '添加菜单'
|
|
|
+}
|
|
|
+
|
|
|
+// 处理删除
|
|
|
+async function handleDelete(item: NavItem) {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm('确认删除该菜单吗?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+
|
|
|
+ const response = await delNavigator(item.navId!)
|
|
|
+ if (response.code === 200) {
|
|
|
+ ElMessage.success('删除成功')
|
|
|
+ loadNavData()
|
|
|
+ } else {
|
|
|
+ ElMessage.error(response.msg || '删除失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ if (error !== 'cancel') {
|
|
|
+ ElMessage.error('删除失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 处理提交
|
|
|
+async function handleSubmit() {
|
|
|
+ try {
|
|
|
+ await formRef.value.validate()
|
|
|
+ submitting.value = true
|
|
|
+
|
|
|
+ const payload: any = {
|
|
|
+ navId: form.navId,
|
|
|
+ name: form.name,
|
|
|
+ pic: form.pic,
|
|
|
+ color: form.color,
|
|
|
+ jumpType: form.jumpType,
|
|
|
+ jumpPath: form.jumpType === 2 ? form.jumpPath || '#' : form.jumpPath,
|
|
|
+ activityType: form.activityType,
|
|
|
+ appId: form.appId,
|
|
|
+ sortNum: form.sortNum,
|
|
|
+ status: form.status,
|
|
|
+ remark: form.remark,
|
|
|
+ createTime: form.createTime,
|
|
|
+ updateTime: form.updateTime,
|
|
|
+ // type: '4' // 菜单固定类型
|
|
|
+ }
|
|
|
+
|
|
|
+ let response
|
|
|
+ if (form.navId) {
|
|
|
+ response = await updateNavigator(payload)
|
|
|
+ } else {
|
|
|
+ response = await addNavigator(payload)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (response.code === 200) {
|
|
|
+ ElMessage.success(form.navId ? '修改成功' : '添加成功')
|
|
|
+ dialogVisible.value = false
|
|
|
+ loadNavData()
|
|
|
+ } else {
|
|
|
+ ElMessage.error(response.msg || '操作失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('提交失败:', error)
|
|
|
+ ElMessage.error('操作失败')
|
|
|
+ } finally {
|
|
|
+ submitting.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 重置表单
|
|
|
+function reset() {
|
|
|
+ Object.assign(form, {
|
|
|
+ navId: undefined,
|
|
|
+ name: '',
|
|
|
+ pic: '',
|
|
|
+ color: '#409EFF',
|
|
|
+ jumpType: 1,
|
|
|
+ jumpPath: '',
|
|
|
+ activityType: 1,
|
|
|
+ appId: '',
|
|
|
+ sortNum: nextSortNum.value,
|
|
|
+ status: 0,
|
|
|
+ remark: '',
|
|
|
+ createTime: '',
|
|
|
+ updateTime: ''
|
|
|
+ })
|
|
|
+ colorInput.value = '#409EFF'
|
|
|
+ formRef.value?.clearValidate()
|
|
|
+}
|
|
|
+
|
|
|
+// 跳转类型变更处理
|
|
|
+function handleJumpTypeChange() {
|
|
|
+ if (form.jumpType === 2) {
|
|
|
+ form.jumpPath = '#'
|
|
|
+ } else if (form.jumpType === 4) {
|
|
|
+ form.jumpPath = ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 图片上传前验证
|
|
|
+function beforeIconUpload(file: 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
|
|
|
+}
|
|
|
+
|
|
|
+// 图片上传成功
|
|
|
+function handleIconSuccess(response: any) {
|
|
|
+ if (response.code === 200) {
|
|
|
+ form.pic = response.data.url
|
|
|
+ ElMessage.success('图片上传成功')
|
|
|
+ } else {
|
|
|
+ ElMessage.error(response.msg || '图片上传失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 颜色输入框的值
|
|
|
+const colorInput = ref('#409EFF')
|
|
|
+
|
|
|
+// 将RGB颜色转换为16进制
|
|
|
+const rgbToHex = (rgb: string): string => {
|
|
|
+ // 如果是16进制格式,直接返回
|
|
|
+ if (rgb.startsWith('#')) {
|
|
|
+ return rgb
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是RGB格式,转换为16进制
|
|
|
+ const rgbMatch = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/)
|
|
|
+ if (rgbMatch) {
|
|
|
+ const r = parseInt(rgbMatch[1])
|
|
|
+ const g = parseInt(rgbMatch[2])
|
|
|
+ const b = parseInt(rgbMatch[3])
|
|
|
+ return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果无法解析,返回默认值
|
|
|
+ return '#409EFF'
|
|
|
+}
|
|
|
+
|
|
|
+// 处理颜色输入框变化
|
|
|
+const handleColorInput = (value: string) => {
|
|
|
+ // 实时同步到颜色选择器
|
|
|
+ if (value.startsWith('#')) {
|
|
|
+ form.color = value
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 处理颜色选择器变化
|
|
|
+const handleColorChange = (value: string) => {
|
|
|
+ // 当颜色选择器改变时,将16进制值填充到输入框
|
|
|
+ if (value) {
|
|
|
+ const hexValue = rgbToHex(value)
|
|
|
+ colorInput.value = hexValue
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 验证颜色输入框
|
|
|
+const validateColorInput = () => {
|
|
|
+ const hexRegex = /^#[0-9A-Fa-f]{6}$/
|
|
|
+ if (!hexRegex.test(colorInput.value)) {
|
|
|
+ ElMessage.warning('请输入正确的16进制颜色格式,如 #FF0000')
|
|
|
+ colorInput.value = form.color
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化数据
|
|
|
+onMounted(() => {
|
|
|
+ loadNavData()
|
|
|
+ // 初始化排序值
|
|
|
+ form.sortNum = nextSortNum.value
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.bottom-main-nav {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-card {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.action-card {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.list-card {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.mb8 {
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16px;
|
|
|
+ padding: 8px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-image {
|
|
|
+ width: 60px;
|
|
|
+ height: 60px;
|
|
|
+ border-radius: 6px;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #f5f7fa;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-image img {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+}
|
|
|
+
|
|
|
+.no-image {
|
|
|
+ color: #c0c4cc;
|
|
|
+ font-size: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-details {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-title,
|
|
|
+.nav-module {
|
|
|
+ margin-bottom: 6px;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.4;
|
|
|
+}
|
|
|
+
|
|
|
+.label {
|
|
|
+ color: #909399;
|
|
|
+ margin-right: 6px;
|
|
|
+ font-weight: normal;
|
|
|
+}
|
|
|
+
|
|
|
+.value {
|
|
|
+ color: #333;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.link-info,
|
|
|
+.create-info {
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.5;
|
|
|
+ padding: 8px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.link-type,
|
|
|
+.link-address,
|
|
|
+.status,
|
|
|
+.create-time {
|
|
|
+ margin-bottom: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.status {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.create-time {
|
|
|
+ margin-top: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.sort-number {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-content {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-item {
|
|
|
+ display: flex;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ align-items: flex-start;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-label {
|
|
|
+ width: 120px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #606266;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-value {
|
|
|
+ flex: 1;
|
|
|
+ color: #333;
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-image {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-image img {
|
|
|
+ max-width: 200px;
|
|
|
+ max-height: 120px;
|
|
|
+ border-radius: 6px;
|
|
|
+ object-fit: cover;
|
|
|
+}
|
|
|
+
|
|
|
+.no-image-text {
|
|
|
+ color: #909399;
|
|
|
+ font-style: italic;
|
|
|
+}
|
|
|
+
|
|
|
+.dialog-footer {
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+.icon-uploader {
|
|
|
+ border: 1px dashed #d9d9d9;
|
|
|
+ border-radius: 6px;
|
|
|
+ cursor: pointer;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+ width: 100px;
|
|
|
+ height: 100px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.icon-uploader:hover {
|
|
|
+ border-color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.icon-preview {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+}
|
|
|
+
|
|
|
+.icon-uploader-icon {
|
|
|
+ font-size: 28px;
|
|
|
+ color: #8c939d;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-tip {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-top: 8px;
|
|
|
+}
|
|
|
+</style>
|