Material UI

使用Material UI為React設計版面

2019/09/30

2019/10/06 (增加連結)

2020/07/05 (改寫)

React + Material-UI(Tables)

這次接續使用上一章的範例 (教學: Functional ComponentForm ),示範如何用Material-UI的「Tables 」模組將資料顯示在頁面上。Material Design是Google開發的設計語言,Material-UI就是一些Material Design的react模組/元件。

Install Material-UI

Material-UI的官網有安裝指令,我們直接使用npm指令安裝 :

// 用npm安装

npm install @material-ui/core

感謝伍庭儀學姊撰寫這部分的教材

Table Component

官網的左側導覽列中,點擊Components裡的裡的Tables項目。這裡有很多種功能的表格類型,我們先從最簡單的Simple Table 做練習。

範例下方的按鍵打開可以看見範例代碼:

import React from 'react';

import { makeStyles } from '@material-ui/core/styles';

import Table from '@material-ui/core/Table';

import TableBody from '@material-ui/core/TableBody';

import TableCell from '@material-ui/core/TableCell';

import TableHead from '@material-ui/core/TableHead';

import TableRow from '@material-ui/core/TableRow';

import Paper from '@material-ui/core/Paper';


const useStyles = makeStyles(theme => ({

root: {

width: '100%',

marginTop: theme.spacing(3),

overflowX: 'auto',

},

table: {

minWidth: 650,

},

}));


function createData(name, calories, fat, carbs, protein) {

return { name, calories, fat, carbs, protein };

}


const rows = [

createData('Frozen yoghurt', 159, 6.0, 24, 4.0),

createData('Ice cream sandwich', 237, 9.0, 37, 4.3),

createData('Eclair', 262, 16.0, 24, 6.0),

createData('Cupcake', 305, 3.7, 67, 4.3),

createData('Gingerbread', 356, 16.0, 49, 3.9),

];


function SimpleTable() {

const classes = useStyles();


return (

<Paper className={classes.root}>

<Table className={classes.table}>

<TableHead>

<TableRow>

<TableCell>Dessert (100g serving)</TableCell>

<TableCell align="right">Calories</TableCell>

<TableCell align="right">Fat&nbsp;(g)</TableCell>

<TableCell align="right">Carbs&nbsp;(g)</TableCell>

<TableCell align="right">Protein&nbsp;(g)</TableCell>

</TableRow>

</TableHead>

<TableBody>

{rows.map(row => (

<TableRow key={row.name}>

<TableCell component="th" scope="row">

{row.name}

</TableCell>

<TableCell align="right">{row.calories}</TableCell>

<TableCell align="right">{row.fat}</TableCell>

<TableCell align="right">{row.carbs}</TableCell>

<TableCell align="right">{row.protein}</TableCell>

</TableRow>

))}

</TableBody>

</Table>

</Paper>

);

}


export default SimpleTable;


import

最上面的部分import所需使用的模組

import React from 'react';

import { makeStyles } from '@material-ui/core/styles';

import Table from '@material-ui/core/Table';

import TableBody from '@material-ui/core/TableBody';

import TableCell from '@material-ui/core/TableCell';

import TableHead from '@material-ui/core/TableHead';

import TableRow from '@material-ui/core/TableRow';

import Paper from '@material-ui/core/Paper';

makeStyles()

Material-UI用來定義頁面樣式的hook函式。

const useStyles = makeStyles(theme => ({

root: {

width: '100%',

marginTop: theme.spacing(3),

overflowX: 'auto',

},

table: {

minWidth: 650,

},

}));

React 特別之處就在於它可以全部使用JavaScript去撰寫網頁,包含html、CSS 跟 Script(雖然這部分本來就是用JS寫的啦哈哈哈)的部分,我們可以統一使用JS定義完成。這種使用JS定義CSS的方法稱為——「 CSS in JS 」(JSS)

當然我們也可以透過import CSS檔,用傳統的方式撰寫網頁樣式,但如果使用了Material-UI,還是建議使用JSS的方式定義樣式,即便要將所有樣式集中到同一個檔案裡管理,也還是可以使用JSS的方式定義樣式。

