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