前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >umi+electron开始一个桌面应用

umi+electron开始一个桌面应用

作者头像
用户4793865
发布2023-02-03 15:31:03
5.1K0
发布2023-02-03 15:31:03
举报
文章被收录于专栏:前端小菜鸡yym

theme: channing-cyan highlight: androidstudio


「这是我参与2022首次更文挑战的第11天,活动详情查看:2022首次更文挑战

创建umi

我们的项目名称TODO-WORKBENCH,然后再其下面新建main文件夹。我们这里用的是umi框架,cd进入到main文件夹。执行创建命令yarn create @umijs/umi-app来创建umi项目。创建完成后,安装依赖:执行yarn。然后再main文件夹下启动:yarn start。如下图,说明我们创建完成了一个umi项目。

image.png
image.png

在pages/index.tsx 就是我们见到的页面

image.png
image.png

如下,.umirc.ts文件是umi配置的路由。我们现在创建的是简单的umi项目。其实复杂的路由配置不会放到.umirc.ts文件,会有一个config文件。

image.png
image.png

electron

我们想要实现的是一个桌面应用,这里就需要使用electron外壳再去包裹umi。

什么是electron?

介绍

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包管理文件。

加入electron执行配置

在package.json中加入,当我们执行yarn start就相当于执行了electron .命令。

代码语言:javascript
复制
"scripts": {
      "start": "electron ."
  }
代码语言:javascript
复制
{
  "name": "todo-workbench-app",
  "version": "1.0.0",
  // 入口文件是index.js
  "main": "index.js",
  "license": "MIT",
  "scripts": {
      "start": "electron ."
  }
}

创建入口文件

如上,其入口文件是index.js

代码语言:javascript
复制
// 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

代码语言:javascript
复制
<!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>

安装依赖

代码语言:javascript
复制
yarn add --dev electron
image.png
image.png

直到如上出现各个依赖包名称,才安装完成。

运行

代码语言:javascript
复制
yarn start

如图,打开了一个桌面应用。

image.png
image.png

express

创建

生成器 https://www.expressjs.com.cn/starter/generator.html

安装 https://expressjs.com/en/starter/installing.html

我们在我们的项目中加入服务端。在app文件夹下新建一个server文件夹,cd进入server文件下。 执行

代码语言:javascript
复制
npx express-generator

安装依赖

代码语言:javascript
复制
yarn

如下是server/express框架创建的内容

image.png
image.png

启动

如图在 /bin/www.js 中配置这我们express项目的启动端口。

image.png
image.png

执行 yarn start,在3000端口可以看到如下效果

image.png
image.png

我们在public下新建一个index.html

image.png
image.png

再次访问我们的3000端口,可以发现它就加载了index.html的内容了。

image.png
image.png

打通express和前端

打包

cd 进入main文件夹

代码语言:javascript
复制
yarn build

可以看到生成了dist文件夹,并且文件夹下有 umi.css umi.jsindex.html三个文件。

image.png
image.png

移走

将main项目中生成的三个文件都移到express项目中。(把刚才我们再express中创建的index.html也覆盖掉)。

image.png
image.png

可以看到我们前端的内容已经能在3000端口运行了

image.png
image.png

打通electron

首先,我们的服务端运行是在 /bin/www.js/ 下的。

image.png
image.png

如上图部分是服务端启动的命令代码。我们只要让它不在这个地方运行,将其在electron执行的时候再去运行就打通了electronexpress

我们将此部分写成一个函数,然后将其导出。

改变运行位置

在底部新建一个函数,将此部分剪切到函数中执行。但是注意var server = http.createServer(app);这句话不能放入函数中,因为在其他地方也使用了server变量。

代码语言:javascript
复制
const startServer = () => {
  /**
   * Listen on provided port, on all network interfaces.
   */
  server.listen(port);
  server.on('error', onError);
  server.on('listening', onListening);

}

导出

注意这里不可以使用这种导出方式

代码语言:javascript
复制
export { startServer }

会报错,因为是在node中

image.png
image.png

使用下面的导出方式

代码语言:javascript
复制
module.exports = startServer;

可以直接复制下面的内容 www.js

代码语言:javascript
复制
#!/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中引用

找到在electron的启动文件index.js,在创建窗口函数中执行 startServer 启动服务端。

image.png
image.png

运行

在 app 文件夹下 运行yarn start

