今天主要来美化一下界面,优化用户体验。
文章目录
美化普通用户端
普通用户的界面就是前面展示的离线下载界面。我决定把它弄的宽一点,然后给状态加个颜色。
弄宽一点的话,我找到了 share dialog 的设置方式,偷了过来:
<Modal isOpen={true} style={{maxWidth: '760px'}} toggle={this.toggle}>
然后关于颜色问题的话,我找到了当时 Virus Scan 部分的列表显示代码,把它偷了过来:
let stateStr = <span className="text-red">Unknown</span>;
switch (item.status) {
case 1:
stateStr = <span className="text-purple">Waiting</span>;
break;
case 2:
stateStr = <span className="text-azure">Downloading</span>;
break;
case 3:
stateStr = <span className="text-green">OK</span>;
break;
case 4:
stateStr = <span className="text-red">Error</span>;
break;
}
做好的效果如图:
顺眼多了。
提交后自动删除输入框内容
啊,这其实是个小问题,提交文件后输入框内容还在,用户使用起来也不方便。加一条 JS 语句就好啦。
addOfflineDownloadTask(this.props.repoID, this.props.path, q).then((res) => {
this.setState({ q: '' }); // Here it is!
this.refreshList();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
this.setState({
errMessage: errMessage
});
});
有个BUG导致 seafevents 自动退出
在测试的时候发现每过一段时间,离线下载功能就不好用了,卡在 Waiting 状态上。添加了中间输出以后,发现问题:
[WARNING] Failed to get offline download tasks (1) from db: QueuePool limit of size 5 overflow 10 reached, connection timed out, timeout 30.00 (Background on this error at: http://sqlalche.me/e/14/3o7r).
是 SQLAlchemy 的连接线程过多了。既然刚开始的时候能用,那说明一定是处理的时候有什么线程没有关闭。
排查以后发现:
def set_record_status(self, odr_id, status, comment=None):
session: Session = self.edb_session()
try:
q = session.query(OfflineDownloadRecord).filter(OfflineDownloadRecord.odr_id == odr_id)
r = q.first()
if not r:
logger.error('No offline download record: %d.', odr_id)
return
else:
r.status = status
if comment is not None:
r.comment = comment
session.commit()
except Exception as e:
logger.warning('Failed to update offline download status from db: %s.', e)
罪魁祸首就是这个函数,应该在后面加上:
finally:
session.close()
就好了。
添加用户界面本地化
本地化好说,还是那两个文件。
弄好本地化以后的效果:
BUG:中文导致 events 退出的问题
测试的时候发现,只要给出一个中文的地址,这玩意就会报错:
UnicodeEncodeError: 'ascii' codec can't encode characters in position 35-41: ordinal not in range(128)
但是我的文件全部设置了 utf-8 的编码,并且我尝试用 sys.getdefaultencoding
,都是输出 utf-8
,网上的方法一点用也没。
最后我想到,先前用 vim 的时候,也是完全没办法输入中文,会不会是系统不支持中文导致的?尤其是 docker 这种,它对中文的支持应该更差。
于是按照这篇教程:https://blog.csdn.net/llllllloooooo/article/details/102852027,我给容器安装了中文语言包,这样就 OK 了。
无法下载 ed2k 链接(暂未解决)
磁力链接理论上来说 Aria2 是支持的才对。。。不过看来 ed2k 的是不支持的,哈哈
我又放了一个 magnet 的链接上去,是可以下载的:
再优化一下 task 列表刷新逻辑
现在 task 列表刷新逻辑还可以再优化一下。咱们实时刷新是为了在 Task 完成的时候,用户可以及时看到。那么如果列表本身没有正在进行的任务了,那也就没有必要刷新列表了。
什么?如果用户在别的界面上创建了新任务怎么办?那关我什么事?用户闲的没事干非要这么玩吗?(呼呼,直接怼回去
intervalRefreshList() {
if (this.state.ent_ongoing_num > 0) this.refreshList();
}
优化断点续传
断点续传功能在 Aria2 上也是支持的,只要开启 -c
选项即可。不过目前的程序逻辑来看,每次启动 Aria2 都需要重新生成一个新的临时文件夹,这样效率不高啊。
我们在写程序的时候预留了一个 comment 字段在数据库里,当时是为了保存错误原因。我们知道错误原因只有在出了错以后才会保存,那没有错误的时候呢?当然就可以随便用啦。
tdir = self.db_oper.get_record_comment(download_task.odr_id)
if tdir is None or len(tdir) == 0 or not os.path.isdir(tdir):
tdir = tempfile.mkdtemp(dir=self.settings.temp_dir)
self.db_oper.set_record_comment(download_task.odr_id, tdir)
logger.debug("Created temp dir '%s' for task '%d'", tdir, download_task.odr_id)
else:
logger.debug("Using old temp dir '%s' for task '%d'", tdir, download_task.odr_id)
加一个管理员界面
管理界面就是把所有的 task 全部显示在界面上就好。模仿一下 Virus Files 的界面吧。
看起来有些简单?咳咳,说简单其实也不是那么简单,一点一点来。
左侧列表按钮
就是左边那个离线上传的选项卡需要添加一个新条目,这个就在当时给 Virus Scan 和 审计系统解除 isPro 限制的地方(frontend/src/pages/sys-admin/side-panel.js
):
{otherPermission &&
<li className="nav-item">
<Link
className={`nav-link ellipsis ${this.getActiveClass('offline-downloads')}`}
to={siteRoot + 'sys/offline-downloads'}
onClick={() => this.props.tabItemClick('offline-downloads')}
>
<span className="sf2-icon-link" aria-hidden="true"></span>
<span className="nav-text">{gettext('Offline Upload')}</span>
</Link>
</li>
}
主界面
主界面我是复制了一个 Virus Scan 的文件列表过来进行修改的。与 Virus Scan 的列表不同的是,我这个列表无需支持任何操作,因此只需要一个表格即可。
return (
<tr className={this.state.highlight ? 'tr-highlight' : ''} onm ouseEnter={this.handleMouseEnter} onm ouseLeave={this.handleMouseLeave}>
<td>{taskInfo.task_owner}</td>
<td>{taskInfo.repo_name}</td>
<td>{taskInfo.repo_owner}</td>
<td>{taskInfo.file_path}</td>
<td onClick={this.onUrlClick} title={taskInfo.url}>{this.state.showAllUrl ? taskInfo.url : shortenUrl}</td>
<td>{taskInfo.status === 4 ? `${Utils.bytesToSize(taskInfo.size)}` : '-'}</td>
<td>{stateStr}</td>
</tr>
);
这里我做了一些小优化,在管理员界面默认不显示全部的 url,只有当点击到 url 的时候,才会去显示全部的 url。
大小显示这里,是学的 all repo 那个界面,调用了 Utils.bytesToSize
函数,可以用一个好看的格式把 byte 单位的大小信息显示出来。
Paginator BUG
我在写的时候发现一个 BUG,就是它给的 Paginator,点击下一页以后虽然加载了,但是底下的页码依然显示 1,并且无法返回上一页。
但是很奇怪,这部分我明明是完全按照 virus scan 的写法来写的,怎么会出错呢?我打开了 Paginator 的源代码:
goToNext = () => {
const { currentPage, curPerPage } = this.props;
this.updateURL(currentPage + 1, curPerPage);
this.props.gotoNextPage();
}
很奇怪,这里根本没有修改 currentPage,也就是说底部的数字是取决于外面的 Component。
抱着尝试的心态,我又打开了 All Repo 的界面,结果就发现了 setState 中还有一个 currentPage 的设置:
seafileAPI.sysAdminListFileUpdateLogs(page, perPage).then((res) => {
this.setState({
logList: res.data.file_update_log_list,
loading: false,
currentPage: page,
hasNextPage: res.data.has_next_page,
});
})
然而在 Virus Scan 里并没有设置它!!!也就是说,原版的 Virus Scan 里面也是有 BUG 的!!!!!!
海文麻烦打一下钱,给你们找出来一个 BUG(
统一的 State Resolver
目前我们还是比较原始的做法,在两个界面的文件里直接用 switch-case 做状态码和文字之间的转换,并且判断是否成功之类的也是直接和数字判断。这样的坏处就是如果以后状态码修改了,那数字什么的很容易就乱了。
我在 Utils 文件里放了一个新字段:
offlineDownloadStatus: {
UNKNOWN: 0,
WAITING: 1,
QUEUING: 2,
DOWNLOADING: 3,
OK: 4,
ERROR: 5,
toDisplayText: function toDisplayText(status) {
switch (status) {
case Utils.offlineDownloadStatus.WAITING:
return <span className="text-purple">{gettext('Waiting')}</span>;
case Utils.offlineDownloadStatus.QUEUING:
return <span className="text-purple">{gettext('Queuing')}</span>;
......
}
}
},
然后在代码里就可以这么用:
let is_not_ok = this.state.taskList[i].status === Utils.offlineDownloadStatus.WAITING ||
this.state.taskList[i].status === Utils.offlineDownloadStatus.QUEUING ||
this.state.taskList[i].status === Utils.offlineDownloadStatus.DOWNLOADING;
啊,虽然代码长度增加了,但是可读性也强了很多。
数据问题
就是要加一个新的接口。这里的接口定义我又模仿了 VirusScan 的获取列表,然后稍微改了一下,具体怎么改的就不再赘述了。
后记
好啦,离线下载功能也完成啦,我给自己安排的任务就基本完成啦。
整个离线下载功能其实满打满算的话,总共用了 3 天的时间。根据整个博客看下来,不算很难?当然不是,之所以这次开发这么顺利,是因为有先前的经验积累,我知道每个功能大致的实现方式,知道每个组件大致的位置和逻辑,所以我自己添加新功能的时候,每当我需要借鉴什么,我都能快速定位到对应的位置。整这种大项目不仅需要项目经验,更需要对【这个】项目有经验(毕竟这玩意劝退了我们小组的两名组员。。。
另外,千万不要觉得我前面“偷” pro 版本的功能很简单,我只是在某些地方轻描淡写,说“啊,pro 里有拿过来用就好啦”,但是至于我是怎么发现的,怎么拿来的,中间做了哪些繁琐的尝试和修改,提到的不多,但也是最难的。你想想,如果只是复制几个文件过来,随便改几行代码就把 pro 的功能弄好了,那海文公司还卖什么 pro 版?还赚什么钱?以后哪个公司想用 Seafile pro 的功能,随便找个本科生程序员来改改就有了,那还了得吗?
一些探索过程之类的东西没有写到博客里,一方面因为某些探索过程实在是比较长、比较繁杂,写到博客里也没意思,另外就是我一般找到头绪以后会顺着一直往前走,等都弄完以后才来写博客,有些也就忘了。