diff --git a/README.md b/README.md index 0e91ec7..e92f1a1 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ __*!!!警告!!!*__ -请在主仓库提交代码,而非镜像仓库!在镜像仓库提交的代码将会在同步时被覆盖! +*请在主仓库提交代码,而非镜像仓库!在镜像仓库提交的代码将会在同步时被覆盖!* 主仓库: http://git.mrkbear.com/MrKBear/mini-dlpu-v3 @@ -15,8 +15,9 @@ __*!!!警告!!!*__ - [社区介绍](#小程序社区) - [项目设计](#第三代小程序) - [贡献规范](#社区贡献规范) +- [社区福利](#贡献者分配制度) - [API文档](https://docs.apipost.cn/preview/e737de418d4ef150/419d45d8c97d6a9f) -- [入门文档(等待撰写)](#第三代掌上教务处小程序) +- [入门文档(等待撰写)](#快速入门) - [设计架构(等待撰写)](#第三代掌上教务处小程序) ## 小程序社区 @@ -98,6 +99,65 @@ __*!!!警告!!!*__ 5. 一个文件不要超过 1000 行代码,尽量保证代码可读性 +## 贡献者分配制度 + +作为公益的开源项目: + +第三代开发时,将计划加入一个赞助功能,每个月赞助累计到达一定数额,将在下个月去除开屏广告。 + +第三代上线后,我们将在每个月公示小程序的账目流水,去除服务器成本和其他费用(微信认证,微信支付,域名,...)后,若有剩余数额将按照开发时大家的贡献分配。 + +小程序的广告位将计划外包给其他组织管理,我们也会得到一定收入,此收入也将按上面的规则处理。 + +小程序开屏广告收入也同样按上面的规则处理。 + +以上内容请大家仔细阅读,另外有意向负责项目财务的同学,处理财务账目也算做贡献。 + +## 快速入门 + +下面对大家的小问题进行解答: + +> 我在参与贡献之前,我需要先会哪些知识?他们好学吗? + +下面列出此项目使用的全部技术,从上到下是推荐学习顺序和学习重点,也是难度顺序: +- HTML + - 标签结构和语法 +- CSS + - 基础样式 + - 选择器 + - 盒模型 + - 布局和定位 + - 行内元素和块级元素 +- JS + - 数据类型 + - 基础运算符 + - 流程控制语句 + - 函数与闭包 (瓶颈) + - 原型和对象 (突破) +- Vue (不用深入了解) + - 组件化设计思想 +- 小程序 API + - 了解小程序如何编写页面 + - 了解小程序大概的 API + - 不用深入了解,随时查阅 +- TypeScript (只要JS数据结构玩的6,TS五分钟学会) + - 类型约束 + - 接口 + - 泛型 + - 类型运算 +- Sass (拓展了CSS语法,实际上没有任何新知识) + - 语法 + +> 小程序和主流前端技术差别在哪?对我以后职业发展帮助大嘛? + +如果你已经掌握了前端主流技术,例如 Vue,React,那么上手小程序只是 __1__ 天的事情 + +换句话来说小程序开发用到技术和主流前端技术,有大概 __90%__ 是重叠的。 + +小程序学了可以成为加分项,参与贡献拥有 __20000__ 人的项目,丰富项目经历,稳赚不亏。 + +掌上教务处前端项目采用了很多创新的架构设计,虽然不一定优秀,但是一定是值得学习的。 + ## 贡献者 @MrKBear (熊鲜森) \ No newline at end of file diff --git a/miniprogram/api/Login.ts b/miniprogram/api/Login.ts new file mode 100644 index 0000000..47b3caf --- /dev/null +++ b/miniprogram/api/Login.ts @@ -0,0 +1,149 @@ +import { API, HTTPMethod, IParamSetting, GeneralCallbackResult } from "../core/Api"; + +interface ILoginInput { + + /** + * 学号 + */ + studentId: string; + + /** + * 教务处密码 + */ + password: string; +} + +interface ILoginOutput { + + /** + * 身份证后六位 + * 用于尝试水卡登录 + */ + idCardLast6: string; + + /** + * 使用的教务处链接 + */ + eduService: string; + + /** + * 用户的真实姓名 + */ + actualName: string; + + /** + * 用户是否关注了公共号 + */ + isSubscribeWxAccount: boolean; + + /** + * 教务处的 session + */ + eduSession: string; +} + +interface ILoginEvent { + + /** + * session 过期 + */ + expire: GeneralCallbackResult; + + /** + * 登录失败 + */ + unauthorized: GeneralCallbackResult; + + /** + * 未知的问题 + */ + error: GeneralCallbackResult; + + /** + * 数据损坏或丢失 + */ + badData: GeneralCallbackResult; +} + +/** + * Login API + * 此 API 用来向教务处发起登录请求 + * 请求成功后将获得教务处返回的 session + */ +class Login extends API { + + public override url: string = "/login"; + + public override method: HTTPMethod = HTTPMethod.POST; + + public override params: IParamSetting = { + + studentId: { + mapKey: "id", + tester: /^\d{8,12}$/, + }, + + password: { + mapKey: "pwd" + } + }; + + public constructor() { + super(); + this.initDebugLabel("Login"); + + this.addFailedCallBack(); + + this.on("success", (data) => { + + let isSuccess = true; + let errMsg = ""; + let res: ILoginOutput | undefined; + const info: any = data.data; + + // 数据缺失检测 + if(!info) { + isSuccess = false; + errMsg = "Bad Data"; + this.emit("badData", { errMsg }); + } + + if (isSuccess) switch (info.code) { + case (1): + res = { + idCardLast6: info.data.idCard, + eduService: info.data.ip, + actualName: info.data.name, + isSubscribeWxAccount: info.data.official, + eduSession: info.data.session + }; + errMsg = info.err_msg ?? "Success"; + this.emit("ok", res); + break; + + case (2): + isSuccess = false; + errMsg = info.err_msg ?? "Session Expire"; + this.emit("expire", { errMsg }); + break; + + case (3): + isSuccess = false; + errMsg = info.err_msg ?? "Unauthorized"; + this.emit("unauthorized", { errMsg }); + break; + + case (4): + isSuccess = false; + errMsg = info.err_msg ?? "Error"; + this.emit("error", { errMsg }); + break; + } + + if (!isSuccess) this.emit("no", { errMsg }); + this.emit("done", { errMsg, data: res }); + }); + } +} + +export { Login, ILoginInput, ILoginOutput }; \ No newline at end of file diff --git a/miniprogram/core/Api.ts b/miniprogram/core/Api.ts index d0027ef..21dda78 100644 --- a/miniprogram/core/Api.ts +++ b/miniprogram/core/Api.ts @@ -1,4 +1,5 @@ -import { Emitter } from "./Emitter"; +import { Emitter, EventType } from "./Emitter"; +import { API_FAILED_SHOW_MESSAGE } from "./Config"; import { Logger, LogLabel, LevelLogLabel, colorRadio, StatusLabel } from "./Logger"; interface IAppAPIParam { api: { @@ -31,6 +32,11 @@ type DeepReadonly = { */ type IParamSetting = { [P in keyof T]: { + + /** + * 键值映射 + */ + mapKey?: string, /** * 默认值 @@ -116,16 +122,57 @@ type IAPIEvent = { } /** - * 接口调用 + * 输出事件类型 */ -class API extends Emitter> { +type IAPIResultEvent = { + + /** + * 成功获取数据 + */ + ok: O, + + /** + * 无论因为什么 + * 总之数据获取到 + */ + no: GeneralCallbackResult & U, + + /** + * 完成了 + * 无论失败与否 + */ + done: { data?: O } & GeneralCallbackResult & U +} + +/** + * API 接口调用 + * @template I API 输入数据 + * @template O API 输出数据 + * @template E API 中的事件 + * @template U 用户自定义的输出数据 + */ +class API< + I extends IAnyData = IAnyData, + O extends IAnyData = IAnyData, + E extends Record = Record, + U extends IAnyData = IAnyData +> extends Emitter < + { + // 这个复杂的泛型是为了 MixIn 用户自定义事件类型 + // 懂得如何使用就可以了 + // 不要试图去理解下面这三行代码,真正的恶魔在等着你 + [P in (keyof (IAPIEvent & IAPIResultEvent) | keyof E)] : + P extends keyof IAPIEvent ? IAPIEvent[P] : + P extends keyof IAPIResultEvent ? IAPIResultEvent[P] : E[P] + } +> { /** * 基础 URL * TODO: 这里可能涉及负载均衡 */ public static get baseUrl():string { - return "https://blog.mrkbear.com"; + return "https://jwc.nogg.cn"; } public static defaultLogLabel:LogLabel = new LogLabel( @@ -301,14 +348,15 @@ class API extends Emitter extends Emitter { - if (api === this) return; + if ((api as API | this) === this) return; if (!api.requestData) return; if (api.requestData!.url !== this.requestData!.url) return; if (api.requestData!.method !== this.requestData!.method) return; @@ -471,9 +519,9 @@ class API extends Emitter>; - public wait(callBack?: ICallBack): this; - public wait(callBack?: ICallBack): Promise> | this { + public waitRequest(): Promise>; + public waitRequest(callBack: ICallBack): this; + public waitRequest(callBack?: ICallBack): Promise> | this { // 存在 callback 使用传统回调 if (callBack) { @@ -491,6 +539,96 @@ class API extends Emitter["done"]>; + public wait(callBack: IResCallBack): this; + public wait(callBack?: IResCallBack): Promise["done"]> | this { + + // 存在 callback 使用传统回调 + if (callBack) { + callBack.ok && this.on("ok", callBack.ok); + callBack.no && this.on("no", callBack.no); + callBack.done && this.on("done", callBack.done); + return this; + } + + // 不存在 callback 使用 Promise 对象 + else { + return new Promise((r) => { + this.on("done", r); + }); + } + } + + /** + * 请求加载时显示加载动画 + * @param message 消息 可以为函数 + * @param mask 使用蒙版阻止点击穿透 + */ + public showLoading(message: string | ((data?: Partial) => string), mask: boolean = false): this { + + // 获取标题 + let title: string = message instanceof Function ? message(this.data) : message; + + this.on("request", () => { + wx.showLoading({ + title, mask + }) + }); + + this.on("complete", () => { + wx.hideLoading(); + }); + + return this; + }; + + /** + * 显示导航栏加载动画 + */ + public showNavigationBarLoading(): this { + + this.on("request", () => { + wx.showNavigationBarLoading() + }); + + this.on("complete", () => { + wx.hideNavigationBarLoading(); + }); + + return this; + } + + /** + * 自动挂载失败回调 + */ + public addFailedCallBack(): this { + this.on("fail", (e) => { + this.emit("no", e as any); + this.emit("done", e as any); + }); + return this; + } + + /** + * 请求失败后的提示语句 + */ + public showFailed(): this { + + // 生成随机索引值 + let randomIndex = Math.floor( + Math.random() * API_FAILED_SHOW_MESSAGE.length + ); + + this.on("fail", () => { + wx.showToast({ + title: API_FAILED_SHOW_MESSAGE[randomIndex], + icon: "none" + }); + }); + + return this; + }; } /** @@ -507,6 +645,12 @@ interface ICallBack { complete?: (data: IAPIEvent<{}, O>["complete"]) => any; } +interface IResCallBack { + ok?: (data: IAPIResultEvent["ok"]) => any; + no?: (data: IAPIResultEvent["no"]) => any; + done?: (data: IAPIResultEvent["done"]) => any; +} + /** * Request 请求策略 * 此策略用于节流 @@ -553,4 +697,4 @@ enum HTTPMethod { } export default API; -export { API, IParamSetting, IAppAPIParam, ICallBack, HTTPMethod, RequestPolicy } \ No newline at end of file +export { API, IParamSetting, IAppAPIParam, ICallBack, HTTPMethod, RequestPolicy, GeneralCallbackResult } \ No newline at end of file diff --git a/miniprogram/core/Config.ts b/miniprogram/core/Config.ts index 1a9dca9..6a968e8 100644 --- a/miniprogram/core/Config.ts +++ b/miniprogram/core/Config.ts @@ -36,3 +36,14 @@ export const LOGGER_FILTER:Array[] = [ // 输出警告和错误 // ["WARN", "ERROR", "FATAL"], ]; + +/** + * 请求失败的提示用语 + * 请求失败时如果选择自动显示消息 + * 则会从以下内容中选择 + */ +export const API_FAILED_SHOW_MESSAGE: string[] = [ + "失败啦(ó_ò。)", "服务器睡着了", "数据移民火星了", + "数据在半路走丢了", "服务器打了个瞌睡", "服务器被玩坏了", + "服务器在扶老奶奶过马路", "服务器累了", "服务器在拯救世界" +] \ No newline at end of file diff --git a/miniprogram/pages/Account/Account.scss b/miniprogram/pages/Account/Account.scss index 1557d1d..685a369 100644 --- a/miniprogram/pages/Account/Account.scss +++ b/miniprogram/pages/Account/Account.scss @@ -1,39 +1,3 @@ -@import "../../app.scss"; - -view.container { - - // 用户卡片 - view.user-card { - margin-top: 20px; - height: 100px; - display: flex; - padding-top: 20px; - padding-bottom: 20px; - - view.avatar { - width: 80px; - height: 80px; - border-radius: 1000px; - overflow: hidden; - } - - view.info { - width: calc(100% - 80px - 20px); - padding-left: 20px; - - view.nick { - margin: 4px 0; - } - - view.student { - margin: 1px 0; - } - } - } -} - -@media (prefers-color-scheme: dark) { - view.container view.user-card view.avatar{ - filter: brightness(.8); - } -} \ No newline at end of file +@import "./UserCard.scss"; +@import "./MainFunction.scss"; +@import "./FunctionList.scss"; \ No newline at end of file diff --git a/miniprogram/pages/Account/Account.ts b/miniprogram/pages/Account/Account.ts index 5c5896d..2cf08a9 100644 --- a/miniprogram/pages/Account/Account.ts +++ b/miniprogram/pages/Account/Account.ts @@ -1,66 +1,10 @@ -// pages/Account/Account.ts -Page({ +import { Manager } from "../../core/Module"; +import { UserCard } from "./UserCard"; +import { MainFunction } from "./MainFunction"; +import { FunctionList } from "./FunctionList"; - /** - * 页面的初始数据 - */ - data: { - - }, - - /** - * 生命周期函数--监听页面加载 - */ - onLoad() { - - }, - - /** - * 生命周期函数--监听页面初次渲染完成 - */ - onReady() { - - }, - - /** - * 生命周期函数--监听页面显示 - */ - onShow() { - - }, - - /** - * 生命周期函数--监听页面隐藏 - */ - onHide() { - - }, - - /** - * 生命周期函数--监听页面卸载 - */ - onUnload() { - - }, - - /** - * 页面相关事件处理函数--监听用户下拉动作 - */ - onPullDownRefresh() { - - }, - - /** - * 页面上拉触底事件的处理函数 - */ - onReachBottom() { - - }, - - /** - * 用户点击右上角分享 - */ - onShareAppMessage() { - - } -}) \ No newline at end of file +Manager.Page((manager) => { + manager.addModule(UserCard, "userCard"); + manager.addModule(MainFunction, "mainFunction"); + manager.addModule(FunctionList, "functionList"); +}); \ No newline at end of file diff --git a/miniprogram/pages/Account/FunctionList.scss b/miniprogram/pages/Account/FunctionList.scss new file mode 100644 index 0000000..c582eaa --- /dev/null +++ b/miniprogram/pages/Account/FunctionList.scss @@ -0,0 +1 @@ +@import "../../app.scss"; \ No newline at end of file diff --git a/miniprogram/pages/Account/FunctionList.ts b/miniprogram/pages/Account/FunctionList.ts new file mode 100644 index 0000000..72909dd --- /dev/null +++ b/miniprogram/pages/Account/FunctionList.ts @@ -0,0 +1,11 @@ +import { Modular, Manager } from "../../core/Module"; + +class FunctionList extends Modular { + + public override onLoad() { + // Do something + } +} + +export { FunctionList }; +export default FunctionList; \ No newline at end of file diff --git a/miniprogram/pages/Account/MainFunction.scss b/miniprogram/pages/Account/MainFunction.scss new file mode 100644 index 0000000..c582eaa --- /dev/null +++ b/miniprogram/pages/Account/MainFunction.scss @@ -0,0 +1 @@ +@import "../../app.scss"; \ No newline at end of file diff --git a/miniprogram/pages/Account/MainFunction.ts b/miniprogram/pages/Account/MainFunction.ts new file mode 100644 index 0000000..9b69d4c --- /dev/null +++ b/miniprogram/pages/Account/MainFunction.ts @@ -0,0 +1,11 @@ +import { Modular, Manager } from "../../core/Module"; + +class MainFunction extends Modular { + + public override onLoad() { + // Do something + } +} + +export { MainFunction }; +export default MainFunction; \ No newline at end of file diff --git a/miniprogram/pages/Account/UserCard.scss b/miniprogram/pages/Account/UserCard.scss new file mode 100644 index 0000000..81bff8e --- /dev/null +++ b/miniprogram/pages/Account/UserCard.scss @@ -0,0 +1,36 @@ +@import "../../app.scss"; + +// 用户卡片 +view.user-card { + margin-top: 20px; + height: 100px; + display: flex; + padding-top: 20px; + padding-bottom: 20px; + + view.avatar { + width: 80px; + height: 80px; + border-radius: 1000px; + overflow: hidden; + } + + view.info { + width: calc(100% - 80px - 20px); + padding-left: 20px; + + view.nick { + margin: 4px 0; + } + + view.student { + margin: 1px 0; + } + } +} + +@media (prefers-color-scheme: dark) { + view.container view.user-card view.avatar{ + filter: brightness(.8); + } +} \ No newline at end of file diff --git a/miniprogram/pages/Account/UserCard.ts b/miniprogram/pages/Account/UserCard.ts new file mode 100644 index 0000000..7fe07b0 --- /dev/null +++ b/miniprogram/pages/Account/UserCard.ts @@ -0,0 +1,11 @@ +import { Modular, Manager } from "../../core/Module"; + +class UserCard extends Modular { + + public override onLoad() { + // Do something + } +} + +export { UserCard }; +export default UserCard; \ No newline at end of file diff --git a/miniprogram/pages/Timetable/TestCore.ts b/miniprogram/pages/Timetable/TestCore.ts index cc9c7cd..6a81997 100644 --- a/miniprogram/pages/Timetable/TestCore.ts +++ b/miniprogram/pages/Timetable/TestCore.ts @@ -1,5 +1,5 @@ import { Modular, Manager, ILifetime } from "../../core/Module"; -import { API, IParamSetting } from "../../core/Api"; +import { Login } from "../../api/Login"; import { Storage } from "../../core/Storage"; /** @@ -27,44 +27,13 @@ implements Partial { setTimeout(() => { s.set("be", 12); }, 1000) - - interface ITestApiInput { - name: string, - id: number, - info: { - data: string - } - } - class TestApi extends API { - - public override params: IParamSetting = { - name: { - tester: "123", - isHeader: true - }, - id: { - parse: (i) => ++i, - }, - info: {} - } - - public constructor() { - super(); - this.initDebugLabel("TestApi"); - } - } - - let api = new TestApi(); - api.param({ - name: "123", - id: 456, - info: { - data: "abc" - } - }).request().wait({ - success: (d) => console.log(d) - }) + new Login().param({studentId: "1806240113", password: "qazxsw123"}) + .request().wait({ + ok: (w) => {console.log("ok", w)}, + no: (w) => {console.log("no", w)}, + done: (w) => {console.log("done", w)} + }); } }