Commit 5b9c2099 by HoMeTown

Initial commit

parents
# Local
.DS_Store
*.local
*.log*
# Dist
node_modules
dist/
# IDE
.vscode/*
!.vscode/extensions.json
.idea
# Ignore auto generated CSS declarations
*.module.css.d.ts
*.module.sass.d.ts
*.module.scss.d.ts
*.module.less.d.ts
*.module.styl.d.ts
*.module.stylus.d.ts
\ No newline at end of file
# Rsbuild Project
## Setup
Install the dependencies:
```bash
pnpm install
```
## Get Started
Start the dev server:
```bash
pnpm dev
```
Build the app for production:
```bash
pnpm config
```
Preview the production build locally:
```bash
pnpm preview
```
build test
```bash
pnpm run build:test
```
deploy test
```bash
pnpm run deploy:test
```
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "eppfax-h5",
"private": true,
"version": "1.0.0",
"scripts": {
"dev": "rsbuild dev --open --config rsbuild/build/build.dev.ts",
"test": "rsbuild dev --open --config rsbuild/build/build.test.ts",
"prod": "rsbuild dev --open --config rsbuild/build/build.prod.ts",
"build:test": "rsbuild build --config rsbuild/build/build.test.ts",
"build:dev": "rsbuild build --config rsbuild/build/build.dev.ts",
"build:prod": "rsbuild build --config rsbuild/build/build.prod.ts",
"deploy:test": "pnpm build:test && sh ./scripts/deploy.test.sh",
"build": "rsbuild build",
"preview": "rsbuild preview"
},
"dependencies": {
"@nutui/nutui-react": "^2.2.0",
"axios": "^1.6.2",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0"
},
"devDependencies": {
"@rsbuild/core": "^0.1.2",
"@rsbuild/plugin-image-compress": "^0.1.4",
"@rsbuild/plugin-react": "^0.1.2",
"@types/lodash": "^4.14.202",
"@types/react": "^18",
"@types/react-dom": "^18",
"postcss-px-to-viewport": "^1.1.1",
"typescript": "^5.3.0"
}
}
This diff is collapsed. Click to expand it.
import { type RsbuildConfig as BaseRsbuildConfig } from '@rsbuild/shared';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginImageCompress } from '@rsbuild/plugin-image-compress';
import { CSS_MODULES_LOCAL_IDENT_NAME, HTML_TEMPLATE_CONFIG } from '../config'
import { postcssPxToViewportCreator } from '../plugins'
const buildBaseConfig: BaseRsbuildConfig = {
plugins: [pluginReact()],
output: {
enableCssModuleTSDeclaration: true,
cssModules: {
localIdentName: CSS_MODULES_LOCAL_IDENT_NAME
}
},
html: HTML_TEMPLATE_CONFIG,
tools: {
postcss: (opts) => {
console.log(postcssPxToViewportCreator());
opts.postcssOptions?.plugins?.push(postcssPxToViewportCreator())
},
},
performance: {
chunkSplit: {
strategy: 'split-by-experience'
}
},
server: {
port: 3000
}
}
export default buildBaseConfig
import {type RsbuildConfig as BaseRsbuildConfig} from '@rsbuild/shared';
import {defineConfig} from '@rsbuild/core';
import merge from 'lodash/merge';
import buildBaseConfig from "./build.base";
import {ENV_CONFIG, ENV_DEV} from "../config";
function defineConfigCreator(): BaseRsbuildConfig {
const devEnvConfig = ENV_CONFIG[ENV_DEV];
for (let prodEnvConfigKey in devEnvConfig) {
process.env[prodEnvConfigKey] = devEnvConfig[prodEnvConfigKey as keyof typeof devEnvConfig]
}
const devBuildConfig: BaseRsbuildConfig = {
source: {
define: {
'process.env.REACT_APP_ENV': JSON.stringify(ENV_DEV),
'process.env.REACT_APP_API_URL': JSON.stringify(devEnvConfig.REACT_APP_API_URL)
}},
tools: { bundlerChain(chain, { env }) { chain.devtool('eval'); } }
}
return defineConfig(merge({}, buildBaseConfig, devBuildConfig))
}
export default defineConfigCreator();
import {type RsbuildConfig as BaseRsbuildConfig} from '@rsbuild/shared';
import {defineConfig} from '@rsbuild/core';
import merge from 'lodash/merge';
import buildBaseConfig from "./build.base";
import {ENV_CONFIG, ENV_PROD} from "../config";
function defineConfigCreator(): BaseRsbuildConfig {
const prodEnvConfig = ENV_CONFIG[ENV_PROD];
for (let prodEnvConfigKey in prodEnvConfig) {
process.env[prodEnvConfigKey] = prodEnvConfig[prodEnvConfigKey as keyof typeof prodEnvConfig]
}
const prodBuildConfig: BaseRsbuildConfig = {
output: { disableSourceMap: true },
performance: {removeConsole: ['log', 'warn'],},
source: {
define: {
'process.env.REACT_APP_ENV': JSON.stringify(ENV_PROD),
'process.env.REACT_APP_API_URL': JSON.stringify(prodEnvConfig.REACT_APP_API_URL)
}},
}
return defineConfig(merge({}, buildBaseConfig, prodBuildConfig))
}
export default defineConfigCreator();
import {type RsbuildConfig as BaseRsbuildConfig} from '@rsbuild/shared';
import {defineConfig} from '@rsbuild/core';
import merge from 'lodash/merge';
import buildBaseConfig from "./build.base";
import {ENV_CONFIG, ENV_TEST} from "../config";
function defineConfigCreator(): BaseRsbuildConfig {
const testEnvConfig = ENV_CONFIG[ENV_TEST];
for (let prodEnvConfigKey in testEnvConfig) {
process.env[prodEnvConfigKey] = testEnvConfig[prodEnvConfigKey as keyof typeof testEnvConfig]
}
const testBuildConfig: BaseRsbuildConfig = {
source: {
define: {
'process.env.REACT_APP_ENV': JSON.stringify(ENV_TEST),
'process.env.REACT_APP_API_URL': JSON.stringify(testEnvConfig.REACT_APP_API_URL)
}}
}
return defineConfig(merge({}, buildBaseConfig, testBuildConfig))
}
export default defineConfigCreator();
/**
* 自定义 CSS Modules 生成的类名规则
*/
export const CSS_MODULES_LOCAL_IDENT_NAME = '[hash:base64:8]'
\ No newline at end of file
/**
* 环境变量:开发
*/
export const ENV_DEV = 'development'
/**
* 环境变量:测试
*/
export const ENV_TEST = 'test'
/**
* 环境变量:生产
*/
export const ENV_PROD = 'production'
/**
* 环境配置
*/
export const ENV_CONFIG = {
[ENV_DEV]: {
REACT_APP_ENV: ENV_DEV,
REACT_APP_ENV_ZH: '开发环境',
REACT_APP_API_URL: 'https://mall-test.eppfax.com/apjf-shopping-mall-api'
},
[ENV_TEST]: {
REACT_APP_ENV: ENV_TEST,
REACT_APP_ENV_ZH: '测试环境',
REACT_APP_API_URL: 'https://mall-test.eppfax.com/apjf-shopping-mall-api'
},
[ENV_PROD]: {
REACT_APP_ENV: ENV_PROD,
REACT_APP_ENV_ZH: '生产环境',
REACT_APP_API_URL: 'https://mall.eppfax.com/apjf-shopping-mall-api'
},
}
\ No newline at end of file
import { type RsbuildConfig as BaseRsbuildConfig } from '@rsbuild/shared';
type HtmlTemplateType = BaseRsbuildConfig['html']
export const HTML_TEMPLATE_CONFIG:HtmlTemplateType = {
title: 'test',
favicon: './static/favicon.ico',
tags: [
{ tag: 'meta', attrs: { charset: 'UTF-8'} },
{ tag: 'meta', attrs: { name: 'viewport', content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover'} },
{ tag: 'meta', attrs: { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge,chrome=1'} },
{ tag: 'meta', attrs: { 'http-equiv': 'Expires', content: '0' }},
{ tag: 'meta', attrs: { 'http-equiv': 'Pragma', content: 'no-cache' }},
{ tag: 'meta', attrs: { 'http-equiv': 'Cache-control', content: 'no-cache' }},
{ tag: 'meta', attrs: { 'http-equiv': 'Cache', content: 'no-cache' }},
]
}
\ No newline at end of file
export * from './css-modules'
export * from './html-template'
export * from './env'
\ No newline at end of file
export * from './postcss-px-to-viewport'
\ No newline at end of file
/**
* 创建px2vw
*/
export const postcssPxToViewportCreator = function() {
const postcssPxToViewport = require('postcss-px-to-viewport')
return postcssPxToViewport({
unitToConvert: 'px', // 要转化的单位
viewportWidth: 375,
viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用 rem
fontViewportUnit: 'vw', // 字体使用的视口单位
unitPrecision: 13, // 指定`px`转换为视窗单位值的小数后 x位数
propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
selectorBlackList: ['ignore'], // 指定不转换为视窗单位的类名,
minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
replace: true, // 是否转换后直接更换属性值
exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
landscape: false, // 是否处理横屏情况
})
}
\ No newline at end of file
scp -i ~/.ssh/id_rsa -r /Users/hometown/dh/eppfax-h5/dist/* root@123.56.20.201:/data/app_front_test/eppfax-h5
.testBox {
width: 200px;
height: 200px;
background-color: #fcc;
position: relative;
.testBoxText {
color: #000;
font-weight: bold;
font-size: 14px;
}
.testBoxImg {
width: 57px;
height: 20px;
position: absolute;
right: 0;
top: 0;
}
}
\ No newline at end of file
import styles from './App.module.scss'
import testImg from '@/assets/images/notGetStatus.png'
import {useNavigate} from "react-router-dom";
const App = () => {
const navigate = useNavigate()
function handleClick() {
navigate("/testdemo?id=1234", {
state: {
id: 1234567
}
})
}
return (
<div className="content">
<div className={styles.testBox}>
<span className={styles.testBoxText}>hello, 你好 {process.env.REACT_APP_ENV}</span>
<button onClick={handleClick}>点击跳转</button>
<img className={styles.testBoxImg} src={testImg} alt=""/>
</div>
</div>
);
};
export default App;
import axiosInstance from '@/utils/axios'
/**
* 获取登录地址
*/
export const getLoginUrl = async (): Promise<BackendResponse<any>> => {
const response = await axiosInstance.get<BackendResponse<LoginResponseData>>('/api/login/get_login_url');
return response.data
}
interface LoginRequestData {
code: string;
state: string;
amp: string;
}
interface LoginResponseData {
token: string;
}
/**
* 登录
* @returns Promise<LoginResponseData>
* @param requestData
*/
export const login = async (requestData: LoginRequestData): Promise<BackendResponse<LoginResponseData>> => {
const response = await axiosInstance.post<BackendResponse<LoginResponseData>>('/api/login/authorize', requestData);
return response.data;
}
interface QueryUserOrderResponse {
orderId: string;
lotteryOrderId: string;
}
export const queryUserOrder = async (): Promise<BackendResponse<QueryUserOrderResponse>> => {
const response = await axiosInstance.get<BackendResponse<QueryUserOrderResponse>>('/api/order/getUserOrder')
return response.data
}
/**
* 查询产品列表
*/
export const queryProductList = async (): Promise<BackendResponse<any>> => {
const response = await axiosInstance.get<BackendResponse<any>>('/api/product/query_list');
return response.data
}
/**
* 获取首页配置信息
*/
export const queryHomeConfig = async (): Promise<BackendResponse<any>> => {
const response = await axiosInstance.get<BackendResponse<any>>('/api/common/config/query_home_config');
return response.data
}
export interface QueryBranchListResponseData {
branchCode: string;
branchName: string;
showOrder: 1;
}
/**
* 获取网点列表
*/
export const queryBranchList = async (): Promise<BackendResponse<QueryBranchListResponseData[]>> => {
const response = await axiosInstance.get<BackendResponse<QueryBranchListResponseData[]>>('/api/branch/query_list');
return response.data
}
export interface CreateOrderRequestData {
branchName: string;
branchCode: string;
productCode: string;
number: number;
}
/**
* 创建订单
* @param requestData
*/
export const createOrder = async (requestData: CreateOrderRequestData): Promise<BackendResponse<string>> => {
const response = await axiosInstance.post<BackendResponse<string>>('/api/order/create_order', requestData);
return response.data
}
interface GetPayUrlRequestData {
orderId: string
}
/**
* 查询支付地址
* @param requestData
*/
export const queryPayUrl = async (requestData: GetPayUrlRequestData): Promise<BackendResponse<string>> => {
const response = await axiosInstance.post<BackendResponse<string>>('/api/pay', requestData);
return response.data
}
/**
* 查询支付配置
*/
export const queryPayConfig = async (): Promise<BackendResponse<any>> => {
const response = await axiosInstance.get<BackendResponse<any>>('/api/pay/pay_config');
return response.data
}
interface QueryOrderInfoResponseOrderDetail {
branchCode: string;
branchName: string;
mobile: string;
orderAmount: string;
payStatus: string;
payTime: string;
}
interface QueryOrderInfoResponseProductDetailInfo {
productCode: string;
productName: string;
}
export interface QueryOrderInfoResponse {
orderDetail: QueryOrderInfoResponseOrderDetail;
productDetailInfo: QueryOrderInfoResponseProductDetailInfo
}
/**
* 查询订单详情
* @param orderId
*/
export const queryOrderInfo = async (orderId: string):Promise<BackendResponse<any>> => {
const response = await axiosInstance.get<BackendResponse<any>>(`/api/order/detail/${orderId}`)
return response.data
}
\ No newline at end of file
import * as common from './common'
export default {
common
}
\ No newline at end of file
/// <reference types="@rsbuild/core/types" />
interface Window {
AlipayJSBridge: Record<string, any>
}
\ No newline at end of file
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Home from "@/pages/Home";
import Lottery from "@/pages/Lottery";
import OrderDetail from "@/pages/OrderDetail";
import './styles/reset.css';
import './styles/nut-var.css';
import '@nutui/nutui-react/dist/style.css'
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path = '/' element={<Home />}></Route>
<Route path = '/lottery' element={<Lottery />}></Route>
<Route path = '/orderDetail' element={<OrderDetail />}></Route>
</Routes>
</BrowserRouter>
</React.StrictMode>,
);
import React from "react";
import styles from "@/pages/Home/index.module.scss";
import homeTopBannerImg from "@/assets/images/homeTopBanner.png";
import homeBotBannerImg from "@/assets/images/homeBotBanner.png";
const HomeBg: React.FC = () => {
return (
<div className={styles.bgWrap}>
<img className={styles.topBannerImg} src={homeTopBannerImg} alt=""/>
<img className={styles.botBannerImg} src={homeBotBannerImg} alt=""/>
</div>
)
}
export default HomeBg
\ No newline at end of file
import React, {useEffect, useState} from "react";
import homeProductBanner1Img from "@/assets/images/homeProductBanner1.png";
import homeProductBanner2Img from "@/assets/images/homeProductBanner2.png";
import styles from "@/pages/Home/index.module.scss";
import HomeProductItem from "@/pages/Home/components/HomeProductItem";
import { Overlay, Popup, Toast } from "@nutui/nutui-react";
import homeRulesOverlayCloseImg from "@/assets/images/homeRulesOverlayClose.png";
import HomeRuleContent from "@/pages/Home/components/HomeRuleContent";
import apis from "@/apis";
import {type QueryBranchListResponseData} from '@/apis/common'
import OrderHelper from "@/pages/Home/helper/order";
const HomeContent: React.FC = () => {
const [productList] = useState([
{ bannerImg: homeProductBanner1Img, title: '厅堂实物 1元购', buttonTextColor: '#F41E71', productCode: '4'},
{ bannerImg: homeProductBanner2Img, title: '微信立减金等你来拿', buttonTextColor: '#1FA189', productCode: '5'},
])
const [branchList, setBranchList] = useState<QueryBranchListResponseData[]>([])
const [rulesOverlayVisible, setRulesOverlayVisible] = useState(false)
const [showBranchListPopup, setShowBranchListPopup] = useState(false)
const [activeBranchIdx, setActiveBranchIdx] = useState(-1)
const [activeProductIdx, setActiveProductIdx] = useState(-1)
const [toasted, setToasted] = useState(false)
function handleClickProduct(idx: number) {
if(toasted) {
setActiveProductIdx(idx)
setShowBranchListPopup(true)
} else {
Toast.show('请在网点工作人员指导下购买,每人仅有一次活动参与机会');
setToasted(true)
}
}
function handleConfirm() {
OrderHelper.submitOrder({
branchName: branchList[activeBranchIdx].branchName,
branchCode: branchList[activeBranchIdx].branchCode,
productCode: productList[activeProductIdx].productCode,
number: 1
})
}
useEffect(() => {
apis.common.queryBranchList().then(res => {
if(res.ok) {
setBranchList(res.data)
}
})
}, [])
return (
<div className={styles.contentWrap}>
{productList.map((item, index) => {
return (
<HomeProductItem
bannerImg={item.bannerImg}
title={item.title}
buttonTextColor={item.buttonTextColor}
key={index}
idx={index}
onButtonClick={handleClickProduct}
/>
)
})}
<div className={styles.productTips}>活动最终解释权归本行所有</div>
{/*网点列表*/}
<Popup
visible={ showBranchListPopup }
position="bottom"
round
onClose={ () => { setShowBranchListPopup(false) }
}>
<div className={styles.branchListPopupWrap}>
<div className={styles.branchListPopupHeader}>
<div className={styles.branchListPopupHeaderCancel} onClick={() => {
setShowBranchListPopup(false);
setActiveBranchIdx(-1);
}}>取消</div>
<div className={styles.branchListPopupHeaderTitle}>选择营业网点</div>
<div className={styles.branchListPopupHeaderConfirm} onClick={handleConfirm}>确定</div>
</div>
<div className={styles.branchListPopupContent}>
{ branchList.map((item, index) => {
return (
<div
className={styles.branchListItem}
onClick={() => setActiveBranchIdx(index)}
key={item.branchCode}
style={{
color: activeBranchIdx === index ? '#e64545' : '',
background: activeBranchIdx === index ? '#f7f7f7' : ''
}}
>
{item.branchName}
</div>
)
}) }
</div>
</div>
</Popup>
{/*网点列表*/}
{/*活动规则*/}
<div className={styles.contentRulesButton} onClick={() => setRulesOverlayVisible(true)}>活动规则</div>
<Overlay
visible={rulesOverlayVisible}
closeOnOverlayClick={false}
onClick={() => setRulesOverlayVisible(false)}
>
<div className={styles.rulesOverlayWrap}>
<div className={styles.rulesOverlayContent}>
<img className={styles.rulesOverlayCloseImg} src={homeRulesOverlayCloseImg} onClick={() => setRulesOverlayVisible(false)} alt=""/>
<HomeRuleContent />
</div>
</div>
</Overlay>
{/*活动规则*/}
</div>
)
}
export default HomeContent
\ No newline at end of file
import React from "react";
import styles from "@/pages/Home/index.module.scss";
interface HomeProductItemProps {
/**
* 背景图
*/
bannerImg: string;
/**
* 标题
*/
title: string;
/**
* 按钮文字
*/
buttonText?: string;
/**
* 按钮文字颜色
*/
buttonTextColor: string;
/**
* 点击
*/
onButtonClick: (idx: number) => void
/**
* 索引
*/
idx: number
}
const HomeProductItem: React.FC<HomeProductItemProps> = (props) => {
const buttonStyle = { color: props.buttonTextColor }
return (
<div className={styles.productItem}>
<img className={styles.productItemBg} src={props.bannerImg} alt={props.title}/>
<h3 className={styles.productItemTitle}>{ props.title }</h3>
<div className={styles.productItemButton} style={buttonStyle} onClick={() => props.onButtonClick(props.idx)}>{ props.buttonText || '立即购买' }</div>
</div>
)
}
export default HomeProductItem
\ No newline at end of file
import React from "react";
import styles from "@/pages/Home/index.module.scss";
const HomeRuleContent: React.FC = () => {
return (
<div className={styles.rulesOverlayContentInner}>
<h2>活动规则</h2>
<h3>闸北支行一元购,新年钜惠等你来拿</h3>
<h4>活动时间:</h4>
<p>2023年1月9日~2023年4月30日</p>
<h4>活动对象:</h4>
<p>掌银新客户,信用卡新客户,财富贵宾客户</p>
<h4>活动规则:</h4>
<p>
1.打开掌银,扫描网点厅堂二维码进入活动页面,在网点工作人员的指导下购买新客礼,每人在活动期限内限参与1次活动。
<br />
2.同一证件号、同一手机号、同一设备均视为同一客户。
<br />
3.成功支付后请出示订单详情页,于网点大堂当场核销礼品。
<br />
4.凡参加本次活动者,即视为同意接受本次活动相关规则,活动期间用户不得使用不正当手段破坏活动规则、违背活动公平原则,否则有权取消活动参与资格。
</p>
</div>
)
}
export default HomeRuleContent
\ No newline at end of file
import apis from "@/apis";
import {type CreateOrderRequestData, queryUserOrder} from '@/apis/common'
import SessionStorageUtil from "@/utils/session";
import { type NavigateFunction } from "react-router-dom";
class OrderHelper {
/**
* 提交订单:
* 1. 创建订单
* 2. 查询支付地址
* 3. 查询支付配置
* 4. 唤起app支付
* @param params CreateOrderRequestData
*/
static async submitOrder(params: CreateOrderRequestData) {
const createOrderRes = await apis.common.createOrder(params)
const queryPayUrlRes = await apis.common.queryPayUrl({orderId: createOrderRes.data})
const tokenId = queryPayUrlRes.data.split('=')[1]
const queryPayConfigRes = await apis.common.queryPayConfig()
const data = queryPayConfigRes.data
data.param.tokenId = tokenId
window.AlipayJSBridge.call("startApp", data, function (result: string) {
console.log(result, "支付结果");
});
}
/**
* 获取用户的订单
*/
static async queryUserOrder(navigate: NavigateFunction) {
const res = await apis.common.queryUserOrder()
if(res.ok) {
if(res.data?.lotteryOrderId) {
navigate(`/lottery?lotteryOrderId=${res.data.lotteryOrderId}`, {
replace: true
})
}
if(res.data?.orderId) {
navigate(`/orderDetail?orderId=${res.data.orderId}`, {
replace: true
})
}
}
}
}
export default OrderHelper
\ No newline at end of file
.wrap {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
.bgWrap {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
.topBannerImg {
width: 100%;
height: 497px;
}
.botBannerImg {
width: 100%;
height: 154px;
}
}
.contentWrap {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
z-index: 2;
box-sizing: border-box;
padding: 0 16px;
display: flex;
flex-direction: column;
justify-content: flex-end;
.contentRulesButton {
position: absolute;
right: 0;
top: 20px;
z-index: 2;
width: 66px;
height: 24px;
background: rgba(7,103,90,0.6);
border-radius: 20px 0 0 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #FFFFFF;
}
.productItem {
width: 343px;
height: 120px;
margin-bottom: 16px;
position: relative;
box-sizing: border-box;
padding: 25px 24px;
.productItemBg {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
.productItemTitle {
font-size: 20px;
color: #FFFFFF;
line-height: 26px;
position: relative;
z-index: 2;
}
.productItemButton {
width: 100px;
height: 32px;
background: #FFFFFF;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 500;
color: #F41E71;
line-height: 16px;
position: relative;
z-index: 3;
margin-top: 12px;
}
}
.productTips {
width: 100%;
text-align: center;
font-size: 12px;
color: #666666;
line-height: 14px;
letter-spacing: 2px;
margin-bottom: 18px;
}
}
}
.rulesOverlayWrap {
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
.rulesOverlayContent {
width: 280px;
height: 400px;
border-radius: 10px;
background-color: #fff;
position: relative;
.rulesOverlayCloseImg {
width: 22px;
height: 22px;
position: absolute;
right: 10px;
top: -30px;
}
.rulesOverlayContentInner {
width: 100%;
height: 100%;
overflow-y: scroll;
overflow-x: hidden;
box-sizing: border-box;
padding: 20px 24px;
h2 {
width: 100%;
font-size: 16px;
font-weight: 500;
color: #333333;
display: flex;
justify-content: center;
/* padding-top: 20px; */
padding-bottom: 16px;
}
h3 {
font-size: 14px;
font-weight: 500;
color: #333333;
line-height: 20px;
margin: 0;
margin-bottom: 12px;
}
h4 {
font-size: 14px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #333333;
line-height: 20px;
margin: 0;
}
p {
font-size: 14px;
font-family: PingFangSC-Medium, PingFang SC;
color: #666;
line-height: 20px;
margin-bottom: 12px;
}
}
}
}
.branchListPopupWrap {
.branchListPopupHeader {
display: flex;
width: 100%;
height: 60px;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding: 0 16px;
border-bottom: 1px solid #e5e5e5;
.branchListPopupHeaderCancel {
font-size: 16px;
font-weight: 400;
color: #999999;
line-height: 22px;
}
.branchListPopupHeaderTitle {
font-size: 16px;
font-weight: 500;
color: #333333;
line-height: 22px;
}
.branchListPopupHeaderConfirm {
font-size: 16px;
font-weight: 500;
color: #e64545;
line-height: 22px;
}
}
.branchListPopupContent {
box-sizing: border-box;
padding: 10px 16px;
background-color: #fff;
.branchListItem {
width: 100%;
height: 40px;
background-color: #fff;
font-size: 15px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #333333;
line-height: 22px;
display: flex;
align-items: center;
box-sizing: border-box;
padding-left: 16px;
}
}
}
\ No newline at end of file
import React, {useEffect, useState} from "react";
import styles from './index.module.scss'
import {useNavigate, useSearchParams} from "react-router-dom";
import HomeBg from "@/pages/Home/components/HomeBg";
import HomeContent from "@/pages/Home/components/HomeContent";
import AuthUtil from "@/utils/auth";
import OrderHelper from "@/pages/Home/helper/order";
const Home: React.FC = () => {
const [params] = useSearchParams()
const navigate = useNavigate()
const [isAuthed, setIsAuthed] = useState(false)
useEffect( () => {
AuthUtil.auth(params)
.then(res => {
OrderHelper.queryUserOrder(navigate).then(_ => {
setIsAuthed(res)
})
})
}, []);
return (
<div className={styles.wrap}>
{ isAuthed && <HomeBg /> }
{ isAuthed && <HomeContent /> }
</div>
)
}
export default Home
\ No newline at end of file
import React from "react";
const Lottery: React.FC = () => {
return (
<div>抽奖页</div>
)
}
export default Lottery
\ No newline at end of file
.wrap {
width: 100%;
height: 100%;
background: #F7F7F7;
box-sizing: border-box;
padding: 10px 16px;
.contentWrap {
width: 100%;
box-sizing: border-box;
padding: 20px 16px;
background: #FFFFFF;
border-radius: 10px;
margin-bottom: 10px;
.contentTitle {
font-size: 15px;
font-weight: 500;
color: #333333;
line-height: 22px;
margin-bottom: 13px;
}
.contentItem {
display: flex;
align-items: center;
margin-bottom: 18px;
&:last-child {
margin-bottom: 0;
}
.contentItemLabel {
font-size: 14px;
font-weight: 400;
color: #999999;
line-height: 20px;
}
.contentItemValue {
font-size: 15px;
font-weight: 500;
color: #222222;
line-height: 22px;
}
.contentItemAmount {
font-size: 12px;
font-weight: 500;
color: #E64545;
line-height: 22px;
> span {
font-size: 16px;
}
}
}
}
}
\ No newline at end of file
import React, {useEffect, useState} from "react";
import {useSearchParams} from "react-router-dom";
import apis from "@/apis";
import { type QueryOrderInfoResponse } from '@/apis/common'
import styles from './index.module.scss'
import { Divider, Skeleton } from '@nutui/nutui-react';
const OrderDetail: React.FC = () => {
const [params] = useSearchParams()
const [orderInfo, setOrderInfo] = useState<QueryOrderInfoResponse>()
const [checked, setChecked] = useState(false)
useEffect(() => {
console.log(params.get('orderId'))
apis.common.queryOrderInfo(params.get('orderId') || '').then(res => {
console.log(res.data)
if(res.ok) {
setOrderInfo(res.data)
setChecked(true)
}
})
}, [])
return (
<div className={styles.wrap}>
<div className={styles.contentWrap}>
<Skeleton rows={2} title animated visible={checked}>
<div className={styles.contentTitle}>商品详情</div>
<div className={styles.contentItem}>
<span className={styles.contentItemLabel}>商品名称:</span>
<span className={styles.contentItemValue}>{orderInfo?.productDetailInfo.productName}</span>
</div>
<div className={styles.contentItem}>
<span className={styles.contentItemLabel}>支付金额:</span>
<span className={styles.contentItemAmount}><span>{orderInfo?.orderDetail.orderAmount}</span></span>
</div>
</Skeleton>
</div>
<div className={styles.contentWrap}>
<Skeleton rows={4} title animated visible={checked}>
<div className={styles.contentTitle}>订单详情</div>
<div className={styles.contentItem}>
<span className={styles.contentItemLabel}>机构编码:</span>
<span className={styles.contentItemValue}>{orderInfo?.orderDetail.branchCode}</span>
</div>
<div className={styles.contentItem}>
<span className={styles.contentItemLabel}>所属网点:</span>
<span className={styles.contentItemValue}>{orderInfo?.orderDetail.branchName}</span>
</div>
<Divider />
<div className={styles.contentItem}>
<span className={styles.contentItemLabel}>支付时间:</span>
<span className={styles.contentItemValue}>{orderInfo?.orderDetail.payTime}</span>
</div>
<div className={styles.contentItem}>
<span className={styles.contentItemLabel}>下单账号:</span>
<span className={styles.contentItemValue}>{orderInfo?.orderDetail.mobile}</span>
</div>
</Skeleton>
</div>
</div>
)
}
export default OrderDetail
\ No newline at end of file
:root {
--nutui-toast-inner-padding: 8px 12px;
--nutui-toast-inner-border-radius: 8px;
--nutui-divider-border-color: rgba(229, 229, 229, .5);
}
\ No newline at end of file
/* CSS Reset for HTML5 Applications */
html, body, #root {
width: 100%;
height: 100%;
overflow: hidden;
}
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
interface BackendResponse<T> {
code: number;
msg: string;
ok: boolean;
data: T;
}
\ No newline at end of file
import { type NavigateFunction} from "react-router-dom";
import apis from '@/apis'
import SessionStorageUtil from "@/utils/session";
class AuthUtil {
static auth(params: URLSearchParams): Promise<boolean> {
return new Promise((resolve, reject) => {
if(process.env.REACT_APP_ENV === 'development') {
SessionStorageUtil.setItem('__TOKEN__', '6d29e9d470e24cf9af48bd5881b97f80')
resolve(true)
} else {
const token = SessionStorageUtil.getItem('__TOKEN__')
if(!token) {
if(!params.get('code')) {
apis.common.getLoginUrl().then(res => {
if(res.ok) {
window.location.replace(res.data)
}
})
} else {
apis.common.login({
code: params.get('code') || '',
state: params.get('state') || '',
amp: params.get('amp') || '',
}).then(res => {
if(res.ok) {
SessionStorageUtil.setItem('__TOKEN__', res.data.token)
}
resolve(true)
})
}
} else {
resolve(true)
}
}
})
}
}
export default AuthUtil
\ No newline at end of file
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from "axios";
import SessionStorageUtil from "@/utils/session";
import { Toast } from "@nutui/nutui-react";
const axiosInstance: AxiosInstance = axios.create({
baseURL: process.env.REACT_APP_API_URL,
timeout: 30000,
})
axiosInstance.interceptors.request.use(
(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
config.headers.token = SessionStorageUtil.getItem('__TOKEN__');
return config;
},
(error) => {
return Promise.reject(error);
}
);
axiosInstance.interceptors.response.use(
(response: AxiosResponse ): AxiosResponse => {
// 处理登录失效
if(response.data.code === 9999) {
Toast.show('登录失效')
setTimeout(() => {
SessionStorageUtil.clear()
window.location.replace('/')
}, 2000)
}
return response;
},
(error) => {
return Promise.reject(error);
}
);
export default axiosInstance;
\ No newline at end of file
class SessionStorageUtil {
/**
* 存储数据到sessionStorage
* @param key 键名
* @param value 值,将被自动转换为字符串
*/
static setItem(key: string, value: any): void {
const stringValue = JSON.stringify(value);
sessionStorage.setItem(key, stringValue);
}
/**
* 从sessionStorage获取数据
* @param key 键名
* @returns 返回解析后的值,如果不存在则返回null
*/
static getItem<T>(key: string): T | null {
const item = sessionStorage.getItem(key);
if (item === null) {
return null;
}
try {
return JSON.parse(item) as T;
} catch (e) {
console.error("Error parsing sessionStorage item", e);
return null;
}
}
/**
* 从sessionStorage中删除特定数据
* @param key 键名
*/
static removeItem(key: string): void {
sessionStorage.removeItem(key);
}
/**
* 更新sessionStorage中的数据
* @param key 键名
* @param value 新的值,将被自动转换为字符串
*/
static updateItem(key: string, value: any): void {
this.setItem(key, value);
}
/**
* 清除所有sessionStorage数据
*/
static clear(): void {
sessionStorage.clear();
}
}
export default SessionStorageUtil
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover"
/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta name="HandheldFriendly" content="true" />
<meta name="MobileOptimized" content="320" />
<meta name="msapplication-tap-highlight" content="no" />
<meta name="apple-mobile-web-app-title" content="标题" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="format-detection" content="telephone=yes" />
<meta http-equiv="Expires" content="0" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Cache-control" content="no-cache" />
<meta http-equiv="Cache" content="no-cache" />
<title><%= appName %></title>
</head>
<body>
<div id="<%= mountId %>"></div>
</body>
</html>
\ No newline at end of file
{
"compilerOptions": {
"target": "ES2020",
"lib": ["DOM", "ES2020"],
"module": "ESNext",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"isolatedModules": true,
"resolveJsonModule": true,
"moduleResolution": "bundler",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
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