104 |
105 | {souceSwitch()}
106 |
{headerText()}
107 |
108 | {slider()}
109 |
110 |
111 | props.notifyKeywordChange(e.target.value) }
115 | allowClear
116 | prefix={}
117 | style={{ }}
118 | />
119 |
123 |
136 |
137 |
(
144 | { handleItemClicked(item) } }>
145 |
146 |
147 | )}
148 | />
149 | {LEFT_FOLD}
150 |
151 | )
152 | }
153 |
154 | export default InfoHeader;
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useState, useMemo} from "react";
2 | import { BaiduMap, InfoHeader } from "./components";
3 | import { COLOR_MAP } from './common/constant'
4 | import './styles/App.css';
5 | import ReportModal from "./components/ReportModal";
6 |
7 | function App() {
8 | const [timeRange, setTimeRange] = useState(6)
9 | const [data, setData] = useState({})
10 | const [dataSource, setDataSource] = useState('weibo')
11 | const [bounds, setBounds] = useState(null)
12 | const [listDefaultText, setListDefaultText] = useState("")
13 | // map center
14 | const [center, setCenter] = useState({ lng: 113.802193, lat: 34.820333 })
15 |
16 | // filter relevant states
17 | const [ keyword, setKeyword ] = useState('')
18 | const [ selectedCategory, setSelectedCategory ] = useState('')
19 | const [ selectedTypes, setSelectedTypes ] = useState([])
20 |
21 | // highlight relevant states
22 | // changeList: (id -> icon) dict
23 | const [ changeList, setChangeList ] = useState({})
24 |
25 | // modal relevant states
26 | const [ modalState, setModalState ] = useState({ modalVisible: false, item: null })
27 |
28 | function createDataItem(item) {
29 | // including different ways to create the latLong and time fields to make it compatible
30 | // across different versions of json format; only for the transition phase
31 | item.isWeibo = !item.link.startsWith('no_link')
32 | // generate random to prevent overlap
33 | let random1 = Math.random()-0.5
34 | let random2 = Math.random()-0.5
35 | if (!item.isWeibo) {
36 | random1 = random1 / 200
37 | random2 = random2 / 200
38 | } else {
39 | random1 = random1 / 1000
40 | random2 = random2 / 1000
41 | }
42 | item.location = {
43 | lng: ((item.location && item.location.lng) || item.lng) + random1,
44 | lat: ((item.location && item.location.lat) || item.lat) + random2
45 | }
46 |
47 | item.time = item.Time || item.time
48 | if (item.isWeibo) {
49 | // format time
50 | item.timestamp = Date.parse(item.time)
51 | const date = new Date(item.timestamp)
52 | item.formatTime = `${date.getMonth() + 1}月${date.getDate()}日 ${item.time.substring(11, 20)}`
53 | } else {
54 | item.formatTime = ''
55 | }
56 |
57 | if (!item.isWeibo) {
58 | let text = '地址: ' + item.address + '\n'
59 | if (item.post) text += '内容: ' + item.post + '\n'
60 | if (item.contact_person) text += '联系人: ' + item.contact_person + '\n'
61 | if (item.contact_info) text += '联系方式: ' + item.contact_info
62 |
63 | item.post = text
64 | }
65 |
66 | // use last part of link as id
67 | let arr = item.link.split('/')
68 | item.id = arr[arr.length - 1]
69 |
70 | // fill null category
71 | // revised_category has a higher priority
72 | item.category = item.revised_category || item.category || '未分类'
73 |
74 | // item category and types
75 | const category = item.category
76 | arr = category.split('_').map(e => e.trim())
77 | // the first is category
78 | item.category = arr.shift()
79 | item.types = arr
80 | item.color = COLOR_MAP[item.category]
81 |
82 | // default icon
83 | item.icon = 'loc_red'
84 |
85 | // fill null address
86 | item.address = item.address || ''
87 |
88 | return item
89 | }
90 |
91 | // Fetch data on init
92 | useEffect(() => {
93 | let xhr_weibo = new XMLHttpRequest();
94 | xhr_weibo.onload = function () {
95 | if ('weibo' in data) return
96 | const serverData = JSON.parse(xhr_weibo.responseText)
97 | const items = serverData.map(createDataItem)
98 | setData(previousData => ({...previousData, weibo: items}))
99 | };
100 | xhr_weibo.open("GET", "https://api-henan.tianshili.me/parse_json.json");
101 | xhr_weibo.send()
102 |
103 | let xhr_sheet = new XMLHttpRequest();
104 | xhr_sheet.onload = function () {
105 | if ('sheet' in data) return
106 | const serverData = JSON.parse(xhr_sheet.responseText)
107 | const items = serverData.map(createDataItem)
108 | setData(previousData => ({...previousData, sheet: items}))
109 | };
110 | xhr_sheet.open("GET", "https://api-henan.tianshili.me/manual.json");
111 | xhr_sheet.send()
112 | })
113 |
114 | // [SECTION] Data generation
115 | let filterData = useMemo(() => {
116 | if (!(dataSource in data)) return []
117 | let currentFilteredData
118 | // convert selectedTypes into map, with (item -> true)
119 | const selectedTypesMap = selectedTypes.reduce((result, item) => {
120 | result[item] = true
121 | return result
122 | }, {})
123 |
124 | if (dataSource === 'weibo') {
125 | const beginTime = Date.now() - timeRange * 60 * 60 * 1000
126 | currentFilteredData = data[dataSource].filter(item => {
127 | const result = (item.timestamp > beginTime) &&
128 | item.post.indexOf(keyword) > -1 &&
129 | item.category.indexOf(selectedCategory) > -1
130 | // if already false
131 | if (result === false) { return false }
132 | // default select all
133 | if (selectedTypes.length === 0) return true
134 | // if previous condition is true, check selected types
135 | for (const type of item.types) {
136 | if (selectedTypesMap[type]) { return true }
137 | }
138 | return false
139 | })
140 | } else {
141 | // filter for manual sheet source
142 | currentFilteredData = data[dataSource].filter(item => {
143 | let contains_keyword = ((item.address.indexOf(keyword) > -1) ||
144 | (item.post && item.post.indexOf(keyword) > -1))
145 | const result = contains_keyword && item.category.indexOf(selectedCategory) > -1
146 | if (!result) { return false }
147 | if (selectedTypes.length === 0) return true
148 | for (const type of item.types) {
149 | if (selectedTypesMap[type]) { return true }
150 | }
151 | return false
152 | })
153 | }
154 | return currentFilteredData
155 | }, [data, dataSource, timeRange, keyword, selectedCategory, selectedTypes])
156 |
157 | // [SECTION] component call backs
158 | function handleDataSourceSwitch(value) {
159 | setDataSource(value)
160 | }
161 |
162 | function handleSliderChange(e) {
163 | setTimeRange(e)
164 | }
165 |
166 | function updateBounds(newBounds) {
167 | setBounds(newBounds)
168 | setListDefaultText("无数据")
169 | }
170 |
171 | function handleMapInited() {
172 | setListDefaultText("移动地图显示列表")
173 | }
174 |
175 | function handleInfoSelected(item) {
176 | let list = Object.assign({}, changeList)
177 |
178 | // revert last change
179 | for (const id in list) {
180 | if (id === item.id) continue
181 |
182 | const prevItem = data[dataSource].find(e => e.id === id)
183 | // if prevItem exists and it's highlighted, un-highlight it
184 | if (prevItem && list[id] !== prevItem.icon) {
185 | list[id] = prevItem.icon
186 | } else {
187 | // no previous item exists or it's already not highlighted, remove from list
188 | delete list[id]
189 | }
190 | }
191 |
192 | // highlight item
193 | list[item.id] = 'loc_blue'
194 | setChangeList(list)
195 | setCenter(item.location)
196 | }
197 |
198 | function handleCorrection(item) {
199 | setModalState({ visible: true, item: item })
200 | }
201 |
202 | function handleModalVisible(isVisible, item, newCategory) {
203 | // if successfully updated
204 | if (isVisible === false && item) {
205 | let items = [...data.weibo]
206 | let index = items.findIndex(e => e.id === item.id)
207 | if (index > -1) {
208 | let newItem = {...items[index]}
209 | newItem.category = newCategory
210 | items[index] = createDataItem(newItem)
211 | setData(previousData => ({...previousData, weibo: items}))
212 | }
213 | }
214 |
215 | setModalState({ visible: isVisible, item: item })
216 | }
217 |
218 | return (
219 |