image.png
image.png

electron中展示前端

app/index.html中 通过iframe引入前端页面。也就是引入 http://localhost:3000/

image.png
image.png

发现iframe只出现了一个小框,通过view-->Toggle Developer Tools打开控制台发现报错了

image.png
image.png

这个报错大概就是安全问题,iframe使用外部资源,被electron拦截了。当然其实这也不算是外部资源。

查了一下,原因在于<meta>,我们把上面的两行<meta>注释掉

可以看到前端的内容可以展示了。

image.png
image.png

去掉iframe样式

添加样式

代码语言:javascript
复制
<!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>

最后效果如下

image.png
image.png

开始组件UI编写

我们参考飞书的待办任务

image.png
image.png

因为我们这是一个单页面应用,因此不需要路由了。首先在前端的pages下新建一个components文件夹用于存放组件。再分别创建 MainMenu 和 TaskList两个组件

image.png
image.png

MainMenu/index.tsx

代码语言:javascript
复制
import styles from './index.less';

export default function MainMenu() {
  return (
    <div>
      <h1 className={styles.title}>主菜单</h1>
    </div>
  );
}

TaskList/index.tsx

代码语言:javascript
复制
import styles from './index.less';

export default function TaskList() {
  return (
    <div>
      <h1 className={styles.title}>任务列表</h1>
    </div>
  );
}

引入两个组件

在主页面文件中引入两个组件

image.png
image.png
代码语言:javascript
复制
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

代码语言:javascript
复制
.page {
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  display: flex;
}

这个时候我们想看效果,不要忘了,我们是将前端打包之后的文件复制到electron中。如果想要看到效果需要再次打包,或者直接运行前端来看效果。

我们进入到main文件夹下 yarn start启动项目。在 8000端口下看效果

image.png
image.png

简单布局

主菜单占小部分,任务列表占大部分。分别为MainMenu和TaskList添加样式

MainMenu

index.tsx

代码语言:javascript
复制
import styles from './index.less';

export default function MainMenu() {
  return (
    <div className={styles.main_menu}>
      <h1 className={styles.title}>主菜单</h1>
    </div>
  );
}

index.less

宽度占20%、给一个最小宽度。当缩小浏览器,其到100px后就不再缩小。

代码语言:javascript
复制
.main_menu {
  background: rgb(121, 242, 157);
  width: 20%;
  min-width: 100px;
}

TaskList

index.tsx

代码语言:javascript
复制
import styles from './index.less';

export default function TaskList() {
  return (
    <div className={styles.task_list}>
      <h1 className={styles.title}>任务列表</h1>
    </div>
  );
}

index.less

flex:1 让其占据剩余的所有空间。

代码语言:javascript
复制
.task_list {
  background: rgb(134, 109, 204);
  // 占用剩余空间
  flex:1;
}

效果如下

image.png
image.png

实现左侧导航的组件

image.png
image.png

新建

在MainMenu文件夹下新建components用于存放组件。我们再给这个组件起名为MainItem

image.png
image.png

MenuItem/index.tsx

如下的Iprops是ts语法,定义props的数据类型。

name是字符串类型,对应进行中、由我处理等。

count是数值型,对应进行中的数量。

icon是React节点,对应着图标元素,这里我们先不添加。

active是布尔类型,对应着点击的激活状态。

onClick是空返回值类型的函数,对应着点击事件。

代码语言:javascript
复制
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>
);
}

引用MenuItem组件

代码语言:javascript
复制
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,所以样式有些丑。

image.png
image.png

MainMenu

给MainItem的容器MainMenu先进行改造。

MainMenu/index.less

让 main_menu 内部也是flex布局,垂直方向。

代码语言:javascript
复制
.main_menu {
  background: rgb(121, 242, 157);
  width: 20%;
  min-width: 100px;
  display: flex;
  flex-direction: column;
  padding: 4px;
}
image.png
image.png

去掉main_menu的背景色

MenuItem/index.less

给按钮添加样式

代码语言:javascript
复制
.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;
  // 鼠标悬停样式

  &amp;: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;
  }
}

效果如下

image.png
image.png

动态变量生成MenuItem

新建config.tsx

存放MenuItem数据

image.png
image.png
代码语言:javascript
复制
const config = [
    {
        name:'进行中',
        key:'doing',
        count:1
    },
    {
        name:'已完成',
        key:'done',
        count:10
    }
]

