feat: alot

This commit is contained in:
mozzie 2023-03-17 17:58:37 +08:00
parent ca8111dab0
commit 3514c1e395
35 changed files with 674 additions and 1029 deletions

View File

@ -14,7 +14,7 @@
"@midwayjs/logger": "^2.14.0", "@midwayjs/logger": "^2.14.0",
"@midwayjs/validate": "^3.0.0", "@midwayjs/validate": "^3.0.0",
"@midwayjs/static-file": "^3.0.0", "@midwayjs/static-file": "^3.0.0",
"@midwayjs/redis": "^3.0.0", "@midwayjs/redis": "3.10.13",
"@midwayjs/typeorm": "^3.0.0", "@midwayjs/typeorm": "^3.0.0",
"@midwayjs/upload": "3.10.14", "@midwayjs/upload": "3.10.14",
"mongoose": "^6.0.7", "mongoose": "^6.0.7",

View File

@ -105,10 +105,14 @@ export class UserController {
try { try {
const { phoneNumber: phoneNumbers } = params; const { phoneNumber: phoneNumbers } = params;
const code = Math.floor(Math.random() * 9000 + 1000); const code = Math.floor(Math.random() * 9000 + 1000);
await this.redisService.set('' + phoneNumbers, code, 'EX', 60); const res = await this.redisService.set(
console.log('redis here'); '' + phoneNumbers,
await this.smsService.send({ code, phoneNumbers }); code,
console.log('sms here'); 'EX',
60
);
console.log('redis here', res);
// await this.smsService.send({ code, phoneNumbers });
return { code: BizCode.OK }; return { code: BizCode.OK };
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title> <title>回溯 - Backset.cn</title>
</head> </head>
<body> <body>
<div id="bs-app"></div> <div id="bs-app"></div>

View File

@ -13,7 +13,6 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "6.8.0", "react-router-dom": "6.8.0",
"@arco-design/web-react": "2.45.0",
"@ricons/fluent": "0.12.0", "@ricons/fluent": "0.12.0",
"@ricons/utils": "0.1.6", "@ricons/utils": "0.1.6",
"dplayer": "1.27.1", "dplayer": "1.27.1",
@ -28,6 +27,7 @@
"vite-tsconfig-paths": "4.0.5", "vite-tsconfig-paths": "4.0.5",
"typescript": "^4.9.3", "typescript": "^4.9.3",
"vite": "^4.1.0", "vite": "^4.1.0",
"@types/dplayer": "1.25.2" "@types/dplayer": "1.25.2",
"@types/identicon.js": "2.3.1"
} }
} }

