React引入组件出现Module not found: Can't resolve 'components'

有如下组件:

clipboard.png

index.js:(src/components/FileTree)

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import './index.less'
import { Icon, Input, Popconfirm } from 'antd'
import classnames from 'classnames'
import { Iconfont } from 'components'

export default class FileTree extends Component {
  constructor (props) {
    super(props)
    this.state = {
      count: 0,
      selectedKey: props.selectedKey || null,
      expandedKeysMap: this.arrayToMap(props.expandedKeys),
      hoverNode: null,
      confirmVisible: false
    }

    this.tempExpandedMap = {}
    this.placeHolder = document.createElement('li')
    this.placeHolder.className = 'tree-placeholder'
  }

  arrayToMap (list) {
    const object = {}
    list && list.map(l => {
      object[l] = true
    })
    return object
  }

  componentWillReceiveProps (nextProps) {
    if (nextProps.selectedKey !== this.props.selectedKey) {
      this.setState({
        selectedKey: nextProps.selectedKey
      })
    }
    if (nextProps.expandedKeys && nextProps.expandedKeys.length !== this.props.expandedKeys.length) {
      this.setState({
        expandedKeysMap: this.arrayToMap(nextProps.expandedKeys)
      })
    }
    if (nextProps.search !== this.props.search) {
      // 保留搜索前的展开状态
      if (this.props.search === '') {
        this.preMap = { ...this.state.expandedKeysMap }
      }
      // 恢复搜索前的展开状态
      if (nextProps.search === '') {
        this.setState({
          expandedKeysMap: { ...this.preMap }
        })
        return
      }
      this.tempExpandedMap = {}
      // 查找需要展开的节点
      this.filterTree(nextProps.data, nextProps.search, false)
      this.setState({
        expandedKeysMap: { ...this.tempExpandedMap }
      })
    }
  }

  onClick (el) {
    const { onClick, onSelectedKeyChange } = this.props
    const { expandedKeysMap } = this.state
    expandedKeysMap[el.id] = !expandedKeysMap[el.id]
    this.setState({
      selectedKey: el.id,
      expandedKeysMap
    })
    onSelectedKeyChange && onSelectedKeyChange(el.id, el)
    onClick && onClick(el)
  }

  onDoubleClick (e, el) {
    this.props.onDoubleClick && this.props.onDoubleClick(e, el, this.props.type)
  }

  onContextMenu (el, e) {
    e.stopPropagation()
    this.props.onContextMenu && this.props.onContextMenu(e, el)
  }

  onDragStart (it, e) {
    e.stopPropagation()
    this.dragged = e.currentTarget.parentNode
    e.dataTransfer.effectAllowed = 'move'
    this.props.onDrag && this.props.onDrag(it)
  }

  onDragEnd (it, e) {
    const { data, onMove } = this.props
    const from = this.dragged.dataset.id
    const to = this.over.dataset.id
    const fromPath = from.split('_')
    const toPath = to.split('_')
    const isAfter = this.nodePlacement === 'after'

    this.dragged.style.display = 'block'
    this.over.parentNode.removeChild(this.placeHolder)

    // 寻找需要更新的两个文件夹并进行移动
    if (fromPath.length === toPath.length) {
      let fromSearch = data
      let toSearch = data
      fromPath.map((f, i) => {
        const fromStep = parseInt(f)
        let toStep = parseInt(toPath[i])
        if (i === fromPath.length - 1) {
          if (isAfter) toStep++
          if (fromSearch === toSearch && fromStep < toStep) toStep--
          // 移动源数据
          onMove(data, fromSearch, fromStep, toSearch, toStep)
          // toSearch.splice(toStep, 0, fromSearch.splice(fromStep, 1)[0])
        } else {
          fromSearch = fromSearch[fromStep].subTree
          toSearch = toSearch[toStep].subTree
        }
      })
      // 关闭拖拽
      this.triggerDraggable(it, false)()
    }
  }

  onDrop (it, e) {
    e.stopPropagation()
    e.preventDefault()
    this.props.onDrop && this.props.onDrop(it)
  }

  onDragEnter (it, e) {
    const { expandedKeysMap } = this.state
    const targetLi = e.currentTarget.parentNode
    // 同级拖动,折叠文件夹,子级拖动到父级,展开文件夹
    expandedKeysMap[it.id] = targetLi.parentNode !== this.dragged.parentNode
    this.setState({
      expandedKeysMap
    })
  }