export default config;

MainMenu/index.tsx

动态渲染MenuItem组件。

代码语言:javascript
复制
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>
  );
}

这里提一个问题:

代码语言:javascript
复制
// 使用大括号就需要写return
map((item=>{return <div>11</div>}))
// 使用大括号就需要写return
map((item=>(<div>11</div>)))

实现任务列表的组件

image.png
image.png

创建文件

在TaskList下新建一个components文件夹用于存放TaskItem

image.png
image.png

同样在TaskItem文件夹下新建index.tsx index.less

index.tsx

代码语言:javascript
复制
import styles from './index.less';

export default function TaskItem() {
  return (
    <div>
    </div>
  );

约束数据

我们看一下这个组件都需要哪些数据,并且约束一下数据类型

image.png
image.png
代码语言:javascript
复制
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>
  );
}

开始画我们组件

实现的效果大概是这样

image.png
image.png

改主菜单部分布局

之前做好的主菜单部分的内容,我们为了适应右边的任务列表做了一些改动。

MainMenu/index.tsx

代码语言:javascript
复制
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>
  );
}

MainMenu/index.less

代码语言:javascript
复制
.main_menu {
  width: 20%;
  min-width: 100px;
  display: flex;
  flex-direction: column;
  padding:0px 4px 0px 20px;
  // 新增加了边距
  .title_container{
    margin:3px;
  }
}

TaskItem组件

index.tsx

代码语言:javascript
复制
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>
  );
}

index.less

代码语言:javascript
复制
.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;
  // 加入悬停阴影
  &amp;: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较多的时候,我们让其滚动。但是滚动条又不是很好看,我们这里将其隐藏掉,但是依旧能滚动。

TaskList/index.tsx

代码语言:javascript
复制
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>
  );
}

TaskList/index.less

代码语言:javascript
复制
.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; /*对水平流动条有效*/
  }

实现一下添加任务

image.png
image.png
image.png
image.png

创建文件

在TaskList/components下新建AddTask组件

image.png
image.png

index.tsx

