chore: initialize mini program frontend
This commit is contained in:
+1894
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
apiBaseUrl: 'https://api.jiahengsmart.top',
|
||||
// apiBaseUrl: 'http://192.168.3.42:8020',
|
||||
// apiBaseUrl: 'http://172.16.144.66:8020',
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
const api = require('./api')
|
||||
|
||||
const EARTH_RADIUS_METERS = 6371000
|
||||
const LOCATION_RETRY_COUNT = 3
|
||||
const LOCATION_RETRY_DELAY_MS = 600
|
||||
|
||||
const isValidNumber = value => typeof value === 'number' && Number.isFinite(value)
|
||||
|
||||
const toRadians = degrees => (degrees * Math.PI) / 180
|
||||
|
||||
const sleep = ms => new Promise(resolve => {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
|
||||
const distanceMeters = (from, to) => {
|
||||
const lat1 = toRadians(from.latitude)
|
||||
const lat2 = toRadians(to.latitude)
|
||||
const deltaLat = toRadians(to.latitude - from.latitude)
|
||||
const deltaLng = toRadians(to.longitude - from.longitude)
|
||||
const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2)
|
||||
+ Math.cos(lat1) * Math.cos(lat2)
|
||||
* Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2)
|
||||
return 2 * EARTH_RADIUS_METERS * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
||||
}
|
||||
|
||||
const normalizePoint = point => {
|
||||
const latitudeValue = point.latitude
|
||||
const longitudeValue = point.longitude
|
||||
const latitude = latitudeValue === '' || latitudeValue === null || latitudeValue === undefined
|
||||
? NaN
|
||||
: Number(latitudeValue)
|
||||
const longitude = longitudeValue === '' || longitudeValue === null || longitudeValue === undefined
|
||||
? NaN
|
||||
: Number(longitudeValue)
|
||||
return {
|
||||
name: point.name || '',
|
||||
latitude,
|
||||
longitude,
|
||||
radiusMeters: Number(point.radiusMeters || 500),
|
||||
}
|
||||
}
|
||||
|
||||
const getFactoryConfigs = async (scope = 'accessible') => {
|
||||
const source = scope === 'public'
|
||||
? await api.listPublicAttendancePoints()
|
||||
: await api.listAccessibleAttendancePoints()
|
||||
const points = source.map(normalizePoint)
|
||||
const validPoints = points.filter(point => (
|
||||
point.name
|
||||
&& isValidNumber(point.latitude)
|
||||
&& isValidNumber(point.longitude)
|
||||
&& Number.isFinite(point.radiusMeters)
|
||||
&& point.radiusMeters > 0
|
||||
))
|
||||
if (!validPoints.length) {
|
||||
throw new Error('未配置考勤点经纬度,请经理先在考勤设置中配置')
|
||||
}
|
||||
return validPoints
|
||||
}
|
||||
|
||||
const getCurrentLocation = () => new Promise((resolve, reject) => {
|
||||
wx.getLocation({
|
||||
type: 'gcj02',
|
||||
isHighAccuracy: true,
|
||||
highAccuracyExpireTime: 8000,
|
||||
success: resolve,
|
||||
fail: error => {
|
||||
const message = error && error.errMsg ? error.errMsg : ''
|
||||
if (message.indexOf('auth deny') >= 0 || message.indexOf('authorize no response') >= 0) {
|
||||
reject(new Error('请允许获取位置信息后再扫码报工'))
|
||||
return
|
||||
}
|
||||
reject(new Error('获取当前位置失败,请确认已开启定位权限'))
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const locationAccuracyText = location => {
|
||||
const accuracy = Number(location && location.accuracy)
|
||||
return Number.isFinite(accuracy) && accuracy > 0 ? `,定位精度约${Math.round(accuracy)}米` : ''
|
||||
}
|
||||
|
||||
const evaluateLocation = (current, factories, attendancePointName = '') => {
|
||||
const targetName = String(attendancePointName || '').trim()
|
||||
const matches = factories
|
||||
.map(factory => ({
|
||||
current,
|
||||
factory,
|
||||
distance: distanceMeters(current, factory),
|
||||
}))
|
||||
.sort((a, b) => a.distance - b.distance)
|
||||
|
||||
const targetMatch = targetName
|
||||
? matches.find(item => item.factory.name === targetName)
|
||||
: null
|
||||
const matched = targetMatch
|
||||
? (targetMatch.distance <= targetMatch.factory.radiusMeters ? targetMatch : null)
|
||||
: matches.find(item => item.distance <= item.factory.radiusMeters)
|
||||
return matched || {
|
||||
current,
|
||||
factory: null,
|
||||
distance: (targetMatch || matches[0]) ? (targetMatch || matches[0]).distance : 0,
|
||||
nearest: targetMatch || matches[0] || null,
|
||||
}
|
||||
}
|
||||
|
||||
const getCurrentAttendancePoint = async (scope = 'accessible', attendancePointName = '') => {
|
||||
const factories = await getFactoryConfigs(scope)
|
||||
const attempts = []
|
||||
let lastError = null
|
||||
|
||||
for (let index = 0; index < LOCATION_RETRY_COUNT; index += 1) {
|
||||
try {
|
||||
const current = await getCurrentLocation()
|
||||
if (isValidNumber(current.latitude) && isValidNumber(current.longitude)) {
|
||||
const result = evaluateLocation(current, factories, attendancePointName)
|
||||
attempts.push(result)
|
||||
if (result.factory) {
|
||||
return {
|
||||
...result,
|
||||
sampleCount: attempts.length,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
}
|
||||
if (index < LOCATION_RETRY_COUNT - 1) {
|
||||
await sleep(LOCATION_RETRY_DELAY_MS)
|
||||
}
|
||||
}
|
||||
|
||||
if (!attempts.length && lastError) {
|
||||
throw lastError
|
||||
}
|
||||
const best = attempts.sort((a, b) => (a.distance || 0) - (b.distance || 0))[0]
|
||||
return {
|
||||
...(best || { current: null, factory: null, distance: 0, nearest: null }),
|
||||
sampleCount: attempts.length,
|
||||
}
|
||||
}
|
||||
|
||||
const ensureFactoryLocation = async (attendancePointName = '', scope = 'accessible') => {
|
||||
const result = await getCurrentAttendancePoint(scope, attendancePointName)
|
||||
if (!result.factory) {
|
||||
const nearest = result.nearest
|
||||
if (nearest && nearest.factory) {
|
||||
throw new Error(`已多次尝试定位,当前位置距离考勤点【${nearest.factory.name}】约${Math.round(nearest.distance)}米,超过${nearest.factory.radiusMeters}米范围${locationAccuracyText(result.current)},不能扫码报工`)
|
||||
}
|
||||
throw new Error('当前位置未进入任何考勤点,不能扫码报工')
|
||||
}
|
||||
if (attendancePointName && result.factory.name !== attendancePointName) {
|
||||
throw new Error(`当前位置已进入【${result.factory.name}】考勤点,不是该二维码所属的【${attendancePointName}】考勤点`)
|
||||
}
|
||||
return {
|
||||
current: result.current,
|
||||
factory: result.factory,
|
||||
distance: result.distance,
|
||||
}
|
||||
}
|
||||
|
||||
const ensureTemporaryWorkerLocation = async () => {
|
||||
const result = await getCurrentAttendancePoint('public')
|
||||
if (!result.factory) {
|
||||
const nearest = result.nearest
|
||||
if (nearest && nearest.factory) {
|
||||
throw new Error(`已多次尝试定位,当前位置距离最近考勤点【${nearest.factory.name}】约${Math.round(nearest.distance)}米,超过${nearest.factory.radiusMeters}米范围${locationAccuracyText(result.current)},不能创建临时工`)
|
||||
}
|
||||
throw new Error('当前位置未进入任何考勤点,不能创建临时工')
|
||||
}
|
||||
return {
|
||||
current: result.current,
|
||||
factory: result.factory,
|
||||
distance: result.distance,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ensureFactoryLocation,
|
||||
ensureTemporaryWorkerLocation,
|
||||
getCurrentAttendancePoint,
|
||||
distanceMeters,
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
const people = [
|
||||
{
|
||||
phone: '13800000001',
|
||||
name: '张三',
|
||||
role: 'worker',
|
||||
},
|
||||
{
|
||||
phone: '13800000002',
|
||||
name: '李四',
|
||||
role: 'admin',
|
||||
},
|
||||
{
|
||||
phone: '13800000003',
|
||||
name: '王经理',
|
||||
role: 'manager',
|
||||
},
|
||||
{
|
||||
phone: '13800000004',
|
||||
name: '赵六',
|
||||
role: 'worker',
|
||||
},
|
||||
]
|
||||
|
||||
const products = [
|
||||
{
|
||||
projectNo: 'W02',
|
||||
profileNo: 'XUS-157-6001-003',
|
||||
productName: 'LAC-56011041后防撞梁左吸能盒端板',
|
||||
materialCode: '110101002022',
|
||||
materialName: 'LAC-56011041-后防撞梁左吸能盒端板-半成品',
|
||||
deviceNo: '20#',
|
||||
process: '1序',
|
||||
stampingMethod: '切边冲孔',
|
||||
operatorCount: 1,
|
||||
standardBeat: 10.984,
|
||||
standardWorkload: 2622,
|
||||
},
|
||||
{
|
||||
projectNo: 'W02',
|
||||
profileNo: 'XUS-157-6001-003',
|
||||
productName: 'LAC-56011044后防撞梁右吸能盒端板',
|
||||
materialCode: '110101002023',
|
||||
materialName: 'LAC-56011044-后防撞梁右吸能盒端板-冲切-半成品',
|
||||
deviceNo: '4#',
|
||||
process: '1序',
|
||||
stampingMethod: '切边冲孔',
|
||||
operatorCount: 1,
|
||||
standardBeat: 10.984,
|
||||
standardWorkload: 2622,
|
||||
},
|
||||
{
|
||||
projectNo: 'W02',
|
||||
profileNo: 'XUS-157-6009-001',
|
||||
productName: 'LAC-56010050/0014后防撞梁左连接板',
|
||||
materialCode: '110501000186',
|
||||
materialName: 'LAC-56010050-后防撞梁左连接板-半成品',
|
||||
deviceNo: '18#',
|
||||
process: '1序',
|
||||
stampingMethod: '切边冲孔',
|
||||
operatorCount: 1,
|
||||
standardBeat: 5.112,
|
||||
standardWorkload: 5634,
|
||||
},
|
||||
{
|
||||
projectNo: 'W02',
|
||||
profileNo: 'XUS-157-6009-001',
|
||||
productName: 'LAC-56010050/0014后防撞梁右连接板',
|
||||
materialCode: '110701000359',
|
||||
materialName: 'LAC-56000014-后防撞梁右连接板-半成品',
|
||||
deviceNo: '18#',
|
||||
process: '1序',
|
||||
stampingMethod: '切边冲孔',
|
||||
operatorCount: 1,
|
||||
standardBeat: 5.112,
|
||||
standardWorkload: 5634,
|
||||
},
|
||||
{
|
||||
projectNo: 'W02',
|
||||
profileNo: 'XUS-157-6010-002',
|
||||
productName: 'LAC-80010836左后纵梁后段加强板1',
|
||||
materialCode: '110501000199',
|
||||
materialName: 'LAC-80010836-左后纵梁后段加强板一-半成品',
|
||||
deviceNo: '2#',
|
||||
process: '1序',
|
||||
stampingMethod: '切边冲孔',
|
||||
operatorCount: 1,
|
||||
standardBeat: 12.952,
|
||||
standardWorkload: 2224,
|
||||
},
|
||||
{
|
||||
projectNo: 'W02',
|
||||
profileNo: 'XUS-157-6010-002',
|
||||
productName: 'LAC-80010837右后纵梁后段加强板1',
|
||||
materialCode: '110501000200',
|
||||
materialName: 'LAC-80010837-右后纵梁后段加强板一-半成品',
|
||||
deviceNo: '2#',
|
||||
process: '1序',
|
||||
stampingMethod: '切边冲孔',
|
||||
operatorCount: 1,
|
||||
standardBeat: 12.952,
|
||||
standardWorkload: 2224,
|
||||
},
|
||||
{
|
||||
projectNo: 'W02',
|
||||
profileNo: 'XUS-157-6011-001',
|
||||
productName: '前围下横梁左支架',
|
||||
materialCode: '110501000211',
|
||||
materialName: '前围下横梁左支架-半成品',
|
||||
deviceNo: '28#',
|
||||
process: '1序',
|
||||
stampingMethod: '落料冲孔',
|
||||
operatorCount: 1,
|
||||
standardBeat: 8.64,
|
||||
standardWorkload: 3333,
|
||||
},
|
||||
{
|
||||
projectNo: 'W02',
|
||||
profileNo: 'XUS-157-6011-002',
|
||||
productName: '前围下横梁右支架',
|
||||
materialCode: '110501000212',
|
||||
materialName: '前围下横梁右支架-半成品',
|
||||
deviceNo: '28#',
|
||||
process: '1序',
|
||||
stampingMethod: '落料冲孔',
|
||||
operatorCount: 1,
|
||||
standardBeat: 8.64,
|
||||
standardWorkload: 3333,
|
||||
},
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
people,
|
||||
products,
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
const config = require('./config')
|
||||
|
||||
const AUTH_KEY = 'jh_wrs_auth'
|
||||
|
||||
const getAuth = () => wx.getStorageSync(AUTH_KEY) || null
|
||||
|
||||
const setAuth = auth => {
|
||||
wx.setStorageSync(AUTH_KEY, auth)
|
||||
return auth
|
||||
}
|
||||
|
||||
const clearAuth = () => {
|
||||
wx.removeStorageSync(AUTH_KEY)
|
||||
}
|
||||
|
||||
const buildQuery = params => Object.keys(params || {})
|
||||
.filter(key => params[key] !== undefined && params[key] !== null && params[key] !== '')
|
||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
|
||||
.join('&')
|
||||
|
||||
const formatErrorDetail = detail => {
|
||||
if (Array.isArray(detail)) {
|
||||
return detail.map(item => item.msg || item.message || JSON.stringify(item)).join(';')
|
||||
}
|
||||
if (detail && typeof detail === 'object') {
|
||||
return detail.msg || detail.message || JSON.stringify(detail)
|
||||
}
|
||||
return detail
|
||||
}
|
||||
|
||||
const request = options => new Promise((resolve, reject) => {
|
||||
const auth = getAuth()
|
||||
const query = buildQuery(options.params)
|
||||
const url = `${config.apiBaseUrl}${options.url}${query ? `?${query}` : ''}`
|
||||
wx.request({
|
||||
url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || undefined,
|
||||
timeout: options.timeout || 15000,
|
||||
header: {
|
||||
'content-type': 'application/json',
|
||||
...(auth && auth.accessToken ? { Authorization: `Bearer ${auth.accessToken}` } : {}),
|
||||
...(options.header || {}),
|
||||
},
|
||||
success: res => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(res.data)
|
||||
return
|
||||
}
|
||||
if (res.statusCode === 401) {
|
||||
clearAuth()
|
||||
}
|
||||
const detail = res.data && (res.data.detail || res.data.message)
|
||||
const message = formatErrorDetail(detail) || `接口请求失败:${res.statusCode}`
|
||||
const error = new Error(message)
|
||||
error.statusCode = res.statusCode
|
||||
error.detail = detail
|
||||
reject(error)
|
||||
},
|
||||
fail: err => {
|
||||
reject(new Error(err.errMsg || '网络请求失败'))
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const upload = options => new Promise((resolve, reject) => {
|
||||
const auth = getAuth()
|
||||
wx.uploadFile({
|
||||
url: `${config.apiBaseUrl}${options.url}`,
|
||||
filePath: options.filePath,
|
||||
name: options.name || 'file',
|
||||
timeout: options.timeout || 30000,
|
||||
header: {
|
||||
...(auth && auth.accessToken ? { Authorization: `Bearer ${auth.accessToken}` } : {}),
|
||||
...(options.header || {}),
|
||||
},
|
||||
success: res => {
|
||||
let data = res.data
|
||||
try {
|
||||
data = data ? JSON.parse(data) : {}
|
||||
} catch (error) {
|
||||
data = { raw: res.data }
|
||||
}
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(data)
|
||||
return
|
||||
}
|
||||
reject(new Error(formatErrorDetail(data && (data.detail || data.message)) || `上传失败:${res.statusCode}`))
|
||||
},
|
||||
fail: err => {
|
||||
reject(new Error(err.errMsg || '上传失败'))
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const download = options => new Promise((resolve, reject) => {
|
||||
const auth = getAuth()
|
||||
const query = buildQuery(options.params)
|
||||
wx.downloadFile({
|
||||
url: `${config.apiBaseUrl}${options.url}${query ? `?${query}` : ''}`,
|
||||
header: {
|
||||
...(auth && auth.accessToken ? { Authorization: `Bearer ${auth.accessToken}` } : {}),
|
||||
...(options.header || {}),
|
||||
},
|
||||
timeout: options.timeout || 30000,
|
||||
success: res => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(res.tempFilePath)
|
||||
return
|
||||
}
|
||||
if (res.statusCode === 401) {
|
||||
clearAuth()
|
||||
}
|
||||
reject(new Error(`下载失败:${res.statusCode}`))
|
||||
},
|
||||
fail: err => {
|
||||
reject(new Error(err.errMsg || '下载失败'))
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const downloadUrl = options => new Promise((resolve, reject) => {
|
||||
const auth = getAuth()
|
||||
wx.downloadFile({
|
||||
url: options.url,
|
||||
filePath: options.filePath,
|
||||
header: {
|
||||
...(auth && auth.accessToken ? { Authorization: `Bearer ${auth.accessToken}` } : {}),
|
||||
...(options.header || {}),
|
||||
},
|
||||
timeout: options.timeout || 30000,
|
||||
success: res => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(res.filePath || res.tempFilePath || options.filePath)
|
||||
return
|
||||
}
|
||||
if (res.statusCode === 401) {
|
||||
clearAuth()
|
||||
}
|
||||
reject(new Error(`下载失败:${res.statusCode}`))
|
||||
},
|
||||
fail: err => {
|
||||
reject(new Error(err.errMsg || '下载失败'))
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
clearAuth,
|
||||
download,
|
||||
downloadUrl,
|
||||
getAuth,
|
||||
request,
|
||||
setAuth,
|
||||
upload,
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
const mockData = require('./mockData')
|
||||
|
||||
const STORE_KEY = 'jh_wrs_store_v1'
|
||||
|
||||
const clone = value => JSON.parse(JSON.stringify(value))
|
||||
|
||||
const baseStore = () => ({
|
||||
currentUserPhone: '',
|
||||
people: clone(mockData.people),
|
||||
products: clone(mockData.products),
|
||||
reports: [],
|
||||
sessions: [],
|
||||
})
|
||||
|
||||
const getStore = () => {
|
||||
const store = wx.getStorageSync(STORE_KEY)
|
||||
if (store && store.people && store.products) {
|
||||
return store
|
||||
}
|
||||
|
||||
const nextStore = baseStore()
|
||||
wx.setStorageSync(STORE_KEY, nextStore)
|
||||
return nextStore
|
||||
}
|
||||
|
||||
const saveStore = store => {
|
||||
wx.setStorageSync(STORE_KEY, store)
|
||||
return store
|
||||
}
|
||||
|
||||
const resetStore = () => saveStore(baseStore())
|
||||
|
||||
const getCurrentUser = () => {
|
||||
const store = getStore()
|
||||
return store.people.find(person => person.phone === store.currentUserPhone) || null
|
||||
}
|
||||
|
||||
const setCurrentUser = phone => {
|
||||
const store = getStore()
|
||||
const user = store.people.find(person => person.phone === phone)
|
||||
store.currentUserPhone = user ? phone : ''
|
||||
saveStore(store)
|
||||
return user || null
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
clone,
|
||||
getCurrentUser,
|
||||
getStore,
|
||||
resetStore,
|
||||
saveStore,
|
||||
setCurrentUser,
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
const formatNumber = n => {
|
||||
n = n.toString()
|
||||
return n[1] ? n : `0${n}`
|
||||
}
|
||||
|
||||
const formatDate = date => {
|
||||
const d = date instanceof Date ? date : new Date(date)
|
||||
const year = d.getFullYear()
|
||||
const month = d.getMonth() + 1
|
||||
const day = d.getDate()
|
||||
|
||||
return `${[year, month, day].map(formatNumber).join('-')}`
|
||||
}
|
||||
|
||||
const formatTime = date => {
|
||||
const d = date instanceof Date ? date : new Date(date)
|
||||
if (Number.isNaN(d.getTime())) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const year = d.getFullYear()
|
||||
const month = d.getMonth() + 1
|
||||
const day = d.getDate()
|
||||
const hour = d.getHours()
|
||||
const minute = d.getMinutes()
|
||||
const second = d.getSeconds()
|
||||
|
||||
return `${[year, month, day].map(formatNumber).join('-')} ${[hour, minute, second].map(formatNumber).join(':')}`
|
||||
}
|
||||
|
||||
const today = () => formatDate(new Date())
|
||||
|
||||
const parseDate = value => {
|
||||
if (value instanceof Date) {
|
||||
return new Date(value.getFullYear(), value.getMonth(), value.getDate())
|
||||
}
|
||||
const text = String(value || '').trim()
|
||||
const matched = text.match(/^(\d{4})-(\d{2})-(\d{2})$/)
|
||||
if (matched) {
|
||||
return new Date(Number(matched[1]), Number(matched[2]) - 1, Number(matched[3]))
|
||||
}
|
||||
return new Date()
|
||||
}
|
||||
|
||||
const addDays = (value, days) => {
|
||||
const date = parseDate(value)
|
||||
date.setDate(date.getDate() + toNumber(days))
|
||||
return formatDate(date)
|
||||
}
|
||||
|
||||
const uid = prefix => `${prefix}_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`
|
||||
|
||||
const toNumber = value => {
|
||||
const number = Number(value)
|
||||
return Number.isFinite(number) ? number : 0
|
||||
}
|
||||
|
||||
const minutesBetween = (start, end) => {
|
||||
const startTime = new Date(start).getTime()
|
||||
const endTime = new Date(end).getTime()
|
||||
if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || endTime <= startTime) {
|
||||
return 0
|
||||
}
|
||||
return Math.round((endTime - startTime) / 60000)
|
||||
}
|
||||
|
||||
const round = (value, digits = 1) => {
|
||||
const base = Math.pow(10, digits)
|
||||
return Math.round(toNumber(value) * base) / base
|
||||
}
|
||||
|
||||
const percent = (value, digits = 0) => `${round(value, digits)}%`
|
||||
|
||||
const paginate = (rows, page = 1, pageSize = 10) => {
|
||||
const safePage = Math.max(1, toNumber(page) || 1)
|
||||
const safePageSize = Math.max(1, toNumber(pageSize) || 10)
|
||||
const total = rows.length
|
||||
const totalPages = Math.max(1, Math.ceil(total / safePageSize))
|
||||
const current = Math.min(safePage, totalPages)
|
||||
const start = (current - 1) * safePageSize
|
||||
|
||||
return {
|
||||
rows: rows.slice(start, start + safePageSize),
|
||||
page: current,
|
||||
pageSize: safePageSize,
|
||||
total,
|
||||
totalPages,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addDays,
|
||||
formatDate,
|
||||
formatTime,
|
||||
formatNumber,
|
||||
minutesBetween,
|
||||
paginate,
|
||||
percent,
|
||||
round,
|
||||
today,
|
||||
toNumber,
|
||||
uid,
|
||||
}
|
||||
Reference in New Issue
Block a user