feat(shop): 新增首页专区管理功能
- 在商品模型中添加所属专区字段 sectionIds,支持逗号分隔字符串 - 商品编辑界面增加所属专区多选下拉框,联动专区数据 - 新增首页专区相关接口,包括分页查询、增删改查及权限校验等 - 实现首页专区列表页面,支持专区的增删改查及状态切换 - 新增专区白名单用户管理弹窗,支持多选用户设置白名单 - 实现专区商品列表抽屉,支持分页查看专区内商品 - 首页专区编辑弹窗支持标题、副标题、banner等多字段编辑及上传功能 - 完善专区管理界面交互和表格展示,支持搜索和状态筛选功能
This commit is contained in:
@@ -151,6 +151,8 @@ export interface ShopGoods {
|
||||
activityType?: number;
|
||||
// 配送方式:0送上门 1限自提
|
||||
deliveryMode?: number;
|
||||
// 所属专区(逗号分隔的 sectionId 字符串,对应 shop_goods.section_ids 列)
|
||||
sectionIds?: string;
|
||||
}
|
||||
|
||||
export interface BathSet {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
</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-input
|
||||
allow-clear
|
||||
@@ -709,6 +720,7 @@ import {ShopExpressTemplate} from '@/api/shop/shopExpressTemplate/model';
|
||||
import {listShopExpressTemplate} from '@/api/shop/shopExpressTemplate';
|
||||
import {listShopCommissionRole} from '@/api/shop/shopCommissionRole';
|
||||
import {listShopGoodsRoleCommission} from '@/api/shop/shopGoodsRoleCommission';
|
||||
import {listHomeSections} from '@/api/shop/shopZone';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
@@ -859,6 +871,9 @@ const skuList = ref<ShopGoodsSku[]>([]);
|
||||
const files = ref<ItemType[]>([]);
|
||||
const goodsSpec = ref<ShopGoodsSpec>();
|
||||
const category = ref<string[]>([]);
|
||||
// 所属专区(多选)
|
||||
const sectionIdList = ref<number[]>([]);
|
||||
const zoneOptions = ref<{ label: string; value: number }[]>([]);
|
||||
|
||||
// 批量设置
|
||||
const batchPrice = ref<number | undefined>(undefined);
|
||||
@@ -1803,6 +1818,10 @@ const save = () => {
|
||||
|
||||
const formData: any = {
|
||||
...form,
|
||||
// 所属专区:数字数组 -> 逗号分隔字符串
|
||||
sectionIds: sectionIdList.value.length
|
||||
? sectionIdList.value.join(',')
|
||||
: '',
|
||||
content: content.value,
|
||||
category: JSON.stringify(category.value),
|
||||
files: JSON.stringify(files.value),
|
||||
@@ -1966,8 +1985,20 @@ watch(
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
await getExpressTemplateList();
|
||||
// 加载所属专区选项
|
||||
listHomeSections({})
|
||||
.then((list) => {
|
||||
zoneOptions.value = (list || []).map((z) => ({
|
||||
label: z.title || '',
|
||||
value: z.sectionId as number
|
||||
}));
|
||||
})
|
||||
.catch(() => {
|
||||
zoneOptions.value = [];
|
||||
});
|
||||
|
||||
images.value = [];
|
||||
sectionIdList.value = [];
|
||||
category.value = [];
|
||||
files.value = [];
|
||||
videos.value = [];
|
||||
@@ -1976,6 +2007,13 @@ watch(
|
||||
ensureTagItem.value = '';
|
||||
if (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) {
|
||||
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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user