Add product pages.

This commit is contained in:
Yang Luo 2022-02-27 18:20:58 +08:00
parent 2c97f8a8b7
commit 39ab71c5db
19 changed files with 1102 additions and 25 deletions

116
controllers/product.go Normal file
View File

@ -0,0 +1,116 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/astaxie/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetProducts
// @Title GetProducts
// @Tag Product API
// @Description get products
// @Param owner query string true "The owner of products"
// @Success 200 {array} object.Product The Response object
// @router /get-products [get]
func (c *ApiController) GetProducts() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetProducts(owner)
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetProductCount(owner, field, value)))
products := object.GetPaginationProducts(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
c.ResponseOk(products, paginator.Nums())
}
}
// @Title GetProduct
// @Tag Product API
// @Description get product
// @Param id query string true "The id of the product"
// @Success 200 {object} object.Product The Response object
// @router /get-product [get]
func (c *ApiController) GetProduct() {
id := c.Input().Get("id")
c.Data["json"] = object.GetProduct(id)
c.ServeJSON()
}
// @Title UpdateProduct
// @Tag Product API
// @Description update product
// @Param id query string true "The id of the product"
// @Param body body object.Product true "The details of the product"
// @Success 200 {object} controllers.Response The Response object
// @router /update-product [post]
func (c *ApiController) UpdateProduct() {
id := c.Input().Get("id")
var product object.Product
err := json.Unmarshal(c.Ctx.Input.RequestBody, &product)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.UpdateProduct(id, &product))
c.ServeJSON()
}
// @Title AddProduct
// @Tag Product API
// @Description add product
// @Param body body object.Product true "The details of the product"
// @Success 200 {object} controllers.Response The Response object
// @router /add-product [post]
func (c *ApiController) AddProduct() {
var product object.Product
err := json.Unmarshal(c.Ctx.Input.RequestBody, &product)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.AddProduct(&product))
c.ServeJSON()
}
// @Title DeleteProduct
// @Tag Product API
// @Description delete product
// @Param body body object.Product true "The details of the product"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-product [post]
func (c *ApiController) DeleteProduct() {
var product object.Product
err := json.Unmarshal(c.Ctx.Input.RequestBody, &product)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.DeleteProduct(&product))
c.ServeJSON()
}

View File

@ -17,7 +17,6 @@ package object
import (
"fmt"
"runtime"
"xorm.io/core"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor/conf"
@ -25,6 +24,7 @@ import (
//_ "github.com/denisenkom/go-mssqldb" // db = mssql
_ "github.com/go-sql-driver/mysql" // db = mysql
//_ "github.com/lib/pq" // db = postgres
"xorm.io/core"
"xorm.io/xorm"
)
@ -183,6 +183,11 @@ func (a *Adapter) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(Product))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Payment))
if err != nil {
panic(err)

129
object/product.go Normal file
View File

@ -0,0 +1,129 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
type Product struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Image string `xorm:"varchar(100)" json:"image"`
Tag string `xorm:"varchar(100)" json:"tag"`
Currency string `xorm:"varchar(100)" json:"currency"`
Price int `json:"price"`
Quantity int `json:"quantity"`
Sold int `json:"sold"`
Providers []string `xorm:"varchar(100)" json:"providers"`
State string `xorm:"varchar(100)" json:"state"`
}
func GetProductCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Product{})
if err != nil {
panic(err)
}
return int(count)
}
func GetProducts(owner string) []*Product {
products := []*Product{}
err := adapter.Engine.Desc("created_time").Find(&products, &Product{Owner: owner})
if err != nil {
panic(err)
}
return products
}
func GetPaginationProducts(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Product {
products := []*Product{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&products)
if err != nil {
panic(err)
}
return products
}
func getProduct(owner string, name string) *Product {
if owner == "" || name == "" {
return nil
}
product := Product{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&product)
if err != nil {
panic(err)
}
if existed {
return &product
} else {
return nil
}
}
func GetProduct(id string) *Product {
owner, name := util.GetOwnerAndNameFromId(id)
return getProduct(owner, name)
}
func UpdateProduct(id string, product *Product) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getProduct(owner, name) == nil {
return false
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(product)
if err != nil {
panic(err)
}
return affected != 0
}
func AddProduct(product *Product) bool {
affected, err := adapter.Engine.Insert(product)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteProduct(product *Product) bool {
affected, err := adapter.Engine.ID(core.PK{product.Owner, product.Name}).Delete(&Product{})
if err != nil {
panic(err)
}
return affected != 0
}
func (product *Product) GetId() string {
return fmt.Sprintf("%s/%s", product.Owner, product.Name)
}

View File

@ -149,6 +149,12 @@ func initAPI() {
beego.Router("/api/add-cert", &controllers.ApiController{}, "POST:AddCert")
beego.Router("/api/delete-cert", &controllers.ApiController{}, "POST:DeleteCert")
beego.Router("/api/get-products", &controllers.ApiController{}, "GET:GetProducts")
beego.Router("/api/get-product", &controllers.ApiController{}, "GET:GetProduct")
beego.Router("/api/update-product", &controllers.ApiController{}, "POST:UpdateProduct")
beego.Router("/api/add-product", &controllers.ApiController{}, "POST:AddProduct")
beego.Router("/api/delete-product", &controllers.ApiController{}, "POST:DeleteProduct")
beego.Router("/api/get-payments", &controllers.ApiController{}, "GET:GetPayments")
beego.Router("/api/get-payment", &controllers.ApiController{}, "GET:GetPayment")
beego.Router("/api/update-payment", &controllers.ApiController{}, "POST:UpdatePayment")

View File

@ -43,6 +43,8 @@ import SyncerListPage from "./SyncerListPage";
import SyncerEditPage from "./SyncerEditPage";
import CertListPage from "./CertListPage";
import CertEditPage from "./CertEditPage";
import ProductListPage from "./ProductListPage";
import ProductEditPage from "./ProductEditPage";
import PaymentListPage from "./PaymentListPage";
import PaymentEditPage from "./PaymentEditPage";
import AccountPage from "./account/AccountPage";
@ -128,6 +130,8 @@ class App extends Component {
this.setState({ selectedMenuKey: '/syncers' });
} else if (uri.includes('/certs')) {
this.setState({ selectedMenuKey: '/certs' });
} else if (uri.includes('/products')) {
this.setState({ selectedMenuKey: '/products' });
} else if (uri.includes('/payments')) {
this.setState({ selectedMenuKey: '/payments' });
} else if (uri.includes('/signup')) {
@ -413,6 +417,13 @@ class App extends Component {
</Menu.Item>
);
// res.push(
// <Menu.Item key="/products">
// <Link to="/products">
// {i18next.t("general:Products")}
// </Link>
// </Menu.Item>
// );
// res.push(
// <Menu.Item key="/payments">
// <Link to="/payments">
// {i18next.t("general:Payments")}
@ -490,6 +501,8 @@ class App extends Component {
<Route exact path="/syncers/:syncerName" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/certs" render={(props) => this.renderLoginIfNotLoggedIn(<CertListPage account={this.state.account} {...props} />)}/>
<Route exact path="/certs/:certName" render={(props) => this.renderLoginIfNotLoggedIn(<CertEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/products" render={(props) => this.renderLoginIfNotLoggedIn(<ProductListPage account={this.state.account} {...props} />)}/>
<Route exact path="/products/:productName" render={(props) => this.renderLoginIfNotLoggedIn(<ProductEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/payments" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentListPage account={this.state.account} {...props} />)}/>
<Route exact path="/payments/:paymentName" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)}/>

View File

@ -166,12 +166,12 @@ class ApplicationEditPage extends React.Component {
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel("Logo", i18next.t("general:Logo - Tooltip"))} :
{Setting.getLabel("general:Logo", i18next.t("general:Logo - Tooltip"))} :
</Col>
<Col span={22} style={(Setting.isMobile()) ? {maxWidth:'100%'} :{}}>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
URL:
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col>
<Col span={23} >
<Input prefix={<LinkOutlined/>} value={this.state.application.logo} onChange={e => {
@ -440,7 +440,7 @@ class ApplicationEditPage extends React.Component {
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Grant Types"), i18next.t("application:Grant Types - Tooltip"))} :
{Setting.getLabel(i18next.t("application:Grant types"), i18next.t("application:Grant types - Tooltip"))} :
</Col>
<Col span={22} >

View File

@ -113,12 +113,12 @@ class OrganizationEditPage extends React.Component {
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel("Favicon", i18next.t("general:Favicon - Tooltip"))} :
{Setting.getLabel("general:Favicon", i18next.t("general:Favicon - Tooltip"))} :
</Col>
<Col span={22} >
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
URL:
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col>
<Col span={23} >
<Input prefix={<LinkOutlined/>} value={this.state.organization.favicon} onChange={e => {
@ -188,7 +188,7 @@ class OrganizationEditPage extends React.Component {
<Col span={22} >
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
URL:
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col>
<Col span={23} >
<Input prefix={<LinkOutlined/>} value={this.state.organization.defaultAvatar} onChange={e => {

View File

@ -13,15 +13,10 @@
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, Row, Select, Switch} from 'antd';
import {Button, Card, Col, Input, Row} from 'antd';
import * as PaymentBackend from "./backend/PaymentBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as UserBackend from "./backend/UserBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import * as RoleBackend from "./backend/RoleBackend";
const { Option } = Select;
class PaymentEditPage extends React.Component {
constructor(props) {
@ -105,16 +100,6 @@ class PaymentEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.payment.name} onChange={e => {
// this.updatePaymentField('name', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Provider"), i18next.t("general:Provider - Tooltip"))} :

View File

@ -14,7 +14,7 @@
import React from "react";
import {Link} from "react-router-dom";
import {Button, Popconfirm, Switch, Table} from 'antd';
import {Button, Popconfirm, Table} from 'antd';
import moment from "moment";
import * as Setting from "./Setting";
import * as PaymentBackend from "./backend/PaymentBackend";

276
web/src/ProductEditPage.js Normal file
View File

@ -0,0 +1,276 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, InputNumber, Row, Select} from 'antd';
import * as ProductBackend from "./backend/ProductBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import {LinkOutlined} from "@ant-design/icons";
import * as ProviderBackend from "./backend/ProviderBackend";
const { Option } = Select;
class ProductEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
productName: props.match.params.productName,
product: null,
providers: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
UNSAFE_componentWillMount() {
this.getProduct();
this.getPaymentProviders();
}
getProduct() {
ProductBackend.getProduct("admin", this.state.productName)
.then((product) => {
this.setState({
product: product,
});
});
}
getPaymentProviders() {
ProviderBackend.getProviders("admin")
.then((res) => {
this.setState({
providers: res.filter(provider => provider.category === "Payment"),
});
});
}
parseProductField(key, value) {
if ([""].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateProductField(key, value) {
value = this.parseProductField(key, value);
let product = this.state.product;
product[key] = value;
this.setState({
product: product,
});
}
renderProduct() {
return (
<Card size="small" title={
<div>
{this.state.mode === "add" ? i18next.t("product:New Product") : i18next.t("product:Edit Product")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitProductEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitProductEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteProduct()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.product.name} onChange={e => {
this.updateProductField('name', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.product.displayName} onChange={e => {
this.updateProductField('displayName', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Image"), i18next.t("product:Image - Tooltip"))} :
</Col>
<Col span={22} style={(Setting.isMobile()) ? {maxWidth:'100%'} :{}}>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col>
<Col span={23} >
<Input prefix={<LinkOutlined/>} value={this.state.product.image} onChange={e => {
this.updateProductField('image', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{i18next.t("general:Preview")}:
</Col>
<Col span={23} >
<a target="_blank" rel="noreferrer" href={this.state.product.image}>
<img src={this.state.product.image} alt={this.state.product.image} height={90} style={{marginBottom: '20px'}}/>
</a>
</Col>
</Row>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Tag"), i18next.t("product:Tag - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.product.tag} onChange={e => {
this.updateProductField('tag', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Currency"), i18next.t("product:Currency - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.product.currency} onChange={(value => {
this.updateProductField('currency', value);
})}>
{
[
{id: 'USD', name: 'USD'},
{id: 'CNY', name: 'CNY'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.product.price} onChange={value => {
this.updateProductField('price', value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Quantity"), i18next.t("product:Quantity - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.product.quantity} onChange={value => {
this.updateProductField('quantity', value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Sold"), i18next.t("product:Sold - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.product.sold} onChange={value => {
this.updateProductField('sold', value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Payment providers"), i18next.t("product:Payment providers - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}} value={this.state.product.providers} onChange={(value => {this.updateProductField('providers', value);})}>
{
this.state.providers.map((provider, index) => <Option key={index} value={provider.name}>{provider.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:State"), i18next.t("general:State - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.product.state} onChange={(value => {
this.updateProductField('state', value);
})}>
{
[
{id: 'Published', name: 'Published'},
{id: 'Draft', name: 'Draft'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
</Card>
)
}
submitProductEdit(willExist) {
let product = Setting.deepCopy(this.state.product);
ProductBackend.updateProduct(this.state.product.owner, this.state.productName, product)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", `Successfully saved`);
this.setState({
productName: this.state.product.name,
});
if (willExist) {
this.props.history.push(`/products`);
} else {
this.props.history.push(`/products/${this.state.product.name}`);
}
} else {
Setting.showMessage("error", res.msg);
this.updateProductField('name', this.state.productName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
});
}
deleteProduct() {
ProductBackend.deleteProduct(this.state.product)
.then(() => {
this.props.history.push(`/products`);
})
.catch(error => {
Setting.showMessage("error", `Product failed to delete: ${error}`);
});
}
render() {
return (
<div>
{
this.state.product !== null ? this.renderProduct() : null
}
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitProductEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitProductEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteProduct()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);
}
}
export default ProductEditPage;

295
web/src/ProductListPage.js Normal file
View File

@ -0,0 +1,295 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Col, List, Popconfirm, Row, Table, Tooltip} from 'antd';
import moment from "moment";
import * as Setting from "./Setting";
import * as ProductBackend from "./backend/ProductBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import {EditOutlined} from "@ant-design/icons";
class ProductListPage extends BaseListPage {
newProduct() {
const randomName = Setting.getRandomName();
return {
owner: "admin",
name: `product_${randomName}`,
createdTime: moment().format(),
displayName: `New Product - ${randomName}`,
image: "https://cdn.casdoor.com/logo/casdoor-logo_1185x256.png",
tag: "Casdoor Summit 2022",
currency: "USD",
price: 300,
quantity: 99,
sold: 10,
providers: [],
state: "Published",
}
}
addProduct() {
const newProduct = this.newProduct();
ProductBackend.addProduct(newProduct)
.then((res) => {
this.props.history.push({pathname: `/products/${newProduct.name}`, mode: "add"});
}
)
.catch(error => {
Setting.showMessage("error", `Product failed to add: ${error}`);
});
}
deleteProduct(i) {
ProductBackend.deleteProduct(this.state.data[i])
.then((res) => {
Setting.showMessage("success", `Product deleted successfully`);
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
.catch(error => {
Setting.showMessage("error", `Product failed to delete: ${error}`);
});
}
renderTable(products) {
const columns = [
{
title: i18next.t("general:Name"),
dataIndex: 'name',
key: 'name',
width: '140px',
fixed: 'left',
sorter: true,
...this.getColumnSearchProps('name'),
render: (text, record, index) => {
return (
<Link to={`/products/${text}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("general:Created time"),
dataIndex: 'createdTime',
key: 'createdTime',
width: '160px',
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
}
},
{
title: i18next.t("general:Display name"),
dataIndex: 'displayName',
key: 'displayName',
width: '170px',
sorter: true,
...this.getColumnSearchProps('displayName'),
},
{
title: i18next.t("product:Image"),
dataIndex: 'image',
key: 'image',
width: '170px',
render: (text, record, index) => {
return (
<a target="_blank" rel="noreferrer" href={text}>
<img src={text} alt={text} width={150} />
</a>
)
}
},
{
title: i18next.t("product:Tag"),
dataIndex: 'tag',
key: 'tag',
width: '160px',
sorter: true,
...this.getColumnSearchProps('tag'),
},
{
title: i18next.t("product:Currency"),
dataIndex: 'currency',
key: 'currency',
width: '120px',
sorter: true,
...this.getColumnSearchProps('currency'),
},
{
title: i18next.t("product:Price"),
dataIndex: 'price',
key: 'price',
width: '120px',
sorter: true,
...this.getColumnSearchProps('price'),
},
{
title: i18next.t("product:Quantity"),
dataIndex: 'quantity',
key: 'quantity',
width: '120px',
sorter: true,
...this.getColumnSearchProps('quantity'),
},
{
title: i18next.t("product:Sold"),
dataIndex: 'sold',
key: 'sold',
width: '120px',
sorter: true,
...this.getColumnSearchProps('sold'),
},
{
title: i18next.t("general:State"),
dataIndex: 'state',
key: 'state',
width: '120px',
sorter: true,
...this.getColumnSearchProps('state'),
},
{
title: i18next.t("product:Payment providers"),
dataIndex: 'providers',
key: 'providers',
width: '500px',
...this.getColumnSearchProps('providers'),
render: (text, record, index) => {
const providers = text;
if (providers.length === 0) {
return "(empty)";
}
const half = Math.floor((providers.length + 1) / 2);
const getList = (providers) => {
return (
<List
size="small"
locale={{emptyText: " "}}
dataSource={providers}
renderItem={(providerName, i) => {
return (
<List.Item>
<div style={{display: "inline"}}>
<Tooltip placement="topLeft" title="Edit">
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/providers/${providerName}`)} />
</Tooltip>
<Link to={`/providers/${providerName}`}>
{providerName}
</Link>
</div>
</List.Item>
)
}}
/>
)
}
return (
<div>
<Row>
<Col span={12}>
{
getList(providers.slice(0, half))
}
</Col>
<Col span={12}>
{
getList(providers.slice(half))
}
</Col>
</Row>
</div>
)
},
},
{
title: i18next.t("general:Action"),
dataIndex: '',
key: 'op',
width: '170px',
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} type="primary" onClick={() => this.props.history.push(`/products/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete product: ${record.name} ?`}
onConfirm={() => this.deleteProduct(index)}
>
<Button style={{marginBottom: '10px'}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
)
}
},
];
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
<div>
<Table scroll={{x: 'max-content'}} columns={columns} dataSource={products} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Products")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addProduct.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
let sortField = params.sortField, sortOrder = params.sortOrder;
if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({ loading: true });
ProductBackend.getProducts("", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
if (res.status === "ok") {
this.setState({
loading: false,
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
}
});
};
}
export default ProductListPage;

View File

@ -0,0 +1,56 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as Setting from "../Setting";
export function getProducts(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-products?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}
export function getProduct(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-product?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}
export function updateProduct(owner, name, product) {
let newProduct = Setting.deepCopy(product);
return fetch(`${Setting.ServerUrl}/api/update-product?id=${owner}/${encodeURIComponent(name)}`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newProduct),
}).then(res => res.json());
}
export function addProduct(product) {
let newProduct = Setting.deepCopy(product);
return fetch(`${Setting.ServerUrl}/api/add-product`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newProduct),
}).then(res => res.json());
}
export function deleteProduct(product) {
let newProduct = Setting.deepCopy(product);
return fetch(`${Setting.ServerUrl}/api/delete-product`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newProduct),
}).then(res => res.json());
}

View File

@ -14,6 +14,8 @@
"Enable signup": "Anmeldung aktivieren",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "Datei erfolgreich hochgeladen",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
"Password ON": "Passwort AN",
"Password ON - Tooltip": "Whether to allow password login",
@ -112,6 +114,7 @@
"Email": "E-Mail",
"Email - Tooltip": "email",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "URL vergessen",
"Forget URL - Tooltip": "Unique string-style identifier",
"Home": "Zuhause",
@ -122,6 +125,7 @@
"Is enabled - Tooltip": "Ist aktiviert - Tooltip",
"LDAPs": "LDAPs",
"LDAPs - Tooltip": "LDAPs - Tooltip",
"Last name": "Last name",
"Logo - Tooltip": "App's image tag",
"Master password": "Master-Passwort",
"Master password - Tooltip": "Masterpasswort - Tooltip",
@ -146,6 +150,7 @@
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Vorschau",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
"Provider": "Anbieter",
"Provider - Tooltip": "Provider - Tooltip",
"Providers": "Anbieter",
@ -164,11 +169,14 @@
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Die von Ihnen besuchte Seite existiert leider nicht.",
"State": "State",
"State - Tooltip": "State - Tooltip",
"Swagger": "Swagger",
"Syncers": "Syncers",
"Timestamp": "Zeitstempel",
"Tokens": "Token",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "Hoch",
"User": "Benutzer",
"User - Tooltip": "Benutzer - Tooltip",
@ -263,6 +271,24 @@
"Resource type - Tooltip": "Ressourcentyp - Tooltip",
"Resources": "Ressourcen"
},
"product": {
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Edit Product": "Edit Product",
"Image": "Image",
"Image - Tooltip": "Image - Tooltip",
"New Product": "New Product",
"Payment providers": "Payment providers",
"Payment providers - Tooltip": "Payment providers - Tooltip",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",
"Tag": "Tag",
"Tag - Tooltip": "Tag - Tooltip"
},
"provider": {
"Access key": "Zugangsschlüssel",
"Access key - Tooltip": "Zugriffsschlüssel - Tooltip",
@ -389,6 +415,8 @@
"Please input your address!": "Bitte geben Sie Ihre Adresse ein!",
"Please input your affiliation!": "Bitte geben Sie Ihre Zugehörigkeit ein!",
"Please input your display name!": "Bitte geben Sie Ihren Anzeigenamen ein!",
"Please input your first name!": "Please input your first name!",
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Bitte geben Sie Ihre Telefonnummer ein!",
"Please input your real name!": "Bitte geben Sie Ihren persönlichen Namen ein!",
"Please select your country/region!": "Bitte wählen Sie Ihr Land/Ihre Region!",

View File

@ -10,10 +10,12 @@
"Edit Application": "Edit Application",
"Enable code signin": "Enable code signin",
"Enable code signin - Tooltip": "Enable code signin - Tooltip",
"Enable signin session - Tooltip": "Whether to preserve the session in Casdoor after login",
"Enable signin session - Tooltip": "Enable signin session - Tooltip",
"Enable signup": "Enable signup",
"Enable signup - Tooltip": "Enable signup - Tooltip",
"File uploaded successfully": "File uploaded successfully",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
"Password ON": "Password ON",
"Password ON - Tooltip": "Password ON - Tooltip",
@ -112,6 +114,7 @@
"Email": "Email",
"Email - Tooltip": "Email - Tooltip",
"Favicon - Tooltip": "Favicon - Tooltip",
"First name": "First name",
"Forget URL": "Forget URL",
"Forget URL - Tooltip": "Forget URL - Tooltip",
"Home": "Home",
@ -122,6 +125,7 @@
"Is enabled - Tooltip": "Is enabled - Tooltip",
"LDAPs": "LDAPs",
"LDAPs - Tooltip": "LDAPs - Tooltip",
"Last name": "Last name",
"Logo - Tooltip": "Logo - Tooltip",
"Master password": "Master password",
"Master password - Tooltip": "Master password - Tooltip",
@ -146,6 +150,7 @@
"Phone prefix - Tooltip": "Phone prefix - Tooltip",
"Preview": "Preview",
"Preview - Tooltip": "Preview - Tooltip",
"Products": "Products",
"Provider": "Provider",
"Provider - Tooltip": "Provider - Tooltip",
"Providers": "Providers",
@ -164,11 +169,14 @@
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.",
"State": "State",
"State - Tooltip": "State - Tooltip",
"Swagger": "Swagger",
"Syncers": "Syncers",
"Timestamp": "Timestamp",
"Tokens": "Tokens",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "Up",
"User": "User",
"User - Tooltip": "User - Tooltip",
@ -263,6 +271,24 @@
"Resource type - Tooltip": "Resource type - Tooltip",
"Resources": "Resources"
},
"product": {
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Edit Product": "Edit Product",
"Image": "Image",
"Image - Tooltip": "Image - Tooltip",
"New Product": "New Product",
"Payment providers": "Payment providers",
"Payment providers - Tooltip": "Payment providers - Tooltip",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",
"Tag": "Tag",
"Tag - Tooltip": "Tag - Tooltip"
},
"provider": {
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
@ -389,6 +415,8 @@
"Please input your address!": "Please input your address!",
"Please input your affiliation!": "Please input your affiliation!",
"Please input your display name!": "Please input your display name!",
"Please input your first name!": "Please input your first name!",
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Please input your phone number!",
"Please input your real name!": "Please input your real name!",
"Please select your country/region!": "Please select your country/region!",

View File

@ -14,6 +14,8 @@
"Enable signup": "Activer l'inscription",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "Fichier téléchargé avec succès",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
"Password ON": "Mot de passe activé",
"Password ON - Tooltip": "Whether to allow password login",
@ -112,6 +114,7 @@
"Email": "Courriel",
"Email - Tooltip": "email",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "Oublier l'URL",
"Forget URL - Tooltip": "Unique string-style identifier",
"Home": "Domicile",
@ -122,6 +125,7 @@
"Is enabled - Tooltip": "Est activé - infobulle",
"LDAPs": "LDAPs",
"LDAPs - Tooltip": "LDAPs - Infobulle",
"Last name": "Last name",
"Logo - Tooltip": "App's image tag",
"Master password": "Mot de passe maître",
"Master password - Tooltip": "Mot de passe maître - Infobulle",
@ -146,6 +150,7 @@
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Aperçu",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
"Provider": "Fournisseur",
"Provider - Tooltip": "Provider - Tooltip",
"Providers": "Fournisseurs",
@ -164,11 +169,14 @@
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Désolé, la page que vous avez visitée n'existe pas.",
"State": "State",
"State - Tooltip": "State - Tooltip",
"Swagger": "Swagger",
"Syncers": "Synchronisateurs",
"Timestamp": "Horodatage",
"Tokens": "Jetons",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "Monter",
"User": "Utilisateur",
"User - Tooltip": "Utilisateur - infobulle",
@ -263,6 +271,24 @@
"Resource type - Tooltip": "Type de ressource - infobulle",
"Resources": "Ressource"
},
"product": {
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Edit Product": "Edit Product",
"Image": "Image",
"Image - Tooltip": "Image - Tooltip",
"New Product": "New Product",
"Payment providers": "Payment providers",
"Payment providers - Tooltip": "Payment providers - Tooltip",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",
"Tag": "Tag",
"Tag - Tooltip": "Tag - Tooltip"
},
"provider": {
"Access key": "Clé d'accès",
"Access key - Tooltip": "Touche d'accès - Infobulle",
@ -389,6 +415,8 @@
"Please input your address!": "Veuillez saisir votre adresse !",
"Please input your affiliation!": "Veuillez entrer votre affiliation !",
"Please input your display name!": "Veuillez entrer votre nom d'affichage !",
"Please input your first name!": "Please input your first name!",
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Veuillez entrer votre numéro de téléphone!",
"Please input your real name!": "Veuillez entrer votre nom personnel !",
"Please select your country/region!": "Veuillez sélectionner votre pays/région!",

View File

@ -14,6 +14,8 @@
"Enable signup": "サインアップを有効にする",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "ファイルが正常にアップロードされました",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
"Password ON": "パスワードON",
"Password ON - Tooltip": "Whether to allow password login",
@ -112,6 +114,7 @@
"Email": "Eメールアドレス",
"Email - Tooltip": "email",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "URLを忘れた",
"Forget URL - Tooltip": "Unique string-style identifier",
"Home": "ホーム",
@ -122,6 +125,7 @@
"Is enabled - Tooltip": "有効にする - ツールチップ",
"LDAPs": "LDAP",
"LDAPs - Tooltip": "LDAP - ツールチップ",
"Last name": "Last name",
"Logo - Tooltip": "App's image tag",
"Master password": "マスターパスワード",
"Master password - Tooltip": "マスターパスワード - ツールチップ",
@ -146,6 +150,7 @@
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "プレビュー",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
"Provider": "プロバイダー",
"Provider - Tooltip": "Provider - Tooltip",
"Providers": "プロバイダー",
@ -164,11 +169,14 @@
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "申し訳ありませんが、訪問したページは存在しません。",
"State": "State",
"State - Tooltip": "State - Tooltip",
"Swagger": "Swagger",
"Syncers": "Syncers",
"Timestamp": "タイムスタンプ",
"Tokens": "トークン",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "上へ",
"User": "ユーザー",
"User - Tooltip": "ユーザー → ツールチップ",
@ -263,6 +271,24 @@
"Resource type - Tooltip": "リソースタイプ - ツールチップ",
"Resources": "リソース"
},
"product": {
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Edit Product": "Edit Product",
"Image": "Image",
"Image - Tooltip": "Image - Tooltip",
"New Product": "New Product",
"Payment providers": "Payment providers",
"Payment providers - Tooltip": "Payment providers - Tooltip",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",
"Tag": "Tag",
"Tag - Tooltip": "Tag - Tooltip"
},
"provider": {
"Access key": "アクセスキー",
"Access key - Tooltip": "アクセスキー → ツールチップ",
@ -389,6 +415,8 @@
"Please input your address!": "住所を入力してください!",
"Please input your affiliation!": "所属を入力してください!",
"Please input your display name!": "表示名を入力してください。",
"Please input your first name!": "Please input your first name!",
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "電話番号を入力してください!",
"Please input your real name!": "個人名を入力してください!",
"Please select your country/region!": "あなたの国/地域を選択してください!",

View File

@ -14,6 +14,8 @@
"Enable signup": "Enable signup",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "File uploaded successfully",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
"Password ON": "Password ON",
"Password ON - Tooltip": "Whether to allow password login",
@ -112,6 +114,7 @@
"Email": "Email",
"Email - Tooltip": "email",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "Forget URL",
"Forget URL - Tooltip": "Unique string-style identifier",
"Home": "Home",
@ -122,6 +125,7 @@
"Is enabled - Tooltip": "Is enabled - Tooltip",
"LDAPs": "LDAPs",
"LDAPs - Tooltip": "LDAPs - Tooltip",
"Last name": "Last name",
"Logo - Tooltip": "App's image tag",
"Master password": "Master password",
"Master password - Tooltip": "Master password - Tooltip",
@ -146,6 +150,7 @@
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Preview",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
"Provider": "Provider",
"Provider - Tooltip": "Provider - Tooltip",
"Providers": "Providers",
@ -164,11 +169,14 @@
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.",
"State": "State",
"State - Tooltip": "State - Tooltip",
"Swagger": "Swagger",
"Syncers": "Syncers",
"Timestamp": "Timestamp",
"Tokens": "Tokens",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "Up",
"User": "User",
"User - Tooltip": "User - Tooltip",
@ -263,6 +271,24 @@
"Resource type - Tooltip": "Resource type - Tooltip",
"Resources": "Resources"
},
"product": {
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Edit Product": "Edit Product",
"Image": "Image",
"Image - Tooltip": "Image - Tooltip",
"New Product": "New Product",
"Payment providers": "Payment providers",
"Payment providers - Tooltip": "Payment providers - Tooltip",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",
"Tag": "Tag",
"Tag - Tooltip": "Tag - Tooltip"
},
"provider": {
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
@ -389,6 +415,8 @@
"Please input your address!": "Please input your address!",
"Please input your affiliation!": "Please input your affiliation!",
"Please input your display name!": "Please input your display name!",
"Please input your first name!": "Please input your first name!",
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Please input your phone number!",
"Please input your real name!": "Please input your real name!",
"Please select your country/region!": "Please select your country/region!",

View File

@ -14,6 +14,8 @@
"Enable signup": "Включить регистрацию",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "Файл успешно загружен",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
"Password ON": "Пароль ВКЛ",
"Password ON - Tooltip": "Whether to allow password login",
@ -112,6 +114,7 @@
"Email": "Почта",
"Email - Tooltip": "email",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "Забыть URL",
"Forget URL - Tooltip": "Unique string-style identifier",
"Home": "Домашний",
@ -122,6 +125,7 @@
"Is enabled - Tooltip": "Включено - Подсказка",
"LDAPs": "LDAPы",
"LDAPs - Tooltip": "LDAPs - Подсказки",
"Last name": "Last name",
"Logo - Tooltip": "App's image tag",
"Master password": "Мастер-пароль",
"Master password - Tooltip": "Мастер-пароль - Tooltip",
@ -146,6 +150,7 @@
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Предпросмотр",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
"Provider": "Поставщик",
"Provider - Tooltip": "Provider - Tooltip",
"Providers": "Поставщики",
@ -164,11 +169,14 @@
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Извините, посещенная вами страница не существует.",
"State": "State",
"State - Tooltip": "State - Tooltip",
"Swagger": "Swagger",
"Syncers": "Синхронизаторы",
"Timestamp": "Отметка времени",
"Tokens": "Жетоны",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "Вверх",
"User": "Пользователь",
"User - Tooltip": "Пользователь - Подсказка",
@ -263,6 +271,24 @@
"Resource type - Tooltip": "Тип ресурса - Подсказка",
"Resources": "Ресурсы"
},
"product": {
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Edit Product": "Edit Product",
"Image": "Image",
"Image - Tooltip": "Image - Tooltip",
"New Product": "New Product",
"Payment providers": "Payment providers",
"Payment providers - Tooltip": "Payment providers - Tooltip",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",
"Tag": "Tag",
"Tag - Tooltip": "Tag - Tooltip"
},
"provider": {
"Access key": "Ключ доступа",
"Access key - Tooltip": "Ключ доступа - Подсказка",
@ -389,6 +415,8 @@
"Please input your address!": "Пожалуйста, введите ваш адрес!",
"Please input your affiliation!": "Пожалуйста, введите вашу партнерство!",
"Please input your display name!": "Пожалуйста, введите ваше отображаемое имя!",
"Please input your first name!": "Please input your first name!",
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Пожалуйста, введите ваш номер телефона!",
"Please input your real name!": "Пожалуйста, введите ваше личное имя!",
"Please select your country/region!": "Пожалуйста, выберите вашу страну/регион!",

View File

@ -14,6 +14,8 @@
"Enable signup": "启用注册",
"Enable signup - Tooltip": "是否允许用户注册",
"File uploaded successfully": "文件上传成功",
"Grant types": "Grant types",
"Grant types - Tooltip": "选择允许哪些OAuth协议中的Grant types",
"New Application": "添加应用",
"Password ON": "开启密码",
"Password ON - Tooltip": "是否允许密码登录",
@ -112,6 +114,7 @@
"Email": "电子邮箱",
"Email - Tooltip": "电子邮件:",
"Favicon - Tooltip": "网站的图标",
"First name": "名字",
"Forget URL": "忘记密码URL",
"Forget URL - Tooltip": "忘记密码URL",
"Home": "首页",
@ -122,6 +125,7 @@
"Is enabled - Tooltip": "是否启用",
"LDAPs": "LDAP",
"LDAPs - Tooltip": "LDAPs",
"Last name": "姓氏",
"Logo - Tooltip": "应用程序向外展示的图标",
"Master password": "万能密码",
"Master password - Tooltip": "可用来登录该组织下的所有用户,方便管理员以该用户身份登录,以解决技术问题",
@ -146,6 +150,7 @@
"Phone prefix - Tooltip": "移动电话号码前缀,用于区分国家或地区",
"Preview": "预览",
"Preview - Tooltip": "预览",
"Products": "商品",
"Provider": "提供商",
"Provider - Tooltip": "第三方登录需要配置的提供方",
"Providers": "提供商",
@ -164,11 +169,14 @@
"Signup application": "注册应用",
"Signup application - Tooltip": "表示用户注册时通过哪个应用注册的",
"Sorry, the page you visited does not exist.": "抱歉,您访问的页面不存在",
"State": "状态",
"State - Tooltip": "状态",
"Swagger": "API文档",
"Syncers": "同步器",
"Timestamp": "时间戳",
"Tokens": "令牌",
"URL": "链接",
"URL - Tooltip": "URL链接",
"Up": "上移",
"User": "用户",
"User - Tooltip": "用户 - 工具提示",
@ -263,6 +271,24 @@
"Resource type - Tooltip": "授权资源的类型",
"Resources": "资源"
},
"product": {
"Currency": "币种",
"Currency - Tooltip": "币种 - 工具提示",
"Edit Product": "编辑商品",
"Image": "图片",
"Image - Tooltip": "图片 - 工具提示",
"New Product": "添加商品",
"Payment providers": "支付提供商",
"Payment providers - Tooltip": "支付提供商 - 工具提示",
"Price": "价格",
"Price - Tooltip": "价格 - 工具提示",
"Quantity": "库存",
"Quantity - Tooltip": "库存 - 工具提示",
"Sold": "售出",
"Sold - Tooltip": "售出 - 工具提示",
"Tag": "标签",
"Tag - Tooltip": "标签 - 工具提示"
},
"provider": {
"Access key": "访问密钥",
"Access key - Tooltip": "Access key",
@ -389,6 +415,8 @@
"Please input your address!": "请输入您的地址!",
"Please input your affiliation!": "请输入您所在的工作单位!",
"Please input your display name!": "请输入您的显示名称!",
"Please input your first name!": "请输入您的名字!",
"Please input your last name!": "请输入您的姓氏!",
"Please input your phone number!": "请输入您的手机号码!",
"Please input your real name!": "请输入您的姓名!",
"Please select your country/region!": "请选择您的国家/地区",