chore: initialize mini program frontend

This commit is contained in:
souplearn
2026-06-24 15:19:14 +08:00
commit fa73a3c184
96 changed files with 20561 additions and 0 deletions
+1894
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -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',
}
+183
View File
@@ -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,
}
+134
View File
@@ -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,
}
+156
View File
@@ -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,
}
+53
View File
@@ -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
View File
@@ -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,
}