另外,JSS要特別注意的地方是,由於JS用Object的概念定義樣式,所以有些地方的格式跟CSS不太一樣:

  1. 樣式名稱不用 ' - ' 符號連結而是駝峰式命名。例:overflowX: 'auto',原本在CSS中是 over-flow 的寫法,這裡把連結符號刪掉,第二個單字的第一個字母大寫,變成 overFlow

  2. 樣式的內容為需為變數或字串。例:width: '100%',原本在CSS 100%的數值需要加上' ' 或 " "(JS中兩著皆可);如數值不加單位例:minWidth: 650,預設單位則為 px,即 min-width 等於 650px。

  3. 樣式換行結尾使用 ' , '符號。與一般CSS用分號不同,JSS用的是逗號。

createData()

創建文件資料。這部分不會使用於此次範例中。

function createData(name, calories, fat, carbs, protein) {

return { name, calories, fat, carbs, protein };

}


const rows = [

createData('Frozen yoghurt', 159, 6.0, 24, 4.0),

createData('Ice cream sandwich', 237, 9.0, 37, 4.3),

createData('Eclair', 262, 16.0, 24, 6.0),

createData('Cupcake', 305, 3.7, 67, 4.3),

createData('Gingerbread', 356, 16.0, 49, 3.9),

];

Function Component(SimpleTable())

輸出組件頁面與嵌入資料陣列。

function SimpleTable() {

const classes = useStyles();


return (

<Paper className={classes.root}>

<Table className={classes.table}>

<TableHead>

<TableRow>

<TableCell>Dessert (100g serving)</TableCell>

<TableCell align="right">Calories</TableCell>

<TableCell align="right">Fat&nbsp;(g)</TableCell>

<TableCell align="right">Carbs&nbsp;(g)</TableCell>

<TableCell align="right">Protein&nbsp;(g)</TableCell>

</TableRow>

</TableHead>

<TableBody>

{rows.map(row => (

<TableRow key={row.name}>

<TableCell component="th" scope="row">

{row.name}

</TableCell>

<TableCell align="right">{row.calories}</TableCell>

<TableCell align="right">{row.fat}</TableCell>

<TableCell align="right">{row.carbs}</TableCell>

<TableCell align="right">{row.protein}</TableCell>

</TableRow>

))}

</TableBody>

</Table>

</Paper>

);

}


export default SimpleTable;

Functional Components

我們來為我們在Functional ComponentForm 的範例加上Material-UI吧!

在src\product\productList.js裡,用到了table,我們來改用Material-UI的Table吧! 我們先參考Simple Table。(詳參: Table API)

先加上該import的內容。

import ProductAdd from './ProductAdd'

import React, {useState} from 'react';

import { makeStyles } from '@material-ui/core/styles';

import Table from '@material-ui/core/Table';

import TableBody from '@material-ui/core/TableBody';

import TableCell from '@material-ui/core/TableCell';

import TableContainer from '@material-ui/core/TableContainer';

import TableHead from '@material-ui/core/TableHead';

import TableRow from '@material-ui/core/TableRow';

import Paper from '@material-ui/core/Paper';

設定style (詳參: @material-ui/styles),這邊的語法不是CSS的語法,而是CSS in JS,或者稱為JSS。(詳參: Objects based styles syntax for declaring Style SheetsJSS integration with React)

const useStyles = makeStyles({

table: {

minWidth: 650,

width: 'auto',

margin: 'auto',

},

})


