|
|
|
|
@@ -0,0 +1,445 @@
|
|
|
|
|
<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">
|
|
|
|
|
在此处配置的轮播图将实时同步至微信小程序端首页顶部横幅。支持配置活动跳转链接、标签角标、背景渐变色、排序优先级及一键上下线。
|
|
|
|
|
</a-alert>
|
|
|
|
|
|
|
|
|
|
<!-- 表格区域 -->
|
|
|
|
|
<a-table
|
|
|
|
|
:data="bannerList"
|
|
|
|
|
:loading="loading"
|
|
|
|
|
:pagination="{ pageSize: 10, showTotal: true }"
|
|
|
|
|
row-key="id"
|
|
|
|
|
>
|
|
|
|
|
<template #columns>
|
|
|
|
|
<a-table-column title="ID" data-index="id" :width="70" />
|
|
|
|
|
|
|
|
|
|
<a-table-column title="封面图片" :width="140">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<div class="banner-preview-box" :style="{ background: record.bg }">
|
|
|
|
|
<a-image
|
|
|
|
|
v-if="record.image"
|
|
|
|
|
:src="record.image"
|
|
|
|
|
:width="110"
|
|
|
|
|
:height="56"
|
|
|
|
|
fit="cover"
|
|
|
|
|
style="border-radius: 4px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);"
|
|
|
|
|
/>
|
|
|
|
|
<div v-else class="empty-img-text">无图卡片</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
|
|
|
|
|
<a-table-column title="标题 / 副标题" :width="280">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<div class="banner-title-text">{{ record.title }}</div>
|
|
|
|
|
<div class="banner-subtitle-text">{{ record.subtitle || '(未设置副标题)' }}</div>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
|
|
|
|
|
<a-table-column title="标签 / 角标" :width="150">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<div style="margin-bottom: 4px">
|
|
|
|
|
<a-tag color="arcoblue" size="small">{{ record.tag || '默认标签' }}</a-tag>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<a-tag color="orangered" size="small">{{ record.badge || 'HOT' }}</a-tag>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
|
|
|
|
|
<a-table-column title="跳转链接" :width="200">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<span class="link-text">{{ record.link }}</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="160" 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="640"
|
|
|
|
|
@ok="handleModalSubmit"
|
|
|
|
|
@cancel="modalVisible = false"
|
|
|
|
|
>
|
|
|
|
|
<a-form :model="formData" layout="vertical">
|
|
|
|
|
<a-form-item label="主标题" required>
|
|
|
|
|
<a-input
|
|
|
|
|
v-model="formData.title"
|
|
|
|
|
placeholder="如:同城聚会狂欢 | 网红火锅探店与量贩KTV嗨唱"
|
|
|
|
|
maxlength="100"
|
|
|
|
|
show-word-limit
|
|
|
|
|
/>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
|
|
|
|
<a-form-item label="副标题">
|
|
|
|
|
<a-input
|
|
|
|
|
v-model="formData.subtitle"
|
|
|
|
|
placeholder="如:AA制铜锅涮肉 · 8人热烈局"
|
|
|
|
|
maxlength="150"
|
|
|
|
|
/>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
|
|
|
|
<a-row :gutter="16">
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
<a-form-item label="主分类标签">
|
|
|
|
|
<a-input v-model="formData.tag" placeholder="如:热门陪伴 / K歌对唱" />
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
<a-form-item label="角标高亮文本">
|
|
|
|
|
<a-input v-model="formData.badge" placeholder="如:HOT 热门组局 / 限时立减" />
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
</a-row>
|
|
|
|
|
|
|
|
|
|
<a-form-item label="轮播图封面图片">
|
|
|
|
|
<div style="display: flex; gap: 12px; align-items: flex-start; width: 100%;">
|
|
|
|
|
<a-input
|
|
|
|
|
v-model="formData.image"
|
|
|
|
|
placeholder="可直接输入图片URL或点击右侧上传"
|
|
|
|
|
style="flex: 1;"
|
|
|
|
|
/>
|
|
|
|
|
<a-upload
|
|
|
|
|
action="/api/upload"
|
|
|
|
|
name="file"
|
|
|
|
|
:show-file-list="false"
|
|
|
|
|
@success="handleUploadSuccess"
|
|
|
|
|
@error="handleUploadError"
|
|
|
|
|
>
|
|
|
|
|
<template #upload-button>
|
|
|
|
|
<a-button type="outline">
|
|
|
|
|
<template #icon><icon-upload /></template>
|
|
|
|
|
上传图片
|
|
|
|
|
</a-button>
|
|
|
|
|
</template>
|
|
|
|
|
</a-upload>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="formData.image" style="margin-top: 10px">
|
|
|
|
|
<a-image
|
|
|
|
|
:src="formData.image"
|
|
|
|
|
:height="90"
|
|
|
|
|
fit="cover"
|
|
|
|
|
style="border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
|
|
|
|
<a-row :gutter="16">
|
|
|
|
|
<a-col :span="14">
|
|
|
|
|
<a-form-item label="跳转页面路径">
|
|
|
|
|
<a-input
|
|
|
|
|
v-model="formData.link"
|
|
|
|
|
placeholder="如:/pages/activity/detail?id=act_101"
|
|
|
|
|
/>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
<a-col :span="10">
|
|
|
|
|
<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-input
|
|
|
|
|
v-model="formData.bg"
|
|
|
|
|
placeholder="如:linear-gradient(135deg, #F43F5E 0%, #FB7185 100%)"
|
|
|
|
|
/>
|
|
|
|
|
<div style="margin-top: 6px; display: flex; gap: 8px;">
|
|
|
|
|
<a-button
|
|
|
|
|
size="mini"
|
|
|
|
|
@click="formData.bg = 'linear-gradient(135deg, #F43F5E 0%, #FB7185 100%)'"
|
|
|
|
|
>
|
|
|
|
|
绯红活力
|
|
|
|
|
</a-button>
|
|
|
|
|
<a-button
|
|
|
|
|
size="mini"
|
|
|
|
|
@click="formData.bg = 'linear-gradient(135deg, #7C3AED 0%, #6366F1 100%)'"
|
|
|
|
|
>
|
|
|
|
|
紫蓝魅夜
|
|
|
|
|
</a-button>
|
|
|
|
|
<a-button
|
|
|
|
|
size="mini"
|
|
|
|
|
@click="formData.bg = 'linear-gradient(135deg, #059669 0%, #10B981 100%)'"
|
|
|
|
|
>
|
|
|
|
|
清新薄荷
|
|
|
|
|
</a-button>
|
|
|
|
|
<a-button
|
|
|
|
|
size="mini"
|
|
|
|
|
@click="formData.bg = 'linear-gradient(135deg, #D97706 0%, #F59E0B 100%)'"
|
|
|
|
|
>
|
|
|
|
|
暖阳金秋
|
|
|
|
|
</a-button>
|
|
|
|
|
</div>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
|
|
|
|
<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 BannerItem {
|
|
|
|
|
id: number;
|
|
|
|
|
title: string;
|
|
|
|
|
subtitle: string;
|
|
|
|
|
tag: string;
|
|
|
|
|
badge: string;
|
|
|
|
|
badgeType: string;
|
|
|
|
|
bg: string;
|
|
|
|
|
image: string;
|
|
|
|
|
link: string;
|
|
|
|
|
sortOrder: number;
|
|
|
|
|
status: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
const bannerList = ref<BannerItem[]>([]);
|
|
|
|
|
const statusLoadingId = ref<number | null>(null);
|
|
|
|
|
|
|
|
|
|
const modalVisible = ref(false);
|
|
|
|
|
const isEdit = ref(false);
|
|
|
|
|
const editingId = ref<number | null>(null);
|
|
|
|
|
|
|
|
|
|
const defaultFormData = {
|
|
|
|
|
title: '',
|
|
|
|
|
subtitle: '',
|
|
|
|
|
tag: '热门活动',
|
|
|
|
|
badge: 'HOT 热门组局',
|
|
|
|
|
badgeType: 'badge-hot',
|
|
|
|
|
bg: 'linear-gradient(135deg, #F43F5E 0%, #FB7185 100%)',
|
|
|
|
|
image: '',
|
|
|
|
|
link: '/pages/activity/list',
|
|
|
|
|
sortOrder: 1,
|
|
|
|
|
status: 1,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formData = ref({ ...defaultFormData });
|
|
|
|
|
|
|
|
|
|
const fetchData = async () => {
|
|
|
|
|
loading.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const res: any = await axios.get('/api/admin/banners');
|
|
|
|
|
if (res && res.data && res.data.code === 20000) {
|
|
|
|
|
bannerList.value = res.data.data;
|
|
|
|
|
} else if (Array.isArray(res.data)) {
|
|
|
|
|
bannerList.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 };
|
|
|
|
|
modalVisible.value = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const openEditModal = (record: BannerItem) => {
|
|
|
|
|
isEdit.value = true;
|
|
|
|
|
editingId.value = record.id;
|
|
|
|
|
formData.value = {
|
|
|
|
|
title: record.title,
|
|
|
|
|
subtitle: record.subtitle || '',
|
|
|
|
|
tag: record.tag || '热门活动',
|
|
|
|
|
badge: record.badge || 'HOT',
|
|
|
|
|
badgeType: record.badgeType || 'badge-hot',
|
|
|
|
|
bg: record.bg || 'linear-gradient(135deg, #F43F5E 0%, #FB7185 100%)',
|
|
|
|
|
image: record.image || '',
|
|
|
|
|
link: record.link || '/pages/activity/list',
|
|
|
|
|
sortOrder: record.sortOrder || 0,
|
|
|
|
|
status: record.status,
|
|
|
|
|
};
|
|
|
|
|
modalVisible.value = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleUploadSuccess = (fileItem: any) => {
|
|
|
|
|
const response = fileItem.response;
|
|
|
|
|
if (response && response.code === 20000 && response.data?.url) {
|
|
|
|
|
formData.value.image = response.data.url;
|
|
|
|
|
Message.success('轮播图片上传成功并已持久化');
|
|
|
|
|
} else {
|
|
|
|
|
Message.warning('上传完成,但响应格式异常');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleUploadError = () => {
|
|
|
|
|
Message.error('图片上传失败,请检查文件大小及网络');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleModalSubmit = async () => {
|
|
|
|
|
if (!formData.value.title || !formData.value.title.trim()) {
|
|
|
|
|
Message.warning('轮播图主标题不能为空');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (isEdit.value && editingId.value) {
|
|
|
|
|
const res: any = await axios.put(`/api/admin/banners/${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/banners', 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: BannerItem, checked: any) => {
|
|
|
|
|
const newStatus = checked ? 1 : 0;
|
|
|
|
|
statusLoadingId.value = record.id;
|
|
|
|
|
try {
|
|
|
|
|
const res: any = await axios.post(`/api/admin/banners/${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/banners/${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;
|
|
|
|
|
}
|
|
|
|
|
.banner-preview-box {
|
|
|
|
|
padding: 4px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
.empty-img-text {
|
|
|
|
|
width: 110px;
|
|
|
|
|
height: 56px;
|
|
|
|
|
line-height: 56px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
color: #fff;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
.banner-title-text {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--color-text-1);
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
.banner-subtitle-text {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--color-text-3);
|
|
|
|
|
}
|
|
|
|
|
.link-text {
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #165dff;
|
|
|
|
|
}
|
|
|
|
|
</style>
|