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 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 (
<>
<PageContainer1 >
@ -100,7 +154,7 @@ const PostsPage = () => {
<>
<Formik
initialValues={currentSeries}
onSubmit={submitPost}
onSubmit={submitSeries}
>
{(props) => (
<Form>
@ -135,11 +189,45 @@ const PostsPage = () => {
<Field>
{({ field, form }) => (
<FormControl isInvalid={form.errors.brief && form.touched.brief}>
<Divider mt="0" mb="6" />
<FormLabel></FormLabel>
<Button variant="ghost"></Button>
<Divider mt="6" mb="6" />
<FormLabel>
<HStack>
<Text></Text>
<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>
)}
</Field>
@ -164,17 +252,17 @@ const PostsPage = () => {
:
<>
<Flex alignItems="center" justify="space-between">
<Heading size="md">({posts.length})</Heading>
<Button colorScheme="teal" size="sm" onClick={() => setCurrentSeries(newSeries)} _focus={null}></Button>
<Heading size="md">({series.length})</Heading>
<Button colorScheme="teal" size="sm" onClick={() => addSeries()} _focus={null}></Button>
</Flex>
{
posts.length === 0 ? <Empty />
series.length === 0 ? <Empty />
:
<>
<VStack mt="4">
{posts.map(post =>
{series.map(post =>
<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" />
</Box>
)}

@ -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))
}

@ -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()
}
}

@ -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);
`,
}

@ -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)

@ -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
}

@ -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
}

@ -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
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
}

@ -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