theme: channing-cyan highlight: androidstudio
「这是我参与2022首次更文挑战的第11天,活动详情查看:2022首次更文挑战」
我们的项目名称TODO-WORKBENCH,然后再其下面新建main文件夹。我们这里用的是umi框架,cd进入到main文件夹。执行创建命令yarn create @umijs/umi-app
来创建umi项目。创建完成后,安装依赖:执行yarn
。然后再main文件夹下启动:yarn start
。如下图,说明我们创建完成了一个umi项目。
在pages/index.tsx 就是我们见到的页面
如下,.umirc.ts
文件是umi配置的路由。我们现在创建的是简单的umi项目。其实复杂的路由配置不会放到.umirc.ts
文件,会有一个config文件。
我们想要实现的是一个桌面应用,这里就需要使用electron外壳再去包裹umi。
Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需要本地开发 经验。
简言之,就是你可以通过html js css
能够实现window或者Mac再或者Linux的桌面应用。
https://www.electronjs.org/zh/docs/latest/
新建个app文件用于存放electron。
cd 进入到 app文件夹,执行yarn init
。会生成package.json包管理文件。
在package.json中加入,当我们执行yarn start
就相当于执行了electron .
命令。
"scripts": {
"start": "electron ."
}
{
"name": "todo-workbench-app",
"version": "1.0.0",
// 入口文件是index.js
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "electron ."
}
}
如上,其入口文件是index.js
// app控制应用程序的事件生命周期 BrowserWindow创建和管理应用程序 窗口
// 主进程运行着Node.js所以可以使用require引用
const { app, BrowserWindow } = require('electron')
// 创建窗口将index.html加载进一个新的BrowserWindow实例
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600
})
win.loadFile('index.html')
}
// 只有在 app 模块的 ready 事件被激发后才能创建浏览器窗口,app.whenReady()用来监听事件
// 在whenReady()成功后执行createWindow()
app.whenReady().then(() => {
createWindow()
})
// 关闭所有窗口时退出应用
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
其,是将index.html渲染到窗口中。因此我们再建一个index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
We are using Node.js <span id="node-version"></span>,
Chromium <span id="chrome-version"></span>,
and Electron <span id="electron-version"></span>.
</body>
</html>
yarn add --dev electron
直到如上出现各个依赖包名称,才安装完成。
yarn start
如图,打开了一个桌面应用。
生成器 https://www.expressjs.com.cn/starter/generator.html
安装 https://expressjs.com/en/starter/installing.html
我们在我们的项目中加入服务端。在app文件夹下新建一个server文件夹,cd进入server文件下。 执行
npx express-generator
安装依赖
yarn
如下是server/express框架创建的内容
如图在 /bin/www.js
中配置这我们express项目的启动端口。
执行 yarn start
,在3000端口可以看到如下效果
我们在public下新建一个index.html
再次访问我们的3000端口,可以发现它就加载了index.html
的内容了。
cd 进入main文件夹
yarn build
可以看到生成了dist文件夹,并且文件夹下有 umi.css umi.js
和 index.html
三个文件。
将main项目中生成的三个文件都移到express项目中。(把刚才我们再express中创建的index.html也覆盖掉)。
可以看到我们前端的内容已经能在3000端口运行了
首先,我们的服务端运行是在 /bin/www.js/
下的。
如上图部分是服务端启动的命令代码。我们只要让它不在这个地方运行,将其在electron
执行的时候再去运行就打通了electron
和express
。
我们将此部分写成一个函数,然后将其导出。
在底部新建一个函数,将此部分剪切到函数中执行。但是注意var server = http.createServer(app);
这句话不能放入函数中,因为在其他地方也使用了server变量。
const startServer = () => {
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
}
注意这里不可以使用这种导出方式
export { startServer }
会报错,因为是在node中
使用下面的导出方式
module.exports = startServer;
可以直接复制下面的内容 www.js
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('server:server');
var http = require('http');
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}
const startServer = () => {
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
}
// 这么导出导出的并不是函数
// module.exports = startServer;
module.exports.startServer = startServer();
找到在electron的启动文件index.js
,在创建窗口函数中执行 startServer
启动服务端。
在 app 文件夹下 运行yarn start
在 app/index.html
中 通过iframe引入前端页面。也就是引入 http://localhost:3000/
发现iframe只出现了一个小框,通过view-->Toggle Developer Tools打开控制台发现报错了
这个报错大概就是安全问题,iframe使用外部资源,被electron拦截了。当然其实这也不算是外部资源。
查了一下,原因在于<meta>
,我们把上面的两行<meta>
注释掉
可以看到前端的内容可以展示了。
添加样式
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'"> -->
<title>Hello World!</title>
<style>
* {
padding: 0;
margin: 0;
}
#container {
width: 100vw;
height: 100vh;
}
iframe {
width: 100%;
height: 100%;
overflow: hidden;
border: 0;
}
</style>
</head>
<body>
<div id='container'>
<iframe src="http://localhost:3000/"></iframe>
</div>
</body>
</html>
最后效果如下
我们参考飞书的待办任务
因为我们这是一个单页面应用,因此不需要路由了。首先在前端的pages下新建一个components文件夹用于存放组件。再分别创建 MainMenu 和 TaskList两个组件
import styles from './index.less';
export default function MainMenu() {
return (
<div>
<h1 className={styles.title}>主菜单</h1>
</div>
);
}
import styles from './index.less';
export default function TaskList() {
return (
<div>
<h1 className={styles.title}>任务列表</h1>
</div>
);
}
在主页面文件中引入两个组件
import MainMenu from './components/MainMenu';
import TaskList from './components/TaskList';
import styles from './index.less';
export default function IndexPage() {
return (
<div className={styles.page}>
<MainMenu/>
<TaskList/>
</div>
);
}
index.less
.page {
width: 100vw;
height: 100vh;
overflow: hidden;
display: flex;
}
这个时候我们想看效果,不要忘了,我们是将前端打包之后的文件复制到electron中。如果想要看到效果需要再次打包,或者直接运行前端来看效果。
我们进入到main文件夹下 yarn start
启动项目。在 8000端口下看效果
主菜单占小部分,任务列表占大部分。分别为MainMenu和TaskList添加样式
import styles from './index.less';
export default function MainMenu() {
return (
<div className={styles.main_menu}>
<h1 className={styles.title}>主菜单</h1>
</div>
);
}
宽度占20%、给一个最小宽度。当缩小浏览器,其到100px后就不再缩小。
.main_menu {
background: rgb(121, 242, 157);
width: 20%;
min-width: 100px;
}
import styles from './index.less';
export default function TaskList() {
return (
<div className={styles.task_list}>
<h1 className={styles.title}>任务列表</h1>
</div>
);
}
用flex:1
让其占据剩余的所有空间。
.task_list {
background: rgb(134, 109, 204);
// 占用剩余空间
flex:1;
}
效果如下
在MainMenu文件夹下新建components用于存放组件。我们再给这个组件起名为MainItem
如下的Iprops是ts语法,定义props的数据类型。
name是字符串类型,对应进行中、由我处理等。
count是数值型,对应进行中的数量。
icon是React节点,对应着图标元素,这里我们先不添加。
active是布尔类型,对应着点击的激活状态。
onClick是空返回值类型的函数,对应着点击事件。
import { ReactNode } from 'react';
import styles from './index.less';
interface Iprops{
name:string,
count:number,
icon?:ReactNode,
active:boolean,
onClick:()=>void
}
export default function MenuItem(props:Iprops) {
const {name,count,icon,active,onClick} = props
return (
<button className={`${styles.menu_item} ${active?styles.active:''}` } onClick={onClick}>
<span className={styles.menu_item_name}>{name}</span>
<span className={styles.menu_item_count}>{count}</span>
</button>
);
}
import MenuItem from './components/MenuItem';
import styles from './index.less';
export default function MainMenu() {
return (
<div className={styles.main_menu}>
<h1 className={styles.title}>主菜单</h1>
// 先传写死的参数
<MenuItem name={'测试'} onClick={()=>console.log('test')} count={1}/>
</div>
);
}
因为现在使用的是原生的button,所以样式有些丑。
给MainItem的容器MainMenu先进行改造。
让 main_menu 内部也是flex布局,垂直方向。
.main_menu {
background: rgb(121, 242, 157);
width: 20%;
min-width: 100px;
display: flex;
flex-direction: column;
padding: 4px;
}
去掉main_menu的背景色
给按钮添加样式
.menu_item {
// 阴影
box-shadow: 0 0 1px 1px #dddadaad;
background: white;
border: 0;
padding: 6px 10px;
border-radius: 2px;
// 鼠标悬停会出现手
cursor: pointer;
// 动画效果
transition: all .4s cubic-bezier(.215, .610, .355, 1);
// 文字左对齐
text-align: left;
// 将其位置变为相对位置 让menu_item_count变为绝对位置
position: relative;
// 每个按钮底部间距
margin-bottom: 8px;
// 鼠标悬停样式
&:hover {
box-shadow: 0 0 2px 2px #c2bfbfad;
}
.menu_item_count {
// 绝对位置
position: absolute;
background: rgb(236, 69, 69);
width: 20px;
text-align: center;
border-radius: 50%;
line-height: 20px;
// 据右边距离
right: 10px;
color: white;
font-weight: bold;
font-size: 12px;
}
}
// 点击按钮的激活状态
.active {
background-color: #edf4fe;
.menu_item_name {
color: #3372fe;
font-weight: bolder;
}
}
效果如下
存放MenuItem数据
const config = [
{
name:'进行中',
key:'doing',
count:1
},
{
name:'已完成',
key:'done',
count:10
}
]
export default config;
动态渲染MenuItem组件。
import { useState } from 'react';
import MenuItem from './components/MenuItem';
import config from './config';
import styles from './index.less';
export default function MainMenu() {
const [activeKy,setActiveKey] = useState('doing');
return (
<div className={styles.main_menu}>
<h1 className={styles.title}>主菜单</h1>
{
config.map((item)=>(
<MenuItem name={item.name} active={activeKy==item.key} onClick={()=>setActiveKey(item.key)} count={item.count} key={item.key}/>
))
}
</div>
);
}
这里提一个问题:
// 使用大括号就需要写return
map((item=>{return <div>11</div>}))
// 使用大括号就需要写return
map((item=>(<div>11</div>)))
在TaskList下新建一个components文件夹用于存放TaskItem
同样在TaskItem文件夹下新建index.tsx index.less
import styles from './index.less';
export default function TaskItem() {
return (
<div>
</div>
);
我们看一下这个组件都需要哪些数据,并且约束一下数据类型
import styles from './index.less';
interface TaskProps{
title:string,
desc:string,
startTime:string,
endTiem:string,
status: 'doing|done'
}
export default function TaskItem(props:TaskProps) {
return (
<div>
</div>
);
}
实现的效果大概是这样
之前做好的主菜单部分的内容,我们为了适应右边的任务列表做了一些改动。
import { useState } from 'react';
import MenuItem from './components/MenuItem';
import config from './config';
import styles from './index.less';
export default function MainMenu() {
const [activeKy,setActiveKey] = useState('doing');
return (
<div className={styles.main_menu}>
// 新增 包了一层
<div className={styles.title_container}>
<h1 className={styles.title}>主菜单</h1>
</div>
{
config.map((item)=>(
<MenuItem name={item.name} active={activeKy==item.key} onClick={()=>setActiveKey(item.key)} count={item.count} key={item.key}/>
))
}
</div>
);
}
.main_menu {
width: 20%;
min-width: 100px;
display: flex;
flex-direction: column;
padding:0px 4px 0px 20px;
// 新增加了边距
.title_container{
margin:3px;
}
}
import styles from './index.less';
interface TaskProps{
title:string,
desc:string,
startTime:string,
endTime:string,
status:'doing'|'done'
}
export default function TaskItem(props:TaskProps) {
const {title,desc,startTime,endTime,status} = props
return (
<div className={styles.task_item}>
<div className={styles.task_item_info}>
<div className={styles.task_item_title}>
{title}
</div>
<div className={styles.task_item_desc}>
{desc}
</div>
</div>
<div className={styles.task_item_status}>
<div className={styles.task_item_time}>{endTime}</div>
<button className={styles.task_item_btn}>
完成
</button>
</div>
</div>
);
}
.task_item {
// 让其绝对位置 其子元素绝对位置
position: relative;
width: 100%;
height: 80px;
justify-content: center;
display: flex;
flex: 1;
// 每个task_item按列分布
flex-direction: column;
box-shadow: 0 0 1px 1px #dddadaad;
transition: all .4s cubic-bezier(.215, .610, .355, 1);
background: white;
margin-bottom: 8px;
border-radius:5px;
cursor:cell;
// 加入悬停阴影
&:hover {
box-shadow: 0 0 2px 2px #c2bfbfad;
}
// task_item_info和task_item_status是 task_item的两个子元素
.task_item_info {
display: flex;
flex-direction: column;
// 使用绝对位置
position: absolute;
// 配合position: absolute使用,居左20px
left: 20px;
// align-items: center;
// 中间对齐
justify-content: center;
height: 80px;
// title
.task_item_title{
margin-bottom:10px;
}
// desc
.task_item_desc{
color:#7e7e7e
}
}
.task_item_status {
display: flex;
position: absolute;
flex-direction: column;
position: absolute;
// 配合绝对定位使用,居右20px
right: 20px;
margin: auto;
.task_item_time{
margin-bottom:10px;
color: #7971f3;
background-color: aliceblue;
border-radius:3px;
padding: 2px 6px;
}
.task_item_btn{
}
}
}
我们这里先复制试一下多个TaskItem的效果。当TaskItem较多的时候,我们让其滚动。但是滚动条又不是很好看,我们这里将其隐藏掉,但是依旧能滚动。
import TaskItem from './components/TaskItem';
import styles from './index.less';
export default function TaskList() {
return (
<div className={styles.task_list}>
<div>
<h1 className={styles.title}>任务列表</h1>
</div>
<div className={styles.taskItem_contanier}>
<TaskItem title={'测试'} desc={'这是描述'} endTime={'2022-2-9'} status={'doing'} startTime={'20222-2-9'}/>
<TaskItem title={'测试'} desc={'这是描述'} endTime={'2022-2-9'} status={'doing'} startTime={'20222-2-9'}/>
<TaskItem title={'测试'} desc={'这是描述'} endTime={'2022-2-9'} status={'doing'} startTime={'20222-2-9'}/>
<TaskItem title={'测试'} desc={'这是描述'} endTime={'2022-2-9'} status={'doing'} startTime={'20222-2-9'}/>
<TaskItem title={'测试'} desc={'这是描述'} endTime={'2022-2-9'} status={'doing'} startTime={'20222-2-9'}/>
<TaskItem title={'测试'} desc={'这是描述'} endTime={'2022-2-9'} status={'doing'} startTime={'20222-2-9'}/>
<TaskItem title={'测试'} desc={'这是描述'} endTime={'2022-2-9'} status={'doing'} startTime={'20222-2-9'}/>
<TaskItem title={'测试'} desc={'这是描述'} endTime={'2022-2-9'} status={'doing'} startTime={'20222-2-9'}/>
<TaskItem title={'测试'} desc={'这是描述'} endTime={'2022-2-9'} status={'doing'} startTime={'20222-2-9'}/>
<TaskItem title={'测试'} desc={'这是描述'} endTime={'2022-2-9'} status={'doing'} startTime={'20222-2-9'}/>
<TaskItem title={'测试'} desc={'这是描述'} endTime={'2022-2-9'} status={'doing'} startTime={'20222-2-9'}/>
<TaskItem title={'测试'} desc={'这是描述'} endTime={'2022-2-9'} status={'doing'} startTime={'20222-2-9'}/>
</div>
</div>
);
}
.task_list {
// 占用剩余空间
flex: 1;
display: flex;
flex-direction: column;
padding: 0px 20px 0px 20px;
.taskItem_contanier{
padding:3px 20px 0px 10px;
overflow:auto;
}
}
::-webkit-scrollbar {
width: 1px; /*对垂直流动条有效*/
height: 10px; /*对水平流动条有效*/
}
在TaskList/components下新建AddTask组件
我们看一下都需要哪些变量,及其类型,因为我们的数据都在组件内部使用,所以不需要props。
}
## 样式功能完善
### 未点击前
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9d24a17b275b4646962a6c0df25cf379~tplv-k3u1fbpfcp-watermark.image?)
**index.tsx**
- 通过 task.clickStatu 的 `true/false` 来进行切换。
- 我们这里用到了图标,`<PlusOutlined> ` 导入`import {PlusOutlined} from '@ant-design/icons'`
```js
import styles from './index.less';
import {useState} from 'react';
import {PlusOutlined} from '@ant-design/icons'
import {Input} from 'antd'
interface AddTask{
content:string;
startTime:string;
endTime:string;
clickStatu:boolean;
}
export default function AddTask() {
const [task, setTask] = useState<AddTask>({
content:'',
startTime:'',
endTime:'',
clickStatu:false
})
return (
<div className={styles.task_add}>
{
task.clickStatu ? <div>11</div> :
<div className={styles.task_add_before} onClick={()=>{setTask({...task, clickStatu:true})}}>
<div className={styles.task_add_before_icon_container}>
<PlusOutlined className={styles.task_add_before_icon} />
</div>
<div className={styles.task_add_before_name}>添加任务</div>
</div>
}
</div>
);
}
.task_add {
box-shadow: 0 0 1px 1px #dddadaad;
transition: all .4s cubic-bezier(.215, .610, .355, 1);
background: white;
margin-bottom: 8px;
border-radius: 5px;
margin: 3px 20px 10px 10px;
&_before {
height: 50px;
width: 100%;
display: flex;
&_icon_container {
width: 30px;
line-height: 50px;
padding-left: 10px;
.task_add_before_icon {
font-size: 20px;
color: #3372fe;
cursor: pointer;
:hover {
font-size: 22px;
color: #2565ee;
}
}
}
&_name {
color: #ada7a7;
line-height: 50px;
padding-left: 10px;
}
}
}
我们实现了如下效果,并且点击之后切换到另一个状态(下面要写的添加内容状态)。
首先上面是一个输入框,下面是今天、明天及自定义时间,还有创建按钮和取消按钮。
我们实现的效果如下
我这里先·贴上完整代码,然后再分开讲解每一部分
import styles from './index.less';
import React, { useState, useEffect } from 'react';
import { PlusOutlined, CloseCircleOutlined } from '@ant-design/icons';
import IconFont from '@/public/Icon';
import { Input, Button, Tag, DatePicker, Popover } from 'antd';
import 'moment/locale/zh-cn';
// 时间选择器底部英文-->中文
import locale from 'antd/lib/date-picker/locale/zh_CN';
import moment from 'moment';
interface AddTask {
content: string;
startTime: string;
endTime: string;
clickStatu: boolean;
showTime: boolean;
}
export default function AddTask() {
const [task, setTask] = useState<AddTask>({
content: '',
startTime: '',
endTime: '',
clickStatu: true,
showTime: false,
});
// 用于时间选择器以及tag使用的时间
const [defaultTime, setDefaultTime] = useState<any>({
time: '',
tag: '',
});
// 自定义时间时的时间选择器是否展示
const [definedDatePickShow, setDefinedDatePick] = useState(false);
// useRef
const inputRef = React.useRef<any>(null);
// Input获得焦点配置参数
const sharedProps = {
style: { width: '100%' },
ref: inputRef,
};
// 关闭Tag
const closeTag = () => {
setTask({ ...task, showTime: false });
};
// 取消创建任务
const onCancle = () => {
setTask({ ...task, clickStatu: false });
};
// 在时间选择器中修改日期
const changeDate = date => {
const todayStart = moment(moment().format('YYYY-MM-DD 00:00'));
const yestardayStart = moment(moment(todayStart).subtract(1, 'days'));
const todayEnd = moment(moment().format('YYYY-MM-DD 24:00'));
const tomorrowEnd = moment(moment(todayEnd).add(1, 'days'));
console.log(tomorrowEnd, 'tomorrowEndtomorrowEnd');
let today = '';
if (todayStart < date && date < todayEnd) {
today = '(今天)';
} else if (todayEnd < date && date < tomorrowEnd) {
today = '(明天)';
} else if (yestardayStart < date && date < todayStart) {
today = '(昨天)';
} else {
today = `(星期${moment(date).day() == 0 ? '日' : moment(date).day()})`;
}
setDefaultTime({ time: date, tag: moment(date).format(`MM月DD日 ${today} HH:mm`) });
};
// 创建任务
const onCreate = () => {
console.log(defaultTime);
};
// 选择今天 明天
const chooseDefaultTime = (day: string) => {
if (day == 'today') {
const time = moment(moment().format('YYYY-MM-DD 18:00'));
const tag = `${moment().format('M月DD日')}(今天)18:00`;
setDefaultTime({ time, tag });
} else if (day == 'tomorrow') {
const time = moment(moment().add(1, 'days').format('YYYY-MM-DD 18:00'));
const tag = `${moment().add(1, 'days').format('M月DD日')}(明天)18:00`;
setDefaultTime({ time, tag });
}
setDefinedDatePick(false);
};
useEffect(() => {
// 点击状态是true的时候再去让Input获得焦点
task.clickStatu ? inputRef.current!.focus({ cursor: 'start' }) : '';
}, [task.clickStatu]);
return (
<div className={styles.task_add}>
{task.clickStatu ? (
<div className={styles.task_add_after}>
<Input {...sharedProps} placeholder="输入任务内容" />
<div className={styles.btn_container}>
{task.showTime && (
<div>
<Popover
placement="bottomLeft"
title={''}
content={(
<DatePicker
showTime
defaultValue={defaultTime.time}
locale={locale}
autoFocus
onChange={e => {
changeDate(e);
}}
/>
)}
trigger="click"
>
<Tag closable onClose={closeTag} color="gold" style={{ padding: '5px' }}>
{defaultTime.tag}
</Tag>
</Popover>
</div>
)}
{!task.showTime && (
<div className={styles.time_btn_container}>
<div
className={`${styles.time_btn} ${definedDatePickShow ? styles.disabled : ''}`}
onClick={() => {
setTask({ ...task, showTime: true });
chooseDefaultTime('today');
}}
>
<IconFont type="iconjintian" className={styles.icon} />
今天
</div>
<div
className={`${styles.time_btn} ${definedDatePickShow ? styles.disabled : ''}`}
onClick={() => {
setTask({ ...task, showTime: true });
chooseDefaultTime('tomorrow');
}}
>
<IconFont type="icona-rili2" className={styles.icon} />
明天
</div>
<div
className={styles.time_btn}
onClick={() => {
setDefinedDatePick(true);
}}
>
<IconFont type="icona-rili3" className={styles.icon} />
自定义
</div>
{definedDatePickShow ? (
<div style={{ height: '30px', lineHeight: '40px', marginLeft: '30px' }}>
<DatePicker
showTime
defaultValue={defaultTime.time}
locale={locale}
autoFocus
style={{ height: '30px', lineHeight: '40px' }}
onChange={e => {
changeDate(e);
}}
/>
<span style={{ marginLeft: '3px', color: '#1890ff' }}>
{' '}
<CloseCircleOutlined
onClick={() => {
setDefinedDatePick(false);
}}
/>
</span>
</div>
) : (
<span />
)}
</div>
)}
<div className={styles.function_btn_container}>
<Button onClick={onCancle}>取消</Button>
<Button type="primary" className={styles.create_btn} onClick={onCreate}>
创建
</Button>
</div>
</div>
</div>
) : (
<div
className={styles.task_add_before}
onClick={() => {
setTask({ ...task, clickStatu: true });
}}
>
<div className={styles.task_add_before_icon_container}>
<PlusOutlined className={styles.task_add_before_icon} />
</div>
<div className={styles.task_add_before_name}>添加任务</div>
</div>
)}
</div>
);
}
.task_add {
box-shadow: 0 0 1px 1px #dddadaad;
transition: all .4s cubic-bezier(.215, .610, .355, 1);
background: white;
margin-bottom: 8px;
border-radius: 5px;
margin: 3px 20px 10px 10px;
&_before {
height: 50px;
width: 100%;
display: flex;
&_icon_container {
width: 30px;
line-height: 50px;
padding-left: 10px;
.task_add_before_icon {
font-size: 20px;
color: #3372fe;
cursor: pointer;
:hover {
font-size: 22px;
color: #2565ee;
}
}
}
&_name {
color: #ada7a7;
line-height: 50px;
padding-left: 10px;
}
}
&_after {
width: 100%;
padding: 10px;
.disabled {
pointer-events: none;
}
.btn_container {
display: flex;
padding: 8px 0;
.time_btn_container {
display: flex;
flex: 3;
cursor: pointer;
.time_btn {
margin-top: 4px;
padding: 8px;
font-size: 12px;
background-color: #f5f1f1;
border-radius: 5px;
&:hover {
background-color: #cfcbcb ;
}
.icon {
font-size: 15px;
margin-right: 3px;
}
}
.time_btn:nth-child(n+2) {
margin-left: 5px;
}
}
.function_btn_container {
flex: 2;
text-align: right;
line-height: 38px;
.create_btn {
margin-left: 10px;
}
}
}
}
}
在点击添加切换到添加内容状态后,要默认将鼠标焦点定位到📌输入框中。
antdesgin的Input组件中正好有这个功能。
// 导入Input组件
import { Input, Button, Tag, DatePicker, Popover } from 'antd';
// useRef也可以 放到{}中引入 然后下面就不需要React.useRef 直接 useRef
import React, { useState, useEffect } from 'react';
// useRef
const inputRef = React.useRef<any>(null);
// Input获得焦点配置参数
const sharedProps = {
style: { width: '100%' },
ref: inputRef,
};
// 因为这个输入框本来是不存在的 当点击添加任务切换状态才有的。直接使用
// inputRef.current!.focus({ cursor: 'start' }) 会报错。因此我们在useEffect中通过
// 监测task.clickStatu的改变,并且是true的时候赋值。cursor: 'start'是将焦点放到输入框
// 开始的位置,另外还有 'all','end'
useEffect(() => {
// 点击状态是true的时候再去让Input获得焦点
task.clickStatu ? inputRef.current!.focus({ cursor: 'start' }) : '';
}, [task.clickStatu]);
<div className={styles.task_add}>
{
// 上面定义的数据
task.clickStatu ? (
<div className={styles.task_add_after}>
// sharedProps 中获得了inputRef
<Input {...sharedProps} placeholder='输入任务内容' />
</div>)
: (
<div
className={styles.task_add_before}
onClick={() => {
setTask({ ...task, clickStatu: true });
}}
>
<div className={styles.task_add_before_icon_container}>
<PlusOutlined className={styles.task_add_before_icon} />
</div>
<div className={styles.task_add_before_name}>添加任务</div>
</div>
)
.task_add {
box-shadow: 0 0 1px 1px #dddadaad;
transition: all .4s cubic-bezier(.215, .610, .355, 1);
background: white;
margin-bottom: 8px;
border-radius: 5px;
margin: 3px 20px 10px 10px;
&_before{ .... }
// & 代表task_add
&_after{
width: 100%;
padding: 10px;
}
}
我们用的是阿里的iconfont图标,图标的使用可以看我之前的一篇文章✈️
因为我们使用的图标是自定义图标,所以最好封装成一个组件。我在src下新建了一个public文件夹,用于存放Icon组件
在Icon下新建index.tsx
import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({
scriptUrl: [
// 每次更新图标需要更换此地址
'//at.alicdn.com/t/font_2503482_lgnt38a7d1f.js'
]
});
export default IconFont
然后在我们的 AddTask组件中引用
import IconFont from '@/public/Icon';
// type对应 iconfont官网复制代码的内容
<IconFont type="iconjintian" className={styles.icon} />
定义了一个calss time_btn_container
来存放底部的所有内容。其子元素有 time_btn_container
和 function_btn_container
。 time_btn_container
下的按钮我也成了div,class名为time_btn
&_after {
width: 100%;
padding: 10px;
.btn_container {
display: flex;
padding: 8px 0;
.time_btn_container {
display: flex;
flex: 3;
cursor: pointer;
.time_btn {
margin-top: 4px;
padding: 8px;
font-size: 12px;
background-color: #f5f1f1;
border-radius: 5px;
&:hover {
background-color: #cfcbcb ;
}
.icon {
font-size: 15px;
margin-right: 3px;
}
}
.time_btn:nth-child(n+2) {
margin-left: 5px;
}
}
.function_btn_container {
flex: 2;
text-align: right;
line-height: 38px;
.create_btn {
margin-left: 10px;
}
}
}
}
点击今天后默认时间是18:00并且将这些按钮替换为了CloseTag
今天、明天、自定义按钮和Tag的切换。通过task.showTime值来控制
写法
{task.showTime && (<div>Tag</div> ) }
{!task.showTime && (<div>今天明天按钮</div> ) }
这是今天的按钮,onClick事件 让task.showTime变为true。同时触发chooseDefaultTime('today')
函数。
<div
className={styles.time_btn}
onClick={() => {
setTask({ ...task, showTime: true });
chooseDefaultTime('today');
}}>
<IconFont type="iconjintian" className={styles.icon} />
今天
</div>
这里用到了一个新变量defaultTime,time是时间选择器DatePicker使用的moment对象。tag是标签需要用到的时间格式。对了时间选择器用到moment我们还需要导入moment
// 用于时间选择器以及tag使用的时间
const [defaultTime, setDefaultTime] = useState<any>({
time: '',
tag: '',
});
// 选择今天 明天
const chooseDefaultTime = (day: string) => {
// 点击今天时
if (day == 'today') {
// 默认是18:00
const time = moment(moment().format('YYYY-MM-DD 18:00'));
// 默认今天18:00 显示今天 明天 昨天 其他是星期几
const tag = `${moment().format('M月DD日') }(今天)18:00`;
setDefaultTime({ time, tag });
// 点击明天按钮时
} else if (day == 'tomorrow') {
const time = moment(moment().add(1, 'days').format('YYYY-MM-DD 18:00'));
const tag = `${moment().add(1, 'days').format('M月DD日') }(明天)18:00`;
setDefaultTime({ time, tag });
}
setDefinedDatePick(false)
};
// closable 变为可关闭标签 color 标签颜色 onClose 关闭函数
<Tag closable onClose={closeTag} color="gold" style={{ padding: '5px' }}>
{defaultTime.tag}
</Tag>
这里用到了弹层Popover组件,点击Tag出现弹层,只需要把Tag都写到Popover的内部即可。
这里再说一点,时间选择器是英语,而不是中文。需要引入此内容 并且在DatePicker中添加 locale={locale}
import 'moment/locale/zh-cn';
// 时间选择器底部英文-->中文
import locale from 'antd/lib/date-picker/locale/zh_CN';
<Popover
// 弹层的位置
placement="bottomLeft"
// 标题 这里为空
title={''}
// 弹层的内容 这里是时间选择器
content={(
<DatePicker
showTime
defaultValue={defaultTime.time}
locale={locale}
autoFocus
onChange={e => {
changeDate(e);
}}
/>
)}
// 触发方式
trigger="click"
>
<Tag closable onClose={closeTag} color="gold" style={{ padding: '5px' }}>
{defaultTime.tag}
</Tag>
</Popover>
也就是时间选择器中的 changeDate(e);
函数
(星期${moment(date).day() == 0 ? '日' : moment(date).day()})
; } setDefaultTime({ time: date, tag: moment(date).format(MM月DD日 ${today} HH:mm
) }); };
### 自定义时间
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/493ae3a0235a448f8f6f3863026d523e~tplv-k3u1fbpfcp-watermark.image?)
我们在其右侧再写一个时间选择器,通过变量definedDatePickShow控制其是否显示。同时当自定义时间的选择器出现今天、明天按钮就不可以点了
```js
<div className={styles.time_btn}
onClick={() => {
setDefinedDatePick(true);
}}>
<IconFont
type="icona-rili3"
className={styles.icon}
/>
自定义
</div>
{definedDatePickShow ? (
<div style={{height:'30px',lineHeight:'40px',marginLeft:'30px'}}>
<DatePicker
showTime
defaultValue={defaultTime.time}
locale={locale}
autoFocus
style={{height:'30px',lineHeight:'40px'}}
onChange={e => {
changeDate(e);
}}
/>
<span style={{marginLeft:'3px',color:'#1890ff'}}> <CloseCircleOutlined onClick={()=>{
setDefinedDatePick(false)
}} /></span>
</div>
) : (
<span />
)}
我们的实现效果
<TaskDetail>
,在任务组件<TaskItem>
中引用<TaskItem>
中定义一个变量visible传入组件<TaskDetail>
中,控制抽屉开关<TaskDetail>
中展示需要的数据,通过props传给它。index.tsx 我们进行了props的类型约束。
import styles from './index.less';
import { Drawer, Input,Popover,Tag,DatePicker } from 'antd';
import { useEffect, useState } from 'react';
import { CloseOutlined, CalendarOutlined } from '@ant-design/icons';
import IconFont from '@/public/Icon/index';
import 'moment/locale/zh-cn';
interface TaskDetailProps {
data: data;
visible: boolean;
handleClose: () => void;
timeTag:string;
}
interface data {
title: string;
desc: string;
startTime: string;
endTime: string;
status: 'doing' | 'done';
}
export default function TaskDetail(props: TaskDetailProps) {
return (
<div>任务详情</div>
);
}
这有的是后面要用的,在这里都先引用。
import styles from './index.less';
import { Drawer, Input,Popover,Tag,DatePicker } from 'antd';
import { useEffect, useState } from 'react';
import { CloseOutlined, CalendarOutlined } from '@ant-design/icons';
import IconFont from '@/public/Icon/index';
参数分别是
}
对应的样式
```less
.container {
width: 100%;
.close {
width: 100%;
height: 30px;
text-align: right;
}
}
这里我用的普通的 <input>
并没用antdesign的组件。我们这里并不想要input的边框。
<div className={styles.inputCon}>
<input
className={styles.input}
value={submitData.title}
onChange={e => {
setSubmitData({ ...submitData, title: e.target.value });
}}
/>
</div>
对应样式
.inputCon {
width: 100%;
.input {
/** 去掉边框 */
border: 0;
outline: none;
border: none;
width: 100%;
/** 修改颜色 */
outline-color: "red";
font-size: 20px;
}
}
const { TextArea } = Input;
<div className={styles.desc}>
<TextArea
// 占五行
rows={5}
// 是否展示字数
showCount={true}
value={submitData.desc}
// 最大字数
maxLength={100}
onChange={e => {
setSubmitData({ ...submitData, desc: e.target.value });
}}
/>
</div>
对应样式
.desc {
margin-top: 20px;
}
// 用于时间标签和按钮的切换
const [isBtnShow,setIsBtnShow] = useState(false)
这部分都是之前写过的处理时间的,主要的逻辑有:
chooseDefaultTime
函数 // 用于时间选择器以及tag使用的时间
const [defaultTime, setDefaultTime] = useState<any>({
time: '',
tag: '',
});
// 自定义时间时的时间选择器是否展示
const [datePickShow, setDefinedDatePick] = useState(false);
<div className={styles.calender}>
// 日历图标
<div className={styles.iconCon}>
<CalendarOutlined style={{ fontSize: '17px' }} />
</div>
<div className={styles.dateCon}>
{
!isBtnShow ? (
<>
<div
className={`${styles.time_btn} ${isBtnShow ? styles.disabled : ''}`}
onClick={() => {
setDefinedDatePick(true)
chooseDefaultTime('today');
}}
>
<IconFont type="iconjintian" className={styles.icon} />
今天
</div>
<div
className={`${styles.time_btn} ${isBtnShow ? styles.disabled : ''}`}
onClick={() => {
setDefinedDatePick(true)
chooseDefaultTime('tomorrow');
}}
>
<IconFont type="icona-rili2" className={styles.icon} />
明天
</div>
<div
className={styles.time_btn}
onClick={() => {
setDefinedDatePick(true)
}}
>
<IconFont type="icona-rili3" className={styles.icon} />
自定义
</div>
</>
):(
<>
<Tag closable onClose={closeTag} className={styles.tag} color="gold" style={{ padding: '5px' }} onClick={()=>{setDefinedDatePick(true)}}>
{defaultTime.tag}
</Tag>
</>
)
}
</div>
</div>
</div>
用到的方法也是之前添加任务时的方法
// 选择今天 明天
const chooseDefaultTime = (day: string) => {
if (day == 'today') {
const time = moment(moment().format('YYYY-MM-DD 18:00'));
const tag = `${moment().format('M月DD日')}(今天)18:00`;
setDefaultTime({ time, tag });
} else if (day == 'tomorrow') {
const time = moment(moment().add(1, 'days').format('YYYY-MM-DD 18:00'));
const tag = `${moment().add(1, 'days').format('M月DD日')}(明天)18:00`;
setDefaultTime({ time, tag });
}
};
// 在时间选择器中修改日期
const changeDate = (date:any) => {
const todayStart = moment(moment().format('YYYY-MM-DD 00:00'));
const yestardayStart = moment(moment(todayStart).subtract(1, 'days'));
const todayEnd = moment(moment().format('YYYY-MM-DD 24:00'));
const tomorrowEnd = moment(moment(todayEnd).add(1, 'days'));
console.log(tomorrowEnd, 'tomorrowEndtomorrowEnd');
let today = '';
if (todayStart < date && date < todayEnd) {
today = '(今天)';
} else if (todayEnd < date && date < tomorrowEnd) {
today = '(明天)';
} else if (yestardayStart < date && date < todayStart) {
today = '(昨天)';
} else {
today = `(星期${moment(date).day() == 0 ? '日' : moment(date).day()})`;
}
setDefaultTime({ time: date, tag: moment(date).format(`MM月DD日 ${today} HH:mm`) });
};
// 关闭Tag
const closeTag = () => {
setDefinedDatePick(false);
setIsBtnShow(false)
};
我们创建任务时的时间选择器,是有一个输入框,点击后才打开时间选择器的。
我们今天这里不想再有这个输入框
直接使用 open={datePickShow}
控制时间选择器的开关。
但是这个输入框还在
没有别的办法,我就用一层div
将输入框盖掉。如下的className为dateConwrap
<DatePicker
showTime
open={datePickShow}
defaultValue={defaultTime.time}
locale={locale}
onOk={()=>{setDefinedDatePick(false);setIsBtnShow(true)}}
autoFocus
onChange={e => {
changeDate(e);
}}
/>
<div className={styles.dateConwrap}></div>
.dateConwrap{
position:absolute;
width:90%;
height:40px;
background-color: #fff;
}
import styles from './index.less';
import { Drawer, Input,Popover,Tag,DatePicker } from 'antd';
import { useEffect, useState } from 'react';
import { CloseOutlined, CalendarOutlined } from '@ant-design/icons';
import IconFont from '@/public/Icon/index';
import 'moment/locale/zh-cn';
// 时间选择器底部英文-->中文
import locale from 'antd/lib/date-picker/locale/zh_CN';
import moment from 'moment';
interface TaskDetailProps {
data: data;
visible: boolean;
handleClose: () => void;
timeTag:string;
}
interface data {
title: string;
desc: string;
startTime: string;
endTime: string;
status: 'doing' | 'done';
}
export default function TaskDetail(props: TaskDetailProps) {
const { TextArea } = Input;
const { data, visible, handleClose,timeTag } = props;
const [submitData, setSubmitData] = useState({
title: '',
desc: '',
startTime: '',
endTime: '',
status: '',
});
// 用于时间选择器以及tag使用的时间
const [defaultTime, setDefaultTime] = useState<any>({
time: '',
tag: '',
});
const [isBtnShow,setIsBtnShow] = useState(false)
// 自定义时间时的时间选择器是否展示
const [datePickShow, setDefinedDatePick] = useState(false);
// 选择今天 明天
const chooseDefaultTime = (day: string) => {
if (day == 'today') {
const time = moment(moment().format('YYYY-MM-DD 18:00'));
const tag = `${moment().format('M月DD日')}(今天)18:00`;
setDefaultTime({ time, tag });
} else if (day == 'tomorrow') {
const time = moment(moment().add(1, 'days').format('YYYY-MM-DD 18:00'));
const tag = `${moment().add(1, 'days').format('M月DD日')}(明天)18:00`;
setDefaultTime({ time, tag });
}
};
// 在时间选择器中修改日期
const changeDate = (date:any) => {
const todayStart = moment(moment().format('YYYY-MM-DD 00:00'));
const yestardayStart = moment(moment(todayStart).subtract(1, 'days'));
const todayEnd = moment(moment().format('YYYY-MM-DD 24:00'));
const tomorrowEnd = moment(moment(todayEnd).add(1, 'days'));
console.log(tomorrowEnd, 'tomorrowEndtomorrowEnd');
let today = '';
if (todayStart < date && date < todayEnd) {
today = '(今天)';
} else if (todayEnd < date && date < tomorrowEnd) {
today = '(明天)';
} else if (yestardayStart < date && date < todayStart) {
today = '(昨天)';
} else {
today = `(星期${moment(date).day() == 0 ? '日' : moment(date).day()})`;
}
setDefaultTime({ time: date, tag: moment(date).format(`MM月DD日 ${today} HH:mm`) });
};
// 关闭Tag
const closeTag = () => {
setDefinedDatePick(false);
setIsBtnShow(false)
};
useEffect(() => {
setSubmitData(data);
setDefaultTime({
// time:endTime,
tag:timeTag,
})
console.log(timeTag,'timeTagtimeTagtimeTag')
timeTag?setIsBtnShow(true):setIsBtnShow(false)
}, [data]);
console.log(submitData);
return (
<Drawer placement="right" onClose={handleClose} visible={visible} keyboard={true} closable={false}>
<div className={styles.container}>
<div className={styles.close}>
<CloseOutlined onClick={handleClose} />
</div>
<div className={styles.inputCon}>
<input
className={styles.input}
value={submitData.title}
onChange={e => {
setSubmitData({ ...submitData, title: e.target.value });
}}
/>
</div>
<div className={styles.desc}>
<TextArea
rows={5}
showCount={true}
value={submitData.desc}
maxLength={100}
onChange={e => {
setSubmitData({ ...submitData, desc: e.target.value });
}}
/>
</div>
<div className={styles.calender}>
<div className={styles.iconCon}>
<CalendarOutlined style={{ fontSize: '17px' }} />
</div>
<div className={styles.dateCon}>
{
!isBtnShow ? (
<>
<div
className={`${styles.time_btn} ${isBtnShow ? styles.disabled : ''}`}
onClick={() => {
setDefinedDatePick(true)
chooseDefaultTime('today');
}}
>
<IconFont type="iconjintian" className={styles.icon} />
今天
</div>
<div
className={`${styles.time_btn} ${isBtnShow ? styles.disabled : ''}`}
onClick={() => {
setDefinedDatePick(true)
chooseDefaultTime('tomorrow');
}}
>
<IconFont type="icona-rili2" className={styles.icon} />
明天
</div>
<div
className={styles.time_btn}
onClick={() => {
setDefinedDatePick(true)
}}
>
<IconFont type="icona-rili3" className={styles.icon} />
自定义
</div>
</>
):(
<>
<Tag closable onClose={closeTag} className={styles.tag} color="gold" style={{ padding: '5px' }} onClick={()=>{setDefinedDatePick(true)}}>
{defaultTime.tag}
</Tag>
</>
)
}
<DatePicker
showTime
open={datePickShow}
defaultValue={defaultTime.time}
locale={locale}
onOk={()=>{setDefinedDatePick(false);setIsBtnShow(true)}}
autoFocus
onChange={e => {
changeDate(e);
}}
/>
<div className={styles.dateConwrap}></div>
</div>
</div>
</div>
</Drawer>
);
}
.container {
width: 100%;
.close {
width: 100%;
height: 30px;
text-align: right;
}
.inputCon {
width: 100%;
.input {
border: 0;
outline: none;
border: none;
width: 100%;
/** 修改颜色 */
outline-color: "red";
font-size: 20px;
}
}
.desc {
margin-top: 20px;
}
.calender {
width: 100%;
margin-top: 40px;
display: flex;
// height:40px;
.iconCon {
width: 30px;
display: flex;
align-items: center;
vertical-align: middle;
}
.tag{
z-index:10;
}
.dateConwrap{
position:absolute;
width:90%;
height:40px;
background-color: #fff;
}
.dateCon {
flex:1;
display: flex;
.time_btn {
margin-top: 4px;
padding: 5px;
font-size: 12px;
background-color: #f5f1f1;
z-index: 10;
border-radius: 5px;
&:hover {
background-color: #cfcbcb ;
}
.icon {
font-size: 15px;
margin-right: 3px;
}
}
.time_btn:nth-child(n+2) {
margin-left: 5px;
}
}
}
:global{
.ant-picker-input{
// display: none;
}
.ant-picker{
width:30px;
}
}
}
因为我们的数据量并不会很大,所以,我们就打算将数据存入JSON文件。而不是存入MySQL、sqlite这种数据库中。
在src新建api文件夹,并新建config.ts
和 index.ts
fetch请求
// 后端服务地址
const localServer = 'http://127.0.0.1:3000';
// 请求地址localServer + url
const api = (url: string) => fetch(localServer + url).then(response => {
response.json()
});
// 导出 api
export {api};
请求的具体路由。 先写一个测试一下
const apiConfig = {
'create':{
url:'/create',
},
}
export default apiConfig
在服务端的routes文件夹下的index.js
中添加新的路由。我们先用get请求测试一下。先随便写一个返回值。
router.get('/create',function(req,res,next){
res.send({
data:'ok'
})
})
将封装的两个文件引进去
import apiConfig from '@/api/config';
import { api } from '@/api/index'
// 创建任务
const onCreate = () => {
console.log(defaultTime);
api(apiConfig.create.url).then(data=>{
console.log(data,'api')
}).catch(e=>{
console.log(e)
})
};
进入main文件夹,
yarn start
点击创建任务按钮后发下了如下错误,这是因为没忘记启动服务端了所以一直报错服务端拒绝。
进入app下运行服务端
yarn start
再次点击创建,又发现一个报错,跨域问题
在/app/app.js中添加如下配置,解决跨域
app.all("*",function(req,res,next){
//设置允许跨域的域名,*代表允许任意域名跨域
res.header("Access-Control-Allow-Origin","*");
//允许的header类型
res.header("Access-Control-Allow-Headers","content-type");
//跨域允许的请求方式
res.header("Access-Control-Allow-Methods","DELETE,PUT,POST,GET,OPTIONS");
if (req.method.toLowerCase() == 'options')
res.send(200); //让options尝试请求快速结束
else
next();
});
刚才上面的使我们先试一试,写的get请求。我们再写一个post请求
我们现在一共三种请求方式:post请求、无参数的get请求、有参数的get请求
// 后端服务地址
const localServer = 'http://127.0.0.1:3000';
// 没有参数的get请求
const api = (url:string)=>{
return fetch(localServer + url).then((response)=>{response.json()})
}
// 有参数的get请求
// 请求地址localServer + url
const getApi = (url: string, data: any) => {
const querString = Object.entries(data).map(i => `${i[0]}=${i[1]}`);
return fetch(`${localServer + url }${querString}`).then(response => {
response.json();
});
};
// post请求
const postApi = (url: string, data: any) => fetch(localServer + url, {
method: 'post',
body: JSON.stringify(data),
// 传的是json
headers: {
'Content-Type': 'application/json'
// 'Content-Type': 'application/x-www-form-urlencoded',
},
}).then(response => {
response.json();
});
// 导出 api
export { api,getApi, postApi };
在我们创建任务组件中使用postApi
// 创建任务
const onCreate = () => {
postApi(apiConfig.create.url,{test:'test'}).then(data=>{
console.log(data,'api')
}).catch(e=>{
console.log(e)
})
};
打印一下req.body的内容
我们可以在控制台中发现得到我们请求传过来的参数
最后就是将我们要存储的数据都传给服务端。之前考虑的不是很好,这里对之前的 AddTask组件修改了很多。下面是全部代码
主要修改 就是 task对象的内容、并新增了一个control对象用于对一些状态的控制。还有一些方法中对task对象值的赋值。
import styles from './index.less';
import React, { useState, useEffect } from 'react';
import { PlusOutlined, CloseCircleOutlined } from '@ant-design/icons';
import IconFont from '@/public/Icon/index';
import { Input, Button, Tag, DatePicker, Popover } from 'antd';
import 'moment/locale/zh-cn';
// 时间选择器底部英文-->中文
import locale from 'antd/lib/date-picker/locale/zh_CN';
import moment from 'moment';
import apiConfig from '@/api/config';
import { api, postApi } from '@/api/index'
interface AddTask {
content: string;
startTime: string;
endTime: string;
timeTag:string;
}
interface controlInt{
clickStatu: false,
showTime: false,
}
export default function AddTask() {
// 用于时间选择器以及tag使用的时间
const [defaultTime, setDefaultTime] = useState<any>({
time: '',
tag: '',
});
const [task, setTask] = useState<AddTask>({
content: '',
startTime: '',
endTime: '',
timeTag:defaultTime.tag,
});
const [control,setControl] = useState({
clickStatu: false,
showTime: false,
})
// 自定义时间时的时间选择器是否展示
const [definedDatePickShow, setDefinedDatePick] = useState(false);
// useRef
const inputRef = React.useRef<any>(null);
// Input获得焦点配置参数
const sharedProps = {
style: { width: '100%' },
ref: inputRef,
};
// 关闭Tag
const closeTag = () => {
setControl({ ...control, showTime: false });
};
// 取消创建任务
const onCancle = () => {
setControl({ ...control, clickStatu: false });
};
// 在时间选择器中修改日期
const changeDate = (date:any) => {
const todayStart = moment(moment().format('YYYY-MM-DD 00:00'));
const yestardayStart = moment(moment(todayStart).subtract(1, 'days'));
const todayEnd = moment(moment().format('YYYY-MM-DD 24:00'));
const tomorrowEnd = moment(moment(todayEnd).add(1, 'days'));
console.log(tomorrowEnd, 'tomorrowEndtomorrowEnd');
let today = '';
if (todayStart < date && date < todayEnd) {
today = '(今天)';
} else if (todayEnd < date && date < tomorrowEnd) {
today = '(明天)';
} else if (yestardayStart < date && date < todayStart) {
today = '(昨天)';
} else {
today = `(星期${moment(date).day() == 0 ? '日' : moment(date).day()})`;
}
setDefaultTime({ time: date, tag: moment(date).format(`MM月DD日 ${today} HH:mm`) });
console.log(11)
setTask({...task,
endTime:date.format('YYYY-MM-DD HH:mm'),
timeTag:moment(date).format(`MM月DD日 ${today} HH:mm`)
})
};
// 创建任务
const onCreate = () => {
console.log(defaultTime);
postApi(apiConfig.create.url,{...task,
startTime: moment().format('YYYY-MM-DD HH:mm')
}).then(data=>{
console.log(data,'api')
}).catch(e=>{
console.log(e)
})
};
// 选择今天 明天
const chooseDefaultTime = (day: string) => {
if (day == 'today') {
const time = moment(moment().format('YYYY-MM-DD 18:00'));
const tag = `${moment().format('M月DD日')}(今天)18:00`;
setDefaultTime({ time, tag });
setTask({...task,
endTime:moment().format('YYYY-MM-DD 18:00'),
timeTag:tag
})
} else if (day == 'tomorrow') {
const time = moment(moment().add(1, 'days').format('YYYY-MM-DD 18:00'));
const tag = `${moment().add(1, 'days').format('M月DD日')}(明天)18:00`;
setDefaultTime({ time, tag });
setTask({...task,
endTime:moment().add(1, 'days').format('YYYY-MM-DD 18:00'),
timeTag:tag
})
}
setDefinedDatePick(false);
};
useEffect(() => {
// 点击状态是true的时候再去让Input获得焦点
control.clickStatu ? inputRef.current!.focus({ cursor: 'start' }) : '';
}, [control.clickStatu]);
return (
<div className={styles.task_add}>
{control.clickStatu ? (
<div className={styles.task_add_after}>
<Input {...sharedProps} placeholder="输入任务内容" onChange={(e)=>{
setTask({...task,content:e.target.value})
}}/>
<div className={styles.btn_container}>
{control.showTime && (
<div>
<Popover
placement="bottomLeft"
title={''}
content={(
<DatePicker
showTime
defaultValue={defaultTime.time}
locale={locale}
autoFocus
onChange={e => {
changeDate(e);
}}
/>
)}
trigger="click"
>
<Tag closable onClose={closeTag} color="gold" style={{ padding: '5px' }}>
{defaultTime.tag}
</Tag>
</Popover>
</div>
)}
{!control.showTime && (
<div className={styles.time_btn_container}>
<div
className={`${styles.time_btn} ${definedDatePickShow ? styles.disabled : ''}`}
onClick={() => {
setControl({ ...control, showTime: true });
chooseDefaultTime('today');
}}
>
<IconFont type="iconjintian" className={styles.icon} />
今天
</div>
<div
className={`${styles.time_btn} ${definedDatePickShow ? styles.disabled : ''}`}
onClick={() => {
setControl({ ...control, showTime: true });
chooseDefaultTime('tomorrow');
}}
>
<IconFont type="icona-rili2" className={styles.icon} />
明天
</div>
<div
className={styles.time_btn}
onClick={() => {
setDefinedDatePick(true);
}}
>
<IconFont type="icona-rili3" className={styles.icon} />
自定义
</div>
{definedDatePickShow ? (
<div style={{ height: '30px', lineHeight: '40px', marginLeft: '30px' }}>
<DatePicker
showTime
defaultValue={defaultTime.time}
locale={locale}
autoFocus
style={{ height: '30px', lineHeight: '40px' }}
onChange={e => {
changeDate(e);
}}
/>
<span style={{ marginLeft: '3px', color: '#1890ff' }}>
{' '}
<CloseCircleOutlined
onClick={() => {
setDefinedDatePick(false);
}}
/>
</span>
</div>
) : (
<span />
)}
</div>
)}
<div className={styles.function_btn_container}>
<Button onClick={onCancle}>取消</Button>
<Button type="primary" className={styles.create_btn} onClick={onCreate}>
创建
</Button>
</div>
</div>
</div>
) : (
<div
className={styles.task_add_before}
onClick={() => {
setControl({ ...control, clickStatu: true });
}}
>
<div className={styles.task_add_before_icon_container}>
<PlusOutlined className={styles.task_add_before_icon} />
</div>
<div className={styles.task_add_before_name}>添加任务</div>
</div>
)}
</div>
);
}
我们已经能将我们的数据传到后端。这篇我们要将数据存储下来。我们不存到数据库中,而是存入到json文件中。
在服务端创建一个db文件夹,在其下面新建一个DOING.json。点击创建任务时读取DOING.json文件,然后将数据写入JSON文件中。
我们参考一下node读取文件
const fs = require('fs')
fs.readFile('/Users/joe/test.txt', 'utf8' , (err, data) => {
if (err) {
console.error(err)
return
}
console.log(data)
})
// 读取文件
const fs = require('fs');
// 找到现在所在路径
const path = require('path')
router.post('/create', function (req, res, next) {
console.log(__dirname)
res.send({
data: 'ok'
})
我们想要找到db目录
使用path.join()
方法。 ..
到router的上一层。
router.post('/create', function (req, res, next) {
console.log(__dirname)
const dbPath = path.join(__dirname,'..')
console.log(dbPath)
res.send({
data: 'ok'
})
})
可以看到现在的路径是server这一层
再向join方法中添加参数'db',就是db文件夹的位置
const dbPath = path.join(__dirname,'..','db')
在DOING.json中随便写点
记得服务端要重新运行一下。
router.post('/create', function (req, res, next) {
console.log(__dirname)
const dbPath = path.join(__dirname,'..','db')
console.log(dbPath)
// '\\'不然有转义
fs.readFile(`${dbPath}\/DOING.json`, 'utf8', (err, data) => {
if (err) {
console.error(err)
return
}
console.log(data)
})
res.send({
data: 'ok'
})
})
可以看到读取成功了。
const newTask = req.body
const dbPath = path.join(__dirname, '..', 'db')
const dbFile = `${dbPath}\/DOING.json`
是一个嵌套,在读文件的函数中去调用写的操作函数。
如果读失败或者写失败,返回data:[]
最后写文件成功的话,返回新的json数据 📢 新的newData数据要进行一下JSON处理。
router.post('/create', function (req, res, next) {
// 创建的新的任务数据
const newTask = req.body
const dbPath = path.join(__dirname, '..', 'db')
const dbFile = `${dbPath}\/DOING.json`
// '\'不然有转义
/**
* 先读文件 再写文件
*/
fs.readFile(dbFile, 'utf8', (err, data) => {
// 失败返回空数据
if (err) {
console.error(err)
res.send({
data: [],
code: 0,
msg: err
})
return
}
// 合并新的任务数据 和 读取原文件中的数据
const newData = JSON.stringify([newTask, ...data])
fs.writeFile(dbFile, newData, wrerr => {
// 失败返回空数据
if (wrerr) {
console.error(err)
res.send({
data: [],
code: 0,
msg: err
})
return
}
console.log(newData)
//文件写入成功。
res.send({
data: newData,
code: 1,
msg: ''
})
})
})
})
当我们插入两条数据就开始变成下面这样了,被拆分成一个个字符了。
后来发现使我们格式有些问题。做如下改进:
[{},{}]
判断一下读取的数据是否为空,为空
let newData = null;
// 合并新的任务数据 和 读取原文件中的数据 记得要做一下JSON处理
if(data){
const oldData = JSON.parse(data)
// stringify() 方法将一个 JavaScript 对象或值转换为 JSON 字符串
newData = JSON.stringify([...oldData,newTask])
console.log(newData,'nnn')
}else{
// 将其放到[]中
newData = JSON.stringify([newTask])
}