完成热门管理
This commit is contained in:
parent
abfbfbb95e
commit
0aab21db19
|
@ -0,0 +1,5 @@
|
|||
import { atom } from 'jotai';
|
||||
|
||||
// 定义一个简单的原子
|
||||
export const linkTypeAtom = atom('');
|
||||
|
|
@ -4,7 +4,7 @@ import { Link as _Link } from "../api/link/route";
|
|||
import { LinkType } from "../api/linkType/route";
|
||||
|
||||
export default function LinkListBox({ linkTypeList, linkList }: { linkTypeList: LinkType[]; linkList: _Link[] }) {
|
||||
return <div className="flex w-full flex-col gap-y-2">
|
||||
return <div className="flex w-full flex-col gap-y-4">
|
||||
{
|
||||
linkTypeList.map(item => (
|
||||
<div className="flex flex-col gap-y-2" key={item._id}>
|
||||
|
@ -17,11 +17,11 @@ export default function LinkListBox({ linkTypeList, linkList }: { linkTypeList:
|
|||
{
|
||||
linkList.filter(val => val.type === item._id).map(val => (
|
||||
<Link key={val._id} className="flex gap-x-2 bg-white rounded-lg py-4 pl-2 cursor-pointer duration-150 hover:-translate-y-1 shadow-sm"
|
||||
href={val.link || ''}>
|
||||
href={val.link || ''} target="_blank">
|
||||
<img src={val.logoLink} className="w-[40px] h-[40px]"></img>
|
||||
<div className="flex-1 w-0 flex flex-col justify-between">
|
||||
<span className=" font-bold text-ellipsis overflow-hidden whitespace-nowrap">{val.name}</span>
|
||||
<span className=" text-ellipsis overflow-hidden whitespace-nowrap text-[#666] text-xs">{val.description}</span>
|
||||
<span className=" text-ellipsis overflow-hidden whitespace-nowrap text-[#666] text-xs" title={val.description}>{val.description}</span>
|
||||
</div>
|
||||
</Link>
|
||||
))
|
||||
|
|
|
@ -82,7 +82,7 @@ export default function Search() {
|
|||
}
|
||||
}, [activeSearchKey])
|
||||
return (
|
||||
<div className="w-full flex justify-center flex-col items-center py-10">
|
||||
<div className="w-full flex justify-center flex-col items-center py-10 ">
|
||||
|
||||
<div className="w-[200px]">
|
||||
<Logo></Logo>
|
||||
|
|
|
@ -5,17 +5,19 @@ import Logo from "./Logo";
|
|||
import clsx from "clsx";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { LinkType } from "../api/linkType/route";
|
||||
import { useAtom } from "jotai";
|
||||
import { linkTypeAtom } from "../_lib/atom";
|
||||
|
||||
export default function SiderNav({ linkList }: { linkList: LinkType[] }) {
|
||||
const pathname = usePathname()
|
||||
console.log(pathname);
|
||||
|
||||
const [selectType, setSelectType] = useAtom(linkTypeAtom)
|
||||
return (
|
||||
<div className="w-[220px] flex flex-col gap-y-2 fixed left-0 top-0 h-[100vh] bg-[#F9F9F9]">
|
||||
<div>
|
||||
<Logo />
|
||||
</div>
|
||||
<nav className="flex flex-col px-1">
|
||||
<nav className="flex flex-col py-1">
|
||||
{
|
||||
linkList.map((item) => {
|
||||
return (
|
||||
|
@ -28,7 +30,11 @@ export default function SiderNav({ linkList }: { linkList: LinkType[] }) {
|
|||
}
|
||||
<span>{item.label}</span>
|
||||
</Link> :
|
||||
<div className="cursor-pointer py-3 flex gap-x-2 items-center hover:bg-[#E0E0E0] rounded pl-3 text-[#515C6B] hover:text-[#5961F9] text-[14px]" key={item._id}>
|
||||
<div className="cursor-pointer py-3 flex gap-x-2 items-center hover:bg-[#E0E0E0] rounded pl-3 text-[#515C6B] hover:text-[#5961F9] text-[14px]" key={item._id}
|
||||
onClick={() => {
|
||||
setSelectType(item._id)
|
||||
}}
|
||||
>
|
||||
<img src={item.icon as string} className="w-[20px] h-[20px] object-cover"></img>
|
||||
|
||||
<span>{item.label}</span>
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
import { Button, Form, Input, message } from "antd";
|
||||
import { useEffect } from "react";
|
||||
import { mRequest } from "@/app/_lib/request";
|
||||
|
||||
export default function EditIconContent({
|
||||
item,
|
||||
onRefresh,
|
||||
}: {
|
||||
item: | null;
|
||||
onRefresh: () => void;
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
console.log(item);
|
||||
|
||||
useEffect(() => {
|
||||
if (item) {
|
||||
form.setFieldsValue({ ...item });
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [item, form]);
|
||||
return (
|
||||
<Form
|
||||
layout="vertical"
|
||||
form={form}
|
||||
initialValues={{
|
||||
// ...item,
|
||||
// name: "",
|
||||
// icons: [],
|
||||
// link: "",
|
||||
}}
|
||||
onFinish={(res) => {
|
||||
console.log(res);
|
||||
mRequest(item ? "PUT" : "POST", "/app/linkIcon", {
|
||||
data: item ? { id: item.id, ...res } : res,
|
||||
returnType: "text",
|
||||
}).then(() => {
|
||||
message.success((item ? "修改" : "新增") + "成功");
|
||||
onRefresh();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
label="名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "名称必填" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
{
|
||||
!item ?
|
||||
<>
|
||||
<Form.Item
|
||||
label="链接名称"
|
||||
name="linkName"
|
||||
rules={[{ required: true, message: "链接必填" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="链接标签"
|
||||
name="linkTag"
|
||||
rules={[{ required: true, message: "链接必填" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</> : ''
|
||||
|
||||
}
|
||||
|
||||
|
||||
<Form.Item className="flex justify-end">
|
||||
<Button type="primary" htmlType="submit">
|
||||
确认{item ? "修改" : "添加"}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
import { mRequest } from "@/app/_lib/request"
|
||||
import ImageUpload from "@/app/_ui/ImageUpload"
|
||||
import { Link } from "@/app/api/link/route"
|
||||
import { LinkType } from "@/app/api/linkType/route"
|
||||
import { useRequest } from "ahooks"
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
|
@ -11,15 +13,22 @@ import {
|
|||
message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Radio,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
} from "antd"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import useSWR from "swr"
|
||||
|
||||
|
||||
export default function LinkTable(props: { id: string }) {
|
||||
const [list, setList] = useState<Link[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { data: LinkTypeList } = useRequest(async () => mRequest<{
|
||||
list: LinkType[]
|
||||
}>('GET', '/api/linkType'))
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true)
|
||||
const res = await mRequest<{ list: Link[] }>(
|
||||
|
@ -68,7 +77,13 @@ export default function LinkTable(props: { id: string }) {
|
|||
<Image width={80} src={row.logoLink}></Image>
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
title: "是否热门",
|
||||
dataIndex: "isHot",
|
||||
render: (_, row) => (
|
||||
row.isHot ? '是' : '否'
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
fixed: "right",
|
||||
|
@ -81,9 +96,7 @@ export default function LinkTable(props: { id: string }) {
|
|||
<Popconfirm
|
||||
title="确认删除?"
|
||||
onConfirm={async () => {
|
||||
await mRequest("DELETE", "/app/link/" + row.id, {
|
||||
returnType: "text",
|
||||
})
|
||||
await mRequest("DELETE", "/api/link/" + row._id,)
|
||||
refresh()
|
||||
message.success("删除成功")
|
||||
}}
|
||||
|
@ -110,12 +123,14 @@ export default function LinkTable(props: { id: string }) {
|
|||
initialValues={
|
||||
selected
|
||||
? selected
|
||||
: { title: "", url: "", logoLink: "", description: "", priority: 0 }
|
||||
: { title: "", url: "", logoLink: "", description: "", priority: 0, type: props.id }
|
||||
}
|
||||
onFinish={async (res) => {
|
||||
if (selected) {
|
||||
await mRequest("PUT", "/api/link", {
|
||||
id: selected._id, ...res
|
||||
_id: selected._id,
|
||||
...res,
|
||||
type: props.id
|
||||
})
|
||||
} else {
|
||||
await mRequest("POST", "/api/link", {
|
||||
|
@ -134,6 +149,9 @@ export default function LinkTable(props: { id: string }) {
|
|||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="link" label="链接" rules={[{ required: true, message: "链接必填" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="logoLink"
|
||||
label="图标名称"
|
||||
|
@ -145,15 +163,38 @@ export default function LinkTable(props: { id: string }) {
|
|||
height={60}
|
||||
></ImageUpload>
|
||||
</Form.Item>
|
||||
<Form.Item name="link" label="链接" rules={[{ required: true, message: "链接必填" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="详情" >
|
||||
|
||||
<Form.Item name="description" label="详情"
|
||||
rules={[{ required: true, message: "详情必填" }]}
|
||||
>
|
||||
<Input.TextArea />
|
||||
</Form.Item>
|
||||
<Form.Item name="type" label="分类"
|
||||
rules={[{ required: true, message: "分类必填" }]}
|
||||
|
||||
>
|
||||
<Select options={LinkTypeList?.list.map(item => ({
|
||||
label: item.label,
|
||||
value: item._id
|
||||
}))}></Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="priority" label="优先级">
|
||||
<InputNumber></InputNumber>
|
||||
</Form.Item>
|
||||
<Form.Item name="isHot" label="是否热门">
|
||||
<Radio.Group
|
||||
options={[
|
||||
{
|
||||
label: '是',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: '否',
|
||||
value: 0
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item className="flex justify-end">
|
||||
<Button type="primary" htmlType="submit">
|
||||
确认
|
||||
|
@ -161,6 +202,6 @@ export default function LinkTable(props: { id: string }) {
|
|||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import ImageUpload from "@/app/_ui/ImageUpload";
|
|||
import { useForm } from "antd/es/form/Form";
|
||||
|
||||
export default function Page() {
|
||||
const [form] = useForm()
|
||||
const { tableProps, refresh } = useAntdTable(
|
||||
async ({ current, pageSize }) => {
|
||||
return mRequest<{
|
||||
|
@ -120,6 +121,7 @@ export default function Page() {
|
|||
}}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
labelCol={{ span: 4 }}
|
||||
initialValues={selectedType ?
|
||||
selectedType
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import { getCollection, getDb } from "@/app/_lib/mongodb";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { NextRequest } from "next/server";
|
||||
import { Link } from "../route";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const collection = await getCollection('link');
|
||||
// Check if the user is authenticated
|
||||
|
||||
const page = parseInt(req.nextUrl.searchParams.get('page') || '1') || 1;
|
||||
const pageSize = parseInt(req.nextUrl.searchParams.get('pageSize') || '10') || 10;
|
||||
const typeId = req.nextUrl.searchParams.get('typeId')
|
||||
// 计算起始索引和结束索引
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
|
||||
// 查询数据
|
||||
const cursor = collection.find<Link>({ type: typeId }).skip(startIndex).limit(pageSize);
|
||||
const data = await cursor.toArray();
|
||||
|
||||
// 计算总数量
|
||||
const total = (await collection.find<Link>({ type: typeId }).toArray()).length
|
||||
|
||||
return Response.json({
|
||||
total,
|
||||
list: data,
|
||||
})
|
||||
} catch (e) {
|
||||
return Response.error()
|
||||
}
|
||||
}
|
||||
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
// 获取路径参数
|
||||
const slug = (await params).id
|
||||
const collection = await getCollection('link')
|
||||
collection.deleteOne({
|
||||
_id: new ObjectId(slug)
|
||||
})
|
||||
return Response.json({ message: '删除成功' })
|
||||
} catch (e) {
|
||||
return Response.error()
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ export type Link = {
|
|||
type: string;
|
||||
priority: number;
|
||||
logoLink: string;
|
||||
isHot?: boolean;
|
||||
|
||||
}
|
||||
export async function GET(req: NextRequest) {
|
||||
|
@ -52,26 +53,13 @@ export async function POST(req: NextRequest) {
|
|||
return Response.error()
|
||||
}
|
||||
}
|
||||
export async function DELETE(req: NextRequest) {
|
||||
try {
|
||||
// 获取路径参数
|
||||
const segments = req.nextUrl.pathname.split('/')
|
||||
const dynamicParam = segments[segments.length - 1]
|
||||
const collection = await getCollection('link')
|
||||
collection.deleteOne({
|
||||
_id: new ObjectId(dynamicParam)
|
||||
})
|
||||
return Response.json({ message: '删除成功' })
|
||||
} catch (e) {
|
||||
return Response.error()
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(req: NextRequest) {
|
||||
try {
|
||||
// 获取待更新的对象
|
||||
const link = await req.json() as Link
|
||||
const collection = await getCollection('link')
|
||||
await collection.updateOne({ _id: new ObjectId(link.id) }, { $set: link })
|
||||
await collection.replaceOne({ _id: new ObjectId(link._id) }, { ...link, _id: new ObjectId(link._id) })
|
||||
return Response.json({ message: '成功' })
|
||||
} catch (e) {
|
||||
return Response.error()
|
||||
|
|
|
@ -9,6 +9,7 @@ export type LinkType = {
|
|||
iconElement?: ReactNode;
|
||||
_id: string;
|
||||
href?: string;
|
||||
priority: number;
|
||||
location?: string;
|
||||
|
||||
}
|
||||
|
|
|
@ -20,11 +20,13 @@
|
|||
"clsx": "^2.1.1",
|
||||
"icons": "link:@awesome.me/kit-KIT_CODE/icons",
|
||||
"jose": "^5.9.6",
|
||||
"jotai": "^2.11.1",
|
||||
"mongodb": "^6.12.0",
|
||||
"next": "15.1.4",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"swr": "^2.3.0",
|
||||
"uuid": "^11.0.5",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
|
|
|
@ -41,6 +41,9 @@ importers:
|
|||
jose:
|
||||
specifier: ^5.9.6
|
||||
version: 5.9.6
|
||||
jotai:
|
||||
specifier: ^2.11.1
|
||||
version: 2.11.1(@types/react@19.0.7)(react@19.0.0)
|
||||
mongodb:
|
||||
specifier: ^6.12.0
|
||||
version: 6.12.0(socks@2.8.3)
|
||||
|
@ -56,6 +59,9 @@ importers:
|
|||
react-dom:
|
||||
specifier: ^19.0.0
|
||||
version: 19.0.0(react@19.0.0)
|
||||
swr:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0(react@19.0.0)
|
||||
uuid:
|
||||
specifier: ^11.0.5
|
||||
version: 11.0.5
|
||||
|
@ -950,6 +956,10 @@ packages:
|
|||
delegates@1.0.0:
|
||||
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
||||
|
||||
dequal@2.0.3:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
destroy@1.2.0:
|
||||
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
|
@ -1524,6 +1534,18 @@ packages:
|
|||
jose@5.9.6:
|
||||
resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==}
|
||||
|
||||
jotai@2.11.1:
|
||||
resolution: {integrity: sha512-41Su098mpHIX29hF/XOpDb0SqF6EES7+HXfrhuBqVSzRkxX48hD5i8nGsEewWZNAsBWJCTTmuz8M946Ih2PfcQ==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
peerDependencies:
|
||||
'@types/react': '>=17.0.0'
|
||||
react: '>=17.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
|
||||
js-base64@2.6.4:
|
||||
resolution: {integrity: sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==}
|
||||
|
||||
|
@ -2500,6 +2522,11 @@ packages:
|
|||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
swr@2.3.0:
|
||||
resolution: {integrity: sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==}
|
||||
peerDependencies:
|
||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
tailwindcss@3.4.17:
|
||||
resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
@ -2607,6 +2634,11 @@ packages:
|
|||
proxy-agent:
|
||||
optional: true
|
||||
|
||||
use-sync-external-store@1.4.0:
|
||||
resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
|
@ -3632,6 +3664,8 @@ snapshots:
|
|||
|
||||
delegates@1.0.0: {}
|
||||
|
||||
dequal@2.0.3: {}
|
||||
|
||||
destroy@1.2.0: {}
|
||||
|
||||
detect-libc@2.0.3: {}
|
||||
|
@ -4393,6 +4427,11 @@ snapshots:
|
|||
|
||||
jose@5.9.6: {}
|
||||
|
||||
jotai@2.11.1(@types/react@19.0.7)(react@19.0.0):
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.7
|
||||
react: 19.0.0
|
||||
|
||||
js-base64@2.6.4: {}
|
||||
|
||||
js-cookie@3.0.5: {}
|
||||
|
@ -5490,6 +5529,12 @@ snapshots:
|
|||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
swr@2.3.0(react@19.0.0):
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
react: 19.0.0
|
||||
use-sync-external-store: 1.4.0(react@19.0.0)
|
||||
|
||||
tailwindcss@3.4.17:
|
||||
dependencies:
|
||||
'@alloc/quick-lru': 5.2.0
|
||||
|
@ -5642,6 +5687,10 @@ snapshots:
|
|||
optionalDependencies:
|
||||
proxy-agent: 6.5.0
|
||||
|
||||
use-sync-external-store@1.4.0(react@19.0.0):
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
utility@1.18.0:
|
||||
|
|
Loading…
Reference in New Issue