提交 9e1e9c77 作者: ZhangLingKun

功能:服务列表页面

上级 c4c3c112
流水线 #7624 已通过 于阶段
in 4 分 42 秒
import { InterFunction, InterListFunction } from '@/api/interface';
// 一级行业列表
export type GetIndustryListPagesType = InterListFunction<
{
pageNo: number;
pageSize: number;
typeName?: string;
id?: number;
},
{
createTime: string;
description: string;
id: number;
inspectionDTOS: Array<{
caseImg: string;
caseVideo: string;
companyInspectionDTOS: Array<{
companyInfoId: number;
companyName: string;
detailPage: string;
id: number;
industryTypeDTO: {};
inspectionDTO: {};
inspectionFileDTOS: Array<{
companyInspectionId: number;
fileType: number;
fileUrl: string;
first: number;
id: number;
}>;
inspectionFirstImg: string;
inspectionId: number;
inspectionPriceUnitId: number;
inspectionTagDTO: {
id: number;
inspectionId: number;
tagName: string;
};
inspectionTagId: number;
price: number;
priceRemark: string;
remark: string;
saleState: number;
serviceArea: string;
}>;
id: number;
industryTypeId: number;
inspectionDescription: string;
inspectionImg: string;
inspectionName: string;
inspectionNo: string;
saleState: number;
}>;
saleState: number;
typeImg: string;
typeName: string;
}
>;
// 单位服务列表-小程序展示
export type GetListAPPCompanyInspectionPageType = InterListFunction<
{
companyInfoId?: number;
industryTypeId?: number;
inspectionId?: number;
inspectionTagId?: number;
keyword?: string;
},
{
id: number;
companyInfoId: number;
serviceArea: string;
inspectionId: number;
inspectionTagId: null;
price: number;
priceRemark: string;
inspectionPriceUnitId: number;
detailPage: string;
saleState: number;
remark: string;
inspectionFirstImg: string;
industryTypeDTO: {
id: number;
typeName: string;
typeImg: null;
description: null;
saleState: null;
createTime: null;
inspectionDTOS: null;
};
inspectionDTO: {
id: number;
inspectionNo: string;
inspectionName: string;
industryTypeId: null;
inspectionImg: null;
inspectionDescription: null;
saleState: null;
caseImg: null;
caseVideo: null;
createTime: null;
companyInspectionDTOS: null;
};
inspectionTagDTO: {
id: null;
tagName: null;
inspectionId: number;
};
inspectionFileDTOS: Array<{
id: number;
fileType: number;
first: number;
companyInspectionId: number;
fileUrl: string;
}>;
companyName: string;
}
>;
// 价格单位列表
export type ListInspectionPriceUnit = InterFunction<
{},
{
id: number;
unitName: string;
}[]
>;
import {
GetIndustryListPagesType,
GetListAPPCompanyInspectionPageType,
ListInspectionPriceUnit,
} from '@/api/interface/service';
import request from '@/api/request';
export class ServiceAPI {
// 一级行业列表
static getIndustryListPages: GetIndustryListPagesType = (params) =>
request.post('/pms/industry/listPages', params);
// 单位服务列表-小程序展示
static getListAPPCompanyInspectionPage: GetListAPPCompanyInspectionPageType =
(params) =>
request.post(
'/pms/company-inspection/listAPPCompanyInspectionPage',
params,
);
// 价格单位列表
static listInspectionPriceUnit: ListInspectionPriceUnit = (params) =>
request.get('/pms/company-inspection/listInspectionPriceUnit', params);
}
......@@ -3,69 +3,6 @@ import { DownOutlined, UpOutlined } from '@ant-design/icons';
import { useRouter } from 'next/router';
import styled from 'styled-components';
const CategorySelectWrap = styled.div`
position: relative;
width: 100%;
min-height: 2rem;
border: 1px solid #ebebeb;
margin-bottom: 1rem;
.category-select {
position: relative;
width: 100%;
min-height: 2rem;
flex-wrap: wrap;
border-bottom: 1px solid #ebebeb;
.select-item {
position: relative;
box-sizing: border-box;
color: #333333;
padding: 0 1.25rem;
min-height: 2rem;
line-height: 2rem;
display: flex;
align-items: center;
justify-content: center;
&:first-child {
color: #999999;
}
&:not(:first-child):active,
&:not(:first-child):hover {
background: #ececec;
}
&:not(:first-child) {
cursor: pointer;
}
.text {
margin-right: 0.25rem;
}
}
.item-active {
background: #ececec;
}
}
.category-list {
position: relative;
width: 100%;
min-height: 3rem;
flex-wrap: wrap;
box-sizing: border-box;
padding: 0 0.5rem;
.list-item {
position: relative;
min-height: 1.68rem;
line-height: 1.68rem;
padding: 0 0.5rem;
cursor: pointer;
margin-right: 0.5rem;
}
.item-active {
color: #fff;
background: #ff552d;
border-radius: 4px;
}
}
`;
// 分类列表类型
export type CategoryType = {
value: number;
......@@ -77,7 +14,11 @@ const CategorySelectView: React.FC<{
list: CategoryType;
onMain?: (arr: number) => void;
onSecond?: (arr: number[]) => void;
}> = ({ list, onMain, onSecond }) => {
onSelect?: ({ main, second }: { main?: number; second?: number[] }) => void;
isMultiple?: boolean;
allText?: string;
}> = ({ list, onMain, onSecond, onSelect, allText, isMultiple }) => {
CategorySelectView.defaultProps = { allText: '全部商品', isMultiple: true };
// 图标样式
const IconStyle = {
fontSize: '8px',
......@@ -96,12 +37,20 @@ const CategorySelectView: React.FC<{
const arr = has
? secondIndex?.filter((i) => i !== index)
: [...secondIndex, index];
setSecondIndex(arr);
onSecond?.(
arr?.length !== 0
? arr?.map((i) => list[currentIndex]?.children?.[i]?.value || 0)
: list[currentIndex]?.children?.map((i) => i?.value) || [],
);
const brr = isMultiple ? arr : [index];
setSecondIndex(brr);
// 二级筛选列表
const second =
brr?.length !== 0
? brr?.map((i) => list[currentIndex]?.children?.[i]?.value || 0)
: list[currentIndex]?.children?.map((i) => i?.value) || [];
// 二级筛选回调
onSecond?.(second);
// 统一返回筛选
onSelect?.({
main: list[currentIndex]?.value,
second,
});
};
// 主索引选择
const handleMain = (index: number) => {
......@@ -111,11 +60,21 @@ const CategorySelectView: React.FC<{
onMain?.(list[index]?.value);
// 清空二级分类,并返回全部二级分类
onSecond?.(list[index]?.children?.map((i) => i?.value) || []);
// 统一返回筛选
onSelect?.({
main: list[index]?.value,
second: list[index]?.children?.map((i) => i?.value) || [],
});
};
// 选择全部商品
const handleAll = () => {
setSecondIndex([]);
onSecond?.(list[currentIndex]?.children?.map((i) => i?.value) || []);
// 统一返回筛选
onSelect?.({
main: list[currentIndex]?.value,
second: list[currentIndex]?.children?.map((i) => i?.value) || [],
});
};
// 组件挂载
useEffect(() => {
......@@ -139,12 +98,20 @@ const CategorySelectView: React.FC<{
setSecondIndex([index]);
onSecond?.(children?.[index]?.value ? [children?.[index]?.value] : []);
}
// 如果任何id都没有
if (!router?.query?.main && !router?.query?.second) {
// 初始化返回一级分类
onMain?.(list[currentIndex]?.value);
// 初始化全部二级分类
onSecond?.(list[currentIndex]?.children?.map((i) => i?.value) || []);
}
// 统一返回筛选
onSelect?.({
main: mainID || list[currentIndex]?.value,
second: secondID
? [secondID]
: list[currentIndex]?.children?.map((i) => i?.value) || [],
});
}, [list, router]);
return (
<CategorySelectWrap>
......@@ -170,7 +137,7 @@ const CategorySelectView: React.FC<{
className={`list-item ${secondIndex?.length === 0 && 'item-active'}`}
onClick={handleAll}
>
全部商品
{allText || '全部商品'}
</div>
{list[currentIndex]?.children?.map((i, j) => (
<div
......@@ -187,3 +154,66 @@ const CategorySelectView: React.FC<{
};
export default CategorySelectView;
const CategorySelectWrap = styled.div`
position: relative;
width: 100%;
min-height: 2rem;
border: 1px solid #ebebeb;
margin-bottom: 1rem;
.category-select {
position: relative;
width: 100%;
min-height: 2rem;
flex-wrap: wrap;
border-bottom: 1px solid #ebebeb;
.select-item {
position: relative;
box-sizing: border-box;
color: #333333;
padding: 0 1.25rem;
min-height: 2rem;
line-height: 2rem;
display: flex;
align-items: center;
justify-content: center;
&:first-child {
color: #999999;
}
&:not(:first-child):active,
&:not(:first-child):hover {
background: #ececec;
}
&:not(:first-child) {
cursor: pointer;
}
.text {
margin-right: 0.25rem;
}
}
.item-active {
background: #ececec;
}
}
.category-list {
position: relative;
width: 100%;
min-height: 3rem;
flex-wrap: wrap;
box-sizing: border-box;
padding: 0 0.5rem;
.list-item {
position: relative;
min-height: 1.68rem;
line-height: 1.68rem;
padding: 0 0.5rem;
cursor: pointer;
margin: 0.25rem 0.5rem 0.25rem 0;
}
.item-active {
color: #fff;
background: #ff552d;
border-radius: 4px;
}
}
`;
......@@ -38,6 +38,8 @@ export const HomeTitleWrap = styled.div`
background-size: cover;
background-position: center;
z-index: 1;
cursor: pointer;
user-select: none;
&::after {
position: absolute;
content: '';
......
......@@ -48,7 +48,11 @@ const MallView: React.FC<{
const res = await MallAPI.queryGoodsInfoByCategorySub(list);
if (res && res.code === '200') {
setGoodsInfoList(res.result || []);
setPagination({ ...pagination, totalCount: res.result?.length || 0 });
setPagination({
...pagination,
pageNo: 1,
totalCount: res.result?.length || 0,
});
// console.log('商品列表 --->', res.result);
}
};
......
......@@ -147,7 +147,9 @@ const ProductItemView: React.FC<{
</div>
<div className="product-store flex-start">
<PropertySafetyFilled style={{ color: '#FF552D' }} />
<div className="title text-ellipsis">{detail?.companyName}</div>
<div className="title text-ellipsis" title={detail?.companyName}>
{detail?.companyName}
</div>
</div>
<div className="product-cart">
<ShoppingCartOutlined style={{ color: '#ffffff', fontSize: '16px' }} />
......
import React, { useEffect, useState } from 'react';
import { Empty } from 'antd';
import styled from 'styled-components';
import { InterListType, InterReqType } from '@/api/interface';
import {
GetIndustryListPagesType,
GetListAPPCompanyInspectionPageType,
} from '@/api/interface/service';
import { ServiceAPI } from '@/api/modules/service';
import BreadcrumbView from '@/components/breadcrumb';
import CategorySelectView, { CategoryType } from '@/components/categorySelect';
import LayoutView from '@/components/layout';
import ProductListView from '@/components/productList';
import ServiceItemView from '@/components/serviceItem';
// 分类列表类型
type CategoryListType = InterListType<GetIndustryListPagesType>;
// 请求参数类型
type ReqType = InterReqType<GetListAPPCompanyInspectionPageType>;
// 服务列表
type ListType = InterListType<GetListAPPCompanyInspectionPageType>;
const ServiceView: React.FC<{
categoryList: CategoryListType;
}> = (props) => {
// 分类列表
const [categoryList, setCategoryList] = useState<CategoryType>([]);
// 转换分类列表
const getCategoryList = () => {
setCategoryList(
props?.categoryList
?.filter((i) => i?.inspectionDTOS?.length)
?.map((i) => ({
value: i.id,
label: i.typeName,
children: i?.inspectionDTOS?.map((n) => ({
value: n.id,
label: n.inspectionName,
})),
})),
);
};
// 分页数据
const [pagination, setPagination] = useState({
pageNo: 1,
pageSize: 18,
totalCount: 0,
});
// 服务列表
const [inspectionList, setInspectionList] = useState<ListType>([]);
// 获取服务列表
const getListAPPCompanyInspectionPage = async (data: ReqType) => {
const res = await ServiceAPI.getListAPPCompanyInspectionPage({
pageNo: pagination.pageNo,
pageSize: pagination.pageSize,
...data,
});
if (res && res.code === '200') {
const { list, totalCount, pageNo, pageSize } = res.result;
setInspectionList(list || []);
setPagination({
...pagination,
totalCount,
pageNo,
pageSize,
});
}
};
// 分类筛选
const handleSelect = async (e: { main?: number; second?: number[] }) => {
if (e.second?.length && e.second?.length > 1) {
await getListAPPCompanyInspectionPage({ industryTypeId: e?.main });
} else {
await getListAPPCompanyInspectionPage({ inspectionId: e?.second?.[0] });
}
};
// 翻页回调
const handlePageChange = async (pageNo: number, pageSize: number) => {
await getListAPPCompanyInspectionPage({ pageNo, pageSize });
};
// 组件挂载
useEffect(() => {
if (!props) return;
getCategoryList();
}, [props]);
return (
<LayoutView>
<ServiceWrap>
{/* 面包屑 */}
<BreadcrumbView />
{/* 类型筛选 */}
<CategorySelectView
allText="全部服务"
list={categoryList}
isMultiple={false}
onSelect={handleSelect}
/>
{/* 产品列表 */}
<ProductListView pagination={pagination} onChange={handlePageChange}>
{inspectionList?.length ? (
inspectionList?.map((i, j) => (
<ServiceItemView key={j} detail={i} />
))
) : (
<div className="list-empty flex-center">
<Empty />
</div>
)}
</ProductListView>
</ServiceWrap>
</LayoutView>
);
};
export default ServiceView;
export const ServiceWrap = styled.div`
position: relative;
max-width: 1190px;
box-sizing: border-box;
padding: 2rem 0 0 0;
margin: 0 auto;
`;
import React from 'react';
import { PropertySafetyFilled, ShoppingCartOutlined } from '@ant-design/icons';
import { useRouter } from 'next/router';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { InterListType } from '@/api/interface';
import { GetListAPPCompanyInspectionPageType } from '@/api/interface/service';
import { GlobalDataState } from '@/store/module/globalData';
import { formatMoney } from '@/utils/money';
// 商品详情类型
type GoodsInfoListType = InterListType<GetListAPPCompanyInspectionPageType>[0];
const ServiceItemView: React.FC<{
detail: GoodsInfoListType;
}> = ({ detail }) => {
// 路由钩子
const router = useRouter();
// globalData
const globalData = useSelector(
(state: any) => state.globalData,
) as GlobalDataState;
// 跳转商品详情
const handleDetail = () => {
router.push(`/service/detail/${detail?.id}`).then();
};
// 获取商品的单位
const getPriceUnit = () => {
const unit = globalData?.priceUnitList?.find(
(i) => i.id === detail?.inspectionPriceUnitId,
);
return unit?.unitName || '次';
};
return (
<ProductItemWrap onClick={handleDetail}>
<div className="product-image">
<img
className="image"
src={`${detail?.inspectionFirstImg}?x-oss-process=image/quality,q_10`}
alt="图片"
/>
</div>
<div className="product-title">
【{detail?.industryTypeDTO?.typeName}】
{detail?.inspectionDTO?.inspectionName}
</div>
<div className="product-desc flex-between">
<div className="money">
{detail?.price ? (
<>
<span className="label">¥</span>
<span
className="num"
title={`${formatMoney(detail?.price)}元起每${getPriceUnit()}`}
>
{formatMoney(detail?.price)}
</span>
<div
className="unit text-ellipsis"
title={`${formatMoney(detail?.price)}元起每${getPriceUnit()}`}
>
起/{getPriceUnit()}
</div>
</>
) : (
<span className="label">咨询报价</span>
)}
</div>
<div className="text text-ellipsis">
成交{Math.floor(detail.id * 3.14)}件
</div>
</div>
<div className="product-store flex-start">
<PropertySafetyFilled style={{ color: '#FF552D' }} />
<div className="title text-ellipsis" title={detail?.companyName}>
{detail?.companyName}
</div>
</div>
<div className="product-cart">
<ShoppingCartOutlined style={{ color: '#ffffff', fontSize: '16px' }} />
</div>
</ProductItemWrap>
);
};
export default ServiceItemView;
const ProductItemWrap = styled.div`
position: relative;
box-sizing: border-box;
width: calc((100% - (0.83rem * 5)) / 6);
height: 16rem;
border: 0.02rem solid #e3e3e3;
margin: 0 0.83rem 0.83rem 0;
padding: 0.67rem;
cursor: pointer;
&:nth-child(6n) {
margin-right: 0;
}
&:active,
&:hover {
background: #fff9f6;
border: 0.04rem solid #ff7a3e;
}
.product-image {
position: relative;
width: 100%;
height: 8rem;
box-sizing: border-box;
margin-bottom: 0.5rem;
.image {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.product-title {
width: 100%;
font-size: 13px;
font-weight: 500;
color: #333333;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
margin-bottom: 0.33rem;
min-height: 2rem;
}
.product-desc {
position: relative;
width: 100%;
font-size: 12px;
font-weight: 400;
color: #999999;
margin-bottom: 0.1rem;
align-items: baseline;
.label {
width: 60%;
}
.text {
width: 40%;
text-align: right;
}
.money {
font-size: 16px;
font-weight: 500;
color: #ff1b1b;
.label {
font-size: 12px;
font-weight: bold;
}
.num {
max-width: 60%;
}
.unit {
margin-left: 0.68rem;
font-size: 10px;
font-weight: 400;
color: #999999;
}
}
}
.product-store {
position: absolute;
left: 0.67rem;
bottom: 0.67rem;
.title {
width: 6rem;
font-size: 12px;
font-weight: 400;
color: #666666;
margin-left: 0.25rem;
}
}
.product-cart {
position: absolute;
right: 0.67rem;
bottom: 0.67rem;
width: 2rem;
height: 2rem;
background: #ff8b2e;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
&:active,
&:hover {
filter: brightness(0.9);
}
}
`;
......@@ -9,8 +9,8 @@ import { PersistGate } from 'redux-persist/integration/react';
import { wrapper } from '@/store';
import themeConfig from '../theme/themeConfig';
const App = ({ Component, pageProps, ...rest }: AppProps) => {
const { store } = wrapper.useWrappedStore(rest);
const App = ({ Component, ...rest }: AppProps) => {
const { store, props } = wrapper.useWrappedStore(rest);
return (
<Provider store={store}>
......@@ -23,7 +23,7 @@ const App = ({ Component, pageProps, ...rest }: AppProps) => {
...themeConfig,
}}
>
<Component {...pageProps} />
<Component {...props.pageProps} />
</ConfigProvider>
</PersistGate>
</Provider>
......
......@@ -46,7 +46,7 @@ const HomeView = () => {
{/* 推荐商品 */}
<HomeProductView />
{/* 无人机服务 */}
<HomeTitleView title="无人机服务" />
<HomeTitleView title="无人机服务" path="/service" />
{/* 服务列表 */}
<HomeServiceView />
{/* 底部标签 */}
......
......@@ -28,6 +28,6 @@ export async function getServerSideProps() {
}
const MallPageView: React.FC<{
categoryList: CategoryListType;
}> = (props) => MallView(props);
}> = (props) => <MallView {...props} />;
export default MallPageView;
import React from 'react';
import { InterDataType, InterListType } from '@/api/interface';
import {
GetIndustryListPagesType,
ListInspectionPriceUnit,
} from '@/api/interface/service';
import { ServiceAPI } from '@/api/modules/service';
import ServiceView from '@/components/service-comp';
import { wrapper } from '@/store';
import { setGlobalData } from '@/store/module/globalData';
// 分类列表类型
type CategoryListType = InterListType<GetIndustryListPagesType>;
// 单位列表类型
type PriceUnitListType = InterDataType<ListInspectionPriceUnit>;
// 使用wrapper.getServerSideProps高阶函数包裹getServerSideProps函数
export const getServerSideProps = wrapper.getServerSideProps(
(store) => async () => {
// 分类数据
let categoryList: CategoryListType = [];
// 获取各个目录及分类信息
const getIndustryListPages = async () => {
const res = await ServiceAPI.getIndustryListPages({
pageNo: 1,
pageSize: 999,
});
if (res && res.code === '200') {
categoryList = res?.result?.list || [];
}
};
// 获取价格单位列表
const getPriceUnitList = async () => {
const res = await ServiceAPI.listInspectionPriceUnit();
if (res && res.code === '200') {
store.dispatch(setGlobalData({ priceUnitList: res?.result || [] }));
}
};
// 依次获取接口数据
await (async () => {
await getIndustryListPages();
await getPriceUnitList();
})();
return { props: { categoryList } };
},
);
const ServicePageView: React.FC<{
categoryList: CategoryListType;
}> = (props) => <ServiceView {...props} />;
export default ServicePageView;
......@@ -2,6 +2,7 @@
import { createSlice } from '@reduxjs/toolkit';
import { HYDRATE } from 'next-redux-wrapper';
import { InterDataType } from '@/api/interface';
import { ListInspectionPriceUnit } from '@/api/interface/service';
import { UserAddressSelectList } from '@/api/interface/user';
export type GlobalDataState = {
......@@ -12,16 +13,12 @@ export type GlobalDataState = {
userAddressList?: InterDataType<UserAddressSelectList>;
userAddressSelectId?: number;
productSpecNum?: number;
priceUnitList?: InterDataType<ListInspectionPriceUnit>;
};
const initialState: GlobalDataState = {
loginModalVisible: false,
qrcodeModalVisible: false,
qrcodeModalPath: undefined,
qrcodeModalScene: undefined,
userAddressList: undefined,
userAddressSelectId: undefined,
productSpecNum: undefined,
};
const globalDataSlice = createSlice({
......
......@@ -6,22 +6,21 @@ export type SystemState = {
token?: string;
};
const initialState: SystemState = {
token: undefined,
};
const initialState: SystemState = {};
const systemSlice = createSlice({
name: 'system',
initialState,
reducers: {
setSystem: (state, action) => {
return action.payload;
return { ...state, ...action.payload };
},
},
extraReducers: {
// hydrated 用于获取服务端注入的state并选择更新
[HYDRATE]: (state, action) => {
return {
...state,
...action.payload.system,
};
},
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论