|
|
|
|
@@ -0,0 +1,405 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="container">
|
|
|
|
|
<a-breadcrumb class="breadcrumb">
|
|
|
|
|
<a-breadcrumb-item>首页轮播与运营</a-breadcrumb-item>
|
|
|
|
|
<a-breadcrumb-item>金刚区导航品类</a-breadcrumb-item>
|
|
|
|
|
</a-breadcrumb>
|
|
|
|
|
|
|
|
|
|
<a-card class="general-card" title="首页金刚区活动品类导航控制台">
|
|
|
|
|
<template #extra>
|
|
|
|
|
<a-space>
|
|
|
|
|
<a-button type="primary" @click="openCreateModal">
|
|
|
|
|
<template #icon><icon-plus /></template>
|
|
|
|
|
新增品类导航
|
|
|
|
|
</a-button>
|
|
|
|
|
<a-button @click="fetchData">
|
|
|
|
|
<template #icon><icon-refresh /></template>
|
|
|
|
|
刷新
|
|
|
|
|
</a-button>
|
|
|
|
|
</a-space>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- 提示栏 -->
|
|
|
|
|
<a-alert style="margin-bottom: 16px" type="info">
|
|
|
|
|
金刚区为小程序首页核心导流宫格网格。支持自由配置品类名称、SVG图标标识或上传自定图标、微光背景底色、跳转链接路径、右上角特惠高亮角标与显示排序。
|
|
|
|
|
</a-alert>
|
|
|
|
|
|
|
|
|
|
<!-- 数据表格 -->
|
|
|
|
|
<a-table
|
|
|
|
|
:data="categoryList"
|
|
|
|
|
:loading="loading"
|
|
|
|
|
:pagination="{ pageSize: 12, showTotal: true }"
|
|
|
|
|
row-key="id"
|
|
|
|
|
>
|
|
|
|
|
<template #columns>
|
|
|
|
|
<a-table-column title="ID" data-index="id" :width="70" />
|
|
|
|
|
|
|
|
|
|
<a-table-column title="图标标识" :width="110">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<div class="icon-avatar-box" :class="record.bgClass">
|
|
|
|
|
<span class="icon-code-text">{{ record.icon }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
|
|
|
|
|
<a-table-column title="品类名称" :width="160">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<div class="cat-name-text">
|
|
|
|
|
{{ record.name }}
|
|
|
|
|
<a-tag v-if="record.badgeText" color="red" size="small" style="margin-left: 6px">
|
|
|
|
|
{{ record.badgeText }}
|
|
|
|
|
</a-tag>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="cat-code-sub">标识码: {{ record.code }}</div>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
|
|
|
|
|
<a-table-column title="品类分类与底色" :width="160">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<div style="margin-bottom: 4px">
|
|
|
|
|
<a-tag color="blue" size="small">{{ record.cat }}</a-tag>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<a-tag color="gray" size="small">{{ record.bgClass }}</a-tag>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
|
|
|
|
|
<a-table-column title="跳转路径" :width="240">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<span class="url-text">{{ record.targetUrl }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
|
|
|
|
|
<a-table-column title="显示排序" data-index="sortOrder" :width="100">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<a-badge :count="record.sortOrder" :max-count="999" color="#165dff" />
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
|
|
|
|
|
<a-table-column title="状态" :width="110">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<a-switch
|
|
|
|
|
:model-value="record.status === 1"
|
|
|
|
|
:loading="statusLoadingId === record.id"
|
|
|
|
|
@change="(val) => handleToggleStatus(record, val)"
|
|
|
|
|
>
|
|
|
|
|
<template #checked>上架</template>
|
|
|
|
|
<template #unchecked>下线</template>
|
|
|
|
|
</a-switch>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
|
|
|
|
|
<a-table-column title="操作" :width="150" fixed="right">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<a-space>
|
|
|
|
|
<a-button type="text" size="small" @click="openEditModal(record)">
|
|
|
|
|
<template #icon><icon-edit /></template>
|
|
|
|
|
编辑
|
|
|
|
|
</a-button>
|
|
|
|
|
<a-popconfirm
|
|
|
|
|
content="确定彻底删除此金刚区品类吗?删除后小程序将不再呈现。"
|
|
|
|
|
@ok="handleDelete(record.id)"
|
|
|
|
|
>
|
|
|
|
|
<a-button type="text" status="danger" size="small">
|
|
|
|
|
<template #icon><icon-delete /></template>
|
|
|
|
|
删除
|
|
|
|
|
</a-button>
|
|
|
|
|
</a-popconfirm>
|
|
|
|
|
</a-space>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table>
|
|
|
|
|
</a-card>
|
|
|
|
|
|
|
|
|
|
<!-- 新增 / 编辑 弹窗 -->
|
|
|
|
|
<a-modal
|
|
|
|
|
v-model:visible="modalVisible"
|
|
|
|
|
:title="isEdit ? '编辑金刚区导航品类' : '新增金刚区导航品类'"
|
|
|
|
|
:width="600"
|
|
|
|
|
@ok="handleModalSubmit"
|
|
|
|
|
@cancel="modalVisible = false"
|
|
|
|
|
>
|
|
|
|
|
<a-form :model="formData" layout="vertical">
|
|
|
|
|
<a-row :gutter="16">
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
<a-form-item label="品类名称" required>
|
|
|
|
|
<a-input v-model="formData.name" placeholder="如:美食探店 / 剧本桌游" maxlength="20" />
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
<a-form-item label="唯一编码 (code)">
|
|
|
|
|
<a-input v-model="formData.code" placeholder="如:meal / sing / esports" :disabled="isEdit" />
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
</a-row>
|
|
|
|
|
|
|
|
|
|
<a-row :gutter="16">
|
|
|
|
|
<a-col :span="14">
|
|
|
|
|
<a-form-item label="矢量图标标识 (SVG key / 图片URL)" required>
|
|
|
|
|
<a-input
|
|
|
|
|
v-model="formData.icon"
|
|
|
|
|
placeholder="如: utensils, micVocal, billiards 或 http开头的图片URL"
|
|
|
|
|
/>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
<a-col :span="10">
|
|
|
|
|
<a-form-item label="常用图标预设">
|
|
|
|
|
<a-select v-model="formData.icon" placeholder="快捷选择">
|
|
|
|
|
<a-option value="gameAll">电竞游戏 (gameAll)</a-option>
|
|
|
|
|
<a-option value="utensils">美食餐饮 (utensils)</a-option>
|
|
|
|
|
<a-option value="micVocal">KTV嗨唱 (micVocal)</a-option>
|
|
|
|
|
<a-option value="billiards">同城台球 (billiards)</a-option>
|
|
|
|
|
<a-option value="mountain">户外登山 (mountain)</a-option>
|
|
|
|
|
<a-option value="coffee">咖啡茶歇 (coffee)</a-option>
|
|
|
|
|
<a-option value="dice">桌游骰子 (dice)</a-option>
|
|
|
|
|
<a-option value="fitness">运动健身 (fitness)</a-option>
|
|
|
|
|
<a-option value="warmFlame">火热微醺 (warmFlame)</a-option>
|
|
|
|
|
<a-option value="gridAll">全部活动 (gridAll)</a-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
</a-row>
|
|
|
|
|
|
|
|
|
|
<a-row :gutter="16">
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
<a-form-item label="背景微光色 (bg_class)">
|
|
|
|
|
<a-select v-model="formData.bgClass" placeholder="选择卡片底色">
|
|
|
|
|
<a-option value="bg-indigo">靛蓝 (bg-indigo)</a-option>
|
|
|
|
|
<a-option value="bg-amber">琥珀 (bg-amber)</a-option>
|
|
|
|
|
<a-option value="bg-rose">玫瑰 (bg-rose)</a-option>
|
|
|
|
|
<a-option value="bg-sky">天蓝 (bg-sky)</a-option>
|
|
|
|
|
<a-option value="bg-emerald">翡翠 (bg-emerald)</a-option>
|
|
|
|
|
<a-option value="bg-teal">青碧 (bg-teal)</a-option>
|
|
|
|
|
<a-option value="bg-purple">罗兰 (bg-purple)</a-option>
|
|
|
|
|
<a-option value="bg-orange">暖橙 (bg-orange)</a-option>
|
|
|
|
|
<a-option value="bg-blue">经典蓝 (bg-blue)</a-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
<a-form-item label="分类筛选标识 (cat)">
|
|
|
|
|
<a-input v-model="formData.cat" placeholder="如:offline_meal / offline_sing / all" />
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
</a-row>
|
|
|
|
|
|
|
|
|
|
<a-form-item label="跳转目标页面路径" required>
|
|
|
|
|
<a-input v-model="formData.targetUrl" placeholder="如:/pages/activity/list?category=MEAL" />
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
|
|
|
|
<a-row :gutter="16">
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
<a-form-item label="右上角角标文案 (如: HOT / AA制 / 周末)">
|
|
|
|
|
<a-input v-model="formData.badgeText" placeholder="留空则不显示角标" maxlength="6" />
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
<a-form-item label="排序优先级 (越小越靠前)">
|
|
|
|
|
<a-input-number v-model="formData.sortOrder" :min="0" :max="999" />
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
</a-row>
|
|
|
|
|
|
|
|
|
|
<a-form-item label="初始发布状态">
|
|
|
|
|
<a-radio-group v-model="formData.status">
|
|
|
|
|
<a-radio :value="1">立即上架(小程序端立即可见)</a-radio>
|
|
|
|
|
<a-radio :value="0">暂存草稿(下线隐藏)</a-radio>
|
|
|
|
|
</a-radio-group>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-form>
|
|
|
|
|
</a-modal>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
|
|
import { ref, onMounted } from 'vue';
|
|
|
|
|
import axios from 'axios';
|
|
|
|
|
import { Message } from '@arco-design/web-vue';
|
|
|
|
|
|
|
|
|
|
interface NavCategoryItem {
|
|
|
|
|
id: number;
|
|
|
|
|
code: string;
|
|
|
|
|
name: string;
|
|
|
|
|
icon: string;
|
|
|
|
|
bgClass: string;
|
|
|
|
|
cat: string;
|
|
|
|
|
targetUrl: string;
|
|
|
|
|
badgeText: string;
|
|
|
|
|
sortOrder: number;
|
|
|
|
|
status: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
const categoryList = ref<NavCategoryItem[]>([]);
|
|
|
|
|
const statusLoadingId = ref<number | null>(null);
|
|
|
|
|
|
|
|
|
|
const modalVisible = ref(false);
|
|
|
|
|
const isEdit = ref(false);
|
|
|
|
|
const editingId = ref<number | null>(null);
|
|
|
|
|
|
|
|
|
|
const defaultFormData = {
|
|
|
|
|
code: '',
|
|
|
|
|
name: '',
|
|
|
|
|
icon: 'utensils',
|
|
|
|
|
bgClass: 'bg-indigo',
|
|
|
|
|
cat: 'all',
|
|
|
|
|
targetUrl: '/pages/activity/list?category=ALL',
|
|
|
|
|
badgeText: '',
|
|
|
|
|
sortOrder: 1,
|
|
|
|
|
status: 1,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formData = ref({ ...defaultFormData });
|
|
|
|
|
|
|
|
|
|
const fetchData = async () => {
|
|
|
|
|
loading.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const res: any = await axios.get('/api/admin/nav-categories');
|
|
|
|
|
if (res && res.data && res.data.code === 20000) {
|
|
|
|
|
categoryList.value = res.data.data;
|
|
|
|
|
} else if (Array.isArray(res.data)) {
|
|
|
|
|
categoryList.value = res.data;
|
|
|
|
|
}
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
Message.error(err.response?.data?.message || '获取金刚区品类列表失败');
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const openCreateModal = () => {
|
|
|
|
|
isEdit.value = false;
|
|
|
|
|
editingId.value = null;
|
|
|
|
|
formData.value = {
|
|
|
|
|
...defaultFormData,
|
|
|
|
|
code: `cat_${Date.now()}`,
|
|
|
|
|
};
|
|
|
|
|
modalVisible.value = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const openEditModal = (record: NavCategoryItem) => {
|
|
|
|
|
isEdit.value = true;
|
|
|
|
|
editingId.value = record.id;
|
|
|
|
|
formData.value = {
|
|
|
|
|
code: record.code,
|
|
|
|
|
name: record.name,
|
|
|
|
|
icon: record.icon,
|
|
|
|
|
bgClass: record.bgClass || 'bg-indigo',
|
|
|
|
|
cat: record.cat || 'all',
|
|
|
|
|
targetUrl: record.targetUrl || '/pages/activity/list?category=ALL',
|
|
|
|
|
badgeText: record.badgeText || '',
|
|
|
|
|
sortOrder: record.sortOrder || 0,
|
|
|
|
|
status: record.status,
|
|
|
|
|
};
|
|
|
|
|
modalVisible.value = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleModalSubmit = async () => {
|
|
|
|
|
if (!formData.value.name || !formData.value.name.trim()) {
|
|
|
|
|
Message.warning('品类名称不能为空');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!formData.value.icon || !formData.value.icon.trim()) {
|
|
|
|
|
Message.warning('图标标识不能为空');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (isEdit.value && editingId.value) {
|
|
|
|
|
const res: any = await axios.put(`/api/admin/nav-categories/${editingId.value}`, formData.value);
|
|
|
|
|
if (res && (res.data.code === 20000 || res.status === 200)) {
|
|
|
|
|
Message.success('品类导航修改成功');
|
|
|
|
|
modalVisible.value = false;
|
|
|
|
|
fetchData();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const res: any = await axios.post('/api/admin/nav-categories', formData.value);
|
|
|
|
|
if (res && (res.data.code === 20000 || res.status === 200)) {
|
|
|
|
|
Message.success('品类导航新增成功,已同步至小程序首页');
|
|
|
|
|
modalVisible.value = false;
|
|
|
|
|
fetchData();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
Message.error(err.response?.data?.message || '保存失败,请稍后重试');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleToggleStatus = async (record: NavCategoryItem, checked: any) => {
|
|
|
|
|
const newStatus = checked ? 1 : 0;
|
|
|
|
|
statusLoadingId.value = record.id;
|
|
|
|
|
try {
|
|
|
|
|
const res: any = await axios.post(`/api/admin/nav-categories/${record.id}/status`, {
|
|
|
|
|
status: newStatus,
|
|
|
|
|
});
|
|
|
|
|
if (res && (res.data.code === 20000 || res.status === 200)) {
|
|
|
|
|
record.status = newStatus;
|
|
|
|
|
Message.success(newStatus === 1 ? '品类已上架至小程序' : '品类已下线隐藏');
|
|
|
|
|
}
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
Message.error(err.response?.data?.message || '切换状态失败');
|
|
|
|
|
} finally {
|
|
|
|
|
statusLoadingId.value = null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDelete = async (id: number) => {
|
|
|
|
|
try {
|
|
|
|
|
const res: any = await axios.delete(`/api/admin/nav-categories/${id}`);
|
|
|
|
|
if (res && (res.data.code === 20000 || res.status === 200)) {
|
|
|
|
|
Message.success('品类导航已彻底删除');
|
|
|
|
|
fetchData();
|
|
|
|
|
}
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
Message.error(err.response?.data?.message || '删除失败');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
fetchData();
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.container {
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
}
|
|
|
|
|
.breadcrumb {
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
.icon-avatar-box {
|
|
|
|
|
width: 68px;
|
|
|
|
|
height: 38px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
background: rgba(99, 102, 241, 0.12);
|
|
|
|
|
border: 1px solid rgba(99, 102, 241, 0.25);
|
|
|
|
|
}
|
|
|
|
|
.icon-code-text {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #165dff;
|
|
|
|
|
}
|
|
|
|
|
.cat-name-text {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
color: var(--color-text-1);
|
|
|
|
|
}
|
|
|
|
|
.cat-code-sub {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--color-text-3);
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
}
|
|
|
|
|
.url-text {
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: #165dff;
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
}
|
|
|
|
|
</style>
|