feat(web): forum home page with boards, list, post
This commit is contained in:
parent
899c4eb42e
commit
fd84b0b0ff
|
|
@ -0,0 +1,126 @@
|
|||
import { defineConfig } from '@umijs/max'
|
||||
|
||||
const COLOR_PRIMARY = '#B43533'
|
||||
const COLOR_TEXT_PRIMARY = 'rgba(0, 0, 0, 0.88)'
|
||||
const COLOR_TEXT_DISABLED = 'rgba(0, 0, 0, 0.25)'
|
||||
|
||||
export default defineConfig({
|
||||
initialState: {},
|
||||
access: {},
|
||||
model: {},
|
||||
antd: {
|
||||
theme: {
|
||||
token: {
|
||||
colorPrimary: COLOR_PRIMARY,
|
||||
colorLink: COLOR_PRIMARY,
|
||||
colorTextDisabled: COLOR_TEXT_PRIMARY
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
defaultBorderColor: COLOR_PRIMARY,
|
||||
defaultColor: COLOR_PRIMARY,
|
||||
colorTextDisabled: COLOR_TEXT_DISABLED
|
||||
},
|
||||
Cascader: {
|
||||
dropdownHeight: 460
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
title: '装修室内空气质量预测系统',
|
||||
layout: {
|
||||
locale: false
|
||||
},
|
||||
tailwindcss: {},
|
||||
routes: [
|
||||
{
|
||||
name: '登录',
|
||||
path: '/login',
|
||||
component: './login',
|
||||
layout: false
|
||||
},
|
||||
{
|
||||
name: '落地页',
|
||||
path: '/landing',
|
||||
component: './landing',
|
||||
layout: false
|
||||
},
|
||||
{
|
||||
name: '健康装修大家谈',
|
||||
path: '/forum',
|
||||
component: './forum',
|
||||
layout: false
|
||||
},
|
||||
{
|
||||
name: '帖子详情',
|
||||
path: '/forum/thread/:id',
|
||||
component: './forum/thread',
|
||||
layout: false
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/landing'
|
||||
},
|
||||
{
|
||||
name: '首页',
|
||||
path: '/home',
|
||||
component: './home'
|
||||
},
|
||||
{
|
||||
name: '污染源识别',
|
||||
path: '/source',
|
||||
component: './source'
|
||||
},
|
||||
{
|
||||
name: '模板库',
|
||||
path: '/template',
|
||||
component: './template'
|
||||
},
|
||||
{
|
||||
name: '材料库',
|
||||
path: '/material',
|
||||
component: './material'
|
||||
},
|
||||
{
|
||||
name: '材料类别管理',
|
||||
path: '/material-category',
|
||||
component: './material-category',
|
||||
access: 'canMtrlCatMgmt'
|
||||
},
|
||||
{
|
||||
name: '历史记录',
|
||||
path: '/history-project',
|
||||
component: './history-project'
|
||||
},
|
||||
{
|
||||
name: '个人设置',
|
||||
path: '/personal-settings',
|
||||
component: './personal-settings',
|
||||
hideInMenu: true
|
||||
}
|
||||
],
|
||||
mock: false,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:6060',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/socket.io': {
|
||||
target: 'http://localhost:6060',
|
||||
ws: true
|
||||
}
|
||||
},
|
||||
history: {
|
||||
type: 'hash'
|
||||
},
|
||||
publicPath: '/iapip-web/',
|
||||
favicons: ['/iapip-web/favicon.ico'],
|
||||
request: {
|
||||
dataField: ''
|
||||
},
|
||||
mfsu: {
|
||||
strategy: 'normal'
|
||||
},
|
||||
esbuildMinifyIIFE: true,
|
||||
npmClient: 'pnpm'
|
||||
})
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { history } from '@umijs/max'
|
||||
import { Tabs, List, Button, Modal, Form, Input, Select, Pagination, message, Empty } from 'antd'
|
||||
import api from '@/services/api'
|
||||
import type { ForumThreadListItem } from '@/services/api/forum'
|
||||
import { LOC_STOR_KEY } from '@/common/constants'
|
||||
import PhoneAuthModal from '@/components/phone-auth-modal'
|
||||
|
||||
const hasToken = () => !!localStorage.getItem(LOC_STOR_KEY.AUTH_TOKEN)
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
export default function ForumHome() {
|
||||
const [boards, setBoards] = useState<{ board: string, count: number }[]>([])
|
||||
const [active, setActive] = useState<string>('all')
|
||||
const [list, setList] = useState<ForumThreadListItem[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const [authOpen, setAuthOpen] = useState(false)
|
||||
const [postOpen, setPostOpen] = useState(false)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const loadBoards = useCallback(async () => {
|
||||
try { setBoards(await api.forum.getBoards()) } catch { /* 全局提示 */ }
|
||||
}, [])
|
||||
|
||||
const loadThreads = useCallback(async (p: number, board: string) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await api.forum.getThreads({
|
||||
board: board === 'all' ? undefined : board,
|
||||
page: p,
|
||||
size: PAGE_SIZE
|
||||
})
|
||||
setList(res.list)
|
||||
setTotal(res.total)
|
||||
} catch { /* 全局提示 */ } finally { setLoading(false) }
|
||||
}, [])
|
||||
|
||||
useEffect(() => { loadBoards() }, [loadBoards])
|
||||
useEffect(() => { loadThreads(page, active) }, [page, active, loadThreads])
|
||||
|
||||
const onTab = (key: string) => { setActive(key); setPage(1) }
|
||||
|
||||
const openPost = () => {
|
||||
if (!hasToken()) { setAuthOpen(true); return }
|
||||
form.resetFields()
|
||||
setPostOpen(true)
|
||||
}
|
||||
|
||||
const submitPost = async () => {
|
||||
const v = await form.validateFields()
|
||||
try {
|
||||
await api.forum.createThread(v)
|
||||
message.success('发布成功')
|
||||
setPostOpen(false)
|
||||
setActive(v.board); setPage(1)
|
||||
loadThreads(1, v.board)
|
||||
loadBoards()
|
||||
} catch { /* 全局提示 */ }
|
||||
}
|
||||
|
||||
const boardOptions = boards.map(b => ({ label: `${b.board} (${b.count})`, value: b.board }))
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 900, margin: '0 auto', padding: '24px 16px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 16 }}>
|
||||
<a onClick={() => history.push('/landing')} style={{ color: '#888' }}>← 返回首页</a>
|
||||
<h1 style={{ flex: 1, textAlign: 'center', margin: 0, fontSize: 22 }}>健康装修大家谈</h1>
|
||||
<Button type="primary" onClick={openPost}>发帖</Button>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
activeKey={active}
|
||||
onChange={onTab}
|
||||
items={[{ key: 'all', label: '全部' }, ...boards.map(b => ({ key: b.board, label: `${b.board} (${b.count})` }))]}
|
||||
/>
|
||||
|
||||
<List
|
||||
loading={loading}
|
||||
locale={{ emptyText: <Empty description="还没有帖子,来发第一帖吧" /> }}
|
||||
dataSource={list}
|
||||
renderItem={t => (
|
||||
<List.Item
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => history.push(`/forum/thread/${t.id}`)}
|
||||
actions={[<span key="r">💬 {t.reply_count}</span>, <span key="l">👍 {t.like_count}</span>]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={<span>[{t.board}] {t.title}</span>}
|
||||
description={`${t.author_name} · ${new Date(t.created_at).toLocaleString()}`}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
|
||||
{total > PAGE_SIZE && (
|
||||
<div style={{ textAlign: 'center', marginTop: 16 }}>
|
||||
<Pagination current={page} pageSize={PAGE_SIZE} total={total} onChange={setPage} showSizeChanger={false} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Modal title="发布新帖" open={postOpen} onOk={submitPost} onCancel={() => setPostOpen(false)} okText="发布" destroyOnClose>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="board" label="板块" rules={[{ required: true, message: '请选择板块' }]}>
|
||||
<Select options={boardOptions} placeholder="选择板块" />
|
||||
</Form.Item>
|
||||
<Form.Item name="title" label="标题" rules={[{ required: true, message: '请输入标题' }, { max: 60 }]}>
|
||||
<Input placeholder="一句话说清问题" />
|
||||
</Form.Item>
|
||||
<Form.Item name="content" label="正文" rules={[{ required: true, message: '请输入正文' }, { max: 5000 }]}>
|
||||
<Input.TextArea rows={6} placeholder="详细描述你的问题或经验" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<PhoneAuthModal
|
||||
open={authOpen}
|
||||
onSuccess={() => { setAuthOpen(false); openPost() }}
|
||||
onCancel={() => setAuthOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue