feat(shop): 新增首页专区管理功能

- 在商品模型中添加所属专区字段 sectionIds,支持逗号分隔字符串
- 商品编辑界面增加所属专区多选下拉框,联动专区数据
- 新增首页专区相关接口,包括分页查询、增删改查及权限校验等
- 实现首页专区列表页面,支持专区的增删改查及状态切换
- 新增专区白名单用户管理弹窗,支持多选用户设置白名单
- 实现专区商品列表抽屉,支持分页查看专区内商品
- 首页专区编辑弹窗支持标题、副标题、banner等多字段编辑及上传功能
- 完善专区管理界面交互和表格展示,支持搜索和状态筛选功能
This commit is contained in:
2026-08-15 03:30:48 +08:00
parent 053b776d9a
commit b5c67988a6
7 changed files with 1076 additions and 0 deletions
+2
View File
@@ -151,6 +151,8 @@ export interface ShopGoods {
activityType?: number; activityType?: number;
// 配送方式:0送上门 1限自提 // 配送方式:0送上门 1限自提
deliveryMode?: number; deliveryMode?: number;
// 所属专区(逗号分隔的 sectionId 字符串,对应 shop_goods.section_ids 列)
sectionIds?: string;
} }
export interface BathSet { export interface BathSet {
+156
View File
@@ -0,0 +1,156 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type {
HomeSection,
HomeSectionParam,
SectionPermission,
SectionGoods,
SectionUser
} from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询首页专区
*/
export async function pageHomeSections(params: HomeSectionParam) {
const res = await request.get<ApiResult<PageResult<HomeSection>>>(
MODULES_API_URL + '/shop/shop-home-section/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询首页专区列表
*/
export async function listHomeSections(params?: HomeSectionParam) {
const res = await request.get<ApiResult<HomeSection[]>>(
MODULES_API_URL + '/shop/shop-home-section',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询首页专区
*/
export async function getHomeSection(id: number) {
const res = await request.get<ApiResult<HomeSection>>(
MODULES_API_URL + '/shop/shop-home-section/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加首页专区
*/
export async function addHomeSection(data: HomeSection) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-home-section',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改首页专区
*/
export async function updateHomeSection(data: HomeSection) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-home-section',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除首页专区
*/
export async function removeHomeSection(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-home-section/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询专区白名单用户
*/
export async function listSectionUsers(id: number) {
const res = await request.get<ApiResult<SectionUser[]>>(
MODULES_API_URL + '/shop/shop-home-section/' + id + '/users'
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 保存专区白名单用户(覆盖式)
*/
export async function setSectionUsers(id: number, userIds: number[]) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-home-section/' + id + '/users',
userIds
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询专区商品分页
*/
export async function listSectionGoods(
id: number,
params?: { page?: number; limit?: number }
) {
const res = await request.get<ApiResult<PageResult<SectionGoods>>>(
MODULES_API_URL + '/shop/shop-home-section/' + id + '/goods',
{
params: { page: 1, limit: 10, ...params }
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 校验专区访问权限
*/
export async function checkSectionPermission(sectionIds: number[]) {
const res = await request.post<ApiResult<SectionPermission[]>>(
MODULES_API_URL + '/shop/shop-home-section/check-permission',
{ sectionIds }
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+88
View File
@@ -0,0 +1,88 @@
import type { PageParam } from '@/api';
/**
* 首页专区
*/
export interface HomeSection {
// 专区ID
sectionId?: number;
// 租户id
tenantId?: number;
// 专区标题
title?: string;
// 专区副标题
subtitle?: string;
// banner 图片URL
banner?: string;
// 关联分类ID(逗号分隔字符串,可选)
categoryIds?: string;
// 样式类型 0默认
styleType?: number;
// 是否受限专区 0否 1是
restricted?: number;
// 排序号(数字越小越靠前)
sortNumber?: number;
// 状态 0正常 1禁用
status?: number;
// 生效开始时间
startTime?: string;
// 生效结束时间
endTime?: string;
// 用户ID
userId?: number;
// 是否删除 0否 1是
deleted?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 首页专区搜索条件
*/
export interface HomeSectionParam extends PageParam {
// 专区标题(模糊)
title?: string;
// 状态 0正常 1禁用
status?: number;
// 是否受限专区 0/1
restricted?: number;
// 排序号
sortNumber?: number;
}
/**
* 专区权限校验结果
*/
export interface SectionPermission {
// 专区ID
sectionId?: number;
// 是否受限专区
restricted?: boolean;
// 当前用户是否允许访问
allowed?: boolean;
}
/**
* 专区商品(只读展示,字段按需取用)
*/
export interface SectionGoods {
goodsId?: number;
name?: string;
image?: string;
price?: string;
salePrice?: string;
stock?: number;
status?: number;
[key: string]: any;
}
/**
* 专区白名单用户
*/
export interface SectionUser {
id?: number;
sectionId?: number;
userId?: number;
}
@@ -48,6 +48,17 @@
@change="onCategoryIds" @change="onCategoryIds"
/> />
</a-form-item> </a-form-item>
<a-form-item label="所属专区" name="sectionIds">
<a-select
v-model:value="sectionIdList"
mode="multiple"
:max-tag-count="3"
:options="zoneOptions"
style="width: 320px"
placeholder="请选择所属专区(可多选)"
allow-clear
/>
</a-form-item>
<a-form-item label="商品编码" name="code"> <a-form-item label="商品编码" name="code">
<a-input <a-input
allow-clear allow-clear
@@ -709,6 +720,7 @@ import {ShopExpressTemplate} from '@/api/shop/shopExpressTemplate/model';
import {listShopExpressTemplate} from '@/api/shop/shopExpressTemplate'; import {listShopExpressTemplate} from '@/api/shop/shopExpressTemplate';
import {listShopCommissionRole} from '@/api/shop/shopCommissionRole'; import {listShopCommissionRole} from '@/api/shop/shopCommissionRole';
import {listShopGoodsRoleCommission} from '@/api/shop/shopGoodsRoleCommission'; import {listShopGoodsRoleCommission} from '@/api/shop/shopGoodsRoleCommission';
import {listHomeSections} from '@/api/shop/shopZone';
// 是否是修改 // 是否是修改
const isUpdate = ref(false); const isUpdate = ref(false);
@@ -859,6 +871,9 @@ const skuList = ref<ShopGoodsSku[]>([]);
const files = ref<ItemType[]>([]); const files = ref<ItemType[]>([]);
const goodsSpec = ref<ShopGoodsSpec>(); const goodsSpec = ref<ShopGoodsSpec>();
const category = ref<string[]>([]); const category = ref<string[]>([]);
// 所属专区(多选)
const sectionIdList = ref<number[]>([]);
const zoneOptions = ref<{ label: string; value: number }[]>([]);
// 批量设置 // 批量设置
const batchPrice = ref<number | undefined>(undefined); const batchPrice = ref<number | undefined>(undefined);
@@ -1803,6 +1818,10 @@ const save = () => {
const formData: any = { const formData: any = {
...form, ...form,
// 所属专区:数字数组 -> 逗号分隔字符串
sectionIds: sectionIdList.value.length
? sectionIdList.value.join(',')
: '',
content: content.value, content: content.value,
category: JSON.stringify(category.value), category: JSON.stringify(category.value),
files: JSON.stringify(files.value), files: JSON.stringify(files.value),
@@ -1966,8 +1985,20 @@ watch(
async (visible) => { async (visible) => {
if (visible) { if (visible) {
await getExpressTemplateList(); await getExpressTemplateList();
// 加载所属专区选项
listHomeSections({})
.then((list) => {
zoneOptions.value = (list || []).map((z) => ({
label: z.title || '',
value: z.sectionId as number
}));
})
.catch(() => {
zoneOptions.value = [];
});
images.value = []; images.value = [];
sectionIdList.value = [];
category.value = []; category.value = [];
files.value = []; files.value = [];
videos.value = []; videos.value = [];
@@ -1976,6 +2007,13 @@ watch(
ensureTagItem.value = ''; ensureTagItem.value = '';
if (props.data) { if (props.data) {
assignObject(form, props.data); assignObject(form, props.data);
// 所属专区:逗号分隔字符串 -> 数字数组
sectionIdList.value = form.sectionIds
? form.sectionIds
.split(',')
.map((s) => Number(s.trim()))
.filter((n) => !Number.isNaN(n))
: [];
if (form.commissionType === undefined || form.commissionType === null) { if (form.commissionType === undefined || form.commissionType === null) {
form.commissionType = 10; form.commissionType = 10;
} }
@@ -0,0 +1,161 @@
<!-- 白名单用户多选弹窗 -->
<template>
<ele-modal
:width="820"
:visible="visible"
:maskClosable="false"
title="设置专区白名单用户"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="confirm"
>
<ele-pro-table
ref="tableRef"
row-key="userId"
:datasource="datasource"
:columns="columns"
:row-selection="rowSelection"
:pagination="true"
:page-size="10"
>
<template #toolbar>
<a-space>
<a-input-search
allow-clear
v-model:value="searchText"
placeholder="姓名/手机号/昵称"
style="width: 220px"
@search="reload"
@pressEnter="reload"
/>
</a-space>
</template>
</ele-pro-table>
<div class="mt-2 ele-text-secondary">
已选 {{ rowSelection.selectedRowKeys.length }}
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import {
ColumnItem,
DatasourceFunction
} from 'ele-admin-pro/es/ele-pro-table/types';
import { pageUsers } from '@/api/system/user';
import { EleProTable } from 'ele-admin-pro';
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 专区ID
sectionId?: number;
// 已选用户ID
selectedUserIds?: number[];
}>();
const emit = defineEmits<{
(e: 'confirm', userIds: number[]): void;
(e: 'update:visible', visible: boolean): void;
}>();
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 搜索内容
const searchText = ref<string>('');
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'userId',
key: 'userId',
align: 'center',
width: 80
},
{
title: '姓名',
dataIndex: 'realName',
key: 'realName',
align: 'center'
},
{
title: '手机号码',
dataIndex: 'mobile',
key: 'mobile',
align: 'center'
},
{
title: '昵称',
dataIndex: 'nickname',
key: 'nickname',
align: 'center'
},
{
title: '所属部门',
dataIndex: 'organizationName',
key: 'organizationName',
align: 'center'
}
]);
// 多选配置
const rowSelection = reactive({
type: 'checkbox' as const,
selectedRowKeys: [] as number[],
onChange: (keys: (string | number)[]) => {
rowSelection.selectedRowKeys = keys.map((k) => Number(k));
}
});
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
if (searchText.value) {
where.keywords = searchText.value;
}
return pageUsers({
isStaff: true,
keywords: searchText.value,
...where,
...orders,
page,
limit
});
};
/* 搜索 */
const reload = () => {
tableRef?.value?.reload({ page: 1 });
};
/* 确认保存 */
const confirm = () => {
emit('confirm', [...rowSelection.selectedRowKeys]);
updateVisible(false);
};
// 打开弹窗时初始化已选
watch(
() => props.visible,
(visible) => {
if (visible) {
rowSelection.selectedRowKeys = props.selectedUserIds
? [...props.selectedUserIds]
: [];
if (tableRef.value) {
// 等待表格挂载后刷新
setTimeout(() => reload(), 0);
}
}
},
{ immediate: true }
);
</script>
<style lang="less"></style>
@@ -0,0 +1,251 @@
<!-- 首页专区编辑弹窗 -->
<template>
<ele-modal
:width="640"
:visible="visible"
:maskClosable="false"
:title="isUpdate ? '编辑专区' : '添加专区'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ flex: '90px' }"
:wrapper-col="{ flex: '1' }"
>
<a-form-item label="专区标题" name="title">
<a-input
allow-clear
placeholder="请输入专区标题"
v-model:value="form.title"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="副标题" name="subtitle">
<a-input
allow-clear
placeholder="可选,专区副标题"
v-model:value="form.subtitle"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="banner图" name="banner">
<a-upload
:before-upload="handleBannerUpload"
:show-upload-list="false"
accept="image/*"
>
<a-button v-if="!form.banner">
<template #icon><UploadOutlined /></template>
上传 banner
</a-button>
<div v-else class="flex items-center gap-2">
<img :src="form.banner" style="width: 120px; height: 48px; object-fit: cover; border-radius: 4px; border: 1px solid #d9d9d9;" />
<a-button type="text" danger size="small" @click.stop="form.banner = undefined">删除</a-button>
</div>
</a-upload>
</a-form-item>
<a-form-item label="关联分类" name="categoryIds">
<a-input
allow-clear
placeholder="可选,分类ID逗号分隔,如:1,2,3"
v-model:value="form.categoryIds"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="样式类型" name="styleType">
<a-input-number
:min="0"
:max="99"
style="width: 100%"
placeholder="默认0"
v-model:value="form.styleType"
/>
</a-form-item>
<a-form-item label="是否受限" name="restricted">
<a-switch
size="small"
v-model:checked="form.restricted"
:checked-value="1"
:un-checked-value="0"
checked-children="受限专区"
un-checked-children="普通专区"
/>
</a-form-item>
<a-form-item label="排序号" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
style="width: 100%"
placeholder="数值越小越靠前"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="状态" name="status">
<a-switch
size="small"
v-model:checked="form.status"
:checked-value="0"
:un-checked-value="1"
checked-children="正常"
un-checked-children="禁用"
/>
</a-form-item>
<a-form-item label="生效开始" name="startTime">
<a-date-picker
show-time
style="width: 100%"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="可选,专区生效开始时间"
:value="form.startTime || undefined"
@update:value="(val: any) => (form.startTime = val || undefined)"
/>
</a-form-item>
<a-form-item label="生效结束" name="endTime">
<a-date-picker
show-time
style="width: 100%"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="可选,专区生效结束时间"
:value="form.endTime || undefined"
@update:value="(val: any) => (form.endTime = val || undefined)"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue';
import { UploadOutlined } from '@ant-design/icons-vue';
import { assignObject } from 'ele-admin-pro';
import { addHomeSection, updateHomeSection } from '@/api/shop/shopZone';
import { uploadFile } from '@/api/system/file';
import type { HomeSection } from '@/api/shop/shopZone/model';
import type { FormInstance } from 'ant-design-vue/es/form';
const isUpdate = ref(false);
const props = defineProps<{
visible: boolean;
data?: HomeSection | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const loading = ref(false);
const formRef = ref<FormInstance | null>(null);
const form = reactive<HomeSection>({
sectionId: undefined,
tenantId: undefined,
title: undefined,
subtitle: undefined,
banner: undefined,
categoryIds: undefined,
styleType: 0,
restricted: 0,
sortNumber: 100,
status: 0,
startTime: undefined,
endTime: undefined,
userId: undefined,
deleted: 0,
createTime: undefined,
updateTime: undefined
});
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules: Record<string, any> = {
title: [
{
required: true,
type: 'string',
message: '请输入专区标题',
trigger: 'blur'
}
]
};
const save = () => {
if (!formRef.value) return;
formRef.value
.validate()
.then(() => {
loading.value = true;
const saveOrUpdate = isUpdate.value ? updateHomeSection : addHomeSection;
saveOrUpdate({ ...form })
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
// banner 上传
const handleBannerUpload = async (file: File) => {
const isImage = file.type?.startsWith('image/');
if (!isImage) {
message.error('只能上传图片文件');
return false;
}
try {
const res = await uploadFile(file);
if (res?.path) {
form.banner = res.path;
}
} catch (e: any) {
message.error(e.message || '上传失败');
}
return false; // 阻止默认上传
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
Object.assign(form, {
sectionId: undefined,
tenantId: undefined,
title: undefined,
subtitle: undefined,
banner: undefined,
categoryIds: undefined,
styleType: 0,
restricted: 0,
sortNumber: 100,
status: 0,
startTime: undefined,
endTime: undefined
});
isUpdate.value = false;
}
} else {
formRef.value?.resetFields();
}
},
{ immediate: true }
);
</script>
+380
View File
@@ -0,0 +1,380 @@
<!-- 首页专区管理 -->
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="sectionId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<a-space :size="10" style="flex-wrap: wrap">
<a-input-search
v-model:value="where.title"
placeholder="搜索专区标题"
allow-clear
style="width: 200px"
@search="() => reload()"
@pressEnter="() => reload()"
/>
<a-select
v-model:value="where.status"
placeholder="状态"
allow-clear
style="width: 120px"
@change="() => reload()"
>
<a-select-option :value="0">正常</a-select-option>
<a-select-option :value="1">禁用</a-select-option>
</a-select>
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
<template #icon><PlusOutlined /></template>
<span>添加专区</span>
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'banner'">
<img
v-if="record.banner"
:src="getCompressedImageUrl(record.banner, { width: 96, quality: 85 })"
style="max-width: 80px; height: 32px; object-fit: cover; border-radius: 4px;"
/>
<span v-else>-</span>
</template>
<template v-if="column.key === 'restricted'">
<a-tag :color="record.restricted === 1 ? 'red' : 'default'">
{{ record.restricted === 1 ? '受限专区' : '普通专区' }}
</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-switch
:checked="record.status === 0"
checked-children="正常"
un-checked-children="禁用"
size="small"
@change="(val: boolean) => toggleStatus(record, val)"
/>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">编辑</a>
<a-divider type="vertical" />
<a @click="openUsers(record)">白名单</a>
<a-divider type="vertical" />
<a @click="openGoods(record)">商品</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此专区吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
</div>
<!-- 编辑弹窗 -->
<ZoneEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 白名单用户选择弹窗 -->
<UserSelectModal
v-model:visible="showUser"
:sectionId="currentSectionId"
:selectedUserIds="currentUserIds"
@confirm="onSaveUsers"
/>
<!-- 专区商品抽屉 -->
<a-drawer
:width="800"
:visible="showGoods"
title="专区商品"
:maskClosable="false"
@update:visible="(v: boolean) => (showGoods = v)"
>
<a-empty v-if="goodsLoading" description="加载中..." />
<template v-else>
<a-table
row-key="goodsId"
:dataSource="goodsList"
:columns="goodsColumns"
:pagination="{
total: goodsTotal,
current: goodsPage,
pageSize: 10,
onChange: (page: number) => {
goodsPage = page;
loadGoods(currentSectionId);
}
}"
:scroll="{ y: 480 }"
>
<template #bodyCell="{ column, text }">
<template v-if="column.key === 'image'">
<img
v-if="text"
:src="text"
style="width: 48px; height: 48px; object-fit: cover; border-radius: 4px;"
/>
<span v-else>-</span>
</template>
</template>
</a-table>
</template>
</a-drawer>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import ZoneEdit from './components/zoneEdit.vue';
import UserSelectModal from './components/UserSelectModal.vue';
import { getCompressedImageUrl } from '@/utils/image';
import {
pageHomeSections,
removeHomeSection,
updateHomeSection,
listSectionUsers,
setSectionUsers,
listSectionGoods
} from '@/api/shop/shopZone';
import type {
HomeSection,
HomeSectionParam
} from '@/api/shop/shopZone/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 当前编辑数据
const current = ref<HomeSection | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 白名单弹窗
const showUser = ref(false);
const currentSectionId = ref<number>(0);
const currentUserIds = ref<number[]>([]);
// 商品抽屉
const showGoods = ref(false);
const goodsLoading = ref(false);
const goodsList = ref<any[]>([]);
const goodsTotal = ref(0);
const goodsPage = ref(1);
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
return pageHomeSections({
...where,
...orders,
page,
limit
}).then((res) => {
return { list: res?.list || [], count: res?.count || 0 };
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'sectionId',
key: 'sectionId',
align: 'center',
width: 70
},
{
title: '专区标题',
dataIndex: 'title',
key: 'title',
align: 'left',
minWidth: 160
},
{
title: 'banner',
dataIndex: 'banner',
key: 'banner',
align: 'center',
width: 110
},
{
title: '是否受限',
dataIndex: 'restricted',
key: 'restricted',
align: 'center',
width: 110
},
{
title: '排序',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
width: 80,
sorter: true
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center',
width: 90
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
width: 120,
sorter: true,
customRender: ({ text }: any) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '操作',
key: 'action',
width: 220,
fixed: 'right',
align: 'center',
hideInSetting: true
}
] as unknown as ColumnItem[]);
// 商品表格列
const goodsColumns = [
{ title: 'ID', dataIndex: 'goodsId', key: 'goodsId', align: 'center', width: 70 },
{ title: '商品名称', dataIndex: 'name', key: 'name', align: 'left' },
{
title: '图片',
dataIndex: 'image',
key: 'image',
align: 'center',
width: 90
},
{ title: '商城价', dataIndex: 'price', key: 'price', align: 'center', width: 100 },
{ title: '库存', dataIndex: 'stock', key: 'stock', align: 'center', width: 90 }
] as any[];
/* 搜索条件 */
const where = ref<HomeSectionParam>({
title: '',
status: undefined,
restricted: undefined,
sortNumber: undefined
});
/* 搜索 */
const reload = (w?: HomeSectionParam) => {
if (w) {
where.value = { ...where.value, ...w };
}
tableRef?.value?.reload({ where: where.value });
};
/* 打开编辑弹窗 */
const openEdit = (row?: HomeSection) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 切换状态 */
const toggleStatus = (row: HomeSection, val: boolean) => {
const newStatus = val ? 0 : 1;
updateHomeSection({ ...row, status: newStatus })
.then((msg) => {
message.success(msg);
row.status = newStatus;
})
.catch((e) => {
message.error(e.message);
});
};
/* 删除单个 */
const remove = (row: HomeSection) => {
const hide = message.loading('请求中..', 0);
removeHomeSection(row.sectionId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 打开白名单弹窗 */
const openUsers = (row: HomeSection) => {
currentSectionId.value = row.sectionId || 0;
listSectionUsers(row.sectionId || 0)
.then((list) => {
currentUserIds.value = (list || []).map((u) => u.userId || 0).filter(Boolean);
showUser.value = true;
})
.catch((e) => {
message.error(e.message);
});
};
/* 保存白名单 */
const onSaveUsers = (userIds: number[]) => {
setSectionUsers(currentSectionId.value, userIds)
.then((msg) => {
message.success(msg || '白名单已保存');
showUser.value = false;
})
.catch((e) => {
message.error(e.message);
});
};
/* 打开商品抽屉 */
const openGoods = (row: HomeSection) => {
currentSectionId.value = row.sectionId || 0;
goodsPage.value = 1;
showGoods.value = true;
loadGoods(row.sectionId || 0);
};
const loadGoods = (sectionId: number) => {
goodsLoading.value = true;
listSectionGoods(sectionId, { page: goodsPage.value, limit: 10 })
.then((res) => {
goodsList.value = res?.list || [];
goodsTotal.value = res?.count || 0;
})
.catch((e) => {
message.error(e.message);
})
.finally(() => {
goodsLoading.value = false;
});
};
/* 行属性:双击编辑 */
const customRow = (record: HomeSection) => {
return {
onDblclick: () => {
openEdit(record);
}
};
};
</script>