我们看一下都需要哪些变量,及其类型,因为我们的数据都在组件内部使用,所以不需要props。

  • 首先,需要认为内容。
  • 任务时间,这里最好有创建时间、和截止时间。
  • 还有鼠标焦点放在输入框和不在输入框的状态,来切换样式。 ```js import styles from './index.less'; interface AddTask{ content:string; startTime:string; endTime:string; clickStatu:boolean; showTime: boolean; } export default function AddTask() { const [task, setTask] = useState({ content:'', startTime:'', endTime:'', clickStatu:false, showTime: false; }) return ( );

}

代码语言:javascript
复制
## 样式功能完善
### 未点击前
![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>
  );
}

index.less

代码语言:javascript
复制
.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;

  &amp;_before {
    height: 50px;
    width: 100%;
    display: flex;

    &amp;_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;
        }
      }   
    }
  

    &amp;_name {
      color: #ada7a7;
      line-height: 50px;
      padding-left: 10px;

    }
  }
}

我们实现了如下效果,并且点击之后切换到另一个状态(下面要写的添加内容状态)。

image.png
image.png

点击后【添加内容】

首先上面是一个输入框,下面是今天、明天及自定义时间,还有创建按钮和取消按钮。

image.png
image.png

我们实现的效果如下

image.png
image.png

我这里先·贴上完整代码,然后再分开讲解每一部分

代码语言:javascript
复制
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 &amp;&amp; date < todayEnd) {
      today = '(今天)';
    } else if (todayEnd < date &amp;&amp; date < tomorrowEnd) {
      today = '(明天)';
    } else if (yestardayStart < date &amp;&amp; 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 &amp;&amp; (
              <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 &amp;&amp; (
              <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>
  );
}
代码语言:javascript
复制
.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;

  &amp;_before {
    height: 50px;
    width: 100%;
    display: flex;

    &amp;_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;
        }
      }   
    }
  

    &amp;_name {
      color: #ada7a7;
      line-height: 50px;
      padding-left: 10px;
    }
  }

  &amp;_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;
          

          &amp;: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组件中正好有这个功能。

代码语言:javascript
复制
// 导入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>
        )
代码语言:javascript
复制
.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;
   
   &amp;_before{ .... }
   // &amp; 代表task_add
   &amp;_after{
      width: 100%;
      padding: 10px;
   }
 }

图标的使用

image.png
image.png

我们用的是阿里的iconfont图标,图标的使用可以看我之前的一篇文章✈️

因为我们使用的图标是自定义图标,所以最好封装成一个组件。我在src下新建了一个public文件夹,用于存放Icon组件

image.png
image.png

在Icon下新建index.tsx

代码语言:javascript
复制
import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({
  scriptUrl: [ 
    // 每次更新图标需要更换此地址
    '//at.alicdn.com/t/font_2503482_lgnt38a7d1f.js'
  ]
});
export default IconFont

然后在我们的 AddTask组件中引用

代码语言:javascript
复制
import IconFont from '@/public/Icon';
// type对应 iconfont官网复制代码的内容
<IconFont type="iconjintian" className={styles.icon} />
image.png
image.png
image.png
image.png

底部布局

定义了一个calss time_btn_container来存放底部的所有内容。其子元素有 time_btn_containerfunction_btn_containertime_btn_container下的按钮我也成了div,class名为time_btn

image.png
image.png
代码语言:javascript
复制
  &amp;_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;
   
          &amp;: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;
        }
      }
      
    }
  }

点击今天切换为Tag

点击今天后默认时间是18:00并且将这些按钮替换为了CloseTag

image.png
image.png
image.png
image.png

今天、明天、自定义按钮和Tag的切换。通过task.showTime值来控制

  • task.showTime是true显示 Tag。
  • task.showTime是false显示今天、明天、自定义按钮。

写法

代码语言:javascript
复制
{task.showTime &amp;&amp; (<div>Tag</div> ) }
{!task.showTime &amp;&amp; (<div>今天明天按钮</div> ) }

这是今天的按钮,onClick事件 让task.showTime变为true。同时触发chooseDefaultTime('today')函数。

代码语言:javascript
复制
   <div
     className={styles.time_btn}
     onClick={() => {
            setTask({ ...task, showTime: true });
            chooseDefaultTime('today');
       }}>
       <IconFont type="iconjintian" className={styles.icon} />
       今天
   </div>

chooseDefaultTime函数

这里用到了一个新变量defaultTime,time是时间选择器DatePicker使用的moment对象。tag是标签需要用到的时间格式。对了时间选择器用到moment我们还需要导入moment

代码语言:javascript
复制
  // 用于时间选择器以及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)
  };

closeTag

代码语言:javascript
复制
// closable 变为可关闭标签 color 标签颜色 onClose 关闭函数
  <Tag closable onClose={closeTag} color="gold" style={{ padding: '5px' }}>
       {defaultTime.tag}
  </Tag>

点击Tag再弹出时间选择器

image.png
image.png

这里用到了弹层Popover组件,点击Tag出现弹层,只需要把Tag都写到Popover的内部即可。

这里再说一点,时间选择器是英语,而不是中文。需要引入此内容 并且在DatePicker中添加 locale={locale}

代码语言:javascript
复制
import 'moment/locale/zh-cn';
// 时间选择器底部英文-->中文
import locale from 'antd/lib/date-picker/locale/zh_CN';
代码语言:javascript
复制
      <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>

改变时间选择器重新渲染Tag

也就是时间选择器中的 changeDate(e); 函数

  • 我们想要今天明天昨天和星期几的效果
image.png
image.png
  • 比较时间范围 判断 是今天....,比较的事moment对象
  • 为了比较,所以需要几个时间段:今天的00:00 、今天的24:00、 昨天的 00:00、 明天的24:00。
  • subtract是moment获得前几天的方法
  • add是moment获得后几天的方法 ```js // 在时间选择器中修改日期 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')); 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) }); };
代码语言:javascript
复制
### 自定义时间

![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>
代码语言:javascript
复制
{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 />
          )}

实现任务详细的一个抽屉。

image.png
image.png

我们的实现效果

image.png
image.png

思路

  • 当我们点击任务之后,展开抽屉,并且展示任务的内容。
  • 把抽屉写成一个组件<TaskDetail>,在任务组件<TaskItem>中引用
  • <TaskItem>中定义一个变量visible传入组件<TaskDetail>中,控制抽屉开关
  • 同样将<TaskDetail>中展示需要的数据,通过props传给它。
  • 抽屉中有标题、任务详情、和时间的展示和修改。

新建文件夹