export default function ProductList() {

const classes = useStyles();

表格內容:

return (

<TableContainer component={Paper}>

<Table className={classes.table} aria-label="simple table">

<TableHead>

<TableRow>

<TableCell>#</TableCell>

<TableCell align="left">產品描述</TableCell>

<TableCell align="right">價格</TableCell>

<TableCell align="left">產品類型</TableCell>

<TableCell align="right">庫存量</TableCell>

<TableCell align="right">安全存量</TableCell>

</TableRow>

</TableHead>

<TableBody>

{products.map((product, index) => (

<TableRow key={index}>

<TableCell component="th" scope="row">

{index}

</TableCell>

<TableCell align="left">{product.desc}</TableCell>

<TableCell align="right">{product.price}</TableCell>

<TableCell align="left">{product.category}</TableCell>

<TableCell align="right">{product.inventory}</TableCell>

<TableCell align="right">{product.safetyStock}</TableCell>

</TableRow>

))}

</TableBody>

</Table>

<ProductAdd update={update}/>

</TableContainer>


);

完整的內容

src\product\productList.js

//import React from 'react';

import ProductAdd from './ProductAdd'

import React, {useState} from 'react';

import { makeStyles } from '@material-ui/core/styles';

import Table from '@material-ui/core/Table';

import TableBody from '@material-ui/core/TableBody';

import TableCell from '@material-ui/core/TableCell';

import TableContainer from '@material-ui/core/TableContainer';

import TableHead from '@material-ui/core/TableHead';

import TableRow from '@material-ui/core/TableRow';

import Paper from '@material-ui/core/Paper';


const useStyles = makeStyles({

table: {

minWidth: 650,

width: 'auto',

margin: 'auto',

},

})


export default function ProductList() {

const classes = useStyles();

const [products, setProducts] = useState([

{desc:"iPad", price:20000, category:"平板電腦", inventory: 15, safetyStock: 10},

{desc:"iPhone X", price:30000, category:"智慧型手機", inventory: 50, safetyStock: 30}

]);



function update(newProduct){

setProducts(oldProducts=>[...oldProducts, newProduct]);

}


return (

<TableContainer component={Paper}>

<Table className={classes.table} aria-label="simple table">

<TableHead>

<TableRow>

<TableCell>#</TableCell>

<TableCell align="left">產品描述</TableCell>

<TableCell align="right">價格</TableCell>

<TableCell align="left">產品類型</TableCell>

<TableCell align="right">庫存量</TableCell>

<TableCell align="right">安全存量</TableCell>

</TableRow>

</TableHead>

<TableBody>

{products.map((product, index) => (

<TableRow key={index}>

<TableCell component="th" scope="row">

{index}

</TableCell>

<TableCell align="left">{product.desc}</TableCell>

<TableCell align="right">{product.price}</TableCell>

<TableCell align="left">{product.category}</TableCell>

<TableCell align="right">{product.inventory}</TableCell>

<TableCell align="right">{product.safetyStock}</TableCell>

</TableRow>

))}

</TableBody>

</Table>

<ProductAdd update={update}/>

</TableContainer>


);

}

productAdd用到Text FieldButtonSelect。(詳參: Button APITextField APISelect API)

Button

<Button variant="contained" color="primary" onClick={() => props.update({desc,price,category, inventory, safetyStock})}>

新增

</Button>

也可以利用className來套用style。

const useStyles = makeStyles((theme) => ({

button: {

background: theme.palette.primary.main,

color: theme.palette.primary.contrastText,

margin: theme.spacing(1),

},

}));

寫法:

<Button className={classes.button} onClick={() => props.update({desc,price,category, inventory, safetyStock})}>

新增

</Button>

root: {

'& > *': {

margin: theme.spacing(1),

width: '25ch',

},

},

  • margin使用了material預設主題的spacing設定 (詳參: Spacing),width: '25ch' 代表25個"0"的寬度 (詳參: CSS Units)。將這樣的style套到form裡面,就會讓form裡的原件都有一致的margin跟width。

root: {

'& > *': {

margin: theme.spacing(1),

width: '25ch',

},

},

將root套用在form。

<form className={classes.root} noValidate autoComplete="off">

  • style的內容,要注意重複的問題。當我們加了formControl後,事實上,是跟root的設定重複了,因為,當我們使用的時候,是將formControl放在form底下,雖然,root的規則適用,但是,又被formControl蓋掉了。所以,我們會發現,改root底下的margin是沒用的。

formControl: {

margin: theme.spacing(1),

minWidth: 120,

},

所以,同樣的設定只要留一個就好,避免未來困擾自己。

const useStyles = makeStyles((theme) => ({

root: {

'& > *': {

margin: theme.spacing(1),

width: '25ch',

},

},

button: {

background: theme.palette.primary.main,

color: theme.palette.primary.contrastText,

margin: theme.spacing(1),

},

formControl: {

minWidth: 120,

},

}));

可以直接套用theme的預設值,theme的預設值可參考: Default Theme

const useStyles = makeStyles((theme) => ({

root: {

'& > *': {

margin: theme.spacing(1),

width: '25ch',

},

},

button: {

background: theme.palette.primary.main,

color: theme.palette.primary.contrastText,

margin: theme.spacing(1),

},

formControl: {

minWidth: 120,

},

}));

有個小小的注意事項,onChange是當內容修改的時候才會去更動state。如果是這樣寫,會發現選擇"個人電腦"時,內容會是空的。

const [category, setCategory] = useState("");

要改成:

const [category, setCategory] = useState("個人電腦");

這樣的話,如果使用者沒有更動內容的話,才不會內容是空白。

完整的內容

src\product\productAdd.js

import React, {useState} from 'react';

import { makeStyles } from '@material-ui/core/styles';


import Button from '@material-ui/core/Button';

import FormControl from '@material-ui/core/FormControl';

import InputLabel from '@material-ui/core/InputLabel';

import Select from '@material-ui/core/Select';

import TextField from '@material-ui/core/TextField';


const useStyles = makeStyles((theme) => ({

root: {

'& > *': {

margin: theme.spacing(1),

width: '25ch',

},

},

button: {

background: theme.palette.primary.main,

color: theme.palette.primary.contrastText,

margin: theme.spacing(1),

},

formControl: {

minWidth: 120,

},

}));



export default function ProductAdd(props) {

const classes = useStyles();


const [desc, setDesc] = useState("");

const [price, setPrice] = useState("");

const [category, setCategory] = useState("個人電腦");

const [inventory, setInventory] = useState(0);

const [safetyStock, setSafetyStock] = useState(0);


const categories = ["個人電腦","筆記型電腦","平板電腦","智慧型手機"];

return (

<form className={classes.root} noValidate autoComplete="off">

<FormControl className={classes.formControl}>

<TextField id="desc" label="產品描述" value={desc} onChange={e => setDesc(e.target.value)}/>

</FormControl>

<FormControl className={classes.formControl}>

<TextField id="price" label="價格" value={price} onChange={e => setPrice(e.target.value)}/>

</FormControl>

<FormControl className={classes.formControl}>

<InputLabel htmlFor="filled-age-native-simple">產品類型</InputLabel>

<Select

native

value={category}

onChange={e => setCategory(e.target.value)}

inputProps={{

name: 'category',

id: 'category',

}}

>

{categories.map((category, index) => (

<option key={index} value={category}>{category}</option>

))}


</Select>

</FormControl>

<FormControl className={classes.formControl}>

<TextField id="inventory" label="庫存量" value={inventory} onChange={e => setInventory(e.target.value)}/>

</FormControl>

<FormControl className={classes.formControl}>

<TextField id="saftyStock" label="安全存量" value={safetyStock} onChange={e => setSafetyStock(e.target.value)}/>

</FormControl>

<FormControl className={classes.formControl}>

<Button className={classes.button} onClick={() => props.update({desc,price,category, inventory, safetyStock})}>

新增

</Button>

</FormControl>

</form>


);

}

目前我們寫了兩個元件,都在元件中各自定義style,比較好的方式是共用同樣的style。當元件很多的時候,這樣的寫法就不是最好的寫法,還有別的寫法,後面會再介紹。

先新增一個src/product/style.js

import { makeStyles } from '@material-ui/core/styles';


const useStyles = makeStyles((theme) => ({

root: {

'& > *': {

margin: theme.spacing(1),

width: '25ch',

},

},

table: {

minWidth: 650,

width: 'auto',

margin: 'auto',

},

button: {

background: theme.palette.primary.main,

color: theme.palette.primary.contrastText,

margin: theme.spacing(1),

},

formControl: {

minWidth: 60,

},

}));

export default useStyles;

ProductList加上import

import useStyles from './style';

因為ProductAdd被包含在ProductList裡,如果也是利用import,style會衝突,所以,將ProductList的style利用prop傳給ProductAdd。

<ProductAdd classes ={classes} update={update}/>

src/product/ProductList.js

import ProductAdd from './ProductAdd'


import React, {useState} from 'react';


import Table from '@material-ui/core/Table';

import TableBody from '@material-ui/core/TableBody';

import TableCell from '@material-ui/core/TableCell';

import TableContainer from '@material-ui/core/TableContainer';

import TableHead from '@material-ui/core/TableHead';

import TableRow from '@material-ui/core/TableRow';

import Paper from '@material-ui/core/Paper';


import useStyles from './style';


export default function ProductList() {

const classes = useStyles();

const [products, setProducts] = useState([

{desc:"iPad", price:20000, category:"平板電腦", inventory: 15, safetyStock: 10},

{desc:"iPhone X", price:30000, category:"智慧型手機", inventory: 50, safetyStock: 30}

]);



function update(newProduct){

//products.push(newProduct);

//setProducts(products);

console.log(newProduct);

setProducts(oldProducts=>[...oldProducts, newProduct]);

}


return (

<TableContainer component={Paper}>

<Table className={classes.table} aria-label="simple table">

<TableHead>

<TableRow>

<TableCell>#</TableCell>

<TableCell align="left">產品描述</TableCell>

<TableCell align="right">價格</TableCell>

<TableCell align="left">產品類型</TableCell>

<TableCell align="right">庫存量</TableCell>

<TableCell align="right">安全存量</TableCell>

</TableRow>

</TableHead>

<TableBody>

{products.map((product, index) => (

<TableRow key={index}>

<TableCell component="th" scope="row">

{index}

</TableCell>

<TableCell align="left">{product.desc}</TableCell>

<TableCell align="right">{product.price}</TableCell>

<TableCell align="left">{product.category}</TableCell>

<TableCell align="right">{product.inventory}</TableCell>

<TableCell align="right">{product.safetyStock}</TableCell>

</TableRow>

))}

</TableBody>

</Table>

<ProductAdd classes ={classes} update={update}/>

</TableContainer>


);

}

利用props的classes

const classes = props.classes;

src/product/ProductAdd.js

import React, {useState} from 'react';


import Button from '@material-ui/core/Button';

import FormControl from '@material-ui/core/FormControl';

import InputLabel from '@material-ui/core/InputLabel';

import Select from '@material-ui/core/Select';

import TextField from '@material-ui/core/TextField';


export default function ProductAdd(props) {

const classes = props.classes; //useStyles in ProductList to prevent conflict


const [desc, setDesc] = useState("");

const [price, setPrice] = useState("");

const [category, setCategory] = useState("個人電腦");

const [inventory, setInventory] = useState(0);

const [safetyStock, setSafetyStock] = useState(0);


const categories = ["個人電腦","筆記型電腦","平板電腦","智慧型手機"];

return (

<form className={classes.root} noValidate autoComplete="off">

<FormControl className={classes.formControl}>

<TextField id="desc" label="產品描述" value={desc} onChange={e => setDesc(e.target.value)}/>

</FormControl>

<FormControl className={classes.formControl}>

<TextField id="price" label="價格" value={price} onChange={e => setPrice(e.target.value)}/>

</FormControl>

<FormControl className={classes.formControl}>

<InputLabel htmlFor="category">產品類型</InputLabel>

<Select

native

value={category}

onChange={e => setCategory(e.target.value)}

inputProps={{

name: 'category',

id: 'category',

}}

>

{categories.map((category, index) => (

<option key={index} value={category}>{category}</option>

))}


</Select>

</FormControl>

<FormControl className={classes.formControl}>

<TextField id="inventory" label="庫存量" value={inventory} onChange={e => setInventory(e.target.value)}/>

</FormControl>

<FormControl className={classes.formControl}>

<TextField id="saftyStock" label="安全存量" value={safetyStock} onChange={e => setSafetyStock(e.target.value)}/>

</FormControl>

<FormControl className={classes.formControl}>

<Button className={classes.button} onClick={() => props.update({desc,price,category, inventory, safetyStock})}>

新增

</Button>

</FormControl>

</form>


);

}

作業

試試看把新增的部分,改用Dialog,並利用Floating action button

參考資料

Class Components

先來改寫ProductList.js,Table裡包一個TableBody。

import React, {Component} from 'react';

import Product from './product';

import Table from '@material-ui/core/Table';

import TableBody from '@material-ui/core/TableBody';


export default class ProductList extends Component {



state = {

products: [

{id:"0", desc:"iPad", price:20000},

{id:"1", desc:"iPhone X", price:30000}

]

}


render() {

return (


<Table>

<TableBody>

{ this.state.products.map((product, index) => <Product key ={key} product={product}/>)}

</TableBody>

</Table>

)

}


}

再來改寫Product.js,TableRow裡包三個TableCell。

import React, {Component} from 'react';

import TableCell from '@material-ui/core/TableCell';

import TableRow from '@material-ui/core/TableRow';

export default class Product extends Component{


render(){

return (

<TableRow>

<TableCell>{this.props.product.id}</TableCell>

<TableCell>{this.props.product.desc}</TableCell>

<TableCell> {this.props.product.price}</TableCell>

</TableRow>

)

}

}

這樣,我們套用了Material-UI的Table了。

我們來加上表頭,TableHead裡包三個TableCell。

import React, {Component} from 'react';

import Product from './product';

import Table from '@material-ui/core/Table';

import TableBody from '@material-ui/core/TableBody';

import TableCell from '@material-ui/core/TableCell';

import TableHead from '@material-ui/core/TableHead';


export default class ProductList extends Component {



state = {

products: [

{id:"0", desc:"iPad", price:20000},

{id:"1", desc:"iPhone X", price:30000}

]

}


render() {

return (


<Table>

<TableHead>

<TableRow>

<TableCell>ID</TableCell>

<TableCell>Desc</TableCell>

<TableCell>Price</TableCell>

</TableRow>

</TableHead>

<TableBody>

{ this.state.products.map((product, index) => <Product key = {index} product={product}/>)}

</TableBody>

</Table>

)

}


}

Style

比起Functional Components,Class Component在這部分比較複雜一點。

這邊用到material提供的一個Higher Order Component, withStyles (Day 25 - 二周目 - React component 套件 Material-UI: Google Material Design 的實作套件),將styles當作props傳進ProductList,再重新命名為classes。

import React, {Component} from 'react';

import Product from './product';

import Table from '@material-ui/core/Table';

import TableBody from '@material-ui/core/TableBody';

import TableCell from '@material-ui/core/TableCell';

import TableHead from '@material-ui/core/TableHead';

import TableRow from '@material-ui/core/TableRow';

import Paper from '@material-ui/core/Paper';


import { withStyles } from "@material-ui/core/styles";

const styles = theme => ({

root: {

width: '85%',

marginTop: theme.spacing(3),

marginLeft: theme.spacing(3),

overflowX: 'auto', },

table: {

minWidth: 450,

},

});


class ProductList extends Component {

state = {

products: [

{id:"0", desc:"iPad", price:20000},

{id:"1", desc:"iPhone X", price:30000}

]

}


render() {

return (

<Paper className={this.props.classes.root}>

<Table className={this.props.classes.table}>

<TableHead>

<TableRow>

<TableCell>ID</TableCell>

<TableCell>Desc</TableCell>

<TableCell>Price</TableCell>

</TableRow>

</TableHead>

<TableBody>

{ this.state.products.map((product, index) => <Product key={index} product={product}/>)}

</TableBody>

</Table>

</Paper>

)

}


}

export default withStyles(styles) (ProductList);

可以將傳進來的props改個名稱

const { classes } = this.props;

完整的程式碼:

import React, {Component} from 'react';

import Product from './product';

import Table from '@material-ui/core/Table';

import TableBody from '@material-ui/core/TableBody';

import TableCell from '@material-ui/core/TableCell';

import TableHead from '@material-ui/core/TableHead';

import Paper from '@material-ui/core/Paper';


import { withStyles } from "@material-ui/core/styles";


const styles = theme => ({

root: {

width: '85%',

marginTop: theme.spacing(3),

marginLeft: theme.spacing(3),

overflowX: 'auto',

},

table: {

minWidth: 450,

},

});


class ProductList extends Component {

state = {

products: [

{id:"0", desc:"iPad", price:20000},

{id:"1", desc:"iPhone X", price:30000}

]

}


render() {

const { classes } = this.props;

//is equivalent to

//const classes = this.props.classes;

return (

<Paper className={classes.root}>

<Table className={classes.table}>

<TableHead>

<TableRow>

<TableCell>ID</TableCell>

<TableCell>Desc</TableCell>

<TableCell>Price</TableCell>

</TableRow>

</TableHead>

<TableBody>

{ this.state.products.map((product, index) => <Product key ={index} product={product}/>)}

</TableBody>

</Table>

</Paper>

)

}


}

export default withStyles(styles)(ProductList);

作業

請參考範例,改寫Class Component作業:

  • 編號 / id (int)

  • 產品描述 / desc (String)

  • 價格 / price (int)

  • 產品類型 / category (String) (個人電腦、筆記型電腦、平板電腦、智慧型手機)

  • 庫存量 / inventory (int)

  • 安全存量 / safetyStock (int)