pull/52/head
sunface 4 years ago
parent 0ee209f273
commit af8c4e85bf

@ -86,7 +86,7 @@ const UserNavbarPage = () => {
<>
<PageContainer1>
<Box display="flex">
<Sidebar routes={adminLinks} width={["120px", "120px", "250px", "250px"]} height="fit-content" title="博客设置" />
<Sidebar routes={adminLinks} width={["120px", "120px", "250px", "250px"]} height="fit-content" title="管理员" />
<Card ml="4" width="100%">
<Flex justifyContent="space-between" alignItems="center">
<Heading size="sm"></Heading>

@ -0,0 +1,147 @@
import { Text, Box, Heading, Image, Center, Button, Flex, VStack, Divider, useToast, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, HStack, Wrap, useMediaQuery, Avatar, Textarea, Table, Thead, Tr, Th, Tbody, Td, IconButton, useDisclosure, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, Select, NumberInput, NumberInputField, NumberInputStepper, NumberIncrementStepper, NumberDecrementStepper } from "@chakra-ui/react"
import Card from "components/card"
import PageContainer from "layouts/page-container"
import Sidebar from "layouts/sidebar/sidebar"
import React, { useEffect, useState } from "react"
import { adminLinks, settingLinks } from "src/data/links"
import { requestApi } from "utils/axios/request"
import { config } from "configs/config"
import { getSvgIcon } from "components/svg-icon"
import { Navbar, NavbarType } from "src/types/user"
import { cloneDeep } from "lodash"
import { IDType } from "src/types/id"
import { Story } from "src/types/story"
import PageContainer1 from "layouts/page-container1"
import { HomeSidebar } from "src/types/misc"
import { SearchFilter } from "src/types/search"
const SidebarsPage = () => {
const [sidebars, setSidebars]:[HomeSidebar[],any] = useState([])
const [currentSidebar, setCurrentSidebar]: [HomeSidebar, any] = useState(null)
const { isOpen, onOpen, onClose } = useDisclosure()
const toast = useToast()
useEffect(() => {
getSidebars()
}, [])
const getSidebars = async () => {
const res = await requestApi.get("/sidebars")
setSidebars(res.data)
}
const submitNavbar = async () => {
await requestApi.get(`/tag/info/${currentSidebar.tagName}`)
await requestApi.post(`/sidebar`, currentSidebar)
setCurrentSidebar(null)
onClose()
getSidebars()
}
const onAddNavbar = () => {
setCurrentSidebar({tagName: "", sort: SearchFilter.Recent , displayCount:5, weight: 0})
onOpen()
}
const onEditNavbar = nav => {
setCurrentSidebar(nav)
onOpen()
}
const onSidebarChange = () => {
const nv = cloneDeep(currentSidebar)
setCurrentSidebar(nv)
}
const onDeleteNavbar = async id => {
requestApi.delete(`/sidebar/${id}`)
setTimeout( () => getSidebars(),300)
}
return (
<>
<PageContainer1>
<Box display="flex">
<Sidebar routes={adminLinks} width={["120px", "120px", "250px", "250px"]} height="fit-content" title="管理员" />
<Card ml="4" width="100%">
<Flex justifyContent="space-between" alignItems="center">
<Heading size="sm"></Heading>
<Button colorScheme="teal" size="sm" onClick={onAddNavbar} _focus={null}></Button>
</Flex>
<Table variant="simple" mt="4">
<Thead>
<Tr>
<Th>Tag Name</Th>
<Th>Sort</Th>
<Th>Display count</Th>
<Th>Weight</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{
sidebars.map((nv,i) => <Tr key={i}>
<Td>{nv.tagName}</Td>
<Td>{nv.sort}</Td>
<Td>{nv.displayCount}</Td>
<Td>{nv.weight}</Td>
<Td>
<IconButton aria-label="edit navbar" variant="ghost" icon={getSvgIcon('edit', ".95rem")} onClick={() => onEditNavbar(nv)}/>
<IconButton aria-label="delete navbar" variant="ghost" icon={getSvgIcon('close', "1rem")} onClick={() => onDeleteNavbar(nv.id)} />
</Td>
</Tr>)
}
</Tbody>
</Table>
</Card>
</Box>
</PageContainer1>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
{currentSidebar && <ModalContent>
<ModalHeader>{currentSidebar.tagName ? "编辑侧边栏" : "新建侧边栏"}</ModalHeader>
<ModalBody mb="2">
<VStack spacing="4" alignItems="left">
<HStack spacing="4">
<Heading size="xs" width="150px">Tag name</Heading>
<Input value={currentSidebar.tagName} _focus={null} variant="flushed" onChange={e => { currentSidebar.tagName = e.currentTarget.value; onSidebarChange() }}></Input>
</HStack>
{/* <HStack spacing="4">
<Heading size="xs" width="150px">Sort</Heading>
<Input value={currentSidebar.sort} _focus={null} variant="flushed" onChange={e => { currentSidebar.value = e.currentTarget.value; onSidebarChange() }} placeholder="enter a url, e.g /search"/>
</HStack> */}
<HStack spacing="4">
<Heading size="xs" width="105px">Display count</Heading>
<NumberInput min={0} max={10} value={currentSidebar.displayCount} variant="flushed" onChange={e => { currentSidebar.displayCount = parseInt(e); onSidebarChange() }}>
<NumberInputField _focus={null} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</HStack>
<HStack spacing="4">
<Heading size="xs" width="105px">Weight</Heading>
<NumberInput min={0} max={10} value={currentSidebar.weight} variant="flushed" onChange={e => { currentSidebar.weight = parseInt(e); onSidebarChange() }}>
<NumberInputField _focus={null} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</HStack>
</VStack>
<Button colorScheme="teal" variant="outline" mt="6" onClick={submitNavbar}></Button>
</ModalBody>
</ModalContent>}
</Modal>
</>
)
}
export default SidebarsPage

@ -9,7 +9,7 @@ import {
import siteConfig from "configs/site-config"
import PageContainer1 from "layouts/page-container1"
import React, { useEffect, useState } from "react"
import { HomeSidebar } from 'pages/index'
import { IndexSidebar } from 'pages/index'
import Card from "components/card"
import { config } from "configs/config"
import { Tag } from "src/types/tag"
@ -108,7 +108,7 @@ import StoryCard from "components/story/story-card"
}
</Card>
</VStack>
<HomeSidebar />
<IndexSidebar />
</HStack>
</PageContainer1>
</>

@ -19,8 +19,11 @@ import { requestApi } from "utils/axios/request"
import StoryFilters from "components/story/story-filter"
import { concat } from "lodash"
import useInfiniteScroll from 'src/hooks/use-infinite-scroll'
import { HomeSidebar } from "src/types/misc"
import Link from "next/link"
import { ReserveUrls } from "src/data/reserve-urls"
const HomePage = () => {
const [filter,setFilter] = useState('Best')
const [filter, setFilter] = useState('Best')
const initData = (p) => {
return requestApi.get(`/story/posts/home?filter=${filter}&page=${p}&per_page=5`)
}
@ -40,7 +43,7 @@ const HomePage = () => {
<VStack alignItems="left" width={["100%", "100%", "70%", "70%"]} spacing="3">
<Card p="2">
<Flex justifyContent="space-between" alignItems="center">
<StoryFilters onChange={onFilterChange}/>
<StoryFilters onChange={onFilterChange} />
<Menu>
<MenuButton
as={IconButton}
@ -62,10 +65,10 @@ const HomePage = () => {
</Flex>
</Card>
<Card width="100%" height="fit-content" p="0">
<Stories onLoad={initData} filter={filter}/>
<Stories onLoad={initData} filter={filter} />
</Card>
</VStack>
<HomeSidebar />
<IndexSidebar />
</HStack>
</PageContainer1>
@ -75,35 +78,56 @@ const HomePage = () => {
export default HomePage
export const HomeSidebar = () => {
const [posts, setPosts] = useState([])
// const initData = async () => {
// const res = await requestApi.get(`/story/posts/home/aa`)
// setPosts(res.data)
// }
export const IndexSidebar = () => {
const [sidebars, setSidebars]: [HomeSidebar[], any] = useState([])
const getSidebars = async () => {
const res = await requestApi.get("/sidebars")
setSidebars(res.data)
}
// useEffect(() => {
// initData()
// }, [])
useEffect(() => {
getSidebars()
}, [])
return (
<VStack alignItems="left" width="30%" display={{ base: "none", md: "flex" }}>
<Card p="0">
{
sidebars.map(sb =>
<IndexSidebarCard sidebar={sb} />
)
}
</VStack>
)
}
const IndexSidebarCard = ({ sidebar }) => {
const [posts, setPosts] = useState([])
useEffect(() => {
initData()
}, [])
const initData = async () => {
const res = await requestApi.get(`/tag/info/${sidebar.tagName}`)
const res1 = await requestApi.get(`/tag/posts/${res.data.id}?filter=${sidebar.sort}&page=${1}&per_page=${sidebar.displayCount}`)
setPosts(res1.data)
}
return (
<>
{posts.length > 0 && <Card p="0">
<CardHeader>
<Heading size="sm"></Heading>
<HStack>
<Button variant="ghost" size="sm" height="auto">1d</Button>
<Button variant="ghost" size="sm" height="auto">1w</Button>
<Button variant="ghost" size="sm" height="auto">1m</Button>
</HStack>
<Link href={`${ReserveUrls.Tags}/${sidebar.tagName}`}><Heading size="sm" cursor="pointer">#{sidebar.tagName}</Heading></Link>
</CardHeader>
<Divider />
<CardBody>
{/* <VStack alignItems="left">
<Stories stories={posts} card={SimplePostCard} size="sm" showFooter={false}/>
</VStack> */}
<VStack alignItems="left" mt="1">
{
posts.map(p => <Box mb="1"><SimplePostCard story={p} /></Box>)
}
</VStack>
</CardBody>
</Card>
</VStack>
</Card>}
</>
)
}

@ -142,6 +142,8 @@ func (s *Server) Start() error {
r.POST("/navbar", IsLogin(), SubmitNavbar)
r.DELETE("/navbar/:id", IsLogin(), DeleteNavbar)
r.GET("/sidebars", GetSidebars)
r.POST("/sidebar", IsLogin(), SubmitSidebar)
err := router.Run(config.Data.Server.Addr)
if err != nil {
logger.Crit("start backend server error", "error", err)

@ -0,0 +1,59 @@
package internal
import (
"net/http"
"sort"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/user"
"github.com/imdotdev/im.dev/server/pkg/common"
"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 SubmitSidebar(c *gin.Context) {
side := &models.Sidebar{}
c.Bind(&side)
u := user.CurrentUser(c)
if !u.Role.IsAdmin() {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
return
}
var err error
if side.ID == 0 {
_, err = db.Conn.Exec("INSERT INTO home_sidebar (tag_name,sort,display_count,weight) VALUES (?,?,?,?)",
side.TagName, side.Sort, side.DisplayCount, side.Weight)
} else {
_, err = db.Conn.Exec("UPDATE home_sidebar SET tag_name=?,sort=?,display_count=?,weight=? WHERE id=?", side.TagName, side.Sort, side.DisplayCount, side.Weight, side.ID)
}
if err != nil {
logger.Warn("submit sidebar error", "error", err)
c.JSON(http.StatusInternalServerError, common.RespError(e.Internal))
return
}
c.JSON(http.StatusOK, common.RespSuccess(nil))
}
func GetSidebars(c *gin.Context) {
navbars := make(models.Sidebars, 0)
rows, err := db.Conn.Query("SELECT id,tag_name,sort,display_count,weight FROM home_sidebar")
if err != nil {
c.JSON(http.StatusInternalServerError, common.RespError(e.Internal))
return
}
for rows.Next() {
nv := &models.Sidebar{}
rows.Scan(&nv.ID, &nv.TagName, &nv.Sort, &nv.DisplayCount, &nv.Weight)
navbars = append(navbars, nv)
}
sort.Sort(navbars)
c.JSON(http.StatusOK, common.RespSuccess(navbars))
}

@ -240,4 +240,12 @@ var sqlTables = map[string]string{
weight TINYINT DEFAULT 0
);
`,
"home_sidebar": `CREATE TABLE IF NOT EXISTS home_sidebar (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tag_name VARCHAR(255),
sort VARCHAR(20),
display_count TINYINT,
weight TINYINT DEFAULT 0
);`,
}

@ -100,7 +100,7 @@ func GetTag(id string, name string) (*models.Tag, *e.Error) {
)
if err != nil {
if err == sql.ErrNoRows {
return nil, e.New(http.StatusNotFound, e.NotFound)
return nil, e.New(http.StatusNotFound, "Tag不存在")
}
logger.Warn("get post error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)

@ -0,0 +1,17 @@
package models
type Sidebar struct {
ID int `json:"id"`
TagName string `json:"tagName"`
Sort string `json:"sort"`
DisplayCount int `json:"displayCount"`
Weight int `json:"weight"`
}
type Sidebars []*Sidebar
func (t Sidebars) Len() int { return len(t) }
func (t Sidebars) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t Sidebars) Less(i, j int) bool {
return t[i].Weight > t[j].Weight
}

@ -19,19 +19,19 @@ export const SimpleStoryCard = (props: Props) => {
const [isLargeScreen] = useMediaQuery("(min-width: 768px)")
const Layout = isLargeScreen ? HStack : VStack
return (
<VStack alignItems="left" spacing="0">
<VStack alignItems="left" spacing="1">
<Link href={getStoryUrl(story)}><Heading pb="2" fontSize=".9rem" cursor="pointer">{story.title}</Heading></Link>
<HStack pl="1" spacing="2" fontSize={size==='md'? '1rem' : ".9rem"}>
<Link href={`/${story.creator.username}`}><Text cursor="pointer" fontSize="0.8rem">{story.creator.nickname}</Text></Link>
<HStack pl="1" spacing="1" fontSize={size==='md'? '1rem' : ".9rem"}>
{/* <Link href={`/${story.creator.username}`}><Text cursor="pointer" fontSize="0.8rem">{story.creator.nickname}</Text></Link> */}
<HStack opacity="0.9">
{/* <HStack opacity="0.9">
<Like liked={story.liked} count={story.likes} storyID={story.id} fontSize="0.8rem"/>
</HStack>
</HStack> */}
<a href={`${getCommentsUrl(story)}#comments`}>
<HStack opacity="0.9" cursor="pointer" spacing="3">
{getSvgIcon("comments1", "0.9rem")}
<Text fontSize="0.8rem" opacity="0.8">{story.comments}</Text>
<HStack opacity="0.9" cursor="pointer" spacing="2">
<Text fontSize="0.9rem" opacity="0.85">{story.comments}</Text>
<Text fontSize="0.9rem" opacity="0.85">comments</Text>
</HStack>
</a>
</HStack>

@ -1,6 +1,6 @@
import { getSvgIcon } from 'components/svg-icon'
import React from 'react'
import { FaFileAlt, FaScroll, FaBookOpen, FaTags, FaUserCircle, FaRegFile, FaUser, FaRegUser, FaUserFriends } from 'react-icons/fa'
import { FaFileAlt, FaScroll, FaBookOpen, FaTags, FaUserCircle, FaRegFile, FaUser, FaRegUser, FaUserFriends, FaElementor } from 'react-icons/fa'
import { Route } from 'src/types/route'
import { SearchFilter } from 'src/types/search'
import { ReserveUrls } from './reserve-urls'
@ -76,6 +76,12 @@ export const adminLinks: Route[] = [{
icon: getSvgIcon("user"),
disabled: false
},
{
title: '首页侧栏',
path: `${ReserveUrls.Admin}/sidebars`,
icon: <FaElementor />,
disabled: false
},
]

@ -0,0 +1,7 @@
export interface HomeSidebar {
id?: number,
tagName: string
sort: string
displayCount: number
weight: number
}
Loading…
Cancel
Save