image.png
image.png

文件

image.png
image.png

index.tsx 我们进行了props的类型约束。

  • data 是任务组件的数据,包括五项内容
  • visible 是控制抽屉展开和关闭的变量
  • handleClose 是用于关闭抽屉的回调函数,当关闭抽屉,子组件传值到父组件去改变父组件中的visible的值。
  • timeTag 是时间标签的值,我们在创建任务的时候把这个值存上,那么就省着在这个组件做处理了
image.png
image.png
代码语言:javascript
复制
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>
  );
}

添加抽屉并能开关

导入组件

这有的是后面要用的,在这里都先引用。

代码语言:javascript
复制
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';

参数分别是

  • placement="right" 在右侧打弹框
  • onClose={handleClose} 关闭抽屉函数
  • visible={visible} 展示抽屉
  • keyboard={true} 是否支持键盘esc关闭
  • closable={false} 是否显示左上角的关闭按钮 因为他是在左侧的,有些突兀我们将它去掉自己再写一个图标作为关闭按钮 ```js export default function TaskDetail(props: TaskDetailProps) { const { TextArea } = Input; //解构出props内容 const { data, visible, handleClose,timeTag } = props; // 修改后提交用到的参数 const [submitData, setSubmitData] = useState({ title: '', desc: '', startTime: '', endTime: '', status: '', }); return ( );

}

代码语言:javascript
复制
对应的样式

```less
.container {
  width: 100%;

  .close {
    width: 100%;
    height: 30px;
    text-align: right;
  }
 }

任务标题输入框

这里我用的普通的 <input>并没用antdesign的组件。我们这里并不想要input的边框。

代码语言:javascript
复制
 <div className={styles.inputCon}>
          <input
            className={styles.input}
            value={submitData.title}
            onChange={e => {
              setSubmitData({ ...submitData, title: e.target.value });
            }}
          />
  </div>

对应样式

代码语言:javascript
复制
  .inputCon {
    width: 100%;
    .input {
      /** 去掉边框 */
      border: 0;
      outline: none;
      border: none;
      width: 100%;

      /**  修改颜色 */
      outline-color: "red";
      font-size: 20px;
    }
  }

任务描述文本框

代码语言:javascript
复制
 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>

对应样式

代码语言:javascript
复制
  .desc {
    margin-top: 20px;
  }

时间

展示内容切换

代码语言:javascript
复制
 // 用于时间标签和按钮的切换
 const [isBtnShow,setIsBtnShow] = useState(false)
image.png
image.png

这部分都是之前写过的处理时间的,主要的逻辑有:

  • 点击今天、明天、自定义按钮 将时间选择器的展示状态变为true。点击今天、明天调用chooseDefaultTime函数
  • 点击自定义时间按钮后,将今天、明天按钮禁掉
代码语言:javascript
复制
  // 用于时间选择器以及tag使用的时间
  const [defaultTime, setDefaultTime] = useState<any>({
    time: '',
    tag: '',
  });
  // 自定义时间时的时间选择器是否展示
  const [datePickShow, setDefinedDatePick] = useState(false);
代码语言:javascript
复制
   <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>

用到的方法也是之前添加任务时的方法

代码语言:javascript
复制
 // 选择今天 明天
  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 &amp;&amp; date < todayEnd) {
          today = '(今天)';
        } else if (todayEnd < date &amp;&amp; date < tomorrowEnd) {
          today = '(明天)';
        } else if (yestardayStart < date &amp;&amp; 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)
  };

时间选择器

去掉输入框

我们创建任务时的时间选择器,是有一个输入框,点击后才打开时间选择器的。

image.png
image.png

我们今天这里不想再有这个输入框

image.png
image.png

直接使用 open={datePickShow} 控制时间选择器的开关。

但是这个输入框还在

image.png
image.png

没有别的办法,我就用一层div将输入框盖掉。如下的className为dateConwrap

代码语言:javascript
复制
   <DatePicker
                      showTime
                      open={datePickShow}
                      defaultValue={defaultTime.time}
                      locale={locale}
                      onOk={()=>{setDefinedDatePick(false);setIsBtnShow(true)}}
                      autoFocus
                      onChange={e => {
                        changeDate(e);
                      }}
                    />
           
                    <div className={styles.dateConwrap}></div>       
