├── src
├── pages
│ ├── Blog
│ │ ├── blog.less
│ │ ├── Detail
│ │ │ ├── detail.less
│ │ │ └── index.tsx
│ │ ├── Lists
│ │ │ ├── lists.less
│ │ │ └── index.tsx
│ │ ├── action.ts
│ │ └── index.tsx
│ ├── Music
│ │ ├── Sheet
│ │ │ ├── sheet.less
│ │ │ └── index.tsx
│ │ ├── index.tsx
│ │ ├── Search
│ │ │ ├── search.less
│ │ │ └── index.tsx
│ │ ├── Rank
│ │ │ ├── rank.less
│ │ │ └── index.tsx
│ │ ├── SheetDetail
│ │ │ ├── sheet-detail.less
│ │ │ └── index.tsx
│ │ └── action.ts
│ ├── InterLink
│ │ ├── action.ts
│ │ ├── inter-link.less
│ │ └── index.tsx
│ ├── Home
│ │ ├── home.less
│ │ └── index.tsx
│ └── Setting
│ │ ├── setting.less
│ │ └── index.tsx
├── style
│ ├── common
│ │ ├── base.less
│ │ ├── README.md
│ │ ├── _utils.less
│ │ └── _mixin.less
│ ├── common.less
│ ├── index.less
│ ├── type-writer.less
│ ├── reset.less
│ ├── val.less
│ ├── high-custom.less
│ ├── main.less
│ ├── transition.less
│ └── high-default.less
├── config
│ ├── storage.ts
│ ├── color.ts
│ ├── nav.ts
│ ├── constance.ts
│ └── music.ts
├── assets
│ ├── favicon
│ │ └── favicon.ico
│ └── svg
│ │ ├── index.ts
│ │ ├── pause.svg
│ │ ├── play.svg
│ │ ├── menu.svg
│ │ ├── creat.svg
│ │ ├── next.svg
│ │ ├── close.svg
│ │ ├── read.svg
│ │ ├── cat.svg
│ │ ├── tag.svg
│ │ └── logo.svg
├── components
│ ├── MusicListGroup
│ │ ├── music-list-group.less
│ │ └── index.tsx
│ ├── Icon
│ │ ├── icon.less
│ │ └── index.tsx
│ ├── Loading
│ │ ├── loading.less
│ │ └── index.tsx
│ ├── Generic
│ │ ├── generic.less
│ │ └── index.tsx
│ ├── SheetGroup
│ │ ├── sheet-group.less
│ │ └── index.tsx
│ ├── LoadingTips
│ │ ├── loading-tips.less
│ │ └── index.tsx
│ ├── Notice
│ │ ├── message.less
│ │ ├── Notice.tsx
│ │ ├── index.ts
│ │ ├── notice.less
│ │ └── Message.tsx
│ ├── LazyImg
│ │ ├── lazy-img.less
│ │ └── index.tsx
│ ├── CopyRight
│ │ ├── copy-right.less
│ │ └── index.tsx
│ ├── MenuBar
│ │ ├── menu-bar.less
│ │ └── index.tsx
│ ├── SheetList
│ │ ├── sheet-list.less
│ │ └── index.tsx
│ ├── Switch
│ │ ├── switch.less
│ │ └── index.tsx
│ ├── BlogList
│ │ ├── blog-list.less
│ │ └── index.tsx
│ ├── SiderWarp
│ │ ├── sider-warp.less
│ │ └── index.tsx
│ ├── Nav
│ │ ├── index.tsx
│ │ └── nav.less
│ ├── Lyric
│ │ ├── lyric.less
│ │ └── index.tsx
│ ├── MusicList
│ │ ├── music-list.less
│ │ └── index.tsx
│ ├── Dialog
│ │ └── index.tsx
│ └── DAudio
│ │ ├── d-audio.less
│ │ └── index.tsx
├── use
│ ├── useNavType
│ │ └── index.ts
│ ├── useScroll
│ │ └── index.ts
│ └── useLoadingTips
│ │ └── index.ts
├── enum.ts
├── store
│ ├── index.ts
│ ├── models
│ │ ├── color.ts
│ │ ├── nav.ts
│ │ └── music.ts
│ └── types.ts
├── app.tsx
├── type.ts
├── utils
│ ├── use.ts
│ ├── event.ts
│ ├── typewriter.ts
│ ├── utils.ts
│ ├── fetch.ts
│ └── music.ts
├── index.tsx
├── Main.tsx
├── api
│ └── api.ts
└── loadable.ts
├── README.md
├── global.d.ts
├── url
├── .gitignore
├── .babelrc
├── tsconfig.json
├── index.html
├── webpack.dev.js
├── package.json
├── webpack.prod.js
└── webpack.common.js
/src/pages/Blog/blog.less:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-website
2 | 第三版个人网站更新
3 |
--------------------------------------------------------------------------------
/src/style/common/base.less:
--------------------------------------------------------------------------------
1 | @import "./_utils.less";
2 |
--------------------------------------------------------------------------------
/src/style/common/README.md:
--------------------------------------------------------------------------------
1 | 这里是less的通用样式,mixin,和一些方法,同scss
2 |
--------------------------------------------------------------------------------
/global.d.ts:
--------------------------------------------------------------------------------
1 | interface Window {
2 | __FIRST_IN_HOME__: boolean
3 | }
4 |
--------------------------------------------------------------------------------
/src/style/common.less:
--------------------------------------------------------------------------------
1 | @import "./common/base.less";
2 | @import "./val.less";
--------------------------------------------------------------------------------
/url:
--------------------------------------------------------------------------------
1 | [dw 2b6b230] music——
2 | 4 files changed, 15 insertions(+), 4 deletions(-)
3 |
--------------------------------------------------------------------------------
/src/config/storage.ts:
--------------------------------------------------------------------------------
1 | export const StorageKeys = {
2 | color: 'react_website_color'
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/Music/Sheet/sheet.less:
--------------------------------------------------------------------------------
1 |
2 | // .@{project-prefix}-music-sheet {
3 | // width:
4 | // }
5 |
--------------------------------------------------------------------------------
/src/style/index.less:
--------------------------------------------------------------------------------
1 |
2 | @import "./reset.less";
3 | @import "./main.less";
4 | @import "./transition.less";
5 |
--------------------------------------------------------------------------------
/src/assets/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IFmiss/react-website/HEAD/src/assets/favicon/favicon.ico
--------------------------------------------------------------------------------
/src/style/type-writer.less:
--------------------------------------------------------------------------------
1 | .type-writer-bar {
2 | display: inline-block;
3 | width: 3px;
4 | background: red;
5 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | package-lock.json
8 |
--------------------------------------------------------------------------------
/src/components/MusicListGroup/music-list-group.less:
--------------------------------------------------------------------------------
1 | .@{project-prefix}-music-list-group {
2 | &-transition {
3 | width: 100%;
4 | }
5 | }
--------------------------------------------------------------------------------
/src/components/Icon/icon.less:
--------------------------------------------------------------------------------
1 | .@{project-prefix}-comp-icon {
2 | width: 100%;
3 | height: 100%;
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | line-height: 1;
8 | }
--------------------------------------------------------------------------------
/src/components/Loading/loading.less:
--------------------------------------------------------------------------------
1 | .loadable-loading {
2 | width: 100%;
3 | height: 300px;
4 | margin: 0 auto;
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 | }
9 |
--------------------------------------------------------------------------------
/src/assets/svg/index.ts:
--------------------------------------------------------------------------------
1 | const requireAll = (requireContext: any) => requireContext.keys().map(requireContext)
2 | const req = require.context('./', false, /\.svg$/)
3 | requireAll(req)
4 | export default req
5 |
--------------------------------------------------------------------------------
/src/use/useNavType/index.ts:
--------------------------------------------------------------------------------
1 | import store from './../../store'
2 | import { useEffect } from 'react'
3 | export default function useNavType(type: number) {
4 | useEffect(() => {
5 | store.navStore.setNavLists(type)
6 | }, [])
7 | }
--------------------------------------------------------------------------------
/src/components/Generic/generic.less:
--------------------------------------------------------------------------------
1 | .generic-component {
2 | transition: color,background 0.3s;
3 | .transition-text {
4 | transition: color 0.3s;
5 | }
6 | .transition-ele {
7 | transition: color,background 0.3s;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/SheetGroup/sheet-group.less:
--------------------------------------------------------------------------------
1 | .@{project-prefix}-sheet-group {
2 | margin-top: 30px;
3 | > div {
4 | display: flex;
5 | align-items: center;
6 | justify-content: flex-start;
7 | flex-wrap: wrap;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/LoadingTips/loading-tips.less:
--------------------------------------------------------------------------------
1 | .@{project-prefix}-comp-loading-tips {
2 | display: none;
3 | align-items: center;
4 | justify-content: center;
5 | padding: 5px 0;
6 | &.loading-tips-active {
7 | display: flex;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/pages/InterLink/action.ts:
--------------------------------------------------------------------------------
1 | import Http from '../../utils/fetch'
2 | import API from '../../api/api'
3 | export const getLinkLists = async () => {
4 | const data = await Http.get(API.OTHER.links, {
5 | name: 'getLinks'
6 | })
7 | return data
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/src/pages/Home/home.less:
--------------------------------------------------------------------------------
1 | .@{home-prefix} {
2 | span {
3 | cursor: pointer;
4 | transition: color 0.3s;
5 | &.underline {
6 | text-decoration: underline;
7 | &:hover {
8 | color: var(--text-color-active);
9 | }
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/src/enum.ts:
--------------------------------------------------------------------------------
1 | export enum NAV_TYPE {
2 | HOME = 1,
3 | MUSIC = 2
4 | }
5 |
6 | export enum MUSIC_SHEET_TYPE {
7 | LANGUAGES,
8 | STYLES,
9 | SCENCE,
10 | EMOTION,
11 | THEME,
12 | ALL
13 | }
14 |
15 | export enum IAMGE_LOAD_STATUS {
16 | LOADING,
17 | SUCCES,
18 | ERRER
19 | }
20 |
--------------------------------------------------------------------------------
/src/assets/svg/pause.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import ColorModel from './models/color'
2 | import NavModel from './models/nav'
3 | import MusicModel from './models/music'
4 | import { IStore } from './types'
5 | const store: IStore = {
6 | colorStore: new ColorModel(),
7 | navStore: new NavModel(),
8 | musicStore: new MusicModel()
9 | }
10 | export default store
11 |
--------------------------------------------------------------------------------
/src/components/Notice/message.less:
--------------------------------------------------------------------------------
1 | .@{project-prefix}-comp-notice-queue {
2 | position: fixed;
3 | right: 0;
4 | padding-right: 40px;
5 | top: @nav-height + 40px;
6 | width: 300px;
7 | max-height: calc(100vh - (@nav-height + 80px));
8 | z-index: @notice-z-index;
9 | overflow: auto;
10 | &::-webkit-scrollbar {
11 | display: none;
12 | width: 0;
13 | }
14 | }
--------------------------------------------------------------------------------
/src/components/LazyImg/lazy-img.less:
--------------------------------------------------------------------------------
1 | .@{project-prefix}-comp-lazy-img {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | width: 100%;
6 | height: 100%;
7 | .tips {
8 | display: flex;
9 | align-items: center;
10 | justify-content: center;
11 | font-size: 12px;
12 | height: 100%;
13 | width: 100%;
14 | border-bottom: 1px solid var(--border-color)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/store/models/color.ts:
--------------------------------------------------------------------------------
1 | import { observable, computed, action } from 'mobx'
2 |
3 | import { getNewSelfColor } from './../../config/color'
4 | export default class ColorModel {
5 | @observable
6 | mode: string = 'light'
7 |
8 | @action
9 | changeMode (mode?: string) {
10 | if (mode) {
11 | this.mode = mode
12 | return
13 | }
14 | this.mode = this.mode === 'light' ? 'dark' : 'light'
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/app.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Route, Switch } from 'react-router'
3 | import { BrowserRouter } from 'react-router-dom'
4 | import store from './store'
5 | import { storeContext } from './utils/use'
6 | import Main from './Main'
7 |
8 | const App = () => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | )
16 | }
17 |
18 | export default App
19 |
--------------------------------------------------------------------------------
/src/assets/svg/play.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Loading/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './loading.less';
3 |
4 | interface ILoadingProps {
5 | tip?: string
6 | isLoading: boolean;
7 | pastDelay: boolean;
8 | timedOut: boolean;
9 | error: any;
10 | retry: () => void;
11 | }
12 |
13 | const Loading = (props: ILoadingProps) => {
14 | return (
15 |
16 | { props.tip }
17 |
18 | )
19 | }
20 |
21 | Loading.defaultPorps = {
22 | tip: 'loading ...'
23 | }
24 |
25 | export default Loading
26 |
--------------------------------------------------------------------------------
/src/store/models/nav.ts:
--------------------------------------------------------------------------------
1 | import { get, set, observable, values, computed, action } from 'mobx'
2 | import { INavLists, HomeNav, MusicNav } from './../../config/nav'
3 | import { NAV_TYPE } from './../../enum'
4 | export default class ColorModel {
5 | @observable
6 | lists: INavLists = HomeNav
7 |
8 | @action.bound
9 | setNavLists (type: NAV_TYPE) {
10 | switch (type) {
11 | case NAV_TYPE.MUSIC:
12 | this.lists = MusicNav
13 | break;
14 | default:
15 | this.lists = HomeNav
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/CopyRight/copy-right.less:
--------------------------------------------------------------------------------
1 | .@{project-prefix}-comp-copy-right {
2 | position: fixed;
3 | right: 0;
4 | bottom: 0;
5 | width: auto;
6 | display: inline-block;
7 | font-size: 14px;
8 | padding: 6px 10px;
9 | font-weight: 200;
10 | cursor: default;
11 | .list {
12 | opacity: 0.3;
13 | margin: 0 5px;
14 | }
15 | &-info, .split {
16 | .mixinTextColor();
17 | }
18 | a {
19 | &:hover {
20 | opacity: 0.6;
21 | color: var(--primary-color);
22 | text-decoration: underline;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/pages/Blog/Detail/detail.less:
--------------------------------------------------------------------------------
1 | .@{blog-prefix}-detail {
2 | padding: 0 0 40px 0;
3 | &-entry {
4 | margin-top: 50px;
5 | padding: 10px 0;
6 | display: flex;
7 | align-items: center;
8 | justify-content: space-between;
9 | &-prev, &-next {
10 | flex: 1;
11 | overflow: hidden;
12 | text-overflow: ellipsis;
13 | white-space: nowrap;
14 | }
15 | &-prev {
16 | margin-right: 20px!important;
17 | }
18 | &-next {
19 | margin-left: 20px!important;
20 | text-align: right;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/type.ts:
--------------------------------------------------------------------------------
1 | export type SheetGroupLists = SheetGroupList[] | any
2 | export interface SheetGroupList {
3 |
4 | }
5 |
6 | export type artists = artist[]
7 | export interface artist {
8 | id: number;
9 | img1v1Url: string;
10 | name: string;
11 | picId: number;
12 | }
13 |
14 | export type MusicGroupLists = MusicGroupList[]
15 | export interface MusicGroupList {
16 | album: any;
17 | alias: any[];
18 | artists: artists;
19 | duration: number;
20 | id: number;
21 | name: string;
22 | status: number;
23 | rtype: number;
24 | fee: number;
25 | ftype: number;
26 | }
27 |
--------------------------------------------------------------------------------
/src/pages/InterLink/inter-link.less:
--------------------------------------------------------------------------------
1 | .@{project-prefix}-interlink {
2 | position: relative;
3 | & > div {
4 | display: flex;
5 | align-items: flex-start;
6 | justify-content: flex-start;
7 | flex-wrap: wrap;
8 | }
9 | .list {
10 | width: auto;
11 | padding: 30px 40px;
12 | box-sizing: border-box;
13 | overflow: hidden;
14 | text-overflow: ellipsis;
15 | white-space: nowrap;
16 | &-href {
17 | &:hover {
18 | opacity: 0.6;
19 | color: var(--primary-color);
20 | text-decoration: underline;
21 | }
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/src/assets/svg/menu.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/creat.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utils/use.ts:
--------------------------------------------------------------------------------
1 | import { useContext, useState } from 'react'
2 | import { MUSIC_SHEET_TRANSITION_DURATION } from './../config/constance'
3 | import React, { useEffect, useLayoutEffect } from 'react'
4 | import store from './../store'
5 |
6 | // react context
7 | export const storeContext = React.createContext(store);
8 |
9 | // store数据
10 | export const useStore = () => {
11 | const store = useContext(storeContext)
12 | return store;
13 | }
14 |
15 | export function useUpdate () {
16 | const [isUpdate, setIsUpdate] = useState(false)
17 | useEffect(() => {
18 | }, [isUpdate])
19 |
20 | setIsUpdate((isUpdate) => !isUpdate)
21 | }
22 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env", {
4 | "modules": "false"
5 | }],
6 | ["@babel/preset-react"]
7 | ],
8 | "compact": false,
9 | "plugins":[
10 | ["@babel/plugin-proposal-decorators", { "legacy": true }],
11 | ["@babel/plugin-syntax-dynamic-import"],
12 | ["@babel/plugin-proposal-class-properties", { "loose" : true }],
13 | [
14 | "@babel/plugin-transform-runtime",
15 | {
16 | "absoluteRuntime": false,
17 | "corejs": false,
18 | "helpers": true,
19 | "regenerator": true,
20 | "useESModules": false
21 | }
22 | ]
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "experimentalDecorators": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "preserve"
22 | },
23 | "include": [
24 | "src",
25 | "global.d.ts"
26 | ]
27 | }
--------------------------------------------------------------------------------
/src/assets/svg/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/close.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/LoadingTips/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import classNames from 'classnames'
3 | import { PROJECT_NAME } from '../../config/constance';
4 | import './loading-tips.less'
5 |
6 | interface ILoadingTips {
7 | show?: boolean;
8 | text?: string;
9 | }
10 |
11 | const LoadingTips = (props: ILoadingTips) => {
12 | const classString = classNames({
13 | [`${PROJECT_NAME}-comp-loading-tips`]: true,
14 | ['loading-tips-active']: props.show
15 | })
16 | return (
17 |
18 | {props.text}
19 |
20 | )
21 | }
22 |
23 | LoadingTips.defaultProps = {
24 | show: false,
25 | text: '加载中...'
26 | }
27 |
28 | export default LoadingTips
29 |
--------------------------------------------------------------------------------
/src/utils/event.ts:
--------------------------------------------------------------------------------
1 | import { TITLE, TITLE_ENTRY, TITLE_OUT } from './../config/constance'
2 | class EventClass {
3 | private t: any = null
4 | public addVisiblityChange () {
5 | const self = this
6 | // visibilitychange
7 | const onVisibilityChange = function () {
8 | clearTimeout(self.t)
9 | if (!document['hidden']) {
10 | document.title = TITLE_ENTRY
11 | self.t = setTimeout(() => {
12 | document.title = TITLE
13 | }, 3000)
14 | } else {
15 | document.title = TITLE_OUT
16 | }
17 | }
18 | document.addEventListener('visibilitychange', onVisibilityChange)
19 | }
20 | }
21 |
22 | const selfEvent = new EventClass()
23 | export default selfEvent
24 |
--------------------------------------------------------------------------------
/src/config/color.ts:
--------------------------------------------------------------------------------
1 | import { StorageKeys } from './storage'
2 |
3 | export interface IColorInfo {
4 | primaryColor: string
5 | }
6 |
7 | export const selfColor: IColorInfo = {
8 | primaryColor: 'blue'
9 | }
10 |
11 | export const getNewSelfColor = (): any => {
12 | const colorStorage = localStorage.getItem(StorageKeys.color)
13 | if (colorStorage) {
14 | return localStorage.getItem(JSON.parse(StorageKeys.color))
15 | }
16 | return selfColor
17 | }
18 |
19 | export const setNewSelfColor = (data: any) => {
20 | // const colorStorage = localStorage.getItem(StorageKeys.color)
21 | // if (colorStorage) {
22 | // return localStorage.getItem(JSON.parse(StorageKeys.color))
23 | // }
24 | // return defaultColor
25 | }
26 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './app'
4 | import './style/index.less'
5 | import { initPageMode, initDAudiConfig } from './utils/utils'
6 | import selfEvent from './utils/event'
7 | import './assets/svg'
8 | import PF from 'd-utils/lib/performanceUtils'
9 | import LogUtils from 'd-utils/lib/logUtils'
10 | import { CONSOLE_TEXT, CONSOLE_BGS } from './config/constance'
11 |
12 | PF.logger()
13 | LogUtils.logBeauty(CONSOLE_TEXT, {
14 | isMax: true,
15 | colors: CONSOLE_BGS
16 | })
17 |
18 | selfEvent.addVisiblityChange()
19 | initDAudiConfig()
20 |
21 | ReactDOM.render(
22 | ,
25 | document.getElementById('react-website')
26 | )
27 |
--------------------------------------------------------------------------------
/src/components/Generic/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './generic.less'
3 | import { IColorInfo } from './../../config/color'
4 | import { NameSpaceStore } from './../../store/types'
5 | import { IStore } from './../../store/types'
6 | import { PROJECT_NAME } from './../../config/constance'
7 |
8 | export interface GenericProps {
9 | className?: string;
10 | store?: IStore;
11 | color?: NameSpaceStore.IColorModel;
12 | prefixClass?: string;
13 | }
14 |
15 | export interface GenericState {}
16 |
17 | export default class GenericComponent extends React.Component
{
18 | // props: P & GenericProps;
19 | // state: S & GenericState;
20 |
21 | static defaultProps = {
22 | prefixClass: PROJECT_NAME,
23 | className: `generic-component ${PROJECT_NAME}`
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/use/useScroll/index.ts:
--------------------------------------------------------------------------------
1 | import { debounce } from 'd-utils/lib/genericUtils'
2 | import { useEffect } from 'react'
3 |
4 | /**
5 | * 滚动监听
6 | * @param ref
7 | * @param requestCallBack
8 | */
9 | export default function useScroll (ref: any, requestCallBack: () => void, t: number = 3000) {
10 | if (!ref) return
11 | const eventHandler = (e: any) => {
12 | const newRef = ref.current ? ref.current : ref
13 | if (newRef.clientHeight + newRef.scrollTop === newRef.scrollHeight) {
14 | callBack()
15 | }
16 | }
17 |
18 | const callBack = debounce(requestCallBack, t)
19 |
20 | useEffect(() => {
21 | const newRef = ref.current ? ref.current : ref
22 | newRef.addEventListener('scroll', eventHandler)
23 |
24 | return () => {
25 | newRef.removeEventListener('scroll', eventHandler)
26 | }
27 | }, [])
28 | }
29 |
--------------------------------------------------------------------------------
/src/pages/Blog/Lists/lists.less:
--------------------------------------------------------------------------------
1 | .@{blog-prefix}-lists-info {
2 | .list-contariner {
3 | padding: 20px 0 40px 0;
4 | }
5 | .sider-title {
6 | margin: 0;
7 | padding: 10px 0;
8 | font-size: 14px;
9 | }
10 | .sider-lists {
11 | padding: 5px 0;
12 | span {
13 | display: inline-block;
14 | font-size: 12px;
15 | padding: 2px 10px;
16 | color: var(--text-color);
17 | border: 1px solid var(--text-color);
18 | border-radius: 1em;
19 | margin-left: 10px;
20 | margin-bottom: 15px;
21 | cursor: pointer;
22 | transition: all 0.3s;
23 | &:hover {
24 | color: var(--text-color-active);
25 | border: 1px solid var(--text-color-active);
26 | }
27 | &.active {
28 | color: var(--primary-color);
29 | border: 1px solid var(--primary-color);
30 | }
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/style/common/_utils.less:
--------------------------------------------------------------------------------
1 | @import "./_mixin.less";
2 | // 浮动
3 | .right{
4 | float: right;
5 | }
6 |
7 | .left{
8 | float: left;
9 | }
10 |
11 | .center{
12 | margin: 0 auto;
13 | float: none;
14 | }
15 |
16 | // 清除浮动
17 | .clear-both{
18 | clear: both;
19 | height:1px;
20 | margin-top:-1px;
21 | overflow:hidden;
22 | &:before{
23 | display:none;
24 | }
25 | &:after{
26 | display:none;
27 | }
28 | }
29 |
30 | .block_area{
31 | margin: 0 auto;
32 | position: relative;
33 | font-size: 14px;
34 | }
35 |
36 | // inline-block对齐 或者文本对齐
37 | .text-left{
38 | text-align:left;
39 | }
40 |
41 | .text-right{
42 | text-align:right;
43 | }
44 |
45 | .text-center{
46 | text-align:center;
47 | }
48 |
49 | .text-bold{
50 | font-weight:bold;
51 | }
52 |
53 | .activeB{
54 | display: block;
55 | }
56 |
57 | .activeF{
58 | display: flex;
59 | }
60 |
61 | .activeN{
62 | display:none;
63 | }
64 |
--------------------------------------------------------------------------------
/src/pages/Blog/action.ts:
--------------------------------------------------------------------------------
1 | // blog 的相关操作
2 | import {
3 | BLOG_LIST_DEFAULT_LIMIT
4 | } from './../../config/constance'
5 | import Http from './../../utils/fetch'
6 | import API from './../../api/api'
7 |
8 | export const getBlogLists = async (offset: number, tagName: string = '全部', limit: number = BLOG_LIST_DEFAULT_LIMIT): Promise => {
9 | return await Http.post(API.BLOG.list, {
10 | tagName,
11 | offset,
12 | limit,
13 | })
14 | }
15 |
16 | export const getBolgTags = async () => {
17 | return await Http.get(API.BLOG.tags, {
18 | name: 'getArticleTags'
19 | })
20 | }
21 |
22 | export const getBlogDetail = async (id: number | string): Promise => {
23 | return await Http.post(API.BLOG.detail, {
24 | id
25 | })
26 | }
27 |
28 | export const pv = async (id: number | string): Promise => {
29 | return await Http.post(API.BLOG.pv, {
30 | id
31 | })
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/src/pages/Blog/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import classNames from 'classnames'
3 | import { PROJECT_NAME } from './../../config/constance'
4 | import { Route, Switch, Redirect } from 'react-router-dom'
5 | import { BlogLists, BlogDetail } from './../../loadable';
6 | import store from './../../store'
7 |
8 | interface IBlogProps {}
9 |
10 | const Blog: React.FC = (props: IBlogProps) => {
11 | const classString = classNames({
12 | [`${PROJECT_NAME}-blog`]: true,
13 | [`dw-page-router`]: true
14 | })
15 |
16 | store.navStore.setNavLists(1)
17 |
18 | return (
19 |
20 | {/* this is Blog */}
21 |
22 |
23 |
24 |
25 |
26 |
27 | )
28 | }
29 |
30 | export default Blog
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | 未曾遗忘的青春 | web前端_技术分享_戴伟的个人网站
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const merge = require('webpack-merge');
2 | const common = require('./webpack.common.js');
3 | const path = require('path');
4 | const resolve = function (dir) {
5 | return path.resolve(__dirname, dir);
6 | }
7 | module.exports = merge(common, {
8 | mode: 'development',
9 | entry: {
10 | app: './src/index.tsx'
11 | },
12 | module: {
13 | rules: [
14 | {
15 | test: /\.(js|jsx)$/,
16 | loader: 'babel-loader'
17 | },
18 | ]
19 | },
20 | output: {
21 | path: resolve('dist'),
22 | publicPath: '/',
23 | filename: 'js/[name]-[hash].js'
24 | },
25 | devtool: 'inline-source-map',
26 | devServer: {
27 | // 当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html。通过传入以下启用:
28 | // contentBase: "./",
29 | host: '0.0.0.0',
30 | // 端口号
31 | port: 2005,
32 | //当有编译器错误或警告时,在浏览器中显示全屏覆盖。默认禁用。如果您只想显示编译器错误:
33 | noInfo: true,
34 | // 配置端口号
35 | overlay: true,
36 | historyApiFallback: true
37 | }
38 | });
39 |
--------------------------------------------------------------------------------
/src/pages/Setting/setting.less:
--------------------------------------------------------------------------------
1 | .@{setting-prefix} {
2 | &-title {
3 | // font-weight: 400;
4 | .mixinTextColorActive();
5 | transition: color 0.3s;
6 | }
7 | &-wrap-title {
8 | font-weight: 500;
9 | position: relative;
10 | text-indent: 10px;
11 | margin: 20px auto 15px auto;
12 | &::before {
13 | content: '';
14 | position: absolute;
15 | top: 50%;
16 | left: 0;
17 | transform: translate(0, -50%);
18 | width: 3px;
19 | height: 18px;
20 | border-radius: 2px;
21 | .mixinPrimaryBg();
22 | transition: background-color 0.3s;
23 | }
24 | }
25 | &-wrap-content {
26 | padding: 0 0 0 20px;
27 | margin: 10px auto;
28 | &-list {
29 | list-style: none;
30 | display: flex;
31 | align-items: center;
32 | justify-content: space-between;
33 | padding: 10px 0;
34 | font-size: 14px;
35 | border-bottom: 1px solid var(--border-color);
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/src/components/MenuBar/menu-bar.less:
--------------------------------------------------------------------------------
1 | .@{project-prefix}-menu-bar {
2 | &-main, &-cat {
3 | display: flex;
4 | align-items: center;
5 | justify-content: center;
6 | margin: 6px auto;
7 | &-list {
8 | margin: 0 8px;
9 | padding: 10px 5px;
10 | cursor: pointer;
11 | font-size: 14px;
12 | font-weight: 500;
13 | .mixinTextColor();
14 | position: relative;
15 | transition: color 0.3s;
16 | &.active {
17 | .mixinTextColorActive();
18 | &::before {
19 | width: 50%;
20 | transform: translate(-50%, 0);
21 | }
22 | }
23 | &::before {
24 | content: '';
25 | position: absolute;
26 | background: var(--primary-color);
27 | bottom: 5px;
28 | left: 50%;
29 | width: 0;
30 | height: 2px;
31 | border-radius: 4px;
32 | transition: all 0.3s;
33 | transform: translate(-50%, 400%);
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/SheetList/sheet-list.less:
--------------------------------------------------------------------------------
1 | .@{project-prefix}-sheet-list {
2 | width: 20%;
3 | padding: 15px;
4 | box-sizing: border-box;
5 | &-content {
6 | position: relative;
7 | width: 100%;
8 | height: 100%;
9 | cursor: pointer;
10 | box-shadow: 1px 1px 24px 0 var(--shadow-color);
11 | border-radius: @raduis;
12 | &-top {
13 | width: 100%;
14 | padding-top: 100%;
15 | position: relative;
16 | .sheet-list-lazy-img {
17 | position: absolute;
18 | top: 0;
19 | left: 0;
20 | width: 100%;
21 | height: 100%;
22 | border-radius: @raduis @raduis 0 0;
23 | img {
24 | position: absolute;
25 | top: 0;
26 | left: 0;
27 | width: 100%;
28 | height: 100%;
29 | border: none;
30 | border-radius: @raduis @raduis 0 0;
31 | }
32 | }
33 | }
34 | &-title {
35 | margin: 0;
36 | margin: 8px;
37 | font-size: 14px;
38 | .lineclamp(44px, 2);
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/src/use/useLoadingTips/index.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import LoadingTips from './../../components/Loading'
3 |
4 | interface IuseLoadingTips {
5 | /** 是否显示 提示信息 */
6 | loading: boolean;
7 |
8 | /** 提示信息的文字信息 */
9 | text: string;
10 |
11 | /** 显示LoadingTips 可以设置文字 可选 */
12 | showLoading(text?: string): void;
13 |
14 | /** 关闭 */
15 | hideLoading(): void;
16 | }
17 |
18 |
19 | /**
20 | * 控制显示LoadingTips的组件
21 | * 依赖于LoadingTips组件
22 | */
23 | export default function useLoadingTips(state: boolean = false, text: string = 'loading...'): IuseLoadingTips {
24 | const [loading, setLoading] = useState(state)
25 | const [loadingText, setLoadingText] = useState(text)
26 |
27 | function showLoading (text?: string) {
28 | setLoading((loading) => loading = true)
29 | text && setLoadingText((loadingText) => loadingText = text)
30 | }
31 |
32 | function hideLoading () {
33 | setLoading((loading) => loading = false)
34 | }
35 |
36 | return {
37 | loading,
38 | text: loadingText,
39 | showLoading,
40 | hideLoading
41 | }
42 | }
43 |
44 |
--------------------------------------------------------------------------------
/src/pages/Music/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import classNames from 'classnames'
3 | import { PROJECT_NAME } from './../../config/constance'
4 | import { Route, Switch, Redirect } from 'react-router-dom'
5 | import { MusicSheet, MusicRank, MusicSearch, MusicSheetDetail } from './../../loadable'
6 | import useNavType from './../../use/useNavType'
7 | interface IMusicProps {}
8 |
9 | const Music: React.FC = (props: IMusicProps) => {
10 | const classString = classNames({
11 | [`${PROJECT_NAME}-music`]: true,
12 | [`dw-page-router`]: true
13 | })
14 |
15 | useNavType(2)
16 |
17 | return (
18 |
19 | {/* this is Music */}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | export default Music
--------------------------------------------------------------------------------
/src/assets/svg/read.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/CopyRight/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { PROJECT_NAME, COPY_RIGHT_CONFIG } from './../../config/constance'
3 | import classNames from 'classnames'
4 | import { formatDate } from 'd-utils/lib/genericUtils'
5 | import './copy-right.less';
6 | import { Link } from 'react-router-dom'
7 |
8 | const CopyRight: React.FC = () => {
9 | const classString = classNames({
10 | [`${PROJECT_NAME}-comp-copy-right`]: true
11 | })
12 |
13 | return (
14 |
15 |
© 2016 - { formatDate('yyyy', new Date()) } from dw
16 |
|
17 |
18 | {
19 | COPY_RIGHT_CONFIG.map((item) => (
20 | item.to.includes('http') ? (
21 | {item.name}
22 | ) : (
23 | {item.name}
24 | )
25 | ))
26 | }
27 |
28 |
29 | )
30 | }
31 | export default CopyRight
32 |
--------------------------------------------------------------------------------
/src/Main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | // import { Route, Switch, Redirect } from 'react-router'
3 | import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom'
4 | import { Home, Blog, Music, Setting, InterLink } from './loadable'
5 | import Nav from './components/Nav/index'
6 | import CopyRight from './components/CopyRight'
7 |
8 | interface IMainProps {
9 | prefixClass?: string;
10 | history?: any;
11 | }
12 |
13 | interface IMainState {}
14 |
15 | const Main = (props: IMainProps) => {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | )
34 | }
35 |
36 | export default Main
37 |
--------------------------------------------------------------------------------
/src/components/SheetList/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import classNames from 'classnames'
3 | import { PROJECT_NAME } from './../../config/constance'
4 | import './sheet-list.less'
5 | import LazyImg from './../LazyImg'
6 |
7 | interface ISheetListProps {
8 | list: any;
9 | history: any;
10 | }
11 |
12 | const SheetList = (props: ISheetListProps) => {
13 | const toDetail = () => {
14 | props.history.push(`/music/sheetDetail?sheetId=${props.list.id}`)
15 | }
16 |
17 | const classString = classNames({
18 | [`${PROJECT_NAME}-sheet-list`]: true,
19 | })
20 |
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
28 | {
29 | `${props.list.name}`
30 | }
31 |
32 |
33 |
34 | )
35 | }
36 |
37 | export default SheetList
38 |
--------------------------------------------------------------------------------
/src/pages/Music/Search/search.less:
--------------------------------------------------------------------------------
1 | .@{project-prefix}-music-search {
2 | // height: -webkit-fill-available;
3 | // position: relative;
4 | overflow: hidden;
5 | width: @container-music-list-width;
6 | margin: 0 auto;
7 | &-entry {
8 | position: relative;
9 | height: 36px;
10 | width: 100%;
11 | display: flex;
12 | align-items: center;
13 | justify-content: center;
14 | margin-top: 20%;
15 | margin-bottom: 60px;
16 | transition: all 0.3s;
17 | &-input {
18 | background: transparent;
19 | border: none;
20 | outline: none;
21 | border-bottom: 1px solid var(--auto-color);
22 | height: 36px;
23 | width: 320px;
24 | font-size: 16px;
25 | color: var(--auto-color);
26 | }
27 | }
28 | &-group{
29 | position: absolute;
30 | top: 140px;
31 | left: 0;
32 | right: 0;
33 | padding: 0 30px;
34 | box-sizing: border-box;
35 | &.show {
36 | bottom: 20px;
37 | overflow: auto;
38 | }
39 | }
40 | &.input-active {
41 | justify-content: flex-start;
42 | .dw-react-web-music-search-entry {
43 | margin-top: 10px;
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/style/reset.less:
--------------------------------------------------------------------------------
1 | body, html {
2 | margin: 0;
3 | padding: 0;
4 | min-height: 100vh;
5 | overflow: hidden;
6 | width: 100%;
7 | font-weight: 200;
8 | font-family: 'PingFangSC-Medium', 'Microsoft YaHei', 'Avenir', Helvetica, Arial, sans-serif;
9 | }
10 |
11 | body {
12 | background: var(--bg);
13 | }
14 |
15 | ul li {
16 | list-style: none;
17 | }
18 |
19 | * {
20 | box-sizing: border-box;
21 | }
22 |
23 | h1, h2, h3, h4, h5, h6, p, span, a {
24 | line-height: 1.5;
25 | }
26 |
27 | // 滚动条
28 | ::-webkit-scrollbar {
29 | width: 14px;
30 | height: 14px;
31 | }
32 |
33 | ::-webkit-scrollbar-track,
34 | ::-webkit-scrollbar-thumb {
35 | border-radius: 999px;
36 | border: 5px solid transparent;
37 | }
38 |
39 | ::-webkit-scrollbar-track {
40 | box-shadow: 1px 1px 5px var(--scroll-color) inset;
41 | }
42 |
43 | ::-webkit-scrollbar-thumb {
44 | min-height: 20px;
45 | background-clip: content-box;
46 | box-shadow: 0 0 0 5px var(--scroll-color) inset;
47 | }
48 |
49 | ::-webkit-scrollbar-corner {
50 | background: transparent;
51 | }
52 |
53 | a {
54 | text-decoration: none;
55 | color: var(--text-color);
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/Notice/Notice.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import classNames from 'classnames'
3 | import { PROJECT_NAME } from './../../config/constance'
4 | import './notice.less'
5 | import { observer, useObservable, useObserver, useLocalStore, useStaticRendering, useComputed } from "mobx-react-lite"
6 | // import { useStore } from './../../utils/use'
7 | import store from './../../store'
8 |
9 | export enum NoticeType {
10 | INFO = 'info',
11 | SUCCESS = 'success',
12 | ERROR = 'error',
13 | WARNING = 'warning',
14 | DEFAULT = 'default'
15 | }
16 |
17 | export interface INoticeProps {
18 | type: NoticeType;
19 | text: string | number;
20 | }
21 |
22 | const Notice: React.FC = observer((props) => {
23 | const { mode } = store.colorStore
24 | const { type, text } = props
25 | const theme = localStorage.getItem
26 | const classString = classNames({
27 | [`${PROJECT_NAME}-comp-notice`]: true,
28 | [`notice-${type}`]: true,
29 | [`${mode}`]: true
30 | })
31 | return (
32 |
33 | {text}
34 |
35 | )
36 | })
37 |
38 | export default Notice
39 |
--------------------------------------------------------------------------------
/src/pages/Music/Rank/rank.less:
--------------------------------------------------------------------------------
1 | .@{project-prefix}-music-rank {
2 | &-type-lists {
3 | position: fixed;
4 | top: 0;
5 | left: 0;
6 | right: 0;
7 | bottom: 0;
8 | opacity: 0;
9 | visibility: hidden;
10 | display: flex;
11 | align-items: center;
12 | justify-content: center;
13 | z-index: @rank-type-list-z-index;
14 | background: var(--bg);
15 | transition: opacity 0.3s;
16 | .content {
17 | width: 400px;
18 | height: 400px;
19 | display: flex;
20 | align-items: center;
21 | justify-content: flex-start;
22 | flex-wrap: wrap;
23 | .type-list {
24 | margin: 0 20px;
25 | cursor: pointer;
26 | &:hover {
27 | color: var(--text-color-active);
28 | }
29 | &.active {
30 | color: var(--primary-color);
31 | }
32 | }
33 | }
34 | &.active {
35 | opacity: 1;
36 | visibility: visible;
37 | }
38 | }
39 |
40 | &-type-current {
41 | margin-bottom: 20px;
42 | text-align: right;
43 | span {
44 | &.desc {}
45 | &.info {
46 | cursor: pointer;
47 | color: var(--text-color-active);
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/style/val.less:
--------------------------------------------------------------------------------
1 | /* 项目名 */
2 | @project-prefix: dw-react-web;
3 |
4 | @main-prefix: ~"@{project-prefix}-main";
5 | @home-prefix: ~"@{project-prefix}-home";
6 | @music-prefix: ~"@{project-prefix}-music";
7 | @blog-prefix: ~"@{project-prefix}-blog";
8 | @setting-prefix: ~"@{project-prefix}-setting";
9 |
10 |
11 | @nav-height: 64px;
12 |
13 | @container-width: 768px;
14 | @container-music-width: 1080px;
15 | @container-music-list-width: 1080px;
16 |
17 | @z-index-nav: 28;
18 |
19 | @raduis: 8px;
20 |
21 | @side-warp-width: 280px;
22 |
23 | // z-index
24 | @sider-warp-z-index: 118;
25 | @nav-z-index: 28;
26 | @notice-z-index: 50;
27 | @audio-z-index: 120;
28 | @audio-lyric-index: 119;
29 | @rank-type-list-z-index: 29;
30 |
31 | // component 颜色
32 | @primary-color: #32adff;
33 | @primary-color-low: #e1f1fb;
34 | @primary-color-deep: #03111b;
35 |
36 | @error-color: #cc5555;
37 | @error-color-low: #f8e7e7;
38 | @error-color-deep: #1d0808;
39 |
40 | @warning-color: #FFCC33;
41 | @warning-color-low: #fbf4da;
42 | @warning-color-deep: #1d1706;
43 |
44 | @success-color: #69d38c;
45 | @success-color-low: #e1fae9;
46 | @success-color-deep: #080808;
47 |
48 | @default-color: #aaaaaa;
49 | @default-color-low: #f0efef;
50 | @default-color-deep: #2f2f2f;
51 |
52 |
--------------------------------------------------------------------------------
/src/pages/Music/SheetDetail/sheet-detail.less:
--------------------------------------------------------------------------------
1 | .@{project-prefix}-music-sheet-detail {
2 | &-header {
3 | width: 100%;
4 | height: auto;
5 | display: flex;
6 | align-items: flex-start;
7 | justify-content: flex-start;
8 | padding: 20px 0 60px 0;
9 | &-avatar {
10 | flex: 0 0 200px;
11 | width: 200px;
12 | height: 200px;
13 | margin-right: 60px;
14 | border-radius: 10px;
15 | overflow: hidden;
16 | }
17 | &-desc {
18 | flex: 1 1 auto;
19 | overflow: hidden;
20 | .title {
21 | text-overflow: ellipsis;
22 | overflow: hidden;
23 | white-space: nowrap;
24 | margin-top: 0;
25 | color: var(--text-color);
26 | }
27 | .info {
28 | width: 100%;
29 | .lineclamp(66px, 2);
30 | }
31 |
32 | .count {
33 | margin-top: 20px;
34 | font-size: 14px;
35 | color: var(--text-color);
36 | span {
37 | &:last-child {
38 | margin-left: 20px;
39 | }
40 | }
41 | }
42 | }
43 | .close-area {
44 | position: absolute;
45 | top: 52px;
46 | padding: 10px;
47 | right: 10px;
48 | cursor: pointer;
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/src/components/Switch/switch.less:
--------------------------------------------------------------------------------
1 | @switch-w: 52px;
2 | @switch-h: 24px;
3 | @range-w-h: 20px;
4 | @switch-trans: @switch-w - @switch-h;
5 |
6 | .@{project-prefix}-comp-switch {
7 | display: flex;
8 | align-items: center;
9 | justify-content: flex-end;
10 | position: relative;
11 | width: @switch-w;
12 | height: @switch-h;
13 | border-radius: @switch-h / 2;
14 | padding: 2px 8px;
15 | box-sizing: border-box;
16 | background: var(--text-color);
17 | transition: background-color 0.3s;
18 | user-select: none;
19 | overflow: hidden;
20 | cursor: pointer;
21 | .switch-range {
22 | position: absolute;
23 | width: @range-w-h;
24 | height: @range-w-h;
25 | top: (@switch-h - @range-w-h) / 2;
26 | left: (@switch-h - @range-w-h) / 2;
27 | transform: translate(0, 0);
28 | border-radius: 50%;
29 | background: #fff;
30 | transition: transform 0.3s;
31 | }
32 |
33 | .switch-on, .switch-off {
34 | color: #fff;
35 | vertical-align: middle;
36 | text-align: center;
37 | font-size: 14px;
38 | }
39 |
40 | &.switch-checked {
41 | justify-content: flex-start;
42 | background: var(--primary-color);
43 | .switch-range {
44 | transform: translate(@switch-trans, 0);
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/src/api/api.ts:
--------------------------------------------------------------------------------
1 | export const selfBaseUrl = '//www.daiwei.site/php/server-php'
2 | export const musicBaseUrl = '//www.daiwei.site/netease'
3 | export default {
4 | MUSIC: {
5 | /** 歌单列表 */
6 | SHEET_LISTS_BY_CAT: `${musicBaseUrl}/top/playlist`,
7 |
8 | /** 音频搜索 */
9 | MUSIC_SEARCH: `${musicBaseUrl}/search`,
10 |
11 | /** 获取视频播放地址 */
12 | MUSIC_PLAY_URL_BY_ID: `${musicBaseUrl}/video/url`,
13 |
14 | /** 获取排行榜数据 */
15 | MUSIC_TOP_LIST: `${musicBaseUrl}/top/list`,
16 |
17 | /** 根据id获取音乐详情 */
18 | MUSIC_DETAIL_BY_ID: `${musicBaseUrl}/song/detail`,
19 |
20 | /** 根据id获取音乐Url */
21 | MUSIC_URL_BY_ID: `${musicBaseUrl}/song/url`,
22 |
23 | /** 校验音频是否可以播放 */
24 | MUSIC_CHECK: `${musicBaseUrl}/check/music`,
25 |
26 | /** 获取歌单详情 */
27 | MUSIC_SHEET_DETAIL: `${musicBaseUrl}/playlist/detail`,
28 |
29 | /** 歌词 */
30 | LYRIC: `${musicBaseUrl}/lyric`
31 | },
32 | BLOG: {
33 | list: `${selfBaseUrl}/article/index.php?name=getArticleLists`,
34 | tags: `${selfBaseUrl}/article/index.php`,
35 | detail: `${selfBaseUrl}/article/index.php?name=getArticleDetail`,
36 | pv: `${selfBaseUrl}/article/index.php?name=changePv`,
37 | },
38 | OTHER: {
39 | links: `${selfBaseUrl}/other/index.php`
40 | }
41 | }
--------------------------------------------------------------------------------
/src/components/Notice/index.ts:
--------------------------------------------------------------------------------
1 | import Message from './Message'
2 | import { NoticeType } from './Notice'
3 |
4 | const notice = (type: NoticeType,
5 | text: string | number,
6 | duration: number = 3000,
7 | onClose?: () => void) => {
8 | return Message.addMessage({
9 | type,
10 | text,
11 | duration,
12 | onClose,
13 | })
14 | }
15 |
16 | const fn = {
17 | info (text: string | number, duration: number = 3000, onClose?: () => void) {
18 | return notice(NoticeType.INFO, text, duration, onClose)
19 | },
20 |
21 | success (text: string | number, duration: number = 3000, onClose?: () => void) {
22 | return notice(NoticeType.SUCCESS, text, duration, onClose)
23 | },
24 |
25 | error (text: string | number, duration: number = 3000, onClose?: () => void) {
26 | return notice(NoticeType.ERROR, text, duration, onClose)
27 | },
28 |
29 | warning (text: string | number, duration: number = 3000, onClose?: () => void) {
30 | return notice(NoticeType.WARNING, text, duration, onClose)
31 | },
32 |
33 | default (text: string | number, duration: number = 3000, onClose?: () => void) {
34 | return notice(NoticeType.DEFAULT, text, duration, onClose)
35 | }
36 | }
37 |
38 | export default fn
39 |
--------------------------------------------------------------------------------
/src/config/nav.ts:
--------------------------------------------------------------------------------
1 | import { NAV_TYPE } from './../enum'
2 | export interface INavList {
3 | link: string
4 | name: string
5 | type: NAV_TYPE
6 | key: string
7 | }
8 |
9 | export type INavLists = INavList[]
10 |
11 | export const HomeNav: INavLists = [
12 | {
13 | name: '首页',
14 | link: '/home',
15 | type: NAV_TYPE.HOME,
16 | key: 'home_index',
17 | },
18 | {
19 | name: '博客',
20 | link: '/blog',
21 | type: NAV_TYPE.HOME,
22 | key: 'blog_index',
23 | }, {
24 | name: '音乐',
25 | link: '/music/search',
26 | type: NAV_TYPE.MUSIC,
27 | key: 'music_search',
28 | }, {
29 | name: '设置',
30 | link: '/setting',
31 | type: NAV_TYPE.HOME,
32 | key: 'setting_index',
33 | }
34 | ]
35 |
36 | export const MusicNav: INavLists = [
37 | {
38 | name: '去首页',
39 | link: '/home',
40 | type: NAV_TYPE.HOME,
41 | key: 'home_index',
42 | }, {
43 | name: '搜索',
44 | link: '/music/search',
45 | type: NAV_TYPE.MUSIC,
46 | key: 'music_search',
47 | }, {
48 | name: '排行榜',
49 | link: '/music/rank',
50 | type: NAV_TYPE.MUSIC,
51 | key: 'music_rank',
52 | }, {
53 | name: '专辑列表',
54 | link: '/music/sheet',
55 | type: NAV_TYPE.MUSIC,
56 | key: 'music_sheet',
57 | }, {
58 | name: '设置',
59 | link: '/setting',
60 | type: NAV_TYPE.MUSIC,
61 | key: 'setting_index',
62 | }
63 | ]
--------------------------------------------------------------------------------
/src/components/Notice/notice.less:
--------------------------------------------------------------------------------
1 | .@{project-prefix}-comp-notice {
2 | padding: 10px 15px;
3 | border-radius: 6px;
4 | margin-bottom: 20px;
5 | overflow: hidden;
6 | &.notice-success {
7 | color: @success-color;
8 | &.light {
9 | background: @success-color-low;
10 | }
11 | &.dark {
12 | color: darken(@success-color, 26%);
13 | background: @success-color-deep;
14 | }
15 | }
16 |
17 | &.notice-error {
18 | color: @error-color;
19 | &.light {
20 | background: @error-color-low;
21 | }
22 | &.dark {
23 | color: darken(@error-color, 26%);
24 | background: @error-color-deep;
25 | }
26 | }
27 |
28 | &.notice-warning {
29 | color: @warning-color;
30 | &.light {
31 | background: @warning-color-low;
32 | }
33 | &.dark {
34 | color: darken(@warning-color, 26%);
35 | background: @warning-color-deep;
36 | }
37 | }
38 |
39 | &.notice-info {
40 | color: @primary-color;
41 | &.light {
42 | background: @primary-color-low;
43 | }
44 | &.dark {
45 | color: darken(@primary-color, 26%);
46 | background: @primary-color-deep;
47 | }
48 | }
49 |
50 | &.notice-default {
51 | color: @default-color;
52 | &.light {
53 | background: @default-color-low;
54 | }
55 | &.dark {
56 | color: darken(@default-color, 26%);
57 | background: @default-color-deep;
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/src/components/BlogList/blog-list.less:
--------------------------------------------------------------------------------
1 | .@{project-prefix}-comp-blog-list {
2 | list-style: none;
3 | padding: 0 15px;
4 | a {
5 | text-decoration: none;
6 | padding: 15px 0;
7 | display: block;
8 | border-bottom: 1px solid var(--border-color);
9 | transition: border 0.3s;
10 | }
11 | &:hover {
12 | a {
13 | border-bottom: 1px solid var(--mask-bg);
14 | .dw-react-web-comp-blog-list-title {
15 | text-decoration: underline;
16 | }
17 | }
18 | }
19 | &-disc, &-date {
20 | color: #999;
21 | }
22 | &:hover {
23 | &-disc, &-date {
24 | .mixinTextColorActive();
25 | }
26 | }
27 | &-title {
28 | font-weight: 400;
29 | color: var(--primary-color);
30 | margin: 0;
31 | padding: 5px 0;
32 | }
33 | &-disc, &-date {
34 | font-size: 14px;
35 | margin: 8px 0;
36 | }
37 |
38 | &-disc {
39 | text-indent: 2em;
40 | }
41 |
42 | &-conf {
43 | display: flex;
44 | align-items: center;
45 | justify-content: flex-start;
46 | color: #999;
47 | }
48 | &-date {
49 | display: flex;
50 | align-items: center;
51 | justify-content: flex-start;
52 | .c-date {
53 | margin-left: 15px;
54 | }
55 | }
56 | &-read, &-cat, &-tag {
57 | font-size: 14px;
58 | margin-left: 25px;
59 | }
60 |
61 | .cat-list, .tag-list {
62 | margin-right: 5px;
63 | &:last-child {
64 | margin-right: 0;
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/src/components/SiderWarp/sider-warp.less:
--------------------------------------------------------------------------------
1 | .@{project-prefix}-comp-sider-warp {
2 | &.fixed {
3 | .mask {
4 | position: absolute;
5 | top: 0;
6 | left: 0;
7 | right: 0;
8 | bottom: 0;
9 | background: var(--mask-bg);
10 | opacity: 0;
11 | visibility: hidden;
12 | transition: opacity, visibility 0.3s;
13 | z-index: @sider-warp-z-index;
14 | }
15 | }
16 | &.auto {
17 | .mask {
18 | display: none;
19 | }
20 | }
21 | .content {
22 | position: absolute;
23 | width: @side-warp-width;
24 | right: 0;
25 | transform: translate3d(100%, 0, 0);
26 | background: var(--bg);
27 | border: -10px 10px 10px 0 var(--border-color);
28 | top: 0;
29 | bottom: 0;
30 | transition: transform 0.3s;
31 | z-index: @sider-warp-z-index + 1;
32 | padding: 15px;
33 | box-sizing: border-box;
34 | &-switch {
35 | position: fixed;
36 | top: 10px;
37 | left: -60px;
38 | width: 30px;
39 | height: 30px;
40 | cursor: pointer;
41 | z-index: @sider-warp-z-index + 1;
42 | }
43 | }
44 | &.auto {
45 | .content {
46 | top: @nav-height;
47 | z-index: @nav-z-index - 1;
48 | }
49 | }
50 | &.show {
51 | &.fixed {
52 | .mask {
53 | opacity: 1;
54 | visibility: visible;
55 | }
56 | }
57 | .content {
58 | transform: translate3d(0, 0, 0);
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/assets/svg/cat.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/loadable.ts:
--------------------------------------------------------------------------------
1 | import Loadable from 'react-loadable';
2 | import Loading from './components/Loading'
3 |
4 | export const Home = Loadable({
5 | loader: () => import('./pages/Home'),
6 | loading: Loading
7 | })
8 |
9 | export const BlogLists = Loadable({
10 | loader: () => import('./pages/Blog/Lists'),
11 | loading: Loading
12 | })
13 |
14 | export const Blog = Loadable({
15 | loader: () => import('./pages/Blog'),
16 | loading: Loading
17 | })
18 |
19 | export const BlogDetail = Loadable({
20 | loader: () => import('./pages/Blog/Detail'),
21 | loading: Loading
22 | })
23 |
24 | export const Setting = Loadable({
25 | loader: () => import('./pages/Setting'),
26 | loading: Loading
27 | })
28 |
29 | export const Nav = Loadable({
30 | loader: () => import('./components/Nav'),
31 | loading: Loading
32 | })
33 |
34 | export const Music = Loadable({
35 | loader: () => import('./pages/Music/index'),
36 | loading: Loading
37 | })
38 |
39 | export const MusicSheet = Loadable({
40 | loader: () => import('./pages/Music/Sheet'),
41 | loading: Loading
42 | })
43 |
44 | export const MusicRank = Loadable({
45 | loader: () => import('./pages/Music/Rank'),
46 | loading: Loading
47 | })
48 |
49 | export const MusicSearch = Loadable({
50 | loader: () => import('./pages/Music/Search'),
51 | loading: Loading
52 | })
53 |
54 | export const MusicSheetDetail = Loadable({
55 | loader: () => import('./pages/Music/SheetDetail'),
56 | loading: Loading
57 | })
58 |
59 | export const InterLink = Loadable({
60 | loader: () => import('./pages/InterLink'),
61 | loading: Loading
62 | })
63 |
--------------------------------------------------------------------------------
/src/components/Nav/index.tsx:
--------------------------------------------------------------------------------
1 | import { NavLink } from 'react-router-dom'
2 | import { INavLists, INavList } from './../../config/nav'
3 | import classNames from 'classnames'
4 | import './nav.less'
5 | import store from './../../store'
6 | import { PROJECT_NAME, WEBSITE_TITLE } from './../../config/constance'
7 | import React, { useState, useEffect } from 'react'
8 | import { observer, useObservable, useObserver, useLocalStore, useStaticRendering, useComputed } from "mobx-react-lite"
9 | import { useStore } from './../../utils/use'
10 | import Icon from './../Icon'
11 |
12 | interface INavProps {
13 | location?: any
14 | }
15 |
16 | const Nav = observer((props: INavProps) => {
17 | const nav = useStore().navStore
18 | const lists = nav.lists
19 |
20 | const classString = classNames({
21 | [`${PROJECT_NAME}-nav`]: true
22 | })
23 |
24 | return (
25 |
46 | )
47 | })
48 |
49 | export default Nav
50 |
--------------------------------------------------------------------------------
/src/components/SheetGroup/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import classNames from 'classnames'
3 | import { PROJECT_NAME, MUSIC_SHEET_DEFAULT_LIMIT } from '../../config/constance'
4 | import { CSSTransition, TransitionGroup } from 'react-transition-group';
5 | import SheetList from './../SheetList'
6 | import { SheetGroupLists, SheetGroupList } from './../../type'
7 | import './sheet-group.less'
8 |
9 | interface ISheetGroup {
10 | lists: SheetGroupLists;
11 | history: any;
12 | }
13 |
14 | const SheetGroup = (props: ISheetGroup) => {
15 | const classString = classNames({
16 | [`${PROJECT_NAME}-sheet-group`]: true,
17 | })
18 |
19 | const [start, setStart] = useState(false)
20 |
21 | useEffect(() => {
22 | }, [props.lists])
23 |
24 | return (
25 |
26 |
27 | {
28 | props.lists.map((item: any, index: number) => (
29 |
40 |
41 |
42 | ))
43 | }
44 |
45 |
46 | )
47 | }
48 |
49 | export default SheetGroup
50 |
--------------------------------------------------------------------------------
/src/style/high-custom.less:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Atom One Dark by Daniel Gamage
4 | Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
5 |
6 | base: #282c34
7 | mono-1: #abb2bf
8 | mono-2: #818896
9 | mono-3: #5c6370
10 | hue-1: #56b6c2
11 | hue-2: #61aeee
12 | hue-3: #c678dd
13 | hue-4: #98c379
14 | hue-5: #e06c75
15 | hue-5-2: #be5046
16 | hue-6: #d19a66
17 | hue-6-2: #e6c07b
18 |
19 | */
20 |
21 | .hljs {
22 | display: block;
23 | overflow-x: auto;
24 | padding: 0.5em;
25 | color: #abb2bf;
26 | background: #282c34;
27 | }
28 |
29 | .hljs-comment,
30 | .hljs-quote {
31 | color: #5c6370;
32 | font-style: italic;
33 | }
34 |
35 | .hljs-doctag,
36 | .hljs-keyword,
37 | .hljs-formula {
38 | color: #c678dd;
39 | }
40 |
41 | .hljs-section,
42 | .hljs-name,
43 | .hljs-selector-tag,
44 | .hljs-deletion,
45 | .hljs-subst {
46 | color: #e06c75;
47 | }
48 |
49 | .hljs-literal {
50 | color: #56b6c2;
51 | }
52 |
53 | .hljs-string,
54 | .hljs-regexp,
55 | .hljs-addition,
56 | .hljs-attribute,
57 | .hljs-meta-string {
58 | color: #98c379;
59 | }
60 |
61 | .hljs-built_in,
62 | .hljs-class .hljs-title {
63 | color: #e6c07b;
64 | }
65 |
66 | .hljs-attr,
67 | .hljs-variable,
68 | .hljs-template-variable,
69 | .hljs-type,
70 | .hljs-selector-class,
71 | .hljs-selector-attr,
72 | .hljs-selector-pseudo,
73 | .hljs-number {
74 | color: #d19a66;
75 | }
76 |
77 | .hljs-symbol,
78 | .hljs-bullet,
79 | .hljs-link,
80 | .hljs-meta,
81 | .hljs-selector-id,
82 | .hljs-title {
83 | color: #61aeee;
84 | }
85 |
86 | .hljs-emphasis {
87 | font-style: italic;
88 | }
89 |
90 | .hljs-strong {
91 | font-weight: bold;
92 | }
93 |
94 | .hljs-link {
95 | text-decoration: underline;
96 | }
97 |
--------------------------------------------------------------------------------
/src/components/Icon/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react'
2 | import classNames from 'classnames'
3 | import { PROJECT_NAME, SVG_DEFAULT_COLOR } from './../../config/constance'
4 | import './icon.less'
5 |
6 | interface IIconProps {
7 | svgId: string;
8 | size?: number;
9 | color?: string;
10 | hoverColor?: string;
11 | activeColor?: string;
12 | active?: boolean;
13 | }
14 |
15 | const Icon: React.FC = (props) => {
16 | const [color, setColor] = useState(props.active ? props.activeColor : (props.color || SVG_DEFAULT_COLOR))
17 |
18 | const svgStyle: any = {
19 | 'width': `${ props.size ? props.size : 16 }px`,
20 | 'height': `${ props.size ? props.size : 16 }px`,
21 | 'marginRight': props.children ? '5px' : '0'
22 | }
23 |
24 | const classString = classNames({
25 | [`${PROJECT_NAME}-comp-icon`]: true
26 | })
27 |
28 | const setHover = (): boolean => {
29 | if (props.hoverColor) {
30 | setColor((color: any) => color = props.hoverColor)
31 | return true
32 | }
33 | return false
34 | }
35 |
36 | const setDefault = (): boolean => {
37 | if (props.hoverColor) {
38 | setColor((color: any) => color = props.color || SVG_DEFAULT_COLOR)
39 | return true
40 | }
41 | return false
42 | }
43 |
44 | const mouseEnter = () => {
45 | setHover()
46 | }
47 |
48 | const mouseLeave = () => {
49 | setDefault()
50 | }
51 |
52 | return (
53 |
54 |
57 | {props.children}
58 |
59 | )
60 | }
61 |
62 | export default Icon
63 |
--------------------------------------------------------------------------------
/src/components/Lyric/lyric.less:
--------------------------------------------------------------------------------
1 | @lyric_h: 50px;
2 | .@{project-prefix}-lyric-comp {
3 | position: fixed;
4 | bottom: 20px;
5 | height: @lyric_h;
6 | z-index: @audio-lyric-index;
7 | width: 400px;
8 | left: 50%;
9 | right: 50%;
10 | transform: translate3d(-50%, 0, 0);
11 | border-radius: 4px;
12 | display: flex;
13 | align-items: center;
14 | justify-content: flex-start;
15 | opacity: 0;
16 | visibility: visible;
17 | transition: opacity 0.3s;
18 | &.show {
19 | opacity: 1;
20 | visibility: visible;
21 | }
22 | &-close {
23 | flex: 0 0 @lyric_h * 1.5;
24 | height: @lyric_h;
25 | display: flex;
26 | align-items: center;
27 | justify-content: center;
28 | cursor: pointer;
29 | transition: all 0.3s;
30 | font-size: 12px;
31 | }
32 | &-content {
33 | flex: 1 1 auto;
34 | overflow: hidden;
35 | padding: 0 10px 0 40px;
36 | &-list {
37 | text-overflow: ellipsis;
38 | white-space: nowrap;
39 | overflow: hidden;
40 | color: var(--text-color);
41 | }
42 | }
43 | }
44 |
45 | body.light {
46 | .@{project-prefix}-lyric-comp {
47 | background: var(--bg-o);
48 | box-shadow: 0 0 30px 0 var(--shadow-color);
49 | &-content {
50 | &-list {
51 | color: var(--text-color);
52 | }
53 | }
54 | &-close {
55 | color: var(--text-color);
56 | &:hover {
57 | background: var(--shadow-color);
58 | }
59 | }
60 | }
61 | }
62 |
63 | body.dark {
64 | .@{project-prefix}-lyric-comp {
65 | background: var(--bg-o);
66 | box-shadow: 0 0 30px 0 var(--shadow-color);
67 | &-content {
68 | &-list {
69 | color: var(--text-color);
70 | }
71 | }
72 | &-close {
73 | color: var(--text-color);
74 | &:hover {
75 | background: var(--shadow-color);
76 | }
77 | }
78 | }
79 | }
80 |
81 |
--------------------------------------------------------------------------------
/src/components/MusicList/music-list.less:
--------------------------------------------------------------------------------
1 | @config_music_list_w: 120px;
2 | .@{project-prefix}-music-list {
3 | display: flex;
4 | align-items: center;
5 | justify-content: flex-start;
6 | font-size: 14px;
7 | font-weight: 400;
8 | color: var(--text-color);
9 | width: 100%;
10 | padding: 15px;
11 | border-bottom: 1px solid var(--border-color);
12 | &-name, &-artists, &-durantion, &-album {
13 | font-size: 14px;
14 | font-weight: 400;
15 | overflow: hidden;
16 | text-overflow: ellipsis;
17 | white-space: nowrap;
18 | margin-right: 20px;
19 | }
20 | &-name {
21 | flex: 0 0 400px;
22 | margin: 0;
23 | padding-right: @config_music_list_w;
24 | position: relative;
25 | .config {
26 | position: absolute;
27 | top: 0;
28 | right: 0;
29 | bottom: 0;
30 | display: none;
31 | width: @config_music_list_w;
32 | }
33 | }
34 | &-artists {
35 | flex: 1 1 auto;
36 | margin: 0;
37 | &-list{
38 | cursor: pointer;
39 | &::after {
40 | cursor: default;
41 | content: '/';
42 | margin: 0 5px;
43 | }
44 | &:last-child {
45 | &::after {
46 | display: none;
47 | }
48 | }
49 | }
50 | }
51 | &-durantion {
52 | flex: 0 0 120px;
53 | text-align: right;
54 | margin: 0;
55 | margin-right: 0;
56 | }
57 | &-album {
58 | flex: 0 0 200px;
59 | text-align: left;
60 | margin: 0;
61 | }
62 | &.play {
63 | color: var(--primary-color);
64 | }
65 |
66 | &:hover {
67 | .name {
68 | .config {
69 | display: flex;
70 | span, a {
71 | cursor: pointer;
72 | &:hover {
73 | color: var(--text-color-active);
74 | }
75 | }
76 | .download {
77 | margin-left: 15px;
78 | }
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/Switch/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState, useEffect } from 'react'
2 | import classNames from 'classnames'
3 | import { PROJECT_NAME } from './../../config/constance'
4 | import { CSSTransition, TransitionGroup } from 'react-transition-group';
5 | import './switch.less'
6 |
7 | interface ISwitchProps {
8 | checked: boolean
9 | unCheckedName: string
10 | checkedName: string
11 | onChange: (checked: boolean) => void
12 | }
13 |
14 | const Switch = (props: ISwitchProps) => {
15 | const [checked, setChecked] = useState(props.checked)
16 |
17 | useEffect(() => {
18 | setChecked(props.checked);
19 | }, [props.checked])
20 |
21 | const setCheckedHandle = () => {
22 | props.onChange && props.onChange(!checked)
23 | setChecked(checked => !checked)
24 | }
25 |
26 | const classString = classNames({
27 | [`${PROJECT_NAME}-comp-switch`]: true,
28 | [`switch-checked`]: !!checked
29 | })
30 |
31 | return (
32 |
33 |
38 | {props.checkedName}
39 |
40 |
45 | {props.unCheckedName}
46 |
47 |
48 |
49 | )
50 | }
51 |
52 | Switch.defaultProps = {
53 | checked: false,
54 | unCheckedName: 'off',
55 | checkedName: 'on',
56 | onChange: null,
57 | }
58 |
59 | export default Switch
60 |
--------------------------------------------------------------------------------
/src/pages/Music/action.ts:
--------------------------------------------------------------------------------
1 | import Http from '../../utils/fetch'
2 | import API from '../../api/api'
3 | import {
4 | MUSIC_SHEET_DEFAULT_LIMIT,
5 | MUSIC_SEARCH_DEFAULT_LIMIT,
6 | } from '../../config/constance'
7 | // 歌单信息
8 | export const getSheetLists = async (cat: string = '', offset: number = 0, limit: number = MUSIC_SHEET_DEFAULT_LIMIT) => {
9 | const data = await Http.get(API.MUSIC.SHEET_LISTS_BY_CAT, {
10 | cat,
11 | offset,
12 | limit,
13 | })
14 | return data
15 | }
16 |
17 | // 歌曲搜索
18 | export const getSearchLists = async (keywords: string, offset: number = 0, limit: number = MUSIC_SEARCH_DEFAULT_LIMIT) => {
19 | const data = await Http.get(API.MUSIC.MUSIC_SEARCH, {
20 | keywords,
21 | offset,
22 | limit,
23 | })
24 | return data
25 | }
26 |
27 | // 歌曲排行列表
28 | export const getRankLists = async (idx: string = '1', offset: number = 0, limit: number = MUSIC_SEARCH_DEFAULT_LIMIT) => {
29 | const data = await Http.get(API.MUSIC.MUSIC_TOP_LIST, {
30 | idx,
31 | offset,
32 | limit,
33 | })
34 | return data
35 | }
36 |
37 | // 获取歌曲的详细信息
38 | export const getMusicDetailById = async (id: number) => {
39 | const data = await Http.get(API.MUSIC.MUSIC_DETAIL_BY_ID, {
40 | ids: id
41 | })
42 | return data
43 | }
44 |
45 | // 获取歌曲的URL
46 | export const getMusicUrlById = async (id: number) => {
47 | const data = await Http.get(API.MUSIC.MUSIC_PLAY_URL_BY_ID, {
48 | id
49 | })
50 | return data
51 | }
52 |
53 | // 校验音乐是否可以播放
54 | export const checkMusicById = async (id: number) => {
55 | const data = await Http.get(API.MUSIC.MUSIC_CHECK, {
56 | id
57 | })
58 | return data
59 | }
60 |
61 | // 根据id获取音乐歌词
62 | export const musicLyricById = async (id: number) => {
63 | const data = await Http.get(API.MUSIC.LYRIC, {
64 | id
65 | })
66 | return data
67 | }
68 |
69 | // 根据id获取歌单详情
70 | export const sheetDetailById = async (id: number) => {
71 | const data = await Http.get(API.MUSIC.MUSIC_SHEET_DETAIL, {
72 | id
73 | })
74 | return data
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/LazyImg/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react'
2 | import classNames from 'classnames'
3 | import { PROJECT_NAME } from './../../config/constance'
4 | import { IAMGE_LOAD_STATUS } from './../../enum'
5 | import './lazy-img.less'
6 |
7 | interface lazyImgProps {
8 | selfClassName?: string;
9 | src: string;
10 | }
11 |
12 | const LazyImg = (props: lazyImgProps) => {
13 | const imgRef: any = useRef(null)
14 | let [imageStatus, setImageStatus] = useState(IAMGE_LOAD_STATUS.LOADING)
15 |
16 | const classString = classNames({
17 | [`${PROJECT_NAME}-comp-lazy-img`]: true,
18 | [props.selfClassName || '']: true
19 | })
20 |
21 | const OnLoadHandler = () => {
22 | setImageStatus(() => imageStatus = IAMGE_LOAD_STATUS.SUCCES)
23 | imgRef.current.src = props.src
24 | }
25 |
26 | const OnErrorHandler = () => {
27 | setImageStatus(() => imageStatus = IAMGE_LOAD_STATUS.SUCCES)
28 | }
29 |
30 | const pic = props.src
31 | const img = new Image()
32 | img.crossOrigin = 'anonymous'
33 | img.src = pic
34 |
35 | const initImageEvent = () => {
36 | img.addEventListener('load', OnLoadHandler)
37 | img.addEventListener('error', OnErrorHandler)
38 | }
39 |
40 | const destoryImageEvent = () => {
41 | img.removeEventListener('load', OnLoadHandler)
42 | img.removeEventListener('error', OnErrorHandler)
43 | }
44 |
45 | useEffect(() => {
46 | initImageEvent()
47 |
48 | return () => {
49 | destoryImageEvent()
50 | }
51 | }, [imageStatus, img])
52 |
53 | return (
54 |
55 | {
56 | imageStatus === IAMGE_LOAD_STATUS.LOADING ? (
57 |
图片加载中
58 | ) : imageStatus === IAMGE_LOAD_STATUS.ERRER ? (
59 |
图片加载失败
60 | ) : imageStatus === IAMGE_LOAD_STATUS.SUCCES ? (
61 |
![]()
63 | ) : null
64 | }
65 |
66 | )
67 | }
68 |
69 | export default LazyImg
70 |
--------------------------------------------------------------------------------
/src/pages/InterLink/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { PROJECT_NAME, INTER_LINK_DEFAULT_LIMIT } from "./../../config/constance";
3 | import classNames from 'classnames'
4 | import './inter-link.less'
5 | import { getLinkLists } from './action'
6 | import Notice from './../../components/Notice'
7 | import { CSSTransition, TransitionGroup } from 'react-transition-group';
8 |
9 | interface ILinkProps {
10 | id: string;
11 | name: string;
12 | disc: string;
13 | url: string;
14 | isShow: string;
15 | }
16 |
17 | const InterLink: React.FC = () => {
18 | const classString = classNames({
19 | [`${PROJECT_NAME}-interlink`]: true,
20 | [`dw-page-router`]: true
21 | })
22 |
23 | const [lists, setLists] = useState([])
24 |
25 | useEffect(() => {
26 | const fn = async () => {
27 | const res: any = await getLinkLists()
28 | setLists(lists => lists = res.data)
29 | }
30 | fn()
31 | Notice.default('欢迎互换友链!!!')
32 | }, []);
33 |
34 | return (
35 |
36 |
37 | {
38 | lists.map((item: ILinkProps, index: number) => (
39 |
50 |
53 |
54 | ))
55 | }
56 |
57 |
58 | )
59 | }
60 |
61 | export default InterLink
62 |
--------------------------------------------------------------------------------
/src/store/types.ts:
--------------------------------------------------------------------------------
1 | import { INavLists } from './../config/nav'
2 | import { MusicGroupList, MusicGroupLists } from './../type'
3 | import { DAudioPosition } from './../components/DAudio'
4 |
5 | export interface IStore {
6 | colorStore: NameSpaceStore.IColorModel;
7 | navStore: NameSpaceStore.INavModel;
8 | musicStore: NameSpaceStore.IMusicModel;
9 | }
10 |
11 | export enum MusicPlayType {
12 | NO_TYPE,
13 | SEARCH,
14 | RANK,
15 | SHEET,
16 | HOME
17 | }
18 |
19 | export enum MusicLyricType {
20 | /** 关闭歌词显示 */
21 | CLOSE_LYRIC,
22 |
23 | /** 加载中。。。 */
24 | LOADING,
25 | /** 有歌词 */
26 | HAS_LYRIC,
27 | /** 纯音乐 */
28 | ABSOLUTE,
29 | /** 暂无歌词 */
30 | NOT_FOUND
31 | }
32 |
33 | interface ILrc {
34 | lyric: string | null;
35 | version: number;
36 | }
37 |
38 | export interface IMusicLyric {
39 | code: number;
40 | klyric?: ILrc;
41 | tlyric?: ILrc;
42 | lrc?: ILrc;
43 | qfy?: boolean;
44 | sfy?: boolean;
45 | sgc?: boolean;
46 | needDesc?: boolean;
47 | nolyric?: boolean;
48 | objLrc: any[];
49 | lrcType: MusicLyricType;
50 | }
51 |
52 | export namespace NameSpaceStore {
53 | export interface IColorModel {
54 | mode: string;
55 | changeMode: (mode?: string) => void;
56 | }
57 |
58 | export interface INavModel {
59 | lists: INavLists,
60 | setNavLists: (type: number) => void
61 | }
62 |
63 | export interface IMusicModel {
64 | currentList: MusicGroupList;
65 | musicListQueue: MusicGroupLists;
66 | playType: MusicPlayType;
67 | musicPlayIndex: number;
68 | changePosition: (position: DAudioPosition) => void;
69 | setMusicListQueue: (lists: MusicGroupLists) => void;
70 | setMusicPlayIndex: (index: number) => void;
71 |
72 | setPlayType: (type: MusicPlayType) => void;
73 | setMusicPlayTask: (lists: MusicGroupLists,
74 | index: number,
75 | type: MusicPlayType) => void;
76 | musicLyric: IMusicLyric;
77 | setMusicLyric: (lyric: IMusicLyric) => void;
78 | currentLyric: string;
79 | musicLyricIndex: number;
80 | lyricIsShow: boolean;
81 | setMusicLyricIndex: (index: number) => void;
82 | }
83 | }
84 |
85 |
--------------------------------------------------------------------------------
/src/components/Dialog/index.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react'
3 | import ReactDOM from 'react-dom';
4 | import classNames from 'classnames';
5 | import { PROJECT_NAME } from './../../config/constance'
6 | export interface IAlertOption {
7 | title?: string;
8 | desc?: string;
9 | btnText?: string;
10 | onClose?: () => void;
11 | onBtnClick?: () => void;
12 | }
13 |
14 | export default function alert (
15 | title = 'title',
16 | desc = 'desc',
17 | btnText = '确定',
18 | onClose = () => {},
19 | onBtnClick = () => {}
20 | ) {
21 | // console.log(title)
22 | // console.log(desc)
23 | // console.log(btnText)
24 | // console.log(onClose)
25 | // console.log(onBtnClick)
26 | const div: any = document.createElement('div');
27 | document.body.appendChild(div);
28 |
29 | function close () {
30 | onClose && onClose()
31 | ReactDOM.unmountComponentAtNode(div);
32 | if (div && div.parentNode) {
33 | div.parentNode.removeChild(div);
34 | }
35 | }
36 |
37 | function confirm () {
38 | onBtnClick && onBtnClick()
39 | ReactDOM.unmountComponentAtNode(div);
40 | if (div && div.parentNode) {
41 | div.parentNode.removeChild(div);
42 | }
43 | }
44 |
45 | function handleTouchMove (e: any) {
46 | e.preventDefault()
47 | e.stopPropagation()
48 | }
49 |
50 | const classString = classNames({
51 | [`${PROJECT_NAME}-comp-dialog`]: true
52 | })
53 | // const { title, btnText, desc } = props
54 |
55 | ReactDOM.render(
56 |
57 |
58 |
59 |
60 | {
61 | title ? (
{title}
) : null
62 | }
63 | {
64 | desc ? (
{desc}
) : null
65 | }
66 | {
67 | btnText ? (
{btnText}
) : null
68 | }
69 |
70 |
71 |
72 | , div)
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/MusicList/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo } from 'react'
2 | import classNames from 'classnames'
3 | import './music-list.less'
4 | import { MusicGroupList } from './../../type'
5 | import { PROJECT_NAME } from '../../config/constance'
6 | import { artist } from './../../type'
7 | import { parseDuraiton,
8 | getUrlById,
9 | getPlayMuiscList } from './../../utils/music'
10 | import DAudio from './../DAudio'
11 | import store from '../../store';
12 |
13 | interface IMusicListProps {
14 | list: MusicGroupList;
15 | index: number;
16 | addMusicQueue: (index: number) => void;
17 | memoPlay: boolean;
18 | }
19 |
20 | const MusicList = (props: IMusicListProps) => {
21 | const { index, list } = props
22 | const id = list.id
23 |
24 | const classString = classNames({
25 | [`play`]: props.memoPlay,
26 | [`${PROJECT_NAME}-music-list`]: true,
27 | })
28 | useEffect(() => {
29 | }, [props.list])
30 |
31 | const handlePlay = async () => {
32 | const list: MusicGroupList = props.list
33 | const musicList = await getPlayMuiscList(list)
34 |
35 | DAudio.start(musicList)
36 |
37 | // 添加播放列表
38 | props.addMusicQueue(props.index)
39 | }
40 | return (
41 |
42 |
43 | {props.list.name}
44 |
48 |
49 |
50 | {
51 | props.list.artists && props.list.artists.map((artist: artist, index: number) =>
52 | {artist.name}
53 | )
54 | }
55 |
56 |
{props.list.album.name}
57 |
{parseDuraiton(props.list.duration)}
58 |
59 | )
60 | }
61 |
62 | export default MusicList
63 |
--------------------------------------------------------------------------------
/src/components/Nav/nav.less:
--------------------------------------------------------------------------------
1 | .@{project-prefix}-nav {
2 | position: fixed;
3 | top: 0;
4 | left: 0;
5 | right: 0;
6 | height: @nav-height;
7 | z-index: @z-index-nav;
8 | overflow: hidden;
9 | // .mixinBgNav();
10 | background: transparent;
11 | box-shadow: 1px 1px 25px 0 var(--shadow-color);
12 | transition: background-color , color, box-shadow 0.3s;
13 | .nav-content {
14 | width: @container-width;
15 | margin: 0 auto;
16 | height: 100%;
17 | display: flex;
18 | align-items: center;
19 | justify-content: flex-start;
20 | z-index: 1;
21 | .nav-logo {
22 | flex: 0 0 150px;
23 | display: flex;
24 | align-items: center;
25 | justify-content: flex-start;
26 | .nav-img {
27 | height: 50px;
28 | border-radius: 50%;
29 | }
30 | .nav-title {
31 | cursor: default;
32 | font-size: 26px;
33 | .mixinPrimaryColor();
34 | font-weight: 400;
35 | transition: color 0.3s;
36 | }
37 | }
38 | .nav-lists {
39 | height: 100%;
40 | flex: 1 1 auto;
41 | display: flex;
42 | align-items: center;
43 | justify-content: flex-end;
44 | .nav-list {
45 | display: flex;
46 | align-items: center;
47 | height: 100%;
48 | padding: 0 15px;
49 | margin: 0 10px;
50 | text-decoration: none;
51 | position: relative;
52 | transition: all 0.3s;
53 | overflow: hidden;
54 | font-size: 14px;
55 | .mixinTextColor();
56 | transition: color, background-color 0.3s;
57 | &:hover {
58 | .mixinBgNavActive();
59 | }
60 | &::before {
61 | content: '';
62 | position: absolute;
63 | background: var(--primary-color);
64 | bottom: 5px;
65 | left: 50%;
66 | width: 0;
67 | height: 2px;
68 | border-radius: 4px;
69 | transition: all 0.3s;
70 | transform: translate(-50%, 400%);
71 | }
72 | &.active {
73 | .mixinTextColorActive();
74 | &::before {
75 | width: 50%;
76 | transform: translate(-50%, 0);
77 | }
78 | }
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/utils/typewriter.ts:
--------------------------------------------------------------------------------
1 | import { checkType } from 'd-utils/lib/storeUtils'
2 | import './../style/type-writer.less'
3 |
4 | interface ItyperConfig {
5 | interver?: number
6 | sleep?: number
7 | }
8 |
9 | interface IDomAttr {
10 | [propName: string]: any;
11 | }
12 |
13 | export interface ITypeWriterList {
14 | text: string;
15 | tagName: string;
16 | config: ItyperConfig;
17 | container?: Element;
18 | domAttr: IDomAttr;
19 | reactAttr: IDomAttr;
20 | }
21 |
22 | export function getAttrEntries (propName: string, obj: any): any[] {
23 | const val = obj[propName]
24 | const type = checkType(val)
25 | switch (checkType(val)) {
26 | case 'string':
27 | case 'function':
28 | return [type, val]
29 | case 'object':
30 | return [type, {...val}]
31 | default:
32 | return [type, val]
33 | }
34 | }
35 |
36 | export default function typeWriter (typeWriterList: ITypeWriterList): Promise {
37 | const {text, tagName, container, config, domAttr } = typeWriterList
38 | const el = document.createElement(tagName)
39 |
40 | Object.keys(domAttr).forEach(((item) => {
41 | const [type, prop] = getAttrEntries(item, domAttr)
42 | if (type === 'function') {
43 | el.addEventListener(item, prop)
44 | } else {
45 | el.setAttribute(item, prop)
46 | }
47 | }))
48 |
49 | const splitBar = document.createElement('span')
50 | splitBar.className = 'type-writer-bar'
51 |
52 | el.appendChild(splitBar)
53 |
54 | try {
55 | container && container.appendChild(el)
56 | } catch (e) {}
57 |
58 | let timer: any = null,
59 | currentIndex: number = 0,
60 | interver = config && config.interver || 100,
61 | sleep = config && config.sleep || 300
62 |
63 | return new Promise((resolve, reject) => {
64 | function next () {
65 | const textNode = document.createTextNode(text[currentIndex])
66 | el.insertBefore(textNode, splitBar)
67 | currentIndex++
68 | if (currentIndex < text.length) {
69 | timer = setTimeout(() => {
70 | next()
71 | }, interver)
72 | } else {
73 | timer = setTimeout(() => {
74 | // el.removeChild(splitBar)
75 | clearTimeout(timer)
76 | resolve()
77 | }, sleep)
78 | }
79 | }
80 | next()
81 | })
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/SiderWarp/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState, useEffect, useImperativeHandle } from 'react'
2 | import classNames from 'classnames'
3 | import { PROJECT_NAME } from './../../config/constance'
4 | import './sider-warp.less'
5 | import Icon from './../Icon'
6 |
7 | interface ISiderWarpProps {
8 | show: boolean;
9 | switchTop?: string;
10 | type?: string;
11 | hideFn?: () => void;
12 | showFn?: () => void;
13 | }
14 |
15 | const SiderWarp: React.FC = (props, ref) => {
16 | const [show, setShow] = useState(props.show)
17 |
18 | const classString = classNames({
19 | [`${PROJECT_NAME}-comp-sider-warp`]: true,
20 | [`show`]: show,
21 | ['fixed']: props.type && props.type === 'fixed',
22 | ['auto']: props.type && props.type !== 'fixed',
23 | })
24 |
25 | const styleTop = {
26 | top: props.switchTop ? props.switchTop : '50px'
27 | }
28 |
29 | const hideComp = () => {
30 | setShow((show) => show = false)
31 | props.hideFn && props.hideFn()
32 | }
33 |
34 | const showComp = () => {
35 | setShow((show) => show = true)
36 | props.showFn && props.showFn()
37 | }
38 |
39 | const toggleSiderWarp = () => {
40 | show ? props.hideFn && props.hideFn() : props.showFn && props.showFn()
41 | // if (!show) {
42 | // DomUtils.addClass(document.body, 'sider-s')
43 | // } else {
44 | // DomUtils.removeClass(document.body, 'sider-s')
45 | // }
46 | setShow((show) => show = !show)
47 | }
48 |
49 | const svgId = show ? 'close' : 'menu'
50 |
51 | const mapRef = useRef(null)
52 |
53 | useImperativeHandle(ref, () => {
54 | return {
55 | showComp,
56 | hideComp
57 | }
58 | })
59 |
60 | return (
61 |
62 |
63 |
64 |
65 |
66 |
67 | {props.children}
68 |
69 |
70 | )
71 | }
72 |
73 | // SiderWarp.defaultProps = {
74 | // show: false,
75 | // switchTop: '50px',
76 | // type: 'fixed'
77 | // }
78 |
79 | const SiderWarpComponent = React.forwardRef(SiderWarp)
80 |
81 | export default SiderWarpComponent as any
--------------------------------------------------------------------------------
/src/style/main.less:
--------------------------------------------------------------------------------
1 | // 这是body到其他路由的样式设置
2 | #@{project-prefix} {
3 | position: relative;
4 | }
5 |
6 | :root {
7 | --shadow-color: #f0f0f0;
8 | --border-color: rgba(33, 33, 33, 0.03);
9 | --primary-color: #158a4c;
10 | --text-color: #666;
11 | --text-color-active: #333;
12 | --bg-nav: #fff;
13 | --bg-nav-hover: #fafafa;
14 | --bg: #fff;
15 | }
16 |
17 | #dw-react-web-container.light {
18 | --auto-color: #aaa;
19 | --scroll-color: #f2f2f2;
20 | --shadow-color: #f0f0f0;
21 | --border-color: rgba(33, 33, 33, 0.03);
22 | --border-color-deep: rgba(33, 33, 33, 0.2);
23 | --primary-color: green;
24 | --text-color: #666;
25 | --text-color-active: #333;
26 | --bg-nav: #fff;
27 | --bg-nav-active: #fafafa;
28 | --mask-bg: rgba(244,244,244,0.3);
29 | --bg: #fff;
30 | --h-color: #333;
31 | }
32 |
33 | #dw-react-web-container.dark {
34 | --auto-color: #999;
35 | --scroll-color: #666;
36 | --shadow-color: #0e0e0f;
37 | --border-color: rgba(222, 222, 222, 0.03);
38 | --border-color-deep: rgba(222, 222, 222, 0.2);
39 | --primary-color: pink;
40 | --text-color: #999;
41 | --text-color-active: #eee;
42 | --bg-nav: #1e1e22;
43 | --bg-nav-active: #1a1a1d;
44 | --mask-bg: rgba(0,0,0,0.3);
45 | --bg: #201e1e;
46 | --h-color: #eee;
47 | }
48 |
49 | body {
50 | transition: padding 0.3s, background-color 0.3s;
51 | &.sider-s {
52 | padding-right: @side-warp-width - 16px;
53 | }
54 | }
55 |
56 | body.light {
57 | --bg: #fff;
58 | --bg-o: rgba(255,255,255,0.77);
59 | --text-color: #666;
60 | --text-color-active: #333;
61 | --shadow-color: #f0f0f0;
62 | }
63 |
64 | body.dark {
65 | --bg: #201e1e;
66 | --bg-o: rgb(32, 30, 30, 0.77);
67 | --text-color: #999;
68 | --text-color-active: #eee;
69 | --shadow-color: #0e0e0f;
70 | }
71 |
72 | .dw-page-router {
73 | width: @container-width;
74 | margin: 0 auto;
75 | padding: 40px 5px 15px 5px;
76 | min-height: calc(100vh - @nav-height);
77 | .mixinTextColor();
78 | }
79 |
80 | .@{project-prefix}-music {
81 | width: @container-music-width;
82 | position: relative;
83 | }
84 |
85 | #dw-react-web-container {
86 | // .mixinBg();
87 | background: transparent;
88 | margin-top: @nav-height;
89 | height: calc(100vh - @nav-height);
90 | overflow: auto;
91 | transition: background-color 0.3s;
92 | a {
93 | cursor: pointer;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/utils/utils.ts:
--------------------------------------------------------------------------------
1 | import { randomColor } from 'd-utils/lib/genericUtils'
2 | import { hasClass } from 'd-utils/lib/domUtils'
3 | import store from './../store'
4 | import { DAudioPosition } from './../components/DAudio'
5 |
6 | /**
7 | * 改版页面主题模式
8 | */
9 | export function changePageMode () {
10 | const container: any = document.getElementById('dw-react-web-container')
11 | const body: HTMLElement = document.body
12 | if (hasClass(container, 'light')) {
13 | container.className = 'dark'
14 | body.className = 'dark'
15 | store.colorStore.changeMode('dark')
16 | localStorage.setItem('mode', 'dark')
17 | } else {
18 | container.className = 'light'
19 | body.className = 'light'
20 | store.colorStore.changeMode('light')
21 | localStorage.setItem('mode', 'light')
22 | }
23 | }
24 |
25 | export function randomPrimaryColor () {
26 | const color = randomColor()
27 | const container: any = document.getElementById('dw-react-web-container')
28 | container.style.setProperty(`--primary-color`, color);
29 | }
30 |
31 | /**
32 | * 初始化页面主题模式
33 | */
34 | export function initPageMode (): string {
35 | const storageMode = localStorage.getItem('mode')
36 | const body: HTMLElement = document.body
37 | let mode!: string
38 | if (!storageMode) {
39 | mode = 'light'
40 | localStorage.setItem('mode', mode)
41 | } else {
42 | mode = storageMode
43 | }
44 | store.colorStore.changeMode(mode)
45 | body.className = mode
46 | return mode
47 | }
48 |
49 | /**
50 | * 初始化音频配置
51 | */
52 | export function initDAudiConfig () {
53 | const position = localStorage.getItem('daudio-positon')
54 | if (Number(position) === DAudioPosition.BOTTOM_LEFT) {
55 | localStorage.setItem('daudio-positon', String(DAudioPosition.BOTTOM_LEFT))
56 | } else {
57 | localStorage.setItem('daudio-positon', String(DAudioPosition.BOTTOM_RIGHT))
58 | }
59 | }
60 |
61 | /**
62 | * nav 设置切换类型
63 | */
64 | export function initNavType (type: number) {
65 | store.navStore.setNavLists(type)
66 | }
67 |
68 | /**
69 | * 判断是否是开发环境
70 | */
71 | export function isProduction (): boolean {
72 | return process.env.NODE_ENV === 'production'
73 | }
74 |
75 | export function isTitle (tagName: string): number {
76 | const titleArr = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6']
77 | if (titleArr.includes(tagName)) {
78 | return +tagName.slice(1)
79 | }
80 | return 0
81 | }
82 |
--------------------------------------------------------------------------------
/src/components/BlogList/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 | import './blog-list.less'
4 | import { PROJECT_NAME } from './../../config/constance'
5 | import classNames from 'classnames'
6 | import Icon from './../Icon'
7 |
8 | export interface IBlogListCategorieOrTag {
9 | id: string;
10 | name: string;
11 | }
12 |
13 | export interface IBlogList {
14 | id: string;
15 | name: string;
16 | readCount: string;
17 | editDate: string;
18 | createDate: string;
19 | categorie: IBlogListCategorieOrTag[];
20 | tagLists: IBlogListCategorieOrTag[];
21 | contentLite: string;
22 | }
23 |
24 | interface IBlogListProps {
25 | list: IBlogList;
26 | }
27 |
28 | const BlogList = (props: IBlogListProps) => {
29 | const classString = classNames({
30 | [`${PROJECT_NAME}-comp-blog-list`]: true
31 | })
32 |
33 | const { list } = props
34 |
35 | return (
36 |
37 |
38 | {list.name}
39 |
40 |
41 | {list.createDate.split(' ')[0]}
42 |
43 |
44 | {list.readCount}
45 |
46 |
47 |
48 | {
49 | list.tagLists && list.tagLists.map ((item, index) => (
50 |
51 | {
52 | index > 0 ? ( , ) : null
53 | }
54 | {item.name}
55 |
56 | ))
57 | }
58 |
59 |
60 |
61 |
62 |
63 | )
64 | }
65 |
66 | BlogList.defaultProps = {
67 | list: {
68 | id: '1',
69 | name: '这是一个title',
70 | readCount: '2',
71 | editDate: '2013-12-18 22:99:11',
72 | createDate: '2013-12-18 22:99:11',
73 | categorie: [{
74 | id: 1,
75 | name: '前端'
76 | }],
77 | tagLists: [{
78 | id: 1,
79 | name: 'js'
80 | }]
81 | }
82 | }
83 |
84 | export default BlogList
85 |
--------------------------------------------------------------------------------
/src/components/MenuBar/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import classNames from 'classnames'
3 | import { PROJECT_NAME } from './../../config/constance'
4 | import { MUSCI_MENU } from './../../config/music'
5 | import { MUSIC_SHEET_TYPE } from './../../enum'
6 | import './menu-bar.less'
7 |
8 | // interface IMenuBar {
9 | // lists: []
10 | // }
11 | export interface IMenuSub {
12 | name: string;
13 | type: number;
14 | category: number;
15 | hot: boolean;
16 | }
17 |
18 | interface IMenuProps {
19 | menuType: number
20 | cat: string
21 | history?: any
22 | checkMusicType: (t: number) => void
23 | }
24 |
25 | const MenuBar = (props: IMenuProps) => {
26 | let [catLists, setCatLists] = useState([{}])
27 |
28 | // 获取分类信息
29 | const getCatLists = () => {
30 | return MUSCI_MENU['sub'].filter((item: IMenuSub) => item.category === props.menuType)
31 | }
32 |
33 | const setCatListsFn = () => {
34 | setCatLists(() => catLists = getCatLists())
35 | }
36 |
37 | const checkMusicType = (t: number) => {
38 | props.checkMusicType(t)
39 | }
40 |
41 | const selectMusicSheetType = (c: string) => {
42 | props.history.push(`${props.history.location.pathname}?cat=${c}`)
43 | }
44 |
45 | useEffect(() => {
46 | setCatListsFn()
47 | }, [props.menuType])
48 |
49 | const classString = classNames({
50 | [`${PROJECT_NAME}-menu-bar`]: true
51 | })
52 | return (
53 |
54 |
55 | -
57 | 全部
58 |
59 | {
60 | Object.entries(MUSCI_MENU['categories']).map((item: string[], index: number) =>
61 | -
64 | {
65 | item[1]
66 | }
67 |
68 | )
69 | }
70 |
71 |
72 | {
73 | catLists.map((item: IMenuSub | any, index: number) =>
74 | -
78 | {item.name}
79 |
80 | )
81 | }
82 |
83 |
84 | )
85 | }
86 |
87 | export default MenuBar
88 |
--------------------------------------------------------------------------------
/src/store/models/music.ts:
--------------------------------------------------------------------------------
1 | import { observable, computed, action, isComputedProp } from 'mobx'
2 | import { DAudioPosition } from './../../components/DAudio'
3 | import { MusicGroupList, MusicGroupLists } from './../../type'
4 | import { MusicPlayType, MusicLyricType, IMusicLyric } from './../types'
5 | export default class MusicModel {
6 | @observable
7 | playPosition: DAudioPosition = DAudioPosition.BOTTOM_LEFT
8 |
9 | @action
10 | changePosition (position: DAudioPosition) {
11 | this.playPosition = position
12 | }
13 |
14 | /** 设置当前播放播放列表 */
15 | @action
16 | setMusicListQueue (lists: MusicGroupLists) {
17 | this.musicListQueue = lists
18 | }
19 |
20 | /** 设置播放类型 */
21 | @action
22 | setPlayType (type: MusicPlayType) {
23 | this.playType = type
24 | }
25 |
26 | /** 设置音乐播放的索引 */
27 | @action
28 | setMusicPlayIndex (index: number) {
29 | this.musicPlayIndex = index
30 | }
31 |
32 | /** 设置音乐播放的任务 */
33 | @action
34 | setMusicPlayTask (lists: MusicGroupLists, index: number, type: MusicPlayType) {
35 | // this.setCurrentList(list)
36 | this.setMusicPlayIndex(index)
37 | this.setPlayType(type)
38 | this.setMusicListQueue(lists)
39 | }
40 |
41 | /** 当前播放的音乐信息 */
42 | @computed get
43 | currentList (): MusicGroupList {
44 | return this.musicListQueue[this.musicPlayIndex]
45 | }
46 |
47 | /** 当前音乐的播放索引 */
48 | @observable
49 | musicPlayIndex: number = -1
50 |
51 | /** 当前播放类型 */
52 | @observable
53 | playType: MusicPlayType = MusicPlayType.NO_TYPE
54 |
55 | /** 当前播放的列表数据 */
56 | @observable
57 | musicListQueue: MusicGroupLists = []
58 |
59 | /** 音乐的歌词进度索引 */
60 | @observable
61 | musicLyricIndex: number = 0
62 |
63 | /** 设置歌词进度索引 */
64 | @action
65 | setMusicLyricIndex (index: number) {
66 | this.musicLyricIndex = index
67 | }
68 |
69 | /** 当前歌词内容 */
70 | @computed get
71 | currentLyric (): string {
72 | switch (this.musicLyric.lrcType) {
73 | case MusicLyricType.LOADING:
74 | return '歌词加载中,请稍等'
75 | case MusicLyricType.ABSOLUTE:
76 | return '纯音乐,请欣赏'
77 | case MusicLyricType.HAS_LYRIC:
78 | const lrc = this.musicLyric.objLrc &&
79 | this.musicLyric.objLrc[this.musicLyricIndex] &&
80 | this.musicLyric.objLrc[this.musicLyricIndex].lrc
81 | return lrc || '...'
82 | default:
83 | return '暂无歌词'
84 | }
85 | }
86 |
87 | /** 当前的音乐lyric */
88 | @observable
89 | musicLyric: IMusicLyric = { code: 0, objLrc: [], lrcType: MusicLyricType.LOADING }
90 |
91 | /** 设置音乐lyric */
92 | @action
93 | setMusicLyric (lyric: IMusicLyric) {
94 | this.musicLyric = lyric
95 | }
96 |
97 | @observable
98 | lyricIsShow: boolean = false
99 | }
100 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-website",
3 | "version": "1.0.0",
4 | "description": "react-website",
5 | "scripts": {
6 | "test": "echo \"Error: no test specified\" && exit 1",
7 | "build": "webpack --progress --mode production --config ./webpack.prod.js",
8 | "dev": "webpack-dev-server --mode development --config ./webpack.dev.js --progress",
9 | "watch": "webpack --colors --watch",
10 | "analyz": "NODE_ENV=production npm_config_report=true npm run build"
11 | },
12 | "keywords": [],
13 | "author": "daiwei",
14 | "license": "ISC",
15 | "devDependencies": {
16 | "@babel/core": "^7.4.3",
17 | "@babel/plugin-proposal-class-properties": "^7.4.0",
18 | "@babel/plugin-proposal-decorators": "^7.4.0",
19 | "@babel/plugin-syntax-dynamic-import": "^7.2.0",
20 | "@babel/plugin-transform-runtime": "^7.4.3",
21 | "@babel/preset-env": "^7.4.3",
22 | "@babel/preset-react": "^7.0.0",
23 | "@babel/runtime": "^7.4.3",
24 | "@types/classnames": "^2.2.8",
25 | "@types/node": "^11.13.2",
26 | "@types/qs": "^6.5.3",
27 | "@types/react": "^16.9.5",
28 | "@types/react-dom": "16.8.4",
29 | "@types/react-loadable": "^5.5.1",
30 | "@types/react-router-dom": "^4.3.4",
31 | "@types/react-transition-group": "^2.9.2",
32 | "babel-loader": "^8.0.5",
33 | "babel-polyfill": "^6.26.0",
34 | "classnames": "^2.2.6",
35 | "clean-webpack-plugin": "^1.0.0",
36 | "css-loader": "^0.28.10",
37 | "html-webpack-plugin": "^3.0.6",
38 | "less": "^3.9.0",
39 | "less-loader": "^4.1.0",
40 | "mini-css-extract-plugin": "^0.5.0",
41 | "mobx-react": "^5.4.4",
42 | "mobx-react-lite": "^1.4.1",
43 | "optimize-css-assets-webpack-plugin": "^5.0.1",
44 | "postcss-loader": "^3.0.0",
45 | "qs": "^6.7.0",
46 | "react": "^16.8.6",
47 | "react-dom": "^16.8.6",
48 | "react-loadable": "^5.5.0",
49 | "react-router": "^5.0.0",
50 | "react-router-dom": "^5.0.0",
51 | "react-transition-group": "^4.1.1",
52 | "style-loader": "^0.20.3",
53 | "style-resources-loader": "^1.2.1",
54 | "svg-sprite-loader": "^4.1.6",
55 | "ts-loader": "^5.3.3",
56 | "typescript": "^3.2.4",
57 | "uglifyjs-webpack-plugin": "^2.1.1",
58 | "url-loader": "^1.0.1",
59 | "webpack": "^4.29.0",
60 | "webpack-cli": "^3.2.1",
61 | "webpack-dev-server": ">=3.1.11",
62 | "webpack-merge": "^4.2.1"
63 | },
64 | "dependencies": {
65 | "@types/marked": "^0.6.5",
66 | "copy-webpack-plugin": "^5.0.4",
67 | "d-utils": "^3.0.2",
68 | "highlight.js": "^9.15.10",
69 | "js-yaml": "^3.13.1",
70 | "lodash": "^4.17.14",
71 | "marked": "^0.7.0",
72 | "mobx": "5.13.0",
73 | "node-vibrant": "^3.1.3",
74 | "terser-webpack-plugin": "^2.1.2",
75 | "webpack-bundle-analyzer": "^3.5.2",
76 | "whatwg-fetch": "^3.0.0"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/utils/fetch.ts:
--------------------------------------------------------------------------------
1 | import 'whatwg-fetch';
2 | import { stringifyUrl } from 'd-utils/lib/urlUtils'
3 | import LogUtils from 'd-utils/lib/logUtils'
4 | import * as qs from 'qs'
5 | import Notice from './../components/Notice'
6 | import { isProduction } from './utils'
7 |
8 | export const controller = new AbortController();
9 | export default {
10 | /**
11 | * get 请求
12 | * @param {*} url 请求地址
13 | * @param {*} showMessage 是否显示成功的提示
14 | */
15 | get: function (url: string, data: any = {}, showMessage: boolean = false) {
16 | const newUrl = `${url}?${stringifyUrl(data)}`
17 | const signal = controller.signal;
18 | return new Promise((resolve, reject) => {
19 | fetch(newUrl, {
20 | mode: 'cors',
21 | headers: {
22 | 'Content-Type': 'application/x-www-form-urlencoded'
23 | },
24 | signal
25 | })
26 | .then(res => res.json())
27 | .then((data) => {
28 | !isProduction() && LogUtils.logInfo(data, `http-request: url: ${url} => `)
29 | if (parseInt(data.code, 10) === 200 ||
30 | data.success) {
31 | resolve(data)
32 | return
33 | }
34 |
35 | const msg = data.message ? data.message : data.msg
36 | !isProduction() && LogUtils.logError(msg)
37 | Notice.error(msg)
38 | reject(msg)
39 | })
40 | .catch((err: any) => {
41 | if (err.name === 'AbortError') {
42 | reject(`request was aborted${err}`)
43 | return
44 | }
45 | !isProduction() && LogUtils.logError(err)
46 | reject(`请求未知错误${err}`)
47 | })
48 | })
49 | },
50 |
51 | /**
52 | * post 请求
53 | * @param {*} url 请求地址
54 | * @param {*} data 请求的参数
55 | * @param {*} showMessage 是否显示成功的提示
56 | */
57 | post: function (url: string, data: any, showMessage = false) {
58 | return new Promise((resolve, reject) => {
59 | const signal = controller.signal;
60 | fetch(url, {
61 | method: 'POST',
62 | // mode: 'cors',
63 | body: qs.stringify(data),
64 | headers: {
65 | 'Content-Type': 'application/x-www-form-urlencoded'
66 | },
67 | signal
68 | })
69 | .then(res => res.json())
70 | .then((response) => {
71 | if (parseInt(response.code, 10) === 200) {
72 | resolve(response)
73 | return
74 | }
75 | const msg = response.message ? response.message : response.msg
76 | !isProduction() && LogUtils.logError(msg)
77 | Notice.error(msg)
78 | reject(msg)
79 | })
80 | .catch((err) => {
81 | if (err.name === 'AbortError') {
82 | reject(`request was aborted${err}`)
83 | return
84 | }
85 | !isProduction() && LogUtils.logError(err)
86 | reject(`请求未知错误${err}`)
87 | })
88 | })
89 | },
90 | }
91 |
--------------------------------------------------------------------------------
/src/pages/Music/Sheet/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useMemo, useLayoutEffect, useCallback } from 'react'
2 | import MenuBar from './../../../components/MenuBar'
3 | import classNames from 'classnames'
4 | import { PROJECT_NAME, MUSIC_SHEET_DEFAULT_LIMIT } from '../../../config/constance'
5 | import { parseUrl } from 'd-utils/lib/urlUtils'
6 | import { MUSCI_MENU } from './../../../config/music'
7 | import { IMenuSub } from './../../../components/MenuBar'
8 | import { MUSIC_SHEET_TYPE } from './../../../enum'
9 | import SheetGroup from './../../../components/SheetGroup'
10 | import * as MusicFetch from './../action'
11 | import './sheet.less'
12 | import useScroll from './../../../use/useScroll'
13 | import useLoadingTips from './../../../use/useLoadingTips'
14 | import LoadingTips from '../../../components/LoadingTips';
15 |
16 | interface MusicSheetProps {
17 | history: any;
18 | }
19 |
20 | const MusicSheet = (props: MusicSheetProps) => {
21 | const classString = classNames({
22 | [`${PROJECT_NAME}-music-sheet`]: true
23 | })
24 |
25 | const { cat } = parseUrl(decodeURIComponent(location.href))
26 |
27 | const getType = () => {
28 | if (cat) {
29 | const arr: any[] = MUSCI_MENU['sub'].filter((item: IMenuSub) => item.name === cat)
30 | return arr.length ? arr[0].category : MUSIC_SHEET_TYPE.ALL
31 | }
32 | return MUSIC_SHEET_TYPE.ALL
33 | }
34 |
35 | const [sheetType, setSheetType] = useState(() => getType())
36 |
37 | const [sheetLists, setSheetLists] = useState([])
38 |
39 | const [offset, setOffset] = useState(0)
40 |
41 | const loadingTipsFn = useLoadingTips(false, '加载中')
42 |
43 | const loadMoreInfo = () => {
44 | if (loadingTipsFn.loading) return
45 | setOffset((offset) => offset = offset + MUSIC_SHEET_DEFAULT_LIMIT)
46 | }
47 |
48 | useScroll(document.getElementById('dw-react-web-container'), loadMoreInfo)
49 |
50 | const getSheetLists = useCallback(async () => {
51 | loadingTipsFn.showLoading()
52 | const res: any = await MusicFetch.getSheetLists(cat, offset)
53 | setSheetLists((sheetLists) => sheetLists = sheetLists.concat(res.playlists))
54 | loadingTipsFn.hideLoading()
55 | }, [offset, cat])
56 |
57 | const initDefaultConfig = (() => {
58 | setOffset((offset) => offset = 0)
59 | setSheetLists((sheetLists) => sheetLists = [])
60 | })
61 |
62 | useEffect(() => {
63 | getSheetLists()
64 | }, [offset])
65 |
66 | useLayoutEffect(() => {
67 | initDefaultConfig()
68 | getSheetLists()
69 | }, [cat])
70 |
71 | const checkMusicType = (t: number) => {
72 | setSheetType((sheetType: number) => sheetType = t)
73 | }
74 |
75 | return(
76 |
81 | )
82 | }
83 |
84 | export default MusicSheet
85 |
--------------------------------------------------------------------------------
/src/pages/Setting/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useMemo, useRef } from 'react'
2 | import fetch from './../../utils/fetch'
3 | import { IStore, MusicLyricType } from './../../store/types'
4 | import { observer, useObservable, useObserver, useLocalStore, useStaticRendering, useComputed } from "mobx-react-lite"
5 | import { useStore, useUpdate } from './../../utils/use'
6 | import classNames from 'classnames'
7 | import './setting.less'
8 | import { PROJECT_NAME } from './../../config/constance'
9 | import Switch from './../../components/Switch'
10 | import { changePageMode } from './../../utils/utils'
11 | import { randomPrimaryColor } from './../../utils/utils'
12 | import store from '../../store';
13 | import Lyric from './../../components/Lyric'
14 | import useNavType from './../../use/useNavType'
15 |
16 | interface ISettingProps {}
17 |
18 | const Setting = observer((props: ISettingProps) => {
19 | const classString = classNames({
20 | [`${PROJECT_NAME}-setting`]: true,
21 | [`dw-page-router`]: true
22 | })
23 |
24 | let [isDark, setDark] = useState(useStore().colorStore.mode === 'dark')
25 | const [isUpdate, setIsUpdate] = useState(false)
26 | const selfSettingRef = useRef(store.musicStore.lyricIsShow)
27 |
28 | const changePageModeFn = (isDarkMode: boolean) => {
29 | setDark(() => isDark = isDarkMode)
30 | changePageMode()
31 | }
32 |
33 | useNavType(1)
34 |
35 | const changeMusicLyric = () => {
36 | if (selfSettingRef.current) {
37 | selfSettingRef.current = false
38 | Lyric.close()
39 | } else {
40 | selfSettingRef.current = true
41 | Lyric.show()
42 | }
43 | setIsUpdate((isUpdate) => !isUpdate)
44 | }
45 |
46 | let [testProps, setTestProps] = useState(true)
47 | const changeTest = (checked: boolean) => {
48 | setTestProps(testProps => checked)
49 | }
50 | return (
51 |
52 |
Setting
53 |
54 | 基本设置
55 |
56 |
57 | -
58 | 护眼模式
59 |
63 |
64 |
65 |
66 | 音乐设置
67 |
68 |
69 | -
70 | 歌词显示
71 |
75 |
76 |
77 |
78 | )
79 | })
80 |
81 | export default Setting
--------------------------------------------------------------------------------
/src/components/Lyric/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef, useImperativeHandle } from 'react'
2 | import classNames from 'classnames'
3 | import ReactDOM from 'react-dom';
4 | import { PROJECT_NAME } from './../../config/constance'
5 | import { MUSCI_MENU } from './../../config/music'
6 | import './lyric.less'
7 | // import { useStore } from './../../utils/use'
8 | import { useObserver, observer } from 'mobx-react-lite'
9 | import store from '../../store';
10 | import { MusicLyricType } from './../../store/types'
11 | import { bool } from 'prop-types';
12 |
13 | interface ILyricState {
14 | start: () => void;
15 | checkLrc: (lrc: string) => void;
16 | close: () => void;
17 | show: () => void;
18 | }
19 |
20 | interface ILyricProps {}
21 |
22 | export const Lyric: React.FC = (props, ref) => {
23 | const lyricRef = useRef(null)
24 | const [currentLyric, setCurrentLyric] = useState('')
25 | const [isShow, setIsShow] = useState(false)
26 |
27 | const classString = classNames({
28 | [`${PROJECT_NAME}-lyric-comp`]: true,
29 | [`show`]: isShow && store && store.musicStore && store.musicStore.musicLyric.lrcType !== MusicLyricType.LOADING
30 | })
31 |
32 | const start = () => {
33 | // console.log('this is lyric start')
34 | }
35 |
36 | const checkLrc = (lrc: string) => {
37 | if (!store.musicStore.lyricIsShow) return
38 | if (!isShow) setIsShow((isShow) => isShow = true)
39 | setCurrentLyric(currentLyric => currentLyric = lrc)
40 | }
41 |
42 | const close = () => {
43 | setIsShow((isShow) => isShow = false)
44 | store.musicStore.lyricIsShow = false
45 | }
46 |
47 | const show = () => {
48 | setIsShow((isShow) => isShow = true)
49 | store.musicStore.lyricIsShow = true
50 | }
51 |
52 | useImperativeHandle(ref, () => ({
53 | start,
54 | checkLrc,
55 | close,
56 | show
57 | }))
58 |
59 | return (
60 |
67 | )
68 | }
69 |
70 | const LyricComponent = React.forwardRef(Lyric)
71 |
72 | function newInstance (props: ILyricProps) {
73 | const LyricRef = React.createRef()
74 |
75 | const div = document.createElement('div')
76 | document.body.appendChild(div)
77 | ReactDOM.render(, div)
78 |
79 | const destroy = () => {
80 | store.musicStore.lyricIsShow = false
81 | ReactDOM.unmountComponentAtNode(div);
82 | (div.parentNode as HTMLDivElement ).removeChild(div);
83 | }
84 |
85 | const { start, checkLrc, close, show } = LyricRef.current as ILyricState
86 |
87 | return {
88 | start,
89 | destroy,
90 | checkLrc,
91 | close,
92 | show
93 | }
94 | }
95 |
96 | const Ins = newInstance({})
97 | export default Ins
98 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const selfmerge = require('webpack-merge');
2 | const path = require('path');
3 | const webpack = require('webpack');
4 | const TerserPlugin = require('terser-webpack-plugin');
5 | const selfcommon = require('./webpack.common.js');
6 | const CopyWebpackPlugin = require('copy-webpack-plugin');
7 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
8 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
9 |
10 | // css压缩打包相关
11 | var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
12 |
13 | // 打包清除dist目录
14 | const CleanWebpackPlugin = require('clean-webpack-plugin');
15 |
16 | const resolve = function (dir) {
17 | return path.resolve(__dirname, dir);
18 | }
19 | module.exports = selfmerge(selfcommon, {
20 | mode: 'production',
21 | entry: {
22 | app: './src/index.tsx',
23 | },
24 | externals: {
25 | react: 'React',
26 | 'react-dom': 'ReactDOM',
27 | 'highlight.js': 'hljs',
28 | 'mobx': 'mobx'
29 | },
30 | output: {
31 | path: path.resolve(__dirname, 'dist/web_v3'),
32 | publicPath: '/web_v3/',
33 | filename: 'js/[name]-[hash].js',
34 | libraryTarget: 'umd'
35 | },
36 | module: {
37 | rules: [
38 | {
39 | test: /\.js$/,
40 | use: {
41 | loader: 'babel-loader',
42 | options: {
43 | presets: ["@babel/env"]
44 | }
45 | }
46 | },
47 | ]
48 | },
49 | plugins: [
50 | // 清除
51 | new CleanWebpackPlugin(['dist'], {
52 | verbose: false
53 | }),
54 |
55 | // css 压缩
56 | new OptimizeCssAssetsPlugin({}),
57 |
58 | new BundleAnalyzerPlugin()
59 | ],
60 | optimization: {
61 | namedModules: true,
62 | minimizer: [
63 | new TerserPlugin({
64 | cache: true,
65 | parallel: true,
66 | sourceMap: true, // Must be set to true if using source-maps in production
67 | terserOptions: {
68 | // https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions
69 | }
70 | }),
71 | ],
72 | splitChunks: {
73 | chunks: "all",
74 | minSize: 30000,
75 | minChunks: 3,
76 | maxAsyncRequests: 5,
77 | maxInitialRequests: 3,
78 | name: true,
79 | cacheGroups: {
80 | default: {
81 | minChunks: 2,
82 | priority: -20,
83 | reuseExistingChunk: true,
84 | },
85 | vendors: {
86 | test: /[\\/]node_modules[\\/]/,
87 | chunks: "initial",
88 | name: "vendor",
89 | priority: 10,
90 | enforce: true,
91 | },
92 | commons: {
93 | name: 'vendors',
94 | chunks: 'all',
95 | minChunks: 2,
96 | maxInitialRequests: 5, // The default limit is too small to showcase the effect
97 | minSize: 0 // This is example is too small to create commons chunks
98 | }
99 | }
100 | },
101 | runtimeChunk: "single",
102 | minimizer: [
103 | new UglifyJsPlugin({
104 | test: /\.js(\?.*)?$/i
105 | }),
106 | ]
107 | }
108 | });
--------------------------------------------------------------------------------
/src/pages/Home/index.tsx:
--------------------------------------------------------------------------------
1 | // import * as React from 'react'
2 | import classNames from 'classnames'
3 | import './home.less'
4 | import typeWriter, { ITypeWriterList } from './../../utils/typewriter'
5 | import { PROJECT_NAME, TYPE_WRITER, SELF_SHEET_INFO} from './../../config/constance'
6 | import React, { useState, useEffect, useRef } from 'react'
7 | import { sheetDetailById } from './../Music/action'
8 | import store from './../../store'
9 | import { MusicPlayType } from './../../store/types'
10 | import { formatMusicLists } from './../../utils/music'
11 | import useNavType from './../../use/useNavType'
12 |
13 | interface IHomeProps {}
14 |
15 | const Home = (props: IHomeProps) => {
16 | const homeRef: any = useRef(null);
17 |
18 | const initTypeWriter = async () => {
19 | // 加载音乐专辑列表地址
20 | const res: any = await sheetDetailById(SELF_SHEET_INFO.id)
21 | store.musicStore.setMusicPlayTask(formatMusicLists(res.playlist.tracks), 0, MusicPlayType.HOME)
22 |
23 | for (let i = 0; i < TYPE_WRITER.length; i++) {
24 | let list: ITypeWriterList = TYPE_WRITER[i]
25 | list.container = homeRef.current
26 | await typeWriter(TYPE_WRITER[i] as ITypeWriterList)
27 | }
28 | }
29 |
30 | useNavType(1)
31 |
32 | useEffect(() => {
33 | if (!window.__FIRST_IN_HOME__) {
34 | initTypeWriter()
35 | }
36 |
37 | return function clearWriter () {
38 | window.__FIRST_IN_HOME__ = true
39 | }
40 | })
41 |
42 | const classString = classNames({
43 | [`${PROJECT_NAME}-home`]: true,
44 | [`dw-page-router`]: true
45 | })
46 | return (
47 |
48 | {
49 | !window.__FIRST_IN_HOME__ ? (
50 |
51 | ) : (
52 | TYPE_WRITER.map((item, index) => {
53 | return (
54 | item.tagName === 'h1' ? (
55 |
{item.text}
56 | ) : item.tagName === 'h2' ? (
57 |
{item.text}
58 | ) : item.tagName === 'h3' ? (
59 |
{item.text}
60 | ) : item.tagName === 'h4' ? (
61 |
{item.text}
62 | ) : item.tagName === 'h5' ? (
63 |
{item.text}
64 | ) : item.tagName === 'h6' ? (
65 |
{item.text}
66 | ) : item.tagName === 'a' ? (
67 |
{item.text}
68 | ) : item.tagName === 'p' ? (
69 |
{item.text}
70 | ) : item.tagName === 'span' ? (
71 |
{item.text}
72 | ) : (
73 |
{item.text}
74 | )
75 | )
76 | })
77 | )
78 | }
79 |
80 | )
81 | }
82 |
83 | export default Home
84 |
--------------------------------------------------------------------------------
/src/pages/Music/SheetDetail/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import classNames from 'classnames'
3 | import './sheet-detail.less'
4 | import { PROJECT_NAME } from '../../../config/constance'
5 | import LazyImg from './../../../components/LazyImg'
6 | import MusicListGroup from './../../../components/MusicListGroup'
7 | import LoadingTips from './../../../components/LoadingTips'
8 | import { MusicPlayType } from './../../../store/types'
9 | import { sheetDetailById } from './../action'
10 | import { parseUrl } from 'd-utils/lib/urlUtils'
11 | import { formatMusicLists } from './../../../utils/music'
12 | import Icon from './../../../components/Icon'
13 | import { RouteChildrenProps } from 'react-router';
14 |
15 | const SheetDetail: React.FC = (props) => {
16 | const classString = classNames({
17 | [`${PROJECT_NAME}-music-sheet-detail`]: true
18 | })
19 | const [sheetDetailLists, setSheetDetailLists] = useState([])
20 | const [detialInfo, setDetailInfo] = useState({})
21 | const sheetId = parseUrl(decodeURIComponent(location.href)).sheetId
22 |
23 | useEffect(() => {
24 | const getSheetLists = async () => {
25 | const { playlist } = await sheetDetailById(sheetId) as any
26 | setSheetDetailLists((sheetDetailLists: any) => sheetDetailLists = formatMusicLists(playlist.tracks))
27 | setDetailInfo((detialInfo: any) => detialInfo = {
28 | coverImgUrl: playlist.coverImgUrl,
29 | description: playlist.description,
30 | name: playlist.name,
31 | id: playlist.id,
32 | trackCount: playlist.trackCount,
33 | playCount: playlist.playCount,
34 | })
35 | }
36 | getSheetLists()
37 | }, [])
38 |
39 | const closeDetial = () => {
40 | props.history.goBack()
41 | }
42 |
43 | return (
44 |
45 | {
46 | sheetDetailLists.length ? (
47 |
48 |
49 |
50 |
51 |
52 |
53 |
{detialInfo.name}
54 |
{detialInfo.description}
55 |
56 | 共 { detialInfo.trackCount } 首音乐
57 | { detialInfo.playCount } 人听过
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
68 |
69 |
70 | ) : (
71 |
72 | )
73 | }
74 |
75 | )
76 | }
77 |
78 | export default SheetDetail
79 |
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | // MiniCssExtractPlugin
4 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
5 | const env = process.env.NODE_ENV
6 | // HtmlWebpackPlugin
7 | const HtmlWebpackPlugin = require('html-webpack-plugin');
8 |
9 | const resolve = function (dir) {
10 | return path.resolve(__dirname, dir);
11 | }
12 |
13 | const devMode = process.env.NODE_ENV === "development"
14 |
15 | module.exports = {
16 | plugins: [
17 | new HtmlWebpackPlugin ({
18 | filename: 'index.html',
19 | template: 'index.html',
20 | inject: true,
21 | title: "未曾遗忘的青春,戴伟的个人博客",
22 | favicon: 'src/assets/favicon/favicon.ico',
23 | minify: {
24 | removeComments: true
25 | }
26 | }),
27 |
28 | new MiniCssExtractPlugin ({
29 | filename: "css/[name]-[hash].css",
30 | chunkFilename: "css/[name]-[hash].css"
31 | }),
32 |
33 | new webpack.DefinePlugin({
34 | 'process.env': env
35 | })
36 | ],
37 | module: {
38 | rules: [
39 | {
40 | test: /\.(c)ss$/,
41 | use: [
42 | devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
43 | {
44 | loader: 'css-loader',
45 | options: {
46 | modules:true,
47 | localIdentName:'[name]__[local]__[hash:base64:5]'
48 | }
49 | },
50 | {
51 | loader:"postcss-loader",
52 | options: {
53 | plugins: () => [
54 | require('autoprefixer')()
55 | ]
56 | }
57 | },
58 | ]
59 | },
60 | {
61 | test: /\.less$/,
62 | use: [
63 | devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
64 | "css-loader",
65 | "less-loader",
66 | {
67 | loader: 'style-resources-loader',
68 | options: {
69 | patterns: path.resolve(__dirname, './src/style/common.less')
70 | }
71 | }
72 | ],
73 | },
74 | {
75 | test: /\.(ttf|eot|woff|woff2)$/,
76 | use: [
77 | {
78 | loader: 'url-loader'
79 | }
80 | ]
81 | },
82 | {
83 | test: /\.svg$/,
84 | use: {
85 | loader: 'svg-sprite-loader',
86 | options: {
87 | symbolId: '[name]'
88 | }
89 | },
90 | include: path.resolve(__dirname, 'src/assets/svg')
91 | },
92 | {
93 | test: /\.(ts|tsx)$/,
94 | use: [
95 | {loader: 'babel-loader',},
96 | {
97 | loader: 'ts-loader',
98 | options: {
99 | // 加快编译速度
100 | transpileOnly: true,
101 | // 指定特定的ts编译配置,为了区分脚本的ts配置
102 | configFile: path.resolve(__dirname, './tsconfig.json')
103 | }
104 | }
105 | ]
106 | },
107 | {
108 | test: /\.(png|jpg|gif)$/,
109 | use: [
110 | {
111 | loader: 'url-loader',
112 | options: {
113 | limit: 8192
114 | }
115 | }
116 | ]
117 | },
118 | ]
119 | },
120 | resolve: {
121 | alias: {
122 | 'assets': resolve('src/assets'),
123 | 'style': resolve('src/style'),
124 | 'components': resolve('src/components'),
125 | 'pages': resolve('src/pages'),
126 | 'utils': resolve('src/utils')
127 | },
128 | extensions: ['.ts', '.tsx', '.js']
129 | },
130 | };
--------------------------------------------------------------------------------
/src/config/constance.ts:
--------------------------------------------------------------------------------
1 | import { ITypeWriterList } from './../utils/typewriter'
2 | import store from './../store'
3 | import DAudio from './../components/DAudio'
4 | import { getMusicIndexById, getPlayMuiscList, getNextMusicList} from './../utils/music'
5 | import { playDefaultSheet } from './../utils/music'
6 |
7 | export const PROJECT_NAME = 'dw-react-web'
8 | export const WEBSITE_TITLE = 'D.w'
9 | export const TITLE = '未曾遗忘的青春 | web前端_技术分享_戴伟的个人网站'
10 | export const TITLE_ENTRY = 'ヽ(✿゚▽゚)ノ 欢迎回来'
11 | export const TITLE_OUT = 'w(゚Д゚)w 你走了 呜啊啊'
12 | export const SVG_DEFAULT_COLOR = 'green'
13 | export const CONSOLE_TEXT = ' R E A C T - W E B S I T E '
14 | export const CONSOLE_BGS = ['#a18cd1', '#fbc2eb']
15 |
16 | export const SELF_SHEET_INFO = {
17 | id: 2179377798,
18 | name: '程序员必备,带上耳机代码就是全世界'
19 | }
20 |
21 | export const DEFAULT_BLOG_DETAIL = {
22 | detail: {
23 | id: '',
24 | name: '',
25 | readCount: '',
26 | editDate: '',
27 | createDate: '',
28 | content: '',
29 | tagLists: [{
30 | id: '',
31 | name: ''
32 | }]
33 | },
34 | next: {
35 | id: null,
36 | name: null
37 | },
38 | prev: {
39 | id: null,
40 | name: null
41 | }
42 | }
43 |
44 | export const TYPE_WRITER: ITypeWriterList[] = [
45 | {
46 | text: '嗨!',
47 | tagName: 'h1',
48 | config: {},
49 | domAttr: {},
50 | reactAttr: {}
51 | }, {
52 | text: '欢迎来到我的个人网站,这也是我个人的第三版网站',
53 | tagName: 'h2',
54 | config: {},
55 | domAttr: {},
56 | reactAttr: {}
57 | }, {
58 | text: '自我介绍一下',
59 | tagName: 'h2',
60 | config: {},
61 | domAttr: {},
62 | reactAttr: {}
63 | }, {
64 | text: '90后,来自安徽芜湖,现在在上海工作,从事前端开发,个人比较活泼开朗吧,没事学学技术,打打游戏听听歌',
65 | tagName: 'h4',
66 | config: {},
67 | domAttr: {},
68 | reactAttr: {}
69 | }, {
70 | text: '说到听歌,给大家推荐一个个人收藏的歌单',
71 | tagName: 'span',
72 | config: {},
73 | domAttr: {},
74 | reactAttr: {}
75 | }, {
76 | text: `《${SELF_SHEET_INFO.name}》`,
77 | tagName: 'span',
78 | config: {},
79 | domAttr: {
80 | class: 'underline',
81 | click: async () => {
82 | await playDefaultSheet()
83 | }
84 | },
85 | reactAttr: {
86 | className: 'underline',
87 | onClick: async () => {
88 | await playDefaultSheet()
89 | }
90 | }
91 | }
92 | ]
93 |
94 |
95 | export const MUSIC_SHEET_DEFAULT_LIMIT = 30
96 | export const MUSIC_SHEET_TRANSITION_DURATION = MUSIC_SHEET_DEFAULT_LIMIT * 200 / 2
97 |
98 | export const MUSIC_SEARCH_DEFAULT_LIMIT = 20
99 |
100 | export const BLOG_LIST_DEFAULT_LIMIT = 12
101 |
102 | export const INTER_LINK_DEFAULT_LIMIT = 12
103 |
104 | export const BLOG_TAGS_ALL_INFO = {
105 | id: '',
106 | name: '全部'
107 | }
108 |
109 | // 排行榜信息
110 | export const MUSIC_RANK_TYPE: any = {
111 | '0': '云音新歌榜',
112 | '1': '云音热歌榜',
113 | '2': '网易原创歌曲榜',
114 | '3': '云音乐飙升榜',
115 | '4': '云音乐电音榜',
116 | '5': 'UK排行榜周榜',
117 | '6': '美国Billboard周榜',
118 | '7': 'KTV嗨榜',
119 | '8': 'iTunes榜',
120 | '10': '日本Oricon周榜',
121 | '12': '韩国Mnet排行榜周榜',
122 | '17': '华语金曲榜',
123 | '22': '云音乐ACG音乐榜'
124 | }
125 |
126 | // copyright 配置
127 | export const COPY_RIGHT_CONFIG = [
128 | {
129 | name: 'd-utils',
130 | to: 'https://d-utils.daiwei.site'
131 | }, {
132 | name: '友情链接',
133 | to: '/interlink'
134 | }
135 | ]
--------------------------------------------------------------------------------
/src/components/MusicListGroup/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import classNames from 'classnames'
3 | import './music-list-group.less'
4 | import { CSSTransition, TransitionGroup } from 'react-transition-group';
5 | import { PROJECT_NAME, MUSIC_SEARCH_DEFAULT_LIMIT } from '../../config/constance'
6 | import MusicList from './../MusicList'
7 | import { MusicGroupList, MusicGroupLists } from './../../type'
8 | import { useLocalStore, observer } from 'mobx-react-lite'
9 | import { useStore } from './../../utils/use'
10 | import store from './../../store'
11 | import { MusicPlayType } from './../../store/types'
12 |
13 | interface IMusicListsGroupProps {
14 | lists: MusicGroupLists;
15 | transition: boolean;
16 | type: MusicPlayType;
17 | }
18 |
19 | const MusicListGroup = observer((props: IMusicListsGroupProps) => {
20 | const [start, setStart] = useState(false)
21 |
22 | const memoPlayActive = (id: number, index: number) => {
23 | return !!(store.musicStore.currentList
24 | && store.musicStore.currentList.id
25 | && index === store.musicStore.musicPlayIndex
26 | && id === store.musicStore.currentList.id)
27 | }
28 |
29 | const classString = classNames({
30 | [`${PROJECT_NAME}-music-list-group`]: true
31 | })
32 |
33 | const addMusicQueue = (index: number) => {
34 | // 添加播放列表进程
35 | const { lists, type } = props
36 | store.musicStore.setMusicPlayTask(lists, index, type)
37 | }
38 |
39 | useEffect(() => {
40 | }, [props.lists])
41 | return (
42 |
43 | {
44 | props.transition ? (
45 |
47 | {
48 | props.lists.map((item: MusicGroupList, index: number) => (
49 |
60 |
65 |
66 | ))
67 | }
68 |
69 | ) : (
70 |
71 | {
72 | props.lists.map((item: MusicGroupList, index: number) => (
73 |
77 | ))
78 | }
79 |
80 | )
81 | }
82 |
83 | )
84 | })
85 |
86 | MusicListGroup.defaultProps = {
87 | lists: [],
88 | transition: true
89 | }
90 |
91 | export default MusicListGroup
92 |
--------------------------------------------------------------------------------
/src/pages/Music/Rank/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useCallback } from 'react'
2 | import { getRankLists } from './../action'
3 | import className from 'classnames'
4 | import { formatMusicLists } from './../../../utils/music'
5 | import { PROJECT_NAME, MUSIC_RANK_TYPE } from '../../../config/constance'
6 | import MusicListGroup from './../../../components/MusicListGroup'
7 | import LoadingTips from '../../../components/LoadingTips';
8 | import { parseUrl } from 'd-utils/lib/urlUtils'
9 | import DAudio from './../../../components/DAudio';
10 | import './rank.less'
11 | import { MusicPlayType } from './../../../store/types'
12 |
13 | const MusicRank = () => {
14 | const urlKeywords = parseUrl(decodeURIComponent(location.href)).keywords
15 | const [showType, setType] = useState(false)
16 | const [rankLists, setRankLists] = useState([])
17 | const [rankType, setRankType] = useState(Object.values(MUSIC_RANK_TYPE).includes(urlKeywords) ? urlKeywords : '1')
18 |
19 | const rankListDispatch = useCallback(async (rankId: string = '1') => {
20 | const res: any = await getRankLists(rankId)
21 | setRankLists((rankLists) => rankLists = formatMusicLists(res.playlist.tracks))
22 | }, [rankType])
23 |
24 | useEffect(() => {
25 | rankListDispatch(rankType)
26 | }, [rankType])
27 |
28 | const classString = className({
29 | [`${PROJECT_NAME}-music-rank`]: true
30 | })
31 | const classRankGroup = className({
32 | [`${PROJECT_NAME}-music-rank-group`]: true
33 | })
34 |
35 | const classTypeList = className({
36 | [`${classString}-type-lists`]: true,
37 | ['active']: showType
38 | })
39 |
40 | const handlePlay = () => {
41 | DAudio.list
42 | }
43 |
44 | const showTypeList = () => {
45 | setType((showType) => showType = true)
46 | }
47 |
48 | const selectRankType = (typeId: string, e: any) => {
49 | e.stopPropagation()
50 | if (typeId === rankType) {
51 | setType((showType) => showType = false)
52 | return
53 | }
54 | setRankLists((rankLists) => rankLists = [])
55 | setRankType((rankType) => rankType = typeId)
56 | setType((showType) => showType = false)
57 | }
58 |
59 | const hideTypeList = () => {
60 | setType((showType) => showType = false)
61 | }
62 | return(
63 |
64 |
65 |
66 |
67 | {
68 | Object.entries(MUSIC_RANK_TYPE).map((item: any) => (
69 |
{item[1]}
72 | ))
73 | }
74 |
75 |
76 |
77 | 分类:
78 | {MUSIC_RANK_TYPE[rankType]}
79 |
80 |
81 |
82 | {
83 | rankLists.length ? (
84 |
87 | ) : (
88 |
89 | )
90 | }
91 |
92 |
93 | )
94 | }
95 |
96 | export default MusicRank
97 |
--------------------------------------------------------------------------------
/src/pages/Blog/Detail/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useState, useRef, useMemo, useLayoutEffect} from 'react'
2 | import classNames from 'classnames'
3 | import './detail.less'
4 | import './../../../style/high-default.less'
5 | import './../../../style/high-custom.less'
6 | import {
7 | getBlogDetail,
8 | pv
9 | } from './../action'
10 | import LoadingTips from './../../../components/LoadingTips'
11 | import { IBlogListCategorieOrTag } from '../../../components/BlogList'
12 | import { PROJECT_NAME, DEFAULT_BLOG_DETAIL } from './../../../config/constance'
13 | import { parseUrl } from 'd-utils/lib/urlUtils'
14 | import { isProduction } from './../../../utils/utils'
15 | import marked from "marked";
16 |
17 | marked.setOptions({
18 | renderer: new marked.Renderer(),
19 | highlight: function(code: any) {
20 | return require('highlight.js').highlightAuto(code).value;
21 | },
22 | pedantic: false,
23 | gfm: true,
24 | tables: true,
25 | breaks: false,
26 | sanitize: false,
27 | smartLists: true,
28 | smartypants: false,
29 | xhtml: false
30 | });
31 |
32 | interface IBlogDetailProps {
33 | history: any;
34 | }
35 |
36 | interface IBlogDetail {
37 | id: string;
38 | name: string;
39 | readCount: string;
40 | editDate: string;
41 | createDate: string;
42 | tagLists: IBlogListCategorieOrTag[];
43 | content: string;
44 | }
45 |
46 | interface IBlogPrevNext {
47 | id: string | null;
48 | name: string | null;
49 | }
50 |
51 | interface IBlogDetailInfo {
52 | next: IBlogPrevNext;
53 | prev: IBlogPrevNext;
54 | detail: IBlogDetail;
55 | }
56 |
57 | const BlogDetail: React.FC = (props) => {
58 | const [blog, setBlog] = useState(DEFAULT_BLOG_DETAIL)
59 | const contentRef = useRef(null);
60 | const classString = classNames({
61 | [`${PROJECT_NAME}-blog-detail`]: true
62 | })
63 |
64 | const { id } = parseUrl(decodeURIComponent(location.href))
65 |
66 | const handleGo = (info: IBlogPrevNext) => {
67 | if (info.id) {
68 | props.history.push(`/blog/detail?id=${info.id}`)
69 | }
70 | }
71 |
72 | useEffect(() => {
73 | setBlog((blog) => blog = DEFAULT_BLOG_DETAIL)
74 | const fetchDetail = async () => {
75 | const res = await getBlogDetail(id)
76 | if (isProduction()) await pv(id)
77 | setBlog((blog) => blog = res.data)
78 | }
79 | fetchDetail()
80 | }, [id])
81 |
82 | return (
83 |
84 | {
85 | blog.detail.id ? (
86 |
87 |
88 |
{blog.detail.name}
89 |
90 |
93 |
94 | {
95 | blog.detail.editDate ? (
96 |
97 |
---------------
98 | 最后编辑时间: {blog.detail.editDate}
99 |
100 | ) : null
101 | }
102 |
115 |
116 | ) : (
117 |
118 | )
119 | }
120 |
121 | )
122 | }
123 |
124 | export default BlogDetail
125 |
--------------------------------------------------------------------------------
/src/components/Notice/Message.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef, useImperativeHandle } from 'react'
2 | import classNames from 'classnames'
3 | import ReactDOM from 'react-dom'
4 | import { PROJECT_NAME } from '../../config/constance'
5 | import Notice, { NoticeType } from './Notice'
6 | import './message.less'
7 | import { CSSTransition, TransitionGroup } from 'react-transition-group';
8 |
9 | export interface IMessageState {
10 | type: NoticeType;
11 | text: string | number;
12 | duration?: number;
13 | key?: string;
14 | id?: number;
15 | onClose?: () => void;
16 | }
17 |
18 | export interface IMessageQueueState {
19 | addMessage (message: IMessageState): void;
20 | }
21 |
22 | export interface IMessageQueueProps {}
23 |
24 | const MessageQueue: React.FC = (props, ref) => {
25 | let noticeId = 0
26 | const transitionTime: number = 300
27 | const [messageQueue, setMessageQueue] = useState([])
28 |
29 | const messageInclude = (message: IMessageState): boolean => {
30 | return messageQueue.some((item: IMessageState) => message.key === item.key)
31 | }
32 |
33 | const addMessage = (message: IMessageState) => {
34 | noticeId ++
35 | message.key = getMessagekey()
36 | message.id = noticeId
37 | message.duration = message.duration ? message.duration : 3000
38 | if (!messageInclude(message)) {
39 | // push message
40 | setMessageQueue((messageQueue: IMessageState[]) => messageQueue.concat([message]))
41 | if (message.duration > 0) {
42 | setTimeout(() => {
43 | message.key && removeMessage(message.key)
44 | }, message.duration)
45 | }
46 | }
47 | }
48 |
49 | const removeMessage = (key: string) => {
50 | setMessageQueue((messageQueue: IMessageState[]) => messageQueue.filter((item: IMessageState) => {
51 | if (item.key === key) {
52 | item.onClose && item.onClose()
53 | return false
54 | }
55 | return item
56 | }))
57 | }
58 |
59 | const getMessagekey = () => {
60 | return `message-${new Date().getTime()}-${messageQueue.length}-${Math.floor(Math.random() * 10000)}`
61 | }
62 |
63 | const classString = classNames({
64 | [`${PROJECT_NAME}-comp-notice-queue`]: true,
65 | })
66 |
67 | useImperativeHandle(ref, () => ({
68 | addMessage
69 | }))
70 |
71 | return (
72 |
73 |
74 | {
75 | messageQueue.map((message) => (
76 |
87 |
90 |
91 | ))
92 | }
93 |
94 |
95 | )
96 | }
97 |
98 | const MessageQueueComponent = React.forwardRef(MessageQueue)
99 |
100 | function newInstance(props: IMessageQueueProps) {
101 | const MessageQueueRef = React.createRef();
102 |
103 | const div = document.createElement('div')
104 | document.body.appendChild(div)
105 | ReactDOM.render(, div)
106 | const destroy = () => {
107 | ReactDOM.unmountComponentAtNode(div);
108 | (div.parentNode as HTMLDivElement ).removeChild(div);
109 | }
110 |
111 | const { addMessage } = MessageQueueRef.current as IMessageQueueState
112 | return {
113 | addMessage (message: IMessageState) {
114 | return addMessage(message)
115 | },
116 | destroy
117 | }
118 | }
119 |
120 | export default newInstance({})
121 |
--------------------------------------------------------------------------------
/src/style/transition.less:
--------------------------------------------------------------------------------
1 |
2 | // fade
3 | .fade-enter, .fade-appear {
4 | opacity: 0;
5 | }
6 | .fade-enter-active, .fade-appear-active {
7 | opacity: 1;
8 | transition: opacity 300ms;
9 | }
10 | .fade-enter-done {
11 | opacity: 1;
12 | }
13 | .fade-exit {
14 | opacity: 1;
15 | }
16 | .fade-exit-active {
17 | opacity: 0;
18 | transition: opacity 300ms;
19 | }
20 | .fade-exit-done {
21 | opacity: 0;
22 | }
23 |
24 | // sideUpFade
25 | .side-up-fade-enter, .side-up-fade-appear {
26 | transform: translate(0, 10px);
27 | opacity: 0;
28 | }
29 | .side-up-fade-enter-active, .side-up-fade-appear-active {
30 | // opacity: 1;
31 | // transform: translate(0, 0);
32 | // transition: all 300ms;
33 | }
34 | .side-up-fade-enter-done {
35 | opacity: 1;
36 | transform: translate(0, 0);
37 | transition: all 300ms;
38 | }
39 | .side-up-fade-exit {
40 | opacity: 1;
41 | transform: translate(0, 0);
42 | }
43 | .side-up-fade-exit-active {
44 | opacity: 0;
45 | transform: translate(0, 10px);
46 | transition: all 300ms;
47 | }
48 | .side-up-fade-exit-done {
49 | opacity: 0;
50 | transform: translate(0, 10px);
51 | }
52 |
53 |
54 | // sideLeftFade
55 | .side-left-fade-enter, .side-left-fade-appear {
56 | transform: translate(60px, 0);
57 | opacity: 0;
58 | }
59 | .side-left-fade-enter-active, .side-left-fade-appear-active {
60 | // opacity: 1;
61 | // transform: translate(0, 0);
62 | // transition: all 300ms;
63 | }
64 | .side-left-fade-enter-done {
65 | // opacity: 1;
66 | // transform: translate(0, 0);
67 | opacity: 1;
68 | transform: translate(0, 0);
69 | transition: all 300ms;
70 | }
71 | .side-left-fade-exit {
72 | opacity: 1;
73 | transform: translate(0, 0);
74 | }
75 | .side-left-fade-exit-active {
76 | opacity: 0;
77 | transform: translate(60px, 0);
78 | transition: all 300ms;
79 | }
80 | .side-left-fade-exit-done {
81 | opacity: 0;
82 | transform: translate(60px, 0);
83 | }
84 |
85 |
86 | // sideLowLeftFade
87 | .side-low-left-fade-enter, .side-low-left-fade-appear {
88 | transform: translate(10px, 0);
89 | opacity: 0;
90 | }
91 | .side-low-left-fade-enter-active, .side-low-left-fade-appear-active {
92 | // opacity: 1;
93 | // transform: translate(0, 0);
94 | // transition: all 300ms;
95 | }
96 | .side-low-left-fade-enter-done {
97 | // opacity: 1;
98 | // transform: translate(0, 0);
99 | opacity: 1;
100 | transform: translate(0, 0);
101 | transition: all 300ms;
102 | }
103 | .side-low-left-fade-exit {
104 | opacity: 1;
105 | transform: translate(0, 0);
106 | }
107 | .side-low-left-fade-exit-active {
108 | opacity: 0;
109 | transform: translate(10px, 0);
110 | transition: all 300ms;
111 | }
112 | .side-low-left-fade-exit-done {
113 | opacity: 0;
114 | transform: translate(10px, 0);
115 | }
116 |
117 |
118 | // sideLeft
119 | .side-left-enter, .side-left-appear {
120 | opacity: 1;
121 | transform: translate(200%, 0);
122 | }
123 | .side-left-enter-active, .side-left-appear-active {
124 | // transform: translate(0, 0);
125 | // transition: all 300ms;
126 | }
127 | .side-left-enter-done {
128 | // transform: translate(0, 0);
129 | transform: translate(0, 0);
130 | transition: transform 500ms;
131 | }
132 | .side-left-exit {
133 | transform: translate(0, 0);
134 | }
135 | .side-left-exit-active {
136 | transform: translate(200%, 0);
137 | transition: transform 500ms;
138 | }
139 | .side-left-exit-done {
140 | transform: translate(200%, 0);
141 | }
142 |
143 |
144 | @keyframes rotateMusicLogo {
145 | 0% {
146 | transform: rotate(0);
147 | }
148 | 50% {
149 | transform: rotate(180deg);
150 | }
151 | 100% {
152 | transform: rotate(360deg);
153 | }
154 | }
155 | @-webkit-keyframes rotateMusicLogo {
156 | 0% {
157 | transform: rotate(0);
158 | }
159 | 50% {
160 | transform: rotate(180deg);
161 | }
162 | 100% {
163 | transform: rotate(360deg);
164 | }
165 | }
166 | @-moz-keyframes rotateMusicLogo {
167 | 0% {
168 | transform: rotate(0);
169 | }
170 | 50% {
171 | transform: rotate(180deg);
172 | }
173 | 100% {
174 | transform: rotate(360deg);
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/pages/Music/Search/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useLayoutEffect, useState, useMemo, useCallback, useEffect } from 'react'
2 | import className from 'classnames'
3 | import { PROJECT_NAME, MUSIC_SEARCH_DEFAULT_LIMIT } from '../../../config/constance'
4 | import './search.less'
5 | import MusicListGroup from './../../../components/MusicListGroup'
6 | import { isEmptyStr } from 'd-utils/lib/expUtils/index'
7 | import LoadingTips from '../../../components/LoadingTips';
8 | import * as MusicFetch from './../action'
9 | import { parseUrl } from 'd-utils/lib/urlUtils'
10 | import { MusicPlayType } from './../../../store/types'
11 |
12 | import useScroll from './../../../use/useScroll'
13 | import useLoadingTips from './../../../use/useLoadingTips'
14 |
15 | interface MusicSearchProps {
16 | history: any;
17 | }
18 |
19 | const MusicSearch = (props: MusicSearchProps) => {
20 | const searchInput: any = useRef(null)
21 | const searchGroup: any = useRef(null)
22 |
23 | const [offset, setOffset] = useState(0)
24 | const [searchLists, setSearchList] = useState([])
25 |
26 | const [keywords, setKeywords] = useState(parseUrl(decodeURIComponent(location.href)).keywords)
27 |
28 | const loadingTipsFn = useLoadingTips(false, '搜索中...')
29 |
30 | const loadMoreInfo = () => {
31 | if (loadingTipsFn.loading) return
32 | setOffset((offset) => offset = offset + MUSIC_SEARCH_DEFAULT_LIMIT)
33 | }
34 |
35 | useLayoutEffect(() => {
36 | searchInput.current.focus()
37 | }, [searchInput])
38 |
39 | useScroll(searchGroup, loadMoreInfo)
40 |
41 | useEffect(() => {
42 | if (hasSearchList) {
43 | getSearchLists()
44 | } else {
45 | getSearchLists(true)
46 | }
47 | }, [keywords, offset])
48 |
49 | const hasSearchList = useMemo(() => {
50 | return searchLists.length > 0
51 | }, [searchLists])
52 |
53 | const handleSearch = (async (e: any) => {
54 | const event = e || window.event
55 | var code = event.keyCode || event.which || event.charCode
56 | if (code === 13) {
57 | const words = searchInput.current.value
58 | if (isEmptyStr(words) || words === keywords) {
59 | return
60 | }
61 | reset(words)
62 | props.history.replace(`/music/search?keywords=${words}`)
63 | }
64 | })
65 |
66 | const reset = (words: string) => {
67 | setSearchList((searchLists) => searchLists = [])
68 | setOffset((offset) => offset = MUSIC_SEARCH_DEFAULT_LIMIT)
69 | setKeywords((keywords: any) => keywords = words)
70 | }
71 |
72 | const getSearchLists = useCallback(async (isSearch = false) => {
73 | const keywords = searchInput.current.value
74 | if (isEmptyStr(keywords)) {
75 | return
76 | }
77 |
78 | loadingTipsFn.showLoading(isSearch ? '搜索中' : '加载中')
79 |
80 | const res: any = await MusicFetch.getSearchLists(keywords, offset)
81 | const loadLists = Array.isArray(res.result.songs) ? res.result.songs : []
82 | setSearchList((searchLists) => searchLists = searchLists.concat(loadLists))
83 |
84 | // 搜索之后为空的时候判断是否是没有数据 且是否是第一次搜索
85 | if (isSearch && loadLists.length === 0) {
86 | loadingTipsFn.showLoading(`未能搜索到关于 '${keywords}' 相关的各歌曲`)
87 | return
88 | }
89 | loadingTipsFn.hideLoading()
90 | }, [offset])
91 |
92 | const classString = className({
93 | [`${PROJECT_NAME}-music-search`]: true,
94 | [`input-active`]: keywords || hasSearchList
95 | })
96 |
97 | const classSearchGroup = className({
98 | [`${PROJECT_NAME}-music-search-group`]: true,
99 | [`show`]: hasSearchList
100 | })
101 |
102 | return(
103 |
120 | )
121 | }
122 |
123 | export default MusicSearch
124 |
--------------------------------------------------------------------------------
/src/utils/music.ts:
--------------------------------------------------------------------------------
1 | import { artists, artist, MusicGroupList } from './../type'
2 | import { useStore } from './../utils/use'
3 | import { checkMusicById, getMusicDetailById, musicLyricById, sheetDetailById } from './../pages/Music/action'
4 | import DAudio, { IMusicInfo } from './../components/DAudio'
5 | import store from '../store';
6 | import { SELF_SHEET_INFO } from './../config/constance'
7 | import { IMusicLyric, MusicLyricType, MusicPlayType } from './../store/types'
8 |
9 | export const parseDuraiton = (duration: number): string => {
10 | const d = ~~(duration / 1000)
11 | const minT = ~~(d / 60) >= 10 ? ~~(d / 60) : '0' + ~~(d / 60)
12 | const minS = ~~(d % 60) >= 10 ? ~~(d % 60) : '0' + ~~(d % 60)
13 | return minT + ':' + minS
14 | }
15 |
16 | // rank 排行转换成 sheet的数据列表格式
17 | export const formatMusicLists = (lists: any[]) => {
18 | return lists.map((item: any) => {
19 | item.album = item.al
20 | item.artists = item.ar
21 | item.duration = item.dt
22 | return item
23 | })
24 | }
25 |
26 | export const getMusicIndexById = (id: number): any[] => {
27 | const musicQueue = store.musicStore.musicListQueue.slice()
28 | if (musicQueue.length === 0) return [-1, null]
29 | for (let i = 0; i < musicQueue.length; i++) {
30 | if (musicQueue[i].id === id) {
31 | return [i, musicQueue[i]]
32 | }
33 | }
34 | return [-1, null]
35 | }
36 |
37 | export const formatMusicArtists = (artists: artists) => {
38 | return artists.map((artist: artist) => artist.name).join(', ')
39 | }
40 |
41 | export const clipImage = (src: string, w: number = 120, h?: number): string => {
42 | return `${src}?param=${w}y${h ? h : w}`
43 | }
44 |
45 | export const getUrlById = (id: number) => {
46 | return `//music.163.com/song/media/outer/url?id=${id}.mp3`
47 | }
48 |
49 | export const getPlayMuiscList = async (list: MusicGroupList): Promise => {
50 | await checkMusicById(list.id)
51 | const { songs : musicDetail } = await getMusicDetailById(list.id) as any
52 | const formatDetail = formatMusicLists(musicDetail)
53 | // 获取歌词
54 | const res: IMusicLyric = await musicLyricById(list.id) as any
55 | // 歌词类型判断
56 | if (res.nolyric) {
57 | res.lrcType = MusicLyricType.ABSOLUTE
58 | } else if (res.lrc && res.lrc.lyric) {
59 | const { lyric } = res.lrc
60 | res.objLrc = parseLrc(lyric)
61 | res.lrcType = MusicLyricType.HAS_LYRIC
62 | } else {
63 | res.lrcType = MusicLyricType.NOT_FOUND
64 | }
65 | // 提交歌词数据
66 | store.musicStore.setMusicLyric(res)
67 |
68 | return {
69 | id: list.id,
70 | url: getUrlById(list.id),
71 | coverUrl: clipImage(formatDetail[0].album.picUrl),
72 | name: list.name,
73 | singer: formatMusicArtists(list.artists)
74 | }
75 | }
76 |
77 | export const getNextMusicList = (id: number) => {
78 | let [index, queue] = getMusicIndexById(id)
79 | const length = store.musicStore.musicListQueue.slice().length
80 |
81 | // 数据存在
82 | if (index >= 0 && length > 0) {
83 | // 最后一个会自己跳转到第一个
84 | if (index >= length - 1) {
85 | store.musicStore.setMusicPlayIndex(0)
86 | return
87 | }
88 | store.musicStore.setMusicPlayIndex(++index)
89 | return
90 | }
91 | store.musicStore.setMusicPlayIndex(-1)
92 | }
93 |
94 | /**
95 | * 解码音乐歌词
96 | */
97 | export const parseLrc = (lrc: any): any[] => {
98 | if (!lrc) return []
99 | const lyrics = lrc.split('\n')
100 | let lrcObj = []
101 | for (let i = 0; i < lyrics.length; i++) {
102 | // 解码
103 | const lyric = decodeURIComponent(lyrics[i])
104 | const timeReg = /\[\d*:\d*((\.|\:)\d*)*\]/g
105 | const timeRegExpArr = lyric.match(timeReg)
106 | if (!timeRegExpArr) continue
107 | const clause = lyric.replace(timeReg, '')
108 | for (let k = 0, h = timeRegExpArr.length; k < h; k++) {
109 | const t = timeRegExpArr[k]
110 | let min = Number(String(t.match(/\[\d*/i)).slice(1))
111 | let sec = Number(String(t.match(/\:\d*/i)).slice(1))
112 | const time = min * 60 + sec
113 | if (clause) {
114 | lrcObj.push({
115 | t: time,
116 | lrc: clause
117 | })
118 | }
119 | }
120 | }
121 | return lrcObj
122 | }
123 |
124 | export const playDefaultSheet = async () => {
125 | if (store.musicStore.playType !== MusicPlayType.HOME) {
126 | const res: any = await sheetDetailById(SELF_SHEET_INFO.id)
127 | store.musicStore.setMusicPlayTask(formatMusicLists(res.playlist.tracks), 0, MusicPlayType.HOME)
128 | }
129 | const list = await getPlayMuiscList(store.musicStore.currentList)
130 | DAudio.start(list)
131 | }
132 |
--------------------------------------------------------------------------------
/src/pages/Blog/Lists/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useState, useRef, useLayoutEffect} from 'react'
2 | import { CSSTransition, TransitionGroup } from 'react-transition-group';
3 | import classNames from 'classnames'
4 | import './lists.less'
5 | import { getBlogLists, getBolgTags } from './../action'
6 | import LoadingTips from '../../../components/LoadingTips';
7 | import useLoadingTips from './../../../use/useLoadingTips'
8 | import { PROJECT_NAME,
9 | BLOG_LIST_DEFAULT_LIMIT,
10 | BLOG_TAGS_ALL_INFO } from '../../../config/constance'
11 | import BlogList, { IBlogListCategorieOrTag } from '../../../components/BlogList'
12 | import SiderWarp from '../../../components/SiderWarp'
13 | import useScroll from './../../../use/useScroll'
14 | import { parseUrl } from 'd-utils/lib/urlUtils'
15 | import { sleep } from 'd-utils/lib/promiseUtils'
16 |
17 | interface IBlogProps {
18 | history: any;
19 | }
20 |
21 | const Blog: React.FC = (props) => {
22 | const classString = classNames({
23 | [`${PROJECT_NAME}-blog-lists-info`]: true,
24 | })
25 | const loadingTipsFn = useLoadingTips(false, '加载中...')
26 | const [data, setData] = useState([])
27 | const [offset, setOffset] = useState(0)
28 | const [tagLists, setTagLists] = useState([])
29 | const total = useRef(0)
30 | const siderRef = useRef(null)
31 | const start = false
32 |
33 | const { tag: tagName } = parseUrl(decodeURIComponent(location.href))
34 | const [tag, setTag] = useState(tagName)
35 |
36 | const loadMoreInfo = () => {
37 | if (loadingTipsFn.loading) return
38 | setOffset((offset) => offset = offset + BLOG_LIST_DEFAULT_LIMIT)
39 | }
40 |
41 | const filterListsByTagName = async (item: IBlogListCategorieOrTag) => {
42 | props.history.replace(`${props.history.location.pathname}?tag=${item.name}`)
43 |
44 | setData((data) => data = [])
45 | siderRef.current && siderRef.current.hideComp()
46 | await sleep(500)
47 | setOffset((offset) => offset = 0)
48 | setTag((tag) => tag = item.name)
49 | }
50 |
51 | useScroll(document.getElementById('dw-react-web-container'), loadMoreInfo)
52 |
53 | useEffect(() => {
54 | const fetchData = async () => {
55 | if (total.current && offset > total.current) return
56 | loadingTipsFn.showLoading()
57 | const res = await getBlogLists(offset, tag)
58 | loadingTipsFn.hideLoading()
59 | total.current = +res.data.total
60 | setData((data) => data = offset ? data.concat(res.data.lists) : res.data.lists)
61 | }
62 | fetchData()
63 | }, [offset, tag])
64 |
65 | useLayoutEffect (() => {
66 | const fetchTagsList = async () => {
67 | const res: any = await getBolgTags()
68 | setTagLists((tagLists) => tagLists = res.data )
69 | }
70 |
71 | fetchTagsList()
72 | }, [])
73 |
74 | return (
75 |
76 |
77 | {
78 | data.map((item: any, index: number) => (
79 |
91 |
92 |
93 | ))
94 | }
95 |
96 |
97 |
98 | 分类列表
99 |
100 | 全部
102 | {
103 | tagLists && tagLists.length && tagLists.map((item: IBlogListCategorieOrTag, index) => (
104 | { item.name }
107 | ))
108 | }
109 |
110 |
111 |
112 | )
113 | }
114 |
115 | export default Blog
116 |
--------------------------------------------------------------------------------
/src/components/DAudio/d-audio.less:
--------------------------------------------------------------------------------
1 | @d-audio-h: 56px;
2 | @d-audio-w: 240px;
3 |
4 | .d-audio {
5 | position: fixed;
6 | width: @d-audio-w;
7 | height: @d-audio-h;
8 | bottom: 50px;
9 | z-index: @audio-z-index;
10 | left: 50px;
11 | border-radius: 10px;
12 | transition: all 0.3s;
13 | overflow: hidden;
14 | opacity: 0;
15 | visibility: hidden;
16 | user-select: none;
17 | &.show {
18 | opacity: 1;
19 | visibility: visible;
20 | }
21 | &.bottom-left {
22 | left: 50px;
23 | right: unset;
24 | }
25 | &.bottom-right {
26 | right: 50px;
27 | left: unset;
28 | }
29 |
30 | &.rect {
31 | display: flex;
32 | align-items: center;
33 | justify-content: flex-start;
34 | border-radius: 10px;
35 | transform: translateZ(0);
36 | .d-audio-detail {
37 | opacity: 1;
38 | visibility: visible;
39 | transition: all 0.5s;
40 | }
41 | .d-audio-circle {
42 | opacity: 0;
43 | visibility: hidden;
44 | transition: all 0.5s;
45 | }
46 | }
47 |
48 | &.circle {
49 | width: @d-audio-h;
50 | border-radius: 50%;
51 | animation: rotateMusicLogo 8s linear infinite;
52 | animation-play-state: paused;
53 | .d-audio-detail {
54 | opacity: 0;
55 | visibility: hidden;
56 | transition: all 0.5s;
57 | }
58 | .d-audio-circle {
59 | transform: scale(1);
60 | opacity: 1;
61 | visibility: visible;
62 | transition: all 0.5s;
63 | transition-delay: 0s;
64 | }
65 | &.active {
66 | animation-play-state: running;
67 | }
68 | }
69 |
70 | &-circle {
71 | background: pink;
72 | width: @d-audio-h;
73 | height: @d-audio-h;
74 | border-radius: 50%;
75 | opacity: 0;
76 | visibility: hidden;
77 | position: relative;
78 | position: absolute;
79 | top: 0;
80 | left: 0;
81 | overflow: hidden;
82 | z-index: 14;
83 | transform: scale(0.5);
84 | .avatar {
85 | width: 100%;
86 | height: 100%;
87 | display: block;
88 | border: none;
89 | }
90 | .d-audio-range {
91 | position: absolute;
92 | top: 0;
93 | left: 0;
94 | width: @d-audio-h;
95 | height: @d-audio-h;
96 | border-radius: 50%;
97 | transition: border 0.3s;
98 | }
99 | }
100 | &-detail {
101 | display: flex;
102 | align-items: center;
103 | justify-content: flex-start;
104 | opacity: 0;
105 | visibility: hidden;
106 | width: 100%;
107 | transition: all 0.5s;
108 | transform: translateZ(0);
109 | padding: 4px 6px;
110 | z-index: 5;
111 | &-config {
112 | flex: 0 0 84px;
113 | height: 100%;
114 | overflow: hidden;
115 | display: flex;
116 | align-items: center;
117 | padding: 0 4px 0 4px;
118 | box-sizing: border-box;
119 | .d-audio-play-pause, .d-audio-next {
120 | cursor: pointer;
121 | background-color: rgba(0,0,0, 0.18);
122 | border-radius: 50%;
123 | transition: background-color 0.3s;
124 | &:hover {
125 | background-color: rgba(0,0,0, 0.3);
126 | }
127 | }
128 | .d-audio-play-pause {
129 | width: 36px;
130 | height: 36px;
131 | }
132 | .d-audio-next {
133 | margin-left: 10px;
134 | width: 28px;
135 | height: 28px;
136 | }
137 | }
138 | &-info {
139 | flex: 1 1 auto;
140 | flex: 1 1 auto;
141 | height: 100%;
142 | overflow: hidden;
143 | color: #fff;
144 | display: flex;
145 | align-items: center;
146 | justify-content: center;
147 | flex-direction: column;
148 | box-sizing: border-box;
149 | padding: 0 4px 0 4px;
150 | visibility: visible;
151 | opacity: 1;
152 | transform: translateZ(0);
153 | transition: all 0.5s;
154 | cursor: default;
155 | .music-name {
156 | font-size: 16px;
157 | padding: 1px 0;
158 | margin: 0;
159 | font-weight: 200;
160 | text-overflow: ellipsis;
161 | overflow: hidden;
162 | white-space: nowrap;
163 | width: 100%;
164 | text-align: center;
165 | }
166 | .music-singer {
167 | font-weight: 200;
168 | padding: 1px 0;
169 | font-size: 12px;
170 | margin: 0;
171 | color: rgba(255, 255, 255, 0.8);
172 | text-overflow: ellipsis;
173 | overflow: hidden;
174 | white-space: nowrap;
175 | width: 100%;
176 | text-align: center;
177 | margin-bottom: 4px;
178 | }
179 | }
180 | }
181 | &-blur-bg {
182 | position: absolute;
183 | width: 100%;
184 | height: 100%;
185 | top: 0;
186 | left: 0;
187 | z-index: 2;
188 | filter: blur(8px);
189 | transform: scale(1.5);
190 | transition: all 0.3s;
191 | &::before {
192 | content: '';
193 | position: absolute;
194 | top: 0;
195 | left: 0;
196 | right: 0;
197 | bottom: 0;
198 | background: rgba(0,0,0, 0.24);
199 | z-index: 1;
200 | }
201 | }
202 | &-progress {
203 | position: absolute;
204 | bottom: 0;
205 | left: 0;
206 | height: 2px;
207 | width: 0;
208 | z-index: 11;
209 | transition: width 0.3s;
210 | }
211 | }
--------------------------------------------------------------------------------
/src/style/common/_mixin.less:
--------------------------------------------------------------------------------
1 | .lineclamp (@height, @count) {
2 | height: @height;
3 | line-height: @height / @count;
4 | overflow:hidden;
5 | text-overflow:ellipsis;
6 | display:-webkit-box;
7 | -webkit-line-clamp: @count;
8 | word-wrap: break-word;
9 | -webkit-box-orient: vertical;
10 | }
11 |
12 | .textoneline(@height) when (@height = auto) {
13 | height: @height;
14 | line-height: initial;
15 | text-overflow: ellipsis;
16 | white-space: nowrap;
17 | overflow: hidden;
18 | }
19 |
20 | .textoneline(@height) when (@height = height) {
21 | height: @height;
22 | line-height: @height;
23 | text-overflow: ellipsis;
24 | white-space: nowrap;
25 | overflow: hidden;
26 | }
27 |
28 | // top left = > right bottom
29 | .colortext-lt (@color1, @color2) {
30 | position: relative;
31 | background: -webkit-linear-gradient(left top, @color1 , @color2); /* Safari 5.1 - 6.0 */
32 | background: -o-linear-gradient(bottom right, @color1, @color2); /* Opera 11.1 - 12.0 */
33 | background: -moz-linear-gradient(bottom right, @color1, @color2); /* Firefox 3.6 - 15 */
34 | background: linear-gradient(to bottom right, @color1 , @color2); /* 标准的语法 */
35 | -webkit-background-clip: text;
36 | -webkit-text-fill-color: transparent;
37 | }
38 |
39 | // left = > right
40 | .colortext-l (@color1, @color2) {
41 | position: relative;
42 | background: -webkit-linear-gradient(left, @color1 , @color2); /* Safari 5.1 - 6.0 */
43 | background: -o-linear-gradient(right, @color1, @color2); /* Opera 11.1 - 12.0 */
44 | background: -moz-linear-gradient(right, @color1, @color2); /* Firefox 3.6 - 15 */
45 | background: linear-gradient(to right, @color1 , @color2); /* 标准的语法 */
46 | -webkit-background-clip: text;
47 | -webkit-text-fill-color: transparent;
48 | }
49 |
50 | // 添加blur模糊效果
51 | // 如果不是绝对定位,父元素需要设置相对定位
52 | // @blur 为模糊的数值
53 | // @height 区域的高度
54 | // @position 为位置 默认50%
55 | .blur(@blur, @height, @position: 50%, @scale: 1.5) {
56 | position: absolute;
57 | top: 0;
58 | left: 0;
59 | right: 0;
60 | height: @height;
61 | background-repeat: no-repeat;
62 | background-size: cover;
63 | background-position: @position;
64 | -webkit-filter: blur(@blur);
65 | filter: blur(@blur);
66 | -webkit-transform: scale(@scale);
67 | transform: scale(@scale);
68 | overflow: hidden;
69 | &::before{
70 | content: '';
71 | position: absolute;
72 | left: 0;
73 | top: 0;
74 | right: 0;
75 | bottom: 0;
76 | z-index: 1;
77 | background: rgba(0,0,0,0.1);
78 | transition: all 0.3s;
79 | }
80 | &.draken{
81 | &::before{
82 | background: rgba(0,0,0,0.3);
83 | }
84 | }
85 | }
86 |
87 | .blur(@blur, @height, @position: 50%, @scale: 1.5) when (@height = auto) {
88 | bottom: 0;
89 | height: unset;
90 | }
91 |
92 | // 固定底部
93 | .fixfooterflex () {
94 | html{
95 | height: 100%;
96 | }
97 |
98 | body{
99 | display: flex;
100 | flex-direction: column;
101 | min-height: 100%;
102 | font-family: "Hiragino Sans GB","Century Gothic",system, Arial, Verdana, Tahoma,"微软雅黑";
103 | position: relative;
104 | width: 100%;
105 | overflow-x: hidden;
106 | }
107 |
108 | header{
109 | /* 我们希望 header 采用固定的高度,只占用必须的空间 */
110 | /* 0 flex-grow, 0 flex-shrink, auto flex-basis */
111 | flex: 0 0 auto;
112 | }
113 |
114 | .main_content{
115 | /* 将 flex-grow 设置为1,该元素会占用所有的可使用空间
116 | 而其他元素该属性值为0,因此不会得到多余的空间*/
117 | /* 1 flex-grow, 0 flex-shrink, auto flex-basis */
118 | flex: 1 1 auto;
119 | background-color: #fff;
120 | position:relative;
121 | }
122 |
123 | footer{
124 | /* 和 header 一样,footer 也采用固定高度*/
125 | /* 0 flex-grow, 0 flex-shrink, auto flex-basis */
126 | flex: 0 0 auto;
127 | }
128 | }
129 |
130 | // hr 线条和颜色
131 | .hr(@color, @name) {
132 | .hr-@{color} {
133 | width: 100%;
134 | height: 1px;
135 | background: @color;
136 | font-size: 0;
137 | margin: 0 auto;
138 | }
139 | @media (-webkit-min-device-pixel-ratio: 2),(min-device-pixel-ratio: 2) {
140 | transform: translate(0, 1 / 2 * 100'px');
141 | }
142 | @media (-webkit-min-device-pixel-ratio: 3),(min-device-pixel-ratio: 3) {
143 | transform: translate(0, 1 / 2 * 100'px');
144 | }
145 | }
146 |
147 |
148 |
149 | // 色值
150 |
151 | // 主色调
152 | .mixinPrimaryColor() {
153 | color: var(--primary-color);
154 | }
155 |
156 | .mixinPrimaryBg() {
157 | background: var(--primary-color);
158 | }
159 |
160 | // 文字颜色
161 | .mixinTextColor() {
162 | color: var(--text-color);
163 | }
164 |
165 | // 主色调
166 | .mixinTextColorActive() {
167 | color: var(--text-color-active);
168 | }
169 |
170 | // 菜单的背景
171 | .mixinBgNav() {
172 | background-color: var(--bg-nav);
173 | }
174 |
175 | // 菜单的背景hover
176 | .mixinBgNavActive() {
177 | color: var(--primary-color);
178 | background-color: var(--bg-nav-active);
179 | }
180 |
181 | // 主背景
182 | .mixinBg() {
183 | background-color: var(--bg);
184 | }
185 |
186 | .blur(@blur, @height: 50, @position: 50%, @scale: 1.5) {
187 | position: absolute;
188 | top: 0;
189 | left: 0;
190 | right: 0;
191 | background-repeat: no-repeat;
192 | background-size: cover;
193 | background-position: @position;
194 | -webkit-filter: blur(@blur);
195 | filter: blur(@blur);
196 | -webkit-transform: scale(@scale);
197 | transform: scale(@scale);
198 | overflow: hidden;
199 | height: @height;
200 | bottom: 0;
201 | &::before{
202 | content: '';
203 | position: absolute;
204 | left: 0;
205 | top: 0;
206 | right: 0;
207 | bottom: 0;
208 | z-index: 1;
209 | background: rgba(0,0,0,0.1);
210 | transition: all 0.3s;
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/src/assets/svg/tag.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/config/music.ts:
--------------------------------------------------------------------------------
1 | // export const Music
2 | export const MUSCI_MENU = {
3 | "all": {
4 | "name": "全部",
5 | "type": 0,
6 | "category": 4,
7 | "hot": false,
8 | },
9 | "sub": [
10 | {
11 | "name": "流行",
12 | "type": 0,
13 | "category": 5,
14 | "hot": true,
15 | },
16 | {
17 | "name": "华语",
18 | "type": 0,
19 | "category": 5,
20 | "hot": true,
21 | },
22 | {
23 | "name": "放松",
24 | "type": 1,
25 | "category": 5,
26 | "hot": false,
27 | },
28 | {
29 | "name": "清新",
30 | "type": 0,
31 | "category": 5,
32 | "hot": true,
33 | },
34 | {
35 | "name": "90后",
36 | "type": 1,
37 | "category": 5,
38 | "hot": false,
39 | },
40 | {
41 | "name": "经典",
42 | "type": 1,
43 | "category": 5,
44 | "hot": false,
45 | },
46 | {
47 | "name": "电子",
48 | "type": 0,
49 | "category": 5,
50 | "hot": true,
51 | },
52 | {
53 | "name": "乡村",
54 | "type": 1,
55 | "category": 5,
56 | "hot": false,
57 | },
58 | {
59 | "name": "欧美",
60 | "type": 1,
61 | "category": 5,
62 | "hot": false,
63 | },
64 | {
65 | "name": "轻音乐",
66 | "type": 0,
67 | "category": 5,
68 | "hot": true,
69 | },
70 | {
71 | "name": "流行",
72 | "type": 0,
73 | "category": 1,
74 | "hot": true,
75 | },
76 | {
77 | "name": "影视原声",
78 | "type": 0,
79 | "category": 4,
80 | "hot": true,
81 | },
82 | {
83 | "name": "华语",
84 | "type": 0,
85 | "category": 0,
86 | "hot": true,
87 | },
88 | {
89 | "name": "清晨",
90 | "type": 1,
91 | "category": 2,
92 | "hot": false,
93 | },
94 | {
95 | "name": "怀旧",
96 | "type": 0,
97 | "category": 3,
98 | "hot": true,
99 | },
100 | {
101 | "name": "摇滚",
102 | "type": 0,
103 | "category": 1,
104 | "hot": true,
105 | },
106 | {
107 | "name": "夜晚",
108 | "type": 0,
109 | "category": 2,
110 | "hot": true,
111 | },
112 | {
113 | "name": "清新",
114 | "type": 0,
115 | "category": 3,
116 | "hot": true,
117 | },
118 | {
119 | "name": "欧美",
120 | "type": 1,
121 | "category": 0,
122 | "hot": false,
123 | },
124 | {
125 | "name": "学习",
126 | "type": 0,
127 | "category": 2,
128 | "hot": true,
129 | },
130 | {
131 | "name": "民谣",
132 | "type": 0,
133 | "category": 1,
134 | "hot": true,
135 | },
136 | {
137 | "name": "浪漫",
138 | "type": 1,
139 | "category": 3,
140 | "hot": false,
141 | },
142 | {
143 | "name": "日语",
144 | "type": 1,
145 | "category": 0,
146 | "hot": false,
147 | },
148 | {
149 | "name": "工作",
150 | "type": 1,
151 | "category": 2,
152 | "hot": false,
153 | },
154 | {
155 | "name": "电子",
156 | "type": 0,
157 | "category": 1,
158 | "hot": true,
159 | },
160 | {
161 | "name": "校园",
162 | "type": 1,
163 | "category": 4,
164 | "hot": false,
165 | },
166 | {
167 | "name": "韩语",
168 | "type": 1,
169 | "category": 0,
170 | "hot": false,
171 | },
172 | {
173 | "name": "午休",
174 | "type": 1,
175 | "category": 2,
176 | "hot": false,
177 | },
178 | {
179 | "name": "游戏",
180 | "type": 1,
181 | "category": 4,
182 | "hot": false,
183 | },
184 | {
185 | "name": "粤语",
186 | "type": 1,
187 | "category": 0,
188 | "hot": false,
189 | },
190 | {
191 | "name": "小语种",
192 | "type": 1,
193 | "category": 0,
194 | "hot": false,
195 | },
196 | {
197 | "name": "下午茶",
198 | "type": 1,
199 | "category": 2,
200 | "hot": false,
201 | },
202 | {
203 | "name": "治愈",
204 | "type": 0,
205 | "category": 3,
206 | "hot": true,
207 | },
208 | {
209 | "name": "轻音乐",
210 | "type": 0,
211 | "category": 1,
212 | "hot": true,
213 | },
214 | {
215 | "name": "80后",
216 | "type": 1,
217 | "category": 4,
218 | "hot": false,
219 | },
220 | {
221 | "name": "放松",
222 | "type": 1,
223 | "category": 3,
224 | "hot": false,
225 | },
226 | {
227 | "name": "地铁",
228 | "type": 1,
229 | "category": 2,
230 | "hot": false,
231 | },
232 | {
233 | "name": "90后",
234 | "type": 1,
235 | "category": 4,
236 | "hot": false,
237 | },
238 | {
239 | "name": "驾车",
240 | "type": 1,
241 | "category": 2,
242 | "hot": false,
243 | },
244 | {
245 | "name": "孤独",
246 | "type": 1,
247 | "category": 3,
248 | "hot": false,
249 | },
250 | {
251 | "name": "感动",
252 | "type": 1,
253 | "category": 3,
254 | "hot": false,
255 | },
256 | {
257 | "name": "运动",
258 | "type": 0,
259 | "category": 2,
260 | "hot": true,
261 | },
262 | {
263 | "name": "乡村",
264 | "type": 1,
265 | "category": 1,
266 | "hot": false,
267 | },
268 | {
269 | "name": "KTV",
270 | "type": 1,
271 | "category": 4,
272 | "hot": false,
273 | },
274 | {
275 | "name": "旅行",
276 | "type": 1,
277 | "category": 2,
278 | "hot": false,
279 | },
280 | {
281 | "name": "古典",
282 | "type": 1,
283 | "category": 1,
284 | "hot": false,
285 | },
286 | {
287 | "name": "快乐",
288 | "type": 1,
289 | "category": 3,
290 | "hot": false,
291 | },
292 | {
293 | "name": "散步",
294 | "type": 1,
295 | "category": 2,
296 | "hot": false,
297 | },
298 | {
299 | "name": "经典",
300 | "type": 1,
301 | "category": 4,
302 | "hot": false,
303 | },
304 | {
305 | "name": "安静",
306 | "type": 1,
307 | "category": 3,
308 | "hot": false,
309 | },
310 | {
311 | "name": "思念",
312 | "type": 1,
313 | "category": 3,
314 | "hot": false,
315 | },
316 | {
317 | "name": "钢琴",
318 | "type": 1,
319 | "category": 4,
320 | "hot": false,
321 | },
322 | {
323 | "name": "榜单",
324 | "type": 1,
325 | "category": 4,
326 | "hot": false,
327 | },
328 | {
329 | "name": "00后",
330 | "type": 1,
331 | "category": 4,
332 | "hot": false,
333 | },
334 | {
335 | "name": "古风",
336 | "type": 1,
337 | "category": 1,
338 | "hot": false,
339 | }
340 | ],
341 | "categories": {
342 | "0": "语种",
343 | "1": "风格",
344 | "2": "场景",
345 | "3": "情感",
346 | "4": "主题"
347 | }
348 | }
349 |
--------------------------------------------------------------------------------
/src/components/DAudio/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useMemo, useLayoutEffect, useRef, useImperativeHandle, useCallback, useEffect } from 'react'
2 | import classNames from 'classnames'
3 | import Vibrant from 'node-vibrant'
4 | import ReactDOM from 'react-dom';
5 | import './d-audio.less'
6 | import store from './../../store'
7 | import { throttle } from 'd-utils/lib/genericUtils'
8 | import Lyric from './../Lyric'
9 | import Icon from './../Icon'
10 | import { getPlayMuiscList, getNextMusicList} from './../../utils/music'
11 |
12 | export interface IMusicInfo {
13 | id: number;
14 | url: string;
15 | coverUrl: string;
16 | name: string;
17 | singer: string;
18 | }
19 |
20 | interface IDAudioImageColor {
21 | defaultColor: string;
22 | circleBorderColor: string;
23 | progressLeftColor: string;
24 | progressRightColor: string;
25 | }
26 |
27 | enum DAudioType {
28 | // 矩形
29 | RECT = 1,
30 |
31 | // 圆
32 | CIRCLE,
33 | }
34 |
35 | export enum DAudioPosition {
36 | BOTTOM_LEFT = 1,
37 | BOTTOM_RIGHT
38 | }
39 |
40 | export interface IDAudioProps {
41 | position?: DAudioPosition;
42 | type?: DAudioType;
43 | }
44 |
45 | interface IDAudioState {
46 | list: IMusicInfo;
47 | progress: boolean;
48 | loop: boolean;
49 | type: DAudioType;
50 | ended: () => void;
51 | start: (list: IMusicInfo) => void;
52 | next: () => void;
53 | play: () => void;
54 | pause: () => void;
55 | playPause: () => void;
56 | }
57 |
58 | const defaultList = {
59 | id: -1,
60 | url: '',
61 | coverUrl: 'https://www.daiwei.site/static/logo/d-audio.jpg',
62 | name: 'd-audio player',
63 | singer: '未曾遗忘的青春'
64 | }
65 |
66 | const defaultImageColor = {
67 | defaultColor: `rgba(255, 56, 56, 1)`,
68 | circleBorderColor: `rgba(255, 56, 56, 0.36)`,
69 | progressLeftColor: `rgba(255, 56, 56, 0.12)`,
70 | progressRightColor: `rgba(255, 56, 56, 0.66)`,
71 | }
72 |
73 | const DAudio: React.FC = function (props, ref) {
74 | const audioRef = useRef(null)
75 | const [type, setType] = useState(props.type || DAudioType.CIRCLE)
76 | const [list, setList] = useState(defaultList)
77 | const [isPlay, setIsPlay] = useState(false)
78 | const [loading, setLoading] = useState(false)
79 | const [imageColor, setImageColor] = useState({...defaultImageColor})
80 | const [progress, setProgress] = useState(0)
81 | const refCurrentList = useRef('')
82 | refCurrentList.current = list
83 |
84 | useEffect(() => {
85 | changeImageColor()
86 | }, [list.id])
87 |
88 | useEffect(() => {
89 | // audioRef
90 | addSelfObserver()
91 | }, [])
92 |
93 | const changeImageColor = useCallback(async () => {
94 | const palette: any = await Vibrant.from(list.coverUrl).getPalette()
95 | const {r, g, b} = palette.LightMuted
96 | setImageColor((imageColor: IDAudioImageColor): any => {
97 | return imageColor = {
98 | defaultColor: `rgba(${r}, ${g}, ${b}, 1)`,
99 | circleBorderColor: `rgba(${r}, ${g}, ${b}, 0.36)`,
100 | progressLeftColor: `rgba(${r}, ${g}, ${b}, 0.12)`,
101 | progressRightColor: `rgba(${r}, ${g}, ${b}, 0.66)`,
102 | }
103 | })
104 | }, [list.id])
105 |
106 | const blurStyle = {
107 | backgroundImage: `url(${list.coverUrl})`,
108 | backgroundSize: 'cover',
109 | backgroundPosition: 'center center'
110 | }
111 | const rangeStyle = {
112 | border: `3px solid ${imageColor.circleBorderColor}`
113 | }
114 |
115 | const progressStyle = {
116 | backgroundImage: `linear-gradient(to right, ${imageColor.progressLeftColor} 30%, ${imageColor.progressRightColor})`,
117 | width: `${progress}%`
118 | }
119 |
120 | const addSelfObserver = () => {
121 | // 添加video 的事件监听
122 | (audioRef.current as any).addEventListener('timeupdate', handleProgress);
123 | (audioRef.current as any).addEventListener('ended', next);
124 |
125 | return () => {
126 | (audioRef.current as any).removeEventListener('timeupdate', handleProgress)
127 | (audioRef.current as any).removeEventListener('ended', next)
128 | }
129 | }
130 |
131 | const handleProgress = throttle(() => {
132 | const { currentTime, duration } = (audioRef.current as any)
133 | // 歌词的操作
134 | if (store.musicStore.lyricIsShow) {
135 | store.musicStore.setMusicLyricIndex(lyricIndex(currentTime))
136 | Lyric.checkLrc(store.musicStore.currentLyric)
137 | }
138 |
139 | setProgress((progress: number) => progress = Number((currentTime / duration * 100).toFixed(2)))
140 | }, 1500)
141 |
142 | const lyricIndex = (currentT: number) => {
143 | const objLrc = store.musicStore.musicLyric.objLrc
144 | let activeIndex = -1
145 | if (objLrc && objLrc.length) {
146 | for (let i = 0; i < objLrc.length; i++) {
147 | if (currentT > objLrc[i].t) {
148 | activeIndex = i
149 | } else {
150 | break
151 | }
152 | }
153 | }
154 | return activeIndex
155 | }
156 |
157 | const selfClass = classNames({
158 | [`d-audio`]: true,
159 | })
160 |
161 | const classString = classNames({
162 | [`d-audio`]: true,
163 | [`bottom-left`]: props.position === DAudioPosition.BOTTOM_LEFT,
164 | [`bottom-right`]: props.position === DAudioPosition.BOTTOM_RIGHT,
165 | [`circle`]: type === DAudioType.CIRCLE,
166 | [`rect`]: type === DAudioType.RECT,
167 | [`active`]: isPlay && type === DAudioType.CIRCLE,
168 | [`show`]: store && store.musicStore && store.musicStore.musicListQueue.length
169 | })
170 |
171 | const classCricle = classNames({
172 | [`d-audio-circle`]: true
173 | })
174 |
175 | const classPlayPause = classNames({
176 | [`d-audio-play-pause`]: true,
177 | [`play`]: isPlay,
178 | [`pause`]: !isPlay
179 | })
180 |
181 | const classLoading = classNames({
182 | [`d-audio-loading`]: true,
183 | [`active`]: loading
184 | })
185 |
186 | const start = (musicList: IMusicInfo) => {
187 | setList((list) => musicList)
188 | play()
189 | Lyric.checkLrc(store.musicStore.currentLyric)
190 | }
191 |
192 | const play = () => {
193 | setIsPlay(isPlay => isPlay = true);
194 | (audioRef.current as any).play()
195 | }
196 |
197 | const next = async (e: any) => {
198 | e.preventDefault()
199 | e.stopPropagation()
200 | getNextMusicList(refCurrentList.current.id)
201 |
202 | const selfList = await getPlayMuiscList(store.musicStore.currentList)
203 | start(selfList)
204 | }
205 |
206 | const pause = () => {
207 | setIsPlay(isPlay => isPlay = false);
208 | (audioRef.current as any).pause()
209 | }
210 |
211 | const playPause = (e: any) => {
212 | e.preventDefault()
213 | e.stopPropagation()
214 | isPlay ? pause() : play()
215 | }
216 |
217 | const checkType = () => {
218 | if (type === DAudioType.RECT) {
219 | setType((type) => type = DAudioType.CIRCLE)
220 | } else {
221 | setType((type) => type = DAudioType.RECT)
222 | }
223 | }
224 |
225 | const playPauseSvg = isPlay ? 'pause' : 'play'
226 |
227 | useImperativeHandle(ref, () => ({
228 | start,
229 | next,
230 | play,
231 | pause,
232 | }));
233 |
234 | return (
235 |
236 |
238 |

239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
{list.name}
252 |
{list.singer}
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 | )
261 | }
262 |
263 | const DuadioComponent = React.forwardRef(DAudio)
264 |
265 | function newInstance(props: IDAudioProps) {
266 | const DAudioRef = React.createRef();
267 |
268 | const div = document.createElement('div')
269 | document.body.appendChild(div)
270 | ReactDOM.render(, div)
271 | const destroy = () => {
272 | ReactDOM.unmountComponentAtNode(div);
273 | (div.parentNode as HTMLDivElement ).removeChild(div);
274 | }
275 |
276 | const { list, loop, type, ended, next, start, play, pause, playPause } = DAudioRef.current as IDAudioState;
277 |
278 | return {
279 | list,
280 | loop,
281 | type,
282 | start,
283 | ended,
284 | next,
285 | destroy,
286 | play,
287 | pause,
288 | playPause
289 | }
290 | }
291 |
292 | export default newInstance({
293 | position: DAudioPosition.BOTTOM_LEFT
294 | })
295 |
--------------------------------------------------------------------------------
/src/assets/svg/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
122 |
--------------------------------------------------------------------------------
/src/style/high-default.less:
--------------------------------------------------------------------------------
1 | .@{blog-prefix}-detail {
2 | textarea {
3 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
4 | font-size: 12px;
5 | resize: none;
6 | }
7 |
8 | img {
9 | display: block;
10 | max-width: 100%;
11 | height: auto;
12 | margin: 10px auto;
13 | border-radius: 4px;
14 | }
15 | @media screen and (min-width: 768px) {
16 | body {
17 | width: 748px;
18 | margin: 10px auto;
19 | }
20 | }
21 | h1, h2, h3, h4 {
22 | color: #111111;
23 | font-weight: 400;
24 | margin-top: 1em;
25 | }
26 |
27 | h1, h2, h3, h4, h5 {
28 | color: var(--h-color);
29 | // font-family: Georgia, Palatino, serif;
30 | }
31 | h1, h2, h3, h4, h5, p , dl{
32 | margin-bottom: 16px;
33 | padding: 0;
34 | }
35 | h1 {
36 | font-size: 48px;
37 | line-height: 60px;
38 | }
39 | h2 {
40 | font-size: 36px;
41 | line-height: 54px;
42 | }
43 | h1, h2 {
44 | border-bottom: 1px solid var(--border-color-deep);
45 | padding-bottom: 10px;
46 | }
47 | h3 {
48 | font-size: 26px;
49 | line-height: 50px;
50 | }
51 | h4 {
52 | font-size: 20px;
53 | line-height: 36px;
54 | }
55 | h5 {
56 | font-size: 18px;
57 | list-style: 28px;
58 | }
59 | a {
60 | color: var(--text-color);
61 | text-decoration: underline;
62 | margin: 0;
63 | padding: 0;
64 | vertical-align: baseline;
65 | }
66 | a:hover {
67 | color: var(--primary-color);
68 | }
69 | ul, ol {
70 | padding: 0;
71 | padding-left: 24px;
72 | margin: 0;
73 | }
74 | li {
75 | line-height: 1.7;
76 | list-style-type: disc;
77 | }
78 | p, ul, ol {
79 | font-size: 16px;
80 | line-height: 1.7;
81 | }
82 |
83 | ol ol, ul ol {
84 | list-style-type: lower-roman;
85 | }
86 |
87 | /*pre {
88 | padding: 0px 24px;
89 | max-width: 800px;
90 | white-space: pre-wrap;
91 | }
92 | code {
93 | font-family: Consolas, Monaco, Andale Mono, monospace;
94 | line-height: 1.5;
95 | font-size: 13px;
96 | }*/
97 |
98 | code, pre {
99 | border-radius: 3px;
100 | background-color: var(--shadow-color);
101 | color: inherit;
102 | }
103 |
104 | code {
105 | font-family: Consolas, Monaco, Andale Mono, monospace;
106 | margin: 0 2px;
107 | }
108 |
109 | pre {
110 | line-height: 1.7em;
111 | overflow: auto;
112 | padding: 15px 15px;
113 | }
114 |
115 |
116 | aside {
117 | display: block;
118 | float: right;
119 | width: 390px;
120 | }
121 | blockquote {
122 | border-left:.5em solid #eee;
123 | padding: 0 0 0 2em;
124 | margin-left:0;
125 | }
126 | blockquote cite {
127 | font-size:14px;
128 | line-height:20px;
129 | color:#bfbfbf;
130 | }
131 | blockquote cite:before {
132 | content: '\2014 \00A0';
133 | }
134 |
135 | blockquote p {
136 | color: #666;
137 | }
138 | hr {
139 | text-align: left;
140 | color: #999;
141 | height: 2px;
142 | padding: 0;
143 | margin: 16px 0;
144 | background-color: #e7e7e7;
145 | border: 0 none;
146 | }
147 |
148 | dl {
149 | padding: 0;
150 | }
151 |
152 | dl dt {
153 | padding: 10px 0;
154 | margin-top: 16px;
155 | font-size: 1em;
156 | font-style: italic;
157 | font-weight: bold;
158 | }
159 |
160 | dl dd {
161 | padding: 0 16px;
162 | margin-bottom: 16px;
163 | }
164 |
165 | dd {
166 | margin-left: 0;
167 | }
168 |
169 | /* Code below this line is copyright Twitter Inc. */
170 |
171 | button,
172 | input,
173 | select,
174 | textarea {
175 | font-size: 100%;
176 | margin: 0;
177 | vertical-align: baseline;
178 | *vertical-align: middle;
179 | }
180 | button, input {
181 | line-height: normal;
182 | *overflow: visible;
183 | }
184 | button::-moz-focus-inner, input::-moz-focus-inner {
185 | border: 0;
186 | padding: 0;
187 | }
188 | button,
189 | input[type="button"],
190 | input[type="reset"],
191 | input[type="submit"] {
192 | cursor: pointer;
193 | -webkit-appearance: button;
194 | }
195 | input[type=checkbox], input[type=radio] {
196 | cursor: pointer;
197 | }
198 | /* override default chrome & firefox settings */
199 | input:not([type="image"]), textarea {
200 | -webkit-box-sizing: content-box;
201 | -moz-box-sizing: content-box;
202 | box-sizing: content-box;
203 | }
204 |
205 | input[type="search"] {
206 | -webkit-appearance: textfield;
207 | -webkit-box-sizing: content-box;
208 | -moz-box-sizing: content-box;
209 | box-sizing: content-box;
210 | }
211 | input[type="search"]::-webkit-search-decoration {
212 | -webkit-appearance: none;
213 | }
214 | label,
215 | input,
216 | select,
217 | textarea {
218 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
219 | font-size: 13px;
220 | font-weight: normal;
221 | line-height: normal;
222 | margin-bottom: 18px;
223 | }
224 | input[type=checkbox], input[type=radio] {
225 | cursor: pointer;
226 | margin-bottom: 0;
227 | }
228 | input[type=text],
229 | input[type=password],
230 | textarea,
231 | select {
232 | display: inline-block;
233 | width: 210px;
234 | padding: 4px;
235 | font-size: 13px;
236 | font-weight: normal;
237 | line-height: 18px;
238 | height: 18px;
239 | color: #808080;
240 | border: 1px solid var(--border-color-deep);
241 | -webkit-border-radius: 3px;
242 | -moz-border-radius: 3px;
243 | border-radius: 3px;
244 | }
245 | select, input[type=file] {
246 | height: 27px;
247 | line-height: 27px;
248 | }
249 | textarea {
250 | height: auto;
251 | }
252 | /* grey out placeholders */
253 | :-moz-placeholder {
254 | color: #bfbfbf;
255 | }
256 | ::-webkit-input-placeholder {
257 | color: #bfbfbf;
258 | }
259 | input[type=text],
260 | input[type=password],
261 | select,
262 | textarea {
263 | -webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
264 | -moz-transition: border linear 0.2s, box-shadow linear 0.2s;
265 | transition: border linear 0.2s, box-shadow linear 0.2s;
266 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
267 | -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
268 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
269 | }
270 | input[type=text]:focus, input[type=password]:focus, textarea:focus {
271 | outline: none;
272 | border-color: rgba(82, 168, 236, 0.8);
273 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
274 | -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
275 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
276 | }
277 | /* buttons */
278 | button {
279 | display: inline-block;
280 | padding: 4px 14px;
281 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
282 | font-size: 13px;
283 | line-height: 18px;
284 | -webkit-border-radius: 4px;
285 | -moz-border-radius: 4px;
286 | border-radius: 4px;
287 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
288 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
289 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
290 | background-color: #0064cd;
291 | background-repeat: repeat-x;
292 | background-image: -khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd));
293 | background-image: -moz-linear-gradient(top, #049cdb, #0064cd);
294 | background-image: -ms-linear-gradient(top, #049cdb, #0064cd);
295 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd));
296 | background-image: -webkit-linear-gradient(top, #049cdb, #0064cd);
297 | background-image: -o-linear-gradient(top, #049cdb, #0064cd);
298 | background-image: linear-gradient(top, #049cdb, #0064cd);
299 | color: #fff;
300 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
301 | border: 1px solid #004b9a;
302 | border-bottom-color: #003f81;
303 | -webkit-transition: 0.1s linear all;
304 | -moz-transition: 0.1s linear all;
305 | transition: 0.1s linear all;
306 | border-color: #0064cd #0064cd #003f81;
307 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
308 | }
309 | button:hover {
310 | color: #fff;
311 | background-position: 0 -15px;
312 | text-decoration: none;
313 | }
314 | button:active {
315 | -webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
316 | -moz-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
317 | box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
318 | }
319 | button::-moz-focus-inner {
320 | padding: 0;
321 | border: 0;
322 | }
323 | table {
324 | *border-collapse: collapse; /* IE7 and lower */
325 | border-spacing: 0;
326 | width: 100%;
327 | }
328 | table {
329 | border: solid var(--border-color-deep) 1px;
330 | border-radius: 2px;
331 | }
332 | table thead tr {
333 | line-height: 2;
334 | }
335 |
336 | table tr:hover {
337 | background: var(--bg-nav-active);
338 | -o-transition: all 0.1s ease-in-out;
339 | -webkit-transition: all 0.1s ease-in-out;
340 | -moz-transition: all 0.1s ease-in-out;
341 | -ms-transition: all 0.1s ease-in-out;
342 | transition: all 0.1s ease-in-out;
343 | }
344 | table td, .table th {
345 | border-left: 1px solid var(--border-color-deep);
346 | border-top: 1px solid var(--border-color-deep);
347 | padding: 10px;
348 | text-align: left;
349 | }
350 |
351 | table th {
352 | background-color: var(--primary-color);
353 | border-top: none;
354 | color: #fff;
355 | padding: 5px;
356 | }
357 |
358 | table td:first-child, table th:first-child {
359 | border-left: none;
360 | }
361 |
362 | table th:first-child {
363 | -moz-border-radius: 2px 0 0 0;
364 | -webkit-border-radius: 2px 0 0 0;
365 | border-radius: 2px 0 0 0;
366 | }
367 | table th:last-child {
368 | -moz-border-radius: 0 2px 0 0;
369 | -webkit-border-radius: 0 2px 0 0;
370 | border-radius: 0 2px 0 0;
371 | }
372 | table th:only-child{
373 | -moz-border-radius: 2px 2px 0 0;
374 | -webkit-border-radius: 2px 2px 0 0;
375 | border-radius: 2px 2px 0 0;
376 | }
377 | table tr:last-child td:first-child {
378 | -moz-border-radius: 0 0 0 2px;
379 | -webkit-border-radius: 0 0 0 2px;
380 | border-radius: 0 0 0 2px;
381 | }
382 | table tr:last-child td:last-child {
383 | -moz-border-radius: 0 0 2px 0;
384 | -webkit-border-radius: 0 0 2px 0;
385 | border-radius: 0 0 2px 0;
386 | }
387 |
388 | header {
389 | padding-top: 10px;
390 | display: flex;
391 | height: 58px;
392 | }
393 |
394 | header h1 {
395 | margin: 0;
396 | }
397 |
398 | .github-ribbon {
399 | position: absolute;
400 | top: 0;
401 | right: 0;
402 | border: 0;
403 | z-index: 1000;
404 | }
405 |
406 | .containers {
407 | display: flex;
408 | height: calc(100vh - 68px);
409 | }
410 |
411 | .container {
412 | flex-basis: 50%;
413 | padding: 5px;
414 | display: flex;
415 | flex-direction: column;
416 | height: 100%;
417 | box-sizing: border-box;
418 | }
419 |
420 | .pane, .inputPane {
421 | margin-top: 5px;
422 | padding: 0.6em;
423 | border: 1px solid var(--border-color-deep);
424 | overflow: auto;
425 | flex-grow: 1;
426 | flex-shrink: 1;
427 | }
428 |
429 | #preview {
430 | display: flex;
431 | }
432 |
433 | #preview iframe {
434 | flex-grow: 1;
435 | }
436 |
437 | #main {
438 | display: none;
439 | }
440 |
441 | .error {
442 | border-color: red;
443 | background-color: #FEE
444 | }
445 | }
446 |
447 |
--------------------------------------------------------------------------------