@tmsfe/tms-core 0.0.82 → 0.0.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmsfe/tms-core",
3
- "version": "0.0.82",
3
+ "version": "0.0.85",
4
4
  "description": "tms运行时框架",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,19 +18,19 @@
18
18
  ]
19
19
  },
20
20
  "devDependencies": {
21
- "@babel/core": "^7.15.0",
21
+ "@babel/core": "^7.17.9",
22
22
  "@babel/plugin-proposal-class-properties": "^7.14.5",
23
23
  "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.2",
24
- "@babel/plugin-proposal-optional-chaining": "^7.14.2",
24
+ "@babel/plugin-proposal-optional-chaining": "^7.14.5",
25
25
  "@rollup/plugin-babel": "^5.0.2",
26
26
  "@rollup/plugin-commonjs": "^19.0.0",
27
27
  "@rollup/plugin-dynamic-import-vars": "^1.1.1",
28
28
  "@rollup/plugin-json": "^4.0.3",
29
- "@typescript-eslint/eslint-plugin": "^4.29.3",
30
- "@typescript-eslint/parser": "^4.29.3",
29
+ "@typescript-eslint/eslint-plugin": "^5.2.0",
30
+ "@typescript-eslint/parser": "^5.2.0",
31
31
  "babel-core": "^6.26.3",
32
32
  "babel-preset-env": "^1.7.0",
33
- "miniprogram-api-typings": "^3.4.4",
33
+ "miniprogram-api-typings": "latest",
34
34
  "rimraf": "^3.0.2",
35
35
  "rollup": "^2.6.1",
36
36
  "rollup-plugin-node-resolve": "^5.2.0",
@@ -0,0 +1,96 @@
1
+ // @copyright 2020-present, Tencent, Inc. All rights reserved.
2
+ // @author Fenggang.Sun <fenggangsun@tencent.com>
3
+ // @file 微信授权工具方法
4
+
5
+ // 微信权限列表
6
+ // 此处列出当前小程序中有使用的权限,全量scope请参考:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/authorize.html
7
+ const scope = {
8
+ writeAlbum: 'scope.writePhotosAlbum', // 添加到相册
9
+ };
10
+
11
+ const scopeName = {
12
+ [scope.writeAlbum]: '保存到相册',
13
+ };
14
+
15
+ /**
16
+ * 获取当前授权状态
17
+ * @param {String} authScope 权限scope
18
+ * @returns {Promise<Object>} 成功获取授权状态时,Promise被resolved,Object.authed字段表示授权状态
19
+ * 授权授权状态出现异常时,Promise被rejected
20
+ * Object.authed: true-已授权,false-拒绝授权,undefined-未发起过授权动作
21
+ */
22
+ const getAuthStatus = authScope => new Promise((resolve, reject) => wx.getSetting({
23
+ success: res => resolve({ authed: res.authSetting[authScope] }),
24
+ fail: reject,
25
+ }));
26
+
27
+ /**
28
+ * 引导用户在设置页面打开授权,获取引导授权结果
29
+ * @param {String} authScope 权限scope
30
+ * @param {Object} guideModal 授权弹窗相关配置
31
+ * @param {String} guideModal.title 标题
32
+ * @param {String} guideModal.content 文案内容
33
+ * @param {Boolean} guideModal.showCancel 是否展示取消按钮
34
+ * @param {String} guideModal.cancelText 取消按钮文案
35
+ * @param {String} guideModal.cancelColor 取消按钮文案颜色
36
+ * @param {String} guideModal.confirmText 确定按钮文案
37
+ * @param {String} guideModal.confirmColor 确定按钮文案颜色
38
+ * @returns {Promise<Object>} 引导授权流程正常执行结束,Promise被resolved,Object.authed字段表示授权状态
39
+ * 引导流程出现异常时,Promise被rejected
40
+ * Object.authed: true-已授权,false-拒绝授权,undefined-未发起过授权动作
41
+ */
42
+ const guideAuth = (authScope, guideModal) => new Promise((resolve, reject) => {
43
+ const {
44
+ title = '', content,
45
+ showCancel = true, cancelText = '取消', cancelColor = '#000000',
46
+ confirmText = '现在去', confirmColor,
47
+ } = guideModal || {};
48
+ const contentText = content || `监测到您没有授权${scopeName[authScope] || '相关'}权限,无法使用该功能,是否去授权?`;
49
+ wx.showModal({
50
+ title, content: contentText,
51
+ showCancel, cancelText, cancelColor,
52
+ confirmText, confirmColor,
53
+ success: (res) => {
54
+ if (!res.confirm) return resolve({ authed: false, confirm: res.confirm }); // 用户拒绝去授权
55
+ wx.openSetting({ // 跳转到授权页面进行授权
56
+ success: ({ authSetting }) => resolve({ authed: authSetting[authScope], confirm: true }), // 确认授权结果
57
+ fail: reject,
58
+ });
59
+ },
60
+ fail: reject,
61
+ });
62
+ });
63
+
64
+ /**
65
+ * 发起授权,获取授权结果
66
+ * 如果是首次授权,拉起底部授权弹窗;授权失败后引导用户进入设置完成授权
67
+ * 如果不是首次授权,直接引导用户进入设置页完成授权
68
+ * @param {String} authScope 权限scope
69
+ * @param {Object} guideModal 授权弹窗相关配置
70
+ * @param {String} guideModal.title 标题
71
+ * @param {String} guideModal.content 文案内容
72
+ * @param {Boolean} guideModal.showCancel 是否展示取消按钮
73
+ * @param {String} guideModal.cancelText 取消按钮文案
74
+ * @param {String} guideModal.cancelColor 取消按钮文案颜色
75
+ * @param {String} guideModal.confirmText 确定按钮文案
76
+ * @param {String} guideModal.confirmColor 确定按钮文案颜色
77
+ * @returns {Promise<Object>} 授权流程正常执行结束,Promise被resolved,Object.authed字段表示授权状态
78
+ * 授权流程出现异常时,Promise被rejected
79
+ * Object.authed: true-已授权,false-拒绝授权,undefined-未发起过授权动作
80
+ */
81
+ const auth = (authScope, guideModal) => new Promise((resolve, reject) => wx.authorize({ // 如果从未授权,此时会拉起一个授权
82
+ scope: authScope,
83
+ success: () => resolve({ authed: true }), // 授权成功
84
+ fail: () => guideAuth(authScope, guideModal) // 授权失败,引导用户在设置页完成授权
85
+ .then(resolve)
86
+ .catch(reject),
87
+ }));
88
+
89
+ const api = {
90
+ scope,
91
+ getAuthStatus,
92
+ guideAuth,
93
+ auth,
94
+ };
95
+
96
+ module.exports = api;
@@ -0,0 +1,289 @@
1
+ class CanvasDrawer {
2
+ constructor(canvasId, container, instance = '', designWindowWidth = 750) {
3
+ this.canvasId = canvasId; // canvasId
4
+ this.container = container; // 海报图容器参数 {bgColor, radius}
5
+ this.compInstance = instance; // 组件中使用需传入组件实例
6
+ this.designWindowWidth = designWindowWidth; // 设计稿尺寸, 默认750
7
+ }
8
+
9
+ init() {
10
+ return new Promise((resolve, reject) => {
11
+ const query = (this.compInstance || wx).createSelectorQuery();
12
+ query.select(this.canvasId)
13
+ .fields({ node: true, size: true })
14
+ .exec((res) => {
15
+ if (!res?.[0]?.node) reject('No canvas element found');
16
+ const canvas = res[0].node;
17
+ this.canvas = canvas;
18
+ this.ctx = canvas.getContext('2d');
19
+ const { pixelRatio: dpr, windowWidth } = wx.getSystemInfoSync();
20
+ canvas.width = res[0].width * dpr;
21
+ canvas.height = res[0].height * dpr;
22
+ this.ratio = windowWidth / this.designWindowWidth;
23
+ this.ctx.scale(dpr, dpr);
24
+ // 海报图宽高自动获取画布宽高
25
+ this.container.width = res[0].width / this.ratio;
26
+ this.container.height = res[0].height / this.ratio;
27
+ // 绘制海报图容器
28
+ this.drawRectContainer(this.container);
29
+ resolve();
30
+ });
31
+ });
32
+ }
33
+
34
+ toPx(rpx) {
35
+ return rpx * this.ratio;
36
+ }
37
+
38
+ /**
39
+ * 绘制矩形容器
40
+ * @param {*} [{ x, y, bgColor, width, height, radius, type }={}] 背景 宽 高 圆角 线/填充
41
+ */
42
+ drawRectContainer({ x = 0, y = 0, bgColor, width, height, radius, type = 'fill' } = {}) {
43
+ this.drawRect(x, y, width, height, radius, bgColor, type);
44
+ this.ctx.clip();
45
+ }
46
+
47
+ // 绘制矩形
48
+ drawRect(x, y, w, h, radius, color, type = 'fill') {
49
+ // eslint-disable-next-line no-param-reassign
50
+ x = this.toPx(x); y = this.toPx(y); w = this.toPx(w); h = this.toPx(h);
51
+ this.ctx.save();
52
+ const { ctx } = this;
53
+ const rad = this.toPx(radius) || 0;
54
+ ctx.beginPath();
55
+ ctx.moveTo(x, y + rad);
56
+ ctx.lineTo(x, y + h - rad);
57
+ ctx.quadraticCurveTo(x, y + h, x + rad, y + h);
58
+ ctx.lineTo(x + w - rad, y + h);
59
+ ctx.quadraticCurveTo(x + w, y + h, x + w, y + h - rad);
60
+ ctx.lineTo(x + w, y + rad);
61
+ ctx.quadraticCurveTo(x + w, y, x + w - rad, y);
62
+ ctx.lineTo(x + rad, y);
63
+ ctx.quadraticCurveTo(x, y, x, y + rad);
64
+ ctx[`${type}Style`] = color;
65
+ ctx.closePath();
66
+ ctx[type]();
67
+ this.ctx.restore();
68
+ }
69
+
70
+ // 绘制圆形
71
+ drawCircular(x, y, r, color = '#ffffff', type = 'fill') {
72
+ const { ctx } = this;
73
+ ctx.beginPath();
74
+ ctx.arc(this.toPx(x + r), this.toPx(y + r), this.toPx(r), 0, 2 * Math.PI);
75
+ ctx.closePath();
76
+ ctx[`${type}Style`] = color;
77
+ ctx[type]();
78
+ }
79
+
80
+ /**
81
+ * 绘制图片
82
+ *
83
+ * @param {*} src
84
+ * @param {*} x
85
+ * @param {*} y
86
+ * @param {*} width
87
+ * @param {*} height
88
+ * @param {*} round 是否切成圆形图片
89
+ * @param {*} borderwidth 圆形模式下边框宽度
90
+ * @param {*} borderColor 圆形模式下边框颜色
91
+ * @returns
92
+ * @memberof Wxml2Poster
93
+ */
94
+ drawImage(src, x, y, width, height, round, borderwidth = 0, borderColor = '#ffffff') {
95
+ return new Promise((resolve, reject) => {
96
+ const img = this.canvas.createImage();
97
+ img.onerror = reject;
98
+ img.src = src;
99
+ img.onload = () => {
100
+ this.ctx.save();
101
+ if (round) {
102
+ const r = (width / 2) + borderwidth; // 半径
103
+ this.drawCircular(x - borderwidth, y - borderwidth, r, borderColor);
104
+ this.ctx.clip();
105
+ }
106
+ this.ctx.drawImage(img, this.toPx(x), this.toPx(y), this.toPx(width), this.toPx(height));
107
+ this.ctx.restore();
108
+ resolve();
109
+ };
110
+ });
111
+ };
112
+
113
+ /**
114
+ * 绘制文本
115
+ *
116
+ * @param {*} text 文本字符串
117
+ * @param {*} x 横坐标位置
118
+ * @param {*} y 纵坐标位置
119
+ * @param {*} size 字体大小
120
+ * @param {*} color 字体颜色
121
+ * @param {string} [align='left'] 对齐方式 left(x值不包含字体宽度), center(x值包含一半字体宽度), right(x值包含整个字体宽度)
122
+ * @param {string} [bold='normal'] 字重,值同css font-weight
123
+ * @param {string} [baseLine='middle'] top(y值不包含字体高度), middle(y值包含一半字体高度),bottom(y值包含整个字体高度)
124
+ * @returns 文本占用宽度
125
+ * @memberof Wxml2Poster
126
+ */
127
+ drawText(text, x, y, size, color, align = 'left', bold = 'normal', baseLine = 'middle', calWidth = false) {
128
+ const str = `${text}`; // 避免某些环境下字体宽度计算异常(如手图),这里统一将绘制文本转为字符串
129
+ this.ctx.save();
130
+ this.ctx.fillStyle = color;
131
+ this.ctx.font = `normal ${bold} ${~~(this.toPx(size))}px sans-serif`;
132
+ let width = 0;
133
+ if (calWidth) width = this.ctx.measureText(str).width; // 必须在设置字体属性后未绘制前计算才能拿到准确值; 无需计算时避免耗费性能
134
+ this.ctx.textAlign = align;
135
+ this.ctx.textBaseline = baseLine;
136
+ this.ctx.fillText(str, this.toPx(x), this.toPx(y));
137
+ this.ctx.restore();
138
+ return width;
139
+ }
140
+
141
+ /**
142
+ * 绘制文本并返回文本占用宽度
143
+ *
144
+ * @param {*} text 文本字符串
145
+ * @param {*} x 横坐标位置
146
+ * @param {*} y 纵坐标位置
147
+ * @param {*} size 字体大小
148
+ * @param {*} color 字体颜色
149
+ * @param {string} [align='left'] 对齐方式 left(x值不包含字体宽度), center(x值包含一半字体宽度), right(x值包含整个字体宽度)
150
+ * @param {string} [bold='normal'] 字重,值同css font-weight
151
+ * @param {string} [baseLine='top'] top(y值不包含字体高度), middle(y值包含一半字体高度),bottom(y值包含整个字体高度)
152
+ * @returns {number} width 文本占用宽度
153
+ * @returns {number} x 横坐标位置
154
+ * @memberof Wxml2Poster
155
+ */
156
+ drawTextWithWidth(text, x, y, size, color, align = 'left', bold = 'normal', baseLine = 'top') {
157
+ const width = this.drawText(text, x, y, size, color, align, bold, baseLine, true);
158
+ return {
159
+ width: width / this.ratio,
160
+ x,
161
+ };
162
+ }
163
+
164
+ /**
165
+ * 绘制由动态文本组成的行
166
+ * 可传入要绘制的文本数组或文本属性数组
167
+ * 若该行文本样式一致时直接传入文本数组即可
168
+ * 若需要个性化单个文本可传入文本属性数组
169
+ * 个性化属性未填写时默认使用全局属性
170
+ *
171
+ * @param {Object} textObj - 文本对象
172
+ * @param {string[]|Object[]} textObj.texts - 要绘制的文本数组或文本属性数组
173
+ * @param {string} textObj.texts[].text - 要绘制的文本
174
+ * @param {number} textObj.texts[].textMargin - 文本间距
175
+ * @param {number} textObj.texts[].size - 字体大小
176
+ * @param {string} textObj.texts[].color - 字体颜色
177
+ * @param {string} textObj.texts[].bold - 字重
178
+ * @param {string} textObj.texts[].originY - 起始y
179
+ * @param {number} textObj.originX - 起始x
180
+ * @param {number} textObj.originY - 起始y
181
+ * @param {number} textObj.textMargin - 文本间距
182
+ * @param {number} textObj.size - 字体大小
183
+ * @param {string} textObj.color - 字体颜色
184
+ * @param {string} textObj.bold - 字重
185
+ * @memberof CanvasDrawer
186
+ */
187
+ drawDynamicTextsRow({ texts = [], originX = 0, originY, textMargin = 0, size, color, bold = 'normal' } = {}) {
188
+ texts.reduce((totalX, item) => {
189
+ let text = item; let oy = originY; let tm = textMargin; let cl = color; let sz = size; let bl = bold;
190
+ if (typeof item === 'object') {
191
+ text = item.text;
192
+ oy = item.originY || originY;
193
+ tm = item.textMargin || textMargin;
194
+ cl = item.color || color;
195
+ sz = item.size || size;
196
+ bl = item.bold || bold;
197
+ }
198
+ const { width } = this.drawTextWithWidth(text, totalX, oy, sz, cl, 'left', bl, 'top');
199
+ return totalX + width + tm;
200
+ }, originX);
201
+ }
202
+
203
+ /**
204
+ * 批量绘制文本
205
+ * @param {Object[]} texts 文字数组
206
+ * @param {Object} texts[i], 文字属性 { text, x, y, size, color, align = 'left', bold = 'normal', baseLine = 'middle' }
207
+ */
208
+ drawTexts(texts) {
209
+ texts.forEach((item) => {
210
+ const { text, x, y, size, color, align = 'left', bold = 'normal', baseLine = 'middle' } = item;
211
+ this.drawText(text, x, y, size, color, align, bold, baseLine);
212
+ });
213
+ }
214
+
215
+ // 获取相册权限
216
+ getAlbumAuth() {
217
+ return new Promise((resolve, reject) => {
218
+ wx.getSetting({
219
+ success(res) {
220
+ if (res.authSetting['scope.writePhotosAlbum']) return resolve();
221
+ wx.authorize({
222
+ scope: 'scope.writePhotosAlbum',
223
+ success: () => resolve(),
224
+ fail: () => {
225
+ reject();
226
+ wx.showModal({
227
+ title: '',
228
+ content: '监测到您没有授权保存到相册权限,无法使用该功能,是否去授权',
229
+ showCancel: true,
230
+ cancelText: '取消',
231
+ cancelColor: '#000000',
232
+ confirmText: '现在去',
233
+ success: (result) => {
234
+ if (result.confirm) wx.openSetting();
235
+ },
236
+ });
237
+ },
238
+ });
239
+ },
240
+ });
241
+ });
242
+ }
243
+
244
+ // 保存图片(可选自定义toast)
245
+ saveImage({ customSuccToast = false, customFailToast = false } = {}) {
246
+ return new Promise((resolve, reject) => {
247
+ if (!this.tempFilePath) {
248
+ wx.showToast({ title: '海报未准备好,请稍等', icon: 'none' });
249
+ reject('资源还未绘制完成');
250
+ };
251
+ this.getAlbumAuth().then(() => {
252
+ wx.saveImageToPhotosAlbum({
253
+ filePath: this.tempFilePath,
254
+ success: () => {
255
+ !customSuccToast && wx.showToast({ title: '保存成功', icon: 'success', duration: 2000 });
256
+ resolve(this.tempFilePath);
257
+ },
258
+ fail: () => {
259
+ !customFailToast && wx.showToast({ title: '保存失败,请重试', icon: 'none' });
260
+ reject('取消保存');
261
+ },
262
+ });
263
+ });
264
+ });
265
+ }
266
+
267
+ // 导出画布(先导出,再saveImage)
268
+ canvasExport() {
269
+ return new Promise((resolve, reject) => {
270
+ wx.canvasToTempFilePath({
271
+ canvas: this.canvas,
272
+ success: (res) => {
273
+ this.tempFilePath = res.tempFilePath;
274
+ resolve(res.tempFilePath);
275
+ },
276
+ fail: () => {
277
+ reject();
278
+ },
279
+ }, this.compInstance);
280
+ });
281
+ }
282
+
283
+ // 获取画布导出状态
284
+ getExportStatus() {
285
+ return !!this.tempFilePath;
286
+ }
287
+ }
288
+
289
+ export default CanvasDrawer;
@@ -0,0 +1,61 @@
1
+ class Common {
2
+ // 获取本月开始时间
3
+ static getMonthStart() {
4
+ const now = new Date(); // 获取当前时间
5
+ const beginTimes = now.getFullYear(); // 开始计算
6
+ let month = now.getMonth() + 1 ; // getMonth()是以0开始的月份
7
+ if (month < 10) {
8
+ month = `0${month}`;
9
+ }
10
+ const startTimes = `${beginTimes}-${month}-01`; // 格式 Y-m-d
11
+ return startTimes;
12
+ }
13
+
14
+ static pointToYuan(num) {
15
+ if (!num || num <= 0) {
16
+ return 0;
17
+ }
18
+ const { tms } = getApp({ allowDefault: true });
19
+ return tms.roundStr(num / 100, 0);
20
+ }
21
+
22
+ static handleApiRes(res) {
23
+ let { errCode, errMsg, resData } = res;
24
+ // 兼容后端返回err、info作为字段的情况
25
+ // eslint-disable-next-line
26
+ if (resData?.data?.hasOwnProperty('err')) {
27
+ errCode = resData.data.err;
28
+ errMsg = resData.data.info;
29
+ resData = resData.data;
30
+ if (errCode !== 0) {
31
+ return Promise.reject({ errCode, errMsg, resData });
32
+ }
33
+ return Promise.resolve(resData);
34
+ }
35
+ if (errCode !== 0) {
36
+ return Promise.reject(res);
37
+ }
38
+ return Promise.resolve(resData);
39
+ }
40
+
41
+ static handleConditionValue(item, value) {
42
+ if (isNaN(value)) {
43
+ return { value, units: item.units };
44
+ }
45
+ let newValue = value;
46
+ let { units } = item;
47
+ // 小数点处理
48
+ if (item.point === 0) {
49
+ newValue = Math.round(newValue);
50
+ }
51
+ if (item.point) {
52
+ newValue = parseFloat(newValue.toFixed(item.point));
53
+ }
54
+ if (newValue > 9999) {
55
+ newValue = `${(newValue / 10000).toFixed(1)}`;
56
+ units = `万${item.units}`;
57
+ }
58
+ return { value: newValue, units };
59
+ }
60
+ }
61
+ export default Common;
@@ -0,0 +1,91 @@
1
+ import m from './Model';
2
+ import Comm from './Comm';
3
+
4
+ export default class Controller {
5
+ static report = m.report;
6
+ static carManager = m.carManager;
7
+ static getConfig = m.getConfig;
8
+ static locManager = m.locManager;
9
+ static rtLog = m.rtLog;
10
+ static dateToString = m.dateToString;
11
+ static log = m.log;
12
+
13
+ // 通用的埋点方法
14
+ // wecarId seriesId modelId必传
15
+ static commReport(currentCar = {}, param = {}) {
16
+ const { wecarId = '', seriesId = 0, modelId = 0 } = currentCar;
17
+ // console.log('我要埋点了', param['27'], { 4: wecarId, 39: seriesId, 30: modelId, ...param });
18
+ return Controller.report({ 4: wecarId, 39: seriesId, 30: modelId, ...param });
19
+ }
20
+
21
+ // 车辆本月etc账单
22
+ static async getETCAmount(currentCar) {
23
+ const { plate, wecarId, userId, plateColor } = currentCar;
24
+ const param = { userId, plateNum: plate, wecarId };
25
+ let result = { etcData: {} };
26
+ try {
27
+ const { data } = await m.queryEtcBindStatus(param) || {};
28
+ if (!data || data.etcStatus !== 1) {
29
+ result.isBindEtc = false;
30
+ return result;
31
+ }
32
+ } catch (error) {
33
+ result.isBindEtc = false;
34
+ return result;
35
+ }
36
+
37
+ try {
38
+ const param2 = { plateColor, dateBegin: Comm.getMonthStart(), dateEnd: Controller.dateToString() };
39
+ Object.assign(param, param2);
40
+ const res = await m.queryEtcMount(param) || {};
41
+ result = {
42
+ isBindEtc: true,
43
+ getEtcDataError: false,
44
+ };
45
+ if (res.totalAmount > 0) {
46
+ result.etcData = { ...res, totalAmount: Comm.pointToYuan(res.totalAmount) };
47
+ } else {
48
+ result.etcData = { };
49
+ }
50
+ } catch (error) {
51
+ result = {
52
+ isBindEtc: true,
53
+ getEtcDataError: true,
54
+ };
55
+ }
56
+ return result;
57
+ }
58
+
59
+ // 获取车况配置
60
+ static async getCarConditionConfig(param) {
61
+ let data = {};
62
+ try {
63
+ data = await m.getCarConditionConfig(param);
64
+ } catch (err) {
65
+ m.log({ name: 'getCarConditionConfig', param, err });
66
+ }
67
+ return data;
68
+ }
69
+
70
+ // 获取车控配置
71
+ static async getCarControlConfig(param) {
72
+ let data = {};
73
+ try {
74
+ data = await m.getCarControlConfig(param);
75
+ } catch (err) {
76
+ m.log({ name: 'getCarControlConfig', param, err });
77
+ }
78
+ return data;
79
+ }
80
+
81
+ // 获取车况
82
+ static async getGactoyotaCondition(param) {
83
+ let data = {};
84
+ try {
85
+ data = await m.getGactoyotaCondition(param);
86
+ } catch (err) {
87
+ m.log({ name: 'getGactoyotaCondition', param, err });
88
+ }
89
+ return data;
90
+ }
91
+ };
@@ -0,0 +1,52 @@
1
+ import Comm from './Comm';
2
+ const app = getApp({ allowDefault: true });
3
+
4
+ export default class Model {
5
+ static report = app.tms.getReporter().report;
6
+ static carManager = app.tms.getCarManager();
7
+ static getConfig = app.tms.getConfig;
8
+ static locManager = app.tms.getLocationManager();
9
+ static rtLog = app.tms.getRealtimeLogManager();
10
+ static dateToString = app.tms.dateToString;
11
+
12
+ static doRequest(path, param, method = 'POST', header) {
13
+ return app.tms.createRequest().doRequest(path, { ...param }, method, header)
14
+ .then(res => Comm.handleApiRes(res))
15
+ .catch(err => Promise.reject(err));
16
+ }
17
+
18
+ static log(...args) {
19
+ console.error(...args);
20
+ Model.rtLog.info(args[0]);
21
+ }
22
+
23
+ // 获取车况数据
24
+ // static getGactoyotaCondition(param) {
25
+ // return Model.doRequest('gactoyota/condition', { ...param }, 'POST');
26
+ // }
27
+ static getGactoyotaCondition(param) {
28
+ return Model.doRequest('carcardvinfosvc/get_car_condition', { ...param }, 'POST', {
29
+ 'X-TAI-Module': 'carcardvinfosvc',
30
+ });
31
+ }
32
+
33
+ // 获取车况配置
34
+ static getCarConditionConfig() {
35
+ const app = getApp({ allowDefault: true });
36
+ return app.tms.getConfig('/sinan/carcondition/config');
37
+ }
38
+
39
+ // 获取车控配置
40
+ static getCarControlConfig() {
41
+ const app = getApp({ allowDefault: true });
42
+ return app.tms.getConfig('/sinan/carcontrol/config');
43
+ }
44
+
45
+ static queryEtcMount(param) {
46
+ return Model.doRequest('etc/totalamount', { ...param }, 'post');
47
+ }
48
+
49
+ static queryEtcBindStatus(param) {
50
+ return Model.doRequest('etc/querybindstatus', { ...param }, 'post');
51
+ }
52
+ };
@@ -0,0 +1,152 @@
1
+ /* 初始化 */
2
+ page {
3
+ --fontDIN: DIN Alternate;
4
+
5
+ line-height: 1.5;
6
+ }
7
+
8
+ view,
9
+ scroll-view,
10
+ swiper,
11
+ button,
12
+ input,
13
+ textarea,
14
+ label,
15
+ navigator,
16
+ image {
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ image {
21
+ width: 100%;
22
+ height: 100%;
23
+ }
24
+
25
+ input {
26
+ width: 100%;
27
+ }
28
+
29
+ /* 隐藏scroll-view的滚动条 */
30
+ ::-webkit-scrollbar {
31
+ width: 0;
32
+ height: 0;
33
+ color: transparent;
34
+ }
35
+
36
+ /* flex布局样式 */
37
+
38
+ /* 水平 */
39
+ .flex-row {
40
+ display: flex;
41
+ flex-direction: row;
42
+ }
43
+
44
+ /* 垂直 */
45
+ .flex-col {
46
+ display: flex;
47
+ flex-direction: column;
48
+ height: 100%;
49
+ }
50
+
51
+ /* 横向水平居中 */
52
+ .flex-row-hc {
53
+ display: flex;
54
+ flex-direction: row;
55
+ justify-content: center;
56
+ }
57
+
58
+ /* 横向垂直居中 */
59
+ .flex-row-vc {
60
+ display: flex;
61
+ flex-direction: row;
62
+ align-items: center;
63
+ }
64
+
65
+ /* 横向水平垂直居中 */
66
+ .flex-row-hvc {
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: center;
70
+ }
71
+
72
+ /* 纵向水平居中 */
73
+ .flex-col-hc {
74
+ display: flex;
75
+ flex-direction: column;
76
+ align-items: center;
77
+ }
78
+
79
+ /* 纵向垂直居中 */
80
+ .flex-col-vc {
81
+ display: flex;
82
+ flex-direction: column;
83
+ justify-content: center;
84
+ }
85
+
86
+ /* 纵向垂直水平居中 */
87
+ .flex-col-vhc {
88
+ display: flex;
89
+ flex-direction: column;
90
+ align-items: center;
91
+ justify-content: center;
92
+ }
93
+
94
+ /* 主轴 左对齐 */
95
+ .flex-main-left {
96
+ justify-content: flex-start;
97
+ }
98
+
99
+ /* 主轴 右对齐 */
100
+ .flex-main-right {
101
+ justify-content: flex-end;
102
+ }
103
+
104
+ /* 主轴 边距对齐 */
105
+ .flex-main-around {
106
+ justify-content: space-around;
107
+ }
108
+
109
+ /* 主轴 边界对齐 */
110
+ .flex-main-between {
111
+ justify-content: space-between;
112
+ }
113
+
114
+ /* 主轴 间距对齐 */
115
+ .flex-main-evenly {
116
+ justify-content: space-evenly;
117
+ }
118
+
119
+ /* 交叉轴 上对齐 */
120
+ .flex-cross-top {
121
+ align-items: flex-start;
122
+ }
123
+
124
+ /* 交叉轴 下对齐 */
125
+ .flex-cross-bottom {
126
+ align-items: flex-end;
127
+ }
128
+
129
+ /* 换行 */
130
+ .flex-wrap {
131
+ flex-wrap: wrap;
132
+ }
133
+
134
+ /* 重置官方按钮样式 包含去掉边框和其他 */
135
+ .resetbtn {
136
+ padding: 0;
137
+ line-height: 0;
138
+ background: transparent;
139
+ border: 0;
140
+ }
141
+
142
+ .resetbtn::after {
143
+ border: 0;
144
+ }
145
+
146
+ /* 超出部分出省略号 */
147
+ .text-overflow {
148
+ overflow: hidden;
149
+ text-overflow: ellipsis;
150
+ word-break: break-all;
151
+ white-space: nowrap;
152
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @desc: 位置工具方法语法糖
3
+ * @author: fenggangsun
4
+ * @date: 2022-06-27 11:46:25
5
+ * @lastEditors: fenggangsun
6
+ * @lastEditTime: 2022-06-29 17:34:24
7
+ * @copyright 2022-present, Tencent, Inc. All rights reserved.
8
+ */
9
+ const defaultLoc = {
10
+ province: '广东省',
11
+ cityName: '深圳市',
12
+ latitude: 22.54286,
13
+ longitude: 114.05956,
14
+ cityCode: '440300',
15
+ adCode: '440301',
16
+ };
17
+
18
+ let locManagerCache;
19
+ const getLocManager = () => {
20
+ locManagerCache ||= getApp().tms.getLocationManager();
21
+ return locManagerCache;
22
+ };
23
+
24
+ const locTypeEnum = { // 位置类型
25
+ default: 'default', // 默认位置
26
+ ip: 'ip', // ip位置
27
+ real: 'real', // 实时定位
28
+ userChosen: 'userChosen', // 用户首页选择城市
29
+ userChosenHistory: 'userChosenHistory', // 用户曾经在首页选择城市(非本次生命中期)
30
+ };
31
+
32
+ /**
33
+ * 按优先级获取用户位置
34
+ * @param {Array<String>} typeOrders 取值次序,每个值代表一种位置数据;可选值见 locTypeEnum
35
+ * @param {Object} option 调用具体接口时的一些参数
36
+ * @param {Boolean} option.showAuth 获取实时定位时,如果未授权,是否发起授权
37
+ * @param {String} option.authTip 获取实时定位,发起授权时的提示语
38
+ * @param {Boolean} option.ipCache 是否使用缓存的 ip 位置,默认 true;ipCache = false 时会重新发起 ip 定位
39
+ * @returns {Promise<Object>} { loc, locType }
40
+ * loc: { adCode, provinceCode, cityCode, province, cityName, longitude, latitude }
41
+ */
42
+ const getPrioritizedLocation = async (typeOrders, option) => {
43
+ const { showAuth, authTip, ipCache = true } = option || {};
44
+ for (const locType of typeOrders) { // 按顺序获取位置
45
+ let getLocProm;
46
+ switch (locType) {
47
+ case locTypeEnum.default: // 默认位置
48
+ getLocProm = defaultLoc;
49
+ break;
50
+ case locTypeEnum.ip:
51
+ getLocProm = getLocManager().getIpLocation(!ipCache)
52
+ .then((data) => {
53
+ const { ad_info: { adcode, cityCode, province, city }, location: { lng, lat } } = data;
54
+ return {
55
+ adCode: adcode, cityCode, provinceCode: `${cityCode.slice(0, 4)}00`,
56
+ province, cityName: city, longitude: lng, latitude: lat,
57
+ ...data,
58
+ };
59
+ });
60
+ break;
61
+ case locTypeEnum.real: // 实时位置
62
+ getLocProm = getLocManager().getLocationDetail(showAuth, undefined, authTip);
63
+ break;
64
+ case locTypeEnum.userChosen: // 本次生命周期用户选择位置
65
+ getLocProm = getLocManager().getUserLocation();
66
+ break;
67
+ case locTypeEnum.userChosenHistory: // 历史曾经选择的位置
68
+ getLocProm = wx.getStorageSync('home.city');
69
+ break;
70
+ default: break;
71
+ }
72
+ const loc = await Promise.resolve(getLocProm).catch(() => null);
73
+ if (loc) return { loc, locType };
74
+ }
75
+ return Promise.reject('按照参数指定的类型未能获取到位置');
76
+ };
77
+
78
+ /**
79
+ * 按优先级获取用户位置
80
+ * @param {Boolean} visitedIndex 是否打开过首页
81
+ * @param {Object} option 调用具体接口时的一些参数
82
+ * @param {Boolean} option.showAuth 获取实时定位时,如果未授权,是否发起授权
83
+ * @param {String} option.authTip 获取实时定位,发起授权时的提示语
84
+ * @returns {Promise<Object>} { loc, locType }
85
+ */
86
+ const getInitLocation = async (visitedIndex, option) => {
87
+ // 从首页进入当前页,按 首页选择位置 > 实时位置 > 首页缓存的用户上一次选择的位置 > 默认位置 的顺序返回位置
88
+ if (visitedIndex) return getPrioritizedLocation(
89
+ [locTypeEnum.userChosen, locTypeEnum.real, locTypeEnum.userChosenHistory, locTypeEnum.default],
90
+ option,
91
+ );
92
+ // 非首页进入当前页,优先使用定位信息,其次使用默认位置
93
+ return getPrioritizedLocation([locTypeEnum.real, locTypeEnum.default], option);
94
+ };
95
+
96
+ const api = {
97
+ defaultLoc,
98
+ locTypeEnum,
99
+ getInitLocation,
100
+ getPrioritizedLocation,
101
+ };
102
+
103
+ export default api;
@@ -0,0 +1,130 @@
1
+ // @copyright 2022-present, Tencent, Inc. All rights reserved.
2
+ // @author Fenggang.Sun <fenggangsun@tencent.com>
3
+ // @file 路由工具方法
4
+
5
+ /**
6
+ * 处理链接,获取页面路径
7
+ * @param {String} url 原始链接
8
+ * @returns {String} 页面路径,开头不带'/',后面不带query
9
+ */
10
+ const getPagePath = (url) => {
11
+ let path = String(url);
12
+ const index = path.indexOf('?');
13
+ if (index > -1) path = path.substring(0, index);
14
+ return path[0] === '/' ? path.substring(1) : path;
15
+ };
16
+
17
+ /**
18
+ * 找到页面访问栈中的某个页面
19
+ * @param {String} url 目标页面路径
20
+ * @param {Object} matchOptions 要检查的页面加载参数;指定此参数时,历史访问页面 onLoad 参数需与 matchOptions 指定的参数相符,才会被判定为要查找的页面
21
+ * @returns {Object} history 历史访问页面
22
+ * @returns {Number} history.size 当前页面栈大小,一共打开了多少页面
23
+ * @returns {Number} history.targetIndex 目标页面在页面栈中的下标,-1表示没有找到目标页面,下标越小表示越靠早访问(靠近栈底)
24
+ * @returns {Number} history.delta 目标页面与当前页面在访问栈中的距离,-1表示未找到目标页面;可使用此字段作为navigateBack参数来回到目标页面
25
+ * 示例说明:假定目前页面访问栈为:[ A?x=1&y=2, B, A?x=1, A, C ],C为当前页面,则:
26
+ * findHistoryPage(A) => { size: 4, targetIndex: 3, delta: 1 }
27
+ * findHistoryPage(A, { x: 1 }) => { size: 4, targetIndex: 2, delta: 2 }
28
+ * findHistoryPage(A, { x: 1, y = 2 }) => { size: 4, targetIndex: 0, delta: 4 }
29
+ * findHistoryPage(A, { x: 2 }) => { size: 4, targetIndex: -1, delta: -1 }
30
+ * findHistoryPage(B) => { size: 4, targetIndex: 1, delta: 2 }
31
+ * findHistoryPage(C) => { size: 4, targetIndex: 4, delta: 0 }
32
+ * findHistoryPage(D) => { size: 4, targetIndex: -1, delta: -1 }
33
+ */
34
+ const findHistoryPage = (url, matchOptions = {}) => {
35
+ const pages = getCurrentPages() || [];
36
+ const currentPageIndex = pages.length - 1;
37
+ let pageIndex = -1;
38
+ const targetPagePath = getPagePath(url);
39
+ pages.some((page, index) => {
40
+ const pathMatch = String(page?.route) === targetPagePath;
41
+ if (!pathMatch) return false;
42
+ const keys = Object.keys(matchOptions || {});
43
+ if (keys.length > 0) { // 检查页面onLoad options是否符合要求
44
+ if (!keys.every((key) => {
45
+ const expt = matchOptions[key];
46
+ const pageOptionVal = page.options[key];
47
+ return String(expt) === String(pageOptionVal);
48
+ })) { // 页面 onLoad 参数检查不符合 matchOptions 要求
49
+ return false;
50
+ }
51
+ }
52
+ // 找到了符合要求的页面
53
+ pageIndex = index;
54
+ return true;
55
+ });
56
+ return {
57
+ size: pages.length,
58
+ targetIndex: pageIndex,
59
+ delta: pageIndex === -1 ? -1 : currentPageIndex - pageIndex,
60
+ };
61
+ };
62
+
63
+ /**
64
+ * 可尽量避免循环跳转的跳转方法
65
+ * 如果可以找到与目录页面相符的历史访问记录,直接回退到该页面;否则打开新页面
66
+ * @param {Object} options 跳转参数,类似 wx.navigateTo 和 wx.navigateBack 参数
67
+ * @param {String} options.url 跳转路径(非tab页),可带参数
68
+ * @param {Function} [options.success] 接口调用成功的回调函数
69
+ * @param {Function} [options.fail] 接口调用失败的回调函数
70
+ * @param {Function} [options.complete] 接口调用结束的回调函数(调用成功、失败都会执行)
71
+ */
72
+ const navigateTo = (options, matchOptions) => {
73
+ const { url, success, fail, complete } = options;
74
+ const { delta } = findHistoryPage(url, matchOptions);
75
+ if (delta < 0) wx.navigateTo({ url, success, fail, complete });
76
+ else wx.navigateBack({ delta, success, fail, complete });
77
+ };
78
+
79
+
80
+ /**
81
+ * 可跳过栈顶指定页面的回退
82
+ * @param {Number} options.delta 返回的页面数;默认1,即返回到上一页;当指定 paths 参数且栈顶有 paths 中的页面时,delta 会被重置
83
+ * @param {Function} options.success 接口调用成功的回调函数
84
+ * @param {Function} options.fail 接口调用失败的回调函数
85
+ * @param {Function} options.complete 接口调用结束的回调函数(调用成功、失败都会执行)
86
+ * @param {Array<String>} paths 要跳过的页面路径
87
+ */
88
+ const navigateBack = (options = {}, paths) => {
89
+ // 如果先选择车型再进入车信息页,返回时应跳过车型选择相关页面
90
+ if (!paths?.length) return wx.navigateBack(options);
91
+ const pages = getCurrentPages();
92
+ let foundPaths = 0;
93
+ const purePaths = paths.map(path => getPagePath(path));
94
+ for (let i = pages.length - 2; i >= 0; i -= 1) {
95
+ if (purePaths.indexOf(pages[i].route) > -1) foundPaths += 1;
96
+ else break;
97
+ }
98
+ wx.navigateBack({ ...options, delta: foundPaths > 0 ? foundPaths + 1 : (options.delta || 1) });
99
+ };
100
+
101
+ let homePagePath;
102
+ const getHomePagePath = () => {
103
+ if (homePagePath) return homePagePath;
104
+ const { isTab, path, tabs } = getApp().tms.getHomePage() || {};
105
+ if (!isTab && path) homePagePath = path;
106
+ else if (isTab && tabs?.length > 0) [homePagePath] = tabs;
107
+ return homePagePath;
108
+ };
109
+
110
+ // 判定页面访问栈中是否有首页
111
+ const visitedIndex = () => {
112
+ const homePagePath = getHomePagePath();
113
+ const homePageRegExp = new RegExp(homePagePath);
114
+ const pages = getCurrentPages();
115
+ return pages?.some?.((page) => {
116
+ let pagePath = String(page?.route);
117
+ if (!pagePath.startsWith('/')) pagePath = ['/', pagePath].join('');
118
+ return homePageRegExp.test(pagePath);
119
+ });
120
+ };
121
+
122
+ const api = {
123
+ findHistoryPage,
124
+ navigateTo,
125
+ navigateBack,
126
+ getHomePagePath,
127
+ visitedIndex,
128
+ };
129
+
130
+ module.exports = api;