Commit b865ee76 by wong.peiyi

1. 重构初始化、引入ts、stylus、mobx、antd-mobile、inversify

2. 登录页面及模态框
parent ca0afd94
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: [
["import", { libraryName: "@ant-design/react-native" }],
["module-resolver", {
"root": ["./app"],
"extensions": [".js", ".jsx", ".ts", ".tsx", ".ios.js", ".android.js"]
......
......@@ -8,13 +8,16 @@
"test": "jest"
},
"dependencies": {
"@ant-design/react-native": "^4.1.0",
"@babel/plugin-proposal-decorators": "^7.13.15",
"@react-native-community/async-storage": "^1.12.1",
"babel-plugin-import": "^1.13.3",
"inversify": "^5.0.5",
"mobx": "^5.0.0",
"mobx-persist": "^0.4.1",
"mobx-react": "~6.0.0",
"moment": "2.29.1",
"querystring": "^0.2.1",
"ramda": "^0.27.1",
"react": "16.8.3",
"react-native": "0.59.9",
......
......@@ -9,8 +9,8 @@ import {
import { font_family_regular, first_text_color } from './assets/styles/base'
import Resolution from './components/common/Resolution'
import Home from './pages/index'
import Mine from './pages/Mine'
import Signin from './pages/Signin'
import Mine from './pages/mine/mine'
import Signin from './pages/signin/signin'
function createNavigator() {
const options = {
......
@import './variable.styl'
.container
flex 1
.center
align-items center
.middle
justify-content center
.icon
resizeMode cover
width 100%
height 100%
hToR($color, opacity = 1)
return rgba(red($color), green($color), blue($color), opacity)
// 背景色
primary_color = #007EFF // 主色
foundation_color = #ffffff // 底色
promary_shadow_color = #3CA2FF // 按钮阴影色
home_background_color = #F7F7F7 // 背景色
btn_sub_color = #0296F7 // 按钮色
dis_sub_color = #BBBBBB // 禁用按钮色
input_background_color = #efefef // 输入框底色
// 字体色
primary_text_color = #000000 // 主字颜色
title_text_color = #ffffff // 标题颜色
placehold_text_color = #919191 // input placeholder颜色
first_text_color = #333333 // 一级字体
second_text_color = #666666 // 次级字体
third_text_color = #999999 // 三级字体
point_color = #ff0000 // * 颜色
text_default_color = #01B2B9 // 默认颜色
text_audit_color = #FF0000 // 拒绝颜色
text_return_color = #007EFF // 归还颜色
text_other_color = #F4B61B // 其他颜色
list_tit_color = rgba(0, 0, 0, 0.87) // 列表标题颜色
list_str_color = #0CB4E8 // 列表加粗颜色
list_one_color = #1B40B5 // 列表一级颜色
list_thr_color = #3B4C82 // 列表其他颜色
list_one_light_color = #3c64e2 // 列表一级较浅颜色
// 字号
first_text_size = 20px // 一级字号
second_text_size = 16px // 二级字号
third_text_size = 12px // 三级字号
// 字体样式
font_family_semibold = 'PingFangSC-Semibold'
font_family_medium = 'PingFangSC-Medium'
font_family_regular = 'PingFangSC-Regular'
font_family_light = 'PingFangSC-Light'
header_height = 58
......@@ -31,7 +31,6 @@ export default class Resolution {
props.fw = { width: fwWidth, height: fwHeight, scale: fwScale, navHeight }
props.fh = { width: fhWidth, height: fhHeight, scale: fhScale, navHeight }
console.log(props);
}
static FixWidthView(p) {
......
import React, { Component } from 'react'
import { Modal, Provider } from '@ant-design/react-native'
import styles from './base.styl'
export type IProps = {
visible: boolean
title: string
maskClosable: boolean
updateVisible: Function
footerButtons?: any[]
}
export default class BModal extends Component<IProps> {
constructor(props) {
super(props)
}
onShow(visible = true) {
this.props.updateVisible(visible)
}
onClose(visible = false) {
this.props.updateVisible(visible)
}
render() {
let { title, visible, footerButtons, maskClosable = false } = this.props
if (visible) {
return (
<Provider>
<Modal
title={title}
transparent={true}
onClose={() => {
this.onClose()
}}
maskClosable={maskClosable}
visible={visible}
footer={footerButtons}
style={styles.base}
>
{this.props.children}
</Modal>
</Provider>
)
} else {
return <></>
}
}
}
@import '../../../assets/styles/base.styl'
@import '../../../assets/styles/variable.styl'
.form
&-item
padding 15px 5px 15px 20px
margin 30px 10px 40px
border-radius 50px
flex-direction row
background-color rgba(239, 239, 239, 1)
&-label
width 55px
font-weight bold
&-input
flex 1
import React, { Component } from 'react'
import { View, Text, TextInput, TouchableHighlight } from 'react-native'
import { inject, observer } from 'mobx-react'
import * as R from 'ramda'
import BaseModal from '../base/base'
import { placehold_text_color } from '../../../assets/styles/base'
import { g } from '../../../utils/utils'
import styles from './host.styl'
export type IProps = {
visible: boolean
updateVisible: Function
store: {
host: string
setHost: Function
}
}
class HostModal extends Component<IProps> {
state = {
protocol: 'https',
domain: '',
}
footerBtns = [
{
text: '取消',
onPress: () => console.log('取消'),
style: g(styles, 'btn'),
},
{
text: '确定',
onPress: () => {
const { domain, protocol } = this.state
this.props.store.setHost(protocol + '://' + domain)
},
style: g(styles, 'btn'),
},
]
constructor(props) {
super(props)
this.updateVisible = this.updateVisible.bind(this)
}
componentWillReceiveProps(nextProps) {
// 每次打开弹窗的时候,设置一下当前的host
if (!this.props.visible && nextProps.visible) {
this.getDomainValue()
}
}
/**
* 打开或关闭弹窗
* @param visible
*/
updateVisible(visible: boolean) {
this.props.updateVisible && this.props.updateVisible(visible)
}
/**
* 获取域名
* @param host
*/
getDomainValue() {
const { host } = this.props.store
let domain = R.split('://', host)
this.setState({ domain: domain[1] })
}
render() {
const { visible } = this.props
const { protocol, domain } = this.state
return (
<BaseModal
title="配置域名"
visible={visible}
updateVisible={this.updateVisible}
footerButtons={this.footerBtns}
>
<View style={g(styles, 'form-item')}>
<Text style={g(styles, 'form-label')}>{protocol}://</Text>
<TextInput
autoCapitalize="none"
placeholder="请输入要请求的域名"
placeholderTextColor={placehold_text_color}
returnKeyType="done"
style={g(styles, 'form-input')}
defaultValue={domain}
clearButtonMode="while-editing"
onChangeText={text => this.setState({ domain: text })}
></TextInput>
</View>
</BaseModal>
)
}
}
export default inject('store')(observer(HostModal))
export enum MsgType {
INFO,
WARN,
ERROR,
SUCCESS,
}
import 'reflect-metadata'
import { Container } from 'inversify'
import { TYPES } from './types'
import Service from '../services/service'
import Store from '../stores/store'
import System from '../stores/system'
import User from '../stores/user'
const container = new Container({ defaultScope: 'Singleton' })
container.bind<Service>(TYPES.Service).to(Service)
container.bind<Store>(TYPES.Store).to(Store)
container.bind<System>(TYPES.SysStore).to(System)
container.bind<User>(TYPES.UserStore).to(User)
export default container
export const TYPES = {
Service: Symbol.for('service'),
Store: Symbol.for('store'),
SysStore: Symbol.for('system'),
UserStore: Symbol.for('user'),
}
import React, { Component } from 'react'
import { View, Text, TouchableOpacity } from 'react-native'
import Resolution from '../components/common/Resolution'
class Signin extends Component {
render() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<TouchableOpacity onPress={() => this.props.navigation.navigate('Main')}>
<Text style={{ fontSize: 18 }}>Signin Screen</Text>
</TouchableOpacity>
</View>
)
}
}
export default Signin
......@@ -8,8 +8,7 @@ import styles from './index.styl'
type IProps = {
store: {
count: number
inc: Function
token: string
}
}
......@@ -17,37 +16,18 @@ class Index extends Component<IProps> {
componentDidMount() {
slashScreen.hide()
const { navigation, store } = this.props
this.timer = setInterval(() => {
// store.inc()
}, 1000)
setTimeout(() => {
const { navigation, store } = this.props
!store.token && navigation.navigate('Signin')
}, 0)
}
componentWillUnmount() {
clearInterval(this.timer)
}
componentWillUnmount() {}
render() {
const { navigation, store } = this.props
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<TouchableHighlight
onPress={() => {
navigation.navigate('Signin', {})
}}
>
<Text style={g(styles, ['index-text', 'index-bg'])}>count: {store.count}</Text>
</TouchableHighlight>
<TouchableHighlight
onPress={() => {
navigation.navigate('Signin', {})
}}
>
<Text style={{ fontSize: 18 }}>Home Screen Heloo</Text>
</TouchableHighlight>
</View>
)
console.log(store)
return <View style></View>
}
}
......
@import '../../assets/styles/base.styl'
@import '../../assets/styles/variable.styl'
.container
flex 1
.login
&-bg
position absolute
top 0
left 0
right 0
bottom 0
&-box
flex 1
justify-content center
&-title
position relative
padding-left 33px
margin-bottom 40px
padding-top 70px
&-decorator
width 30px
height 3px
background-color title_text_color
margin-top 14px
&-text
font-size 21px
font-family font_family_semibold
color title_text_color
&-card
width 348px
height 390px
border-radius 16px
background-color foundation_color
margin-bottom 50px
align-self center
padding 38px 30px
&__title
padding-bottom 23px
&-text
color primary_text_color
font-size 21px
font-family font_family_semibold
&__form-item
margin-bottom 23px
width 288px
height 45px
border-radius 100px
background-color rgba(239, 239, 239, 1)
padding-left 20px
padding-right 10px
justify-content center
&-input
font-size 14px
&__btns
margin-top 40px
&__btn
width 288px
height 45px
border-radius 100px
background btn_sub_color
@extend .middle
@extend .center
&-text
color title_text_color
font-size 21px
font-family font_family_regular
&-plain
align-items flex-end
margin-top 14px
&-text
color second_text_color
font-size 14px
&-footer
align-self center
&-version
text-align center
color home_background_color
font-size second_text_size
padding-top 5px
font-family font_family_semibold
import React, { Component } from 'react'
import {
View,
Text,
TouchableOpacity,
SafeAreaView,
ImageBackground,
Image,
TextInput,
} from 'react-native'
import { inject, observer } from 'mobx-react'
import debounce from 'lodash.debounce'
import { placehold_text_color } from '../../assets/styles/base'
import styles from './signin.styl'
import { g, isBlank, show } from '../../utils/utils'
import { MsgType } from '../../enums'
import HostModal from '../../components/modals/host/host'
type IProps = {
store: {
version: string
setHost: Function
}
userStore: {
username: string
password: string
signin: Function
}
}
/**
* 登录页面
*/
class Signin extends Component<IProps> {
state = {
username: '',
password: '',
visible: false,
loading: false,
}
constructor(props) {
super(props)
this.loginHandler = debounce(this.loginHandler.bind(this), 300)
this.updateVisible = this.updateVisible.bind(this)
}
componentDidMount() {
// 从刚开始的asyncStorage中获取东西要等一下
setTimeout(() => {
const { username, password } = this.props.userStore
this.setState({ username, password })
}, 500)
}
/**
* 登录
*/
async loginHandler() {
const { username, password, loading } = this.state
const { navigation } = this.props
if (loading) return
if (isBlank(username)) return show('请输入您的用户名')
if (isBlank(password)) return show('请输入您的密码')
await this.props.userStore.signin(username, password)
navigation.navigate('Main')
}
/**
* 打开域名弹窗
* @param visible
*/
updateVisible(visible: boolean) {
this.setState({ visible: visible })
}
render() {
const { store } = this.props
const { username, password, showPassword, visible } = this.state
return (
<View style={g(styles, 'container')}>
<SafeAreaView style={styles.container}>
<ImageBackground
resizeMode="cover"
source={require('../../../app/images/login_bg.png')}
style={g(styles, 'login-bg')}
>
<View style={g(styles, 'login-box')}>
<View style={g(styles, 'login-title')}>
<Text style={g(styles, 'login-title-text')}>骨科智慧仓</Text>
<View style={g(styles, 'login-title-decorator')}></View>
</View>
{/* 中间主逻辑部分 */}
<View style={g(styles, 'login-card')}>
<View style={g(styles, 'login-card__title')}>
<Text style={g(styles, 'login-card__title-text')}>账号登陆</Text>
</View>
<View style={g(styles, 'login-card__form-item')}>
<TextInput
autoCapitalize="none"
placeholder="请输入您的用户名"
placeholderTextColor={placehold_text_color}
returnKeyType="next"
style={g(styles, 'login-card__form-item-input')}
defaultValue={username}
clearButtonMode="while-editing"
onChangeText={text => this.setState({ username: text })}
></TextInput>
</View>
<View style={g(styles, 'login-card__form-item')}>
<TextInput
autoCapitalize="none"
placeholder="请输入您的密码"
placeholderTextColor={placehold_text_color}
returnKeyType="done"
style={g(styles, 'login-card__form-item-input')}
defaultValue={password}
secureTextEntry={!showPassword}
clearButtonMode="while-editing"
onChangeText={text => this.setState({ password: text })}
// onFocus={() => this.setState({ focuOnPw: true })}
// onBlur={() => this.setState({ focuOnPw: false })}
/>
{/* {!!password && focuOnPw && (
<TouchableWithoutFeedback
onPress={() => this.setState({ showPassword: !showPassword })}
style={styles.view_password_btn}
>
<Image
source={require('../../images/eye.png')}
style={styles.view_password}
/>
</TouchableWithoutFeedback>
)} */}
</View>
<View style={g(styles, 'login-card__btns')}>
<TouchableOpacity
style={g(styles, 'login-card__btn')}
onPress={this.loginHandler}
activeOpacity={0.8}
>
<Text style={g(styles, 'login-card__btn-text')}>登录</Text>
</TouchableOpacity>
<TouchableOpacity
style={g(styles, 'login-card__btn-plain')}
onPress={() => {
this.updateVisible(true)
}}
activeOpacity={0.8}
>
<Text style={g(styles, 'login-card__btn-plain-text')}>修改域名</Text>
</TouchableOpacity>
</View>
</View>
{/* 最后部分 */}
<View style={g(styles, 'login-footer')}>
<Image
style={g(styles, 'login-footer-image')}
source={require('../../../app/images/logo_foo.png')}
></Image>
<Text style={g(styles, 'login-footer-version')}>v{store.version}</Text>
</View>
</View>
</ImageBackground>
{/* 域名弹窗 */}
<HostModal visible={visible} updateVisible={this.updateVisible} />
</SafeAreaView>
</View>
)
}
}
export default inject('store', 'userStore')(observer(Signin))
// @ts-nocheck
import * as R from 'ramda'
import { isBlank } from '../utils/utils'
import { stringify } from 'querystring'
import Store from '../stores/store'
import container from '../inversify'
import { TYPES } from '../inversify/types'
import { transformObject } from '../utils/transform'
interface RequestConfig {
url: string
method: string
data: any
headers: any
}
export const request = (args: Partial<RequestConfig>) => {
const store = container.get<Store>(TYPES.Store)
return new Promise((resolve, reject) => {
let options: any = {
headers: {
'Content-Type': 'application/json',
},
method: R.propOr('get', 'method', args),
reTries: R.propOr(3, 'reTries', args),
...args,
}
options = R.cond([
[
R.propEq('method', 'get'),
R.applySpec({ url: () => args.url + '?' + stringify(args.data) }),
],
[R.T, () => R.assoc('body', JSON.stringify(args.data), options)],
])(options)
let requestTimes = 0
function doRequest() {
requestTimes += 1
console.log('请求URL:', store.host + options.url)
fetch(store.host + options.url, options)
.then(res => res.json())
.then(res => {
console.log('返回结果:', res)
R.ifElse(
fetchSuccess,
() => resolve(transformObject(res, 'toHump')),
() => {
throw res
},
)(res)
})
.catch(e => {
if (requestTimes <= options.reTries) {
return doRequest()
}
// TODO: 登录判断
// failHandler(e)
// throw e
})
}
doRequest()
})
}
const fetchSuccess = R.propEq('error_code', 0)
const failHandler = async (err: any) => {
R.ifElse(
isBlank,
() => {
// Message.error(`${err.status} ${err.statusText}`)
},
() => {
// Message.error(`${err.code} ${err.msg}`)
},
)(err.code)
}
import { request } from './request'
import { injectable } from 'inversify'
const ctx = '/api/latest'
@injectable()
export default class Service {
/**
* 获取系统配置
*/
getSysProfile(data: any) {
return request({ url: `${ctx}/system/sys_profile/search`, data })
}
/**
* 登录
* @param data
* @returns
*/
signin(data: { data: { user_name: string; user_password: string; grant_type: string } }) {
return request({
url: `${ctx}/access_token/password/search?app_code=MOBILE`,
data,
method: 'POST',
})
}
}
......@@ -4,6 +4,8 @@ import { AsyncStorage } from 'react-native'
import { create } from 'mobx-persist'
const store = container.get<any>(TYPES.Store)
const sysStore = container.get<any>(TYPES.SysStore)
const userStore = container.get<any>(TYPES.UserStore)
const hydrate = create({
storage: AsyncStorage,
......@@ -12,5 +14,7 @@ const hydrate = create({
})
hydrate('store', store)
hydrate('sysStore', sysStore)
hydrate('userStore', userStore)
export default { store }
export default { store, sysStore, userStore }
import { observable, action, reaction } from 'mobx'
import { observable, action, runInAction } from 'mobx'
import { persist } from 'mobx-persist'
import { injectable } from 'inversify'
import { NativeModules } from 'react-native'
import container from '../inversify'
import { TYPES } from '../inversify/types'
import { IFunction } from 'bonehouse'
@injectable()
export default class Store {
@persist @observable count: number = 0
// 默认域名
@persist @observable host: string = 'https://obs.dev.xhk.guke.tech'
@persist @observable token?: string
@persist @observable version: string = '2.0.0'
// 功能模块
@persist('list') @observable funtions!: IFunction[]
@action
inc() {
this.count = this.count + 1
setHost(host: string) {
this.host = host
}
@action
setToken(token: string) {
this.token = token
}
@action
setVersion(v: string) {
this.version = v
}
@action
setFunctions(funtions: IFunction[]) {
this.funtions = funtions
}
}
runInAction(() => {
NativeModules.RNToolsManager.getAppVersion((v: string) => {
const store = container.get<Store>(TYPES.Store)
store.setVersion(v)
})
})
import { observable, action, flow } from 'mobx'
import { persist } from 'mobx-persist'
import { injectable } from 'inversify'
@injectable()
export default class System {
}
import { observable, action, flow, runInAction } from 'mobx'
import { persist } from 'mobx-persist'
import { injectable, inject } from 'inversify'
import { TYPES } from '../inversify/types'
import Service from '../services/service'
import Store from './store'
import { IDepartment, IInventory } from 'bonehouse'
@injectable()
export default class UserStore {
@inject(TYPES.Service)
service!: Service
@inject(TYPES.Store)
store!: Store
// 登录信息
@persist @observable private username: string = ''
@persist @observable private password: string = ''
// 用户信息
@persist @observable private userName: string = ''
@persist @observable private personName: string = ''
@persist @observable private gender: string = ''
@persist('map') @observable private department!: IDepartment
// 用户仓库
@persist('list') @observable inventories!: IInventory[]
@action
setUsername(username: string) {
this.username = username
}
@action
setPassword(password: string) {
this.password = password
}
/**
* 登录
*/
signin = flow(function* (this: UserStore, username, password) {
const params = {
data: {
grant_type: 'PASSWORD',
user_name: username,
user_password: password,
},
}
const res = yield this.service.signin(params)
this.username = username
this.password = password
this.department = {
departmentCode: res.departmentCode,
departmentName: res.departmentName,
}
this.inventories = res.inventorys
this.userName = res.userName
this.personName = res.personName
this.store.setToken(res.accessToken)
this.store.setFunctions(res.functions)
})
}
import { transformObject, toHump, toLine } from './transform'
describe('Object Transform Test', () => {
test('it should be underline', () => {
expect(toLine('aBBca23DefG')).toStrictEqual('a_b_bca23_def_g')
})
})
import * as R from 'ramda'
import { isBlank } from './utils'
/**
* 下划线转换驼峰
* @param name
*/
export function toHump(name: string) {
return name.replace(/\_(\w)/g, function (all, letter) {
return letter.toUpperCase()
})
}
/**
* 驼峰转换下划线
* @param name string
* @returns
*/
export function toLine(name: string) {
return name.replace(/([A-Z])/g, '_$1').toLowerCase()
}
export type ITransformTypes = 'toHump' | 'toLine'
export type ITransObj = { [key: string]: any } | any[]
/**
* 对象下划线和驼峰转换
* @param obj 转换对象
* @param type string 'toHump'|'toLine' 转换类型
*/
export function transformObject(obj: ITransObj, type: ITransformTypes = 'toHump') {
if (isBlank(obj)) {
return null
}
const fn = type === 'toHump' ? toHump : toLine
const transform = (item: any) => {
const desc: ITransObj = {}
R.compose(
R.map((key: string) => {
const dKey = fn.call(null, key)
if (R.type(item[key]) === 'Array' || R.type(item[key]) === 'Object') {
desc[dKey] = isBlank(item[key]) ? item[key] : transformObject(item[key])
} else {
desc[dKey] = item[key]
}
}),
R.keys,
)(item)
return desc
}
let des
if (R.type(obj) === 'Array') {
des = R.map(transform)(obj as any)
} else {
des = transform(obj)
}
return des
}
import * as R from 'ramda'
import Toast from 'react-native-root-toast'
import { MsgType } from '../enums'
export const isBlank = R.anyPass([R.isNil, R.isEmpty])
......@@ -20,10 +22,63 @@ export const getStyles = (styles: any, ...cls: any[]) => {
return getStyles(styles, ...key)
}
return [styles[key]]
})
}),
)(cls)
}
return clses
}
/**
* 样式对象辅助函数
* @param styles 样式
* @param cls 类名
* @returns 样式对象
*/
export const g = getStyles
/**
* 提示框
* @param {*} data 提示内容
* @param {*} type 提示类型
*/
export const show = (data: string, type = MsgType.INFO, position = Toast.positions.CENTER) => {
if (isBlank(data)) return
const types = {
[MsgType.WARN]: {
textColor: '#fa8c16',
backColor: '#fff7e6',
},
[MsgType.ERROR]: {
textColor: '#f5222d',
backColor: '#fff1f0',
},
[MsgType.SUCCESS]: {
textColor: '#52c41a',
backColor: '#f6ffed',
},
[MsgType.INFO]: {
textColor: '#fafafa',
backColor: '#222',
},
}
const colors = types[type]
if (type === MsgType.ERROR) {
data = ${data}`
} else if (type === MsgType.WARN) {
data = `! ${data}`
} else if (type == MsgType.SUCCESS) {
data = `√ ${data}`
}
Toast.show(data, {
duration: Toast.durations.LONG,
position,
shadow: false,
hideOnPress: true,
delay: 0,
visible: true,
backgroundColor: colors.backColor,
textColor: colors.textColor,
shadowColor: colors.textColor,
})
}
......@@ -11,7 +11,9 @@
"noEmit": true,
"strict": true,
"target": "esnext",
"rootDir": "."
"rootDir": ".",
"typeRoots": ["node_modules/@types", "types"]
},
"exclude": ["node_modules", "babel.config.js", "metro.config.js", "jest.config.js"]
"exclude": ["node_modules", "babel.config.js", "metro.config.js", "jest.config.js"],
"include": ["src", "types"]
}
/// <reference types="react" />
declare module 'bonehouse' {
export type IDepartment = {
departmentCode: string
departmentName: string
}
export type IFunction = {
child_list: IFunction[]
function_code: string
function_name: string
function_order: number
}
export type IInventory = {
invCode: string
invName: string
}
}
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 sign in to comment