diff --git a/project.private.config.json b/project.private.config.json index ce8d630..a7287ba 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -9,6 +9,13 @@ "condition": { "miniprogram": { "list": [ + { + "name": "邀请码", + "pathName": "/pages/inviteCode/index", + "query": "", + "launchMode": "default", + "scene": null + }, { "name": "销售统计", "pathName": "pages/saleStatistic/index", diff --git a/src/api/index.ts b/src/api/index.ts index fc037d6..336eeb4 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -35,8 +35,6 @@ export { TakeGoodsOrderRefuse, TakeGoodsOrderAudit, TakeGoodsOrder, - UserInvitationInfoRecord, - GenBarCodeOrQrCode, } from './takeDelivery/index' // 关于销售统计 @@ -50,6 +48,11 @@ export { SaleOrderDataFormdataFormStatus, SaleOrderDataFormApi, } from './statistic/index' +// 邀请码 +export { + GetInvitationInfo, + GenBarCodeOrQrCode, +} from './inviteCode/index' /** * 系列列表 * @returns diff --git a/src/api/inviteCode/index.ts b/src/api/inviteCode/index.ts new file mode 100644 index 0000000..1c433bc --- /dev/null +++ b/src/api/inviteCode/index.ts @@ -0,0 +1,18 @@ +import { CAP_HTML_TO_IMAGE_BASE_URL } from '@/common/constant' +import { useRequest } from '@/use/useHttp' + +// 邀请码 +export const GetInvitationInfo = () => { + return useRequest({ + url: '/v1/mp/user/invitationInfoRecord', + method: 'get', + }) +} +// 生成二维码 +export const GenBarCodeOrQrCode = () => { + return useRequest({ + url: '/xima-caphtml/genBarcodeOrQrCode', + base_url: CAP_HTML_TO_IMAGE_BASE_URL, + method: 'post', + }) +} diff --git a/src/api/takeDelivery/index.ts b/src/api/takeDelivery/index.ts index 620177a..f5d1041 100644 --- a/src/api/takeDelivery/index.ts +++ b/src/api/takeDelivery/index.ts @@ -1,2 +1,2 @@ export { EnumTakeGoodsOrderStatus, EnumTakeGoodsOrderTypeList } from './enum' -export { TakeGoodsOrderList, TakeGoodsOrderRefuse, TakeGoodsOrderAudit, TakeGoodsOrder, UserInvitationInfoRecord, GenBarCodeOrQrCode } from './takeDelivery' +export { TakeGoodsOrderList, TakeGoodsOrderRefuse, TakeGoodsOrderAudit, TakeGoodsOrder } from './takeDelivery' diff --git a/src/api/takeDelivery/takeDelivery.ts b/src/api/takeDelivery/takeDelivery.ts index 8fc336c..371ba31 100644 --- a/src/api/takeDelivery/takeDelivery.ts +++ b/src/api/takeDelivery/takeDelivery.ts @@ -28,18 +28,3 @@ export const TakeGoodsOrder = () => { method: 'get', }) } -// 邀请码 -export const UserInvitationInfoRecord = () => { - return useRequest({ - url: '/v2/mp/user/invitationInfoRecord', - method: 'get', - }) -} -// 生成二维码 -export const GenBarCodeOrQrCode = () => { - return useRequest({ - url: '/xima-caphtml/genBarcodeOrQrCode', - base_url: CAP_HTML_TO_IMAGE_BASE_URL, - method: 'post', - }) -} diff --git a/src/app.config.ts b/src/app.config.ts index d9ac501..6e48a01 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -36,6 +36,10 @@ export default defineAppConfig({ 'custom-wrapper': '/custom-wrapper', }, subPackages: [ + { + root: 'pages/inviteCode', + pages: ['index'], + }, { root: 'pages/saleStatistic', pages: ['index'], diff --git a/src/common/constant.ts b/src/common/constant.ts index da5ee1c..b0944b2 100644 --- a/src/common/constant.ts +++ b/src/common/constant.ts @@ -39,7 +39,10 @@ export const LIST_EMPTY_IMAGE = `${IMG_CND_Prefix}/list_empty.png` export const EMPTY_IMAGE = `${IMG_CND_Prefix}/empty.png` export const SEARCH_EMPTY_IMAGE = `${IMG_CND_Prefix}/search_empty.png` - +// 获取CND资源 +export const getCDNSource = (suffix: string) => { + return IMG_CND_Prefix + suffix +} // 场景值 export const SCENE = { SearchScene: 0, // 商城面料搜索 diff --git a/src/components/Dialog/index.module.scss b/src/components/Dialog/index.module.scss new file mode 100644 index 0000000..afb4530 --- /dev/null +++ b/src/components/Dialog/index.module.scss @@ -0,0 +1,40 @@ +$am-ms: 200ms; + +.dialog{ + position: fixed; + left: 0; + right: 0; + top: 0; + bottom: 0; + width: 100vw; + height: 100vh; + &_mask { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + z-index: 1; + opacity: 0; + transition: opacity $am-ms ease-in; + &_active { + opacity: 1; + } + &--hidden { + background: transparent; + } + } + &_content { + position: fixed; + z-index: 2; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + opacity: 0; + transition: opacity $am-ms ease-in; + &_active { + opacity: 1; + } + } +} diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx new file mode 100644 index 0000000..55cafca --- /dev/null +++ b/src/components/Dialog/index.tsx @@ -0,0 +1,63 @@ +import { Image, View } from '@tarojs/components' +import classnames from 'classnames' +import { useEffect, useState } from 'react' +import style from './index.module.scss' +import { usePropsValue } from '@/use/useCommon' + +interface PropsType { + showOverLay?: boolean + show: boolean + onClose?: (show: boolean) => void + onChange?: (isShow) => void + children?: React.ReactNode +} +// 弹出框 +const Dialog = (props: PropsType) => { + const { showOverLay = true, show = false, onClose: _onClose, onChange: _onChange, children } = props + + const [_show, setShow] = usePropsValue({ + value: show, + defaultValue: false, + onChange: (value) => { + _onChange?.(value) + }, + }) + + const [animShow, setAnimShow] = useState(false) + + const handleAnimShow = () => { + setShow(true) + setTimeout(() => { + setAnimShow(true) + }, 200) + } + const handleAnimHide = () => { + setAnimShow(false) + setTimeout(() => { + setShow(false) + }, 200) + } + const onClose = () => { + handleAnimHide() + _onClose?.(_show) + } + useEffect(() => { + if (show) { + handleAnimShow() + } + }, [show]) + return _show + ? + {/* 遮罩层 start */} + + {/* 遮罩层 end */} + + {children} + + + : +} +export default Dialog diff --git a/src/components/LoadMore/index.module.scss b/src/components/LoadMore/index.module.scss index 6bf198f..c0833ef 100644 --- a/src/components/LoadMore/index.module.scss +++ b/src/components/LoadMore/index.module.scss @@ -7,3 +7,6 @@ color: $color_font_three; padding: 24px 0; } +.empty{ + padding: 100px; +} diff --git a/src/components/LoadMore/index.tsx b/src/components/LoadMore/index.tsx index 933bb7d..d91b3b6 100644 --- a/src/components/LoadMore/index.tsx +++ b/src/components/LoadMore/index.tsx @@ -4,7 +4,7 @@ import Iconfont from '../iconfont/iconfont' import LoadingCard from '../loadingCard/index' import styles from './index.module.scss' -export type LoadMoreStatus = 'more' | 'loading' | 'noMore' +export type LoadMoreStatus = 'more' | 'loading' | 'noMore' | 'empty' interface LoadMoreEvent { onClick?: () => void @@ -15,10 +15,11 @@ interface LoadMorePropsType extends LoadMoreEvent { moreText?: string loadingText?: string noMoreText?: string + emptyText?: string } const LoadMore: FC = (props) => { - const { status, moreText = '查看更多', loadingText = '加载中', noMoreText = '没有更多', onClick } = props + const { status, moreText = '查看更多', loadingText = '加载中', noMoreText = '没有更多', emptyText = '暂无数据', onClick } = props const handleShowMore = () => { onClick && onClick() @@ -36,6 +37,13 @@ const LoadMore: FC = (props) => { ) } + else if (status === 'empty') { + component = ( + + {emptyText} + + ) + } else { component = {noMoreText} } diff --git a/src/components/infiniteScroll/index.tsx b/src/components/infiniteScroll/index.tsx index 2b51a2a..d7f903a 100644 --- a/src/components/infiniteScroll/index.tsx +++ b/src/components/infiniteScroll/index.tsx @@ -29,6 +29,9 @@ interface Params { refresherEnabled?: boolean enableBackToTop?: boolean emptySlot?: React.ReactNode + moreText?: string + loadingText?: string + noMoreText?: string } const InfiniteScroll = ({ styleObj, diff --git a/src/components/table/index.tsx b/src/components/table/index.tsx index e2b2214..fded4e3 100644 --- a/src/components/table/index.tsx +++ b/src/components/table/index.tsx @@ -26,10 +26,15 @@ export interface TablePropsType { dataSource?: { list: RecordType[]; total: number } onLoadMore?: () => void height?: number + moreText?: string + loadingText?: string + noMoreText?: string + emptyText?: string + safeAreaInsetBottom?: boolean } const Table: FC = (props) => { - const { columns, dataSource, onLoadMore, height = 400 } = props + const { columns, dataSource, onLoadMore, height = 400, moreText = '查看更多', loadingText = '加载中', noMoreText = '没有更多了', emptyText = '暂无数据', safeAreaInsetBottom = true } = props const handleShowMore = () => { onLoadMore && onLoadMore() @@ -54,15 +59,18 @@ const Table: FC = (props) => { const loadMoreComponent = useMemo(() => { if (dataSource) { - if (dataSource.list.length < dataSource.total) { - return + if (dataSource.list.length === 0 && dataSource.list.length === dataSource.total) { + return + } + else if (dataSource.list.length < dataSource.total) { + return } else if (dataSource.list.length >= dataSource.total) { - return + return } } else { - return + return } }, [dataSource]) @@ -111,7 +119,7 @@ const Table: FC = (props) => { ) })} - + {sourceContainer()} diff --git a/src/pages/inviteCode/index.config.ts b/src/pages/inviteCode/index.config.ts new file mode 100644 index 0000000..29bfdd0 --- /dev/null +++ b/src/pages/inviteCode/index.config.ts @@ -0,0 +1,7 @@ +export default { + navigationBarTitleText: '邀请码', + navigationBarTextStyle: 'black', + navigationBarBackgroundColor: '#E4EEFD', + backgroundColor: '#E4EEFD', + backgroundColorTop: '#E4EEFD', +} diff --git a/src/pages/inviteCode/index.module.scss b/src/pages/inviteCode/index.module.scss new file mode 100644 index 0000000..206bafc --- /dev/null +++ b/src/pages/inviteCode/index.module.scss @@ -0,0 +1,143 @@ +page { + display: flex; + flex-flow: column nowrap; + height: 100%; +} +.main { + display: flex; + flex-flow: column nowrap; + height: 100%; + background: linear-gradient(to bottom, #E4EEFD 25%, $color_bg_one 42%); + padding-bottom: 0; + box-sizing: border-box; + overflow-y: scroll; + .content{ + flex: 1 1 auto; + overflow: scroll; + } +} +.background { + display: flex; + flex-flow: row nowrap; + padding: 20px 56px; + padding-bottom: 0; + align-items: center; + justify-content: space-between; + overflow: hidden; + .left{ + .title{ + padding-bottom: 15px; + font-size: 60px; + font-weight: 500; + } + .description{ + font-size: 28px; + font-weight: 400; + } + } + .right{ + .iconContainer{ + width: 260px; + position: relative; + bottom: -36px; + .icon{ + width: 130%; + } + } + } +} +.codeBar{ + border-radius: 15px; + background-color: #f7f8fa; + padding: 40px 0; + .inviteCodeBar{ + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + margin-bottom: 16px; + .invite{ + padding: 0 40px; + font-size: 46px; + font-weight: 500; + color: #337FFF; + line-height: 65px; + } + } + +} +.tips{ + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + font-size: 24px; + font-weight: 400; + color: #9fa0a1; + line-height: 28px; + padding: 0 40px; + } +.inviteListTitle{ + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + padding-top: 15px; + padding-bottom: 30px; + .listTitle{ + padding: 0 20px; + font-size: 32px; + font-weight: 500; + color: #000000; + } + .titleIconLeft{ + width: 24px; + height: 4px; + background: linear-gradient(270deg, #333333 0%, rgba(51,51,51,0) 100%); + opacity: 0.3; + } + .titleIconRight{ + width: 24px; + height: 4px; + background: linear-gradient(270deg, rgba(51,51,51,0) 0%, #333333 100%); + opacity: 0.3; + } + +} +.bottomBar { + flex: none; + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + align-items: center; + padding-top: 24px; + padding-right: 24px; + padding-left: 24px; + background-color: #ffffff; + padding-bottom: calc(20px + constant(safe-area-inset-bottom)); + padding-bottom: calc(20px + env(safe-area-inset-bottom)); +} +.codePreview{ + display: flex; + flex-flow: column nowrap; + justify-content: center; + align-items: center; + position: relative; + .imageContainer{ + width: 80vw; + height: auto; + .image{ + width: 100%; + height: 100%; + } + } + .previewTips{ + position: absolute; + bottom: -80px; + font-size: 40px; + font-weight: 400; + color: #c2c2c2; + letter-spacing: 5px; + white-space: nowrap; + } +} diff --git a/src/pages/inviteCode/index.tsx b/src/pages/inviteCode/index.tsx new file mode 100644 index 0000000..f110a70 --- /dev/null +++ b/src/pages/inviteCode/index.tsx @@ -0,0 +1,275 @@ +import { Canvas, Image, Text, View } from '@tarojs/components' +import Taro, { useReady } from '@tarojs/taro' +import { useCallback, useRef, useState } from 'react' +import style from './index.module.scss' +import inviteCodePng from './inviteCode.png' +import QRcode from './inviteCodePopup.png' +import Dialog from '@/components/Dialog' +import LayoutBlock from '@/components/layoutBlock' +import Divider from '@/components/divider' +import type { TablePropsType } from '@/components/table' +import Table from '@/components/table' +import NormalButton from '@/components/normalButton' +import { alert } from '@/common/common' +import { GenBarCodeOrQrCode, GetInvitationInfo } from '@/api' +import { getCDNSource } from '@/common/constant' +// 需要传进来的表头数据示例 +const inviteColumns = [ + { + key: 'invitee', + title: '被邀请人', + dataIndex: 'invitee', + width: '50%', + }, + { + key: 'InviteResults', + title: '邀请进度', + dataIndex: 'InviteResults', + width: '50%', + }, +] +// 邀请码 +const InviteCode = () => { + const { fetchData } = GetInvitationInfo() + const { fetchData: genCode } = GenBarCodeOrQrCode() + const [inviteInfo, setInviteInfo] = useState({}) + const [currentTable, setCurrentTable] = useState({ + columns: inviteColumns, + dataSource: { list: [], total: 0 }, + }) + // 获取邀请码 + const getInviteCode = async() => { + const res = await fetchData() + if (res.success) { + console.log('getInviteCode', res) + setCurrentTable((prev: any) => ({ + ...prev, + dataSource: { + list: res.data.invitation_record.map((item, index: number) => ({ + key: index, + index: index + 1, + invitee: item.name || '--', + InviteResults: '已邀请', + })), + total: res.data.invitation_record.length, + }, + })) + setInviteInfo(res.data) + } + } + // 生成二维码 + const genQRcode = async() => { + const res = await genCode({ content: inviteInfo.invitation_code }) + if (res.success) { + return res.data.qrcode_base64 + } + } + + const [, setForceUpdate] = useState({}) + const canvasNode = useRef() + const ctx = useRef(null) + const [showPopup, setShowPopup] = useState(false) + + const [targetImageUrl, setTargetImageUrl] = useState('') + + const getImageObject = (canvas, src) => { + return new Promise((resolve, reject) => { + console.log('getImageObject param', canvas, src) + const img = canvas.createImage() + img.src = src + img.onload = () => { + console.log('image===>', img) + resolve(img) + } + img.onerror = (err) => { + console.log('image error===>', err) + alert.error('图片加载失败') + reject(err) + } + }) + } + // canvas 生成 图片 + const saveCanvasToImage = (canvas) => { + Taro.canvasToTempFilePath({ + canvas, + fileType: 'png', + success: (res) => { + console.log('tempFilePath', res.tempFilePath) + setTargetImageUrl(res.tempFilePath) + }, + }) + } + const getImageInfo = (filePath) => { + Taro.getImageInfo({ + src: filePath, + success: (infoRes) => { + console.log('', infoRes) + }, + }) + } + // 初始化 canvas + const initCanvas = async() => { + Taro.nextTick(() => { + const query = Taro.createSelectorQuery() + query.select('#canvas').node(({ node: canvas }: { node: Taro.Canvas }) => { + console.log('canvas==>', canvas) + const context = canvas.getContext('2d') + console.log('ctx', context) + canvasNode.current = canvas + ctx.current = context + console.log('canvas', canvas) + setForceUpdate({}) + }).exec() + }) + } + const startPaint = async(ctx, canvas, image) => { + console.log('startPaint param', ctx, canvas, image) + // 开始绘制 + const cCanvasCtx = ctx as any + cCanvasCtx.clearRect(0, 0, canvas.width, canvas.height) + cCanvasCtx.drawImage(image, 0, 0, canvas.width, canvas.height) + cCanvasCtx.save() + cCanvasCtx.font = `${40}px 微软雅黑` + cCanvasCtx.fillStyle = '#000000' + cCanvasCtx.fillText('蜘蛛管家', 40, 80) // text up above canvas + cCanvasCtx.save() + cCanvasCtx.font = `${26}px 微软雅黑` + cCanvasCtx.fillStyle = '#8f9398' + cCanvasCtx.fillText('真挚邀请您建立合作关系', 40, 130) // text up above canvas + cCanvasCtx.save() + cCanvasCtx.font = `${24}px 微软雅黑` + cCanvasCtx.fillStyle = '#a6a6a6' + cCanvasCtx.fillText('请前往邀请码页面,进行扫描邀请', 100, 630) // text up above canvas + cCanvasCtx.save() + cCanvasCtx.font = `${36}px 微软雅黑` + cCanvasCtx.fillStyle = '#7f7f7f' + cCanvasCtx.fillText('邀 请 码', 72, 730) // text up above canvas + cCanvasCtx.save() + cCanvasCtx.font = `${24}px 微软雅黑` + cCanvasCtx.fillStyle = '#cccccc' + cCanvasCtx.fillText('|', 258, 724) // text up above canvas + cCanvasCtx.save() + cCanvasCtx.font = `${36}px 微软雅黑` + cCanvasCtx.fillStyle = '#7f7f7f' + cCanvasCtx.fillText(`${inviteInfo.invitation_code}`, 311, 730) // text up above canvas + cCanvasCtx.save() + const codeUrl = await genQRcode() + try { + const code: any = await getImageObject(canvas, codeUrl) + cCanvasCtx.drawImage(code, 110, 213, 342, 342) + } + catch (err) { + console.error('合成二维邀请码失败') + } + + saveCanvasToImage(canvas) + } + // 绘制最终的海报,图片使用两倍大小,canvas大小是图片的二分之一 就能让canvas生成出来的图片清晰了 + const drawPictorial = async() => { + return new Promise((resolve, reject) => { + if (!ctx.current) { + // 重新初始化canvas + initCanvas() + reject(new Error('ctx error')) + return false + } + const canvas = canvasNode.current + Taro.getImageInfo({ + src: getCDNSource('/user/inviteCodePopup.png'), + success: (res) => { + canvas.width = res.width / 2 + canvas.height = res.height / 2 + getImageObject(canvas, `${res.path}`).then((image) => { + // 开始绘制 + startPaint(ctx.current, canvasNode.current, image) + resolve(true) + }).catch((error) => { + throw new Error(error) + }) + }, + }) + }) + } + // 加载更多 + const handleLoadMore = () => { + + } + const handleQRcodeShare = async() => { + await drawPictorial() + setShowPopup(true) + } + // 复制二维码 + const handleCopyInviteCode = () => { + Taro.setClipboardData({ + data: 'HWWG', + }) + } + const handleChange = (value: boolean) => { + console.log('value', value) + + setShowPopup(value) + } + useReady(() => { + getInviteCode() + setTimeout(() => { + initCanvas() + }, 200) + }) + + return + + + + 蜘蛛管家 + 真挚邀请您建立合作关系 + + + + + + + + + + + 邀请码 + + {inviteInfo.invitation_code} + + 填写邀请码,即可在蜘蛛管家下单购物 + + + + + + 成功邀请 + + + +
+
+
+ 温馨提示:邀请码确定绑定后,不支持解绑。 +
+ + + + 二维码分享 + + + 复制邀请码 + + + + + + {/* showMenuByLongpress 属性只对 小程序有效 */} + + + 长按图片保存到手机 + + + +
+} +export default InviteCode diff --git a/src/pages/login/index.module.scss b/src/pages/login/index.module.scss index 73d6360..b111878 100644 --- a/src/pages/login/index.module.scss +++ b/src/pages/login/index.module.scss @@ -1,9 +1,8 @@ -page { - height: 100vh; +page{ + background-color: #fff; } .login { width: 100%; - height: 100%; background-color: white; Image{ vertical-align: bottom; @@ -34,7 +33,7 @@ page { align-items: center; border: 1px solid #c2c2c2; border-radius: 12px; - margin-top: 40px; + margin-top: 4vh; padding: 15px 20px; font-size: 40px; @mixin inputBaseStyle { @@ -64,13 +63,13 @@ page { display: flex; flex-flow: row nowrap; align-items: center; - margin-top: 40px; + margin-top: 4vh; font-size: 26px; color: #909090; } &-button { height: 90px; - margin-top: 120px; + margin-top: 7vh; font-size: 32px; } } @@ -82,7 +81,7 @@ page { margin: 0 20px; } .quick-login { - margin-top: 100px; + margin-top: 6vh; &--options { display: flex; flex-flow: row nowrap; diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 66b78cd..206d877 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -128,7 +128,8 @@ const Login: FC = () => { 登录 - + {/* H5 显示微信登陆 */} + {process.env.TARO_ENV === 'h5' && } ) diff --git a/src/pages/user/index.tsx b/src/pages/user/index.tsx index eef6295..f8c9049 100644 --- a/src/pages/user/index.tsx +++ b/src/pages/user/index.tsx @@ -56,7 +56,7 @@ const feature: IconCardType[] = [ { iconName: 'icon-yaoqingma', name: '邀请码', - path: '', + path: '/pages/inviteCode/index', jurisdiction: 'invitation_code_page', }, {