From 9a43a37cae5f4c62c027b9d2f597b3198a27782e Mon Sep 17 00:00:00 2001 From: sunface Date: Wed, 10 Mar 2021 13:31:43 +0800 Subject: [PATCH] update --- pages/editor/series.tsx | 130 +++++++++++++++++++++----- server/internal/api/story.go | 86 +++++++++++++++-- server/internal/server.go | 11 +++ server/internal/storage/sql_tables.go | 11 +++ server/internal/story/post.go | 8 +- server/internal/story/series.go | 55 +++++++++++ server/pkg/models/id_type.go | 6 +- server/pkg/models/models.go | 5 + server/pkg/models/story.go | 27 +++++- src/components/story/post-select.tsx | 56 +++++++++++ 10 files changed, 360 insertions(+), 35 deletions(-) create mode 100644 server/pkg/models/models.go create mode 100644 src/components/story/post-select.tsx diff --git a/pages/editor/series.tsx b/pages/editor/series.tsx index 397372dd..e3f72cef 100644 --- a/pages/editor/series.tsx +++ b/pages/editor/series.tsx @@ -1,4 +1,4 @@ -import { Menu, MenuButton, MenuList, MenuItem, Text, Box, Heading, Image, HStack, Center, Button, Flex, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, VStack, Textarea, Divider, useColorModeValue, useToast } from "@chakra-ui/react" +import { Text, Box, Heading, Image, HStack, Center, Button, Flex, FormControl, FormLabel, Input, FormErrorMessage, VStack, Textarea, Divider, useToast, Stack, StackDivider, useColorModeValue, Table, Thead, Tr, Th, Tbody, Td, CloseButton, Editable, EditablePreview, EditableInput } from "@chakra-ui/react" import Card from "components/card" import Sidebar from "layouts/sidebar/sidebar" import React, { useEffect, useState } from "react" @@ -9,26 +9,39 @@ import { Field, Form, Formik } from "formik" import { config } from "configs/config" import TextStoryCard from "components/story/text-story-card" import { Story } from "src/types/story" -import { FaExternalLinkAlt, FaRegEdit } from "react-icons/fa" +import { FaExternalLinkAlt, FaPlus, FaRegEdit } from "react-icons/fa" import { useRouter } from "next/router" import { ReserveUrls } from "src/data/reserve-urls" import Link from "next/link" import PageContainer1 from "layouts/page-container1" import Empty from "components/empty" import { IDType } from "src/types/id" +import PostSelect from "components/story/post-select" +import { cloneDeep, find, remove } from "lodash" +import userCustomTheme from "theme/user-custom" var validator = require('validator'); const newSeries: Story = { title: '', brief: '', cover: '', type: IDType.Series } const PostsPage = () => { const [currentSeries, setCurrentSeries] = useState(null) + const [series, setSeries] = useState([]) const [posts, setPosts] = useState([]) - const router = useRouter() + const [seriesPosts, setSeriesPosts] = useState([]) + const toast = useToast() + + const borderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark) + + const getSeries = () => { + requestApi.get(`/story/posts/editor?type=${IDType.Series}`).then((res) => setSeries(res.data)).catch(_ => setPosts([])) + } + const getPosts = () => { - requestApi.get(`/story/posts/editor?type=${IDType.Series}`).then((res) => setPosts(res.data)).catch(_ => setPosts([])) + requestApi.get(`/story/posts/editor?type=${IDType.Post}`).then((res) => setPosts(res.data)).catch(_ => setPosts([])) } useEffect(() => { + getSeries() getPosts() }, []) @@ -62,8 +75,12 @@ const PostsPage = () => { return error } - const submitPost = async (values, _) => { + const submitSeries = async (values, _) => { + // 这里必须按照顺序同步提交 await requestApi.post(`/story`, values) + await requestApi.post(`/story/series/post/${values.id}`, seriesPosts) + + toast({ description: "提交成功", status: "success", @@ -71,17 +88,19 @@ const PostsPage = () => { isClosable: true, }) setCurrentSeries(null) - getPosts() + getSeries() } - const editPost = (post: Story) => { - console.log(post) - setCurrentSeries(post) + const editSeries = async (series: Story) => { + const res = await requestApi.get(`/story/series/post/${series.id}`) + setSeriesPosts(res.data) + setCurrentSeries(series) } - const onDeletePost = async (id) => { + const onDeleteSeries = async (id) => { + await requestApi.delete(`/story/series/post/${id}`) await requestApi.delete(`/story/post/${id}`) - getPosts() + getSeries() toast({ description: "删除成功", status: "success", @@ -90,6 +109,41 @@ const PostsPage = () => { }) } + const onPostSelect = id => { + const sposts = cloneDeep(seriesPosts) + if (!find(sposts, v => v.id === id)) { + sposts.push({ id: id, priority: 0 }) + setSeriesPosts(sposts) + } + } + + const onPostDelete = id => { + const sposts = cloneDeep(seriesPosts) + remove(sposts, v => v.id === id) + setSeriesPosts(sposts) + } + + const onPriorityChange = (e,s) => { + if (e.currentTarget.value) { + const i = parseInt(e.currentTarget.value) + if (i) { + s.priority = i + } + } else { + s.priority = 0 + } + + const sposts = cloneDeep(seriesPosts) + setSeriesPosts(sposts) + } + + const addSeries = async () => { + const res = await requestApi.get(`/story/id/${IDType.Series}`) + newSeries.id = res.data + setSeriesPosts([]) + setCurrentSeries(newSeries) + } + return ( <> @@ -100,7 +154,7 @@ const PostsPage = () => { <> {(props) => (
@@ -135,11 +189,45 @@ const PostsPage = () => { {({ field, form }) => ( - - 关联文章 - - + + + 关联文章 + + + + {seriesPosts?.length > 0 && }> + + + + + + + + + + { + seriesPosts.map(s => { + const post = find(posts, p => p.id === s.id) + + if (post) { + return + + + + + } + return null + }) + } + +
TitlePriority(desc)
{post.title} + + + onPriorityChange(e,s)}/> + + onPostDelete(s.id)} _focus={null} />
+
}
)}
@@ -164,17 +252,17 @@ const PostsPage = () => { : <> - 系列({posts.length}) - + 系列({series.length}) + { - posts.length === 0 ? + series.length === 0 ? : <> - {posts.map(post => + {series.map(post => - editPost(post)} onDelete={() => onDeletePost(post.id)} showSource={false} /> + editSeries(post)} onDelete={() => onDeleteSeries(post.id)} showSource={false} /> )} diff --git a/server/internal/api/story.go b/server/internal/api/story.go index f45c0a6e..d2ca8b11 100644 --- a/server/internal/api/story.go +++ b/server/internal/api/story.go @@ -9,6 +9,8 @@ import ( "github.com/imdotdev/im.dev/server/internal/user" "github.com/imdotdev/im.dev/server/pkg/common" "github.com/imdotdev/im.dev/server/pkg/e" + "github.com/imdotdev/im.dev/server/pkg/models" + "github.com/imdotdev/im.dev/server/pkg/utils" ) func SubmitStory(c *gin.Context) { @@ -39,18 +41,12 @@ func DeletePost(c *gin.Context) { } user := user.CurrentUser(c) - creator, err := story.GetPostCreator(id) - if err != nil { - c.JSON(err.Status, common.RespError(err.Message)) - return - } - - if user.ID != creator { + if !models.IsStoryCreator(user.ID, id) { c.JSON(http.StatusForbidden, common.RespError(e.NoPermission)) return } - err = story.DeletePost(id) + err := story.DeletePost(id) if err != nil { c.JSON(err.Status, common.RespError(err.Message)) return @@ -76,6 +72,16 @@ func GetStory(c *gin.Context) { c.JSON(http.StatusOK, common.RespSuccess(ar)) } +func GenStoryID(c *gin.Context) { + tp := c.Param("type") + if !models.ValidStoryIDType(tp) { + c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid)) + return + } + + c.JSON(http.StatusOK, common.RespSuccess(utils.GenID(tp))) +} + func Bookmark(c *gin.Context) { storyID := c.Param("storyID") @@ -89,3 +95,67 @@ func Bookmark(c *gin.Context) { c.JSON(http.StatusOK, common.RespSuccess(nil)) } + +func SubmitSeriesPost(c *gin.Context) { + seriesID := c.Param("id") + exist := models.IdExist(seriesID) + if !exist { + c.JSON(http.StatusNotFound, common.RespError(e.NotFound)) + return + } + + u := user.CurrentUser(c) + if !models.IsStoryCreator(u.ID, seriesID) { + c.JSON(http.StatusForbidden, common.RespError(e.NoPermission)) + return + } + posts := make([]*models.SeriesPost, 0) + err0 := c.Bind(&posts) + if err0 != nil { + c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid)) + return + } + + err := story.SubmitSeriesPost(seriesID, posts) + if err != nil { + c.JSON(err.Status, common.RespError(err.Message)) + return + } + + c.JSON(http.StatusOK, common.RespSuccess(nil)) +} + +func GetSeriesPost(c *gin.Context) { + id := c.Param("id") + + posts, err := story.GetSeriesPost(id) + if err != nil { + c.JSON(err.Status, common.RespError(err.Message)) + return + } + + c.JSON(http.StatusOK, common.RespSuccess(posts)) +} + +func DeleteSeriesPost(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid)) + return + } + + user := user.CurrentUser(c) + if !models.IsStoryCreator(user.ID, id) { + c.JSON(http.StatusForbidden, common.RespError(e.NoPermission)) + return + } + + err := story.DeleteSeriesPost(id) + if err != nil { + c.JSON(err.Status, common.RespError(err.Message)) + return + } + + c.JSON(http.StatusOK, common.RespSuccess(nil)) + +} diff --git a/server/internal/server.go b/server/internal/server.go index 6726e1ee..f63265c4 100644 --- a/server/internal/server.go +++ b/server/internal/server.go @@ -46,6 +46,7 @@ func (s *Server) Start() error { //story apis r.GET("/story/post/:id", api.GetStory) + r.GET("/story/id/:type", IsLogin(), InvasionCheck(), api.GenStoryID) r.GET("/story/comments/:id", api.GetStoryComments) r.POST("/story/comment", IsLogin(), api.SubmitComment) r.DELETE("/story/comment/:id", IsLogin(), api.DeleteStoryComment) @@ -53,6 +54,9 @@ func (s *Server) Start() error { r.GET("/story/posts/drafts", IsLogin(), api.GetEditorDrafts) r.GET("/story/posts/home/:filter", api.GetHomePosts) r.POST("/story", IsLogin(), api.SubmitStory) + r.POST("/story/series/post/:id", IsLogin(), api.SubmitSeriesPost) + r.GET("/story/series/post/:id", api.GetSeriesPost) + r.DELETE("/story/series/post/:id", api.DeleteSeriesPost) r.POST("/story/post/draft", IsLogin(), api.SubmitPostDraft) r.DELETE("/story/post/:id", IsLogin(), api.DeletePost) r.POST("/story/bookmark/:storyID", IsLogin(), api.Bookmark) @@ -134,3 +138,10 @@ func IsLogin() gin.HandlerFunc { c.Next() } } + +func InvasionCheck() gin.HandlerFunc { + return func(c *gin.Context) { + //@todo + c.Next() + } +} diff --git a/server/internal/storage/sql_tables.go b/server/internal/storage/sql_tables.go index 8bda580a..80fe0765 100644 --- a/server/internal/storage/sql_tables.go +++ b/server/internal/storage/sql_tables.go @@ -165,4 +165,15 @@ var sqlTables = map[string]string{ CREATE INDEX IF NOT EXISTS bookmarks_userid ON bookmarks (user_id); `, + + "series_post": `CREATE TABLE IF NOT EXISTS series_post ( + series_id VARCHAR(255), + post_id VARCHAR(255), + priority TINYINT + ); + CREATE INDEX IF NOT EXISTS series_post_seriesid + ON series_post (series_id); + CREATE INDEX IF NOT EXISTS series_post_postid + ON series_post (post_id); + `, } diff --git a/server/internal/story/post.go b/server/internal/story/post.go index 6558ad22..bc7d05af 100644 --- a/server/internal/story/post.go +++ b/server/internal/story/post.go @@ -76,8 +76,12 @@ func SubmitStory(c *gin.Context) (map[string]string, *e.Error) { setSlug(user.ID, post) - if post.ID == "" { - post.ID = utils.GenID(post.Type) + exist := models.IdExist(post.ID) + if !exist { + if post.ID == "" { + post.ID = utils.GenID(post.Type) + } + //create _, err := db.Conn.Exec("INSERT INTO story (id,type,creator,slug, title, md, url, cover, brief,status, created, updated) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", post.ID, post.Type, user.ID, post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, models.StatusPublished, now, now) diff --git a/server/internal/story/series.go b/server/internal/story/series.go index aeedf369..d413c3c8 100644 --- a/server/internal/story/series.go +++ b/server/internal/story/series.go @@ -1 +1,56 @@ package story + +import ( + "net/http" + + "github.com/imdotdev/im.dev/server/pkg/db" + "github.com/imdotdev/im.dev/server/pkg/e" + "github.com/imdotdev/im.dev/server/pkg/models" +) + +func SubmitSeriesPost(seriesID string, posts []*models.SeriesPost) *e.Error { + _, err := db.Conn.Exec("DELETE FROM series_post WHERE series_id=?", seriesID) + if err != nil { + logger.Warn("delete series post error", "error", err) + return e.New(http.StatusInternalServerError, e.Internal) + } + + for _, post := range posts { + _, err = db.Conn.Exec("INSERT INTO series_post (series_id,post_id,priority) VALUES (?,?,?)", seriesID, post.PostID, post.Priority) + if err != nil { + logger.Warn("add series post error", "error", err) + } + } + + return nil +} + +func GetSeriesPost(seriesID string) ([]*models.SeriesPost, *e.Error) { + posts := make([]*models.SeriesPost, 0) + rows, err := db.Conn.Query("SELECT post_id,priority FROM series_post WHERE series_id=?", seriesID) + if err != nil { + logger.Warn("select series post error", "error", err) + return nil, e.New(http.StatusInternalServerError, e.Internal) + } + + for rows.Next() { + post := &models.SeriesPost{} + err := rows.Scan(&post.PostID, &post.Priority) + if err != nil { + logger.Warn("scan series post error", "error", err) + continue + } + posts = append(posts, post) + } + + return posts, nil +} + +func DeleteSeriesPost(id string) *e.Error { + _, err := db.Conn.Exec("DELETE FROM series_post WHERE series_id=?", id) + if err != nil { + return e.New(http.StatusInternalServerError, e.Internal) + } + + return nil +} diff --git a/server/pkg/models/id_type.go b/server/pkg/models/id_type.go index a1375ab8..ae91554d 100644 --- a/server/pkg/models/id_type.go +++ b/server/pkg/models/id_type.go @@ -1,9 +1,9 @@ package models import ( + "database/sql" "fmt" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/imdotdev/im.dev/server/pkg/db" ) @@ -56,8 +56,8 @@ func IdExist(id string) bool { var nid string err := db.Conn.QueryRow(fmt.Sprintf("SELECT id from %s WHERE id=?", tbl), id).Scan(&nid) - if err != nil { - logger.Warn("query post error", "error", err) + if err != nil && err != sql.ErrNoRows { + logger.Warn("check id exist error", "error", err, "table", tbl, "id", id) return false } diff --git a/server/pkg/models/models.go b/server/pkg/models/models.go new file mode 100644 index 00000000..5b760c31 --- /dev/null +++ b/server/pkg/models/models.go @@ -0,0 +1,5 @@ +package models + +import "github.com/imdotdev/im.dev/server/pkg/log" + +var logger = log.RootLogger.New("logger", "models") diff --git a/server/pkg/models/story.go b/server/pkg/models/story.go index 695b3f27..07747f27 100644 --- a/server/pkg/models/story.go +++ b/server/pkg/models/story.go @@ -1,6 +1,11 @@ package models -import "time" +import ( + "fmt" + "time" + + "github.com/imdotdev/im.dev/server/pkg/db" +) const ( StatusDraft = 1 @@ -46,3 +51,23 @@ func (s FavorStories) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s FavorStories) Less(i, j int) bool { return s[i].Likes > s[j].Likes } + +type SeriesPost struct { + PostID string `json:"id"` + Priority int `json:"priority"` +} + +func IsStoryCreator(userID string, storyID string) bool { + var nid string + err := db.Conn.QueryRow("SELECT creator FROM story WHERE id=?", storyID).Scan(&nid) + fmt.Println(userID, storyID, err) + if err != nil { + return false + } + + if nid == userID { + return true + } + + return false +} diff --git a/src/components/story/post-select.tsx b/src/components/story/post-select.tsx new file mode 100644 index 00000000..c0e295ba --- /dev/null +++ b/src/components/story/post-select.tsx @@ -0,0 +1,56 @@ +import React from "react" +import { Popover, PopoverTrigger, IconButton, PopoverContent, useDisclosure, Stack, Box, Heading, Text, Divider, StackDivider, useColorModeValue, border, Button, Flex } from "@chakra-ui/react" +import { Story } from "src/types/story" +import { FaPlus } from "react-icons/fa" +import userCustomTheme from "theme/user-custom" +import { find } from "lodash" +import { CheckIcon } from "@chakra-ui/icons" + +interface Props { + posts: Story[] + selected: any[] + onSelect:any +} + +export const PostSelect = (props: Props) => { + const { onOpen, onClose, isOpen } = useDisclosure() + const borderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark) + const beenSelected = id => { + if (find(props.selected,v => id ===v.id)) { + return true + } + + return false + } + + return ( + + + } variant="ghost" _focus={null} /> + + + }> + { + props.posts.map(p => + props.onSelect(p.id)}> + + {p.title} + {p.brief} + + {beenSelected(p.id) ? : null} + ) + } + + + + + ) +} + +export default PostSelect