Commit b16e219c authored by Bill's avatar Bill

feat: 添加营销活动页

parent 0aa96a0b
......@@ -21,7 +21,7 @@
// import rfqRoute from './rfqRoute' // 询价单路由
// import rfqOfferRoute from './rfqOfferRoute' // 询价单路由
// import commentRoutes from './commentRoutes';
import contentRoute from './contentRoute'; // 内容管理
// import contentRoute from './contentRoute'; // 内容管理
// import balancedRoute from './balancedRoute'; // 平台结算管理
// import capitalAccount from './capitalAccountRoute'; // 会员资金账户
// import messageRoute from './messgeRoute'; // 消息管理
......@@ -31,8 +31,9 @@ import contentRoute from './contentRoute'; // 内容管理
// import exchangeManageRoutes from './exchangeManageRoute'; // 换货申请单管理
// import returnManageRoute from './returnManageRoute'; // 退货申请单管理
// import repairManageRoute from './repairManageRoute'; // 维修申请单管理
import purchaseBidRoute from './purchaseBidRoute'; // 采购竞价单审核
import seoSettingRoutes from './seoSettingRoutes' // seo优化
// import purchaseBidRoute from './purchaseBidRoute'; // 采购竞价单审核
// import seoSettingRoutes from './seoSettingRoutes'; // seo优化
// import marketingRoutes from './marketingRoutes'; // 营销
//@ts-ignore
import asyncRoutes from '../router.config.json'
......@@ -103,6 +104,7 @@ const router = [
// icon: 'BarChartOutlined'
// },
// ...routeList,
// marketingRoutes,
...asyncRoutes,
// purchaseBidRoute,
{
......
const marketingRoutes = {
path: '/marketing',
name: 'marketing',
routes:[
{
path: '/marketing/activitiesManagement',
name: '营销活动管理',
routes: [
/** 营销活动页管理 */
{
path: '/marketing/activitiesManagement/activePage',
name: '营销活动页管理',
component: '@/pages/marketing/marketingActivitiesManagement/activePage'
},
/** 营销活动页管理 */
{
path: '/marketing/activitiesManagement/activePage/add',
name: '营销活动页管理',
component: '@/pages/marketing/marketingActivitiesManagement/activePage/add/index',
hidePageHeader: true,
noMargin: true,
}
]
},
]
}
export default marketingRoutes
/*
* @Author: XieZhiXiong
* @Date: 2021-05-12 14:00:38
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-05-12 14:37:54
* @Description: 带描述信息的进度条
*/
import React from 'react';
import { Progress } from 'antd';
import { ProgressProps } from 'antd/lib/progress';
import styles from './index.less';
export interface DescriptionsItem {
/**
* 标题
*/
title: React.ReactNode,
/**
* 值
*/
value: React.ReactNode,
/**
* 自定义渲染
*/
customRender?: React.ReactNode,
}
interface IProps extends ProgressProps {
/**
* 描述列表
*/
descriptions: DescriptionsItem[],
}
const DescProgress: React.FC<IProps> = (props: IProps) => {
const {
descriptions,
...rest
} = props;
return (
<div className={styles['desc-progress']}>
<div className={styles['desc-progress-statistic']}>
{descriptions.map((item, index) => (
!item.customRender ? (
<div key={index} className={styles['desc-progress-statistic-item']}>
<div className={styles['desc-progress-statistic-item-title']}>
{item.title}
</div>
<div className={styles['desc-progress-statistic-item-content']}>
{item.value}
</div>
</div>
) : (
item.customRender
)
))}
</div>
<Progress strokeColor="#6B778C" showInfo={false} {...rest} />
</div>
);
};
export default DescProgress;
import React from 'react';
import { Progress } from 'antd';
import { ProgressProps } from 'antd/lib/progress';
import styles from './index.less';
export interface DescriptionsItem {
/**
* 标题
*/
title: React.ReactNode,
/**
* 值
*/
value: React.ReactNode,
/**
* 自定义渲染
*/
customRender?: React.ReactNode,
}
interface IProps extends ProgressProps {
/**
* 描述列表
*/
descriptions: DescriptionsItem[],
}
const DescProgress: React.FC<IProps> = (props: IProps) => {
const {
descriptions,
...rest
} = props;
return (
<div className={styles['desc-progress']}>
<div className={styles['desc-progress-statistic']}>
{descriptions.map((item, index) => (
!item.customRender ? (
<div key={index} className={styles['desc-progress-statistic-item']}>
<div className={styles['desc-progress-statistic-item-title']}>
{item.title}
</div>
<div className={styles['desc-progress-statistic-item-content']}>
{item.value}
</div>
</div>
) : (
item.customRender
)
))}
</div>
<Progress strokeColor="#6B778C" showInfo={false} {...rest} />
</div>
);
};
export default DescProgress;
import React, { useEffect } from 'react';
import RangeTime from './index';
import { Moment } from 'moment';
interface Iprops {
value: Moment[] | string,
editable: boolean,
ruleErrors: string[],
props: {
['x-component-props']: any,
},
mutators: {
change: (params: Moment[]) => void
},
}
const toArray = (value: string | Moment[]): Moment[] => {
if (!value) {
return [];
}
if (Array.isArray(value)) {
return value;
}
return []
}
const FormilyRangeTime: React.FC<Iprops> = (props: Iprops) => {
const { value, editable, ruleErrors } = props;
// const schemaProps = useSchemaProps()
// console.log(schemaProps);
const componentProps = props.props?.['x-component-props'] || {};
const momentValue = toArray(value)
const hasError = ruleErrors.length > 0;
const onChange = (info: Moment[]) => {
props.mutators.change(info)
}
return (
<div>
<RangeTime disabled={!editable} rangeTime={momentValue} onChange={onChange} {...componentProps} />
{
hasError && (
<p style={{marginBottom: 0, color: '#ff4d4f'}} >{ruleErrors.join(",")}</p>
)
}
</div>
)
}
const WrapFormilyRangeTime: typeof FormilyRangeTime & {
isFieldComponent?: boolean,
} = FormilyRangeTime;
WrapFormilyRangeTime.isFieldComponent = true
export default WrapFormilyRangeTime
import React from "react"
import { Tooltip } from 'antd';
import { QuestionCircleOutlined } from "@ant-design/icons"
const createRichTextUtils = () => {
return {
text(...args) {
return React.createElement('span', {}, ...args)
},
link(text: string, href: string, target: "_blank" | "_self" | "_parent" | "_top") {
return React.createElement('a', { href, target }, text)
},
gray(text: string) {
return React.createElement(
'span',
{ style: { color: 'gray', margin: '0 3px' } },
text
)
},
red(text: string) {
return React.createElement(
'span',
{ style: { color: 'red', margin: '0 3px' } },
text
)
},
help(text: string, offset = 3) {
return React.createElement(
Tooltip,
{ title: text },
<QuestionCircleOutlined
style={{ margin: '0 3px', cursor: 'default', marginLeft: offset }}
/>
)
},
tips(text: string, tips: string) {
return React.createElement(
Tooltip,
{ title: tips },
<span style={{ margin: '0 3px', cursor: 'default' }}>{text}</span>
)
}
}
}
export default createRichTextUtils
.container {
display: flex;
flex-direction: row;
align-items: center;
.wrapFlex {
flex: 1;
}
.splitChar {
text-align: center;
width: 32px;
}
}
import React, { CSSProperties, useCallback, useEffect, useState } from 'react';
import { DatePicker } from 'antd';
import cx from 'classnames';
import styles from './index.less';
import moment, { Moment } from 'moment';
interface Iprops {
containerStyle?: CSSProperties,
/**
* 默认时间
*/
rangeTime?: Moment[],
/**
* placeholader
*/
placeholader?: [string, string],
/**
* 规定起始时间是否大于当前日期
*/
shouldGtCurrent?: boolean,
onChange?: ((rangeTime: Moment[]) => void) | null,
disabled?: boolean,
showTime?: boolean,
}
const RangeTime: React.FC<Iprops> = (props: Iprops) => {
const { containerStyle, rangeTime, onChange, placeholader, shouldGtCurrent, disabled } = props;
const currentDay = moment();
const [innerRangeTime, setInnerRangeTime] = useState({
startTime: null,
endTime: null
});
useEffect(() => {
const [startTime = null, endTime = null] = rangeTime as any;
setInnerRangeTime({
startTime: startTime,
endTime: endTime,
})
}, [props.rangeTime])
const handleChange = (date: Moment | null, dateString: string, mode: "startTime" | "endTime") => {
const newObject = {
...innerRangeTime,
[mode]: date,
}
onChange?.([newObject.startTime as unknown as Moment, newObject.endTime as unknown as Moment])
setInnerRangeTime(newObject)
}
const getDisableDate = useCallback((current: Moment, mode: "startTime" | "endTime") => {
const reverseMode = mode === 'startTime' ? 'endTime' : 'startTime';
const modeTime: Moment | null = innerRangeTime[reverseMode];
// current 为当前日历上的日期, 如果返回值为true,那么表示当前日期为禁用状态
if(!modeTime) {
if(shouldGtCurrent) {
return current < currentDay.endOf('day');
}
return false
}
if (mode === 'startTime') {
return shouldGtCurrent ? (current < currentDay.endOf('day') || current > (modeTime as Moment).endOf('day')) : current > (modeTime as Moment).endOf('day')
} else {
//现在的时间要大于开始的时间, true 为禁用
return shouldGtCurrent ? (current < currentDay.endOf('day') || current < (modeTime as Moment).endOf('day')) : current < (modeTime as Moment).endOf('day')
}
}, [innerRangeTime])
return (
<div className={cx(styles.container, containerStyle)}>
<div className={styles.wrapFlex}>
<DatePicker
style={{width: '100%'}}
value={innerRangeTime.startTime} onChange={(date: Moment | null, dateString: string) => handleChange(date, dateString, "startTime")}
disabledDate={(current) => getDisableDate(current, 'startTime')}
placeholder={placeholader![0]}
disabled={disabled}
showTime
/>
</div>
<span className={styles.splitChar}>~</span>
<div className={styles.wrapFlex}>
<DatePicker
style={{width: '100%'}}
value={innerRangeTime.endTime}
onChange={(date: Moment | null, dateString: string) => handleChange(date, dateString, "endTime")}
disabledDate={(current) => getDisableDate(current, 'endTime')}
placeholder={placeholader![1]}
disabled={disabled}
showTime
/>
</div>
</div>
)
}
RangeTime.defaultProps = {
containerStyle: {},
rangeTime: [],
onChange: null,
placeholader: ["开始时间", "结束时间"],
shouldGtCurrent: true,
disabled: false,
showTime: false,
}
export default RangeTime
......@@ -112,8 +112,8 @@ export enum LAYOUT_TYPE {
}
// 本地环境跳过权限校验
// export const isDev = process.env.NODE_ENV === "development"
export const isDev = false
export const isDev = process.env.NODE_ENV === "development"
// export const isDev = false
export const STATUS_ENUM = [
{
......
......@@ -49,9 +49,9 @@ const StatisticsColumn = (props) => {
icon: totalBrand1,
},
{
count: responseData?.toBeProductValify?.count || 0,
name: responseData?.toBeProductValify?.name,
link: responseData?.toBeProductValify?.link || null,
count: responseData?.toBeBrandValify?.count || 0,
name: responseData?.toBeBrandValify?.name,
link: responseData?.toBeBrandValify?.link || null,
icon: totalBrand2
},
]
......
.card {
padding: 16px;
background-color: #fff;
border-radius: 8px;
.title {
color: '#252D37';
font-size: 14px;
margin-bottom: 16px;
font-weight: 600;
}
}
import { PageHeaderWrapper } from '@ant-design/pro-layout';
import React from 'react';
import NiceForm from '@/components/NiceForm';
import RangeTime from '@/components/RangeTime/FormilyRangeTime';
import { Button, Space } from 'antd'
import styles from './index.less';
import schema from './schema';
import ReutrnEle from '@/components/ReturnEle';
import { history } from 'umi';
import { BgColorsOutlined, SaveOutlined } from '@ant-design/icons';
import { createFormActions } from '@formily/antd';
const actions = createFormActions()
const Add = () => {
const onSubmit = (values: any) => {
}
return (
<PageHeaderWrapper
title="账户详情"
onBack={() => history.goBack()}
backIcon={<ReutrnEle />}
extra={
<Space>
<Button icon={<BgColorsOutlined />}>活动页装修</Button>
<Button icon={<SaveOutlined />} onClick={() => actions.submit()} type="primary">保存</Button>
</Space>
}
>
<div className={styles.card}>
<div className={styles.title}>基本信息</div>
<NiceForm
onSubmit={onSubmit}
schema={schema}
actions={actions}
components={{RangeTime}}
/>
</div>
</PageHeaderWrapper>
)
}
export default Add;
import { ISchema } from "@formily/antd";
const schema: ISchema = {
type: 'object',
properties: {
layout: {
type: 'object',
"x-component": 'mega-layout',
'x-component-props': {
full: true,
columns: 2,
grid: true,
labelCol: 4,
wrapperCol: 17,
},
properties: {
left: {
type: 'object',
"x-component": 'mega-layout',
"x-component-props": {
labelAlign: 'left'
},
properties: {
name: {
type: 'string',
title: '活动页名称',
required: true,
},
environment: {
type: 'string',
enum: [],
title: '活动页使用环境',
required: true,
},
template: {
type: 'string',
enum: [],
title: '活动模板',
required: true,
},
}
},
right: {
type: 'object',
"x-component": 'mega-layout',
"x-component-props": {
labelAlign: 'left'
},
properties: {
'[startTime, endTime]': {
type: 'object',
title: '活动页有效时间',
"x-component": 'RangeTime',
required: true,
},
mall: {
type: 'string',
title: '活动页适用商城',
enum: [],
required: true,
},
}
},
}
}
}
}
export default schema;
@marginBottom: 14px;
.section {
display: flex;
flex-direction: row;
padding: 16px;
background-color: #fff;
border-radius: 8px;
.image {
width: 168px;
height: 112px;
border-radius: 8px;
overflow: hidden;
background-color: red;
margin-right: 16px;
}
.infoContainer {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
flex: 1;
.info {
.header {
color: #303133;
font-size: 16px;
line-height: 16px;
margin-bottom: @marginBottom;
}
.tags {
margin-bottom: @marginBottom;
}
.mall {
margin-bottom: @marginBottom;
.label {
min-width: 72px;
display: inline-block;
}
}
.time {
.startTime {
margin-right: 40px;
}
}
}
}
.status {
margin-left: auto;
margin-right: 104px;
}
}
import StatusTag from '@/components/StatusTag';
import { PlayCircleOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import React from 'react';
import styles from './index.less';
/** partial 去掉 */
interface Iprops {
image?: string,
title?: string,
tags?: string[],
mall?: string,
startTime?: string,
endTime?: string,
}
const ActiveItem: React.FC<Iprops> = (props: Iprops) => {
return (
<div className={styles.section}>
<img className={styles.image} />
<div className={styles.infoContainer}>
<div className={styles.info}>
<div className={styles.header}>9余额平台中秋节促销活动页</div>
<div className={styles.tags}>
<StatusTag type="default" title="WEB中秋主题活动模板" />
</div>
<div className={styles.mall}>
<span className={styles.label}>适用商城:</span>
<span>灵犀超时-WEB个人商城</span>
</div>
<div className={styles.time}>
<span className={styles.startTime}>有效期开始:2020-08-25 09:00:00</span>
<span>有效期结束:2020-08-25 09:00:00</span>
</div>
</div>
<div className={styles.status}>
<StatusTag type="success" title="进行中"/>
</div>
<Button icon={<PlayCircleOutlined />}>已上线</Button>
</div>
</div>
)
}
export default ActiveItem
.container {
display: flex;
flex-direction: column;
.checkboxItem {
margin-bottom: 2px;
.checkbox {
display: flex;
flex-direction: row;
}
&:last-of-type {
margin-bottom: 0px;
}
}
}
import React from 'react';
import { Checkbox } from 'antd';
import styles from './index.less';
type CheckboxType = {
label: string,
value: number | string
extra?: number | string
}
interface Iprops {
props: {
enum: CheckboxType[]
},
mutators: {
change: (params: number[] | string[]) => void
},
value: number[] | string[]
}
const FormilyCheckBox: React.FC<Iprops> & { isFieldComponent: boolean } = (props: Iprops) => {
const { value = [], mutators } = props;
const componentProps = props.props || {};
const enumsMap: CheckboxType[] = componentProps?.enum || [];
const handleChange = (isChecked: boolean, _item: CheckboxType) => {
let newList: string[] | number[] = []
if (isChecked) {
newList = (value as string[]).concat(_item.value as string);
} else {
newList = (value as string[]).filter((_row) => _row !== _item.value)
}
mutators.change(newList)
}
return (
<div className={styles.container}>
{
enumsMap.map((_item) => {
const isChecked = (value as string[]).indexOf((_item as any).value) !== -1;
return (
<div className={styles.checkboxItem} key={_item.value}>
<Checkbox checked={isChecked} onChange={(e) => handleChange(e.target.checked, _item)}>
<div className={styles.checkbox}>
<span>{_item.label}</span>
</div>
</Checkbox>
</div>
)
})
}
</div>
)
}
FormilyCheckBox.isFieldComponent = true;
export default FormilyCheckBox
import { createFormActions, SchemaForm, LifeCycleTypes } from '@formily/antd';
import React from 'react';
import schema from './schema';
import VerticalLayout from './layout';
import FormilyCheckBox from '../FormilyCheckBox';
const actions = createFormActions()
interface Iprops {
onSubmit?: (values: any) => void,
onFormValueChange?: (values: any) => void
}
const SearchPannel: React.FC<Iprops> = (props: Iprops) => {
const { onSubmit, onFormValueChange } = props
const handleSubmit = (values: any) => {
onSubmit?.(values)
}
return (
<SchemaForm
components={{VerticalLayout, FormilyCheckBox}}
actions={actions}
schema={schema}
onSubmit={handleSubmit}
effects={($, { setFieldState }) => {
$(LifeCycleTypes.ON_FORM_INPUT_CHANGE).subscribe((state) => {
onFormValueChange?.(state.values)
})
}}
></SchemaForm>
)
}
export default SearchPannel
import React, { useMemo } from 'react';
interface Iprops {
children: React.ReactNode,
title: string,
props: {
"x-component-props": {
title: string
}
}
}
const VerticalLayout: React.FC<Iprops> & { isVirtualFieldComponent: boolean} = (props: Iprops) => {
const { children, } = props;
const xComponentProps = props.props["x-component-props"] || {};
const { title = "" } = xComponentProps;
const styles = useMemo(() => ({
display: 'flex',
flexDirection: "column",
padding: '16px 16px 0px 16px',
}), [])
return (
<div style={styles as any}>
<div style={{marginBottom: '16px', color: "#252537", fontSize: '14px', lineHeight: '14px', fontWeight: 600}}>{title}</div>
<div>
{ children }
</div>
</div>
)
}
VerticalLayout.isVirtualFieldComponent = true
export default VerticalLayout
import { ISchema } from '@formily/antd';
const schema: ISchema = {
type: 'object',
properties: {
layout: {
type: 'object',
"x-component": 'mega-layout',
"x-component-props": {
"full": true,
labelAlign: "top",
},
properties: {
statusLayout: {
type: 'object',
"x-component": 'VerticalLayout',
"x-component-props": {
title: '状态'
},
properties: {
status: {
type: 'string',
title: '',
"x-component": 'FormilyCheckBox',
enum: [
{
label: '待上线',
value: 1
},
{
label: '已上线',
value: 2
},
{
label: '进行中',
value: 3
},
{
label: '已下线',
value: 4
},
{
label: '已结束',
value: 5
},
]
}
}
},
timeLayout: {
type: 'string',
"x-component": 'VerticalLayout',
"x-component-props": {
title: '有效期'
},
properties: {
start: {
title: '开始',
type: 'date',
},
end: {
title: '结束:',
type: 'date',
}
}
},
environmentLayout: {
type: 'object',
"x-component": 'VerticalLayout',
"x-component-props": {
title: '适用环境'
},
properties: {
status: {
type: 'string',
title: '',
"x-component": 'FormilyCheckBox',
enum: [
{
label: 'WEB',
value: 1
},
{
label: 'APP',
value: 2
},
{
label: '小程序',
value: 3
},
{
label: 'H5',
value: 4
},
]
}
}
},
},
}
}
}
export default schema
@import '../../../../global/styles/index.less';
.page {
display: flex;
flex-direction: row;
.searchPannel {
width: 320px;
background-color: #fff;
border-radius: 8px;
}
.tablePanenl {
margin-left: @margin-md;
flex: 1;
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
.search {
width: 256px;
:global {
.ant-input {
padding: 7px 11px;
border: none;
&:focus {
border-color: transparent;
}
}
.ant-input-search-button {
border-left: none;
border: none;
}
}
}
}
.table {
margin-top: @margin-md;
.tableItem {
margin-bottom: @margin-sm;
&:last-of-type {
margin-bottom: 0px;
}
}
}
.footer {
margin-top: @margin-md;
.pagination {
float: right;
}
}
}
}
import React, { useEffect, useMemo, useState } from 'react';
import { Card, Input, Button, Pagination } from 'antd';
import SearchPannel from './components/SearchPannel';
import styles from './index.less';
import { PlusOutlined } from '@ant-design/icons';
import ActiveItem from './components/ActiveItem';
import { unstable_batchedUpdates } from 'react-dom';
import { useDebounce } from '@umijs/hooks';
const { Search } = Input;
type SearchParamsType = {
status: number[],
}
const ActivePage = () => {
const list = new Array(10).fill(5);
const [currentPage, setPage] = useState<number>(1);
const [currentPageSize, setPageSize] = useState<number>(10);
const [searchParams, setSearchParams] = useState<SearchParamsType | null>(null)
const cacheData = useMemo(() => ({...searchParams, current: currentPage, pageSize: currentPageSize, }), [searchParams, currentPage, currentPageSize])
const debouncedValue = useDebounce(cacheData, 1500);
const [loading, setLoading] = useState<boolean>(false);
const onPaginationChange = (page: number, pageSize: number) => {
unstable_batchedUpdates(() => {
setPage(page);
setPageSize(pageSize)
})
}
const onSearchChange = (values: SearchParamsType) => {
console.log(values);
setSearchParams(values)
}
useEffect(() => {
console.log("123123",debouncedValue);
}, [debouncedValue])
return (
<div className={styles.page}>
<div className={styles.searchPannel}>
<SearchPannel onFormValueChange={onSearchChange} />
</div>
<div className={styles.tablePanenl}>
<div className={styles.header}>
<div className={styles.search}>
<Search placeholder="搜索" />
</div>
<Button icon={<PlusOutlined />} type="primary">新增</Button>
</div>
<div className={styles.table}>
{
list.map((_item, key) => {
return (
<div className={styles.tableItem} key={key} >
<ActiveItem />
</div>
)
})
}
</div>
<div className={styles.footer}>
<div className={styles.pagination}>
<Pagination showQuickJumper total={500} pageSize={currentPageSize} current={currentPage} onChange={onPaginationChange} />
</div>
</div>
</div>
</div>
)
}
export default ActivePage;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment