Merge pull request #72 from kocoler/AddPlaneManagement

Add plane management
This commit is contained in:
Yang Luo 2020-09-23 13:41:12 +08:00 committed by GitHub
commit 642d330e1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1085 additions and 53 deletions

View File

@ -75,6 +75,12 @@ func (c *APIController) forbiddenAccountResp(memberId string) {
c.ServeJSON()
}
func (c *APIController) RequireAdmin(memberId string) {
resp := Response{Status: "error", Msg: "Unauthorized.", Data: memberId}
c.Data["json"] = resp
c.ServeJSON()
}
func (c *APIController) GetCommunityHealth() {
var resp Response
@ -106,7 +112,7 @@ var GlobalSessions *session.Manager
func InitBeegoSession() {
sessionConfig := &session.ManagerConfig{
ProviderConfig: "./tmp",
Gclifetime: 3600 * 24 * 365,
Gclifetime: 3600 * 24 * 365,
}
GlobalSessions, _ = session.NewManager("file", sessionConfig)
go GlobalSessions.GC()

View File

@ -56,9 +56,7 @@ func (c *APIController) UpdateNode() {
var node object.Node
if !object.CheckModIdentity(c.GetSessionUser()) {
resp = Response{Status: "fail", Msg: "Unauthorized."}
c.Data["json"] = resp
c.ServeJSON()
c.RequireAdmin(c.GetSessionUser())
return
}
@ -77,6 +75,11 @@ func (c *APIController) AddNode() {
var node object.Node
var resp Response
if !object.CheckModIdentity(c.GetSessionUser()) {
c.RequireAdmin(c.GetSessionUser())
return
}
err := json.Unmarshal(c.Ctx.Input.RequestBody, &node)
if err != nil {
panic(err)
@ -107,6 +110,11 @@ func (c *APIController) AddNode() {
func (c *APIController) DeleteNode() {
id := c.Input().Get("id")
if !object.CheckModIdentity(c.GetSessionUser()) {
c.RequireAdmin(c.GetSessionUser())
return
}
c.Data["json"] = object.DeleteNode(id)
c.ServeJSON()
}

View File

@ -18,6 +18,7 @@ import (
"encoding/json"
"github.com/casbin/casbin-forum/object"
"github.com/casbin/casbin-forum/util"
)
func (c *APIController) GetPlanes() {
@ -37,6 +38,13 @@ func (c *APIController) GetPlane() {
c.ServeJSON()
}
func (c *APIController) GetPlaneAdmin() {
id := c.Input().Get("id")
c.Data["json"] = object.GetPlaneAdmin(id)
c.ServeJSON()
}
func (c *APIController) GetPlaneList() {
var resp Response
@ -47,21 +55,90 @@ func (c *APIController) GetPlaneList() {
}
func (c *APIController) AddPlane() {
var plane object.Plane
var plane object.AdminPlaneInfo
var resp Response
if !object.CheckModIdentity(c.GetSessionUser()) {
c.RequireAdmin(c.GetSessionUser())
return
}
err := json.Unmarshal(c.Ctx.Input.RequestBody, &plane)
if err != nil {
panic(err)
}
c.Data["json"] = object.AddPlane(&plane)
if plane.Id == "" || plane.Name == "" {
resp = Response{Status: "fail", Msg: "Some information is missing"}
c.Data["json"] = resp
c.ServeJSON()
return
}
if object.HasPlane(plane.Id) {
resp = Response{Status: "fail", Msg: "Plane ID existed"}
c.Data["json"] = resp
c.ServeJSON()
return
}
newPlane := object.Plane{
Id: plane.Id,
Name: plane.Name,
Sorter: plane.Sorter,
CreatedTime: util.GetCurrentTime(),
Image: plane.Image,
BackgroundColor: plane.BackgroundColor,
Color: plane.Color,
Visible: plane.Visible,
}
res := object.AddPlane(&newPlane)
resp = Response{Status: "ok", Msg: "success", Data: res}
c.Data["json"] = resp
c.ServeJSON()
}
func (c *APIController) UpdatePlaneInfo() {
var info updatePlaneInfo
err := json.Unmarshal(c.Ctx.Input.RequestBody, &info)
func (c *APIController) UpdatePlane() {
id := c.Input().Get("id")
var resp Response
var plane object.AdminPlaneInfo
if !object.CheckModIdentity(c.GetSessionUser()) {
c.RequireAdmin(c.GetSessionUser())
return
}
err := json.Unmarshal(c.Ctx.Input.RequestBody, &plane)
if err != nil {
panic(err)
}
newPlane := object.Plane{
Id: plane.Id,
Name: plane.Name,
Sorter: plane.Sorter,
CreatedTime: plane.CreatedTime,
Image: plane.Image,
BackgroundColor: plane.BackgroundColor,
Color: plane.Color,
Visible: plane.Visible,
}
res := object.UpdatePlane(id, &newPlane)
resp = Response{Status: "ok", Msg: "success", Data: res}
c.Data["json"] = object.UpdatePlaneInfo(info.Id, info.Field, info.Value)
c.Data["json"] = resp
c.ServeJSON()
}
func (c *APIController) DeletePlane() {
id := c.Input().Get("id")
if !object.CheckModIdentity(c.GetSessionUser()) {
c.RequireAdmin(c.GetSessionUser())
return
}
c.Data["json"] = Response{Status: "ok", Msg: "success", Data: object.DeletePlane(id)}
c.ServeJSON()
}

View File

@ -55,15 +55,17 @@ func (c *APIController) GetTopic() {
id := util.ParseInt(idStr)
topic := object.GetTopicWithAvatar(id, memberId)
if topic == nil || topic.Deleted {
c.Data["json"] = nil
c.ServeJSON()
return
}
if memberId != "" {
topic.NodeModerator = object.CheckNodeModerator(memberId, topic.NodeId)
}
if topic.Deleted {
c.Data["json"] = nil
} else {
c.Data["json"] = topic
}
c.Data["json"] = topic
c.ServeJSON()
}

View File

@ -138,6 +138,12 @@ func HasTab(id string) bool {
return tab != nil
}
func HasPlane(id string) bool {
plane := GetPlane(id)
return plane != nil
}
// IsMuted check member whether is muted.
func IsMuted(id string) bool {
status := GetMemberStatus(id)

View File

@ -18,6 +18,7 @@ type Plane struct {
Id string `xorm:"varchar(50) notnull pk" json:"id"`
Name string `xorm:"varchar(50)" json:"name"`
Sorter int `xorm:"int" json:"-"`
CreatedTime string `xorm:"varchar(40)" json:"createdTime"`
Image string `xorm:"varchar(200)" json:"image"`
BackgroundColor string `xorm:"varchar(20)" json:"backgroundColor"`
Color string `xorm:"varchar(20)" json:"color"`
@ -34,14 +35,24 @@ func GetPlanes() []*Plane {
return planes
}
func GetAllPlanes() []*Plane {
func GetAllPlanes() []*AdminPlaneInfo {
planes := []*Plane{}
err := adapter.engine.Asc("sorter").Find(&planes)
if err != nil {
panic(err)
}
return planes
var res []*AdminPlaneInfo
for _, v := range planes {
temp := AdminPlaneInfo{
Plane: *v,
Sorter: v.Sorter,
Visible: v.Visible,
NodesNum: GetPlaneNodesNum(v.Id),
}
res = append(res, &temp)
}
return res
}
func GetPlane(id string) *Plane {
@ -58,6 +69,28 @@ func GetPlane(id string) *Plane {
}
}
func GetPlaneAdmin(id string) *AdminPlaneInfo {
plane := Plane{Id: id}
existed, err := adapter.engine.Get(&plane)
if err != nil {
panic(err)
}
res := AdminPlaneInfo{
Plane: plane,
Sorter: plane.Sorter,
Visible: plane.Visible,
NodesNum: GetPlaneNodesNum(plane.Id),
Nodes: GetNodeFromPlane(plane.Id),
}
if existed {
return &res
} else {
return nil
}
}
func AddPlane(plane *Plane) bool {
affected, err := adapter.engine.Insert(plane)
if err != nil {
@ -67,8 +100,12 @@ func AddPlane(plane *Plane) bool {
return affected != 0
}
func UpdatePlaneInfo(id, field, value string) bool {
affected, err := adapter.engine.Table(new(Plane)).ID(id).Update(map[string]interface{}{field: value})
func UpdatePlane(id string, plane *Plane) bool {
if GetPlane(id) == nil {
return false
}
affected, err := adapter.engine.Id(id).AllCols().Update(plane)
if err != nil {
panic(err)
}
@ -90,3 +127,22 @@ func GetPlaneList() []*PlaneWithNodes {
return res
}
func DeletePlane(id string) bool {
affected, err := adapter.engine.Id(id).Delete(&Plane{})
if err != nil {
panic(err)
}
return affected != 0
}
func GetPlaneNodesNum(id string) int {
node := new(Node)
total, err := adapter.engine.Where("plane_id = ?", id).Count(node)
if err != nil {
panic(err)
}
return int(total)
}

View File

@ -88,6 +88,10 @@ func GetTopicWithAvatar(id int, memberId string) *TopicWithAvatar {
panic(err)
}
if !existed {
return nil
}
res := TopicWithAvatar{
Topic: topic,
Avatar: GetMemberAvatar(topic.Author),
@ -95,11 +99,7 @@ func GetTopicWithAvatar(id int, memberId string) *TopicWithAvatar {
Editable: GetTopicEditableStatus(memberId, topic.Author, topic.NodeId, topic.CreatedTime),
}
if existed {
return &res
} else {
return nil
}
return &res
}
func GetTopic(id int) *Topic {

View File

@ -115,3 +115,11 @@ type AdminMemberInfo struct {
ReplyNum int `json:"replyNum"`
LatestLogin string `json:"latestLogin"`
}
type AdminPlaneInfo struct {
Plane
Sorter int `json:"sorter"`
Visible bool `json:"visible"`
NodesNum int `json:"nodesNum"`
Nodes []*Node `json:"nodes"`
}

View File

@ -76,9 +76,9 @@ func initAPI() {
beego.Router("/api/get-nodes", &controllers.APIController{}, "GET:GetNodes")
beego.Router("/api/get-node", &controllers.APIController{}, "GET:GetNode")
beego.Router("/api/update-node", &controllers.APIController{}, "POST:UpdateNode")
beego.Router("/api/add-node", &controllers.APIController{}, "POST:AddNode")
beego.Router("/api/delete-node", &controllers.APIController{}, "POST:DeleteNode")
beego.Router("/api/update-node", &controllers.APIController{}, "POST:UpdateNode") // Update node api just for admin.
beego.Router("/api/add-node", &controllers.APIController{}, "POST:AddNode") // Add node api just for admin.
beego.Router("/api/delete-node", &controllers.APIController{}, "POST:DeleteNode") // Delete node api just for admin.
beego.Router("/api/get-node-info", &controllers.APIController{}, "GET:GetNodeInfo")
beego.Router("/api/get-node-relation", &controllers.APIController{}, "GET:GetNodeRelation")
beego.Router("/api/get-nodes-num", &controllers.APIController{}, "GET:GetNodesNum")
@ -109,9 +109,9 @@ func initAPI() {
beego.Router("/api/get-tab-with-nodes", &controllers.APIController{}, "GET:GetTabWithNodes")
beego.Router("/api/get-tabs-admin", &controllers.APIController{}, "GET:GetAllTabsAdmin")
beego.Router("/api/get-tab-admin", &controllers.APIController{}, "GET:GetTabAdmin")
beego.Router("/api/add-tab", &controllers.APIController{}, "POST:AddTab")
beego.Router("/api/update-tab", &controllers.APIController{}, "POST:UpdateTab")
beego.Router("/api/delete-tab", &controllers.APIController{}, "POST:DeleteTab")
beego.Router("/api/add-tab", &controllers.APIController{}, "POST:AddTab") // Add tab api just for admin.
beego.Router("/api/update-tab", &controllers.APIController{}, "POST:UpdateTab") // Update tab api just for admin.
beego.Router("/api/delete-tab", &controllers.APIController{}, "POST:DeleteTab") // Delete tab api just for admin.
beego.Router("/api/get-notifications", &controllers.APIController{}, "GET:GetNotifications")
beego.Router("/api/delete-notifications", &controllers.APIController{}, "POST:DeleteNotification")
@ -119,11 +119,13 @@ func initAPI() {
beego.Router("/api/update-read-status", &controllers.APIController{}, "POST:UpdateReadStatus")
beego.Router("/api/get-plane", &controllers.APIController{}, "GET:GetPlane")
beego.Router("/api/get-planes", &controllers.APIController{}, "GET:GetPlanes")
beego.Router("/api/add-plane", &controllers.APIController{}, "POST:AddPlane")
beego.Router("/api/get-plane-admin", &controllers.APIController{}, "GET:GetPlaneAdmin")
//beego.Router("/api/get-planes", &controllers.APIController{}, "GET:GetPlanes")
beego.Router("/api/add-plane", &controllers.APIController{}, "POST:AddPlane") // Add plane api just for admin.
beego.Router("/api/get-plane-list", &controllers.APIController{}, "GET:GetPlaneList")
beego.Router("/api/update-plane-info", &controllers.APIController{}, "POST:UpdatePlaneInfo")
beego.Router("/api/get-planes-admin", &controllers.APIController{}, "GET:GetPlanes")
beego.Router("/api/update-plane", &controllers.APIController{}, "POST:UpdatePlane") // Update plane api just for admin.
beego.Router("/api/get-planes-admin", &controllers.APIController{}, "GET:GetPlanesAdmin")
beego.Router("/api/delete-plane", &controllers.APIController{}, "POST:DeletePlane") // Delete plane api just for admin.
beego.Router("/api/get-checkin-bonus-status", &controllers.APIController{}, "GET:GetCheckinBonusStatus")
beego.Router("/api/get-checkin-bonus", &controllers.APIController{}, "GET:GetCheckinBonus")

View File

@ -65,6 +65,7 @@ import AdminNode from "./admin/AdminNode";
import AdminTab from "./admin/AdminTab";
import AdminMember from "./admin/AdminMember";
import i18next from "i18next";
import AdminPlane from "./admin/AdminPlane";
class App extends Component {
constructor(props) {
@ -374,6 +375,24 @@ class App extends Component {
<AdminMember account={this.state.account} />
</div>
</Route>
<Route exact path="/admin/plane">
<div id={pcBrowser ? "Main" : ""}>
{pcBrowser ? <div className="sep20" /> : null}
<AdminPlane account={this.state.account} />
</div>
</Route>
<Route exact path="/admin/plane/new">
<div id={pcBrowser ? "Main" : ""}>
{pcBrowser ? <div className="sep20" /> : null}
<AdminPlane account={this.state.account} event={"new"} />
</div>
</Route>
<Route exact path="/admin/plane/edit/:planeId">
<div id={pcBrowser ? "Main" : ""}>
{pcBrowser ? <div className="sep20" /> : null}
<AdminPlane account={this.state.account} />
</div>
</Route>
</Switch>
)
}

View File

@ -25,6 +25,7 @@ class AdminHomepage extends React.Component {
manageItems: [
{label: i18next.t("admin:Tab management" ), value: "tab", image: Setting.getStatic("/static/img/settings.png")},
{label: i18next.t("admin:Node management"), value: "node", image: Setting.getStatic("/static/img/settings.png")},
{label: i18next.t("admin:Plane management" ), value: "plane", image: Setting.getStatic("/static/img/settings.png")},
{label: i18next.t("admin:Topic management"), value: "topic", image: Setting.getStatic("/static/img/settings.png")},
{label: i18next.t("admin:Member management" ), value: "member", image: Setting.getStatic("/static/img/settings.png")},
],

View File

@ -305,10 +305,12 @@ class AdminMember extends React.Component {
return (
<div className="box">
<div className="header"><a href="/">{Setting.getForumName()}</a>
{" "}<span className="chevron">&nbsp;&nbsp;</span>
<a href={`/admin`}>{i18next.t("admin:Backstage management")}</a>
{" "}<span className="chevron">&nbsp;&nbsp;</span>
<a href={`/admin/member`}>{i18next.t("member:Member management")}</a>
{" "}<span className="chevron">&nbsp;&nbsp;</span>
<span className="gray">
<span>
{this.state.memberId}
</span>
</div>
@ -724,6 +726,8 @@ class AdminMember extends React.Component {
<div className="box">
<div className="header">
<a href="/">{Setting.getForumName()}</a>
{" "}<span className="chevron">&nbsp;&nbsp;</span>
<a href={`/admin`}>{i18next.t("admin:Backstage management")}</a>
<span className="chevron">&nbsp;&nbsp;</span>{" "}{i18next.t("member:Member management")}
<div className="fr f12">
<span className="snow">{i18next.t("member:Total members")}{" "}&nbsp;</span>

View File

@ -326,12 +326,14 @@ class AdminNode extends React.Component {
return (
<div className="box">
<div className="header"><a href="/">{Setting.getForumName()}</a>
{" "}<span className="chevron">&nbsp;&nbsp;</span>
<a href={`/admin`}>{i18next.t("admin:Backstage management")}</a>
{" "}<span className="chevron">&nbsp;&nbsp;</span>
<a href={`/admin/node`}>{i18next.t("node:Node management")}</a>
{" "}<span className="chevron">&nbsp;&nbsp;</span>
{
this.props.event === "new" ?
<span className="gray">
<span>
{i18next.t("node:New node")}
</span> :
<a href={`/go/${this.state.nodeId}`}>{this.state.nodeInfo?.name}</a>
@ -853,6 +855,8 @@ class AdminNode extends React.Component {
<div className="box">
<div className="header">
<a href="/">{Setting.getForumName()}</a>
{" "}<span className="chevron">&nbsp;&nbsp;</span>
<a href={`/admin`}>{i18next.t("admin:Backstage management")}</a>
<span className="chevron">&nbsp;&nbsp;</span>{" "}{i18next.t("node:Node management")}
<div className="fr f12">
<span className="snow">{i18next.t("node:Total nodes")}{" "}&nbsp;</span>

717
web/src/admin/AdminPlane.js Normal file
View File

@ -0,0 +1,717 @@
// Copyright 2020 The casbin 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 {withRouter} from "react-router-dom";
import * as PlaneBackend from "../backend/PlaneBackend.js";
import * as Setting from "../Setting";
import Zmage from "react-zmage";
import {SketchPicker} from "react-color";
import i18next from "i18next";
class AdminPlane extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
nodes: [],
planes: [],
message: "",
errorMessage: "",
form: {},
plane: [],
//event: props.match.params.event,
planeId: props.match.params.planeId,
width: "",
event: "basic",
Management_LIST: [
{label: "Basic Info", value: "basic"},
{label: "Display", value: "display"},
],
color: "",
backgroundColor: "",
displayColorPicker: false,
displayBackgroundColorPicker: false,
};
}
componentDidMount() {
this.getPlane();
this.getPlanes();
}
getPlanes() {
PlaneBackend.getPlanesAdmin()
.then((res) => {
this.setState({
planes: res,
});
});
}
getPlane() {
if (this.state.planeId === undefined && this.props.event !== "new") {
return;
}
PlaneBackend.getPlaneAdmin(this.state.planeId)
.then((res) => {
this.setState({
plane: res,
color: res?.color,
backgroundColor: res?.backgroundColor,
}, () => {
this.initForm();
});
});
}
initForm() {
let form = this.state.form;
if (this.props.event === "new") {
form["sorter"] = 1;
form["visible"] = true;
} else {
form["id"] = this.state.plane?.id;
form["name"] = this.state.plane?.name;
form["createdTime"] = this.state.plane?.createdTime;
form["sorter"] = this.state.plane?.sorter;
form["image"] = this.state.plane?.image;
form["backgroundColor"] = this.state.plane?.backgroundColor;
form["color"] = this.state.plane?.color;
form["visible"] = this.state.plane?.visible;
}
this.setState({
form: form,
});
}
updateFormField(key, value) {
let form = this.state.form;
form[key] = value;
this.setState({
form: form,
});
}
deletePlane(plane, planeId, nodesNum) {
if (window.confirm(`${i18next.t(`plane:Are you sure to delete plane`)} ${plane} ?`)) {
if (nodesNum !== 0) {
alert(`
${i18next.t("plane:Please delete all the nodes or move to another plane before deleting the plane")}
${i18next.t("plane:Currently the number of nodes under the plane is")} ${nodesNum}
`);
} else {
PlaneBackend.deletePlane(planeId)
.then((res) => {
if (res.status === 'ok') {
this.setState({
message: `${i18next.t("plane:Delete plane")} ${plane} ${i18next.t("plane:success")}`,
});
this.getPlanes();
} else {
this.setState({
errorMessage: res?.msg,
});
}
});
}
}
}
updatePlaneInfo() {
PlaneBackend.updatePlane(this.state.planeId, this.state.form)
.then((res) => {
if (res.status === 'ok') {
this.getPlane();
this.setState({
message: i18next.t("plane:Update plane information success"),
});
} else {
this.setState({
message: res?.msg,
});
}
});
}
postNewPlane() {
if (this.state.form.id === undefined || this.state.form.id === "") {
this.setState({
errorMessage: "Please input plane ID",
});
return;
}
if (this.state.form.name === "" || this.state.form.name === undefined) {
this.setState({
errorMessage: "Please input plane name",
});
return;
}
PlaneBackend.addPlane(this.state.form)
.then((res) => {
if (res.status === 'ok') {
this.getPlane();
this.setState({
errorMessage: "",
message: i18next.t("plane:Creat plane success"),
}, () => {
setTimeout(() => Setting.goToLink(`/admin/plane/edit/${this.state.form.id}`), 1600);
});
} else {
this.setState({
errorMessage: res?.msg,
});
}
});
}
clearMessage() {
this.setState({
message: "",
});
}
clearErrorMessage() {
this.setState({
errorMessage: "",
});
}
handleColorChange = (color, event) => {
if (event === "color") {
this.updateFormField("color", color.hex);
return;
}
this.updateFormField("backgroundColor", color.hex);
};
handleColorClick = (event) => {
if (event === "color") {
this.setState({
displayColorPicker: !this.state.displayColorPicker,
});
return;
}
this.setState({
displayBackgroundColorPicker: !this.state.displayBackgroundColorPicker,
});
};
handleColorClose = (event) => {
if (event === "color") {
this.setState({
displayColorPicker: false,
});
return;
}
this.setState({
displayBackgroundColorPicker: false,
});
};
changeEvent(event) {
this.setState({
event: event,
message: "",
});
if (this.props.event !== "new") {
this.initForm();
}
}
resetColor(event) {
if (event === "color") {
this.updateFormField("color", this.state.color);
return;
}
this.updateFormField("backgroundColor", this.state.backgroundColor);
}
renderColorPicker(event) {
return (
<div style={{
padding: '5px',
background: '#fff',
borderRadius: '1px',
boxShadow: '0 0 0 1px rgba(0,0,0,.1)',
display: 'inline-block',
cursor: 'pointer'
}} onClick={() => this.handleColorClick(event)} >
<div style={{
width: '36px',
height: '14px',
borderRadius: '2px',
background: event === "color" ? `${this.state.form.color}` : `${this.state.form.backgroundColor}`
}} />
</div>
);
}
renderNode(node) {
return (
<span>
<a href={`/go/${node.id}`}>
{node.name}
</a>{" "}&nbsp;{" "}
</span>
);
}
renderProblem() {
let problems = [];
if (this.state.errorMessage !== "") {
problems.push(i18next.t(`error:${this.state.errorMessage}`));
}
if (problems.length === 0) {
return null;
}
return (
<div className="problem" onClick={() => this.clearErrorMessage()}>
{i18next.t("error:Please resolve the following issues before submitting")}
<ul>
{
problems.map((problem, i) => {
return <li>{problem}</li>;
})
}
</ul>
</div>
);
}
renderManagementList(item){
return (
<a href="javascript:void(0);" className={this.state.event === item.value ? "tab_current" : "tab"} onClick={() => this.changeEvent(item.value)}>{i18next.t(`plane:${item.label}`)}</a>
);
}
renderHeader() {
return (
<div className="box">
<div className="header"><a href="/">{Setting.getForumName()}</a>
{" "}<span className="chevron">&nbsp;&nbsp;</span>
<a href={`/admin`}>{i18next.t("admin:Backstage management")}</a>
{" "}<span className="chevron">&nbsp;&nbsp;</span>
<a href={`/admin/plane`}>{i18next.t("plane:Plane management")}</a>
{" "}<span className="chevron">&nbsp;&nbsp;</span>
<span>
{
this.props.event === "new" ?
i18next.t("plane:New plane") : this.state.planeId
}
</span>
</div>
<div className="cell">
{
this.state.Management_LIST.map((item) => {
return this.renderManagementList(item);
})
}
</div>
</div>
);
}
renderPlanes(plane) {
const pcBrowser = Setting.PcBrowser;
return (
<div className="cell">
<table cellPadding="0" cellSpacing="0" border="0" width="100%">
<tbody>
<tr>
<td width={pcBrowser ? "200" : "auto"} align="left">
<span className="gray">
{plane?.name}
</span>
</td>
<td width={pcBrowser ? "100" : "auto"} align="center">
<a href={`/admin/plane/edit/${plane?.id}`}>
{i18next.t("plane:Manage")}
</a>
</td>
<td width="10"></td>
<td width={pcBrowser ? "120" : "80"} valign="middle" style={{textAlign: "center"}}>
<span style={{fontSize: "13px"}}>
{plane?.nodesNum}{" "}{i18next.t("plane:nodes")}
</span>
</td>
<td width={pcBrowser ? "120" : "80"} align="left" style={{textAlign: "center"}}>
{
plane?.visible ?
<span className="positive">
{i18next.t("plane:Visible")}
</span> :
<span className="gray">
{i18next.t("plane:Invisible")}
</span>
}
</td>
<td width="50" align="left" style={{textAlign: "right"}}>
<a href="#" onClick={() => this.deletePlane(plane?.name, plane?.id, plane?.nodesNum)}>
{i18next.t("plane:Delete")}
</a>
</td>
</tr>
</tbody>
</table>
</div>
);
}
render() {
const newPlane = (this.props.event === "new");
if (this.state.planeId !== undefined || newPlane) {
if (this.state.planeId !== undefined) {
if (this.state.plane !== null && this.state.plane.length === 0) {
return (
<div className="box">
<div className="header"><a href="/">{Setting.getForumName()}</a><span className="chevron">&nbsp;&nbsp;</span>{" "}{i18next.t("loading:Page is loading")}</div>
<div className="cell"><span className="gray bigger">{i18next.t("loading:Please wait patiently...")}</span></div>
</div>
);
}
if (this.state.plane === null) {
return (
<div class="box">
<div class="header">
<a href="/">{Setting.getForumName()}</a>
<span className="chevron">&nbsp;&nbsp;</span>{" "}{i18next.t("error:Plane not found")}</div>
<div class="cell">
{i18next.t("error:The plane you are trying to view does not exist")}
</div>
</div>
);
}
}
const plane = this.state.plane;
if (this.state.event === "basic") {
return (
<div>
{this.renderHeader()}
<div className="box">
{this.renderProblem()}
{
this.state.message !== "" ?
<div className="message" onClick={() => this.clearMessage()}>
<li className="fa fa-exclamation-triangle"></li>
&nbsp;{" "}
{this.state.message}
</div> : null
}
<div className="inner">
<table cellPadding="5" cellSpacing="0" border="0" width="100%">
<tbody>
{
newPlane ?
<tr>
<td width="120" align="right">{i18next.t("plane:Plane ID")}</td>
<td width="auto" align="left">
<input type="text" className="sl" name="id" id="plane_id" value={this.state.form?.id} onChange={event => this.updateFormField("id", event.target.value)} autoComplete="off" />
</td>
</tr> : null
}
<tr>
<td width="120" align="right">{i18next.t("plane:Plane name")}</td>
<td width="auto" align="left">
<input type="text" className="sl" name="name" id="plane_name" value={this.state.form?.name===undefined ? "" : this.state.form?.name} onChange={event => this.updateFormField("name", event.target.value)} autoComplete="off" />
</td>
</tr>
{
!newPlane ?
<tr>
<td width="120" align="right">{i18next.t("plane:Created time")}</td>
<td width="auto" align="left">
<span className="gray">
{plane?.createdTime}
</span>
</td>
</tr> : null
}
{
!newPlane ?
<tr>
<td width="120" align="right">{i18next.t("plane:Total nodes")}</td>
<td width="auto" align="left">
<span className="gray">
{plane?.nodesNum}
</span>
</td>
</tr> : null
}
{
!newPlane ?
<tr>
<td width="120" align="right">{i18next.t("plane:Nodes")}</td>
<td width="auto" align="left">
<span className="gray">
{
this.state.plane?.nodes.length !== 0 ?
this.state.plane?.nodes.map(node => this.renderNode(node)) :
i18next.t("plane:No node yet")
}
</span>
</td>
</tr> : null
}
<tr>
<td width="120" align="right">{i18next.t("plane:Sorter")}</td>
<td width="auto" align="left">
<input type="range" min="1" max="1000" step="1" value={this.state.form?.sorter === undefined ? 1 : this.state.form?.sorter} onChange={event => this.updateFormField("sorter", parseInt(event.target.value))}/>
&nbsp;{" "}&nbsp;{" "}
<input type="number" name="sorter" min="1" max="1000" step="1" value={this.state.form?.sorter} style={{width: "50px"}} onChange={event => this.updateFormField("sorter", parseInt(event.target.value))} />
</td>
</tr>
<tr>
<td width="120" align="right"></td>
<td width="auto" align="left">
<span className="gray">
{i18next.t("plane:Decide the order of node navigation")}
</span>
</td>
</tr>
<tr>
<td width="120" align="right">{i18next.t("plane:Visible")}</td>
<td width="auto" align="left">
<input type="radio" onClick={() => this.updateFormField("visible", true)} checked={this.state.form?.visible} name="visible" />{i18next.t("plane:Visible")}{" "}
<input type="radio" onClick={() => this.updateFormField("visible", false)} checked={!this.state.form?.visible} name="visible" />{i18next.t("plane:Invisible")}
</td>
</tr>
{
!this.state.form.visible ?
<tr>
<td width="120" align="right"></td>
<td width="auto" align="left">
<span className="gray">
{i18next.t("plane:This plane will not appear on the node navigation")}
</span>
</td>
</tr> : null
}
<tr>
<td width="120" align="right"></td>
<td width="auto" align="left">
{
!newPlane ?
<input type="submit" className="super normal button" value={i18next.t("plane:Save")} onClick={() => this.updatePlaneInfo()}/> : null
}
</td>
</tr>
{
newPlane ?
<tr>
<td width="120" align="right"></td>
<td width="auto" align="left">
<span className="gray">
{i18next.t("plane:Please go to the display page to continue to improve the information and submit")}
</span>
</td>
</tr> : null
}
</tbody>
</table>
</div>
</div>
</div>
);
}
// display
return (
<div>
{this.renderHeader()}
<div className="box">
{this.renderProblem()}
{
this.state.message !== "" ?
<div className="message" onClick={() => this.clearMessage()}>
<li className="fa fa-exclamation-triangle"></li>
&nbsp;{" "}
{this.state.message}
</div> : null
}
<div className="inner">
<table cellPadding="5" cellSpacing="0" border="0" width="100%">
<tbody>
<tr>
<td width="120" align="right">{i18next.t("plane:Image")}</td>
<td width="auto" align="left">
{
this.state.form?.image === undefined || this.state.form?.image === "" ?
<span className="gray">
{i18next.t("plane:Not set")}
</span> :
<Zmage
src={this.state.form?.image} alt={this.state.form?.id} style={{maxWidth: "48px", maxHeight: "48px"}}
/>
}
</td>
</tr>
<tr>
<td width="120" align="right">
{
newPlane ?
i18next.t("plane:Set image") :
i18next.t("plane:Change image")
}
</td>
<td width="auto" align="left">
<input type="text" className="sl" name="image" id="change_image" value={this.state.form?.image===undefined ? "" : this.state.form?.image} onChange={event => this.updateFormField("image", event.target.value)} autoComplete="off" />
</td>
</tr>
<tr>
<td width="120" align="right">{i18next.t("plane:Font color")}</td>
<td width="auto" align="left">
{this.renderColorPicker("color")}
{
this.state.displayColorPicker ?
<div style={{
position: 'absolute',
zIndex: '2'}}
>
<div style={{
position: 'fixed',
top: '0px',
right: '0px',
bottom: '0px',
left: '0px'
}} onClick={() => this.handleColorClose("color")} />
<SketchPicker color={this.state.form.color} onChange={(color) => this.handleColorChange(color, "color")} />
</div> : null
}
{" "}&nbsp;{" "}
{
!newPlane ?
<a href="#" onClick={() => this.resetColor("color")}>
{i18next.t("plane:Restore")}
</a> : null
}
</td>
</tr>
<tr>
<td width="120" align="right">{i18next.t("plane:Background color")}</td>
<td width="auto" align="left">
{this.renderColorPicker("backgroundColor")}
{
this.state.displayBackgroundColorPicker ?
<div style={{
position: 'absolute',
zIndex: '2'}}
>
<div style={{
position: 'fixed',
top: '0px',
right: '0px',
bottom: '0px',
left: '0px'
}} onClick={() => this.handleColorClose("backgroundColor")} />
<SketchPicker color={this.state.form.backgroundColor} onChange={(color) => this.handleColorChange(color, "backgroundColor")} />
</div> : null
}
{" "}&nbsp;{" "}
{
!newPlane ?
<a href="#" onClick={() => this.resetColor("backgroundColor")}>
{i18next.t("plane:Restore")}
</a> : null
}
</td>
</tr>
<tr>
<td width="120" align="right">{i18next.t("plane:Preview")}</td>
<td width="auto" align="left">
<div className="header" style={{backgroundColor: this.state.form?.backgroundColor, color: this.state.form?.color, fontSize: Setting.PcBrowser ? "" : "13px"}}>
<img src={this.state.form?.image} border="0" align="absmiddle" width="24"/>
{" "}&nbsp;{" "}{this.state.form?.name}
<span className="fr" style={{color: this.state.form?.color, lineHeight: "20px"}}>
{this.state.form?.id}{" "}{" "}
{
!newPlane ?
<span className="small">
{plane?.nodesNum}{" "}nodes
</span> : null
}
</span>
</div>
</td>
</tr>
<tr>
<td width="120" align="right"></td>
<td width="auto" align="left">
{
newPlane ?
<input type="submit" className="super normal button" value={i18next.t("plane:Create")} onClick={() => this.postNewPlane()}/> :
<input type="submit" className="super normal button" value={i18next.t("plane:Save")} onClick={() => this.updatePlaneInfo()}/>
}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
);
}
return (
<div className="box">
<div className="header">
<a href="/">{Setting.getForumName()}</a>
{" "}<span className="chevron">&nbsp;&nbsp;</span>
<a href={`/admin`}>{i18next.t("admin:Backstage management")}</a>
<span className="chevron">&nbsp;&nbsp;</span>{" "}{i18next.t("plane:Plane management")}
<div className="fr f12">
<span className="snow">{i18next.t("plane:Total planes")}{" "}&nbsp;</span>
<strong className="gray">{this.state.planes.length}</strong>
</div>
<div className="fr f12">
<strong className="gray">
<a href="plane/new">{i18next.t("plane:Add new plane")}</a>
{" "}&nbsp;
</strong>
</div>
</div>
{
this.state.message !== "" ?
<div className="message" onClick={() => this.clearMessage()}>
<li className="fa fa-exclamation-triangle"></li>
&nbsp;{" "}
{this.state.message}
</div> : null
}
<div id="all-planes">
{
this.state.planes.length !== 0 ?
this.state.planes.map(plane => this.renderPlanes(plane)) : null
}
</div>
</div>
);
}
}
export default withRouter(AdminPlane);

View File

@ -251,10 +251,12 @@ class AdminTab extends React.Component {
return (
<div className="box">
<div className="header"><a href="/">{Setting.getForumName()}</a>
{" "}<span className="chevron">&nbsp;&nbsp;</span>
<a href={`/admin`}>{i18next.t("admin:Backstage management")}</a>
{" "}<span className="chevron">&nbsp;&nbsp;</span>
<a href={`/admin/tab`}>{i18next.t("tab:Tab management")}</a>
{" "}<span className="chevron">&nbsp;&nbsp;</span>
<span className="gray">
<span>
{
this.props.event === "new" ?
i18next.t("tab:New tab") : this.state.tabId
@ -499,6 +501,8 @@ class AdminTab extends React.Component {
<div className="box">
<div className="header">
<a href="/">{Setting.getForumName()}</a>
{" "}<span className="chevron">&nbsp;&nbsp;</span>
<a href={`/admin`}>{i18next.t("admin:Backstage management")}</a>
<span className="chevron">&nbsp;&nbsp;</span>{" "}{i18next.t("tab:Tab management")}
<div className="fr f12">
<span className="snow">{i18next.t("tab:Total tabs")}{" "}&nbsp;</span>

View File

@ -21,9 +21,39 @@ export function getPlaneList() {
}).then(res => res.json());
}
export function getPlaneAdmin(id) {
return fetch(`${Setting.ServerUrl}/api/get-plane-admin?id=${id}`, {
method: 'GET',
credentials: 'include'
}).then(res => res.json());
}
export function getPlanesAdmin() {
return fetch(`${Setting.ServerUrl}/api/get-planes-admin`, {
method: 'GET',
credentials: 'include'
}).then(res => res.json());
}
export function deletePlane(id) {
return fetch(`${Setting.ServerUrl}/api/delete-plane?id=${id}`, {
method: 'POST',
credentials: 'include',
}).then(res => res.json());
}
export function updatePlane(id, plane) {
return fetch(`${Setting.ServerUrl}/api/update-plane?id=${id}`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(plane),
}).then(res => res.json());
}
export function addPlane(plane) {
return fetch(`${Setting.ServerUrl}/api/add-plane`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(plane),
}).then(res => res.json());
}

View File

@ -339,7 +339,45 @@
},
"plane":
{
"Plane list": "位面列表"
"Plane list": "Plane list",
"Are you sure to delete plane": "Are you sure to delete plane",
"Please delete all the nodes or move to another plane before deleting the plane": "Please delete all the nodes or move to another plane before deleting the plane",
"Currently the number of nodes under the plane is": "Currently the number of nodes under the plane is",
"Delete plane": "Delete plane",
"success": "success",
"Update plane information success": "Update plane information success",
"Creat plane success": "Creat plane success",
"Plane management": "Plane management",
"New plane": "New plane",
"Manage": "Manage",
"nodes": "nodes",
"Visible": "Visible",
"Invisible": "Invisible",
"Delete": "Delete",
"Plane ID": "Plane ID",
"Plane name": "Plane name",
"Created time": "Created time",
"Total nodes": "Total nodes",
"Nodes": "Nodes",
"No node yet": "No node yet",
"Sorter": "Sorter",
"Decide the order of node navigation": "Decide the order of node navigation",
"This plane will not appear on the node navigation": "This plane will not appear on the node navigation",
"Save": "Save",
"Please go to the display page to continue to improve the information and submit": "Please go to the display page to continue to improve the information and submit",
"Image": "Image",
"Not set": "Not set",
"Set image": "Set image",
"Change image": "Change image",
"Font color": "Font color",
"Restore": "Restore",
"Background color": "Background color",
"Preview": "Preview",
"Create": "Create",
"Total planes": "Total planes",
"Add new plane": "Add new plane",
"Basic Info": "Basic Info",
"Display": "Display"
},
"newNodeTopic":
{
@ -565,7 +603,8 @@
"Node management": "Node management",
"Topic management": "Topic management",
"Member management": "Member management",
"Tab management": "Tab management"
"Tab management": "Tab management",
"Plane management": "Plane management"
},
"error":
{
@ -631,6 +670,11 @@
"The tab you are trying to view does not exist": "The tab you are trying to view does not exist",
"Tab ID existed": "Tab ID existed",
"Your account has been muted": "Your account has been muted",
"Your account has been forbidden to log in": "Your account has been forbidden to log in"
"Your account has been forbidden to log in": "Your account has been forbidden to log in",
"Plane not found": "Plane not found",
"The plane you are trying to view does not exist": "The plane you are trying to view does not exist",
"Plane ID existed": "Plane ID existed",
"Please input plane ID": "Please input plane ID",
"Please input plane name": "Please input plane name"
}
}

View File

@ -339,7 +339,45 @@
},
"plane":
{
"Plane list": "位面列表"
"Plane list": "位面列表",
"Are you sure to delete plane": "你确定删除位面",
"Please delete all the nodes or move to another plane before deleting the plane": "在删除位面之前请删除位面下所有节点或移动至其他位面",
"Currently the number of nodes under the plane is": "目前此位面下的节点数为",
"Delete plane": "删除位面",
"success": "成功",
"Update plane information success": "更新位面信息成功",
"Creat plane success": "创建位面成功",
"Plane management": "位面管理",
"New plane": "新建位面",
"Manage": "管理",
"nodes": "节点",
"Visible": "可见",
"Invisible": "隐藏",
"Delete": "删除",
"Plane ID": "位面ID",
"Plane name": "位面名称",
"Created time": "创建时间",
"Total nodes": "节点总数",
"Nodes": "节点",
"No node yet": "暂无节点",
"Sorter": "排序",
"Decide the order of node navigation": "决定在节点导航中的顺序",
"This plane will not appear on the node navigation": "此位面将不会出现在节点导航中",
"Save": "保存",
"Please go to the display page to continue to improve the information and submit": "请前往显示页面继续完善信息然后提交",
"Image": "图标",
"Not set": "未设定",
"Set image": "设置图标",
"Change image": "更换图标",
"Font color": "文字颜色",
"Restore": "恢复",
"Background color": "背景颜色",
"Preview": "预览",
"Create": "创建",
"Total planes": "总位面",
"Add new plane": "新增位面",
"Basic Info": "基本信息",
"Display": "显示"
},
"newNodeTopic":
{
@ -565,7 +603,8 @@
"Node management": "节点管理",
"Topic management": "帖子管理",
"Member management": "用户管理",
"Tab management": "类别管理"
"Tab management": "类别管理",
"Plane management": "位面管理"
},
"error":
{
@ -631,6 +670,11 @@
"The tab you are trying to view does not exist": "你即将访问的类别不存在",
"Tab ID existed": "类别ID已存在",
"Your account has been muted": "你的账号已被禁言",
"Your account has been forbidden to log in": "你的账号已被禁止登录"
"Your account has been forbidden to log in": "你的账号已被禁止登录",
"Plane not found": "位面未找到",
"The plane you are trying to view does not exist": "你即将访问的位面不存在",
"Plane ID existed": "位面ID已存在",
"Please input plane ID": "请填写位面ID",
"Please input plane name": "请填写位面名称"
}
}

View File

@ -100,7 +100,7 @@ class TopicBox extends React.Component {
<div className="outdated">
{i18next.t("topic:This is a topic created")} {diffDays} {i18next.t("topic:days ago, the information in it may have changed.")}
</div>
)
);
}
addFavorite() {
@ -223,7 +223,7 @@ class TopicBox extends React.Component {
&nbsp;
</div>
</div>
)
);
}
renderImage = ({alt, src}) => {
@ -285,7 +285,7 @@ class TopicBox extends React.Component {
</div>
{" "}&nbsp;{" "}
</div>
)
);
}
renderDesktopButtons() {
@ -339,19 +339,19 @@ class TopicBox extends React.Component {
: null
}
</div>
)
);
}
render() {
const pcBrowser = Setting.PcBrowser
const pcBrowser = Setting.PcBrowser;
if (this.props.account === undefined || (this.state.topic !== null && this.state.topic.length === 0)) {
if ((this.props.account === undefined) || (this.state.topic !== null && this.state.topic.length === 0)) {
return (
<div class="box">
<div class="header"><a href="/">{Setting.getForumName()}</a> <span class="chevron">&nbsp;&nbsp;</span>{" "}{i18next.t("loading:Topic is loading")}</div>
<div class="cell"><span class="gray bigger">{i18next.t("loading:Please wait patiently...")}</span></div>
</div>
)
);
}
if (this.state.topic === null) {
@ -361,12 +361,12 @@ class TopicBox extends React.Component {
<div class="cell"><span class="gray bigger">404 Topic Not Found</span></div>
<div class="inner"> <a href="/">{i18next.t("error:Back to Home Page")}</a></div>
</div>
)
);
}
if (this.state.event === "review") {
if (this.props.account === null || this.props.account?.id !== this.state.topic?.author) {
goToLink(`/t/${this.state.topic?.id}`)
goToLink(`/t/${this.state.topic?.id}`);
}
return (
<div class="box">
@ -444,7 +444,7 @@ class TopicBox extends React.Component {
</ul>
</div>
</div>
)
);
}
return (