  onDragOver (e) {
    e.preventDefault()

    const targetLi = e.currentTarget.parentNode
    this.dragged.style.display = 'none'

    // 子文件夹不能添加到和父文件夹同一级,拖动到父文件夹上时默认不处理,现在只支持两级
    if (targetLi.parentNode === this.dragged.parentNode.parentNode.parentNode) {
      e.dataTransfer.dropEffect = 'none'
      return
    }
    // 父文件夹不能添加到和子文件夹同一级,拖动到子文件夹上时默认不处理,现在只支持两级
    if (targetLi.parentNode.parentNode.parentNode === this.dragged.parentNode) {
      e.dataTransfer.dropEffect = 'none'
      return
    }

    this.over = targetLi
    const relY = e.clientY - this.over.offsetTop
    const height = this.over.offsetHeight / 2
    const parent = targetLi.parentNode
    if (relY > height) {
      this.nodePlacement = 'after'
      parent.insertBefore(this.placeHolder, targetLi.nextElementSibling)
    } else if (relY < height) {
      this.nodePlacement = 'before'
      parent.insertBefore(this.placeHolder, targetLi)
    }
  }

  onDragLeave () {
  }

  // 拖拽开关
  triggerDraggable = (el, can) => () => {
    el.draggable = can
    const { expandedKeysMap } = this.state
    expandedKeysMap[el.id] = false
    this.setState({
      expandedKeysMap
    })
  }

  // 添加Hover样式
  addHover = (el) => () => {
    if (this.state.confirmVisible) {
      return
    }
    this.setState({
      hoverNode: el.id
    })
  }

  // 移除当前Hover样式
  removeHover = () => {
    if (this.state.confirmVisible) {
      return
    }
    this.setState({
      hoverNode: null
    })
  }

  onConfirmPopover = (visible) => {
    this.setState({
      confirmVisible: visible
    })
  }

  renderAContent (data, el) {
    const { expandedKeysMap } = this.state
    const { search, readOnly, name } = this.props
    const { content, isEdit, id, subTree, count } = el
    // 高亮关键字
    let showContent = content
    if (search.length) {
      const index = content.toLowerCase().search(search.toLowerCase())
      if (index > -1) {
        const beforeStr = content.substr(0, index)
        const afterStr = content.substr(index + search.length)
        const matchStr = content.substr(index, search.length)
        showContent = (
          <span>{beforeStr}<span style={{ color: '#f50', backgroundColor: '#ffff00' }}>{matchStr}</span>{afterStr}</span>
        )
      }
    }
    return (
      <div className="tree_detail"
        onMouseEnter={this.addHover(el)}>
        <div className="tree_main">
          {!isEdit ? (
            <div className="tree_hd">
              <Icon type={ subTree != null ? (!expandedKeysMap[id] ? 'plus-square-o' : 'minus-square-o') : ''}/>
              <span className="tree_title" title={content}>
                {showContent}({count || 0})
              </span>
            </div>
          ) : (
            <div className="tree_hd">
              <Input
                autoFocus
                defaultValue={content}
                onPressEnter={this.hideEdit(el)}
                onBlur={this.hideEdit(el)}
                className="tree_input"
              />
            </div>
          )}
        </div>
        {isEdit || readOnly ? null : (
          <div className="tree_addon">
            <span>
              <Iconfont type="edit" onClick={this.showEdit(el)}/>
              <Popconfirm
                title={
                  <div style={{ width: 300 }}>
                    <p className="tree-confirm-title">删除{name}</p>
                    <p className="tree-confirm-content">{name === '标签' && count ?
                      `该${name}将从系统中删除,您无法再通过“${content}”找到对应的${count}项任务,确定删除吗?` :
                      `确定删除“${content}”吗?`}</p>
                  </div>
                }
                okType="danger"
                okText="删除"
                cancelText="取消"
                overlayClassName="tree-popover-confirm"
                onConfirm={this.deleteItem(data, el)}
                onVisibleChange={this.onConfirmPopover}
                onClick={e => e.stopPropagation()}
              >
                <Icon type="close-circle-o" />
              </Popconfirm>
              {search.length || data.length < 2 ? null : <Iconfont type="sider-collaspe"
                onMouseDown={this.triggerDraggable(el, true)}
                onMouseUp={this.triggerDraggable(el, false)}/>}
            </span>
          </div>
        )}
      </div>
    )
  }

