| New file |
| | |
| | | import { request } from '../http' |
| | | |
| | | export interface Banner { |
| | | id: number |
| | | bannerTitle: string |
| | | bannerPictures: string // JSON string: [{"id":"...","name":"...","url":"..."}] |
| | | startTime: string |
| | | endTime: string |
| | | status: number // 0=未开始, 1=进行中, 2=已结束 (展示用) |
| | | bannerStatus: number // 1=上架, 2=下架 (控制用) |
| | | jumpType: number // 1=小程序内页面, 2=小程序地址, 3=网页地址 |
| | | appletName?: string |
| | | appletId?: string |
| | | jumpUrl?: string |
| | | createTime?: string |
| | | createBy?: string |
| | | updateBy?: string |
| | | updateTime?: string |
| | | } |
| | | |
| | | export interface BannerSearchParams { |
| | | bannerTitle?: string |
| | | status?: number | null |
| | | pageNum: number |
| | | pageSize: number |
| | | } |
| | | |
| | | /** |
| | | * 获取 Banner 分页列表 |
| | | */ |
| | | export function fetchBannerPageList(data: BannerSearchParams) { |
| | | return request.Post<Service.ResponseResult<{ records: Banner[], total: number }>>('/operation/api/v1/banner/pageList', data) |
| | | } |
| | | |
| | | /** |
| | | * 新增 Banner |
| | | */ |
| | | export function addBanner(data: Partial<Banner>) { |
| | | return request.Post<Service.ResponseResult<any>>('/operation/api/v1/banner/saveBanner', data) |
| | | } |
| | | |
| | | /** |
| | | * 编辑 Banner |
| | | */ |
| | | export function updateBanner(data: Partial<Banner>) { |
| | | return request.Post<Service.ResponseResult<any>>('/operation/api/v1/banner/updateBanner', data) |
| | | } |
| | | |
| | | /** |
| | | * 获取 Banner 详情 |
| | | */ |
| | | export function getBannerDetail(id: number) { |
| | | return request.Get<Service.ResponseResult<Banner>>('/operation/api/v1/banner/detailBanner', { params: { id } }) |
| | | } |
| | | |
| | | /** |
| | | * 删除 Banner |
| | | */ |
| | | export function deleteBanner(id: number) { |
| | | return request.Delete<Service.ResponseResult<any>>('/operation/api/v1/banner/deleteBanner?id=' + id, ) |
| | | } |
| New file |
| | |
| | | <script setup lang="tsx"> |
| | | import { ref, computed } from 'vue' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NTag } from 'naive-ui' |
| | | |
| | | interface Props { |
| | | show: boolean |
| | | } |
| | | |
| | | interface Emits { |
| | | (e: 'update:show', value: boolean): void |
| | | (e: 'send-all'): void |
| | | (e: 'send-selected', rows: User[]): void |
| | | } |
| | | |
| | | const props = defineProps<Props>() |
| | | const emits = defineEmits<Emits>() |
| | | |
| | | const showModal = computed({ |
| | | get: () => props.show, |
| | | set: (val) => emits('update:show', val), |
| | | }) |
| | | |
| | | interface User { |
| | | id: number |
| | | userName: string |
| | | whiteListAccount: string |
| | | status: 'Normal' | 'Frozen' |
| | | priorityDispatch: string |
| | | creator: string |
| | | createTime: string |
| | | } |
| | | |
| | | const searchModel = ref({ |
| | | whiteListAccount: '', |
| | | source: null, |
| | | bindingStatus: null, |
| | | }) |
| | | |
| | | const sourceOptions = [ |
| | | { label: '来源1', value: 'source1' }, |
| | | { label: '来源2', value: 'source2' }, |
| | | ] |
| | | |
| | | const bindingStatusOptions = [ |
| | | { label: '已绑定', value: 'bound' }, |
| | | { label: '未绑定', value: 'unbound' }, |
| | | ] |
| | | |
| | | const handleSearch = () => { |
| | | console.log('搜索', searchModel.value) |
| | | } |
| | | |
| | | const handleReset = () => { |
| | | searchModel.value = { |
| | | whiteListAccount: '', |
| | | source: null, |
| | | bindingStatus: null, |
| | | } |
| | | } |
| | | |
| | | const checkedRowKeys = ref<number[]>([]) |
| | | |
| | | const list = ref<User[]>([ |
| | | { id: 1, userName: '杨佩芳', whiteListAccount: '13367609906', status: 'Normal', priorityDispatch: '是', creator: '杨佩芳', createTime: '2021/07/29 10:59' }, |
| | | { id: 2, userName: '曹敏倩', whiteListAccount: '15576590476', status: 'Frozen', priorityDispatch: '是', creator: '曹敏倩', createTime: '2021/07/29 10:59' }, |
| | | { id: 3, userName: '郭贵妃', whiteListAccount: '18878751881', status: 'Normal', priorityDispatch: '是', creator: '郭贵妃', createTime: '2021/07/29 10:59' }, |
| | | { id: 4, userName: '丁汉臻', whiteListAccount: '文字内容', status: 'Frozen', priorityDispatch: '是', creator: '丁汉臻', createTime: '2021/07/29 10:59' }, |
| | | { id: 5, userName: '朱政廷', whiteListAccount: '文字内容', status: 'Normal', priorityDispatch: '否', creator: '朱政廷', createTime: '2021/07/29 10:59' }, |
| | | { id: 6, userName: '方一强', whiteListAccount: '文字内容', status: 'Normal', priorityDispatch: '否', creator: '方一强', createTime: '2021/07/29 10:59' }, |
| | | { id: 7, userName: '倪怡芳', whiteListAccount: '文字内容', status: 'Normal', priorityDispatch: '是', creator: '倪怡芳', createTime: '2021/07/29 10:59' }, |
| | | { id: 8, userName: '黄丽昆', whiteListAccount: '文字内容', status: 'Normal', priorityDispatch: '否', creator: '黄丽昆', createTime: '2021/07/29 10:59' }, |
| | | { id: 9, userName: '吴韵如', whiteListAccount: '文字内容', status: 'Frozen', priorityDispatch: '否', creator: '吴韵如', createTime: '2021/07/29 10:59' }, |
| | | { id: 10, userName: '刘翊惠', whiteListAccount: '文字内容', status: 'Frozen', priorityDispatch: '否', creator: '刘翊惠', createTime: '2021/07/29 10:59' }, |
| | | ]) |
| | | |
| | | const currentPage = ref(1) |
| | | const pageSize = ref(10) |
| | | const total = ref(1000) |
| | | |
| | | const columns: DataTableColumns<User> = [ |
| | | { type: 'selection' }, |
| | | { title: '序号', key: 'id', width: 80, align: 'center' }, |
| | | { title: '用户名称', key: 'userName', width: 150 }, |
| | | { title: '白名单账号', key: 'whiteListAccount' }, |
| | | { |
| | | title: '状态', |
| | | key: 'status', |
| | | width: 100, |
| | | render(row) { |
| | | return ( |
| | | <NTag type={row.status === 'Normal' ? 'info' : 'warning'} size="small" bordered={false} style={{ color: row.status === 'Normal' ? '#1890FF' : '#FAAD14', backgroundColor: row.status === 'Normal' ? '#E6F7FF' : '#FFFBE6' }}> |
| | | {row.status === 'Normal' ? '正常' : '冻结'} |
| | | </NTag> |
| | | ) |
| | | }, |
| | | }, |
| | | { title: '优先派单', key: 'priorityDispatch', width: 100 }, |
| | | { title: '创建人', key: 'creator', width: 120 }, |
| | | { title: '创建时间', key: 'createTime', width: 180 }, |
| | | ] |
| | | |
| | | const handleSendAll = () => { |
| | | emits('send-all') |
| | | showModal.value = false |
| | | } |
| | | |
| | | const handleSendSelected = () => { |
| | | const selectedRows = list.value.filter((item) => checkedRowKeys.value.includes(item.id)) |
| | | emits('send-selected', selectedRows) |
| | | showModal.value = false |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <n-modal |
| | | v-model:show="showModal" |
| | | style="width: 1000px" |
| | | title="选择用户" |
| | | preset="card" |
| | | size="huge" |
| | | content-style="padding: 16px 24px" |
| | | :header-style="{ borderBottom: '1px solid #efeff5', padding: '16px 24px' }" |
| | | :footer-style="{ padding: '16px 24px' }" |
| | | > |
| | | <div class="flex flex-col gap-4"> |
| | | <!-- Search Area --> |
| | | <div class="flex justify-between items-start"> |
| | | <n-form :model="searchModel" label-placement="top" inline :show-feedback="false" class="flex-1"> |
| | | <n-form-item label="白名单账号" path="whiteListAccount"> |
| | | <n-input v-model:value="searchModel.whiteListAccount" placeholder="请输入" clearable /> |
| | | </n-form-item> |
| | | <n-form-item label="来源" path="source" class="ml-4"> |
| | | <n-select |
| | | v-model:value="searchModel.source" |
| | | :options="sourceOptions" |
| | | placeholder="请搜索" |
| | | clearable |
| | | style="width: 200px" |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="绑定状态" path="bindingStatus" class="ml-4"> |
| | | <n-select |
| | | v-model:value="searchModel.bindingStatus" |
| | | :options="bindingStatusOptions" |
| | | placeholder="请搜索" |
| | | clearable |
| | | style="width: 200px" |
| | | /> |
| | | </n-form-item> |
| | | </n-form> |
| | | <div class="flex gap-2 mt-6"> |
| | | <n-button @click="handleReset">重置</n-button> |
| | | <n-button type="primary" @click="handleSearch">查询</n-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- Table Area --> |
| | | <n-data-table |
| | | v-model:checked-row-keys="checkedRowKeys" |
| | | :columns="columns" |
| | | :data="list" |
| | | :row-key="(row) => row.id" |
| | | :pagination="{ |
| | | page: currentPage, |
| | | pageSize: pageSize, |
| | | itemCount: total, |
| | | showSizePicker: true, |
| | | pageSizes: [10, 20, 50, 100], |
| | | prefix: ({ itemCount }) => `第 ${currentPage} - ${Math.min(currentPage * pageSize, itemCount)} 条 / 总共 ${itemCount} 条` |
| | | }" |
| | | /> |
| | | </div> |
| | | |
| | | <template #action> |
| | | <div class="flex justify-end gap-4 mr--4"> |
| | | <n-button type="primary" @click="handleSendAll">一键发放所有用户</n-button> |
| | | <n-button type="primary" @click="handleSendSelected">确认发送选中用户</n-button> |
| | | </div> |
| | | </template> |
| | | </n-modal> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | :deep(.n-card-header) { |
| | | font-weight: bold; |
| | | } |
| | | :deep(.n-card__footer) { |
| | | padding: 16px 24px !important; |
| | | } |
| | | </style> |
| | |
| | | import { ref, computed } from 'vue' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | import BatchSendModal from './BatchSendModal.vue' |
| | | |
| | | interface Coupon { |
| | | id: number |
| | |
| | | console.log('新增') |
| | | } |
| | | |
| | | const showBatchSendModal = ref(false) |
| | | |
| | | const handleBatchSend = () => { |
| | | console.log('批量发券') |
| | | showBatchSendModal.value = true |
| | | } |
| | | |
| | | const handleBatchSendAll = () => { |
| | | console.log('一键发放所有用户') |
| | | // Implement logic here |
| | | } |
| | | |
| | | const handleBatchSendSelected = (rows: any[]) => { |
| | | console.log('确认发送选中用户', rows) |
| | | // Implement logic here |
| | | } |
| | | |
| | | const list = ref<Coupon[]>([ |
| | |
| | | </n-card> |
| | | |
| | | <n-card style="border-radius: 6px; margin-top: 12px" content-style="padding: 12px"> |
| | | <div class="mb-4 flex justify-end gap-2"> |
| | | <div class="mb-4 flex justify-between"> |
| | | <n-button style="width: 84px; height: 32px" @click="handleBatchSend">批量发券</n-button> |
| | | <n-button style="width: 84px; height: 32px; background-color: #E6F4FF; color: #0068FF; border: 1px solid #7ABDFF" @click="handleAdd">新增</n-button> |
| | | <n-button style="width: 84px; height: 32px; background-color: #E6F4FF; color: #0068FF; border: 1px solid #7ABDFF" @click="handleAdd">添加</n-button> |
| | | </div> |
| | | |
| | | <n-data-table |
| | |
| | | :scroll-x="1000" |
| | | /> |
| | | </n-card> |
| | | |
| | | <BatchSendModal |
| | | v-model:show="showBatchSendModal" |
| | | @send-all="handleBatchSendAll" |
| | | @send-selected="handleBatchSendSelected" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| New file |
| | |
| | | operation/api/v1/banner/pageList banner分页列表 post json |
| | | 请求参数:{ |
| | | "bannerTitle": "string", 广告标题 |
| | | "pageNum": 0, |
| | | "pageSize": 0, |
| | | "status": 0 状态(0=未开始,1=进行中,2=已结束) |
| | | } |
| | | 响应: { |
| | | "code": 0, |
| | | "data": { |
| | | "current": 0, |
| | | "hasNextPage": true, |
| | | "hasPrevPage": true, |
| | | "pages": 0, |
| | | "records": [ |
| | | { |
| | | "appletId": "string", 小程序id |
| | | "appletName": "string", 小程序名称 |
| | | "bannerPictures": "string", 广告图片 |
| | | "bannerStatus": 0, 广告状态 1上架 2下架 |
| | | "bannerTitle": "string", 广告标题 |
| | | "createBy": "string", |
| | | "createTime": "2026-02-26T07:41:11.483Z", |
| | | "deleted": true, |
| | | "endTime": "2026-02-26T07:41:11.483Z", 广告结束时间 |
| | | "id": 0, |
| | | "jumpType": 0, 跳转类型1小程序内页面 2小程序地址 3网页地址 |
| | | "jumpUrl": "string", 跳转URL地址 |
| | | "startTime": "2026-02-26T07:41:11.483Z", 广告开始时间 |
| | | "status": 0, 状态(0=未开始,1=进行中,2=已结束) |
| | | "updateBy": "string", |
| | | "updateTime": "2026-02-26T07:41:11.483Z" |
| | | } |
| | | ], |
| | | "size": 0, |
| | | "startIndex": 0, |
| | | "total": 0 |
| | | }, |
| | | "msg": "string" |
| | | } |
| | | |
| | | operation/api/v1/banner/saveBanner 新增 post json |
| | | 请求:{ |
| | | "appletId": "string", |
| | | "appletName": "string", |
| | | "bannerPictures": "string", |
| | | "bannerStatus": 0, |
| | | "bannerTitle": "string", |
| | | "endTime": "2026-02-26T07:36:34.206Z", |
| | | "jumpType": 0, |
| | | "jumpUrl": "string", |
| | | "startTime": "2026-02-26T07:36:34.206Z", |
| | | "status": 0, |
| | | } |
| | | 响应:{ |
| | | "code": 0, |
| | | "data": {}, |
| | | "msg": "string" |
| | | } |
| | | |
| | | |
| | | operation/api/v1/banner/updateBanner 编辑 post json |
| | | 请求:{ |
| | | "appletId": "string", |
| | | "appletName": "string", |
| | | "bannerPictures": "string", |
| | | "bannerStatus": 0, |
| | | "bannerTitle": "string", |
| | | "endTime": "2026-02-26T07:36:34.206Z", |
| | | "jumpType": 0, |
| | | id:'', |
| | | "jumpUrl": "string", |
| | | "startTime": "2026-02-26T07:36:34.206Z", |
| | | "status": 0, |
| | | } |
| | | 响应:{ |
| | | "code": 0, |
| | | "data": {}, |
| | | "msg": "string" |
| | | } |
| | | |
| | | |
| | | operation/api/v1/banner/detailBanner 详情 get params |
| | | 请求 id |
| | | 响应:{ |
| | | "code": 0, |
| | | "data": { |
| | | "appletId": "string", |
| | | "appletName": "string", |
| | | "bannerPictures": "string", |
| | | "bannerStatus": 0, |
| | | "bannerTitle": "string", |
| | | "createBy": "string", |
| | | "createTime": "2026-02-26T07:45:33.715Z", |
| | | "deleted": true, |
| | | "endTime": "2026-02-26T07:45:33.715Z", |
| | | "id": 0, |
| | | "jumpType": 0, |
| | | "jumpUrl": "string", |
| | | "startTime": "2026-02-26T07:45:33.715Z", |
| | | "status": 0, |
| | | "updateBy": "string", |
| | | "updateTime": "2026-02-26T07:45:33.715Z" |
| | | }, |
| | | "msg": "string" |
| | | } |
| | | |
| | | operation/api/v1/banner/deleteBanner 删除 delete params |
| | | 请求:id |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | interface Props { |
| | | row: any |
| | | } |
| | | const props = defineProps<Props>() |
| | | |
| | | const emit = defineEmits<{ |
| | | (e: 'view', row: any): void |
| | | (e: 'edit', row: any): void |
| | | (e: 'delete', row: any): void |
| | | }>() |
| | | |
| | | function onView() { |
| | | emit('view', props.row) |
| | | } |
| | | function onEdit() { |
| | | emit('edit', props.row) |
| | | } |
| | | function onDelete() { |
| | | emit('delete', props.row) |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="flex gap-3"> |
| | | <span v-permission="30223" style="color: #0068FF; cursor: pointer" @click="onView"> |
| | | 查看 |
| | | </span> |
| | | <span v-permission="30224" style="color: #0068FF; cursor: pointer" @click="onEdit"> |
| | | 编辑 |
| | | </span> |
| | | <span v-permission="30225" style="color: #0068FF; cursor: pointer" @click="onDelete"> |
| | | 删除 |
| | | </span> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | </style> |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, watch } from 'vue' |
| | | import type { FormInst, FormRules, UploadFileInfo } from 'naive-ui' |
| | | import { ref, watch, computed } from 'vue' |
| | | import type { FormInst, FormRules, UploadFileInfo, UploadCustomRequestOptions } from 'naive-ui' |
| | | import { useMessage } from 'naive-ui' |
| | | import { addBanner, updateBanner, type Banner } from '@/service/api/banner' |
| | | import { customUploadRequest } from '@/utils/upload' |
| | | import dayjs from 'dayjs' |
| | | |
| | | interface Props { |
| | | show: boolean |
| | | mode?: 'add' | 'edit' | 'view' |
| | | } |
| | | |
| | | interface FormModel { |
| | | image: string |
| | | title: string |
| | | startTime: number | null |
| | | endTime: number | null |
| | | jumpType: string |
| | | miniProgramName: string |
| | | miniProgramId: string |
| | | jumpUrl: string |
| | | status: boolean |
| | | rowData?: Banner | null |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | mode: 'add' |
| | | mode: 'add', |
| | | rowData: null |
| | | }) |
| | | |
| | | const emit = defineEmits<{ |
| | | 'update:show': [value: boolean] |
| | | 'confirm': [data: FormModel] |
| | | 'confirm': [] |
| | | }>() |
| | | |
| | | const message = useMessage() |
| | | const formRef = ref<FormInst | null>(null) |
| | | const fileList = ref<UploadFileInfo[]>([]) |
| | | |
| | | interface FormModel { |
| | | bannerTitle: string |
| | | bannerPictures: string |
| | | startTime: number | null |
| | | endTime: number | null |
| | | jumpType: number |
| | | appletName: string |
| | | appletId: string |
| | | jumpUrl: string |
| | | bannerStatus: boolean |
| | | } |
| | | |
| | | const formModel = ref<FormModel>({ |
| | | image: '', |
| | | title: '', |
| | | bannerTitle: '', |
| | | bannerPictures: '', |
| | | startTime: null, |
| | | endTime: null, |
| | | jumpType: '小程序内页面', |
| | | miniProgramName: '', |
| | | miniProgramId: '', |
| | | jumpType: 1, |
| | | appletName: '', |
| | | appletId: '', |
| | | jumpUrl: '', |
| | | status: true |
| | | bannerStatus: true |
| | | }) |
| | | |
| | | const jumpTypeOptions = [ |
| | | { label: '小程序内页面', value: '小程序内页面' }, |
| | | { label: '小程序地址', value: '小程序地址' }, |
| | | { label: '网页地址', value: '网页地址' } |
| | | { label: '小程序内页面', value: 1 }, |
| | | { label: '小程序地址', value: 2 }, |
| | | { label: '网页地址', value: 3 } |
| | | ] |
| | | |
| | | const rules: FormRules = { |
| | | image: [ |
| | | bannerPictures: [ |
| | | { required: true, message: '请上传广告图', trigger: 'change' } |
| | | ], |
| | | title: [ |
| | | bannerTitle: [ |
| | | { required: true, message: '请输入广告标题', trigger: 'blur' } |
| | | ], |
| | | startTime: [ |
| | |
| | | endTime: [ |
| | | { required: true, type: 'number', message: '请选择广告结束时间', trigger: 'change' } |
| | | ], |
| | | miniProgramName: [ |
| | | { required: true, message: '请输入小程序名称', trigger: 'blur' } |
| | | jumpType: [ |
| | | { required: true, type: 'number', message: '请选择跳转类型', trigger: 'change' } |
| | | ], |
| | | miniProgramId: [ |
| | | { required: true, message: '请输入小程序ID', trigger: 'blur' } |
| | | appletName: [ |
| | | { |
| | | required: true, |
| | | validator(_rule, value) { |
| | | if (formModel.value.jumpType === 2 && !value) { |
| | | return new Error('请输入小程序名称') |
| | | } |
| | | return true |
| | | }, |
| | | trigger: 'blur' |
| | | } |
| | | ], |
| | | appletId: [ |
| | | { |
| | | required: true, |
| | | validator(_rule, value) { |
| | | if (formModel.value.jumpType === 2 && !value) { |
| | | return new Error('请输入小程序ID') |
| | | } |
| | | return true |
| | | }, |
| | | trigger: 'blur' |
| | | } |
| | | ], |
| | | jumpUrl: [ |
| | | { |
| | | required: true, |
| | | validator(_rule, value) { |
| | | if ((formModel.value.jumpType === 1 || formModel.value.jumpType === 3) && !value) { |
| | | return new Error('请输入跳转URL地址') |
| | | } |
| | | return true |
| | | }, |
| | | trigger: 'blur' |
| | | } |
| | | ] |
| | | } |
| | | |
| | | const handleUpload = (options: { file: UploadFileInfo }) => { |
| | | console.log('上传文件', options.file) |
| | | // 这里应该调用上传接口 |
| | | formModel.value.image = 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg' |
| | | return false |
| | | const handleCustomUpload = async (options: UploadCustomRequestOptions) => { |
| | | const result = await customUploadRequest(options as any) |
| | | if (result) { |
| | | // 构建 JSON 数组字符串 |
| | | const pictureData = [{ |
| | | id: result.id, |
| | | name: result.name, |
| | | url: result.url |
| | | }] |
| | | formModel.value.bannerPictures = JSON.stringify(pictureData) |
| | | // 触发校验 |
| | | formRef.value?.validate(errors => { |
| | | if (!errors) return |
| | | }, (rule) => rule?.key === 'bannerPictures') |
| | | } |
| | | } |
| | | |
| | | const handleRemove = () => { |
| | | formModel.value.image = '' |
| | | formModel.value.bannerPictures = '' |
| | | fileList.value = [] |
| | | } |
| | | |
| | |
| | | const handleConfirm = async () => { |
| | | try { |
| | | await formRef.value?.validate() |
| | | emit('confirm', formModel.value) |
| | | emit('update:show', false) |
| | | |
| | | const params = { |
| | | ...formModel.value, |
| | | bannerStatus: formModel.value.bannerStatus ? 1 : 2, |
| | | startTime: dayjs(formModel.value.startTime).format('YYYY-MM-DD HH:mm:ss'), |
| | | endTime: dayjs(formModel.value.endTime).format('YYYY-MM-DD HH:mm:ss') |
| | | } |
| | | |
| | | let error = null |
| | | let code = 0 |
| | | if (props.mode === 'add') { |
| | | const res = await addBanner(params) as any |
| | | error = res.error |
| | | code = res.code |
| | | } else { |
| | | const res = await updateBanner({ |
| | | ...params, |
| | | id: props.rowData?.id |
| | | }) as any |
| | | error = res.error |
| | | code = res.code |
| | | } |
| | | |
| | | if (!error && code === 200) { |
| | | message.success(props.mode === 'add' ? '新增成功' : '编辑成功') |
| | | emit('confirm') |
| | | emit('update:show', false) |
| | | } |
| | | } catch (error) { |
| | | console.log('验证失败', error) |
| | | } |
| | | } |
| | | |
| | | watch(() => props.show, (newVal) => { |
| | | if (!newVal) { |
| | | formRef.value?.restoreValidation() |
| | | formModel.value = { |
| | | image: '', |
| | | title: '', |
| | | startTime: null, |
| | | endTime: null, |
| | | jumpType: '小程序内页面', |
| | | miniProgramName: '', |
| | | miniProgramId: '', |
| | | jumpUrl: '', |
| | | status: true |
| | | if (newVal) { |
| | | if (props.mode !== 'add' && props.rowData) { |
| | | // 编辑/查看模式,回显数据 |
| | | const row = props.rowData |
| | | |
| | | // 解析图片 |
| | | try { |
| | | const pictures = JSON.parse(row.bannerPictures || '[]') |
| | | if (Array.isArray(pictures) && pictures.length > 0) { |
| | | fileList.value = [{ |
| | | id: pictures[0].id, |
| | | name: pictures[0].name, |
| | | status: 'finished', |
| | | url: pictures[0].url |
| | | }] |
| | | } else { |
| | | fileList.value = [] |
| | | } |
| | | } catch (e) { |
| | | fileList.value = [] |
| | | } |
| | | |
| | | formModel.value = { |
| | | bannerTitle: row.bannerTitle, |
| | | bannerPictures: row.bannerPictures, |
| | | startTime: row.startTime ? dayjs(row.startTime).valueOf() : null, |
| | | endTime: row.endTime ? dayjs(row.endTime).valueOf() : null, |
| | | jumpType: row.jumpType || 1, |
| | | appletName: row.appletName || '', |
| | | appletId: row.appletId || '', |
| | | jumpUrl: row.jumpUrl || '', |
| | | bannerStatus: row.bannerStatus === 1 |
| | | } |
| | | } else { |
| | | // 新增模式 |
| | | formModel.value = { |
| | | bannerTitle: '', |
| | | bannerPictures: '', |
| | | startTime: null, |
| | | endTime: null, |
| | | jumpType: 1, |
| | | appletName: '', |
| | | appletId: '', |
| | | jumpUrl: '', |
| | | bannerStatus: true |
| | | } |
| | | fileList.value = [] |
| | | } |
| | | fileList.value = [] |
| | | } else { |
| | | formRef.value?.restoreValidation() |
| | | } |
| | | }) |
| | | </script> |
| | |
| | | :mask-closable="false" |
| | | preset="card" |
| | | :title="mode === 'add' ? '添加广告' : mode === 'edit' ? '编辑广告' : '查看广告'" |
| | | class="banner-modal" |
| | | style="width: 600px;" |
| | | :segmented="{ |
| | | content: true, |
| | | action: true |
| | | }" |
| | | :content-style="{ |
| | | padding: '16px 24px 0px 24px', |
| | | maxHeight: '60vh', |
| | | overflow: 'auto' |
| | | }" |
| | | @close="handleClose" |
| | | > |
| | | <n-form |
| | | ref="formRef" |
| | | :model="formModel" |
| | | :rules="rules" |
| | | label-placement="left" |
| | | label-width="120" |
| | | label-placement="top" |
| | | require-mark-placement="left" |
| | | :disabled="mode === 'view'" |
| | | > |
| | | <n-form-item label="广告图" path="image" required> |
| | | <n-form-item label="广告图" path="bannerPictures" required> |
| | | <div class="flex flex-col"> |
| | | <n-upload |
| | | v-model:file-list="fileList" |
| | | :max="1" |
| | | list-type="image-card" |
| | | :custom-request="handleUpload" |
| | | :custom-request="handleCustomUpload" |
| | | :disabled="mode === 'view'" |
| | | @remove="handleRemove" |
| | | > |
| | |
| | | </div> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="广告标题" path="title" required> |
| | | <n-input v-model:value="formModel.title" placeholder="请输入" /> |
| | | <n-form-item label="广告标题" path="bannerTitle" required> |
| | | <n-input v-model:value="formModel.bannerTitle" placeholder="请输入" /> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="广告开始时间" path="startTime" required> |
| | |
| | | /> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="跳转类型"> |
| | | <n-form-item label="跳转类型" path="jumpType" required> |
| | | <n-select |
| | | v-model:value="formModel.jumpType" |
| | | :options="jumpTypeOptions" |
| | |
| | | /> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="小程序名称" path="miniProgramName" required> |
| | | <n-input v-model:value="formModel.miniProgramName" placeholder="请输入" /> |
| | | <n-form-item v-if="formModel.jumpType === 2" label="小程序名称" path="appletName" required> |
| | | <n-input v-model:value="formModel.appletName" placeholder="请输入" /> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="小程序ID" path="miniProgramId" required> |
| | | <n-input v-model:value="formModel.miniProgramId" placeholder="请输入" /> |
| | | <n-form-item v-if="formModel.jumpType === 2" label="小程序ID" path="appletId" required> |
| | | <n-input v-model:value="formModel.appletId" placeholder="请输入" /> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="跳转URL地址"> |
| | | <n-form-item v-if="formModel.jumpType === 1 || formModel.jumpType === 3" label="跳转URL地址" path="jumpUrl" required> |
| | | <n-input v-model:value="formModel.jumpUrl" placeholder="请输入" /> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="广告上架状态" required> |
| | | <n-switch v-model:value="formModel.status" /> |
| | | <n-switch v-model:value="formModel.bannerStatus" /> |
| | | </n-form-item> |
| | | </n-form> |
| | | |
| | | <template #footer> |
| | | <div class="flex justify-end gap-3"> |
| | | <n-button @click="handleClose">{{ mode === 'view' ? '关闭' : '取消' }}</n-button> |
| | | <n-button v-if="mode !== 'view'" type="primary" @click="handleConfirm">确定</n-button> |
| | | </div> |
| | | <template #action> |
| | | <n-space justify="end" :size="8"> |
| | | <n-button @click="handleClose" style="padding: 7px 29px; border-radius: 4px;"> |
| | | {{ mode === 'view' ? '关闭' : '取消' }} |
| | | </n-button> |
| | | <n-button v-if="mode !== 'view'" type="primary" @click="handleConfirm" style="padding: 7px 29px; border-radius: 4px;"> |
| | | 保存 |
| | | </n-button> |
| | | </n-space> |
| | | </template> |
| | | </n-modal> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | <style lang="scss"> |
| | | .banner-modal .n-card__content { |
| | | &::-webkit-scrollbar { |
| | | width: 6px; |
| | | height: 6px; |
| | | } |
| | | |
| | | &::-webkit-scrollbar-track { |
| | | background-color: transparent; |
| | | } |
| | | |
| | | &::-webkit-scrollbar-thumb { |
| | | background-color: transparent; |
| | | border-radius: 3px; |
| | | } |
| | | |
| | | &:hover::-webkit-scrollbar-thumb { |
| | | background-color: rgba(0, 0, 0, 0.2); |
| | | } |
| | | |
| | | &::-webkit-scrollbar-thumb:hover { |
| | | background-color: rgba(0, 0, 0, 0.4); |
| | | } |
| | | } |
| | | </style> |
| | | |
| | | <style scoped lang="scss"> |
| | | :deep(.n-form-item-label) { |
| | | padding-bottom: 8px !important; |
| | | } |
| | | |
| | | :deep(.n-card__action) { |
| | | background-color: transparent !important; |
| | | } |
| | | |
| | | :deep(.n-form-item) { |
| | | margin-bottom: 8px !important; |
| | | } |
| | | </style> |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, h } from 'vue' |
| | | <script setup lang="tsx"> |
| | | import { ref, h, onMounted } from 'vue' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NButton, NImage, NSwitch, NSpace, NTag } from 'naive-ui' |
| | | import { NImage, NSwitch, NTag, useDialog, useMessage } from 'naive-ui' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | import BannerFormModal from './components/BannerFormModal.vue' |
| | | import { fetchBannerPageList, deleteBanner, type Banner } from '@/service/api/banner' |
| | | import { useBoolean } from '@/hooks' |
| | | |
| | | interface BannerItem { |
| | | id: number |
| | | image: string |
| | | title: string |
| | | startTime: string |
| | | endTime: string |
| | | status: number |
| | | link: string |
| | | createTime: string |
| | | } |
| | | const message = useMessage() |
| | | const dialog = useDialog() |
| | | const selectedRowKeys = ref<number[]>([]) |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | |
| | | const searchForm = ref({ |
| | | title: '', |
| | | status: null as number | null |
| | | status: null as number | null, |
| | | pageNum: 1, |
| | | pageSize: 10 |
| | | }) |
| | | |
| | | const totalCount = ref(0) |
| | | const tableData = ref<Banner[]>([]) |
| | | const showFormModal = ref(false) |
| | | const formMode = ref<'add' | 'edit' | 'view'>('add') |
| | | const currentRow = ref<Banner | null>(null) |
| | | |
| | | const statusOptions = [ |
| | | { label: '全部', value: null }, |
| | |
| | | { label: '已结束', value: 2 } |
| | | ] |
| | | |
| | | const tableData = ref<BannerItem[]>([ |
| | | { |
| | | id: 1, |
| | | image: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg', |
| | | title: '会员显示消息', |
| | | startTime: '2024-05-05 12:25:33', |
| | | endTime: '2024-06-01 12:25:33', |
| | | status: 1, |
| | | link: 'www.baidu.com', |
| | | createTime: '2024-06-05 12:15:11' |
| | | }, |
| | | { |
| | | id: 2, |
| | | image: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg', |
| | | title: '共迎端午感恩更新', |
| | | startTime: '2024-05-05 12:25:33', |
| | | endTime: '2024-06-01 12:25:33', |
| | | status: 0, |
| | | link: 'www.baidu.com', |
| | | createTime: '2024-06-05 12:15:11' |
| | | }, |
| | | { |
| | | id: 3, |
| | | image: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg', |
| | | title: '长假活动', |
| | | startTime: '2024-05-05 12:25:33', |
| | | endTime: '2024-06-01 12:25:33', |
| | | status: 2, |
| | | link: 'www.baidu.com', |
| | | createTime: '2024-06-05 12:15:11' |
| | | } |
| | | ]) |
| | | |
| | | const columns: DataTableColumns<BannerItem> = [ |
| | | const columns: DataTableColumns<Banner> = [ |
| | | { |
| | | type: 'selection' |
| | | }, |
| | | { |
| | | title: '广告图', |
| | | key: 'image', |
| | | key: 'bannerPictures', |
| | | width: 150, |
| | | render: (row) => { |
| | | let url = '' |
| | | try { |
| | | const pictures = JSON.parse(row.bannerPictures) |
| | | if (Array.isArray(pictures) && pictures.length > 0) { |
| | | url = pictures[0].url |
| | | } |
| | | } catch (e) { |
| | | // console.error('解析图片失败', e) |
| | | } |
| | | |
| | | return h(NImage, { |
| | | src: row.image, |
| | | width: 120, |
| | | height: 60, |
| | | src: url, |
| | | width: 60, |
| | | height: 30, |
| | | objectFit: 'cover', |
| | | previewSrc: row.image |
| | | fallbackSrc: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg' // 默认图 |
| | | }) |
| | | } |
| | | }, |
| | | { |
| | | title: '广告标题', |
| | | key: 'title', |
| | | key: 'bannerTitle', |
| | | width: 150 |
| | | }, |
| | | { |
| | | title: '显示时间', |
| | | key: 'time', |
| | | width: 200, |
| | | width: 320, |
| | | render: (row) => { |
| | | return `${row.startTime} 至 ${row.endTime}` |
| | | } |
| | |
| | | 1: { text: '进行中', type: 'success' as const }, |
| | | 2: { text: '已结束', type: 'default' as const } |
| | | } |
| | | const statusInfo = statusMap[row.status as keyof typeof statusMap] |
| | | const statusInfo = statusMap[row.status as keyof typeof statusMap] || { text: '未知', type: 'default' } |
| | | return h(NTag, { type: statusInfo.type }, |
| | | { default: () => statusInfo.text } |
| | | ) |
| | |
| | | }, |
| | | { |
| | | title: '上架状态', |
| | | key: 'switch', |
| | | key: 'bannerStatus', |
| | | width: 100, |
| | | render: (row) => { |
| | | return h(NSwitch, { value: row.status === 1 }) |
| | | return h(NSwitch, { |
| | | value: row.bannerStatus === 1, |
| | | disabled: true // 列表页暂不支持直接切换,需在编辑中修改,或者后续添加切换接口 |
| | | }) |
| | | } |
| | | }, |
| | | { |
| | | title: '跳转类型', |
| | | key: 'jumpType', |
| | | width: 120, |
| | | render: (row) => { |
| | | const map: Record<number, string> = { |
| | | 1: '小程序内页面', |
| | | 2: '小程序地址', |
| | | 3: '网页地址' |
| | | } |
| | | return map[row.jumpType] || '未知' |
| | | } |
| | | }, |
| | | { |
| | | title: '跳转地址', |
| | | key: 'link', |
| | | width: 150 |
| | | key: 'jumpTarget', |
| | | width: 220, |
| | | ellipsis: true, |
| | | render: (row) => { |
| | | if (row.jumpType === 2) { |
| | | const name = row.appletName || '小程序' |
| | | const id = row.appletId ? `(${row.appletId})` : '' |
| | | return `${name}${id}` |
| | | } |
| | | if (row.jumpType === 1) { |
| | | return row.jumpUrl || '小程序内页面' |
| | | } |
| | | return row.jumpUrl || '-' |
| | | } |
| | | }, |
| | | { |
| | | title: '创建时间', |
| | |
| | | { |
| | | title: '操作', |
| | | key: 'actions', |
| | | align: 'center', |
| | | width: 200, |
| | | fixed: 'right', |
| | | render: (row) => { |
| | | return h(NSpace, null, { |
| | | default: () => [ |
| | | h(NButton, { text: true, type: 'primary', onClick: () => handleView(row) }, |
| | | { default: () => '查看' } |
| | | ), |
| | | h(NButton, { text: true, type: 'primary', onClick: () => handleEdit(row) }, |
| | | { default: () => '编辑' } |
| | | ), |
| | | h(NButton, { text: true, type: 'error', onClick: () => handleDelete(row) }, |
| | | { default: () => '删除' } |
| | | ) |
| | | ] |
| | | }) |
| | | } |
| | | render: (row) => ( |
| | | <div class="flex justify-center gap-3"> |
| | | <span |
| | | v-permission="30223" |
| | | style={{ color: '#0068FF', cursor: 'pointer' }} |
| | | onClick={() => handleView(row)} |
| | | > |
| | | 查看 |
| | | </span> |
| | | <span |
| | | v-permission="30224" |
| | | style={{ color: '#0068FF', cursor: 'pointer' }} |
| | | onClick={() => handleEdit(row)} |
| | | > |
| | | 编辑 |
| | | </span> |
| | | <span |
| | | v-permission="30225" |
| | | style={{ color: '#0068FF', cursor: 'pointer' }} |
| | | onClick={() => handleDelete(row)} |
| | | > |
| | | 删除 |
| | | </span> |
| | | </div> |
| | | ) |
| | | } |
| | | ] |
| | | |
| | | // 分页配置 |
| | | const pagination = computed(() => ({ |
| | | page: searchForm.value.pageNum, |
| | | pageSize: searchForm.value.pageSize, |
| | | itemCount: totalCount.value, |
| | | showSizePicker: true, |
| | | pageSizes: [10, 20, 50, 100], |
| | | prefix: ({ itemCount }) => `共 ${itemCount || 0} 条`, |
| | | onChange: (page: number) => { |
| | | searchForm.value.pageNum = page |
| | | fetchData() |
| | | }, |
| | | onUpdatePageSize: (pageSize: number) => { |
| | | searchForm.value.pageSize = pageSize |
| | | searchForm.value.pageNum = 1 |
| | | fetchData() |
| | | } |
| | | })) |
| | | |
| | | const fetchData = async () => { |
| | | startLoading() |
| | | try { |
| | | const { data, error, code } = await fetchBannerPageList({ |
| | | bannerTitle: searchForm.value.title, |
| | | status: searchForm.value.status, |
| | | pageNum: searchForm.value.pageNum, |
| | | pageSize: searchForm.value.pageSize |
| | | }) as any |
| | | if (!error && code === 200 && data) { |
| | | tableData.value = data.records || [] |
| | | totalCount.value = data.total || 0 |
| | | } |
| | | } finally { |
| | | endLoading() |
| | | } |
| | | } |
| | | |
| | | const handleSearch = () => { |
| | | console.log('搜索', searchForm.value) |
| | | searchForm.value.pageNum = 1 |
| | | fetchData() |
| | | } |
| | | |
| | | const handleReset = () => { |
| | | searchForm.value = { |
| | | title: '', |
| | | status: null |
| | | status: null, |
| | | pageNum: 1, |
| | | pageSize: 10 |
| | | } |
| | | fetchData() |
| | | } |
| | | |
| | | const handleAdd = () => { |
| | | formMode.value = 'add' |
| | | currentRow.value = null |
| | | showFormModal.value = true |
| | | } |
| | | |
| | | const handleView = (row: BannerItem) => { |
| | | const handleView = (row: Banner) => { |
| | | formMode.value = 'view' |
| | | currentRow.value = row |
| | | showFormModal.value = true |
| | | console.log('查看', row) |
| | | } |
| | | |
| | | const handleEdit = (row: BannerItem) => { |
| | | const handleEdit = (row: Banner) => { |
| | | formMode.value = 'edit' |
| | | currentRow.value = row |
| | | showFormModal.value = true |
| | | console.log('编辑', row) |
| | | } |
| | | |
| | | const handleDelete = (row: BannerItem) => { |
| | | console.log('删除', row) |
| | | const handleDelete = (row: Banner) => { |
| | | dialog.warning({ |
| | | title: '确认删除', |
| | | content: `确认删除广告 "${row.bannerTitle}" 吗?`, |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | onPositiveClick: async () => { |
| | | const { error, code } = await deleteBanner(row.id) as any |
| | | if (!error && code === 200) { |
| | | message.success('删除成功') |
| | | fetchData() |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const handleConfirmForm = (data: any) => { |
| | | console.log('保存数据', data) |
| | | window.$message?.success('保存成功') |
| | | const handleBatchDelete = () => { |
| | | if (selectedRowKeys.value.length === 0) return |
| | | |
| | | dialog.warning({ |
| | | title: '确认删除', |
| | | content: `确认删除选中的 ${selectedRowKeys.value.length} 条广告吗?`, |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | onPositiveClick: async () => { |
| | | // 并发删除 |
| | | try { |
| | | const promises = selectedRowKeys.value.map(id => deleteBanner(id)) |
| | | const results = await Promise.all(promises) as any[] |
| | | // 检查所有请求是否都成功 (code === 200) |
| | | const allSuccess = results.every((res: any) => !res.error && res.code === 200) |
| | | |
| | | if (allSuccess) { |
| | | message.success('批量删除成功') |
| | | selectedRowKeys.value = [] |
| | | fetchData() |
| | | } else { |
| | | message.error('部分删除失败,请重试') |
| | | fetchData() |
| | | } |
| | | } catch (e) { |
| | | console.error(e) |
| | | message.error('删除失败,请重试') |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const handleCheck = (rowKeys: any[]) => { |
| | | selectedRowKeys.value = rowKeys as number[] |
| | | } |
| | | |
| | | const handleConfirmForm = () => { |
| | | fetchData() |
| | | } |
| | | |
| | | onMounted(() => { |
| | | fetchData() |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="h-full flex flex-col"> |
| | | <n-card style="border-radius: 6px" content-style="padding: 12px"> |
| | | <n-form :model="searchForm" label-placement="left" inline :show-feedback="false"> |
| | | <div class="flex justify-between w-full"> |
| | | <div class="flex gap-4"> |
| | | <SearchPanel> |
| | | <n-gi> |
| | | <n-form-item label="广告标题"> |
| | | <n-input v-model:value="searchForm.title" placeholder="请输入" clearable style="width: 200px;" /> |
| | | <n-input v-model:value="searchForm.title" placeholder="请输入广告标题" clearable /> |
| | | </n-form-item> |
| | | </n-gi> |
| | | <n-gi> |
| | | <n-form-item label="状态"> |
| | | <n-select |
| | | v-model:value="searchForm.status" |
| | | :options="statusOptions as any" |
| | | placeholder="请选择" |
| | | <n-select |
| | | v-model:value="searchForm.status" |
| | | :options="statusOptions as any" |
| | | placeholder="请选择状态" |
| | | clearable |
| | | style="width: 150px;" |
| | | /> |
| | | </n-form-item> |
| | | </div> |
| | | <div class="flex gap-3"> |
| | | <n-button style="width: 84px; height: 32px" @click="handleReset">重置</n-button> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="handleSearch">查询</n-button> |
| | | </div> |
| | | </div> |
| | | </n-gi> |
| | | <template #action> |
| | | <n-button style="padding: 7px 16px; height: 32px" @click="handleReset">重置</n-button> |
| | | <n-button type="primary" style="padding: 7px 16px; height: 32px" @click="handleSearch">查询</n-button> |
| | | </template> |
| | | </SearchPanel> |
| | | </n-form> |
| | | </n-card> |
| | | |
| | | <n-card style="border-radius: 6px; margin-top: 12px;" content-style="padding: 12px"> |
| | | <div class="mb-4 flex justify-between"> |
| | | <n-button type="error" style="width: 100px; height: 32px">批量删除</n-button> |
| | | <n-button style="width: 84px; height: 32px; background-color: #E6F4FF; color: #0068FF; border: 1px solid #7ABDFF;" @click="handleAdd">添加</n-button> |
| | | <div class="mb-4 flex justify-between items-center"> |
| | | <div class="flex gap-2"> |
| | | <n-button |
| | | v-permission="30221" |
| | | :disabled="selectedRowKeys.length === 0" |
| | | style="padding: 7px 16px; height: 32px" |
| | | @click="handleBatchDelete" |
| | | > |
| | | 批量删除 |
| | | </n-button> |
| | | </div> |
| | | <div class="flex gap-2"> |
| | | <n-button |
| | | v-permission="30222" |
| | | @click="handleAdd" |
| | | style="padding: 7px 16px; height: 32px; background-color: #E6F4FF; color: #0068FF; border: 1px solid #7ABDFF;" |
| | | > |
| | | 新增广告 |
| | | </n-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <n-data-table |
| | | :columns="columns" |
| | | :data="tableData" |
| | | :loading="loading" |
| | | :row-key="(row: any) => row.id" |
| | | :scroll-x="1400" |
| | | :single-line="false" |
| | | :pagination="pagination" |
| | | @update:checked-row-keys="handleCheck" |
| | | /> |
| | | </n-card> |
| | | |
| | | <BannerFormModal v-model:show="showFormModal" :mode="formMode" @confirm="handleConfirmForm" /> |
| | | <BannerFormModal v-model:show="showFormModal" :mode="formMode" :row-data="currentRow" @confirm="handleConfirmForm" /> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | </n-tabs> |
| | | </n-card> |
| | | |
| | | <div class="flex justify-end absolute bottom-0 left-0 right-0" style="background-color: white; padding: 16px 24px; border-radius: 6px; margin-top: 12px;"> |
| | | <div v-permission="30211" class="flex justify-end absolute bottom-0 left-0 right-0" style="background-color: white; padding: 16px 24px; border-radius: 6px; margin-top: 12px;"> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="handleSave">保存</n-button> |
| | | </div> |
| | | </div> |