diff --git a/configs/config.ts b/configs/config.ts index 213a90cf..e6ef4e5b 100644 --- a/configs/config.ts +++ b/configs/config.ts @@ -11,7 +11,8 @@ export let config = { }, user: { nicknameMaxLen: 64, - usernameMaxLen: 39 + usernameMaxLen: 39, + navbarMaxLen: 20 } } diff --git a/pages/[username]/index.tsx b/pages/[username]/index.tsx index 39c593c3..5725124d 100644 --- a/pages/[username]/index.tsx +++ b/pages/[username]/index.tsx @@ -9,7 +9,7 @@ import { useRouter } from "next/router" import React, { useEffect, useState } from "react" import { FaFacebook, FaFile, FaGithub, FaHeart, FaPlus, FaRegStar, FaStackOverflow, FaStar, FaTwitter, FaWeibo, FaZhihu } from "react-icons/fa" import { ReserveUrls } from "src/data/reserve-urls" -import { User } from "src/types/user" +import { Navbar, NavbarType, User } from "src/types/user" import { requestApi } from "utils/axios/request" import moment from 'moment' import { Story } from "src/types/story" @@ -35,6 +35,7 @@ const UserPage = () => { const [tags, setTags]: [Tag[], any] = useState([]) const [tagFilter, setTagFilter]: [Tag, any] = useState(null) const [followers, setFollowers]: [User[], any] = useState([]) + const [navbars,setNavbars]:[Navbar[],any] = useState([]) const borderColor = useColorModeValue('white', 'transparent') const stackBorderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark) useEffect(() => { @@ -48,12 +49,17 @@ const UserPage = () => { setUser(res.data) getTags(res.data.id) - + getNavbars(res.data.id) const res1 = await requestApi.get(`/user/posts/${res.data.id}`) setPosts(res1.data) setRawPosts(res1.data) } + const getNavbars = async userID => { + const res = await requestApi.get(`/user/navbars/${userID}`) + setNavbars(res.data) + } + const getTags = async (userID) => { const res = await requestApi.get(`/tag/user/${userID}`) setTags(res.data) @@ -128,12 +134,16 @@ const UserPage = () => { HOME - - - - REACT - - + + { + navbars.map(nv => + + + {nv.label} + + ) + } + diff --git a/pages/settings/navbar.tsx b/pages/settings/navbar.tsx index 7ab1d9fc..b0028e2f 100644 --- a/pages/settings/navbar.tsx +++ b/pages/settings/navbar.tsx @@ -1,110 +1,188 @@ -import { Text, Box, Heading, Image, Center, Button, Flex, VStack, Divider, useToast, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, HStack, Wrap, useMediaQuery, Avatar, Textarea, } from "@chakra-ui/react" +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 Nav from "layouts/nav/nav" 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 { settingLinks } from "src/data/links" import { requestApi } from "utils/axios/request" -import { useRouter } from "next/router" -import { Field, Form, Formik } from "formik" import { config } from "configs/config" -import Tags from "components/tags/tags" -var validator = require('validator'); +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" const UserNavbarPage = () => { - const [user, setUser] = useState(null) - const [skills, setSkills] = useState([]) - const [isLargerThan1280] = useMediaQuery("(min-width: 768px)") + const [navbars, setNavbars]:[Navbar[],any] = useState([]) + const [series, setSeries]: [Story[], any] = useState([]) + const [currentNavbar, setCurrentNavbar]: [Navbar, any] = useState(null) + const { isOpen, onOpen, onClose } = useDisclosure() + const toast = useToast() useEffect(() => { - requestApi.get("/user/self").then(res => setUser(res.data)) + getNavbars() + getSeries() }, []) - const router = useRouter() - const toast = useToast() - const submitUser = async (values, _) => { - await requestApi.post(`/user/update`, values) - setUser(values) - toast({ - description: "更新成功", - status: "success", - duration: 2000, - isClosable: true, - }) + const getNavbars = async () => { + const res = await requestApi.get("/user/navbars/0") + setNavbars(res.data) } - function validateNickname(value) { - let error - if (!value?.trim()) { - error = "昵称不能为空" + const getSeries = async () => { + const res = await requestApi.get(`/story/posts/editor?type=${IDType.Series}`) + setSeries(res.data) + } + + const submitNavbar = async () => { + if (!currentNavbar.label || !currentNavbar.value) { + toast({ + description: "值不能为空", + status: "error", + duration: 2000, + isClosable: true, + }) + return } - if (value?.length > config.user.nicknameMaxLen) { - error = `长度不能超过${config.user.nicknameMaxLen}` + if (currentNavbar.label.length > config.user.navbarMaxLen) { + toast({ + description: `Label长度不能超过${config.user.navbarMaxLen}`, + status: "error", + duration: 2000, + isClosable: true, + }) + return } - return error - } - function validateEmail(value) { - let email = value?.trim() - let error + await requestApi.post(`/user/navbar`, currentNavbar) + setCurrentNavbar(null) + onClose() + getNavbars() + } - if (email?.length > config.user.usernameMaxLen) { - error = `长度不能超过${config.user.usernameMaxLen}` - return error - } + const onAddNavbar = () => { + setCurrentNavbar({ weight: 0, type: NavbarType.Link, label: "", value: "https://" }) + onOpen() + } - if (email) { - if (!validator.isEmail(email)) { - error = "Email格式不合法" - return error - } - } - return error + const onEditNavbar = nav => { + setCurrentNavbar(nav) + onOpen() } + const onNavbarChange = () => { + const nv = cloneDeep(currentNavbar) + setCurrentNavbar(nv) + } - function validateUrl(value, canBeEmpty = true) { - let url = value?.trim() - let error - if (!canBeEmpty) { - if (!url) { - error = "url不能为空" - return error - } + const onNvTypeChange = v => { + const tp = parseInt(v); + currentNavbar.type = tp + if (tp === NavbarType.Link) { + currentNavbar.value = "" + } else { + currentNavbar.value = series[0].id } - - if (url) { - if (!validator.isURL(value)) { - error = "URL格式不合法" - return error + onNavbarChange() + } + const getSeriesTitle = id => { + for (const s of series) { + if (s.id === id) { + return s.title } } - return error + return "" } - function validateLen(value) { - let error - if (value?.length > config.commonMaxlen) { - error = `长度不能超过${config.commonMaxlen}` - } - - return error + const onDeleteNavbar = async id => { + requestApi.delete(`/user/navbar/${id}`) + getNavbars() } - const Layout = isLargerThan1280 ? HStack : VStack return ( <> - {user && - - } + + + 菜单设置 + + + + + + + + + + + + + + { + navbars.map((nv,i) => + + + + + + ) + } + + +
LabelTypeValueWeight
{nv.label}{nv.type === NavbarType.Link ? "link" : "series"}{nv.type === NavbarType.Link ? nv.value : getSeriesTitle(nv.value)}{nv.weight} + onEditNavbar(nv)}/> + onDeleteNavbar(nv.id)} /> +
+
+ + + + {currentNavbar && + {currentNavbar.label ? "编辑菜单项" : "新建菜单项"} + + + + Label + { currentNavbar.label = e.currentTarget.value; onNavbarChange() }}> + + + + Type + + + + + Value + {currentNavbar.type === NavbarType.Link ? { currentNavbar.value = e.currentTarget.value; onNavbarChange() }} /> : + } + + + + Weight + { currentNavbar.weight = parseInt(e); onNavbarChange() }}> + + + + + + + + + + + } + ) } diff --git a/server/internal/api/user.go b/server/internal/api/user.go index d431c0e8..b77e7ae2 100644 --- a/server/internal/api/user.go +++ b/server/internal/api/user.go @@ -98,7 +98,7 @@ func SubmitNavbar(c *gin.Context) { } nav.UserID = u.ID - err1 := user.AddNavbar(nav) + err1 := user.SubmitNavbar(nav) if err != nil { c.JSON(err1.Status, common.RespError(err1.Message)) return @@ -106,3 +106,33 @@ func SubmitNavbar(c *gin.Context) { c.JSON(http.StatusOK, common.RespSuccess(nil)) } + +func GetNavbars(c *gin.Context) { + userID := c.Param("userID") + + if userID == "0" { + u := user.CurrentUser(c) + userID = u.ID + } + + navbars, err := user.GetNavbars(userID) + if err != nil { + c.JSON(err.Status, common.RespError(err.Message)) + return + } + + c.JSON(http.StatusOK, common.RespSuccess(navbars)) +} + +func DeleteNavbar(c *gin.Context) { + id := c.Param("id") + + u := user.CurrentUser(c) + err := user.DeleteNavbar(u.ID, 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/config.go b/server/internal/config.go index 85f324cc..1fd6ba57 100644 --- a/server/internal/config.go +++ b/server/internal/config.go @@ -25,6 +25,7 @@ type PostsConfig struct { type UserConfig struct { NicknameMaxLen int `json:"nicknameMaxLen"` UsernameMaxLen int `json:"usernameMaxLen"` + NavabarMaxLen int `json:"navbarMaxLen"` } // 在后台页面配置,存储到mysql中 @@ -41,6 +42,7 @@ func GetConfig(c *gin.Context) { User: &UserConfig{ UsernameMaxLen: 39, NicknameMaxLen: 64, + NavabarMaxLen: 20, }, } diff --git a/server/internal/server.go b/server/internal/server.go index 592b0ce7..2a64020b 100644 --- a/server/internal/server.go +++ b/server/internal/server.go @@ -85,6 +85,8 @@ func (s *Server) Start() error { r.POST("/user/login", user.Login) r.POST("/user/logout", user.Logout) r.POST("/user/navbar", IsLogin(), api.SubmitNavbar) + r.GET("/user/navbars/:userID", api.GetNavbars) + r.DELETE("/user/navbar/:id", IsLogin(), api.DeleteNavbar) // interaction apis r.POST("/interaction/like/:id", IsLogin(), api.Like) r.POST("/interaction/follow/:id", IsLogin(), api.Follow) diff --git a/server/internal/storage/sql_tables.go b/server/internal/storage/sql_tables.go index d8d00e3d..0cd997f3 100644 --- a/server/internal/storage/sql_tables.go +++ b/server/internal/storage/sql_tables.go @@ -188,6 +188,7 @@ var sqlTables = map[string]string{ `, "user_navbar": `CREATE TABLE IF NOT EXISTS user_navbar ( + id INTEGER PRIMARY KEY AUTOINCREMENT, user_id VARCHAR(255), label VARCHAR(20), type TINYINT, diff --git a/server/internal/user/navbar.go b/server/internal/user/navbar.go index 42a1e096..4436e666 100644 --- a/server/internal/user/navbar.go +++ b/server/internal/user/navbar.go @@ -9,11 +9,17 @@ import ( "github.com/imdotdev/im.dev/server/pkg/models" ) -func AddNavbar(nav *models.Navbar) *e.Error { - _, err := db.Conn.Exec("INSERT INTO user_navbar (user_id,label,type,value,weight) VALUES (?,?,?,?,?)", - nav.UserID, nav.Label, nav.Type, nav.Value, nav.Weight) +func SubmitNavbar(nav *models.Navbar) *e.Error { + var err error + if nav.ID == 0 { + _, err = db.Conn.Exec("INSERT INTO user_navbar (user_id,label,type,value,weight) VALUES (?,?,?,?,?)", + nav.UserID, nav.Label, nav.Type, nav.Value, nav.Weight) + } else { + _, err = db.Conn.Exec("UPDATE user_navbar SET label=?,type=?,value=?,weight=? WHERE id=? and user_id=?", nav.Label, nav.Type, nav.Value, nav.Weight, nav.ID, nav.UserID) + } + if err != nil { - logger.Warn("add user navbar error", "error", err) + logger.Warn("submit user navbar error", "error", err) return e.New(http.StatusInternalServerError, e.Internal) } @@ -21,7 +27,7 @@ func AddNavbar(nav *models.Navbar) *e.Error { } func GetNavbars(userID string) (models.Navbars, *e.Error) { - rows, err := db.Conn.Query("SELECT label,type,value,weight FROM navbar WHERE user_id=?", userID) + rows, err := db.Conn.Query("SELECT id,label,type,value,weight FROM user_navbar WHERE user_id=?", userID) if err != nil { logger.Warn("get user navbar error", "error", err) return nil, e.New(http.StatusInternalServerError, e.Internal) @@ -30,7 +36,7 @@ func GetNavbars(userID string) (models.Navbars, *e.Error) { navs := make(models.Navbars, 0) for rows.Next() { nav := &models.Navbar{} - rows.Scan(&nav.Label, &nav.Type, &nav.Value, &nav.Weight) + rows.Scan(&nav.ID, &nav.Label, &nav.Type, &nav.Value, &nav.Weight) navs = append(navs, nav) } @@ -38,3 +44,13 @@ func GetNavbars(userID string) (models.Navbars, *e.Error) { return navs, nil } + +func DeleteNavbar(userID string, id string) *e.Error { + _, err := db.Conn.Exec("DELETE FROM user_navbar WHERE id=? and user_id=?", id, userID) + if err != nil { + logger.Warn("submit user navbar error", "error", err) + return e.New(http.StatusInternalServerError, e.Internal) + } + + return nil +} diff --git a/server/pkg/models/navbar.go b/server/pkg/models/navbar.go index fda07795..42ccdc08 100644 --- a/server/pkg/models/navbar.go +++ b/server/pkg/models/navbar.go @@ -6,6 +6,7 @@ const ( ) type Navbar struct { + ID int `json:"id"` UserID string `json:"userID"` Label string `json:"label"` Type int `json:"type"` diff --git a/src/components/svg-icon.tsx b/src/components/svg-icon.tsx index afe0a4ab..a3627c3d 100644 --- a/src/components/svg-icon.tsx +++ b/src/components/svg-icon.tsx @@ -52,6 +52,9 @@ export function getSvgIcon(name, height = "1.4rem") { case "navbar": svg = break + case "close": + svg = + break default: break; } diff --git a/src/types/user.ts b/src/types/user.ts index a8fba0ea..f6519c6d 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -45,4 +45,17 @@ export interface UserSimple { username: string nickname: string avatar: string +} + +export enum NavbarType { + Link = 1, + Series = 2 +} + +export interface Navbar { + id?: number, + label: string + type: number + value: string + weight: number } \ No newline at end of file