  renderDropDir (data, liClass, el, subTree, positionId) {
    const { readOnly } = this.props
    return (
      <li className={classnames('tree_item', liClass, { 'actived': this.state.selectedKey === el.id }, { 'hover': this.state.hoverNode === el.id })}
        key={el.id}
        data-id={positionId}
        onContextMenu={this.onContextMenu.bind(this, el)}>
        <div className="tree_wp"
          onDrop={readOnly ? null : this.onDrop.bind(this, el)}
          onDragOver={readOnly ? null : this.onDragOver.bind(this)}
          onDragEnter={readOnly ? null : this.onDragEnter.bind(this, el)}
          onDragLeave={readOnly ? null : this.onDragLeave.bind(this)}
          onClick={this.onClick.bind(this, el)}
          onDoubleClick={this.onDoubleClick.bind(this, el)}>
          {this.renderAContent(data, el)}
        </div>
        {subTree}
      </li>
    )
  }

  renderDragItem (data, liClass, el, subTree, positionId) {
    const { readOnly } = this.props
    return <li className={classnames('tree_item', liClass, { 'actived': this.state.selectedKey === el.id }, { 'hover': this.state.hoverNode === el.id })}
      key={el.id}
      data-id={positionId}
      onContextMenu={this.onContextMenu.bind(this, el)}>
      <div className="tree_wp"
        draggable={!readOnly && el.draggable}
        onDragStart={readOnly ? null : this.onDragStart.bind(this, el)}
        onDragEnd={readOnly ? null : this.onDragEnd.bind(this, el)}
        onClick={this.onClick.bind(this, el)}
        onDoubleClick={this.onDoubleClick.bind(this, el)}>
        {this.renderAContent(data, el)}
      </div>

      {subTree}
    </li>
  }

  renderLi (data, liClass, el, subTree, positionId) {
    return <li className={classnames('tree_item', liClass, { 'actived': this.state.selectedKey === el.id }, { 'hover': this.state.hoverNode === el.id })}
      key={el.id}
      data-id={positionId}
      onContextMenu={this.onContextMenu.bind(this, el)}>
      <div className="tree_wp"
        onClick={this.onClick.bind(this, el)}
        onDoubleClick={this.onDoubleClick.bind(this, el)}>
        {this.renderAContent(data, el)}
      </div>

      {subTree}
    </li>
  }

  renderDragAndDropItem (data, liClass, el, subTree, positionId) {
    const { readOnly } = this.props
    return (
      <li className={classnames('tree_item', liClass, { 'actived': this.state.selectedKey === el.id }, { 'hover': this.state.hoverNode === el.id })}
        key={el.id}
        data-id={positionId}
        onContextMenu={this.onContextMenu.bind(this, el)}>
        <div className="tree_wp"
          draggable={!readOnly && el.draggable}
          onDragStart={readOnly ? null : this.onDragStart.bind(this, el)}
          onDragEnd={readOnly ? null : this.onDragEnd.bind(this, el)}
          onDrop={readOnly ? null : this.onDrop.bind(this, el)}
          onDragOver={readOnly ? null : this.onDragOver.bind(this)}
          onDragEnter={readOnly ? null : this.onDragEnter.bind(this, el)}
          onDragLeave={readOnly ? null : this.onDragLeave.bind(this)}
          onClick={this.onClick.bind(this, el)}
          onDoubleClick={this.onDoubleClick.bind(this, el)}>
          {this.renderAContent(data, el)}
        </div>

        {subTree}
      </li>
    )
  }

  renderTree (parentId, displayData, originalData, isRoot = false, parentPosition = '') {
    const { readOnly } = this.props
    const { expandedKeysMap } = this.state
    return (
      <ul className={classnames('tree', { 'tree-root': isRoot })}
        onMouseLeave={isRoot ? this.removeHover : null}>
        {displayData && displayData.map((it, index) => {
          const { id, subTree, filteredSubTree, liClass, isDrop, isDrag } = it
          const positionId = parentPosition ? parentPosition + '_' + index : `${index}`
          const subTreeRender = filteredSubTree ? this.renderTree(id, filteredSubTree, subTree, false, positionId) : ''
          const isOpened = !!expandedKeysMap[id]
          const liClassName = (isOpened ? 'tree-open' : '') + ' ' + (liClass || '')
          if (isDrop && !isDrag) {
            return this.renderDropDir(originalData, liClassName, it, subTreeRender, positionId)
          } else if (isDrag && !isDrop) {
            return this.renderDragItem(originalData, liClassName, it, subTreeRender, positionId)
          } else if (isDrop && isDrag) {
            return this.renderDragAndDropItem(originalData, liClassName, it, subTreeRender, positionId)
          } else {
            return this.renderLi(originalData, liClassName, it, subTreeRender, positionId)
          }
        })}
        {isRoot || readOnly ? null : (
          <li className="tree_item-add">
            <a href="javascript:void(0)" onClick={this.addItem(originalData, parentId)}><Icon type="plus"/>新建</a>
          </li>
        )}
      </ul>
    )
  }