代码语言:javascript
复制
    .dateConwrap{
      position:absolute;
      width:90%;
      height:40px;
      background-color: #fff;
    }

完整代码

index.tsx

代码语言:javascript
复制
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 &amp;&amp; date < todayEnd) {
          today = '(今天)';
        } else if (todayEnd < date &amp;&amp; date < tomorrowEnd) {
          today = '(明天)';
        } else if (yestardayStart < date &amp;&amp; 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>
  );
}

index.less

代码语言:javascript
复制
.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;        
        &amp;: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这种数据库中。

使用fetch调用接口

在src新建api文件夹,并新建config.tsindex.ts

image.png
image.png

index.ts

fetch请求

代码语言:javascript
复制
// 后端服务地址
const localServer = 'http://127.0.0.1:3000';
// 请求地址localServer + url
const api = (url: string) => fetch(localServer + url).then(response => {
  response.json()
});
// 导出 api
export {api};

config.ts

请求的具体路由。 先写一个测试一下

代码语言:javascript
复制
const apiConfig = {
    'create':{
        url:'/create',
    },
}
export default apiConfig

服务端接口

在服务端的routes文件夹下的index.js中添加新的路由。我们先用get请求测试一下。先随便写一个返回值。

image.png
image.png
代码语言:javascript
复制
router.get('/create',function(req,res,next){
  res.send({
    data:'ok'
  })
})

调用接口

将封装的两个文件引进去

代码语言:javascript
复制
import apiConfig from '@/api/config';
import { api } from '@/api/index'
image.png
image.png
代码语言:javascript
复制
  // 创建任务
  const onCreate = () => {
    console.log(defaultTime);
    api(apiConfig.create.url).then(data=>{
      console.log(data,'api')
    }).catch(e=>{
      console.log(e)
    })
  };

启动

进入main文件夹,

代码语言:javascript
复制
yarn start

点击创建任务按钮后发下了如下错误,这是因为没忘记启动服务端了所以一直报错服务端拒绝。

image.png
image.png

进入app下运行服务端

代码语言:javascript
复制
yarn start

再次点击创建,又发现一个报错,跨域问题

image.png
image.png

在/app/app.js中添加如下配置,解决跨域

image.png
image.png
代码语言:javascript
复制
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();
});

fetch post请求

刚才上面的使我们先试一试,写的get请求。我们再写一个post请求

api/index.ts

我们现在一共三种请求方式:post请求、无参数的get请求、有参数的get请求

代码语言:javascript
复制
  // 后端服务地址
  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 };

请求一下post请求

在我们创建任务组件中使用postApi

image.png
image.png
代码语言:javascript
复制
  // 创建任务
  const onCreate = () => {
    postApi(apiConfig.create.url,{test:'test'}).then(data=>{
      console.log(data,'api')
    }).catch(e=>{
      console.log(e)
    })
  };

服务端

打印一下req.body的内容

image.png
image.png

我们可以在控制台中发现得到我们请求传过来的参数

image.png
image.png

修改之前的创建任务组件

最后就是将我们要存储的数据都传给服务端。之前考虑的不是很好,这里对之前的 AddTask组件修改了很多。下面是全部代码

主要修改 就是 task对象的内容、并新增了一个control对象用于对一些状态的控制。还有一些方法中对task对象值的赋值。

