= 0.9 ? 0.9 : o,
186 | zIndex: 100,
187 | }}
188 | />,
189 | document.body,
190 | )
191 | }, [opacity])
192 |
193 | const Night = useCallback(() => {
194 | const { isNightlight } = useAppSelector(({ win }) => win)
195 | return ReactDOM.createPortal(
196 |
,
210 | document.body,
211 | )
212 | }, [])
213 |
214 | return (
215 | <>
216 | {mask()}
217 | {Night()}
218 |
234 |
250 | >
251 | )
252 | })
253 |
--------------------------------------------------------------------------------
/src/store/win/reducers.ts:
--------------------------------------------------------------------------------
1 | import { getKeyName, isExploer } from '@/utils'
2 | import { getIcon } from '@/utils/appList'
3 | import { local, session } from '@/utils/storage'
4 | import { PayloadAction } from '@reduxjs/toolkit'
5 | import dayjs from 'dayjs'
6 | import { ReactNode } from 'react'
7 | import { TdesktopS, IWinState, TAppList, IActiveAppList, IChain, TIconSortWay } from './state'
8 |
9 | const hideApp = (state: IWinState, name: string) => {
10 | if (isExploer(name)) {
11 | state.appListTar[3].hide = false
12 | }
13 | if (name === 'Microsoft Edge') {
14 | state.appListTar[4].hide = false
15 | }
16 | if (name === '设置') {
17 | state.appListTar[2].hide = false
18 | }
19 | }
20 |
21 | const wallunlock = (state: IWinState) => {
22 | state.lock = !state.lock
23 | session.setItem('lock', state.lock)
24 | }
25 |
26 | const changeTheme = (state: IWinState) => {
27 | state.theme = state.theme === 'darkTheme' ? 'lightTheme' : 'darkTheme'
28 | local.setItem('theme', state.theme)
29 | }
30 |
31 | const changeDesktopSize = (state: IWinState, action: PayloadAction
) => {
32 | state.desktopSize = action.payload
33 | local.setItem('desktopSize', action.payload)
34 | }
35 |
36 | const changeAppList = (
37 | state: IWinState,
38 | action: PayloadAction<{ info?: TAppList; app: (index: number) => ReactNode; name: string }>,
39 | ) => {
40 | action.payload.info && state.appListTar.push(action.payload.info)
41 | state.activeApp = action.payload.name
42 | state.activeAppList.push({ name: state.activeApp, app: action.payload.app, isHide: false })
43 |
44 | state.historyApp = state.historyApp.filter(({ name }) => name !== action.payload.name)
45 | state.historyApp = [{ name: action.payload.name, icon: getIcon(action.payload.name) }, ...state.historyApp]
46 | if (state.historyApp.length === 11) state.historyApp.pop()
47 | local.setItem('historyApp', state.historyApp)
48 |
49 | hideApp(state, action.payload.name)
50 | }
51 |
52 | const changeAppActive = (state: IWinState, action: PayloadAction) => {
53 | hideApp(state, action.payload)
54 | state.activeApp = action.payload
55 | }
56 |
57 | const changeAppIsHide = (state: IWinState, action: PayloadAction) => {
58 | let isPrevew = false
59 | if (typeof action.payload !== 'string') {
60 | action.payload = action.payload.name
61 | isPrevew = true
62 | }
63 |
64 | const item = state.activeAppList.find(({ name }) => name === action.payload)
65 | if (!item) return
66 |
67 | item.isHide = isPrevew ? false : !item.isHide
68 |
69 | if (!item.isHide) {
70 | state.activeApp = action.payload
71 | return
72 | }
73 |
74 | if (!isExploer(action.payload)) {
75 | state.activeApp = ''
76 | return
77 | }
78 |
79 | const eList = state.activeAppList.reduce((target, item) => {
80 | if (isExploer(item.name)) target.push(item)
81 | return target
82 | }, [] as IActiveAppList[])
83 | if (eList.length === 1) {
84 | state.activeApp = ''
85 | } else {
86 | const nextExplorerApp = eList.find((item) => !item.isHide)
87 | state.activeApp = nextExplorerApp ? nextExplorerApp.name : ''
88 | }
89 | }
90 |
91 | const closeApp = (state: IWinState, action: PayloadAction) => {
92 | state.activeAppList = state.activeAppList.reduce((target, item) => {
93 | if (item.name !== action.payload) {
94 | target.push(item)
95 | } else {
96 | changeAppIsHide(state, { payload: action.payload, type: 'win/changeAppIsHide' })
97 | }
98 | return target
99 | }, [] as IActiveAppList[])
100 |
101 | if (isExploer(action.payload)) {
102 | const isExplorerHide = state.activeAppList.reduce((target, item) => {
103 | if (isExploer(item.name)) target.push(item)
104 | return target
105 | }, [] as IActiveAppList[]).length
106 | if (!isExplorerHide) state.appListTar[3].hide = true
107 | } else if (action.payload === 'Microsoft Edge') {
108 | state.appListTar[4].hide = true
109 | } else if (action.payload === '设置') {
110 | state.appListTar[2].hide = true
111 | } else {
112 | state.appListTar = state.appListTar.reduce((target, item) => {
113 | if (item.name !== action.payload) target.push(item)
114 | return target
115 | }, [] as typeof state.appListTar)
116 | }
117 | }
118 |
119 | const changeNight = (state: IWinState) => {
120 | state.isNightlight = !state.isNightlight
121 | local.setItem('isNightlight', state.isNightlight)
122 | }
123 |
124 | const changeOpacity = (state: IWinState, action: PayloadAction) => {
125 | state.opacity = action.payload
126 | local.setItem('opacity', state.opacity)
127 | }
128 |
129 | const changeIamge = (state: IWinState, action: PayloadAction) => {
130 | state.backgroundImage = require(`@/assets/img/wapppaper/${action.payload}.jpg`)
131 | local.setItem('backgroundImage', state.backgroundImage)
132 | }
133 |
134 | const changeWidget = (state: IWinState, action: PayloadAction<0 | 1 | 2>) => {
135 | state.widgetVisible = action.payload
136 | }
137 |
138 | const changeBootResatrOrLock = (state: IWinState, action: PayloadAction) => {
139 | if (action.payload) {
140 | state.boot = true
141 | state.appListTar = [
142 | { src: 'home', hide: true, isRemove: true },
143 | { src: 'search', hide: true, isRemove: true },
144 | { src: 'settings', hide: true, name: '设置', isRemove: true },
145 | { src: 'explorer', hide: true, name: 'explorer', isRemove: true },
146 | { src: 'edge', hide: true, name: 'Microsoft Edge', isRemove: true },
147 | ]
148 | state.activeApp = ''
149 | state.activeAppList = []
150 | }
151 | wallunlock(state)
152 | }
153 |
154 | const pushNotifier = (state: IWinState, action: PayloadAction) => {
155 | const icon = getIcon(action.payload)
156 | if (!icon) return // TODO: 这里未来可能加上浏览器搜索功能
157 | const data = {
158 | name: action.payload,
159 | icon: require(`@/assets/icon/actions/${icon}.png`),
160 | message: `应用 '${action.payload}' 正在开发中, 敬请期待!`,
161 | time: dayjs().format('YYYY/MM/DD H:mm'),
162 | key: new Date().getTime(),
163 | }
164 | const tailChain = {
165 | data,
166 | next: null,
167 | }
168 |
169 | let { listData, chain } = state.notifier
170 | if (!listData[action.payload]) {
171 | listData[action.payload] = []
172 | }
173 | listData[action.payload].push(data)
174 |
175 | if (!chain) {
176 | chain = tailChain
177 | } else {
178 | let currNode = chain
179 | while (currNode.next) {
180 | currNode = currNode.next
181 | }
182 | currNode.next = tailChain
183 | }
184 |
185 | state.notifier = {
186 | chain,
187 | listData,
188 | count: ++state.notifier.count,
189 | }
190 | }
191 |
192 | const clearNotifier = (state: IWinState, action: PayloadAction) => {
193 | if (action.payload) {
194 | state.notifier.listData[action.payload].pop()
195 | if (!state.notifier.listData[action.payload].length) {
196 | delete state.notifier.listData[action.payload]
197 | }
198 | } else {
199 | state.notifier = {
200 | chain: null,
201 | listData: {},
202 | count: 0,
203 | }
204 | }
205 | }
206 |
207 | const clearNotifierCount = (state: IWinState, action: PayloadAction) => {
208 | if (action.payload) {
209 | state.notifier.count = 0
210 | }
211 | state.notifier.chain = null
212 | }
213 |
214 | const NextNotifier = (state: IWinState, action: PayloadAction) => {
215 | if (state.notifier.chain) {
216 | state.notifier.chain = state.notifier.chain.next
217 | }
218 | if (action.payload) {
219 | const count = --state.notifier.count
220 | state.notifier.count = count >= 0 ? count : 0
221 | }
222 | }
223 |
224 | const clearHistoryApp = (state: IWinState, action: PayloadAction) => {
225 | state.historyApp = state.historyApp.filter((_, index) => index !== action.payload)
226 | }
227 |
228 | const refurbishIcon = (state: IWinState) => {
229 | state.refurbish = !state.refurbish
230 | }
231 |
232 | const changeIconSort = (state: IWinState, action: PayloadAction) => {
233 | state.sortWay = action.payload
234 | }
235 |
236 | export default {
237 | wallunlock,
238 | changeTheme,
239 | changeDesktopSize,
240 | changeAppList,
241 | changeAppActive,
242 | changeAppIsHide,
243 | closeApp,
244 | changeNight,
245 | changeOpacity,
246 | changeIamge,
247 | changeWidget,
248 | changeBootResatrOrLock,
249 | pushNotifier,
250 | clearNotifier,
251 | clearNotifierCount,
252 | NextNotifier,
253 | clearHistoryApp,
254 | refurbishIcon,
255 | changeIconSort,
256 | }
257 |
--------------------------------------------------------------------------------
/src/screen/Desktop/components/TaskBar/TaskDate.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, startTransition, useCallback, useEffect, useRef, useState } from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { AiOutlineCaretDown, AiOutlineCaretUp, AiOutlineDown } from 'react-icons/ai'
4 | import dayjs from 'dayjs'
5 | import useEvent from '@/hooks/useEvent'
6 | import { CalendarDiv } from '../../type-css'
7 | import useTime from '@/hooks/UseTime'
8 | import { NotifierWidget } from './Notifier'
9 | import { useAppDispatch, useAppSelector } from '@/store'
10 | import { clearNotifierCount } from '@/store/win'
11 |
12 | // eslint-disable-next-line no-sparse-arrays
13 | const week = [, '一', '二', '三', '四', '五', '六', '日']
14 |
15 | function TaskDate() {
16 | const dateRef = useRef(null)
17 | const dispatch = useAppDispatch()
18 | const { time, date } = useTime({ time: '', date: '' }, (date) => ({
19 | time: date.format('HH:mm'),
20 | date: date.format('YYYY/MM/DD'),
21 | }))
22 |
23 | const [active, setActive] = useState(false)
24 | useEvent({
25 | eventName: 'mouseup',
26 | cb(e: React.MouseEvent | MouseEvent) {
27 | const className = (e.target as HTMLElement).className || ''
28 | if (e.target === dateRef.current || typeof className !== 'string' || className.includes('calendar')) return
29 | setActive(false)
30 | },
31 | })
32 |
33 | return (
34 | <>
35 | {
38 | setActive(!active)
39 | dispatch(clearNotifierCount(true))
40 | }}
41 | ref={dateRef}
42 | >
43 |
44 | {time} {date}
45 |
46 |
47 |
48 |
49 | >
50 | )
51 | }
52 |
53 | const Count = memo(() => {
54 | const {
55 | notifier: { count },
56 | } = useAppSelector(({ win }) => win)
57 |
58 | if (count === 0) return null
59 | return {count}
60 | })
61 |
62 | export default memo(TaskDate)
63 |
64 | function Calendar({ active }: { active: boolean }) {
65 | const [visible, setVisible] = useState(true)
66 | const [calendarDate, setCalendar] = useState<{ month: number; day: number }[]>([])
67 | const [activeDay, setActive] = useState({ month: 0, day: 0 })
68 |
69 | const currentDate = useRef({
70 | currentYear: 0,
71 | currentMonth: 0,
72 | currentDay: 0,
73 | currentDate: '',
74 | offset: 0,
75 | scrollTop: 0,
76 | })
77 | const bodyRef = useRef(null)
78 |
79 | useEffect(() => {
80 | if (!active) {
81 | setActive({ month: 0, day: 0 })
82 | setCalendar([])
83 | currentDate.current.scrollTop = 0
84 | return
85 | }
86 | renderData()
87 | }, [active])
88 |
89 | useEffect(() => {
90 | if (!calendarDate.length) return
91 | bodyRef.current?.scroll(0, 48 * currentDate.current.offset + currentDate.current.scrollTop)
92 | let isEvent = false
93 | const scrollHandle = ({ target }: Event) => {
94 | if (!isEvent) {
95 | isEvent = true
96 | return
97 | }
98 | const { scrollTop, clientHeight } = target as HTMLElement
99 | if (scrollTop <= 48 * 2) {
100 | currentDate.current.scrollTop = scrollTop
101 | renderData(`${currentDate.current.currentYear}-${currentDate.current.currentMonth - 1}`)
102 | setActive({ month: 0, day: 0 })
103 | }
104 |
105 | if (clientHeight + scrollTop >= 13 * 48) {
106 | currentDate.current.scrollTop = clientHeight + scrollTop - 13 * 48
107 | renderData(`${currentDate.current.currentYear}-${currentDate.current.currentMonth + 1}`)
108 | setActive({ month: 0, day: 0 })
109 | }
110 | }
111 | bodyRef.current?.addEventListener('scroll', scrollHandle)
112 | return () => bodyRef.current?.removeEventListener('scroll', scrollHandle)
113 | }, [calendarDate])
114 |
115 | const renderData = useCallback((date?: string) => {
116 | const nowTime = dayjs(date)
117 | const { list, offset } = getCalendarData(nowTime.format('YYYY-MM'))
118 |
119 | currentDate.current = {
120 | currentDate: `${dayjs().format('MM月DD日')}, 星期${week[dayjs().day() || 7]}`,
121 | currentMonth: nowTime.month() + 1,
122 | currentDay: nowTime.format('YYYY-M') === dayjs().format('YYYY-M') ? dayjs().date() : 0,
123 | currentYear: nowTime.year(),
124 | offset,
125 | scrollTop: currentDate.current.scrollTop,
126 | }
127 | startTransition(() => setCalendar(list))
128 | }, [])
129 |
130 | const returnClassName = useCallback(
131 | (month: number, day: number) => {
132 | let className = 'calendar'
133 | if (month === currentDate.current.currentMonth) {
134 | className += ' calendar-month-current'
135 | if (day === currentDate.current.currentDay) {
136 | className += ' calendar-day-current'
137 | if (month === activeDay.month && day === activeDay.day) {
138 | className += ' calendar-active-current'
139 | }
140 | } else {
141 | if (month === activeDay.month && day === activeDay.day) {
142 | className += ' calendar-active'
143 | }
144 | }
145 | } else {
146 | if (month === activeDay.month && day === activeDay.day) {
147 | className += ' calendar-active'
148 | }
149 | }
150 | return className
151 | },
152 | [activeDay],
153 | )
154 |
155 | const clickDayHandle = useCallback((e: React.MouseEvent | MouseEvent) => {
156 | const { month, day } = (e.target as HTMLElement).dataset
157 | if (!month || !day) return
158 | setActive({ month: Number(month), day: Number(day) })
159 | }, [])
160 |
161 | const handleBtnChangeCalendar = (direction: 'previous' | 'next') => {
162 | if (direction === 'previous') {
163 | renderData(`${currentDate.current.currentYear}-${currentDate.current.currentMonth - 1}`)
164 | } else {
165 | renderData(`${currentDate.current.currentYear}-${currentDate.current.currentMonth + 1}`)
166 | }
167 | currentDate.current.scrollTop = 0
168 | setActive({ month: 0, day: 0 })
169 | }
170 |
171 | return ReactDOM.createPortal(
172 | <>
173 |
174 |
175 |
176 | {currentDate.current.currentDate}
177 |
setVisible(!visible)}>
178 |
179 |
180 |
181 |
182 | {currentDate.current.currentYear}年{currentDate.current.currentMonth}月
183 |
handleBtnChangeCalendar('next')} />
184 | handleBtnChangeCalendar('previous')} />
185 |
186 |
187 | {week.map((item) => (
188 |
189 | {item}
190 |
191 | ))}
192 |
193 |
194 |
195 | {calendarDate.map(({ month, day }, index) => {
196 | if (!day)
197 | return (
198 |
202 | )
203 | return (
204 |
210 | {day}
211 |
212 | )
213 | })}
214 |
215 |
216 | >,
217 | document.body,
218 | )
219 | }
220 |
221 | function getCalendarData(currentDate: string) {
222 | const current = dayjs(currentDate)
223 | const next = dayjs(currentDate).add(1, 'month')
224 | const previous = dayjs(currentDate).subtract(1, 'month')
225 |
226 | const getMonthList = (current: dayjs.Dayjs) => {
227 | const date = new Date(current.format('YYYY-MM'))
228 | const year = date.getFullYear()
229 | const month = date.getMonth() + 1
230 | const d = new Date(year, month, 0)
231 |
232 | return Array(d.getDate())
233 | .fill(month)
234 | .map((_, index) => ({ month: month, day: index + 1 }))
235 | }
236 |
237 | const previousList = [...Array((previous.day() || 7) - 1).fill({ month: 0, day: 0 }), ...getMonthList(previous)]
238 |
239 | return {
240 | offset: Math.trunc(previousList.length / 7),
241 | list: [...previousList, ...getMonthList(current), ...getMonthList(next)],
242 | }
243 | }
244 |
--------------------------------------------------------------------------------