  showEdit = (el) => (e) => {
    e.stopPropagation()
    el.isEdit = true
    this.forceUpdate()
  }

  hideEdit = (el) => (e) => {
    const { onRename, onCreate, data } = this.props
    el.isEdit = false
    if (e.target.value || el.isNew) {
      const newName = e.target.value.trim()
      if (el.isNew) {
        if (newName) {
          onCreate(data, el, newName)
        } else {
          el.parent.pop()
        }
      } else {
        if (newName !== el.content) {
          onRename(data, el, newName)
        }
      }
      el.content = newName
    }
    this.forceUpdate()
  }

  addItem = (parent, parentId) => () => {
    const { count } = this.state
    const newItem = {
      'content': '',
      'id': `new${count}`,
      'isDir': false,
      'isDrag': true,
      'isDrop': true,
      'isEdit': true,
      'isNew': true,
      'parentId': parentId,
      'parent': parent
    }
    parent.push(newItem)
    this.setState({
      count: count + 1
    })
  }

  deleteItem = (parent, el) => (e) => {
    const { onDelete, data } = this.props
    e.stopPropagation()
    let index = -1
    parent.find((p, i) => {
      if (p.id === el.id) {
        index = i
        return true
      }
    })
    if (index > -1) {
      onDelete(data, el, parent, index)
      // parent.splice(index, 1)
      this.forceUpdate()
    }
  }

  filterTree (data, search, stop) {
    return data.filter(d => {
      const { subTree, content } = d
      let exist = content.toLowerCase().search(search.toLowerCase()) > -1 || stop
      if (subTree) {
        // 如果父级匹配到了,那么子级就不用再过滤了
        const filteredSubTree = this.filterTree(subTree, search, exist)
        d.filteredSubTree = filteredSubTree
        if (filteredSubTree.length) {
          exist = true
        }
        // 记录需要展开子树的节点
        if (search && exist) {
          this.tempExpandedMap[d.id] = true
        }
      }
      return exist
    })
  }

  render () {
    const { data, noDataTips, search } = this.props
    const filteredTree = this.filterTree(data, search, false)
    return filteredTree.length ? this.renderTree(0, filteredTree, data, true) : <span>{noDataTips}</span>
  }
}

FileTree.defaultProps = {
  name: '文件夹',
  search: '',
  noDataTips: '没有找到相关内容',
  readOnly: false,
  expandedKeys: []
}

FileTree.propTypes = {
  type: PropTypes.string,
  name: PropTypes.string,
  readOnly: PropTypes.bool,
  data: PropTypes.array,
  onChange: PropTypes.func,
  onClick: PropTypes.func,
  onDoubleClick: PropTypes.func,
  onContextMenu: PropTypes.func,
  onDrag: PropTypes.func,
  onDrop: PropTypes.func,
  search: PropTypes.string,
  noDataTips: PropTypes.string,
  selectedKey: PropTypes.any,
  expandedKeys: PropTypes.array,
  onSelectedKeyChange: PropTypes.func,
  onRename: PropTypes.func,
  onDelete: PropTypes.func,
  onCreate: PropTypes.func,
  onMove: PropTypes.func,
}

在(src/Pages/Favorite/Favorite.js)使用如下语句引入:

import FileTree from '@/components/FileTree'

出现下图错误:

clipboard.png

检查到已经export了这个组件,请各位大神看看还有哪里出错了呢?

阅读 31.5k
2 个回答
import 'components'

会被认为是引用全局module,如果要引用当前目录下的某个组件,应该这么写:

import './components'

https://segmentfault.com/a/11...
前段时间的一篇文章这里有介绍到module的路径问题,虽说是原生ES-Module,但是在处理非全局module下,规则基本通用(除了后缀)

写成这样试试
import FileTree from '@/components/FileTree/index'

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