2013年开始投入微信营销功能开发,主要面向公众号,以及后来的小程序,网站主要提供SAAS服务,平台内现有300多种稳定的功能程序可供选择,从建站开始至今,已经服务过近两千名客户,我们可以做到按需要定制,有专业的开发团队,可以根据客户的活动需求做出优质活动方案。价格人性化,根据不同需求,不同流量,提供长短期的租用服务。
左小皓m3u8下载合并工具是一款好用的m3u8视频流获取和合并工具,现在的直播以及视频播放采用的就是m3u8形式的,类似于一小段一小段的文件,有了这款工具就可以对整个视频进行下载和合并了,支持自定义合并名称,欢迎下载。
最新版本为1.0.20,请大家及时更新到最新版本
左小皓原创工具合集:https://lanzoui.com/b00nc1ltc
主要特色
1、一键解析M3U8地址
2、自定义下载名称
3、自定义文件的保存目录
4、下载速度快
使用教程
1.获取m3u8文件网址
我习惯使用猫抓工具来获取
2.获取到m3u8网址后,点击复制
打开软件,将网址粘贴到地址中,点击解析
3.解析成功后,可以自定义合并后视频保存的文件名,以及保存目录
设置完成后,点击下载,开始下载视频文件
4.全部下载完成后,软件自动合并保存视频文件
由于每个网站的m3u8文件规则不尽相同,会存在视频文件解析不完整的情况,遇到这种情况,可以将m3u8文件的网址回复上来,增加兼容性。
下载地址左小皓原创工具合集:https://lanzoui.com/b00nc1ltc
694BF-YUDBG-EAR69-BPRGB-ATQXH
专业版下载地址:
https://ep.wps.cn/download
http://wdl1.cache.wps.cn/wps/download/ep/WPS2019/WPSPro_11.8.2.8721.exe
WPS官网下载地址:https://ep.wps.cn/download
安装后如果没有激活也可使用下面序列号
需要安装VBA,在解压后的根目录有WPSVBA.exe
某政府版激活码:THUV2-32HH7-6NMHN-PTX7Y-QQCTH
某人社局激活码:R7AKQ-KLBXV-RNX3F-BPACQ-NQDGE
企业版序列号1:R8R8P-MTT6F-KLRPM-J7CAB-PJM8C
企业版序列号2:A4XV7-QP9JN-E7FCB-VQFRD-4NLKC
企业版序列号3:694BF-YUDBG-EAR69-BPRGB-ATQXH
企业版序列号4:7LR67-WTXPA-KLUHV-GEK2E-QW4CK
企业版序列号5:EUYTH-3KWKL-PJMX7-XBCPW-9U2DD
企业版序列号6:U2PWU-H7D9H-69T3B-JEYC2-3R2NG
企业版序列号7:7G2HE-JR8KL-ABB9D-Y7789-GLNFL
企业版序列号8:9DP6T-9AGWG-KWV33-9MPC8-JDCV
做淘客的朋友们基本总会遇到下面的这种问题,在服务器上登挂机淘客软件,可是阿里妈妈总是掉线,又不能自动重新登录,需要人工验证,麻烦的很。今天,我来告诉大家终极解决方案,来往下看。
解决方案:
首先保证你的电脑或者服务器的IE浏览器已经为11版本
在电脑上下载安装旺旺,并挂上你需要登录的淘宝号,挂两天以上
使用IE登录淘宝网,并随便找一件商品购买,并在电脑上使用支付宝支付。
支付时,会要求安装数字证书,这一步是关键,安装好证书之后,基本上就不会再出现安全验证了。
ie增强安全配置关闭
点击”WIN“打开"管理工具",找到”服务器管理器“
在”服务器管理器“中找到“服务器摘要”
在“服务器摘要”中找到“安全信息”
在“安全信息”中,找到“IE 增强的安全配置(ESC)”查看目前设置“为Administrator打开””为用户打开“。并找到"配置IE ESC"
点开配置"配置IE ESC",这里看我们的需求,如果需要为administrator关闭增强设置,则给管理员选择禁用。如果要给用户关闭增强设置,则给用户选择禁用。
购买淘客机器人-》》》点击购买
【产品提供API接口,用于对接有开发权限的应用,非市面蓝牙类产品】
产品仅供有开发能力的个人及公司对接使用,用于给自己的产品做配套的支付播报硬件工具,
二代喇叭支持以下几个自定义播报内容:
1.开机欢迎语,每个喇叭可独立设置。(非API接口控制)
2.自定义播报前缀(非API接口控制)
3.播报金额
4.播报后缀(非API接口控制)
【云喇叭小助手】
精准解决中小商户二维码收款的痛点:
■ 二维码收款到账信息不能及时确认,造成通过历史支付结果截图、或伪造收款二维码的支付诈骗隐患
■ 老板不在收银现场,收银人员无法及时确认与收款二维码绑定的老板的账户是否已收到款项
■ 老板虽然在收银现场,但收款App不在启动状态,或者手机播报声音小,无法及时获知到账信息
一、WIFI版(自带锂电池)
二、GPRS-2G流量版(自带一年流量)
支付播报业务平台(SaaS),是整个支付播报业务场景的重要组成环节,
参见如下的场景流程:
下载地址:
https://www.lanzoui.com/b00nc1ltc
【使用方法很简单】1.软件运行后,添加好需要备份的文件或文件夹,
2.按类型设置好邮箱账号信息,设置好点击保存配置,
3.再点击开始运行,
完成以上三步,即可自动按设置的备份周期将备份文件发送到指定邮箱,达到定时备份的效果
一波三折啊,经过论坛@Hmily的严格检测,帖子终于被解除屏蔽了,洗白了,这也是好事,通过Hmily对待论坛原创软件的严谨态度,可以保证论坛的用户们下载到安全的原创软件。
大家放心使用,此软件安全,没有任何后门。
【效果图】
下载地址:
https://www.lanzoui.com/b00nc1ltc
使用非常简单,就不再文字说明了
下载地址:
https://www.lanzoui.com/b00nc1ltc
专业的标签、条形码打印软件,快速便捷设计专业高质量标签、RFID、证卡&条形码。
官方网站:https://www.bartendersoftware.com
版本对比:https://www.bartendersoftware.com.cn/software/compare/
链接:https://pan.baidu.com/s/14O5L1gMBwiG3x_T-3JmKWg
提取码:okh2
复制这段内容后打开百度网盘手机App,操作更方便哦
写在前面该系列文章是为具有开发能力的朋友写作的,目的是帮助他们在scratch 3.0的基础上开发一套完整的集scratch 3.0编程工具、用户社区和作品云端存储及、品牌集成于一体的scratch编程平台。如果您不是开发者,但想要拥有自己的教育平台和品牌,也欢迎学习交流和洽谈合作。所以如果您是想学习scratch少儿编程课程,那请忽略该系列的文章。前言前面我们完成了登录弹窗按钮组件,现在我们开始对接登录接口。开始实现我们先到reducers / user - state.js中添加相关states和actions。
const DONE_USER_LOGIN = 'DONE_USER_LOGIN'; // 标识登录过程成功完成const ERROR_USER_LOGIN = 'ERROR_USER_LOGIN'; // 标识登录过程失败
const initialState = {
error: '',
// 登录错误信息 userData: null, // 用户初始信息 loginState: UserState.NOT_LOGINED, token: '' // 登录获取的token};登录的actions:
const getUserSuccess = data = & gt;
({
type: DONE_USER_LOGIN,
data: data
});
const getUserError = () = & gt;
({
type: ERROR_USER_LOGIN
});
reducer处理登录成功: const reducer = function(state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case DONE_USER_LOGIN:
return ob
ject.assign({}, state, { loginState: UserState.LOGINED,
token: action.data.token,
userData: action.data.user
});
default:
return state;
}
};我们还有一个需要export给提交按钮先来SubmitLoginButton调用的方法,在用户提交时,获取参数然后向后台获取用户信息:
const loginFromServer = (dispatch, account, password) = & gt; {从后台获取用户数据.then(({
data
}) = & gt; {
console.log({
data
});
dispatch(getUserSuccess(data.login));
}).
catch (() = & gt; dispatch(getUserError()));
};从后台获取数据我们稍后实现。记得在最后export这个方法: export {
reducer as
default, initialState as userStateInitialState, UserState, UserStates, getIsLogined, loginFromServer
};再来SubmitLoginButton中,实现我们之前预留的on
click()方法,提交用户输入的信息。 SubmitLoginButton在被时,需要从账号和密码输入框获取用户输入提交。所以它的on
click()方法需要取得LoginModal中的其他表单元素的信息,很显然我们需要将这个处理过程定义到LoginModal中,然后将这一处理过程作为SubmitLoginButton的on click属性值赋给它。这里我们把之前的LoginModal组件进行改造: const LoginModal = props = & gt; ( & lt; Modal className = {
styles.modalContent
}
contentLabel = {
props.title
}
id = & quot; loginModal & quot; onRequestClose = {
props.onCancel
} & gt; & lt; Box & gt; & lt; input className = {
styles.minInput
}
name = & quot; account & quot; placeholder = & quot;账号 & quot; type = & quot; text & quot;
/><br / & gt; & lt; input className = {
styles.minInput
}
name = & quot; password & quot; placeholder = & quot;密码 & quot; type = & quot; password & quot;
/><br / & gt; & lt; SubmitLoginButton className = {
styles.btnSubmit
}
/> </Box & gt; & lt;
/Modal>);之前我们是以方法定义的方式实现它的,如上面。
现在我们用`extends Reactponent`的方式将它改造成受控组件来完成表单内容的接收和提交:
class LoginModal extends React.Component {
constructor(props) {
super(props);
bindAll(this, ['handleAccountChange', 'handlePasswordChange', 'handleSubmit']);
this.state = {
account: '',
password: ''
};
}
handleAccountChange(e) {
this.setState({
account: e.target.value
});
}
handlePasswordChange(e) {
this.setState({
password: e.target.value
});
}
handleSubmit() {
if (this.state.account.trim() === '' || this.state.password.trim() === '') {
alert('账号和密码不能为空');
return;
}
this.props.on
submit(this.state.account, this.state.password); }
render() {
return ( & lt; Modal className = {
styles.modalContent
}
contentLabel = {
this.props.title
}
id = & quot; loginModal & quot; onRequestClose = {
this.props.onCancel
} & gt; & lt; Box & gt; & lt; input className = {
styles.minInput
}
name = & quot; account & quot; placeholder = & quot;账号 & quot; type = & quot; text & quot; value = {
this.state.account
}
on
change = { e = & gt;
this.handleAccountChange(e)
}
/ & gt; & lt; br / & gt; & lt; input className = {
styles.minInput
}
name = & quot; password & quot; placeholder = & quot;密码 & quot; type = & quot; password & quot; value = {
this.state.password
}
on
change = { e = & gt;
this.handlePasswordChange(e)
}
/><br / & gt; & lt; SubmitLoginButton className = {
styles.btnSubmit
}
on
click = { this.handleSubmit
}
/> </Box & gt; & lt;
/Modal> ); }}LoginModal.propTypes = { onCancel: PropTypes.func.isRequired, on
submit: PropTypes.func.isRequired, title: PropTypes.string.isRequired};const mapDispatchToProps = dispatch => ({ onCancel: () => dispatch(closeLoginModal()), on submit: (account, password) => loginFromServer(dispatch, account, password)});export default connect( null, mapDispatchToProps)(LoginModal);上面是我们改造后的LoginModal组件。 里面定义两个state的值用来记录输入的账号和密码。三个方法handleAccountChange,handlePasswordChange和handleSubmit分别处理账号密码的输入和提交请求。
props中的on
submit映射到刚才我们定义的longFromServer方法,将账号和密码传过去,然后与后台服务进行交互。 现在来实现与后台的交互过程。
我们使用apollo client来完成graphql接口与后台的交互。
react-apollo库有完善的使用graphql与后台进行交互并控制组件更新的功能,还包括缓存控制等等,非常强大。但是完整地使用它的话会涉及到对我们scratch里已有的组件的数据绑定和使用的修改,这个工作量不小,而且还可能引入意想不到的问题,所以我们这里只通过apollo client获取数据,更新state来控制我们组件的更新,就像传统的react-redux的工作方式一样。这可能让我们失去使用graphql的一些优势,但是综合考虑成本,我们还是需要适当取舍,毕竟我们是在二次开发别人的项目。
先安装相关的npm包:
npm install graphql apollo - client apollo - cache - inmemory apollo - li
nk - http graphql - tag在lib目录下新建一个apollo.js文件定义apollo client: import { ApolloClient
}
from 'apollo-client';
import {
InMemoryCache
}
from 'apollo-cache-inmemory';
import {
createHttpli
nk }
from 'apollo-li
nk-http'; const client = new ApolloClient({
li
nk: createHttpli nk({ uri: 'localhost:8602/graphql'
}),
cache: new InMemoryCache()
});
export
default client;
localhost:
8602 / graphql就是我们之前实现的后台用户社区服务的graphql接口地址,先暂时把它写死在代码里面。在lib中新建文件夹queries用于存放我们所有的graphql请求,现在在queries下新建目前user用户归类用户相关的请求,在user下新建login.jsx文件,定义我们的登录请求的graphql的query:
import gql from '
graphql - tag ';const LOGIN = gql`query login($account: String!, $password: String!) { login(accountaccount, passwordpassword){ user{ id name account avatar } token }}`;export default LOGIN;现在回到user-state.js中,完善loginFromServer请求后台数据的过程:
const loginFromServer = (dispatch, account, password) => { client.query({ query: LOGIN, variables: { account: account, password: password } }, ).then(({data}) => { console.log({data}); dispatch(getUserSuccess(data.login)); dispatch(closeLoginModal()); }).catch(() => dispatch(getUserError()));};这里的LOGIN就是前面我们定义的graphql query,要注意对照我们之前实现的后台接口返回的数据的结构来获取相应的数据:
{ "data": { "login": { "user": { "id": "3", "name": "馬沙拉", "account": "mashala" "avatar": "https://scratch-store.oss-cn-qingdao.aliyuncs/default-avatar.png" }, "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjQwNDgzMDMsImlhdCI6MTU2NDA0NDcwMywiaWQiOjIsInVzZXJuYW1lIjoibWFkYXJvdSJ9._rDpgQbOdoI95K7kCdN6K-taHwUF18XTIIn724grIPY" } }}现在我们开启后台服务,重新编译运行scratch-gui,进入浏览器(打开浏览器控制台),登录按钮,输入我们事先通过接口注册的账号和密码,提交,控制台中可以看到成功发送了请求并获取到结果:
MenuBar的右上角的登录按钮消失,出现了默认的scratch-cat的头像和姓名:
这说明我们的基本的登录流程已经成功打通了。只是我们还没有将用户信息应用到MenuBar上面来。
现在我们就来完成这个过程。
前面我们已经将获取到的用户信息记录在了state中的userData中。现在我们要像使用`loginState`一样在gui.jsx中使用它。
在containers/gui.jsx中像之前`loginState`一样映射userData state到组件属性:
loginState: getIsLogined(loginState),userData: state.scratchGui.userState.userData然后在components/gui/gui.jsx中定义的GUIComponentz组件中使用它:
const { accountNavOpen, activeTabIndex, alertsVisible, authorId, authorThumbnailUrl, authorUsername, ba
sePath, backdropLibraryVisible, backpackHost, backpackVisible, blocksTabVisible, cardsVisible, canCreateNew, canEditTitle, canRemix, canSave, canCreateCopy, canShare, canUseCloud, children, connectionModalVisible, costumeLibraryVisible, costumesTabVisible, enableCommunity, intl, isCreating, isFullScreen, isPlayerOnly, isRtl, isShared, loading, renderLogin, on clickAccountNav, onCloseAccountNav, onLogOut, onOpenRegistration, onToggleLoginOpen, onUpdateProjectTitle, on activateCostumesTab, on activateSoundsTab, on activateTab, on clickLogo, onExtensionButton click, onProjectTelemetryEvent, onRequestCloseBackdropLibrary, onRequestCloseCostumeLibrary, onRequestCloseTelemetryModal, onSeeCommunity, onShare, onTelemetryModalCancel, onTelemetryModalOptIn, onTelemetryModalOptOut, showComingSoon, showLoginModal, soundsTabVisible, stageSizeMode, targetIsStage, telemetryModalVisible, tipsLibraryVisible, vm, loginState, userData, ..ponentProps } = omit(props, ' dispatch ');在GUIComponent的Menubar中使用它:
<MenuBar accountNavOpen={accountNavOpen} authorId={authorId} authorThumbnailUrl={authorThumbnailUrl} authorUsername={authorUsername} canCreateCopy={canCreateCopy} canCreateNew={canCreateNew} canEditTitle={canEditTitle} canRemix={canRemix} canSave={canSave} canShare={canShare} className={styles.menuBarPosition} enableCommunity={enableCommunity} isShared={isShared} loginState={loginState} renderLogin={renderLogin} showComingSoon={showComingSoon} userData={userData} on
clickAccountNav={on clickAccountNav} on clickLogo={on clickLogo} onCloseAccountNav={onCloseAccountNav} onLogOut={onLogOut} onOpenRegistration={onOpenRegistration} onProjectTelemetryEvent={onProjectTelemetryEvent} onSeeCommunity={onSeeCommunity} onShare={onShare} onToggleLoginOpen={onToggleLoginOpen} onUpdateProjectTitle={onUpdateProjectTitle} />再为components/menu-bar/menu-bar.jsx中定义的MenuBar组件添加属性userData: userData: PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, account: PropTypes.string.isRequired, avatar: PropTypes.string.isRequired }),最后在之前根据loginState的值展示头像和姓名的地方,将默认头像和名字scratch-cat,替换成userData中的avatar和name:
好,现在我们来整理运行看看效果,输入账号密码,提交登录成功,登录框消失,出现了登录用户的名字和头像(这里我使用了scratch-gui提供的小猫作为没有设置头像的用户的默认头像):
整个登录的流程就按计划打通了。
我们最后再来完善一下登录的错误处理。
如果用户登录失败,登录窗不消失,在登录窗显示登录失败的错误信息。
在前面我们已经定义了登录失败的action:
const getUserError = () => ({ type: ERROR_USER_LOGIN});现在需要在reducer中处理它。
先修改`loginFromServer`的代码,增加后台请求错误的处理:
const loginFromServer = (dispatch, account, password) => { client.query({ query: LOGIN, variables: { account: account, password: password } }, ).then(({data}) => { console.log(`login success${data}`); dispatch(getUserSuccess(data.login)); dispatch(closeLoginModal()); }) .catch(error => { console.log(`login error${error}`); if (error.graphQLErrors.length > 0) { dispatch(getUserError(error.graphQLErrors[0].message)); } else { dispatch(getUserError(error.message)); } });};其中分别处理了graphql的错误,这种错误是业务层面的,比如我们的用户名密码错误这些;另外还有一类更宽泛的错误,如网络出错等。
action `getUserError`现在需要接收错误消息:
const getUserError = data => ({ type: ERROR_USER_LOGIN, data: data});reducer处理错误的情况:
const reducer = function (state, action) { if (typeof state === '
undefined ') state = initialState; switch (action.type) { case DONE_USER_LOGIN: return ob
ject.assign({}, state, { loginState: UserState.LOGINED, token: action.data.token, userData: action.data.user }); case ERROR_USER_LOGIN: return ob ject.assign({}, state, { loginState: UserState.NOT_LOGINED, token: '', userData: null, error: action.data }); default: return state; }};现在我们需要在登录弹窗展示错误信息。 回到我们的LoginModal组件,增加一个展示错误信息的label:
同时我们定义loginError属性和映射它的值到state中的error:
LoginModal.propTypes = { loginError: PropTypes.string.isRequired, onCancel: PropTypes.func.isRequired, on
submit: PropTypes.func.isRequired, title: PropTypes.string.isRequired};const mapStateToProps = state => ({ loginError: state.scratchGui.userState.error});下面我们重新运行程序,输入错误的用户名密码登录,看到出现了错误信息提示: 我们再输入正确的账号密码,成功登录。
好了,我们总算按计划对接完成了整个登录流程。
但是还有一个地方需要我们去完善,为了保证用户刷新页面或者在token过期前重新打开新的页面也能成功获取用户信息,避免不必要的重复登录,我们需要将token存到浏览器本地,并且在页面加载时尝试自动获取用户信息。
我们留到下一章来完成这一过程。
【scratch二次开发教程 第1章】基于scratch 3.0搭建自己的少儿编程平台
写在前面
该系列文章是为具有开发能力的朋友写作的,目的是帮助他们在scratch 3.0的基础上开发一套完整的集scratch 3.0编程工具、用户社区和作品云端存储及、品牌集成于一体的scratch编程平台。如果您不是开发者,但想要拥有自己的教育平台和品牌,也欢迎学习交流和洽谈合作。
所以如果您是想学习scratch少儿编程课程,那请忽略该系列的文章。
为什么要自己开发而不使用官方版本?
这个问题要看我们的做少儿编程教育的需求是怎么样的。
scratch本身提供了离线版本以及官网在线平台供我们使用,这足以满足我们对于编程教学模块的需求。但是对于一些教育培训机构或者学校而言,他们可能需要在此基础上整合学生信息,整合课程体系,对学生的学习情况进行统一管理和在线数据分析,包括支持课程作品的与转发,构建自己的教学品牌并实现线上的传播推广,等等,都需要我们在支持编程教学功能的基础上,引入线上社区、课程管理、综合管理等更丰富的功能,而这些就是二次开发的主要工作。
为什么要选择3.0?
不管是从架构还是从UI上,3.0都比2.0都有了很大的改变。
特别是3.0采用HTML5而不是2.0版本使用的flash,随着H5技术逐渐发展,原本采用flash技术的已经逐渐在向H5转移,相对于flash来讲,H5在页面的装载速度上整体上会更快一些,另外也相对较为省电。更为重要的是,flash对于移动端没什么办法,IOS压根就不支持它。Scratch 3.0是可以在手机和平板上运行的,因为H5需要的是ja
vasc ript的支持,这在现代浏览器上基本都是没问题的,移动端也不例外。 另外Scratch 3.0是向下兼容的,因此原来2.0的项目在3.0上也是可以运行的,但从实际测试的效果来看,有些2.0的项目还是会让3.0崩溃。
所以如果计划采用Scratch的,选择3.0是肯定的,如果已经采用了2.0的版本,我的建议是逐渐迁移到3.0上,不管怎么说,单单移动端可以运行的理由就足够了,更不用说其他的一些新的功能比如声音控制Scratch项目。当然目前来开3.0还有一些功能上不稳定的地方,但在MIT和各位开源工作者的一起努力下,都会逐渐得到解决。
Scratch 3.0为我们开源了哪些东西?
https://github/LLK是scratch官方的git仓库。3.0相关的工程主要包括以下几个:
我们这里主要基于scratch-gui项目进行二次开发,它就是一个开源scratch 3.0编辑器,效果就是https://llk.github.io/scratch-gui/develop/。
在使用它创建作品的基础上,我们希望能够实现用户管理、用户社区、作品在线编辑和云端保存、作品、自定义品牌logo和角色等主要功能。
至于其他的项目,我也还没有开始研究,但是从项目介绍我们可以知道scratch-paint是绘图编辑器,scratch-www是像scratch官网一样的作品社区,scratch-I10n是国际化相关的,而如果想对scratch编辑器本身做更高级的扩展,可以研究scratch-vm项目,它本身也是被包含在scratch-gui项目中了。
下一章我们将开始着手拉取scratch-gui项目的代码到我们本地,搭建环境将它运行起来。
【scratch二次开发教程 第2章】运行scratch-gui项目并了解工程结构 写在前面 该系列文章是为具有开发能力的朋友写作的,目的是帮助他们在scratch 3.0的基础上开发一套完整的集scratch 3.0编程工具、用户社区和作品云端存储及、品牌集成于一体的scratch编程平台。如果您不是开发者,但想要拥有自己的教育平台和品牌,也欢迎学习交流和洽谈合作。 所以如果您是想学习scratch少儿编程课程,那请忽略该系列的文章。 这一章我们开始尝试在本地将scratch-gui项目的最新代码运行起来,并对其项目结构有个初步的认识。 本地搭建运行 我们从官方的gitlab仓库拉取最新的代码: git clone https://github/LLK/scratch-gui.git目前主分支是develop,并且在不断的更新中,注意查看一下自己克隆的分支是否正确。 进入工程,安装依赖: cd scratch-guinpm install如果还没有安装npm,请参考https://npmjs/ 这个过程中可能会碰到错误信息而安装失败,这时我们需要根据具体的错误信息自己去网上寻找解决方案。 安装完成后,启动服务: npm start看到如下信息后: 打开浏览器,访问localhost:8601就会看到scratch 3.0的playground为render-gui完整模式的界面,像官网的一样: 由于scratch-gui也处于开发之中,我们看到的有些功能还没有实现,或者没有开源出来。但是对于编程模块的功能,我们可以自己尝试一下,基本上都是跟官方的一样的。如果我们不打算在编程功能模块做进一步扩展的话,这一块的功能我们可以直接拿来使用,而不需要再进行二次开发了。 工程目录结构 我们先一起来过一遍scratch-gui工程的基本目录结构,以便开始我们的二次开发之旅。 我们在WebStorm中打开项目,看到如下目录结构: 熟悉react-redux框架的朋友应该很快就清楚工程各个目录的作用, src目录是我们真正需要了解和二次开发重点要修改的目录,主要包括: components: 组件目录,主要定义UI组件的结构和样式,负责数据的展示 containers:主要负责对components中部分组件的封装,处理组件的业务逻辑,将组件的数据与redux打通,不涉及样式 css: 全局样式设置文件 examples/extensions: 关于scratch扩展的配置示例 lib: 工具方法目录,供组件调用的一些模块和方法,其中以-hoc.jsx结尾的高阶组件,是为UI组件提供额外的功能方法 playground: 多种模式的使用示例:积木模式(blocks-only)、舞台模式(player)、完整模式(render-gui) reducers: redux的store的初始状态、actions和 reducers的定义 index.js: 作为 library 打包的入口文件 好了,本章我们成功拉取scratch-gui的代码,并在本地成功运行起来,然后简单地分析了它的代码目录结构,下一节课我们将尝试在平台中自定义我们的品牌logo.
【scratch二次开发教程 第3章】在scratch3.0中自定义品牌logo 写在前面 该系列文章是为具有开发能力的朋友写作的,目的是帮助他们在scratch 3.0的基础上开发一套完整的集scratch 3.0编程工具、用户社区和作品云端存储及、品牌集成于一体的scratch编程平台。如果您不是开发者,但想要拥有自己的教育平台和品牌,也欢迎学习交流和洽谈合作。 所以如果您是想学习scratch少儿编程课程,那请忽略该系列的文章。 前言 无论我们是作为线下或线上的少儿编程培训机构或平台提供方,肯定想拥有自己教育品牌,并且将其集成到我们的平台中,以达到品牌宣传的效果。现在比较流行的像网易卡搭那些平台就是在scratch开源平台的基础上,植入自己的品牌logo的。 本节课我们就一起来为我们的平台定制自己的品牌logo到scratch-gui项目中。 开始开发 在上节课我们运行起来的项目中,看到顶部menu菜单的左上角有一个scratch的logo: 我们准备把它换掉,换成我们自己的品牌logo,shala-logo.svg: 顺便介绍一下,这只小白猫是笔者的小主,她叫馬沙拉,沙拉少儿编程也是取自她的名字。另外她还有个好姐妹蓝猫馬沙包,后面有机会好好给大家介绍一下她俩。 前面我们说src中的components是是负责UI组件的样式的,很显然我们的logo的样式的设置应该就在这里。 我们在components中发现有如下两个目录: 看名字就知道它们就是与我们的menu菜单样式相关。 挨个打开看看: 第一个是css样式文件,我们不管。 打开menu.jsx,这里面定义了MenuComponent组件,样式是一个< ul >,很可能是跟menu的下拉菜单相关,不是我们想要的,不管。 再打开第二个目录: 咦!我们看到了一个文件名为scratch-logo.svg,这不就是我们要找的logo文件么?打开看看,确实是它! 好,我们再来项目中搜索一下哪里用到这个logo文件。 嗯!只有两个地方用到了它。 看文件名字也可以猜出,第一个是用在了舞台的头部,第二个是用在了菜单栏里(就是我们前面看到的那里)。 现在我们先将我们自己的logo文件加入项目中,放到与scratch-logo.svg相同的目录中。 然后分别替换stage-header.jsx和menu-bar.jsx中的引用。 重新编译项目,刷新浏览器,看到我们的logo成功被替换了: 这样,我们就完成的logo的自定义。 以后使用我们平台的孩子们、第三方机构或是学校都会看到我们醒目而惹眼的品牌logo啦!通过这样的线上线下传播,相信会大大提升我们的品牌知名度,达到独树一帜的效果!
【scratch二次开发教程 第4章】scratch-gui中集成自定义用户系统(一)
写在前面
该系列文章是为具有开发能力的朋友写作的,目的是帮助他们在scratch 3.0的基础上开发一套完整的集scratch
3.0编程工具、用户社区和作品云端存储及、品牌集成于一体的scratch编程平台。如果您不是开发者,但想要拥有自己的教育平台和品牌,也欢迎学习交流和洽谈合作。
所以如果您是想学习scratch少儿编程课程,那请忽略该系列的文章。
前言
前面我们将scratch-gui工程成功搭建运行起来,并且成功植入了我们的品牌logo,这让我们对整个项目有了初步的认识。
现在我们已经有了scratch编程工具,但是我们还缺少两个主要的后台,用户社区后台和GUI的存储后台。目前Scratch3.0团队没有透露社区后端和GUI的存储部分是否有开源计划,考虑到Scratch2.0的后端并未开源,3.0社区后端开源的可能性也不大。
scratch-www项目提供了用户社区的功能,但是需要通过接口去分析它后台的数据存储的结构,我觉得比较麻烦,不如我们自己来开发一个,集成到我们的编程工具scratch-gui中。
所以接下来我们的工作是自己来提供相关的两个后端平台,并与GUI集成到一起。
引入用户登录状态
我们先一步一步来,先做一个比较简单的用户系统,再一步一步迭代。
这一章,我们先来改造一下前面的scratch-gui,引入用户登录状态的检测。
在进入项目时,检测用户是否登录,如果用户未登录,则在右上角显示登录按钮,否则显示用户头像和姓名等基本信息。
先在reducers目录中创建user-state.js文件,用来记录用户的信息。
添加如下内容:
import keyMirror from 'keymirror';
const UserState = keyMirror({
NOT_LOGINED: null,
LOGINED: null
});
const UserStates = ob
ject.keys(UserState) const initialState = {
error: null,
userData: null,
loginState: UserState.NOT_LOGINED
};
const getIsLogined = loginState => ( loginState === UserState.LOGINED);
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
}
export { reducer as default, initialState as userStateInitialState, UserState, UserStates, getIsLogined};
并将以上初始化信息配置在reducers/gui.js中,作为项目的用户相关的初始化信息。
在reducers/gui.js中引入user-state:
import userStateReducer, {userStateInitialState} from './user-state';
```加入到initialState中:```
const guiInitialState = { alerts: alertsInitialState, assetDrag: assetDragInitialState, blockDrag:
blockDragInitialState, cards: cardsInitialState, colorPicker: colorPickerInitialState, connectionModal:
connectionModalInitialState, customProcedures: customProceduresInitialState, editorTab: editorTabInitialState, mode:
modeInitialState, hoveredTarget: hoveredTargetInitialState, stageSize: stageSizeInitialState, menus: menuInitialState,
micIndicator: micIndicatorInitialState, modals: modalsInitialState, monitors: monitorsInitialState, monitorLayout:
monitorLayoutInitialState, projectChanged: projectChangedInitialState, projectState: projectStateInitialState,
projectTitle: projectTitleInitialState, fontsLoaded: fontsLoadedInitialState, restoreDeletion:
restoreDeletionInitialState, targets: targetsInitialState, timeout: timeoutInitialState, toolbox: toolboxInitialState,
vm: vmInitialState, vmStatus: vmStatusInitialState, userState:
userStateInitialState};```将reducer加入到guiReducer中:```const guiReducer = combineReducers({ alerts: alertsReducer,
assetDrag: assetDragReducer, blockDrag: blockDragReducer, cards: cardsReducer, colorPicker: colorPickerReducer,
connectionModal: connectionModalReducer, customProcedures: customProceduresReducer, editorTab: editorTabReducer, mode:
modeReducer, hoveredTarget: hoveredTargetReducer, stageSize: stageSizeReducer, menus: menuReducer, micIndicator:
micIndicatorReducer, modals: modalReducer, monitors: monitorReducer, monitorLayout: monitorLayoutReducer,
projectChanged: projectChangedReducer, projectState: projectStateReducer, projectTitle: projectTitleReducer,
fontsLoaded: fontsLoadedReducer, restoreDeletion: restoreDeletionReducer, targets: targetReducer, timeout:
timeoutReducer, toolbox: toolboxReducer, vm: vmReducer, vmStatus: vmStatusReducer, userState: userStateReducer});
下面去container/gui.jsx中为里面定义的GUI Component添加loginState这个props,用来标识用户是否登录:
GUI.propTypes = { assetHost: PropTypes.string, children: PropTypes.node, cloudHost: PropTypes.string, error:
PropTypes.oneOfType([PropTypes.ob
ject, PropTypes.string]), fetchingProject: PropTypes.bool, intl: intlShape, isError: PropTypes.bool, isLoading: PropTypes.bool, isScratchDesktop: PropTypes.bool, isShowingProject: PropTypes.bool,
loadingStateVisible: PropTypes.bool, onProjectLoaded: PropTypes.func, onSeeCommunity: PropTypes.func, onStorageInit:
PropTypes.func, onUpdateProjectId: PropTypes.func, onUpdateProjectTitle: PropTypes.func, onUpdateReduxProjectTitle:
PropTypes.func, onVmInit: PropTypes.func, projectHost: PropTypes.string, projectId:
PropTypes.oneOfType([PropTypes.string, PropTypes.number]), projectTitle: PropTypes.string, telemetryModalVisible:
PropTypes.bool, vm: PropTypes.instanceOf(VM).isRequired, loginState: PropTypes.bool};
这个`loginState` props的状态值来自于user-state.js中getIsLogined中检测当前的loginState(指state中的)是否等于UserState.LOGINED:
const mapStateToProps = state => { const loadingState = state.scratchGui.projectState.loadingState; const loginState =
state.scratchGui.userState.loginState; return { activeTabIndex: state.scratchGui.editorTab.activeTabIndex,
alertsVisible: state.scratchGui.alerts.visible, backdropLibraryVisible: state.scratchGui.modals.backdropLibrary,
blocksTabVisible: state.scratchGui.editorTab.activeTabIndex === BLOCKS_TAB_INDEX, cardsVisible:
state.scratchGui.cards.visible, connectionModalVisible: state.scratchGui.modals.connectionModal, costumeLibraryVisible:
state.scratchGui.modals.costumeLibrary, costumesTabVisible: state.scratchGui.editorTab.activeTabIndex ===
COSTUMES_TAB_INDEX, error: state.scratchGui.projectState.error, isError: getIsError(loadingState), isFullScreen:
state.scratchGui.mode.isFullScreen, isPlayerOnly: state.scratchGui.mode.isPlayerOnly, isRtl: state.locales.isRtl,
isShowingProject: getIsShowingProject(loadingState), loadingStateVisible: state.scratchGui.modals.loadingProject,
projectId: state.scratchGui.projectState.projectId, soundsTabVisible: state.scratchGui.editorTab.activeTabIndex ===
SOUNDS_TAB_INDEX, targetIsStage: ( state.scratchGui.targets.stage && state.scratchGui.targets.stage.id ===
state.scratchGui.targets.editingTarget ), telemetryModalVisible: state.scratchGui.modals.telemetryModal,
tipsLibraryVisible: state.scratchGui.modals.tipsLibrary, vm: state.scratchGui.vm, loginState: getIsLogined(loginState)
};};
现在container/gui.jsx中定义的Component GUI有了loginState属性了,我们要把它传到menu-bar中,因为我们要在menu-bar中去控制右上角的显示状态。
在这个GUI Component中使用了components/gui/gui.jsx定义的GUIComponent这一component,GUIComponent定义的整个项目的基本样式结构中,可以找到对MenuBar的使用。
首先,在GUIComponent的定义中引入之前定义的`loginState`:
const GUIComponent = props => { const { accountNavOpen, activeTabIndex, alertsVisible, authorId, authorThumbnailUrl,
authorUsername, ba
sePath, backdropLibraryVisible, backpackHost, backpackVisible, blocksTabVisible, cardsVisible, canCreateNew, canEditTitle, canRemix, canSave, canCreateCopy, canShare, canUseCloud, children, connectionModalVisible,
costumeLibraryVisible, costumesTabVisible, enableCommunity, intl, isCreating, isFullScreen, isPlayerOnly, isRtl,
isShared, loading, renderLogin, on
clickAccountNav, onCloseAccountNav, onLogOut, onOpenRegistration, onToggleLoginOpen, onUpdateProjectTitle, on
activateCostumesTab, on activateSoundsTab, on activateTab, on clickLogo, onExtensionButton click, onProjectTelemetryEvent, onRequestCloseBackdropLibrary, onRequestCloseCostumeLibrary, onRequestCloseTelemetryModal,
onSeeCommunity, onShare, onTelemetryModalCancel, onTelemetryModalOptIn, onTelemetryModalOptOut, showComingSoon,
soundsTabVisible, stageSizeMode, targetIsStage, telemetryModalVisible, tipsLibraryVisible, vm, loginState,
..ponentProps } = omit(props, 'dispatch');
...再在使用MenuBar的地方也为MenuBar定义`loginState`属性,它的值就是GUIComponent传进来的`loginState`的值:
accountNavOpen={accountNavOpen}
authorId={authorId}
authorThumbnailUrl={authorThumbnailUrl}
authorUsername={authorUsername}
canCreateCopy={canCreateCopy}
canCreateNew={canCreateNew}
canEditTitle={canEditTitle}
canRemix={canRemix}
canSave={canSave}
canShare={canShare}
className={styles.menuBarPosition}
enableCommunity={enableCommunity}
isShared={isShared}
renderLogin={renderLogin}
showComingSoon={showComingSoon}
on
clickAccountNav={on clickAccountNav} on
clickLogo={on clickLogo} onCloseAccountNav={onCloseAccountNav}
onLogOut={onLogOut}
onOpenRegistration={onOpenRegistration}
onProjectTelemetryEvent={onProjectTelemetryEvent}
onSeeCommunity={onSeeCommunity}
onShare={onShare}
onToggleLoginOpen={onToggleLoginOpen}
onUpdateProjectTitle={onUpdateProjectTitle}
loginState={loginState}
/>
最后修改components/menu-bar.jsx中的MenuBar组件的显示,将右上角替换成:
{this.props.loginState ? ({'scratch-cat'}) :Login}
如果用户已登录,就显示头像和姓名的样式(具体的用户信息需要跟后台打通,我们后面再实现):
否则显示登录按钮:
我们可以通过修改reducers/user-state.js中的loginState的初始值来查看效果:
loginState: UserState.NOT_LOGINED
loginState: UserState.LOGINED这个值我们会在后面根据用户登录的token去获取。
为了与项目整体风格一致,我们修改这个登录按钮的样式,在menu-bar目录中添加login-button.css和login-button.jsx文件,内容分别如下:
@i
mport ".._.._css/colors.css";.login-button { background: $data-primary;} import classNames from 'classnames';import {FormattedMessage} from 'react-intl';import PropTypes from
'prop-types';import React from 'react';import Button from '.._button/button.jsx';import styles from
'./login-button.css';const LoginButton = ({ className, on
click}) => ( styles.loginButton )} on
click={on click}>
ription="Label for login" id="gui.menuBar.login" /> );LoginButton.propTypes = { className: PropTypes.string, on
click: PropTypes.func};LoginButton.defaultProps = { on
click: () => {}};export default LoginButton;然后在menu-bar.jsx中如下使用: 这样看起来就好看多了:
好了,这里接入完成后,我们下一步就可以实现一个后台系统,然后对接后台系统登录和获取用户信息了。
我们下一章继续。
写在前面
该系列文章是为具有开发能力的朋友写作的,目的是帮助他们在scratch 3.0的基础上开发一套完整的集scratch 3.0编程工具、用户社区和作品云端存储及分享、品牌集成于一体的scratch编程平台。如果您不是开发者,但想要拥有自己的教育平台和品牌,也欢迎学习交流和洽谈合作。
所以如果您是想学习scratch少儿编程课程,那请忽略该系列的文章。
前言
前面我们把登录的基本流程实现完成了,不过还留了一个小尾巴,就是用户登出,清除用户的token信息,以确保用户账号的安全性。
我们准备在用户姓名下面做一个下拉菜单,将登出操作放在里面,以后还会在里面放入个人中心,个人作品等菜单项,就像现在mit官网那样:
下面就来实现这一过程。
开始实现
先来实现组件的样式。
找到components/menu-bar/menu-bar.jsx,在之前我们修改的显示头像和姓名的下面,添加一个MenuBarMenu组件,如下:
我们将其open属性先设置为true,让它一直显示,以方便我们调试样式。
编译运行,看到在用户姓名和头像下面正常显示了登出菜单:
下一步来控制菜单的显示与隐藏。
在components/menu-bar/menu-bar.jsx中,为组件MenuBar增加props:userMenuOpen, on
clickUser和onRequestCloseUser,分别表示用户判断下拉菜单是否打开,打开下拉菜单操作和关闭下拉菜单操作: userMenuOpen: PropTypes.bool,onRequestCloseUser: PropTypes.func,on
clickUser: PropTypes.func, 在reducers/menus.js中,增加userMenu状态:
const MENU_USER = 'userMenu';
初始为false关闭状态:
const initialState = {[MENU_ACCOUNT]: false,[MENU_FILE]: false,[MENU_EDIT]: false,[MENU_LANGUAGE]: false,[MENU_LOGIN]: false,[MENU_USER]: false};
增加打开和关闭以及状态检测的actions:
const openUserMenu = () => openMenu(MENU_USER);const closeUserMenu = () => closeMenu(MENU_USER);const userMenuOpen = state => state.scratchGui.menus[MENU_USER];
记得export以上actions:
回到components/menu-bar/menu-bar.jsx中,映射刚才的userMenuOpen props和state:
映射on
clickUser:
映射onRequestCloseUser:
再完善之前显示MenuBar的控制,现在我们根据之前设置的props来控制菜单的显示和关闭:
重新运行,登录成功后并没有菜单显示,点击用户信息,出现下拉菜单,再点击,菜单关闭,一切都按照我们的设计进行的。
最后一步,我们来实现登出的过程。
先到reducers/user-state.js中增加登出的功能实现。
登出actions:
const DONE_USER_LOGOUT = 'DONE_USER_LOGOUT'; // 标识登出过程成功完成
const logoutSuccess = () => ({type: DONE_USER_LOGOUT});登出处理方法,先清空本地token,再关闭下拉菜单,清空state中的用户信息:
const logout = dispatch => {localStorage.removeItem('shala_token');dispatch(closeUserMenu());dispatch(logoutSuccess());};
reducer里更新state:
export logout方法:
回到components/menu-bar/menu-bar.jsx中,为MenuBar组件增加props on
clickLogout: on
clickLogout: PropTypes.func, 并映射dispatch:
on
clickLogout: () => logout(dispatch) 最后在MenuItem的on
click方法中调用它:
运行项目成功登录,点击下拉菜单,登出,用户信息消失并出现登录按钮。打开浏览器开发工具,看到localStorage中存储的token也被清空了。说明成功登出了。
这里登出时我们没有向后台发送请求,让后台也做相关的登出操作,目前来看并没有什么大问题,只要保证客户端没有有效的token存在就行,如果后面有更高的安全需求考虑我们再来进一步完善它。
另外,在登出时,我们可能正在进行scratch项目创作,并且还没有保存,这时需要提示用户进行相关的操作,并且退出后清空用户的工作区内容。
以上工作我们在后面将作品云端保存和加载功能实现后再来一起完善它。
所以下一章起,我们计划开始集成作品管理系统,实现用户scratch作品集的云端保存和载入以及相关管理功能。对于用户系统,其实至少还需要一个管理的页面,我们计划留到作品管理系统集成完后再来完成,因为到那时我们的系统的基本框架就差不多了,围绕基本框架来丰富功能模块就会清晰很多。