代码语言:javascript
复制
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 &amp;&amp; date < todayEnd) {
      today = '(今天)';
    } else if (todayEnd < date &amp;&amp; date < tomorrowEnd) {
      today = '(明天)';
    } else if (yestardayStart < date &amp;&amp; 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 &amp;&amp; (
              <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 &amp;&amp; (
              <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文件中。

image.png
image.png

读取文件

node读取文件

我们参考一下node读取文件

文档地址

代码语言:javascript
复制
const fs = require('fs')
fs.readFile('/Users/joe/test.txt', 'utf8' , (err, data) => {
  if (err) {
    console.error(err)
    return
  }
  console.log(data)
})

应用到我们这里

导入

代码语言:javascript
复制
// 读取文件
const fs = require('fs');
// 找到现在所在路径
const path = require('path')

看一下所在路径

代码语言:javascript
复制
router.post('/create', function (req, res, next) {
  console.log(__dirname)
  res.send({
    data: 'ok'
  })
image.png
image.png

我们想要找到db目录

image.png
image.png

使用path.join()方法。 ..到router的上一层。

代码语言:javascript
复制
router.post('/create', function (req, res, next) {
  console.log(__dirname)
  const dbPath = path.join(__dirname,'..')
  console.log(dbPath)
  res.send({
    data: 'ok'
  })
})

可以看到现在的路径是server这一层

image.png
image.png

再向join方法中添加参数'db',就是db文件夹的位置

代码语言:javascript
复制
const dbPath = path.join(__dirname,'..','db')

测试一下读文件

在DOING.json中随便写点

image.png
image.png

记得服务端要重新运行一下。

代码语言:javascript
复制
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'
  })
})

可以看到读取成功了。

image.png
image.png

读前端传的参数

代码语言:javascript
复制
 const newTask = req.body

定义json文件路径变量

代码语言:javascript
复制
const dbPath = path.join(__dirname, '..', 'db')
const dbFile = `${dbPath}\/DOING.json`

先读文件 再写文件

是一个嵌套,在读文件的函数中去调用写的操作函数。

如果读失败或者写失败,返回data:[]

最后写文件成功的话,返回新的json数据 📢 新的newData数据要进行一下JSON处理。

代码语言:javascript
复制
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: ''
    })
  })
})
})

遇到的问题

当我们插入两条数据就开始变成下面这样了,被拆分成一个个字符了。

image.png
image.png

后来发现使我们格式有些问题。做如下改进:

代码语言:javascript
复制
[{},{}]

服务端

判断一下读取的数据是否为空,为空

代码语言:javascript
复制
 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])
  }
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-02-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 创建umi
  • electron
    • 什么是electron?
      • 介绍
      • 官网地址
    • 创建
      • 初始化
      • 加入electron执行配置
      • 创建入口文件
      • 安装依赖
      • 运行
  • express
    • 创建
      • 启动
        • 打通express和前端
          • 打包
          • 移走
        • 打通electron
          • 改变运行位置
          • 导出
          • electron中引用
          • 运行
          • electron中展示前端
          • 去掉iframe样式
          • MainMenu/index.tsx
          • TaskList/index.tsx
          • 引入两个组件
      • 开始组件UI编写
      • 简单布局
        • MainMenu
          • index.tsx
          • index.less
        • TaskList
          • index.tsx
          • index.less
      • 实现左侧导航的组件
        • 新建
          • MenuItem/index.tsx
          • 引用MenuItem组件
        • MainMenu
          • MainMenu/index.less
        • MenuItem/index.less
          • 动态变量生成MenuItem
            • 新建config.tsx
            • MainMenu/index.tsx
        • 实现任务列表的组件
          • 创建文件
            • index.tsx
          • 约束数据
            • 开始画我们组件
              • 改主菜单部分布局
                • MainMenu/index.tsx
                • MainMenu/index.less
              • TaskItem组件
                • index.tsx
                • index.less
              • 任务列表布局
                • TaskList/index.tsx
                • TaskList/index.less
            • 实现一下添加任务
              • 创建文件
                • index.tsx
                • index.less
              • 点击后【添加内容】
                • 输入框
                • 图标的使用
                • 底部布局
                • 点击今天切换为Tag
                • chooseDefaultTime函数
                • closeTag
                • 点击Tag再弹出时间选择器
                • 改变时间选择器重新渲染Tag
            • 实现任务详细的一个抽屉。
              • 思路
                • 新建文件夹
                • 文件
              • 添加抽屉并能开关
                • 导入组件
              • 任务标题输入框
                • 任务描述文本框
                  • 时间
                    • 展示内容切换
                  • 时间选择器
                    • 去掉输入框
                  • 完整代码
                    • index.tsx
                    • index.less
                • 调用接口
                  • 使用fetch调用接口
                    • index.ts
                    • config.ts
                    • 服务端接口
                    • 调用接口
                    • 启动
                  • fetch post请求
                    • api/index.ts
                    • 请求一下post请求
                    • 服务端
                  • 修改之前的创建任务组件
                  • 数据的存取
                    • 思路
                      • 读取文件
                        • node读取文件
                      • 应用到我们这里
                        • 导入
                        • 看一下所在路径
                        • 测试一下读文件
                      • 读前端传的参数
                        • 定义json文件路径变量
                          • 先读文件 再写文件
                            • 遇到的问题
                              • 服务端
                          相关产品与服务
                          云数据库 MySQL
                          腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档