BIN
apps/web/public/ali256.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
apps/web/public/alipay.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
apps/web/public/wx.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
apps/web/public/wx256.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -1,6 +1,5 @@
import { Spin } from "@arco-design/web-react";
import { Suspense } from "react"; import { Suspense } from "react";
import { Route, Routes, useNavigate } from "react-router-dom"; import { Route, Routes } from "react-router-dom";
import "./assets/base.less"; import "./assets/base.less";
import Nav from "./components/Nav"; import Nav from "./components/Nav";
import { commonRouters, lazyRouters } from "./router"; import { commonRouters, lazyRouters } from "./router";
@ -24,7 +23,7 @@ function App() {
key={router.path} key={router.path}
path={router.path} path={router.path}
element={ element={
<Suspense fallback={<Spin />}> <Suspense fallback={"loading"}>
<Guard>{<router.element />}</Guard> <Guard>{<router.element />}</Guard>
</Suspense> </Suspense>
} }

View File

@ -1,7 +1,5 @@
// import { message } from "antd"; // import { message } from "antd";
import { Message } from "@arco-design/web-react";
import axios from "axios"; import axios from "axios";
import { useLoginModalStore } from "../store/modal.store";
const config = { const config = {
baseURL: "", baseURL: "",
@ -13,7 +11,6 @@ const instance = axios.create(config);
instance.interceptors.request.use( instance.interceptors.request.use(
(config) => { (config) => {
console.log(config);
return config; return config;
}, },
(error) => { (error) => {
@ -27,7 +24,7 @@ instance.interceptors.response.use(
// midwayjs校验 // midwayjs校验
if ("success" in response.data) { if ("success" in response.data) {
const { success, message } = response.data; const { success, message } = response.data;
if (!success) return Message.error(message); if (!success) return console.error(message);
} }
// 业务校验 // 业务校验
const { msg, code } = response.data; const { msg, code } = response.data;
@ -39,7 +36,7 @@ instance.interceptors.response.use(
// Message.error(`接口: ${response.config.url}, 遇到错误`); // Message.error(`接口: ${response.config.url}, 遇到错误`);
break; break;
case 40000: case 40000:
Message.error(msg); console.error(msg);
// console.log('登录') // console.log('登录')
break; break;
default: default:

View File

@ -1,5 +1,23 @@
@import "normalize.css"; @import "normalize.css";
@import "@arco-design/web-react/dist/css/arco.css"; // @import "@arco-design/web-react/dist/css/arco.css";
:root {
--color-primary-1: #006eed;
--color-primary-2: #54a3ff;
--color-fill-1: #f7f8fa;
--color-fill-2: #f2f3f5;
--color-fill-3: #e5e6eb;
--color-fill-4: #c9cdd4;
--color-border-1: #f2f3f5;
--color-border-2: #e5e6eb;
--color-border-3: #c9cdd4;
--color-border-4: #86909c;
--color-text-1: #1d2129;
--color-text-2: #4e5969;
--color-text-3: #86909c;
--color-text-4: #c9cdd4;
--color-text-5: #ededed;
}
* { * {
box-sizing: border-box; box-sizing: border-box;
@ -46,17 +64,17 @@ input {
border: 0; border: 0;
} }
.bs-shadow { .bs {
box-shadow: rgb(0 0 0 / 13%) 0px 2px 4px 0px, rgb(0 0 0 / 11%) 0px 1px 1px 0px; &.shadow {
} box-shadow: rgb(0 0 0 / 13%) 0px 2px 4px 0px,
rgb(0 0 0 / 11%) 0px 1px 1px 0px;
.bs-ellipsis { }
&.ellipsis {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
&.scrollbar {
.bs-scrollbar {
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 12px; width: 12px;
height: 4px; height: 4px;
@ -67,4 +85,124 @@ input {
border-radius: 7px; border-radius: 7px;
background-color: var(--color-text-4); background-color: var(--color-text-4);
} }
}
&.fc {
display: flex;
align-items: center;
&.c {
justify-content: center;
}
&.sb {
justify-content: space-between;
}
&.fe {
justify-content: flex-end;
}
}
&.mr6 {
margin-right: 6px;
}
&.ml6 {
margin-left: 6px;
}
&.ml12 {
margin-left: 12px;
}
&.mr12 {
margin-right: 12px;
}
&.mr24 {
margin-right: 24px;
}
&.mt12 {
margin-top: 12px;
}
&.mt24 {
margin-top: 24px;
}
&.br3 {
border-radius: 3px;
}
&.br6 {
border-radius: 6px;
}
&.tip {
position: relative;
&:hover {
&::after {
animation-name: tooltip-appear;
animation-duration: 0.1s;
animation-fill-mode: forwards;
animation-timing-function: ease-in;
animation-delay: 0.4s;
position: absolute;
content: attr(data-tip);
opacity: 0;
top: 100%;
right: 50%;
margin-top: 6px;
padding: 0.5em 0.75em;
z-index: 1000000;
background: var(--color-text-1);
color: var(--color-text-5);
font-size: 12px;
border-radius: 3px;
word-wrap: break-word;
text-align: center;
white-space: pre;
pointer-events: none;
}
}
}
&.badge {
position: relative;
&::before {
position: absolute;
content: "";
top: -2.5px;
left: -2.5px;
width: 10px;
height: 10px;
background-image: linear-gradient(#54a3ff, #006eed);
background-clip: padding-box;
border: 2px solid #24292f;
border-radius: 50%;
}
}
&.btn {
background-color: rgb(36, 41, 47);
color: #fff;
border: 0;
padding: 0.5rem 1rem;
cursor: pointer;
transition: all 0.25s ease;
&:active {
background: rgba(0, 0, 0, 1);
}
&.outline {
color: rgb(36, 41, 47);
border: 1px solid rgb(36, 41, 47);
background: transparent;
}
}
}
@keyframes tooltip-appear {
from {
opacity: 0;
}
to {
opacity: 1;
}
} }

View File

@ -1,63 +0,0 @@
import { Card } from "@arco-design/web-react";
import { MouseEventHandler } from "react";
import "./index.less";
const { Meta } = Card;
interface IProps {
imgUrl: string;
title: string;
meta?: {
desc?: string;
action?: string;
};
styles?: {};
onClick?: MouseEventHandler;
}
function BsCard(props: IProps) {
const { imgUrl, title, meta, styles, ...rest } = props;
return meta ? (
<Card
{...rest}
className="bs-card"
hoverable
style={{ ...styles }}
cover={
<div
className="cover"
style={{
backgroundImage: `url('${imgUrl}')`,
}}
>
<div className="mask">
<p>{title}</p>
</div>
</div>
}
>
<Meta
description={
<div className="bottom-des">
<span className="bs-ellipsis">{meta?.desc}</span>
<a>{meta?.action}</a>
</div>
}
/>
</Card>
) : (
<Card {...rest} className="bs-card mini" hoverable style={{ ...styles }}>
<div
className="cover"
style={{
backgroundImage: `url('${imgUrl}')`,
}}
>
<div className="mask">
<p>{title}</p>
</div>
</div>
</Card>
);
}
export default BsCard;

View File

@ -11,8 +11,4 @@
color: var(--color-text-3); color: var(--color-text-3);
text-decoration: none; text-decoration: none;
} }
> svg {
width: 20px;
height: 20px;
}
} }

View File

@ -1,5 +1,4 @@
import "./index.less"; import "./index.less";
import { Tooltip } from "@arco-design/web-react";
export const Footer = () => { export const Footer = () => {
return ( return (
@ -11,22 +10,6 @@ export const Footer = () => {
ICP证: 苏ICP备19008833号-5 ICP证: 苏ICP备19008833号-5
</a> </a>
</span> </span>
<Tooltip position="top" trigger="hover" content="x-arctanx">
<svg viewBox="0 0 32 32">
<path
d="M27.086 24.78A6.618 6.618 0 0 0 30 19.465c0-3.88-3.776-7.027-8.434-7.027s-8.434 3.147-8.434 7.027s3.777 7.028 8.434 7.028a9.955 9.955 0 0 0 2.754-.385l.247-.037a.892.892 0 0 1 .448.13l1.847 1.066l.162.053a.281.281 0 0 0 .281-.282l-.045-.205l-.38-1.417l-.03-.18a.56.56 0 0 1 .236-.458zM12.12 4.68C6.53 4.68 2 8.455 2 13.114a7.939 7.939 0 0 0 3.497 6.374a.671.671 0 0 1 .283.55l-.035.215l-.456 1.701l-.055.246a.338.338 0 0 0 .337.338l.196-.063l2.216-1.28a1.058 1.058 0 0 1 .536-.155l.298.044a11.967 11.967 0 0 0 3.304.464l.555-.014a6.515 6.515 0 0 1-.34-2.067c0-4.247 4.133-7.691 9.23-7.691l.55.014c-.762-4.029-4.947-7.11-9.995-7.11zm6.633 13.663a1.125 1.125 0 1 1 1.125-1.125a1.124 1.124 0 0 1-1.125 1.125zm5.624 0a1.125 1.125 0 1 1 1.123-1.125a1.125 1.125 0 0 1-1.123 1.125zm-15.631-6.58a1.35 1.35 0 1 1 1.35-1.348a1.349 1.349 0 0 1-1.35 1.349zm6.747 0a1.35 1.35 0 1 1 1.35-1.348a1.349 1.349 0 0 1-1.35 1.349z"
fill="currentColor"
></path>
</svg>
</Tooltip>
<Tooltip position="top" trigger="hover" content="1141143000">
<svg viewBox="0 0 1024 1024">
<path
d="M824.8 613.2c-16-51.4-34.4-94.6-62.7-165.3C766.5 262.2 689.3 112 511.5 112C331.7 112 256.2 265.2 261 447.9c-28.4 70.8-46.7 113.7-62.7 165.3c-34 109.5-23 154.8-14.6 155.8c18 2.2 70.1-82.4 70.1-82.4c0 49 25.2 112.9 79.8 159c-26.4 8.1-85.7 29.9-71.6 53.8c11.4 19.3 196.2 12.3 249.5 6.3c53.3 6 238.1 13 249.5-6.3c14.1-23.8-45.3-45.7-71.6-53.8c54.6-46.2 79.8-110.1 79.8-159c0 0 52.1 84.6 70.1 82.4c8.5-1.1 19.5-46.4-14.5-155.8z"
fill="currentColor"
></path>
</svg>
</Tooltip>
</footer> </footer>
); );
}; };

View File

@ -51,8 +51,6 @@ header {
} }
.end { .end {
text-align: right;
.btn { .btn {
cursor: pointer; cursor: pointer;
margin-left: 32px; margin-left: 32px;
@ -61,10 +59,64 @@ header {
color: #fff; color: #fff;
} }
} }
.user { .profile {
font-size: 12px; .avatar-ddl {
> div { position: relative;
line-height: 1; z-index: 19940121;
.ddl {
position: absolute;
right: 0;
background: #fff;
color: var(--color-text-1);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
margin-top: 11px;
padding: 8px 0;
border-radius: 3px;
text-align: left;
min-width: 120px;
.header {
font-size: 13px;
div {
padding: 6px 12px;
color: var(--color-text-3);
border-bottom: 1px solid var(--color-border-1);
}
}
.main {
padding-top: 8px;
div {
padding: 6px 12px;
cursor: pointer;
font-size: 14px;
color: var(--color-text-2);
transition: all 0.25s ease;
&:hover {
color: var(--color-text-1);
background: var(--color-fill-3);
}
}
}
}
}
.avatar,
.alert {
cursor: pointer;
img {
margin-right: 2px;
border-radius: 50%;
width: 22px;
height: 22px;
}
svg {
transition: all 0.25s ease;
}
&:hover {
svg {
color: var(--color-text-4);
}
}
} }
} }
} }

View File

@ -2,77 +2,39 @@ import "./index.less";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { menuRouters } from "../../router"; import { menuRouters } from "../../router";
import { useUserStore } from "../../store/user.store"; import { useUserStore } from "../../store/user.store";
import {
Avatar,
Button,
Dropdown,
Link,
Menu,
Space,
Tooltip,
} from "@arco-design/web-react";
import {
IconDown,
IconImport,
IconSettings,
} from "@arco-design/web-react/icon";
import Identicon from "identicon.js"; import Identicon from "identicon.js";
import { Icon } from "@ricons/utils";
const vip1 = ( import CaretDown20Filled from "@ricons/fluent/CaretDown20Filled";
<svg viewBox="0 0 1024 1024" width="20" height="20" fill="currentColor"> import Alert20Regular from "@ricons/fluent/Alert20Regular";
<path d="M849.2 6.3H174.8C81.7 6.3 6.3 81.7 6.3 174.8v674.3c0 93.1 75.4 168.6 168.6 168.6h674.3c93.1 0 168.6-75.4 168.6-168.6V175c0-93.1-75.5-168.7-168.6-168.7z m-81 632.8l7.9-44.9 47.3-30.5h42.2l-52 295h-42.2l44.1-250.3-47.3 30.7z m104.7-385.2v44h-42.7l-536.6 552h-159V297.3l-39.8-0.6v-42.8h158.9v401.2l397.4-401.2h221.8z"></path> import DoorArrowRight20Regular from "@ricons/fluent/DoorArrowRight20Regular";
</svg> import Settings20Regular from "@ricons/fluent/Settings20Regular";
); import PremiumPerson20Regular from "@ricons/fluent/PremiumPerson20Regular";
import { useState } from "react";
const vip2 = ( import { useMount } from "../../hook";
<svg viewBox="0 0 1024 1024" width="20" height="20" fill="currentColor">
<path d="M849.2 6.3H174.8C81.7 6.3 6.3 81.7 6.3 174.8v674.3c0 93.1 75.4 168.6 168.6 168.6h674.3c93.1 0 168.6-75.4 168.6-168.6V175c0-93.1-75.5-168.7-168.6-168.7z m23.7 247.6v44h-42.7l-536.6 552h-159V297.3l-39.8-0.6v-42.8h158.9v401.2l397.4-401.2h221.8zM711.4 818.8l140.5-144.1c5.4-5.8 9.5-11.2 12-16.2 1.9-4.7 3.2-9.1 4-13.3 2-11.1 0.2-20.7-5.2-28.9-5.7-8.3-15.1-12.5-28.2-12.7-11.6 0-21.9 3.7-30.9 11.1-9.3 7.4-15.8 17.9-19.6 31.4h-42.1c5.4-24.6 17.2-44.8 35.4-60.9 18.3-15.7 38.8-23.6 61.5-23.8 25.3 0.2 44.5 8.4 57.4 24.3 13.2 15.8 17.9 35.8 13.8 60.1-3.5 19.4-12.3 36.7-26.9 51.7L767.3 816.4H880l-7.4 42.2H704.3l7.1-39.8z"></path>
</svg>
);
const vip3 = (
<svg viewBox="0 0 1024 1024" width="20" height="20" fill="currentColor">
<path d="M849.2 6.3H174.8C81.7 6.3 6.3 81.7 6.3 174.8v674.3c0 93.1 75.4 168.6 168.6 168.6h674.3c93.1 0 168.6-75.4 168.6-168.6V175c0-93.1-75.5-168.7-168.6-168.7z m23.7 247.6v44h-42.7l-536.6 552h-159V297.3l-39.8-0.6v-42.8h158.9v401.2l397.4-401.2h221.8z m-78.9 434h14.6c14.4 0 26.3-4 35.7-12 9.3-7.7 15.1-17.7 17.2-29.9 2-12.3-0.4-22.6-6.9-30.5-6.5-7.7-16.2-11.6-28.6-12-9 0-17.8 3-26.2 8.6-8.9 5.8-15.7 14.8-20.5 26.9h-42.2c6.1-21.9 18.2-40.3 36.3-55.2 17.9-14.8 37.4-22.3 58.7-22.7 26.8 0.2 46.6 8.9 59.4 25.7 12.2 16.4 16.4 35.9 12.8 58.3-2 11.4-5.9 22.2-11.9 32.7-6.5 10.5-16.7 19.9-30.3 28.2 11 8.1 18.3 17.8 21.7 29.1 3.2 11.4 3.8 23.2 1.6 35.6-5.2 27.7-17.5 49.4-37.2 65.3-19.5 16.3-42.2 24.6-68.3 24.9-20.1 0-36.8-6.4-50-19.4-13.2-13-20-31.7-20.3-56.3h42.2c2.1 9.9 5.9 17.8 11.7 23.8 5.9 6.3 14.7 9.5 26.3 9.5 12.3 0 23.6-4.2 33.6-12.7 10-7.9 16.4-19.3 19.4-34 2.3-14.9 0-26.5-7.2-34.8-7-7.9-17.2-12-30.3-12h-17.8l6.5-37.1z"></path>
</svg>
);
const iconStyle = {
marginRight: 8,
fontSize: 16,
transform: "translateY(1px)",
};
function Nav() { function Nav() {
const navigate = useNavigate(); const navigate = useNavigate();
const user = useUserStore((s: any) => s.user); const user = useUserStore((s: any) => s.user);
const exit = useUserStore((s: any) => s.userExit); const exit = useUserStore((s: any) => s.userExit);
const [profileDropdownVisible, setProfileDropdownVisible] = useState(false);
// user.user_avatar = `data:image/png;base64,${new Identicon(h).toString()}`; useMount(() => {
document.addEventListener("click", (e) => setProfileDropdownVisible(false));
});
const onClickMenuItem = (key: string) => { const onClickProfileItem = (command: string) => {
switch (key) { switch (command) {
case "exit": case "exit":
exit(); exit();
break; break;
case "setting": case "sub":
console.log("navigate"); console.log(user);
break; break;
} }
// navigate("/subscribe");
}; };
const dropList = (
<Menu onClickMenuItem={(key) => onClickMenuItem(key)}>
<Menu.Item key="exit">
<IconImport style={iconStyle} />
退
</Menu.Item>
<Menu.Item key="setting">
<IconSettings style={iconStyle} />
</Menu.Item>
</Menu>
);
return ( return (
<header> <header>
<nav className="container"> <nav className="container">
@ -98,40 +60,78 @@ function Nav() {
) )
)} )}
</div> </div>
<div className="end"> <div className="end bs fc fe">
{!user ? ( {!user ? (
<span className="btn" onClick={() => navigate("/login")}> <span className="btn" onClick={() => navigate("/login")}>
</span> </span>
) : ( ) : (
<Space> <div className="profile bs fc">
<div className="user">
<div <div
style={{ className="alert bs fc mr12 badge tip"
color: "rgba(255,255,255,.7)", data-tip="你有最新的消息"
fontSize: 13, >
paddingBottom: 4, <Icon size={22} color="var(--color-text-3)">
<Alert20Regular />
</Icon>
</div>
<div
className="avatar-ddl bs ml12"
onClick={(e) => {
e.nativeEvent.stopPropagation();
setProfileDropdownVisible(true);
}} }}
> >
{user.display_name} <div className="avatar bs fc">
</div>
<div style={{ color: "rgba(255,255,255,.4)" }}>
{user.user_login}
</div>
</div>
<Dropdown droplist={dropList} trigger="hover" position="br">
<Space>
<Avatar shape="square" size={32}>
<img <img
src={`data:image/png;base64,${new Identicon( src={`data:image/png;base64,${new Identicon(
user.user_avatar user.user_avatar
).toString()}`} ).toString()}`}
/> />
</Avatar> <Icon size={16} color="var(--color-text-3)">
<IconDown /> <CaretDown20Filled />
</Space> </Icon>
</Dropdown> </div>
</Space> {profileDropdownVisible && (
<div className="ddl">
<div className="header">
<div className="bs fc sb">
<span>Hi!</span>{" "}
<span style={{ color: "var(--color-text-2)" }}>
{user.display_name}
</span>
</div>
</div>
<div className="main">
<div
className="bs fc sb"
onClick={() => onClickProfileItem("sub")}
>
<span></span>
<Icon color="var(--color-text-3)">
<PremiumPerson20Regular />
</Icon>
</div>
<div className="bs fc sb">
<span></span>
<Icon color="var(--color-text-3)">
<Settings20Regular />
</Icon>
</div>
<div
className="bs fc sb"
onClick={() => onClickProfileItem("exit")}
>
<span>退</span>
<Icon color="var(--color-text-3)">
<DoorArrowRight20Regular />
</Icon>
</div>
</div>
</div>
)}
</div>
</div>
)} )}
</div> </div>
</nav> </nav>

View File

@ -0,0 +1,9 @@
.result {
text-align: center;
padding-top: 100px;
color: var(--color-text-3);
> div {
margin-top: 1rem;
font-size: 14px;
}
}

View File

@ -0,0 +1,23 @@
import "./index.less";
import FlashOff24Regular from "@ricons/fluent/FlashOff24Regular";
import { Icon } from "@ricons/utils";
import { useNavigate } from "react-router-dom";
export default function Result() {
const navigate = useNavigate();
return (
<div className="container result">
<Icon size={48} color="var(--color-text-3)">
<FlashOff24Regular />
</Icon>
<div className="mt12">访</div>
<button
className="bs btn br3 mt24"
onClick={() => navigate("/subscribe")}
>
</button>
</div>
);
}

View File

@ -1,8 +1,8 @@
import { Button, Result } from "@arco-design/web-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { useUserStore } from "../store/user.store"; import { useUserStore } from "../store/user.store";
import Result from "../components/Result";
interface IGuardProps { interface IGuardProps {
children: JSX.Element; children: JSX.Element;
@ -15,30 +15,14 @@ export const Guard = (props: IGuardProps) => {
const user = useUserStore((s: any) => s.user); const user = useUserStore((s: any) => s.user);
const fetchUser = useUserStore((s: any) => s.fetchUser); const fetchUser = useUserStore((s: any) => s.fetchUser);
const Result403 = (
<div style={{ paddingTop: 100 }}>
<Result
status="403"
subTitle="无权访问,请登录"
extra={
<Button type="primary" onClick={() => navigate("/login")}>
</Button>
}
/>
</div>
);
const location = useLocation(); const location = useLocation();
const sign = Cookies.get("_sign_web"); const sign = Cookies.get("_sign_web");
const needAuth = needAuthList.some((p) => location.pathname.indexOf(p) > -1); const needAuth = needAuthList.some((p) => location.pathname.indexOf(p) > -1);
useEffect(() => { useEffect(() => {
console.log(user);
console.log("location.pathname changed 拦截", location.pathname);
if (!user) fetchUser(); if (!user) fetchUser();
}, [location.pathname]); }, [location.pathname]);
if (!sign && needAuth) return Result403; if (!sign && needAuth) return <Result />;
return props.children; return props.children;
}; };

View File

@ -35,7 +35,7 @@ export const lazyRouters: IRoute[] = [
{ {
path: "/subscribe", path: "/subscribe",
element: lazy(() => import("../view/Subscribe")), element: lazy(() => import("../view/Subscribe")),
name: "订阅", name: "订阅课程",
}, },
{ {
path: "/course/detail/:id", path: "/course/detail/:id",

View File

@ -1,7 +1,6 @@
import { create } from "zustand"; import { create } from "zustand";
import Cookie from "js-cookie"; import Cookie from "js-cookie";
import { userState } from "../api"; import { userState } from "../api";
import { Message } from "@arco-design/web-react";
export const useUserStore = create((set) => { export const useUserStore = create((set) => {
return { return {
@ -19,7 +18,6 @@ export const useUserStore = create((set) => {
set({ user: null }); set({ user: null });
Cookie.remove("_sign_web"); Cookie.remove("_sign_web");
Cookie.remove("_sign_web.sig"); Cookie.remove("_sign_web.sig");
Message.success("拜拜~");
}, },
}; };
}); });

View File

@ -0,0 +1,3 @@
export default function Blog() {
return <div className="blog-list">blogs</div>;
}

View File

@ -1,14 +1,5 @@
.bs-card { .course-card {
overflow: hidden; overflow: hidden;
.arco-card-body {
padding: 10px;
}
&.mini {
.arco-card-body {
padding: 0;
}
}
&:hover { &:hover {
.cover { .cover {
@ -18,7 +9,6 @@
.cover { .cover {
transition: background-size 0.25s linear; transition: background-size 0.25s linear;
height: 164px;
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 100%; background-size: 100%;
@ -43,20 +33,4 @@
} }
} }
} }
.bottom-des {
display: flex;
justify-content: space-between;
font-size: 13px;
span {
flex: 1;
color: var(--color-text-3);
padding-right: 40px;
max-width: 160px;
}
a {
cursor: pointer;
color: rgb(var(--primary-6));
}
}
} }

View File

@ -0,0 +1,28 @@
import { MouseEventHandler } from "react";
import "./index.less";
interface IProps {
imgUrl: string;
title: string;
styles?: {};
onClick?: MouseEventHandler;
}
export default function Card(props: IProps) {
const { imgUrl, title, styles, ...rest } = props;
return (
<div className="course-card" {...rest} style={{ ...styles }}>
<div
className="cover"
style={{
backgroundImage: `url('${imgUrl}')`,
}}
>
<div className="mask">
<p className="bs ellipsis">{title}</p>
</div>
</div>
</div>
);
}

View File

@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import BsCard from "../../../../components/Card"; import Card from "../Card";
import "./index.less"; import "./index.less";
export const recommendListDefault = [ export const recommendListDefault = [
@ -38,7 +38,7 @@ function Recommends() {
return ( return (
<div className="recommends"> <div className="recommends">
{recommendList.map((item, index) => ( {recommendList.map((item, index) => (
<BsCard {...item} key={index} /> <Card {...item} key={index} />
))} ))}
</div> </div>
); );

View File

@ -44,7 +44,7 @@
grid-column-gap: 10px; grid-column-gap: 10px;
grid-row-gap: 10px; grid-row-gap: 10px;
.bs-card { .course-card {
border-radius: 3px; border-radius: 3px;
.cover { .cover {
height: 120px; height: 120px;
@ -61,7 +61,7 @@
} }
&.tab { &.tab {
grid-template-columns: 1fr; grid-template-columns: 1fr;
.bs-card { .course-card {
border-radius: 3px; border-radius: 3px;
.cover { .cover {
height: 330px; height: 330px;

View File

@ -1,18 +1,15 @@
import "./index.less"; import "./index.less";
import { Space, Tooltip, Dropdown, Button, Menu } from "@arco-design/web-react";
import BsCard from "../../components/Card";
import Timeline, { IOnScrollParam } from "./components/Timeline"; import Timeline, { IOnScrollParam } from "./components/Timeline";
import Tab20Regular from "@ricons/fluent/Tab20Regular"; import Tab20Regular from "@ricons/fluent/Tab20Regular";
import Table20Regular from "@ricons/fluent/Table20Regular"; import Table20Regular from "@ricons/fluent/Table20Regular";
import Filter20Regular from "@ricons/fluent/Filter20Regular"; import { useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
// import { courseTimeListDefault } from "./mock";
import { Icon } from "@ricons/utils"; import { Icon } from "@ricons/utils";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useMount } from "../../hook"; import { useMount } from "../../hook";
import { getCourseList } from "../../api"; import { getCourseList } from "../../api";
import { processTime } from "./util"; import { processTime } from "./util";
import { Footer } from "../../components/Footer"; import { Footer } from "../../components/Footer";
import Card from "./components/Card";
export default function Index() { export default function Index() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -22,21 +19,7 @@ export default function Index() {
top: -4, top: -4,
}); });
const dropList = (
<Menu>
<Menu.Item key="asec"></Menu.Item>
<Menu.Item key="desc"></Menu.Item>
</Menu>
);
const [actions, setActions] = useState([ const [actions, setActions] = useState([
{
key: "tab",
icon: <Tab20Regular />,
active: false,
tip: "使用大缩略图显示单个项目",
gridClass: "tab",
},
{ {
key: "table", key: "table",
icon: <Table20Regular />, icon: <Table20Regular />,
@ -44,6 +27,13 @@ export default function Index() {
tip: "列出更多项目", tip: "列出更多项目",
gridClass: "table", gridClass: "table",
}, },
{
key: "tab",
icon: <Tab20Regular />,
active: false,
tip: "使用大缩略图显示单个项目",
gridClass: "tab",
},
]); ]);
useMount(() => { useMount(() => {
@ -87,38 +77,24 @@ export default function Index() {
return ( return (
<div className="container course"> <div className="container course">
<div className="action-bar"> <div className="action-bar">
<Space>
{actions.map((action) => ( {actions.map((action) => (
<Tooltip key={action.key} content={action.tip}> <span
<Button key={action.key}
type="text" style={{ cursor: "pointer" }}
onClick={() => onClickActionItem(action)} onClick={() => onClickActionItem(action)}
icon={ className="bs tip ml6"
data-tip={action.tip}
>
<Icon <Icon
size={20} size={20}
color={ color={
action.active action.active ? "var(--color-text-2)" : "var(--color-text-4)"
? "rgb(var(--primary-6))"
: "var(--color-text-2)"
} }
> >
{action.icon} {action.icon}
</Icon> </Icon>
} </span>
/>
</Tooltip>
))} ))}
<Dropdown droplist={dropList} position="bl">
<Button
type="text"
icon={
<Icon size={20} color="var(--color-text-2)">
<Filter20Regular />
</Icon>
}
/>
</Dropdown>
</Space>
</div> </div>
<div className="thumbnail-timeline"> <div className="thumbnail-timeline">
<div className="thumbnail-container"> <div className="thumbnail-container">
@ -133,7 +109,7 @@ export default function Index() {
className={`grid ${actions.find((a) => a.active)?.gridClass}`} className={`grid ${actions.find((a) => a.active)?.gridClass}`}
> >
{item.data.map((d: any) => ( {item.data.map((d: any) => (
<BsCard <Card
onClick={() => onClickCourseItem(d)} onClick={() => onClickCourseItem(d)}
key={d.course_id} key={d.course_id}
imgUrl={d.course_cover_url} imgUrl={d.course_cover_url}

View File

@ -1,37 +1,53 @@
.course-detail { .course-detail {
padding-top: 60px;
.table-of-content { .table-of-content {
position: fixed;
top: 60px;
bottom: 0;
width: 300px;
padding: 20px; padding: 20px;
border-right: 1px solid var(--color-border-2);
> h2 { > h2 {
margin-bottom: 10px; margin-bottom: 10px;
} }
.toc { .toc {
padding-top: 20px;
.level-1 { .level-1 {
color: var(--color-text-4); color: var(--color-text-4);
padding: 20px 0 5px 0; line-height: 1.5;
padding-top: 10px;
} }
.level-2 { .level-2 {
display: grid; padding: 5px 0;
padding-left: 10px; display: flex;
line-height: 24px; align-items: center;
grid-template-columns: 9fr 1fr; justify-content: space-between;
color: var(--color-text-2); color: var(--color-text-2);
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: var(--color-text-1); color: var(--color-text-1);
} }
&.active { &.active {
color: rgb(var(--primary-4)); color: var(--color-primary-1);
}
.xicon {
margin-right: 6px;
} }
.time { .time {
color: var(--color-text-4); color: var(--color-text-4);
font-size: 13px; font-size: 13px;
text-align: right; text-align: right;
width: 100px;
} }
} }
} }
} }
.content {
position: fixed;
left: 300px;
right: 0;
top: 60px;
bottom: 0;
overflow-y: auto;
}
} }

View File

@ -1,12 +1,15 @@
import { useEffect, useState } from "react"; import { useState } from "react";
import "./index.less"; import "./index.less";
import { ResizeBox, Space, Result, Button } from "@arco-design/web-react";
import Guide from "./components/Guide"; import Guide from "./components/Guide";
import { useMount } from "../../hook"; import { useMount } from "../../hook";
import Player from "./components/DPlayer"; import Player from "./components/DPlayer";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { getChapterGuideById } from "../../api"; import { getChapterGuideById } from "../../api";
import { ms2Time } from "./util"; import { ms2Time } from "./util";
import Result from "../../components/Result";
import PlayCircle20Regular from "@ricons/fluent/PlayCircle20Regular";
import BookLetter20Regular from "@ricons/fluent/BookLetter20Regular";
import { Icon } from "@ricons/utils";
function CourseDetail() { function CourseDetail() {
const { id: course_id = "" } = useParams(); const { id: course_id = "" } = useParams();
@ -22,6 +25,11 @@ function CourseDetail() {
title: item.chapter_title, title: item.chapter_title,
level: +item.chapter_level, level: +item.chapter_level,
time: ms2Time(+item.media_time), time: ms2Time(+item.media_time),
icon: !!item.media_url ? (
<Icon size={20}>
<PlayCircle20Regular />
</Icon>
) : null,
active: false, active: false,
view: ( view: (
<Player <Player
@ -54,32 +62,17 @@ function CourseDetail() {
setToc((t: any) => setToc((t: any) =>
t.map((p: any) => ({ ...p, active: i.title === p.title })) t.map((p: any) => ({ ...p, active: i.title === p.title }))
); );
setView( setView(i.view ?? <Result />);
i.view ?? (
<Result
status="403"
subTitle="无权访问"
extra={<Button type="text"></Button>}
/>
)
);
}; };
return ( return (
<div className="course-detail"> <div className="course-detail">
<ResizeBox.Split
direction="horizontal"
style={{ height: "calc(100vh - 60px)" }}
max={0.9}
min={0.1}
size={0.2}
panes={[
<aside className="table-of-content"> <aside className="table-of-content">
<h2>K线</h2> <h2>K线</h2>
<div> <div>
<Space style={{ color: "var(--color-text-3)" }}> <div style={{ color: "var(--color-text-3)" }}>
<span>202332</span> <span>202332</span>
</Space> </div>
</div> </div>
<div className="toc"> <div className="toc">
{toc.map((i: any) => { {toc.map((i: any) => {
@ -96,17 +89,18 @@ function CourseDetail() {
key={i.title} key={i.title}
onClick={() => onclickItem(i)} onClick={() => onclickItem(i)}
> >
<span className="bs-ellipsis">{i.title}</span> <div className="bs ellipsis fc">
{i.icon}
<span>{i.title}</span>
</div>
<span className="time">{i.time}</span> <span className="time">{i.time}</span>
</div> </div>
); );
} }
})} })}
</div> </div>
</aside>, </aside>
view, <div className="content">{view}</div>
]}
/>
</div> </div>
); );
} }

View File

@ -2,5 +2,6 @@ export const ms2Time = (time: number) => {
const mind = time % (60 * 60); const mind = time % (60 * 60);
const minutes = Math.floor(mind / 60); const minutes = Math.floor(mind / 60);
const seconds = Math.ceil(mind % 60); const seconds = Math.ceil(mind % 60);
return minutes + "分" + seconds + "秒"; const prefix = (m: number) => (m < 10 ? "0" + m : m);
return prefix(minutes) + ":" + prefix(seconds);
}; };

View File

@ -34,22 +34,20 @@
color: rgba(0, 0, 0, 0.4); color: rgba(0, 0, 0, 0.4);
} }
} }
.sms-btn {
min-width: 40px;
text-align: right;
padding-left: 0;
padding-right: 0;
border: 0;
}
} }
.submit-btn { .submit-btn {
margin-top: 1rem; margin-top: 2rem;
background-color: rgb(36, 41, 47);
color: #fff;
border: 0;
border-radius: 6px; border-radius: 6px;
padding: 0.5rem 1rem;
width: 100%; width: 100%;
cursor: pointer;
letter-spacing: 10px; letter-spacing: 10px;
transition: all 0.25s ease;
&:active {
background: rgba(0, 0, 0, 1);
}
} }
.sms-group { .sms-group {

View File

@ -1,6 +1,5 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { sms, userLogin } from "../../api"; import { sms, userLogin } from "../../api";
import { Message, Button, Space } from "@arco-design/web-react";
import "./index.less"; import "./index.less";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useUserStore } from "../../store/user.store"; import { useUserStore } from "../../store/user.store";
@ -20,11 +19,11 @@ export function Login() {
const onClickSmsBtn = () => { const onClickSmsBtn = () => {
if (!/^1[3456789]\d{9}$/.test(loginForm.user_login)) if (!/^1[3456789]\d{9}$/.test(loginForm.user_login))
return Message.error("手机号格式有误"); return alert("手机号格式有误");
sms(loginForm.user_login).then((res: any) => { sms(loginForm.user_login).then((res: any) => {
if (res?.code === 10000) if (res?.code === 10000)
setTimeout(() => { setTimeout(() => {
Message.success("验证码已发送"); alert("验证码已发送");
timer.current = setInterval(() => setCountdown(countdown--), 1000); timer.current = setInterval(() => setCountdown(countdown--), 1000);
}, 50); }, 50);
}); });
@ -34,11 +33,11 @@ export function Login() {
userLogin(loginForm).then((res: any) => { userLogin(loginForm).then((res: any) => {
const { code, data, msg } = res; const { code, data, msg } = res;
if (code === 10000) { if (code === 10000) {
Message.success(msg); console.log(msg);
navigate(-1); navigate(-1);
setUser(data); setUser(data);
} }
if (code === 20000) Message.error(msg); if (code === 20000) console.error(msg);
}); });
}; };
@ -65,17 +64,6 @@ export function Login() {
<h4>Backset</h4> <h4>Backset</h4>
<div style={{ width: 320 }}> <div style={{ width: 320 }}>
<div className="form"> <div className="form">
{/* <input
type="text"
className="input"
placeholder="神秘代码"
onChange={(e) =>
setLoginForm((p) => ({
...p,
xcode: e.target.value,
}))
}
/> */}
<input <input
style={{ marginTop: "1rem" }} style={{ marginTop: "1rem" }}
type="text" type="text"
@ -118,16 +106,15 @@ export function Login() {
} }
placeholder="验证码" placeholder="验证码"
/> />
<Button <span
style={{ marginLeft: 12, minWidth: 60 }} className="bs btn ml12 sms-btn br3 outline"
type="text"
onClick={onClickSmsBtn} onClick={onClickSmsBtn}
> >
{countdown === DURATION ? "获取" : countdown + "s"} {countdown === DURATION ? "获取" : countdown + "s"}
</Button> </span>
</div> </div>
</div> </div>
<button className="submit-btn" onClick={onClickLogin}> <button className="submit-btn bs btn" onClick={onClickLogin}>
</button> </button>
</div> </div>

View File

@ -48,6 +48,9 @@
color: rgb(var(--primary-5)); color: rgb(var(--primary-5));
} }
} }
> img {
width: 100%;
}
.original { .original {
color: var(--color-text-3); color: var(--color-text-3);
@ -57,6 +60,7 @@
letter-spacing: 3px; letter-spacing: 3px;
} }
.price { .price {
margin-bottom: 40px;
position: relative; position: relative;
display: inline-block; display: inline-block;
font-family: DINCondensed-Bold, "Century Gothic", CenturyGothic, font-family: DINCondensed-Bold, "Century Gothic", CenturyGothic,
@ -75,7 +79,7 @@
} }
} }
h3 { h3 {
margin: 40px 0 0 0; margin: 0;
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
color: var(--color-text-2); color: var(--color-text-2);

View File

@ -1,4 +1,3 @@
import { Button } from "@arco-design/web-react";
import Checkmark12Filled from "@ricons/fluent/Checkmark12Filled"; import Checkmark12Filled from "@ricons/fluent/Checkmark12Filled";
import { Icon } from "@ricons/utils"; import { Icon } from "@ricons/utils";
import "./index.less"; import "./index.less";
@ -7,100 +6,49 @@ function Subscribe() {
return ( return (
<div className="subscribe"> <div className="subscribe">
<div className="container"> <div className="container">
<h2></h2> <h2></h2>
<h4> <h4></h4>
/ <h4>IF x-arctanx</h4>
Backset (5 - 30)
</h4>
<h4>
PS/x-arctanx
</h4>
<div className="options overlay"> <div className="options overlay">
<section> <section>
<div className="original">1,299</div> <h3></h3>
<div className="price">199</div> <p>请备注: 手机号</p>
<h3></h3> <img src="/wx256.jpg" />
<p>
线 backset.cn
</p>
<Button type="outline"></Button>
<ul>
<li>
<Icon size={20}>
<Checkmark12Filled />
</Icon>
<div>
<span></span>
</div>
</li>
<li>
<Icon size={20}>
<Checkmark12Filled />
</Icon>
<div>
<span></span>
</div>
</li>
</ul>
</section> </section>
<section> <section>
<div className="original">1,299</div> <h3></h3>
<div className="price">299</div> <p>请备注: 手机号</p>
<h3></h3> <img src="/ali256.jpg" />
<p>
线 backset.cn
</p>
<Button type="outline"></Button>
<ul>
<li>
<Icon size={20}>
<Checkmark12Filled />
</Icon>
<div>
<span></span>
</div>
</li>
<li>
<Icon size={20}>
<Checkmark12Filled />
</Icon>
<div>
<span></span>
</div>
</li>
</ul>
</section> </section>
{/* popular */} {/* popular */}
<section className="popular"> <section className="popular">
<div className="original">1,299</div> <div className="original">499</div>
<div className="price">399</div> <div className="price">256</div>
<h3></h3> <h3></h3>
<p> <p>
线 backset.cn 12线()
</p> </p>
<Button type="primary" size="large" long> {/* <button className="bs btn br3" style={{ width: "100%" }}>
</Button> </button> */}
<ul> <ul>
<li> <li>
<Icon size={20}> <Icon size={20}>
<Checkmark12Filled /> <Checkmark12Filled />
</Icon> </Icon>
<div> <div></div>
<span></span>
</div>
</li> </li>
<li> <li>
<Icon size={20}> <Icon size={20}>
<Checkmark12Filled /> <Checkmark12Filled />
</Icon> </Icon>
<div> <div></div>
<span></span> </li>
</div> <li>
<Icon size={20}>
<Checkmark12Filled />
</Icon>
<div></div>
</li> </li>
</ul> </ul>
</section> </section>

File diff suppressed because it is too large Load Diff