pull/51/head
sunface 4 years ago
parent 001609b801
commit 9a43a37cae

@ -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 Card from "components/card"
import Sidebar from "layouts/sidebar/sidebar" import Sidebar from "layouts/sidebar/sidebar"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
@ -9,26 +9,39 @@ import { Field, Form, Formik } from "formik"
import { config } from "configs/config" import { config } from "configs/config"
import TextStoryCard from "components/story/text-story-card" import TextStoryCard from "components/story/text-story-card"
import { Story } from "src/types/story" 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 { useRouter } from "next/router"
import { ReserveUrls } from "src/data/reserve-urls" import { ReserveUrls } from "src/data/reserve-urls"
import Link from "next/link" import Link from "next/link"
import PageContainer1 from "layouts/page-container1" import PageContainer1 from "layouts/page-container1"
import Empty from "components/empty" import Empty from "components/empty"
import { IDType } from "src/types/id" 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'); var validator = require('validator');
const newSeries: Story = { title: '', brief: '', cover: '', type: IDType.Series } const newSeries: Story = { title: '', brief: '', cover: '', type: IDType.Series }
const PostsPage = () => { const PostsPage = () => {
const [currentSeries, setCurrentSeries] = useState(null) const [currentSeries, setCurrentSeries] = useState(null)
const [series, setSeries] = useState([])
const [posts, setPosts] = useState([]) const [posts, setPosts] = useState([])
const router = useRouter() const [seriesPosts, setSeriesPosts] = useState([])
const toast = useToast() 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 = () => { 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(() => { useEffect(() => {
getSeries()
getPosts() getPosts()
}, []) }, [])
@ -62,8 +75,12 @@ const PostsPage = () => {
return error return error
} }
const submitPost = async (values, _) => { const submitSeries = async (values, _) => {
// 这里必须按照顺序同步提交
await requestApi.post(`/story`, values) await requestApi.post(`/story`, values)
await requestApi.post(`/story/series/post/${values.id}`, seriesPosts)
toast({ toast({
description: "提交成功", description: "提交成功",
status: "success", status: "success",
@ -71,17 +88,19 @@ const PostsPage = () => {
isClosable: true, isClosable: true,
}) })
setCurrentSeries(null) setCurrentSeries(null)
getPosts() getSeries()
} }
const editPost = (post: Story) => { const editSeries = async (series: Story) => {
console.log(post) const res = await requestApi.get(`/story/series/post/${series.id}`)
setCurrentSeries(post) 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}`) await requestApi.delete(`/story/post/${id}`)
getPosts() getSeries()
toast({ toast({
description: "删除成功", description: "删除成功",
status: "success", 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 ( return (
<> <>
<PageContainer1 > <PageContainer1 >
@ -100,7 +154,7 @@ const PostsPage = () => {
<> <>
<Formik <Formik
initialValues={currentSeries} initialValues={currentSeries}
onSubmit={submitPost} onSubmit={submitSeries}
> >
{(props) => ( {(props) => (
<Form> <Form>
@ -135,11 +189,45 @@ const PostsPage = () => {
<Field> <Field>
{({ field, form }) => ( {({ field, form }) => (
<FormControl isInvalid={form.errors.brief && form.touched.brief}> <FormControl isInvalid={form.errors.brief && form.touched.brief}>
<Divider mt="0" mb="6" /> <FormLabel>
<FormLabel></FormLabel> <HStack>
<Button variant="ghost"></Button> <Text></Text>
<Divider mt="6" mb="6" /> <PostSelect selected={seriesPosts} posts={posts} onSelect={onPostSelect} />
</HStack>
</FormLabel>
{seriesPosts?.length > 0 && <Stack spacing="1" py="2" divider={<StackDivider borderColor={borderColor} />}>
<Table variant="simple" size="sm">
<Thead>
<Tr>
<Th>Title</Th>
<Th>Priority(desc)</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{
seriesPosts.map(s => {
const post = find(posts, p => p.id === s.id)
if (post) {
return <Tr key={post.id}>
<Td>{post.title}</Td>
<Td>
<Editable value={s.priority}>
<EditablePreview minWidth="100px"/>
<EditableInput onChange={(e) => onPriorityChange(e,s)}/>
</Editable>
</Td>
<Td width="50px"><CloseButton size="sm" onClick={() => onPostDelete(s.id)} _focus={null} /></Td>
</Tr>
}
return null
})
}
</Tbody>
</Table>
</Stack>}
</FormControl> </FormControl>
)} )}
</Field> </Field>
@ -164,17 +252,17 @@ const PostsPage = () => {
: :
<> <>
<Flex alignItems="center" justify="space-between"> <Flex alignItems="center" justify="space-between">
<Heading size="md">({posts.length})</Heading> <Heading size="md">({series.length})</Heading>
<Button colorScheme="teal" size="sm" onClick={() => setCurrentSeries(newSeries)} _focus={null}></Button> <Button colorScheme="teal" size="sm" onClick={() => addSeries()} _focus={null}></Button>
</Flex> </Flex>
{ {
posts.length === 0 ? <Empty /> series.length === 0 ? <Empty />
: :
<> <>
<VStack mt="4"> <VStack mt="4">
{posts.map(post => {series.map(post =>
<Box width="100%" key={post.id}> <Box width="100%" key={post.id}>
<TextStoryCard story={post} showActions={true} mt="4" onEdit={() => editPost(post)} onDelete={() => onDeletePost(post.id)} showSource={false} /> <TextStoryCard story={post} showActions={true} mt="4" onEdit={() => editSeries(post)} onDelete={() => onDeleteSeries(post.id)} showSource={false} />
<Divider mt="5" /> <Divider mt="5" />
</Box> </Box>
)} )}

@ -9,6 +9,8 @@ import (
"github.com/imdotdev/im.dev/server/internal/user" "github.com/imdotdev/im.dev/server/internal/user"
"github.com/imdotdev/im.dev/server/pkg/common" "github.com/imdotdev/im.dev/server/pkg/common"
"github.com/imdotdev/im.dev/server/pkg/e" "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) { func SubmitStory(c *gin.Context) {
@ -39,18 +41,12 @@ func DeletePost(c *gin.Context) {
} }
user := user.CurrentUser(c) user := user.CurrentUser(c)
creator, err := story.GetPostCreator(id) if !models.IsStoryCreator(user.ID, id) {
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
if user.ID != creator {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission)) c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
return return
} }
err = story.DeletePost(id) err := story.DeletePost(id)
if err != nil { if err != nil {
c.JSON(err.Status, common.RespError(err.Message)) c.JSON(err.Status, common.RespError(err.Message))
return return
@ -76,6 +72,16 @@ func GetStory(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(ar)) 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) { func Bookmark(c *gin.Context) {
storyID := c.Param("storyID") storyID := c.Param("storyID")
@ -89,3 +95,67 @@ func Bookmark(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(nil)) 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))
}

@ -46,6 +46,7 @@ func (s *Server) Start() error {
//story apis //story apis
r.GET("/story/post/:id", api.GetStory) r.GET("/story/post/:id", api.GetStory)
r.GET("/story/id/:type", IsLogin(), InvasionCheck(), api.GenStoryID)
r.GET("/story/comments/:id", api.GetStoryComments) r.GET("/story/comments/:id", api.GetStoryComments)
r.POST("/story/comment", IsLogin(), api.SubmitComment) r.POST("/story/comment", IsLogin(), api.SubmitComment)
r.DELETE("/story/comment/:id", IsLogin(), api.DeleteStoryComment) 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/drafts", IsLogin(), api.GetEditorDrafts)
r.GET("/story/posts/home/:filter", api.GetHomePosts) r.GET("/story/posts/home/:filter", api.GetHomePosts)
r.POST("/story", IsLogin(), api.SubmitStory) 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.POST("/story/post/draft", IsLogin(), api.SubmitPostDraft)
r.DELETE("/story/post/:id", IsLogin(), api.DeletePost) r.DELETE("/story/post/:id", IsLogin(), api.DeletePost)
r.POST("/story/bookmark/:storyID", IsLogin(), api.Bookmark) r.POST("/story/bookmark/:storyID", IsLogin(), api.Bookmark)
@ -134,3 +138,10 @@ func IsLogin() gin.HandlerFunc {
c.Next() c.Next()
} }
} }
func InvasionCheck() gin.HandlerFunc {
return func(c *gin.Context) {
//@todo
c.Next()
}
}

@ -165,4 +165,15 @@ var sqlTables = map[string]string{
CREATE INDEX IF NOT EXISTS bookmarks_userid CREATE INDEX IF NOT EXISTS bookmarks_userid
ON bookmarks (user_id); 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);
`,
} }

@ -76,8 +76,12 @@ func SubmitStory(c *gin.Context) (map[string]string, *e.Error) {
setSlug(user.ID, post) setSlug(user.ID, post)
if post.ID == "" { exist := models.IdExist(post.ID)
post.ID = utils.GenID(post.Type) if !exist {
if post.ID == "" {
post.ID = utils.GenID(post.Type)
}
//create //create
_, err := db.Conn.Exec("INSERT INTO story (id,type,creator,slug, title, md, url, cover, brief,status, created, updated) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", _, 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) post.ID, post.Type, user.ID, post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, models.StatusPublished, now, now)

@ -1 +1,56 @@
package story 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
}

@ -1,9 +1,9 @@
package models package models
import ( import (
"database/sql"
"fmt" "fmt"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
"github.com/imdotdev/im.dev/server/pkg/db" "github.com/imdotdev/im.dev/server/pkg/db"
) )
@ -56,8 +56,8 @@ func IdExist(id string) bool {
var nid string var nid string
err := db.Conn.QueryRow(fmt.Sprintf("SELECT id from %s WHERE id=?", tbl), id).Scan(&nid) err := db.Conn.QueryRow(fmt.Sprintf("SELECT id from %s WHERE id=?", tbl), id).Scan(&nid)
if err != nil { if err != nil && err != sql.ErrNoRows {
logger.Warn("query post error", "error", err) logger.Warn("check id exist error", "error", err, "table", tbl, "id", id)
return false return false
} }

@ -0,0 +1,5 @@
package models
import "github.com/imdotdev/im.dev/server/pkg/log"
var logger = log.RootLogger.New("logger", "models")

@ -1,6 +1,11 @@
package models package models
import "time" import (
"fmt"
"time"
"github.com/imdotdev/im.dev/server/pkg/db"
)
const ( const (
StatusDraft = 1 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 { func (s FavorStories) Less(i, j int) bool {
return s[i].Likes > s[j].Likes 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
}

@ -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 (
<Popover
isOpen={isOpen}
onOpen={onOpen}
onClose={onClose}
placement="right"
autoFocus={false}
>
<PopoverTrigger>
<IconButton aria-label="select post" size="sm" icon={<FaPlus />} variant="ghost" _focus={null} />
</PopoverTrigger>
<PopoverContent maxHeight="400px" overflow="scroll">
<Stack spacing="1" py="2" divider={<StackDivider borderColor={borderColor} />}>
{
props.posts.map(p =>
<Flex key={p.id} justifyContent="space-between" alignItems="center" px="4" cursor="pointer" onClick={() => props.onSelect(p.id)}>
<Box py="2" >
<Heading size="xs">{p.title}</Heading>
<Text layerStyle="textSecondary" mt="2">{p.brief}</Text>
</Box>
{beenSelected(p.id) ? <CheckIcon color="green.400"/> : null}
</Flex>)
}
</Stack>
</PopoverContent>
</Popover>
)
}
export default PostSelect
Loading…
Cancel
Save